feat(kanban): reporte diario al click en dia del calendario (issue 0093)

Adds a daily report dashboard accessible by clicking a day number in the
calendar view. Renders inside a full-width modal (90% width).

Backend (new file backend/reports.go):
- Type DailyReport with KPIs, rankings, done_cards list, reopened cards,
  3-bucket stale list (7/14/30d), lead time avg+p50+p95, 24-hour
  movement histogram, deadlines met/missed list, tag distribution and
  archived count.
- DB.DailyReportFor(date, tz) uses Europe/Madrid by default; computes
  [start,end) in local time, converts to UTC and queries:
  * cards.completed_at in range  -> done list
  * card_events kind=created in range -> created counts
  * card_column_history.entered_at in range -> moves + hourly
  * previousColumnWasDone() -> reopened detection
  * card_lock_history overlapping the day -> blocked_ms
  * stale buckets: open history entries on non-done columns aged >=7d
- New route GET /api/reports/daily?date=YYYY-MM-DD&tz=Europe/Madrid.

Frontend:
- api.ts: DailyReport type + dailyReport(date, tz?) call.
- New component DailyReportView (components/DailyReport.tsx):
  * 6 KPI cards (Hechas, Creadas, Movimientos, Bloqueado, Reabiertas,
    Deadlines on-time %).
  * 4 ranking cards (Top assignees done, Top assignees created,
    Top requesters atendidas, Top requesters aportadas).
  * Done cards table with click-to-jump (links open the card in board).
  * Mantine BarChart with movements per hour.
  * Tag chips, reopened list, deadlines list with late_ms, stale buckets.
- CalendarView wraps the day number in UnstyledButton with data-test
  attribute and forwards onOpenDailyReport.
- App.handleOpenDailyReport opens modals.open size 90% with the view;
  click on a card title closes the modal and jumps to the board with
  highlight (reuses existing handleJumpToCard).

Tests (e2e/daily-report.spec.ts):
- Endpoint shape: kpis, done_cards, hourly_moves[24], stale buckets.
- Calendar day click opens the modal with "Reporte diario" title and
  KPI labels visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 17:43:29 +02:00
parent 9d3ab5f0f3
commit fc7e6a34a7
10 changed files with 2497 additions and 1187 deletions
+84
View File
@@ -119,6 +119,90 @@ export function unarchiveCard(id: string): Promise<void> {
return fetchJSON(`/cards/${id}/unarchive`, { method: "POST" });
}
export interface DailyReport {
date: string;
tz: string;
start_ts: string;
end_ts: string;
kpis: {
done: number;
created: number;
moves: number;
blocked_ms: number;
deadlines_met: number;
deadlines_missed: number;
reopened: number;
archived_auto: number;
archived_manual: number;
};
top_assignees_done: { user_id: string; name: string; count: number }[];
top_assignees_created: { user_id: string; name: string; count: number }[];
top_requesters_added: { name: string; count: number }[];
top_requesters_done: { name: string; count: number }[];
done_cards: {
id: string;
seq_num: number;
title: string;
requester: string;
assignee_id: string | null;
assignee_name: string | null;
tags: string[];
column_id: string;
column_name: string;
completed_at: string;
created_at: string;
lead_time_ms: number;
color: string;
}[];
reopened_cards: {
card_id: string;
title: string;
seq_num: number;
from_column: string;
to_column: string;
ts: string;
actor_id: string | null;
actor_name: string | null;
}[];
stale_cards: {
d7: StaleEntry[];
d14: StaleEntry[];
d30: StaleEntry[];
};
lead_time: { avg_ms: number; p50_ms: number; p95_ms: number; samples: number };
hourly_moves: number[];
deadlines: {
met: number;
missed: number;
list: {
card_id: string;
title: string;
seq_num: number;
deadline: string;
completed_at: string;
late_ms: number;
}[];
};
tags_done: { name: string; count: number }[];
archived_today: number;
}
export interface StaleEntry {
card_id: string;
title: string;
seq_num: number;
column_id: string;
column_name: string;
entered_at: string;
days: number;
}
export function dailyReport(date: string, tz?: string): Promise<DailyReport> {
const params = new URLSearchParams({ date });
if (tz) params.set("tz", tz);
return fetchJSON(`/reports/daily?${params.toString()}`);
}
export function moveCard(id: string, column_id: string, ordered_ids: string[]): Promise<void> {
return fetchJSON(`/cards/${id}/move`, {
method: "POST",