# M10 — Dashboards & Reports

Phase 5. Pure read-layer over Phases 1–4's data — no new domain tables besides
lightweight caching/report-run tracking. This is where the system starts to
feel indispensable to partners.

## Dependencies
M03–M09 (reads across all of them).

## Part A — Executive Dashboard

`GET /dashboard/executive`, permission `dashboard.executive`. **Attention panel
first** — this is not a vanity-metrics page.

| Widget | Query source | Cache |
|---|---|---|
| Attention panel | missed appearances w/o cover, hearings past-date w/o outcome, deadlines ≤14 days, overdue invoices, pending fee-claim approvals | 5 min |
| KPI cards | active matters, hearings this week, outstanding receivables, retainer balance held, unbilled time value, pending approvals count | 5 min |
| Matter pipeline | open matters by stage (bar) | 5 min |
| Practice area mix | open matters by practice area (donut) | 5 min |
| Lawyer workload | matters assigned + hearings this month + billable hours per lawyer (table) | 5 min |
| Financial pulse | invoiced vs collected, monthly, last 6 months (line) | 15 min |
| Recent activity | last 20 activity-log entries across domain models | none (live) |

Every widget is its own method on `ExecutiveDashboardController` (or a
`DashboardMetrics` service) returning a plain array — **unit-testable without
touching the view layer**. Cache keys tagged per firm, busted by the relevant
domain events (e.g. invoice paid busts the financial-pulse tag) rather than
waiting out the TTL blindly where correctness matters (attention panel).

## Part B — "My Day" Lawyer Dashboard

`GET /dashboard` (default landing page for non-executive roles): today's
hearings, my open tasks (overdue highlighted), my pending fee claims, my
unbilled time this week, recent matter activity on matters I'm on. Mobile-first
layout — partners and counsel check this from a phone between court sittings.

## Part C — Matter Health / Assessment View

`GET /dashboard/matters` — filterable grid (stage, practice area, lawyer,
court, status, "no activity in N days" toggle) with a per-row health strip:
last event date, next hearing, open tasks, unbilled time, outstanding balance,
retainer left. Backing query: `MatterHealthQuery` service, reused by both this
view and the "stale matters" weekly command.

## Part D — Standard Report Library

Every report: filter form → HTML preview → PDF (queued DomPDF) + Excel
(queued maatwebsite-excel) buttons, consistent shell across all reports.

| Report | Route | Content |
|---|---|---|
| Matter progress | `/reports/matter-progress` | Per matter: stage history, hearings held, outcomes, next dates — single matter detail or portfolio summary |
| Lawyer performance | `/reports/lawyer-performance` | Per lawyer/period: appearances (attended/missed), matters handled, hours logged, fees generated |
| Court activity | `/reports/court-activity` | Per court/period: hearings, outcome distribution, adjournment rate |
| Financial summary | `/reports/financials` | Invoiced, collected, outstanding, expenses, appearance fees, profitability — per matter/client/period |
| Aging | `/reports/aging` | Built in M07; linked here |
| Client report | `/reports/client/{client}` | Sanitized client-facing status summary — powers both PDF export and the client portal (M11) |
| Deadline compliance | `/reports/deadlines` | On-time vs missed deadlines by type/period — governance/insurance-audit ready |

**Client report sanitization rule** (non-negotiable, tested): excludes
`hearings.remarks`, `matter_comments`, internal financial detail beyond the
client's own invoices/balances. Built as `ClientMatterReportService` returning
a DTO consumed identically by the PDF export and the M11 portal — one
implementation, two renderers, so sanitization can't drift between them.

## `report_runs` (audit + async generation tracking)

```php
$table->id();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->string('report_key');
$table->json('parameters');
$table->string('format');    // pdf/excel
$table->string('status')->default('queued'); // queued/processing/ready/failed
$table->string('file_path')->nullable();
$table->foreignId('requested_by')->constrained('users');
$table->timestamps();
```
Large reports generate via a queued job; the UI polls `report_runs.status` and
offers a download link on `ready` — avoids timing out the request cycle for a
heavy Excel export.

## Acceptance criteria

- [ ] Every dashboard widget has an isolated, unit-tested query method
- [ ] Attention panel surfaces exactly the conditions listed (seeded-data test
      per condition)
- [ ] Non-executive roles get 403 on `/dashboard/executive`
- [ ] Matter health grid filters combine correctly; "no activity in N days"
      toggle matches `matter_events` reality
- [ ] All seven reports produce consistent HTML/PDF/Excel output from the same
      underlying data
- [ ] Client report contains zero internal remarks/comments (explicit
      assertion test) and is reused unchanged by the M11 portal
- [ ] Mobile layout of "My Day" and Matter Health verified at 390px width
- [ ] Large report generation via `report_runs` doesn't block the request cycle

## Claude Code kickoff prompt

> Read CLAUDE.md, docs/05-UI-UX.md §5 (dashboard/attention-panel convention),
> and this file. Implement Part A → B → C → D in order. Build every widget/
> report as a plain-array-returning service method FIRST with a unit test,
> then wire the Blade view — do not write dashboard logic directly in
> controllers or views. Build `ClientMatterReportService` as the single shared
> source for both the PDF report and the M11 client portal.
