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>
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
-
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_clientfor >3 days) -- shows client name, days waiting
-
Given alerts are displayed, When the user clicks an alert item, Then the browser navigates to the relevant declaration detail page
-
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
-
Given the alerts panel is loaded, Then deadline proximity uses the color gradient: green (>7d) → amber (3-7d) → red (<3d) → pulsing red (overdue)
-
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)
-
Given there are no alerts, Then an encouraging message is shown: "Aucune alerte -- tout est en ordre"
-
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
DashboardControllerto include alerts in cached payload (AC: #1, #3, #7)- 1.1 Add
buildAlerts()private method that queries 3 alert categories usingDeclaration::forUser()scope - 1.2 Critical alerts:
due_date < now()->startOfDay()with active status, computedaysOverdue - 1.3 Warning alerts:
due_date BETWEEN now()->startOfDay() AND now()->addDays(3)->endOfDay()with active status, computedaysRemaining - 1.4 Info alerts:
status = en_attente_clientAND status held for >3 days (useupdated_ator status transition timestamp), computedaysWaiting - 1.5 Merge all 3 categories, sort by severity (critical→warning→info), then by urgency within category, cap at 20
- 1.6 Include
alertsarray in the existing cached dashboard data structure (insideCache::rememberclosure) - 1.7 Include
viewAllAlertsUrlprop pointing to/declarations?filter=alertsor appropriate filtered view
- 1.1 Add
- Task 2: Add TypeScript types for alerts (AC: #1)
- 2.1 Add
DashboardAlerttype inresources/js/types/dashboard.tswith fields: id, severity, clientName, declarationType, typeLabel, daysValue, daysLabel, showUrl - 2.2 Extend
DashboardPropsto includealerts: DashboardAlert[]andviewAllAlertsUrl: string
- 2.1 Add
- Task 3: Create
PriorityAlertsPanel.vuecomponent (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
- 3.1 Build component with props:
- Task 4: Integrate alerts panel into
Dashboard.vue(AC: #1, #4)- 4.1 Import and place
PriorityAlertsPanelbetween KPI cards grid and urgent declarations table - 4.2 Pass
alertsandviewAllAlertsUrlprops from page data - 4.3 Add section heading "Alertes prioritaires" with alert count badge
- 4.1 Import and place
- 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)notabort(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 keydashboard:{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:
alertsarray andviewAllAlertsUrlto 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
DashboardPropstype definition -- extend it
resources/js/types/dashboard.ts -- Current types (Story 2.1):
- Has:
DashboardStats,DashboardDeclaration,StatCardLink,DashboardProps - ADD:
DashboardAlerttype and extendDashboardProps
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:
AlertCirclewithtext-red-600 - Warning:
AlertTrianglewithtext-amber-600 - Info:
Infowithtext-blue-600
- Critical:
- Empty state:
CheckCircleicon 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 remainingtext-amber-600: 3-7 days remainingtext-red-600: < 3 days remainingtext-red-600 animate-pulse: overdue (past deadline)
Project Structure Notes
- New component:
resources/js/components/dashboard/PriorityAlertsPanel.vue(alongside existingStatCard.vue) - New tests:
tests/Feature/Dashboard/PriorityAlertsPanelTest.php(alongside existingOwnerDashboardTest.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
/dashboardroute)
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 viaWorkspaceUser - 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_datenotdeadline-- architecture docs use wrong name. Always usedue_date. - Excluded statuses:
termine,mise_en_demeure,fermemust 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:
WorkspaceUserpivot needs explicitwithPivot('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.vueimports 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.workspaceRoleavailable viausePage(). - Cache mock gotcha from 2.1: Don't use
Cache::shouldReceivemocks -- they conflict with middleware Cache calls. Test cache behavior by verifying data structure and TTL directly. DeclarationTypehas alabel()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
diffInDaysreturning negative values for overdue alerts — wrapped withabs()to ensure positive daysValue
Completion Notes List
- Implemented
buildAlerts()private method in DashboardController querying 3 alert categories (critical/warning/info) using existingforUser()scope and excluded statuses - Alert data computed inside the existing
Cache::remember()closure — no separate API call or cache key - Created
DashboardAlertTypeScript type and extendedDashboardPropswithalertsandviewAllAlertsUrl - Built
PriorityAlertsPanel.vuewith 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) — AddedbuildAlerts()method, included alerts in cached payload and Inertia propsresources/js/types/dashboard.ts(modified) — AddedDashboardAlerttype, extendedDashboardPropsresources/js/components/dashboard/PriorityAlertsPanel.vue(new) — Alert list component with severity colors and empty stateresources/js/pages/Dashboard.vue(modified) — Imported and placed PriorityAlertsPanel between KPI cards and tabletests/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)