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,24 +1,61 @@
|
|||
import { Check } from 'lucide-react'
|
||||
import type { Priority, Status } from '../types'
|
||||
import { PRIORITY_LABELS, STATUS_LABELS } from '../types'
|
||||
import { useState } from 'react'
|
||||
import { Check, ChevronDown, Tag } from 'lucide-react'
|
||||
import type { Priority, Category } from '../types'
|
||||
import { PRIORITY_LABELS } from '../types'
|
||||
import { useCategories } from '../context/CategoriesContext'
|
||||
|
||||
export function PriorityBadge({ priority }: { priority: Priority }) {
|
||||
if (priority === 'medium') return null
|
||||
return <span className={`badge badge-prio-${priority}`}>{PRIORITY_LABELS[priority]}</span>
|
||||
}
|
||||
|
||||
export function StatusBadge({ status }: { status: Status }) {
|
||||
return <span className={`badge badge-st-${status}`}>{STATUS_LABELS[status]}</span>
|
||||
// Round completion checkbox. Driven by the status's is_done flag rather than
|
||||
// a fixed literal, since the pipeline is admin-editable.
|
||||
export function Check_({ isDone, onClick }: { isDone: boolean; onClick: (e: React.MouseEvent) => void }) {
|
||||
return (
|
||||
<button className={isDone ? 'check checked' : 'check'} onClick={onClick} aria-label="toggle done" type="button">
|
||||
{isDone && <Check size={13} strokeWidth={3} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Round completion checkbox. 'doing' shows a partial ring, 'done' a filled tick.
|
||||
export function Check_({ status, onClick }: { status: Status; onClick: (e: React.MouseEvent) => void }) {
|
||||
const cls = status === 'done' ? 'check checked' : status === 'doing' ? 'check doing' : 'check'
|
||||
// Collapsible multi-select category filter chip-bar — used on Board/List views.
|
||||
// `selected` empty means "no filter" (show everything).
|
||||
export function CategoryFilter({ selected, onChange }: { selected: number[]; onChange: (ids: number[]) => void }) {
|
||||
const { categories } = useCategories()
|
||||
const [open, setOpen] = useState(false)
|
||||
if (categories.length === 0) return null
|
||||
|
||||
function toggle(id: number) {
|
||||
onChange(selected.includes(id) ? selected.filter(i => i !== id) : [...selected, id])
|
||||
}
|
||||
|
||||
const summary = selected.length === 0
|
||||
? 'All categories'
|
||||
: categories.filter(c => selected.includes(c.id)).map(c => c.name).join(', ')
|
||||
|
||||
return (
|
||||
<button className={cls} onClick={onClick} aria-label="toggle done" type="button">
|
||||
{status === 'done' && <Check size={13} strokeWidth={3} />}
|
||||
{status === 'doing' && <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'currentColor' }} />}
|
||||
</button>
|
||||
<div className="filter-section">
|
||||
<div className="filter-section-header" onClick={() => setOpen(o => !o)}>
|
||||
<Tag size={13} strokeWidth={1.75} />
|
||||
<span className="filter-section-label">Category</span>
|
||||
<span className={`filter-section-summary${selected.length === 0 ? ' muted' : ''}`}>{summary}</span>
|
||||
<ChevronDown size={14} strokeWidth={1.75} className={`filter-section-chevron${open ? ' open' : ''}`} />
|
||||
</div>
|
||||
{open && (
|
||||
<div className="chip-bar" style={{ marginTop: 6 }}>
|
||||
{selected.length > 0 && (
|
||||
<button className="chip" onClick={() => onChange([])}>Clear</button>
|
||||
)}
|
||||
{categories.map((c: Category) => (
|
||||
<button key={c.id} className={`chip${selected.includes(c.id) ? ' active' : ''}`} onClick={() => toggle(c.id)}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: c.colour, display: 'inline-block' }} />
|
||||
{c.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue