fc7e6a34a7
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>
58 lines
2.5 KiB
TypeScript
58 lines
2.5 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { pw_kanban_login } from "../../../../frontend/functions/browser/pw_kanban_login";
|
|
|
|
const USER = process.env.KANBAN_USER || "e2e_user";
|
|
const PWD = process.env.KANBAN_PWD || "e2e_test_pw_2026";
|
|
|
|
/**
|
|
* Issue 0093: reporte diario al pulsar numero del dia en el calendario.
|
|
* Verifica: endpoint responde, calendario abre modal con titulo "Reporte diario",
|
|
* KPIs visibles, tabla de hechas presente.
|
|
*/
|
|
test.describe("daily report (issue 0093)", () => {
|
|
test("endpoint /api/reports/daily devuelve estructura esperada", async ({ page }) => {
|
|
await page.goto("/");
|
|
await pw_kanban_login(page, { username: USER, password: PWD });
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const res = await page.request.get(`/api/reports/daily?date=${today}`);
|
|
expect(res.status()).toBe(200);
|
|
const data = await res.json();
|
|
expect(data).toHaveProperty("kpis");
|
|
expect(data).toHaveProperty("done_cards");
|
|
expect(data).toHaveProperty("hourly_moves");
|
|
expect(Array.isArray(data.hourly_moves)).toBe(true);
|
|
expect(data.hourly_moves.length).toBe(24);
|
|
expect(data).toHaveProperty("stale_cards");
|
|
expect(data.stale_cards).toHaveProperty("d7");
|
|
expect(data.stale_cards).toHaveProperty("d14");
|
|
expect(data.stale_cards).toHaveProperty("d30");
|
|
});
|
|
|
|
test("click en numero del dia del calendario abre modal del reporte", async ({ page }) => {
|
|
await page.goto("/");
|
|
await pw_kanban_login(page, { username: USER, password: PWD });
|
|
|
|
// Switch to Calendario tab.
|
|
await page.getByRole("tab", { name: /Calendario/i }).click();
|
|
|
|
// Wait until the calendar cells render.
|
|
await page.waitForSelector('[data-test^="calendar-day-"]', { timeout: 5000 });
|
|
|
|
// Use yesterday — the seeded DB has activity there.
|
|
const yesterday = new Date(Date.now() - 24 * 3600 * 1000).toISOString().slice(0, 10);
|
|
const cellBtn = page.locator(`[data-test="calendar-day-${yesterday}"]`);
|
|
if ((await cellBtn.count()) === 0) {
|
|
// Fallback: click any visible day.
|
|
await page.locator('[data-test^="calendar-day-"]').first().dispatchEvent("click");
|
|
} else {
|
|
await cellBtn.dispatchEvent("click");
|
|
}
|
|
|
|
// Modal opens.
|
|
const modal = page.locator('[role="dialog"]').filter({ hasText: /Reporte diario/i });
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
await expect(modal.getByText("Hechas", { exact: false }).first()).toBeVisible();
|
|
await expect(modal.getByText("Movimientos", { exact: false }).first()).toBeVisible();
|
|
});
|
|
});
|