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
+11 -18
View File
@@ -8,27 +8,14 @@ import type {
Sticker,
User,
} from "./types";
import { fetchJSON as registryFetchJSON, HTTPError } from "@fn_library/infra/fetch_json";
export { HTTPError };
const BASE = "/api";
export class HTTPError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
async function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
credentials: "include",
...init,
headers: { "Content-Type": "application/json", ...(init?.headers || {}) },
});
if (!res.ok) {
const err = await res.json().catch(() => ({ Message: res.statusText }));
throw new HTTPError(res.status, err.Message || err.message || res.statusText);
}
if (res.status === 204) return undefined as T;
return res.json();
function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> {
return registryFetchJSON<T>(path, init, BASE);
}
export function getBoard(): Promise<Board> {
@@ -84,6 +71,7 @@ export interface UpdateCardInput {
locked?: boolean;
assignee_id?: string | null;
tags?: string[];
deadline?: string | null;
}
export function updateCard(id: string, patch: UpdateCardInput): Promise<void> {
@@ -168,6 +156,10 @@ export function getMe(): Promise<User> {
return fetchJSON("/me");
}
export function updateMe(patch: { color?: string }): Promise<User> {
return fetchJSON("/me", { method: "PATCH", body: JSON.stringify(patch) });
}
export function listUsers(): Promise<User[]> {
return fetchJSON("/users");
}
@@ -186,6 +178,7 @@ export function getMetrics(f: MetricsFilter): Promise<Metrics> {
if (f.to) qs.set("to", f.to);
if (f.assignee_id) qs.set("assignee_id", f.assignee_id);
if (f.requester) qs.set("requester", f.requester);
if (f.tags && f.tags.length > 0) qs.set("tags", f.tags.join(","));
const q = qs.toString();
return fetchJSON(`/metrics${q ? `?${q}` : ""}`);
}