Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/2-2-priority-alerts-panel.md
Saad Ibn-Ezzoubayr 4807376c49 feat: implement Story 2.2 — Priority Alerts Panel with UI fixes
Add PriorityAlertsPanel component to the dashboard, update DashboardController
with alert logic, and apply misc UI fixes across sidebar, forms, and pages.
Includes epic-1 retrospective and sprint status update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:33:27 +00:00

19 KiB

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

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

// 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

<!-- 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)