Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/2-3-worker-scoped-dashboard.md
Saad Ibn-Ezzoubayr 3baf456640 feat: implement Story 2.3 — Worker-Scoped Dashboard
Scope stat cards and urgent declarations table to the authenticated
worker's own assignments. Add empty state when no declarations are
assigned, hide the "Assigné à" column for worker role, and expose
isWorker flag through DashboardController and dashboard types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 17:31:23 +01:00

289 lines
17 KiB
Markdown

# Story 2.3: Worker Scoped Dashboard
Status: review
## Story
As a firm worker,
I want to see only my assigned declarations and their statuses when I open the app,
So that I can quickly identify what I need to work on today without information overload.
## Acceptance Criteria
1. **Given** a Worker is logged in, **When** they navigate to the Dashboard, **Then** the page displays the same KPI card layout as the Owner dashboard but counts reflect only declarations assigned to the Worker
2. **Given** a Worker is logged in, **When** the summary table renders, **Then** it shows only the Worker's assigned declarations sorted by deadline ascending
3. **Given** a Worker is logged in, **When** the priority alerts panel renders, **Then** it shows only alerts for the Worker's assigned declarations
4. **Given** a Worker views the dashboard, **Then** StatCards are clickable and navigate to `/declarations?assignee=me&status={status}` (or equivalent filter that auto-scopes to the Worker)
5. **Given** a Worker views the dashboard, **Then** the page title or subtitle indicates the scoped view (e.g., "Mes déclarations" or the Worker's name)
6. **Given** a Worker has no assigned declarations, **When** they view the Dashboard, **Then** an EmptyState is displayed: "Aucune déclaration assignée — contactez votre responsable"
7. **Given** the `DashboardController` is invoked for a Worker, **Then** it uses the same code path as Owner/Manager but applies `forUser()` scope, resulting in scoped data for Workers
8. **Given** the Worker dashboard is loaded, **Then** Workers do NOT see team workload distribution or activity from other team members
9. **Given** the Worker dashboard is loaded, **Then** the page renders within 3 seconds (NFR5)
## Tasks / Subtasks
- [x] Task 1: Verify and enhance Worker scoping in `DashboardController` (AC: #1, #2, #3, #7)
- [x] 1.1 Read the current `DashboardController` and confirm `forUser()` scope is already applied to all queries (KPI counts, urgent declarations, alerts). **It already is** — the `baseQuery` closure applies `forUser($user, $workspaceUser)`. Verify this works correctly for Worker role by running existing tests.
- [x] 1.2 Add `assignee=me` param to StatCard `href` URLs when the current user is a Worker, so clicking a KPI card pre-filters the declarations list to the Worker's assignments
- [x] 1.3 Add `isWorker` boolean flag to Inertia props to enable frontend conditional rendering
- [x] Task 2: Update `Dashboard.vue` for Worker-specific UI (AC: #4, #5, #6, #8)
- [x] 2.1 Add a scoped view subtitle/header that displays when `isWorker` is true: "Mes déclarations" or "Tableau de bord — {userName}"
- [x] 2.2 Add an EmptyState when `isWorker` is true AND `declarations.length === 0` AND all stat card counts are 0: display "Aucune déclaration assignée — contactez votre responsable" with an appropriate icon (e.g., `FolderOpen` or `ClipboardList`)
- [x] 2.3 Hide the "Assigné à" column in the urgent declarations table when `isWorker` is true (Worker always sees their own — showing their own name is redundant)
- [x] 2.4 Ensure "Relancer" (Nudge) and "Réassigner" dropdown actions remain disabled/hidden for Workers (they already are disabled — verify they stay that way)
- [x] Task 3: Extend StatCard `href` for Worker context (AC: #4)
- [x] 3.1 In `DashboardController`, when building `statCards` array, append `'assignee' => 'me'` (or `$user->id`) to the route params when the user is a Worker. This ensures clicking a KPI card navigates to a filtered declarations list scoped to the Worker.
- [x] 3.2 Verify the `declarations.index` route reads the `assignee` param and applies filtering (prep for Epic 4 FilterBar — for now, the param exists in the URL for future compatibility)
- [x] Task 4: Write Pest feature tests for Worker dashboard (AC: #1, #2, #3, #6, #7, #8)
- [x] 4.1 Test Worker sees ONLY their assigned declarations in KPI counts (overdue, dueThisWeek, enAttenteClient, enCours)
- [x] 4.2 Test Worker does NOT see declarations assigned to other team members in KPI counts
- [x] 4.3 Test Worker sees only their assigned declarations in the urgent declarations table
- [x] 4.4 Test Worker sees only their assigned declarations in the priority alerts
- [x] 4.5 Test Worker dashboard returns `isWorker: true` in Inertia props
- [x] 4.6 Test Worker with no assigned declarations gets zero counts and empty declarations/alerts arrays
- [x] 4.7 Test Worker StatCard hrefs include assignee scoping param
- [x] 4.8 Test Owner/Manager dashboard returns `isWorker: false`
- [x] 4.9 Test cached data is scoped per user (Worker cache key includes user ID — already does: `dashboard:{workspace_id}:{user_id}`)
## 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:** Same `Cache::remember()` with key `dashboard:{workspace_id}:{user_id}` — already user-specific, so Worker data is automatically scoped and cached separately from Owner/Manager data
### CRITICAL: The `forUser()` Scope Already Works
The current `DashboardController` already applies `forUser($user, $workspaceUser)` to ALL queries — both the cached KPI counts and the urgent declarations table. The `Declaration::scopeForUser()` method (in `app/Models/Declaration.php:152`) checks if the role is `Worker` and adds `where('assigned_to', $user->id)`. This means:
- **KPI counts are already Worker-scoped** — no backend counting changes needed
- **Urgent declarations table is already Worker-scoped** — no backend query changes needed
- **Priority alerts are already Worker-scoped** — the `buildAlerts()` method uses the same `baseQuery` which includes `forUser()`
**What IS needed:**
1. **StatCard URLs** need `assignee` param appended for Workers so clicking navigates to a pre-filtered list
2. **Frontend UI** needs Worker-specific elements: subtitle, empty state, column hiding
3. **`isWorker` prop** added to Inertia response for frontend conditional rendering
4. **Tests** to explicitly verify Worker scoping works correctly end-to-end
### 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 caught in Story 2.1 and confirmed in Story 2.2.
### 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 appear in dashboard data.
### Existing Code to Extend (NOT Create Fresh)
**`app/Http/Controllers/DashboardController.php`** — Current controller (Story 2.1 + 2.2):
- Already has `Cache::remember()` with key `dashboard:{workspace_id}:{user_id}` and 5-min TTL
- Already queries with `forUser($user, $workspaceUser)` scope on all queries
- Already returns `stats`, `statCards`, `declarations`, `alerts`, `workspaceName`, `roleLabel`, `declarationsUrl`, `clientsUrl`, `viewAllAlertsUrl`
- **ADD:** `isWorker` boolean prop to Inertia response
- **MODIFY:** StatCard `href` values — append `assignee` param when user is Worker
- The `roleLabel` for Worker already returns `'Collaborateur'` — can use this on the frontend
**`resources/js/pages/Dashboard.vue`** — Current page (Story 2.1 + 2.2):
- Currently has: KPI card grid → PriorityAlertsPanel → urgent declarations table
- **ADD:** Worker subtitle/header conditional on `isWorker` prop
- **ADD:** EmptyState when Worker has no data
- **ADD:** Hide "Assigné à" column when `isWorker` is true
- Already has all deadline proximity logic, status badge variants, row actions
**`resources/js/types/dashboard.ts`** — Current types (Story 2.1 + 2.2):
- Has: `DashboardStats`, `DashboardDeclaration`, `StatCardLink`, `DashboardAlert`, `DashboardProps`
- **ADD:** `isWorker: boolean` to `DashboardProps`
### Files to Modify
| File | Changes |
|------|---------|
| `app/Http/Controllers/DashboardController.php` | Add `isWorker` boolean to Inertia props; modify StatCard `href` to include `assignee` for Workers |
| `resources/js/pages/Dashboard.vue` | Add Worker subtitle, EmptyState for no assignments, hide assignee column for Workers |
| `resources/js/types/dashboard.ts` | Add `isWorker: boolean` to `DashboardProps` |
### New Files to Create
| File | Purpose |
|------|---------|
| `tests/Feature/Dashboard/WorkerDashboardTest.php` | Pest feature tests for Worker-scoped dashboard |
### Component Specifications (from UX Design & Architecture)
**Worker Dashboard Layout:**
- Same 4-column KPI card grid as Owner/Manager
- Same PriorityAlertsPanel component (data is already scoped by backend)
- Same urgent declarations table but WITHOUT "Assigné à" column (redundant for Worker)
- Section heading: "Mes déclarations" (replaces "Déclarations urgentes" for Worker) or keep "Déclarations urgentes" but add subtitle "Mes déclarations" above the KPI cards
- Nudge and Reassign row actions remain disabled (Workers cannot nudge or reassign)
**EmptyState (Worker with no assignments):**
- Icon: `FolderOpen` or `ClipboardList` from lucide-vue-next (consistent with existing empty state icon)
- Title: "Aucune déclaration assignée"
- Subtitle: "Contactez votre responsable pour recevoir des déclarations"
- No action button (Worker cannot self-assign)
- Show this ONLY when all KPI counts are 0 AND declarations array is empty
### Frontend Conditional Rendering Pattern
```vue
<!-- Worker-specific subtitle above KPI cards -->
<p v-if="isWorker" class="text-muted-foreground">
Mes déclarations
</p>
<!-- Full EmptyState for Worker with no assignments -->
<div v-if="isWorker && !hasDeclarations" class="...">
<!-- EmptyState component -->
</div>
<!-- Hide assignee column for Workers -->
<TableHead v-if="!isWorker">Assigné à</TableHead>
<!-- ... in row: -->
<TableCell v-if="!isWorker">{{ declaration.assigneeName }}</TableCell>
```
### Backend Modification Pattern
```php
// In DashboardController::__invoke()
$isWorker = $workspaceUser->role->is(WorkspaceUserRole::Worker);
// Modify statCards href for Workers
$statCards = [
[
'label' => 'En retard',
'count' => $dashboardData['overdue'],
'status' => 'danger',
'href' => route('declarations.index', array_filter([
'overdue' => 1,
'assignee' => $isWorker ? $user->id : null,
])),
],
// ... same pattern for other cards
];
// Add to Inertia props
return Inertia::render('Dashboard', [
// ... existing props
'isWorker' => $isWorker,
]);
```
### 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/WorkerDashboardTest.php`
- Reuse the `setupWorkspaceWithRole()` helper pattern from `OwnerDashboardTest.php`
- Run tests: `composer test`
- **Current test count:** 206 tests, 974 assertions (after Story 2.2). Do NOT break existing tests.
### Previous Story Intelligence (Story 2.1 + 2.2 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.
- **`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.
- **Wayfinder routes in Vue:** All URLs must use Wayfinder type-safe routes. The existing `Dashboard.vue` imports from `@/routes`.
- **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:** Don't use `Cache::shouldReceive` mocks — they conflict with middleware Cache calls. Test cache behavior by verifying data structure directly.
- **`DeclarationType` has a `label()` method** that returns French labels. Use it for display.
- **`abs()` for diffInDays:** Wrap with `abs()` when computing overdue days to avoid negative values (fixed in Story 2.2).
- **Story 2.1 test helper:** `setupWorkspaceWithRole($role)` in `OwnerDashboardTest.php` creates a user, workspace, and client with the given role. Reuse or create a similar helper in the Worker test file.
- **Role label mapping:** `WorkspaceUserRole::Worker` maps to `'Collaborateur'` via the `roleLabels()` method.
### Project Structure Notes
- Tests: `tests/Feature/Dashboard/WorkerDashboardTest.php` (alongside `OwnerDashboardTest.php` and `PriorityAlertsPanelTest.php`)
- Types extended in existing `resources/js/types/dashboard.ts`
- Controller modified in existing `app/Http/Controllers/DashboardController.php`
- Dashboard page modified in existing `resources/js/pages/Dashboard.vue`
- Route: no changes needed (same `/dashboard` route)
### Scope Boundaries — Do NOT Implement
- Do NOT add activity feed (that's Story 2.4)
- Do NOT add new filter bar or search (that's Epic 4)
- Do NOT add real-time updates or WebSockets (deferred post-MVP)
- Do NOT refactor the existing dashboard architecture — extend it minimally
- Do NOT create separate Worker dashboard page/component — use the same `Dashboard.vue` with conditional rendering
### References
- [Source: _bmad-output/planning-artifacts/epics.md#Epic 2 Story 2.3]
- [Source: _bmad-output/planning-artifacts/architecture.md#Role-Scoped Query Patterns]
- [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/ux-design-specification.md#Journey 2 Worker Daily Workflow]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Experience Mechanics]
- [Source: _bmad-output/planning-artifacts/prd.md#FR25]
- [Source: _bmad-output/implementation-artifacts/2-1-owner-manager-command-center-dashboard.md]
- [Source: _bmad-output/implementation-artifacts/2-2-priority-alerts-panel.md]
- [Source: _bmad-output/project-context.md]
- [Source: app/Http/Controllers/DashboardController.php]
- [Source: resources/js/pages/Dashboard.vue]
- [Source: resources/js/types/dashboard.ts]
- [Source: app/Models/Declaration.php#scopeForUser]
- [Source: app/Enums/DeclarationStatus.php]
- [Source: app/Enums/WorkspaceUserRole.php]
- [Source: tests/Feature/Dashboard/OwnerDashboardTest.php]
## Dev Agent Record
### Agent Model Used
Claude Opus 4.6 (1M context)
### Debug Log References
None — clean implementation with no blockers.
### Completion Notes List
- Verified existing `forUser()` scope correctly filters Worker queries (KPI counts, urgent table, alerts) — no backend query changes needed
- Added `isWorker` boolean prop to Inertia response (both normal and null workspace fallback)
- Added `assignee` param to StatCard `href` URLs for Workers (uses `$user->id` for future Epic 4 FilterBar compatibility)
- Added `isWorker: boolean` to `DashboardProps` TypeScript type
- Added Worker subtitle "Mes déclarations" above KPI cards when `isWorker` is true
- Added full EmptyState with `ClipboardList` icon when Worker has no assigned declarations
- Hidden "Assigné à" column in urgent declarations table for Workers (redundant)
- Verified "Relancer" and "Réassigner" dropdown actions remain disabled for Workers (already were)
- Created 9 comprehensive Pest feature tests covering all ACs
- All 215 tests pass (9 new + 206 existing), 1127 assertions, zero regressions
- PHP and JS linting pass
### Change Log
- 2026-03-20: Implemented Story 2.3 — Worker Scoped Dashboard (all 4 tasks, 9 new tests)
### File List
- `app/Http/Controllers/DashboardController.php` (modified — added `isWorker` prop, assignee param in StatCard hrefs)
- `resources/js/pages/Dashboard.vue` (modified — Worker subtitle, EmptyState, hidden assignee column)
- `resources/js/types/dashboard.ts` (modified — added `isWorker` to DashboardProps)
- `tests/Feature/Dashboard/WorkerDashboardTest.php` (new — 9 Pest feature tests)