- Rewrite DashboardController with cached role-scoped KPI aggregation (Cache::remember, 5-min TTL, Declaration::forUser scope) - Create StatCard.vue component with CVA status variants and a11y - Rewrite Dashboard.vue with 4-column KPI grid + urgent declarations table - Add mise_en_demeure status to DeclarationStatus enum with transitions - Exclude termine, mise_en_demeure, ferme from dashboard queries - Set deadline proximity red threshold to ≤5 days - Add abort(404) for non-member workspace access per architecture - Fix null-safe client access for soft-deleted clients - Fix hardcoded routes with Wayfinder type-safe imports - Fix DashboardProps.stats type to allow null - Add aria-pressed to StatCard for accessibility - Install shadcn-vue table component (11 files) - Add 11 Pest feature tests + 3 mise_en_demeure transition tests - Fix DeclarationFactory eager workspace creation causing slug collisions - 196 tests pass, 836 assertions, zero regressions
16 KiB
Story 2.1: Owner/Manager Command Center Dashboard
Status: review
Story
As a firm owner or manager, I want to see my entire firm's operational status on one screen when I open the app, So that I can instantly identify what needs my attention without drilling into individual clients or declarations.
Acceptance Criteria
-
Given an Owner or Manager is logged in, When they navigate to
/dashboard, Then the page displays a row of KPI summary cards (StatCard components) in a 4-column CSS Grid layout:- "En retard" (overdue) -- red, count of declarations past deadline
- "Cette semaine" (due this week) -- amber, count of declarations due within 7 days
- "En attente client" (waiting for client) -- blue, count of declarations with
en_attente_clientstatus - "En cours" (on track) -- green, count of declarations with
en_coursstatus
-
Given the KPI cards are displayed, When a user clicks a StatCard, Then the browser navigates to the Declarations list page with the corresponding filter pre-applied via URL query params (e.g.,
/declarations?status=en_attente_client) -
Given the dashboard is loaded, Then a declarations summary table appears below the KPI cards showing the most urgent declarations (sorted by deadline ascending, limited to 15 rows) with columns: Client, Type, Deadline (with proximity color), Assignee, Status badge
-
Given the summary table is displayed, When a user clicks a table row, Then the browser navigates to the declaration detail page
-
Given the summary table is displayed, Then inline row actions are available via a dropdown menu (View, Nudge placeholder, Reassign placeholder)
-
Given the dashboard data is requested, Then dashboard data is served from Redis cache (
Cache::remember()with 5-minute TTL, key:dashboard:{workspace_id}:{user_id}) -
Given the
DashboardControlleris invoked, Then it aggregates declaration counts by status, overdue counts, and due-this-week counts using role-scoped queries viaDeclaration::forUser()scope -
Given the dashboard page is loaded, Then the page renders within 3 seconds for workspaces with up to 200 active clients (NFR5)
-
Given the dashboard page is loaded, Then the page uses the existing
AppLayoutwith role-driven sidebar navigation (already implemented in Epic 1)
Tasks / Subtasks
- Task 1: Rewrite
DashboardControllerwith cached, role-scoped aggregation (AC: #1, #6, #7)- 1.1 Build cached dashboard data method using
Cache::remember()with keydashboard:{workspace_id}:{user_id}and 5-min TTL - 1.2 Aggregate KPI counts: overdue (
due_date < now()), due this week (due_datebetween now and +7 days),en_attente_clientstatus count,en_coursstatus count -- all usingDeclaration::forUser()scope - 1.3 Query top 15 urgent declarations sorted by
due_date ASCwith eager-loadedclient:id,company_nameandassignee:id,name - 1.4 Pass all URLs as Inertia props via
route()helper (declarationsUrl with filter params for each KPI card) - 1.5 Return role label from
WorkspaceUserfor conditional rendering
- 1.1 Build cached dashboard data method using
- Task 2: Create
StatCard.vuecomponent (AC: #1, #2)- 2.1 Build component with props:
label,count,status(danger|warning|info|success),href - 2.2 Compose from shadcn-vue
Card+ CVA variants for status colors - 2.3 Make clickable with
router.get(href)navigation - 2.4 Add
role="button",aria-pressedfor active state, keyboard focusable - 2.5 Support loading skeleton state
- 2.1 Build component with props:
- Task 3: Rewrite
Dashboard.vuepage with command center layout (AC: #1, #3, #4, #5, #9)- 3.1 Replace current template with 4-column KPI grid using StatCard components
- 3.2 Build declarations summary table below KPI cards with columns: Client, Type, Deadline (proximity color), Assignee, Status badge
- 3.3 Add clickable rows navigating to declaration detail via
showUrlprop - 3.4 Add inline row actions dropdown (View, Nudge placeholder, Reassign placeholder) using shadcn-vue
DropdownMenu - 3.5 Add deadline proximity color: green (>7d), amber (3-7d), red (<3d), pulsing red (overdue)
- 3.6 Preserve no-workspace admin view for users without workspace
- Task 4: Add TypeScript types for dashboard data (AC: #1, #3)
- 4.1 Create
resources/js/types/dashboard.tswithDashboardStats,DashboardDeclaration,DashboardPropstypes - 4.2 Export from
resources/js/types/index.tsbarrel
- 4.1 Create
- Task 5: Write Pest feature tests for dashboard (AC: #1, #6, #7, #8)
- 5.1 Test Owner sees all workspace declarations in KPI counts
- 5.2 Test Manager sees all workspace declarations in KPI counts
- 5.3 Test Worker sees only assigned declarations (Story 2.3 prep -- verify scoping works)
- 5.4 Test Redis cache is used (verifies cache key and data structure)
- 5.5 Test unauthenticated user redirected to login
- 5.6 Test no-workspace user sees admin view
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 -
Cache pattern from architecture (D4):
$dashboardData = Cache::remember( "dashboard:{$workspace->id}:{$user->id}", 300, // 5 minutes fn () => [ 'total_active' => Declaration::workspace($workspace)->active()->forUser($user, $workspace)->count(), 'by_status' => Declaration::workspace($workspace)->active()->forUser($user, $workspace) ->selectRaw('status, count(*) as count') ->groupBy('status') ->pluck('count', 'status'), 'overdue' => Declaration::workspace($workspace)->active()->forUser($user, $workspace) ->where('deadline', '<', now()) ->count(), 'due_this_week' => Declaration::workspace($workspace)->active()->forUser($user, $workspace) ->whereBetween('deadline', [now(), now()->endOfWeek()]) ->count(), ] );IMPORTANT: The architecture uses
deadlinebut the actual DB column isdue_date. Usedue_datein all queries. Also use$workspace->declarations()relationship (notDeclaration::workspace()which doesn't exist) -- see existing DashboardController pattern. -
Declaration statuses (DeclarationStatus enum):
created,en_cours,en_attente_client,termine,ferme -
Exclude from dashboard:
fermestatus declarations (already closed) and archived (archived_at IS NOT NULL) -
Default sort:
due_date ASC(most urgent first), with NULLs last
Existing Code to Rewrite (NOT Create Fresh)
app/Http/Controllers/DashboardController.php -- Current invokable controller:
- Currently filters ONLY by
assigned_to = $user->id(worker-scoped for everyone) - Must be rewritten to use
Declaration::forUser()scope so Owners/Managers see ALL workspace declarations - Currently runs 5 separate queries without caching -- consolidate into cached aggregation
- Currently maps manually -- keep this pattern (no API Resources per project convention)
- Keep the no-workspace branch for admin users
resources/js/pages/Dashboard.vue -- Current page:
- Currently uses inline notification cards and a flat table -- must be replaced with StatCard grid + summary table
- Currently defines status labels (
statusLabels,statusVariant) inline -- these use OLD status values (draft,waiting_documents, etc.) that NO LONGER EXIST. The actual DeclarationStatus enum values are:created,en_cours,en_attente_client,termine,ferme - Must update to use DeclarationStatus labels from the enum
- Preserve the
!hasWorkspaceadmin view (quick links for Users, Workspaces, Clients) - Replace
PlaceholderPatternusage with actual dashboard content
New Files to Create
| File | Purpose |
|---|---|
resources/js/components/dashboard/StatCard.vue |
Clickable KPI card component with CVA status variants |
resources/js/types/dashboard.ts |
TypeScript types for dashboard props |
tests/Feature/Dashboard/OwnerDashboardTest.php |
Pest feature tests for Owner/Manager dashboard |
Files to Modify
| File | Changes |
|---|---|
app/Http/Controllers/DashboardController.php |
Full rewrite: cached aggregation, role-scoped queries, KPI counts, summary table data |
resources/js/pages/Dashboard.vue |
Full rewrite: StatCard grid, summary table, row actions, proximity colors |
resources/js/types/index.ts |
Add barrel export for dashboard.ts types |
Component Specifications (from UX Design)
StatCard:
- Compose from shadcn-vue
Card+ CVA variants for status colors - Props:
label: string,count: number,status: 'danger' | 'warning' | 'info' | 'success',href: string - States: default, active (selected as filter), loading (skeleton)
- Accessibility:
role="button",aria-pressedfor active, keyboard focusable - Status-to-color mapping: danger=red (overdue), warning=amber (due this week), info=blue (waiting client), success=green (on track)
Declarations summary table:
- Use shadcn-vue
Tablecomponents (Table, TableHeader, TableRow, TableHead, TableBody, TableCell) -- installtablecomponent if not present vianpx shadcn-vue@latest add table - Sortable by deadline (server-side sort, default
due_date ASC) - Clickable rows navigating to declaration detail
- Inline row actions via shadcn-vue
DropdownMenu(already installed) - Deadline proximity color classes:
- Green (
text-green-600): > 7 days remaining - Amber (
text-amber-600): 3-7 days remaining - Red (
text-red-600): < 3 days remaining - Pulsing red (
text-red-600 animate-pulse): overdue (past deadline)
- Green (
Layout:
- Dashboard grid:
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4for KPI cards - Full-width table below cards
- Desktop: 4-column KPI cards + full table
- Tablet: 2-column KPI cards + table with core columns
- Mobile: stacked KPI cards + card-based list view
Deadline Proximity Logic (Frontend)
function deadlineProximity(dueDate: string | null): 'safe' | 'approaching' | 'urgent' | 'overdue' | 'none' {
if (!dueDate) return 'none';
const now = new Date();
const deadline = new Date(dueDate);
const diffDays = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays < 0) return 'overdue';
if (diffDays <= 3) return 'urgent';
if (diffDays <= 7) return 'approaching';
return 'safe';
}
Project Structure Notes
- Alignment with architecture: new files go in
resources/js/components/dashboard/directory (per architecture project structure) - Tests go in
tests/Feature/Dashboard/directory (per architecture) - Types go in
resources/js/types/dashboard.ts(per architecture) - Controller stays at
app/Http/Controllers/DashboardController.php(rewrite, not move) - Route stays at
GET /dashboard(no route changes needed)
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 - Group tests in
tests/Feature/Dashboard/subdirectory - Run tests:
composer test
Previous Epic Intelligence (Epic 1 Learnings)
- withPivot gotcha:
WorkspaceUserpivot needs explicitwithPivot('role', 'permissions')on the workspace-user relationship. This was a recurring issue in 3/6 Epic 1 stories. Verify the relationship includeswithPivotwhen querying role. - Wayfinder routes: All URLs in Vue MUST use Wayfinder type-safe routes (e.g.,
import { dashboard } from '@/routes'). Hardcoded routes in Vue are rejection criteria. The existing Dashboard.vue already usesdashboard().urlfor breadcrumbs. - Flash messages: Already implemented in AppSidebarLayout -- success/error toasts auto-dismiss after 4s.
- Role-based sidebar: Already working -- sidebar shows different nav items based on
auth.workspaceRoleshared prop. No sidebar changes needed for this story. - Shared Inertia props available:
auth.user,auth.workspaces,auth.currentWorkspace,auth.workspaceRole-- useusePage()to access workspace role on the frontend. - Test count at Epic 1 end: 182 tests, 677 assertions. All passing. Do not break existing tests.
References
- [Source: _bmad-output/planning-artifacts/epics.md#Epic 2 Story 2.1]
- [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#Role-Scoped Query Patterns]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Journey 1 Owner/Manager Morning Dashboard]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#StatCard Component]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Responsive Strategy]
- [Source: _bmad-output/planning-artifacts/prd.md#FR24 FR25 FR26]
- [Source: _bmad-output/implementation-artifacts/1-6-workspace-switching-for-multi-workspace-owners.md]
- [Source: _bmad-output/implementation-artifacts/epic-1-retro-2026-03-20.md]
- [Source: _bmad-output/project-context.md]
- [Source: app/Http/Controllers/DashboardController.php]
- [Source: resources/js/pages/Dashboard.vue]
- [Source: app/Models/Declaration.php#scopeForUser]
- [Source: app/Enums/DeclarationStatus.php]
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
- Fixed test:
enCourscount includes overdue declarations withen_coursstatus (counts are not mutually exclusive) - Fixed test: replaced
Cache::shouldReceivemock with real cache verification to avoid conflict with middleware Cache calls
Completion Notes List
- Rewrote
DashboardControllerfrom 5 separate uncached worker-scoped queries to a single cached role-scoped aggregation usingCache::remember()with 5-min TTL andDeclaration::forUser()scope - Created
StatCard.vuecomponent with CVA status color variants (danger/warning/info/success), skeleton loading support, keyboard accessibility (role="button",tabindex, Enter/Space handlers) - Rewrote
Dashboard.vuewith 4-column KPI grid + urgent declarations table using shadcn-vueTableandDropdownMenucomponents, deadline proximity colors, and preserved admin no-workspace view - Created TypeScript types (
DashboardStats,DashboardDeclaration,DashboardProps,StatCardLink) inresources/js/types/dashboard.ts - Installed shadcn-vue
tablecomponent (11 files) - Added 11 new Pest feature tests covering: owner/manager/worker role scoping, cache verification, archived exclusion, sort order, 15-row limit, stat card structure, and assignee data
- All 193 tests pass (833 assertions), zero regressions
- PHP Pint and ESLint pass on all new/modified files
Change Log
- 2026-03-20: Implemented Story 2.1 — Owner/Manager Command Center Dashboard (all 5 tasks complete)
File List
New files:
resources/js/components/dashboard/StatCard.vueresources/js/types/dashboard.tstests/Feature/Dashboard/OwnerDashboardTest.phpresources/js/components/ui/table/(11 files — shadcn-vue table component)
Modified files:
app/Http/Controllers/DashboardController.php(full rewrite)resources/js/pages/Dashboard.vue(full rewrite)resources/js/types/index.ts(added dashboard barrel export)