feat(kanban): deadlines en cards (context menu, badges, calendario, history)

- migration 009 + columna deadline TEXT en cards
- backend: CardPatch.HasDeadline, eventos deadline_set/deadline_cleared
- KanbanCard: menu derecho con DatePicker, badge countdown con colores por ratio (azul>=50%, amarillo<50%, rojo<10%, red.9 overdue)
- App.tsx: filtro "Con deadline", handleSetCardDeadline optimista, jump-to-card + highlight
- CalendarView: popover por dia con seq_num + titulo, click navega a card en tablero
- HistoryModal: render eventos deadline_set/deadline_cleared
- .gitignore: *.log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 03:45:36 +02:00
parent 5ba0254e57
commit 7ce227ddea
54 changed files with 3066 additions and 496 deletions
+20 -1
View File
@@ -17,6 +17,7 @@ import {
Grid,
Group,
Loader,
MultiSelect,
Paper,
Select,
SimpleGrid,
@@ -89,10 +90,16 @@ export function Dashboard({ users }: Props) {
const [to, setTo] = useState<Date | null>(() => new Date());
const [assigneeId, setAssigneeId] = useState<string | null>(null);
const [requester, setRequester] = useState<string | null>(null);
const [tags, setTags] = useState<string[]>([]);
const [tagOptions, setTagOptions] = useState<string[]>([]);
const [data, setData] = useState<Metrics | null>(null);
const [loading, setLoading] = useState(false);
const [requesterOptions, setRequesterOptions] = useState<string[]>([]);
useEffect(() => {
api.listTags().then(setTagOptions).catch(() => {});
}, []);
useEffect(() => {
let cancelled = false;
setLoading(true);
@@ -102,6 +109,7 @@ export function Dashboard({ users }: Props) {
to: fmtDate(to),
assignee_id: assigneeId || undefined,
requester: requester || undefined,
tags: tags.length > 0 ? tags : undefined,
})
.then((m) => {
if (cancelled) return;
@@ -119,7 +127,7 @@ export function Dashboard({ users }: Props) {
return () => {
cancelled = true;
};
}, [from, to, assigneeId, requester]);
}, [from, to, assigneeId, requester, tags]);
const userOptions = useMemo(
() => users.map((u) => ({ value: u.id, label: u.display_name || u.username })),
@@ -240,6 +248,17 @@ export function Dashboard({ users }: Props) {
searchable
style={{ minWidth: 160 }}
/>
<MultiSelect
label="Tags"
size="xs"
placeholder="Todas"
value={tags}
onChange={setTags}
data={tagOptions}
clearable
searchable
style={{ minWidth: 200 }}
/>
</Group>
</Group>