Files
kanban/frontend/e2e/daily-report.spec.ts
egutierrez fc7e6a34a7 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>
2026-05-14 17:57:14 +02:00

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();
});
});