289 lines
17 KiB
Markdown
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)
|