Initial commit: personal task planner with categories, projects, and tiered assignment
Personal-first lists/tasks/reminders/recurrence, plus an admin-editable status pipeline, shared categories, project containers, and a three-tier permission model for cross-user work: assign (hand off your own tasks), reallocate (dispatcher — move anyone's tasks), admin (reserved, full manage-all).
This commit is contained in:
commit
d8dc4e2b81
54 changed files with 10908 additions and 0 deletions
78
frontend/src/pages/BoardView.tsx
Normal file
78
frontend/src/pages/BoardView.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { Columns3 } from 'lucide-react'
|
||||
import type { Task, Status } from '../types'
|
||||
import { STATUSES, STATUS_LABELS } from '../types'
|
||||
import { fetchTasks, updateTask } from '../api'
|
||||
import { useTaskDrawer } from '../components/TaskDrawer'
|
||||
import { PriorityBadge, dueInfo } from '../components/shared'
|
||||
|
||||
export default function BoardView() {
|
||||
const { openTask, changeVersion } = useTaskDrawer()
|
||||
const [tasks, setTasks] = useState<Task[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [over, setOver] = useState<Status | null>(null)
|
||||
const dragId = useRef<number | null>(null)
|
||||
|
||||
const load = useCallback(() => {
|
||||
fetchTasks({}).then(setTasks).catch(e => setError(e.message))
|
||||
}, [])
|
||||
useEffect(() => { load() }, [load, changeVersion])
|
||||
|
||||
async function drop(status: Status) {
|
||||
const id = dragId.current
|
||||
dragId.current = null
|
||||
setOver(null)
|
||||
if (id === null) return
|
||||
const task = tasks.find(t => t.id === id)
|
||||
if (!task || task.status === status) return
|
||||
setTasks(ts => ts.map(t => t.id === id ? { ...t, status } : t))
|
||||
await updateTask(id, { status }).catch(() => load())
|
||||
load()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page" style={{ maxWidth: 1200 }}>
|
||||
<div className="page-header">
|
||||
<h1><Columns3 size={18} strokeWidth={1.75} style={{ verticalAlign: -3, marginRight: 6 }} />Board</h1>
|
||||
</div>
|
||||
{error && <div className="error-banner">{error}</div>}
|
||||
|
||||
<div className="board">
|
||||
{STATUSES.map(status => {
|
||||
const items = tasks.filter(t => t.status === status)
|
||||
return (
|
||||
<div key={status}
|
||||
className={`board-col${over === status ? ' drop-target' : ''}`}
|
||||
onDragOver={e => { e.preventDefault(); setOver(status) }}
|
||||
onDragLeave={() => setOver(o => o === status ? null : o)}
|
||||
onDrop={() => drop(status)}>
|
||||
<div className="board-col-head">
|
||||
{STATUS_LABELS[status]}
|
||||
<span className="board-col-count">{items.length}</span>
|
||||
</div>
|
||||
{items.map(t => (
|
||||
<div key={t.id} className="card" style={{ cursor: 'grab', padding: '10px 12px' }}
|
||||
draggable
|
||||
onDragStart={() => { dragId.current = t.id }}
|
||||
onClick={() => openTask(t.id)}>
|
||||
<div style={{ fontSize: 13.5, fontWeight: 500, marginBottom: 4 }}>{t.title}</div>
|
||||
<div className="task-row-meta">
|
||||
{t.list_name && (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<span className="list-dot" style={{ background: t.list_colour, width: 8, height: 8 }} />
|
||||
{t.list_name}
|
||||
</span>
|
||||
)}
|
||||
{(() => { const d = dueInfo(t.due_at); return d && <span className={`due-chip${d.overdue ? ' overdue' : ''}`}>{d.label}</span> })()}
|
||||
<PriorityBadge priority={t.priority} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{items.length === 0 && <div className="muted" style={{ fontSize: 12, padding: '4px 2px' }}>—</div>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue