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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user