# Story 2.2: Priority Alerts Panel

Status: review

## Story

As a firm owner or manager,
I want to see a prioritized list of alerts for items requiring immediate attention,
So that I can act on the most urgent issues before they become missed deadlines.

## Acceptance Criteria

1. **Given** the Owner/Manager dashboard is loaded, **When** the priority alerts panel renders (below the KPI cards, above the urgent declarations table), **Then** alerts are displayed as a list sorted by urgency with the following categories:
   - **Critical (red):** Overdue declarations (past deadline) -- shows client name, declaration type, days overdue
   - **Warning (amber):** Approaching deadlines (due within 3 days) -- shows client name, type, days remaining
   - **Info (blue):** Missing client documents (status `en_attente_client` for >3 days) -- shows client name, days waiting

2. **Given** alerts are displayed, **When** the user clicks an alert item, **Then** the browser navigates to the relevant declaration detail page

3. **Given** there are more than 20 alerts total, **Then** the list is capped at 20 most urgent items with a "Voir tout" link navigating to the filtered declarations list

4. **Given** the alerts panel is loaded, **Then** deadline proximity uses the color gradient: green (>7d) → amber (3-7d) → red (<3d) → pulsing red (overdue)

5. **Given** an alert item's declaration has been acted on (status changed or reassigned), **Then** the alert disappears on next dashboard refresh (cache expires in 5 min)

6. **Given** there are no alerts, **Then** an encouraging message is shown: "Aucune alerte -- tout est en ordre"

7. **Given** the dashboard data is requested, **Then** alert data is included in the same `Cache::remember()` cached dashboard payload (no separate API call)

## Tasks / Subtasks

