import { useState } from 'react' import { Check, ChevronDown, Tag, FolderKanban, ListChecks, type LucideIcon } from 'lucide-react' import type { Priority } from '../types' import { PRIORITY_LABELS } from '../types' import { useCategories } from '../context/CategoriesContext' import { useProjects } from '../context/ProjectsContext' import { useLists } from '../context/ListsContext' import { useTaskFilters } from '../context/TaskFiltersContext' export function PriorityBadge({ priority }: { priority: Priority }) { if (priority === 'medium') return null return {PRIORITY_LABELS[priority]} } // 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 ( ) } interface FilterableEntity { id: number; name: string; colour: string } // Collapsible multi-select filter chip-bar — generic over anything with an // id/name/colour (categories, projects, ...). `selected` empty means "no // filter" (show everything). function FilterChips({ items, selected, onChange, label, icon: Icon }: { items: T[]; selected: number[]; onChange: (ids: number[]) => void label: string; icon: LucideIcon }) { const [open, setOpen] = useState(false) if (items.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 ${label.toLowerCase()}s` : items.filter(x => selected.includes(x.id)).map(x => x.name).join(', ') return (
setOpen(o => !o)}> {label} {summary}
{open && (
{selected.length > 0 && ( )} {items.map(x => ( ))}
)}
) } export function CategoryFilter({ selected, onChange }: { selected: number[]; onChange: (ids: number[]) => void }) { const { categories } = useCategories() return } export function ProjectFilter({ selected, onChange }: { selected: number[]; onChange: (ids: number[]) => void }) { const { projects } = useProjects() return } export function ListFilter({ selected, onChange }: { selected: number[]; onChange: (ids: number[]) => void }) { const { lists } = useLists() return } // One drop-in filter row for category + project + (optionally) list, reading // straight from the global TaskFiltersContext — the "easy way to filter any // view" version. Pass showList={false} for views already scoped to one list // (e.g. ListView itself), where a list chip would be redundant/confusing. export function TaskFilterBar({ showList = true }: { showList?: boolean }) { const { categoryIds, setCategoryIds, projectIds, setProjectIds, listIds, setListIds } = useTaskFilters() return (
{showList && (
)}
) } export function formatDate(iso: string | null | undefined): string { if (!iso) return '—' return new Date(iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) } export function formatDateTime(iso: string | null | undefined): string { if (!iso) return '—' const d = new Date(iso) return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) + ' ' + d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) } function startOfDay(d: Date) { const x = new Date(d); x.setHours(0, 0, 0, 0); return x } // Human relative due label + whether it's overdue. export function dueInfo(iso: string | null): { label: string; overdue: boolean } | null { if (!iso) return null const due = new Date(iso) const today = startOfDay(new Date()) const dueDay = startOfDay(due) const days = Math.round((dueDay.getTime() - today.getTime()) / 86400000) const overdue = days < 0 let label: string if (days === 0) label = 'Today' else if (days === 1) label = 'Tomorrow' else if (days === -1) label = 'Yesterday' else if (days < 0) label = `${-days} days ago` else if (days < 7) label = due.toLocaleDateString('en-GB', { weekday: 'short' }) else label = due.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) const hasTime = due.getHours() !== 0 || due.getMinutes() !== 0 if (hasTime) label += ' ' + due.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) return { label, overdue } } // Convert an ISO timestamp to the value a datetime-local input expects. export function toLocalInput(iso: string | null): string { if (!iso) return '' const d = new Date(iso) const pad = (n: number) => String(n).padStart(2, '0') return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` }