diff --git a/frontend/src/pages/DispatchView.tsx b/frontend/src/pages/DispatchView.tsx
index 6ec220c..a2c37c9 100644
--- a/frontend/src/pages/DispatchView.tsx
+++ b/frontend/src/pages/DispatchView.tsx
@@ -3,6 +3,7 @@ import { Send } from 'lucide-react'
import type { Task } from '../types'
import { fetchTasks, reallocateTask } from '../api'
import { useAuth } from '../components/AuthGate'
+import { useSearch } from '../context/SearchContext'
import { can } from '../types'
import { dueInfo, PriorityBadge } from '../components/shared'
@@ -12,14 +13,15 @@ import { dueInfo, PriorityBadge } from '../components/shared'
// only needs enough per-row info to decide where a task should go.
export default function DispatchView() {
const { user } = useAuth()
+ const { debouncedQuery } = useSearch()
const [tasks, setTasks] = useState
([])
const [error, setError] = useState(null)
const [movingId, setMovingId] = useState(null)
const [target, setTarget] = useState('')
const load = useCallback(() => {
- fetchTasks({ view: 'all' }).then(setTasks).catch(e => setError(e.message))
- }, [])
+ fetchTasks({ view: 'all', search: debouncedQuery || undefined }).then(setTasks).catch(e => setError(e.message))
+ }, [debouncedQuery])
useEffect(() => { load() }, [load])
if (!can(user, 'reallocate')) {
diff --git a/frontend/src/pages/FilteredListView.tsx b/frontend/src/pages/FilteredListView.tsx
new file mode 100644
index 0000000..f04ea48
--- /dev/null
+++ b/frontend/src/pages/FilteredListView.tsx
@@ -0,0 +1,70 @@
+import { useEffect, useState, useCallback } from 'react'
+import { Share2, UserCheck } from 'lucide-react'
+import type { Task } from '../types'
+import { fetchTasks, updateTask } from '../api'
+import { useTaskDrawer } from '../components/TaskDrawer'
+import { useStatuses } from '../context/StatusesContext'
+import { useSearch } from '../context/SearchContext'
+import TaskRow from '../components/TaskRow'
+import { CategoryFilter, ProjectFilter } from '../components/shared'
+
+const ICONS = { shared: Share2, assigned: UserCheck }
+
+export default function FilteredListView({ view, title }: { view: 'shared' | 'assigned'; title: string }) {
+ const { openTask, changeVersion } = useTaskDrawer()
+ const { firstDoneValue, firstNotDoneValue } = useStatuses()
+ const { debouncedQuery } = useSearch()
+ const [tasks, setTasks] = useState([])
+ const [error, setError] = useState(null)
+ const [categoryFilter, setCategoryFilter] = useState([])
+ const [projectFilter, setProjectFilter] = useState([])
+ const Icon = ICONS[view]
+
+ const load = useCallback(() => {
+ fetchTasks({ view, search: debouncedQuery || undefined }).then(setTasks).catch(e => setError(e.message))
+ }, [view, debouncedQuery])
+ useEffect(() => { load() }, [load, changeVersion])
+
+ async function toggle(t: Task) {
+ const next = t.is_done ? firstNotDoneValue() : firstDoneValue()
+ if (!next) return
+ await updateTask(t.id, { status: next }).catch(() => {})
+ load()
+ }
+
+ const visible = tasks
+ .filter(t => categoryFilter.length === 0 || (t.category_id !== null && categoryFilter.includes(t.category_id)))
+ .filter(t => projectFilter.length === 0 || (t.project_id !== null && projectFilter.includes(t.project_id)))
+ const open = visible.filter(t => !t.is_done)
+ const done = visible.filter(t => t.is_done)
+
+ return (
+
+
+
{title}
+
+
+
+ {error &&
{error}
}
+
+ {open.length === 0 && done.length === 0 && (
+
+ {view === 'shared' ? "Nobody's tagged you on anything." : "Nothing's been handed to you."}
+
+ )}
+
+ {open.map(t => (
+
openTask(t.id)} />
+ ))}
+
+ {done.length > 0 && (
+ <>
+ Completed ({done.length})
+ {done.map(t => (
+ openTask(t.id)} />
+ ))}
+ >
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/ListView.tsx b/frontend/src/pages/ListView.tsx
index 042d41a..0b90680 100644
--- a/frontend/src/pages/ListView.tsx
+++ b/frontend/src/pages/ListView.tsx
@@ -5,16 +5,18 @@ import type { Task, List } from '../types'
import { fetchTasks, createTask, updateTask, reorderTasks } from '../api'
import { useLists } from '../context/ListsContext'
import { useStatuses } from '../context/StatusesContext'
+import { useSearch } from '../context/SearchContext'
import { useTaskDrawer } from '../components/TaskDrawer'
import TaskRow from '../components/TaskRow'
import ListEditor from '../components/ListEditor'
-import { CategoryFilter } from '../components/shared'
+import { CategoryFilter, ProjectFilter } from '../components/shared'
export default function ListView() {
const { id } = useParams()
const listId = Number(id)
const { lists } = useLists()
const { firstDoneValue, firstNotDoneValue } = useStatuses()
+ const { debouncedQuery } = useSearch()
const { openTask, changeVersion } = useTaskDrawer()
const list = lists.find(l => l.id === listId)
@@ -23,12 +25,13 @@ export default function ListView() {
const [newTitle, setNewTitle] = useState('')
const [editing, setEditing] = useState(false)
const [categoryFilter, setCategoryFilter] = useState([])
+ const [projectFilter, setProjectFilter] = useState([])
const dragId = useRef(null)
const load = useCallback(() => {
if (!listId) return
- fetchTasks({ list_id: listId }).then(setTasks).catch(e => setError(e.message))
- }, [listId])
+ fetchTasks({ list_id: listId, search: debouncedQuery || undefined }).then(setTasks).catch(e => setError(e.message))
+ }, [listId, debouncedQuery])
useEffect(() => { load() }, [load, changeVersion])
async function add() {
@@ -59,9 +62,9 @@ export default function ListView() {
reorderTasks(ids).catch(() => load())
}
- const visible = categoryFilter.length === 0
- ? tasks
- : tasks.filter(t => t.category_id !== null && categoryFilter.includes(t.category_id))
+ const visible = tasks
+ .filter(t => categoryFilter.length === 0 || (t.category_id !== null && categoryFilter.includes(t.category_id)))
+ .filter(t => projectFilter.length === 0 || (t.project_id !== null && projectFilter.includes(t.project_id)))
const open = visible.filter(t => !t.is_done)
const done = visible.filter(t => t.is_done)
@@ -79,6 +82,7 @@ export default function ListView() {
)}
diff --git a/frontend/src/pages/TodayView.tsx b/frontend/src/pages/TodayView.tsx
index 41afa29..166f087 100644
--- a/frontend/src/pages/TodayView.tsx
+++ b/frontend/src/pages/TodayView.tsx
@@ -4,19 +4,24 @@ import type { Task } from '../types'
import { fetchTasks, updateTask } from '../api'
import { useTaskDrawer } from '../components/TaskDrawer'
import { useStatuses } from '../context/StatusesContext'
+import { useSearch } from '../context/SearchContext'
import TaskRow from '../components/TaskRow'
+import { CategoryFilter, ProjectFilter } from '../components/shared'
function startOfDay(d: Date) { const x = new Date(d); x.setHours(0, 0, 0, 0); return x }
export default function TodayView() {
const { openTask, changeVersion } = useTaskDrawer()
const { firstDoneValue, firstNotDoneValue } = useStatuses()
+ const { debouncedQuery } = useSearch()
const [tasks, setTasks] = useState
([])
const [error, setError] = useState(null)
+ const [categoryFilter, setCategoryFilter] = useState([])
+ const [projectFilter, setProjectFilter] = useState([])
const load = useCallback(() => {
- fetchTasks({ view: 'today' }).then(setTasks).catch(e => setError(e.message))
- }, [])
+ fetchTasks({ view: 'today', search: debouncedQuery || undefined }).then(setTasks).catch(e => setError(e.message))
+ }, [debouncedQuery])
useEffect(() => { load() }, [load, changeVersion])
async function toggle(t: Task) {
@@ -26,13 +31,17 @@ export default function TodayView() {
load()
}
+ const visible = tasks
+ .filter(t => categoryFilter.length === 0 || (t.category_id !== null && categoryFilter.includes(t.category_id)))
+ .filter(t => projectFilter.length === 0 || (t.project_id !== null && projectFilter.includes(t.project_id)))
+
const now = new Date()
const todayStart = startOfDay(now)
const tomorrowStart = new Date(todayStart); tomorrowStart.setDate(tomorrowStart.getDate() + 1)
- const overdue = tasks.filter(t => t.due_at && new Date(t.due_at) < todayStart)
- const today = tasks.filter(t => t.due_at && new Date(t.due_at) >= todayStart && new Date(t.due_at) < tomorrowStart)
- const upcoming = tasks.filter(t => t.due_at && new Date(t.due_at) >= tomorrowStart)
+ const overdue = visible.filter(t => t.due_at && new Date(t.due_at) < todayStart)
+ const today = visible.filter(t => t.due_at && new Date(t.due_at) >= todayStart && new Date(t.due_at) < tomorrowStart)
+ const upcoming = visible.filter(t => t.due_at && new Date(t.due_at) >= tomorrowStart)
const groups: Array<{ key: string; title: string; items: Task[]; overdue?: boolean }> = [
{ key: 'overdue', title: 'Overdue', items: overdue, overdue: true },
@@ -45,9 +54,11 @@ export default function TodayView() {
Today
+
+
{error && {error}
}
- {tasks.length === 0 && (
+ {visible.length === 0 && (
Nothing due in the next 7 days. Enjoy the calm. ☕
)}