- [x] Task 1: Extend `DashboardController` to include alerts in cached payload (AC: #1, #3, #7)
  - [x] 1.1 Add `buildAlerts()` private method that queries 3 alert categories using `Declaration::forUser()` scope
  - [x] 1.2 Critical alerts: `due_date < now()->startOfDay()` with active status, compute `daysOverdue`
  - [x] 1.3 Warning alerts: `due_date BETWEEN now()->startOfDay() AND now()->addDays(3)->endOfDay()` with active status, compute `daysRemaining`
  - [x] 1.4 Info alerts: `status = en_attente_client` AND status held for >3 days (use `updated_at` or status transition timestamp), compute `daysWaiting`
  - [x] 1.5 Merge all 3 categories, sort by severity (critical→warning→info), then by urgency within category, cap at 20
  - [x] 1.6 Include `alerts` array in the existing cached dashboard data structure (inside `Cache::remember` closure)
  - [x] 1.7 Include `viewAllAlertsUrl` prop pointing to `/declarations?filter=alerts` or appropriate filtered view
- [x] Task 2: Add TypeScript types for alerts (AC: #1)
  - [x] 2.1 Add `DashboardAlert` type in `resources/js/types/dashboard.ts` with fields: id, severity, clientName, declarationType, typeLabel, daysValue, daysLabel, showUrl
  - [x] 2.2 Extend `DashboardProps` to include `alerts: DashboardAlert[]` and `viewAllAlertsUrl: string`
- [x] Task 3: Create `PriorityAlertsPanel.vue` component (AC: #1, #2, #3, #4, #6)
  - [x] 3.1 Build component with props: `alerts: DashboardAlert[]`, `viewAllUrl: string`
  - [x] 3.2 Render alerts sorted by severity with color-coded icons (AlertCircle red, AlertTriangle amber, Info blue from lucide-vue-next)
  - [x] 3.3 Each alert row shows: severity icon + client name + declaration type + days metric + link arrow
  - [x] 3.4 Clicking alert navigates to declaration detail via `router.get(alert.showUrl)`
  - [x] 3.5 Show "Voir tout" link at bottom when alerts.length === 20 (indicates more exist)
  - [x] 3.6 Show empty state "Aucune alerte -- tout est en ordre" with CheckCircle icon when alerts.length === 0
  - [x] 3.7 Apply deadline proximity color classes consistent with existing table colors
- [x] Task 4: Integrate alerts panel into `Dashboard.vue` (AC: #1, #4)
  - [x] 4.1 Import and place `PriorityAlertsPanel` between KPI cards grid and urgent declarations table
  - [x] 4.2 Pass `alerts` and `viewAllAlertsUrl` props from page data
  - [x] 4.3 Add section heading "Alertes prioritaires" with alert count badge
- [x] Task 5: Write Pest feature tests for alerts (AC: #1, #2, #3, #5, #6, #7)
  - [x] 5.1 Test overdue declarations appear as critical alerts with correct daysOverdue
  - [x] 5.2 Test approaching deadline (within 3 days) declarations appear as warning alerts
  - [x] 5.3 Test en_attente_client >3 days declarations appear as info alerts
  - [x] 5.4 Test en_attente_client <=3 days declarations do NOT appear as info alerts
  - [x] 5.5 Test alerts capped at 20 items
  - [x] 5.6 Test alerts sorted by severity (critical first, then warning, then info)
  - [x] 5.7 Test Worker sees only alerts for assigned declarations
  - [x] 5.8 Test empty alerts array when no urgent items exist
  - [x] 5.9 Test alerts included in cached dashboard payload (same cache key)
  - [x] 5.10 Test excluded statuses (termine, mise_en_demeure, ferme) don't generate alerts

## Dev Notes

### Architecture Patterns & Constraints

- **Workspace resolution:** Always from session `current_workspace_id`, NEVER from URL params
- **Authorization:** Use `abort(404)` not `abort(403)` for workspace boundary violations
- **Role-scoped queries:** Use existing `Declaration::forUser($user, $workspaceUser)` scope -- Workers see only assigned, Owners/Managers see all
- **Data shaping:** Manually build arrays in controller, NO API Resources
- **URLs as props:** ALL frontend URLs must come from controller via `route()` helper -- never hardcode routes in Vue
- **Wayfinder routes:** All URLs in Vue MUST use Wayfinder type-safe routes. Check existing Dashboard.vue imports for pattern.
- **Cache pattern:** Alert data goes INSIDE the existing `Cache::remember()` closure -- do NOT create a separate cache key or API endpoint

### CRITICAL: DB Column Name

The architecture docs reference `deadline` but the actual database column is **`due_date`**. Use `due_date` in ALL queries. This was a known issue caught in Story 2.1.

### CRITICAL: Excluded Statuses

The dashboard excludes declarations with statuses: `termine`, `mise_en_demeure`, `ferme`. The `DeclarationStatus` enum has 6 values: `created`, `en_cours`, `en_attente_client`, `termine`, `mise_en_demeure`, `ferme`. Only `created`, `en_cours`, and `en_attente_client` should generate alerts.

### CRITICAL: "Days Waiting" Calculation for Info Alerts

The epics spec says `en_attente_client for >3 days`. The simplest approach: use the `updated_at` timestamp as a proxy for when the status was last changed. If `status = en_attente_client AND updated_at < now()->subDays(3)`, the declaration qualifies. This avoids needing a dedicated status history table.

### Existing Code to Extend (NOT Create Fresh)

**`app/Http/Controllers/DashboardController.php`** -- Current controller (Story 2.1):
- Already has `Cache::remember()` with key `dashboard:{workspace_id}:{user_id}` and 5-min TTL
- Already queries urgent declarations with `forUser()` scope and excludes closed statuses
- Already returns `stats`, `statCards`, `declarations` (15 urgent rows), `workspaceName`, `roleLabel`, `declarationsUrl`, `clientsUrl`
- **ADD:** `alerts` array and `viewAllAlertsUrl` to the Inertia props, computed inside the existing cache closure
- The cache closure currently returns stats + declarations. Extend it to also compute and return alerts.

**`resources/js/pages/Dashboard.vue`** -- Current page (Story 2.1):
- Currently has: KPI card grid → urgent declarations table
- **ADD:** PriorityAlertsPanel between KPI cards and table
- Already imports StatCard, uses shadcn-vue Table, has deadline proximity logic
- Already has `DashboardProps` type definition -- extend it

**`resources/js/types/dashboard.ts`** -- Current types (Story 2.1):
- Has: `DashboardStats`, `DashboardDeclaration`, `StatCardLink`, `DashboardProps`
- **ADD:** `DashboardAlert` type and extend `DashboardProps`

### New Files to Create

| File | Purpose |
|------|---------|
| `resources/js/components/dashboard/PriorityAlertsPanel.vue` | Alert list component with severity colors and empty state |
| `tests/Feature/Dashboard/PriorityAlertsPanelTest.php` | Pest feature tests for alerts endpoint |

### Files to Modify

| File | Changes |
|------|---------|
| `app/Http/Controllers/DashboardController.php` | Add `buildAlerts()` method, include alerts in cached payload, add `viewAllAlertsUrl` prop |
| `resources/js/pages/Dashboard.vue` | Import PriorityAlertsPanel, place between KPI cards and table, pass alerts props |
| `resources/js/types/dashboard.ts` | Add `DashboardAlert` type, extend `DashboardProps` with alerts fields |

### Component Specifications (from UX Design & Architecture)

**PriorityAlertsPanel:**
- Section heading: "Alertes prioritaires" with count badge (e.g., "Alertes prioritaires (7)")
- Alert list: max 20 items, sorted by severity then urgency
- Each alert item is a clickable row/card:
  - Left: severity icon (color-coded)
  - Center: client name + declaration type label + days metric ("3 jours en retard" / "2 jours restants" / "En attente depuis 5 jours")
  - Right: chevron or arrow indicating clickable → navigates to declaration detail
- Severity icon mapping (from lucide-vue-next):
  - Critical: `AlertCircle` with `text-red-600`
  - Warning: `AlertTriangle` with `text-amber-600`
  - Info: `Info` with `text-blue-600`
- Empty state: `CheckCircle` icon with green tint + "Aucune alerte -- tout est en ordre"
- "Voir tout" footer link when alerts are capped at 20
- Use shadcn-vue Card for container, styled consistently with existing dashboard
- Responsive: full-width, stacks naturally on mobile

**Alert Severity Sort Order (within each category):**
- Critical: sort by `daysOverdue DESC` (most overdue first)
- Warning: sort by `daysRemaining ASC` (closest deadline first)
- Info: sort by `daysWaiting DESC` (longest waiting first)

### Backend Alert Query Pattern

```php
// Inside DashboardController -- extend the existing cache closure
private function buildAlerts(User $user, WorkspaceUser $workspaceUser, $workspace): array
{
    $baseQuery = fn () => Declaration::where('workspace_id', $workspace->id)
        ->active()
        ->forUser($user, $workspaceUser)
        ->whereNotIn('status', [
            DeclarationStatus::Termine,
            DeclarationStatus::MiseEnDemeure,
            DeclarationStatus::Ferme,
        ])
        ->with('client:id,company_name');

    // Critical: overdue
    $critical = $baseQuery()
        ->whereNotNull('due_date')
        ->where('due_date', '<', now()->startOfDay())
        ->orderBy('due_date', 'asc')
        ->limit(20)
        ->get()
        ->map(fn ($d) => [
            'id' => $d->id,
            'severity' => 'critical',
            'clientName' => $d->client->company_name,
            'declarationType' => $d->type->value,
            'typeLabel' => $d->type->label(),
            'daysValue' => (int) now()->startOfDay()->diffInDays($d->due_date),
            'daysLabel' => 'jours en retard',
            'showUrl' => route('declarations.show', $d),
        ]);

    // Warning: approaching (within 3 days)
    $warning = $baseQuery()
        ->whereNotNull('due_date')
        ->whereBetween('due_date', [now()->startOfDay(), now()->addDays(3)->endOfDay()])
        ->orderBy('due_date', 'asc')
        ->limit(20)
        ->get()
        ->map(fn ($d) => [
            'id' => $d->id,
            'severity' => 'warning',
            'clientName' => $d->client->company_name,
            'declarationType' => $d->type->value,
            'typeLabel' => $d->type->label(),
            'daysValue' => (int) now()->startOfDay()->diffInDays($d->due_date),
            'daysLabel' => 'jours restants',
            'showUrl' => route('declarations.show', $d),
        ]);

    // Info: waiting >3 days
    $info = $baseQuery()
        ->where('status', DeclarationStatus::EnAttenteClient)
        ->where('updated_at', '<', now()->subDays(3))
        ->orderBy('updated_at', 'asc')
        ->limit(20)
        ->get()
        ->map(fn ($d) => [
            'id' => $d->id,
            'severity' => 'info',
            'clientName' => $d->client->company_name,
            'declarationType' => $d->type->value,
            'typeLabel' => $d->type->label(),
            'daysValue' => (int) now()->diffInDays($d->updated_at),
            'daysLabel' => 'jours en attente',
            'showUrl' => route('declarations.show', $d),
        ]);

    return $critical->concat($warning)->concat($info)->take(20)->values()->toArray();
}
```

**IMPORTANT:** This is a REFERENCE pattern. The dev agent should adapt to match the existing controller's code style (e.g., how `forUser` is called, how excluded statuses are filtered, how data is mapped). Read the actual controller first.

### Frontend Integration Pattern

```vue
<!-- In Dashboard.vue, between KPI cards and table -->
<PriorityAlertsPanel
    :alerts="alerts"
    :view-all-url="viewAllAlertsUrl"
/>
```

### Deadline Proximity Colors (Already Implemented in Dashboard.vue)

The existing `Dashboard.vue` already has deadline proximity logic and color classes. Reuse the same approach:
- `text-green-600`: > 7 days remaining
- `text-amber-600`: 3-7 days remaining
- `text-red-600`: < 3 days remaining
- `text-red-600 animate-pulse`: overdue (past deadline)

### Project Structure Notes

- New component: `resources/js/components/dashboard/PriorityAlertsPanel.vue` (alongside existing `StatCard.vue`)
- New tests: `tests/Feature/Dashboard/PriorityAlertsPanelTest.php` (alongside existing `OwnerDashboardTest.php`)
- Types extended in existing `resources/js/types/dashboard.ts`
- Controller extended in existing `app/Http/Controllers/DashboardController.php`
- Dashboard page extended in existing `resources/js/pages/Dashboard.vue`
- Route: no changes needed (same `/dashboard` route)

### Testing Standards

- Use Pest syntax (`test()` closures), never PHPUnit class-based
- Test descriptions: lowercase, descriptive strings
- Use `route()` helper for URLs, never hardcoded paths
- Factory states: `User::factory()->create()` + attach to workspace with role via `WorkspaceUser`
- Use `RefreshDatabase` (auto-applied via Pest.php)
- Assertions: prefer Pest's `expect()` chaining for data assertions, `->assertInertia()` for page props
- Tests go in `tests/Feature/Dashboard/PriorityAlertsPanelTest.php`
- Run tests: `composer test`
- **Current test count:** 193 tests, 833 assertions. Do NOT break existing tests.

### Previous Story Intelligence (Story 2.1 Learnings)

- **DB column is `due_date` not `deadline`** -- architecture docs use wrong name. Always use `due_date`.
- **Excluded statuses:** `termine`, `mise_en_demeure`, `ferme` must be excluded from dashboard queries. The current controller filters these out -- follow the same pattern.
- **`Declaration::forUser($user, $workspaceUser)`** takes both User and WorkspaceUser args (not just User). Check the model scope signature.
- **withPivot gotcha:** `WorkspaceUser` pivot needs explicit `withPivot('role', 'permissions')` on relationships. This was a recurring issue across Epic 1.
- **Wayfinder routes in Vue:** All URLs must use Wayfinder type-safe routes. The existing `Dashboard.vue` imports from `@/routes`. Follow the same pattern for alert URLs.
- **No API Resources:** Manual array building in controller. Do NOT create FormRequest or API Resource classes.
- **Flash messages:** Already implemented -- success/error toasts auto-dismiss after 4s.
- **Shared Inertia props:** `auth.user`, `auth.workspaces`, `auth.currentWorkspace`, `auth.workspaceRole` available via `usePage()`.
- **Cache mock gotcha from 2.1:** Don't use `Cache::shouldReceive` mocks -- they conflict with middleware Cache calls. Test cache behavior by verifying data structure and TTL directly.
- **`DeclarationType` has a `label()` method** that returns French labels. Use it for display.

### References

- [Source: _bmad-output/planning-artifacts/epics.md#Epic 2 Story 2.2]
- [Source: _bmad-output/planning-artifacts/architecture.md#Dashboard Aggregation Patterns]
- [Source: _bmad-output/planning-artifacts/architecture.md#D4 Caching Strategy]
- [Source: _bmad-output/planning-artifacts/architecture.md#Alert Severity Levels]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Journey 1 Owner/Manager Morning Dashboard]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Emotional Design Principles]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Color System]
- [Source: _bmad-output/planning-artifacts/prd.md#FR24 FR25 FR26]
- [Source: _bmad-output/implementation-artifacts/2-1-owner-manager-command-center-dashboard.md]
- [Source: _bmad-output/project-context.md]
- [Source: app/Http/Controllers/DashboardController.php]
- [Source: resources/js/pages/Dashboard.vue]
- [Source: resources/js/components/dashboard/StatCard.vue]
- [Source: resources/js/types/dashboard.ts]
- [Source: app/Models/Declaration.php#scopeForUser]
- [Source: app/Enums/DeclarationStatus.php]
- [Source: app/Enums/DeclarationType.php]

## Dev Agent Record

### Agent Model Used

Claude Opus 4.6 (1M context)

### Debug Log References

- Fixed `diffInDays` returning negative values for overdue alerts — wrapped with `abs()` to ensure positive daysValue

### Completion Notes List

- Implemented `buildAlerts()` private method in DashboardController querying 3 alert categories (critical/warning/info) using existing `forUser()` scope and excluded statuses
- Alert data computed inside the existing `Cache::remember()` closure — no separate API call or cache key
- Created `DashboardAlert` TypeScript type and extended `DashboardProps` with `alerts` and `viewAllAlertsUrl`
- Built `PriorityAlertsPanel.vue` with severity-coded icons (AlertCircle/AlertTriangle/Info from lucide-vue-next), clickable rows navigating to declaration detail, "Voir tout" link when capped at 20, and empty state with CheckCircle
- Integrated panel between KPI cards and urgent declarations table in Dashboard.vue
- 10 Pest feature tests covering all acceptance criteria: severity categories, sorting, 20-item cap, worker scoping, cache integration, excluded statuses, empty state

### File List

- `app/Http/Controllers/DashboardController.php` (modified) — Added `buildAlerts()` method, included alerts in cached payload and Inertia props
- `resources/js/types/dashboard.ts` (modified) — Added `DashboardAlert` type, extended `DashboardProps`
- `resources/js/components/dashboard/PriorityAlertsPanel.vue` (new) — Alert list component with severity colors and empty state
- `resources/js/pages/Dashboard.vue` (modified) — Imported and placed PriorityAlertsPanel between KPI cards and table
- `tests/Feature/Dashboard/PriorityAlertsPanelTest.php` (new) — 10 Pest feature tests for alerts

## Change Log

- 2026-03-20: Story 2.2 implementation complete — Priority Alerts Panel with 3 severity categories, cached data, and 10 feature tests (206 total tests, 974 assertions, 0 regressions)
