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
This commit is contained in:
2026-03-20 12:00:24 +00:00
parent e53b013359
commit a2ab6f365d
23 changed files with 1283 additions and 523 deletions

View File

@@ -0,0 +1,261 @@
# Story 2.1: Owner/Manager Command Center Dashboard
Status: review
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## 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
- [x] Task 1: Rewrite `DashboardController` with cached, role-scoped aggregation (AC: #1, #6, #7)
- [x] 1.1 Build cached dashboard data method using `Cache::remember()` with key `dashboard:{workspace_id}:{user_id}` and 5-min TTL
- [x] 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
- [x] 1.3 Query top 15 urgent declarations sorted by `due_date ASC` with eager-loaded `client:id,company_name` and `assignee:id,name`
- [x] 1.4 Pass all URLs as Inertia props via `route()` helper (declarationsUrl with filter params for each KPI card)
- [x] 1.5 Return role label from `WorkspaceUser` for conditional rendering
- [x] Task 2: Create `StatCard.vue` component (AC: #1, #2)
- [x] 2.1 Build component with props: `label`, `count`, `status` (danger|warning|info|success), `href`
- [x] 2.2 Compose from shadcn-vue `Card` + CVA variants for status colors
- [x] 2.3 Make clickable with `router.get(href)` navigation
- [x] 2.4 Add `role="button"`, `aria-pressed` for active state, keyboard focusable
- [x] 2.5 Support loading skeleton state
- [x] Task 3: Rewrite `Dashboard.vue` page with command center layout (AC: #1, #3, #4, #5, #9)
- [x] 3.1 Replace current template with 4-column KPI grid using StatCard components
- [x] 3.2 Build declarations summary table below KPI cards with columns: Client, Type, Deadline (proximity color), Assignee, Status badge
- [x] 3.3 Add clickable rows navigating to declaration detail via `showUrl` prop
- [x] 3.4 Add inline row actions dropdown (View, Nudge placeholder, Reassign placeholder) using shadcn-vue `DropdownMenu`
- [x] 3.5 Add deadline proximity color: green (>7d), amber (3-7d), red (<3d), pulsing red (overdue)
- [x] 3.6 Preserve no-workspace admin view for users without workspace
- [x] Task 4: Add TypeScript types for dashboard data (AC: #1, #3)
- [x] 4.1 Create `resources/js/types/dashboard.ts` with `DashboardStats`, `DashboardDeclaration`, `DashboardProps` types
- [x] 4.2 Export from `resources/js/types/index.ts` barrel
- [x] Task 5: Write Pest feature tests for dashboard (AC: #1, #6, #7, #8)
- [x] 5.1 Test Owner sees all workspace declarations in KPI counts
- [x] 5.2 Test Manager sees all workspace declarations in KPI counts
- [x] 5.3 Test Worker sees only assigned declarations (Story 2.3 prep -- verify scoping works)
- [x] 5.4 Test Redis cache is used (verifies cache key and data structure)
- [x] 5.5 Test unauthenticated user redirected to login
- [x] 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):**
```php
$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)
```typescript
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)

View File

@@ -50,18 +50,18 @@ development_status:
epic-0-retrospective: done
# Epic 1: Team Management & Permission System
epic-1: in-progress
epic-1: done
1-1-permission-configuration-and-controller-traits: done
1-2-team-management-page-view-and-invite-members: done
1-3-role-assignment-and-member-removal: done
1-4-manager-permission-toggle-matrix: done
1-5-role-based-access-enforcement-across-views: done
1-6-workspace-switching-for-multi-workspace-owners: done
epic-1-retrospective: optional
epic-1-retrospective: done
# Epic 2: Role-Driven Dashboard & Command Center
epic-2: backlog
2-1-owner-manager-command-center-dashboard: backlog
epic-2: in-progress
2-1-owner-manager-command-center-dashboard: review
2-2-priority-alerts-panel: backlog
2-3-worker-scoped-dashboard: backlog
2-4-dashboard-activity-feed: backlog