Frontend: dynamic status pipeline, categories/projects, tag+assign+reallocate UI
- Board/List/Today views now render the admin-editable status pipeline instead of a hardcoded todo/doing/done enum (StatusesContext) - Category filter chip-bar on Board and List views (ported the idea from reviewing the FD's own build, simplified to match our flat single-category model rather than his tree) - New Projects list + detail views, project create/edit modal - New Dispatch view for the 'reallocate' cap -- flat cross-user task list with an inline move action (deliberately not routed through the shared task drawer, since GET /api/tasks/:id only grants visibility to owner/assignee/tagged users, not dispatchers) - Task drawer: dynamic status pills, category/project selects, tag picker (share/watch, no cap needed), assign field (owner + assign cap), reallocate field (reallocate cap) -- all fields disabled for non-owners since the backend's single PATCH endpoint is owner-gated - Settings page: status pipeline + category management (settings cap) - Fixed missing input[type="datetime-local"]/[type="time"] styling -- the Due/Remind fields were rendering with unstyled browser defaults
This commit is contained in:
parent
d8dc4e2b81
commit
017f6dfdc1
19 changed files with 1008 additions and 71 deletions
|
|
@ -1,16 +1,18 @@
|
|||
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 { Columns3, PauseCircle } from 'lucide-react'
|
||||
import type { Task } from '../types'
|
||||
import { fetchTasks, updateTask } from '../api'
|
||||
import { useTaskDrawer } from '../components/TaskDrawer'
|
||||
import { PriorityBadge, dueInfo } from '../components/shared'
|
||||
import { useStatuses } from '../context/StatusesContext'
|
||||
import { PriorityBadge, dueInfo, CategoryFilter } from '../components/shared'
|
||||
|
||||
export default function BoardView() {
|
||||
const { openTask, changeVersion } = useTaskDrawer()
|
||||
const { statuses } = useStatuses()
|
||||
const [tasks, setTasks] = useState<Task[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [over, setOver] = useState<Status | null>(null)
|
||||
const [over, setOver] = useState<string | null>(null)
|
||||
const [categoryFilter, setCategoryFilter] = useState<number[]>([])
|
||||
const dragId = useRef<number | null>(null)
|
||||
|
||||
const load = useCallback(() => {
|
||||
|
|
@ -18,7 +20,7 @@ export default function BoardView() {
|
|||
}, [])
|
||||
useEffect(() => { load() }, [load, changeVersion])
|
||||
|
||||
async function drop(status: Status) {
|
||||
async function drop(status: string) {
|
||||
const id = dragId.current
|
||||
dragId.current = null
|
||||
setOver(null)
|
||||
|
|
@ -30,24 +32,30 @@ export default function BoardView() {
|
|||
load()
|
||||
}
|
||||
|
||||
const visibleTasks = categoryFilter.length === 0
|
||||
? tasks
|
||||
: tasks.filter(t => t.category_id !== null && categoryFilter.includes(t.category_id))
|
||||
|
||||
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>
|
||||
<CategoryFilter selected={categoryFilter} onChange={setCategoryFilter} />
|
||||
{error && <div className="error-banner">{error}</div>}
|
||||
|
||||
<div className="board">
|
||||
{STATUSES.map(status => {
|
||||
const items = tasks.filter(t => t.status === status)
|
||||
{statuses.map(status => {
|
||||
const items = visibleTasks.filter(t => t.status === status.value)
|
||||
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 key={status.value}
|
||||
className={`board-col${over === status.value ? ' drop-target' : ''}${status.is_hold ? ' board-col-hold' : ''}`}
|
||||
onDragOver={e => { e.preventDefault(); setOver(status.value) }}
|
||||
onDragLeave={() => setOver(o => o === status.value ? null : o)}
|
||||
onDrop={() => drop(status.value)}>
|
||||
<div className="board-col-head">
|
||||
{STATUS_LABELS[status]}
|
||||
{status.is_hold && <PauseCircle size={13} strokeWidth={1.75} />}
|
||||
{status.label}
|
||||
<span className="board-col-count">{items.length}</span>
|
||||
</div>
|
||||
{items.map(t => (
|
||||
|
|
@ -63,6 +71,12 @@ export default function BoardView() {
|
|||
{t.list_name}
|
||||
</span>
|
||||
)}
|
||||
{t.category_name && (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<span className="list-dot" style={{ background: t.category_colour, width: 8, height: 8 }} />
|
||||
{t.category_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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue