Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/2-1-owner-manager-command-center-dashboard.md
Saad Ibn-Ezzoubayr a2ab6f365d feat: implement Story 2.1 — Owner/Manager Command Center Dashboard
- 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
2026-03-20 12:00:24 +00:00

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

  1. 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_client status
    • "En cours" (on track) -- green, count of declarations with en_cours status
  2. 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)

  3. 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

  4. Given the summary table is displayed, When a user clicks a table row, Then the browser navigates to the declaration detail page

  5. Given the summary table is displayed, Then inline row actions are available via a dropdown menu (View, Nudge placeholder, Reassign placeholder)

  6. 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})

  7. Given the DashboardController is invoked, Then it aggregates declaration counts by status, overdue counts, and due-this-week counts using role-scoped queries via Declaration::forUser() scope

  8. Given the dashboard page is loaded, Then the page renders within 3 seconds for workspaces with up to 200 active clients (NFR5)

  9. Given the dashboard page is loaded, Then the page uses the existing AppLayout with role-driven sidebar navigation (already implemented in Epic 1)

Tasks / Subtasks

  • Task 1: Rewrite DashboardController with cached, role-scoped aggregation (AC: #1, #6, #7)
    • 1.1 Build cached dashboard data method using Cache::remember() with key dashboard:{workspace_id}:{user_id} and 5-min TTL
    • 1.2 Aggregate KPI counts: overdue (due_date < now()), due this week (due_date between now and +7 days), en_attente_client status count, en_cours status count -- all using Declaration::forUser() scope
    • 1.3 Query top 15 urgent declarations sorted by due_date ASC with eager-loaded client:id,company_name and assignee: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 WorkspaceUser for conditional rendering
  • Task 2: Create StatCard.vue component (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-pressed for active state, keyboard focusable
    • 2.5 Support loading skeleton state
  • Task 3: Rewrite Dashboard.vue page 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 showUrl prop
    • 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.ts with DashboardStats, DashboardDeclaration, DashboardProps types
    • 4.2 Export from resources/js/types/index.ts barrel
  • 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) 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

  • 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 deadline but the actual DB column is due_date. Use due_date in all queries. Also use $workspace->declarations() relationship (not Declaration::workspace() which doesn't exist) -- see existing DashboardController pattern.

  • Declaration statuses (DeclarationStatus enum): created, en_cours, en_attente_client, termine, ferme

  • Exclude from dashboard: ferme status 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 !hasWorkspace admin view (quick links for Users, Workspaces, Clients)
  • Replace PlaceholderPattern usage 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-pressed for 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 Table components (Table, TableHeader, TableRow, TableHead, TableBody, TableCell) -- install table component if not present via npx 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)

Layout:

  • Dashboard grid: grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 for 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 via WorkspaceUser
  • 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: WorkspaceUser pivot needs explicit withPivot('role', 'permissions') on the workspace-user relationship. This was a recurring issue in 3/6 Epic 1 stories. Verify the relationship includes withPivot when 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 uses dashboard().url for 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.workspaceRole shared prop. No sidebar changes needed for this story.
  • Shared Inertia props available: auth.user, auth.workspaces, auth.currentWorkspace, auth.workspaceRole -- use usePage() 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: enCours count includes overdue declarations with en_cours status (counts are not mutually exclusive)
  • Fixed test: replaced Cache::shouldReceive mock with real cache verification to avoid conflict with middleware Cache calls

Completion Notes List

  • Rewrote DashboardController from 5 separate uncached worker-scoped queries to a single cached role-scoped aggregation using Cache::remember() with 5-min TTL and Declaration::forUser() scope
  • Created StatCard.vue component with CVA status color variants (danger/warning/info/success), skeleton loading support, keyboard accessibility (role="button", tabindex, Enter/Space handlers)
  • Rewrote Dashboard.vue with 4-column KPI grid + urgent declarations table using shadcn-vue Table and DropdownMenu components, deadline proximity colors, and preserved admin no-workspace view
  • Created TypeScript types (DashboardStats, DashboardDeclaration, DashboardProps, StatCardLink) in resources/js/types/dashboard.ts
  • Installed shadcn-vue table component (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.vue
  • resources/js/types/dashboard.ts
  • tests/Feature/Dashboard/OwnerDashboardTest.php
  • resources/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)