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>
17 KiB
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
-
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
-
Given a Worker is logged in, When the summary table renders, Then it shows only the Worker's assigned declarations sorted by deadline ascending
-
Given a Worker is logged in, When the priority alerts panel renders, Then it shows only alerts for the Worker's assigned declarations
-
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) -
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)
-
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"
-
Given the
DashboardControlleris invoked for a Worker, Then it uses the same code path as Owner/Manager but appliesforUser()scope, resulting in scoped data for Workers -
Given the Worker dashboard is loaded, Then Workers do NOT see team workload distribution or activity from other team members
-
Given the Worker dashboard is loaded, Then the page renders within 3 seconds (NFR5)
Tasks / Subtasks
-
Task 1: Verify and enhance Worker scoping in
DashboardController(AC: #1, #2, #3, #7)- 1.1 Read the current
DashboardControllerand confirmforUser()scope is already applied to all queries (KPI counts, urgent declarations, alerts). It already is — thebaseQueryclosure appliesforUser($user, $workspaceUser). Verify this works correctly for Worker role by running existing tests. - 1.2 Add
assignee=meparam to StatCardhrefURLs when the current user is a Worker, so clicking a KPI card pre-filters the declarations list to the Worker's assignments - 1.3 Add
isWorkerboolean flag to Inertia props to enable frontend conditional rendering
- 1.1 Read the current
-
Task 2: Update
Dashboard.vuefor Worker-specific UI (AC: #4, #5, #6, #8)- 2.1 Add a scoped view subtitle/header that displays when
isWorkeris true: "Mes déclarations" or "Tableau de bord — {userName}" - 2.2 Add an EmptyState when
isWorkeris true ANDdeclarations.length === 0AND all stat card counts are 0: display "Aucune déclaration assignée — contactez votre responsable" with an appropriate icon (e.g.,FolderOpenorClipboardList) - 2.3 Hide the "Assigné à" column in the urgent declarations table when
isWorkeris true (Worker always sees their own — showing their own name is redundant) - 2.4 Ensure "Relancer" (Nudge) and "Réassigner" dropdown actions remain disabled/hidden for Workers (they already are disabled — verify they stay that way)
- 2.1 Add a scoped view subtitle/header that displays when
-
Task 3: Extend StatCard
hreffor Worker context (AC: #4)- 3.1 In
DashboardController, when buildingstatCardsarray, 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. - 3.2 Verify the
declarations.indexroute reads theassigneeparam and applies filtering (prep for Epic 4 FilterBar — for now, the param exists in the URL for future compatibility)
- 3.1 In
-
Task 4: Write Pest feature tests for Worker dashboard (AC: #1, #2, #3, #6, #7, #8)
- 4.1 Test Worker sees ONLY their assigned declarations in KPI counts (overdue, dueThisWeek, enAttenteClient, enCours)
- 4.2 Test Worker does NOT see declarations assigned to other team members in KPI counts
- 4.3 Test Worker sees only their assigned declarations in the urgent declarations table
- 4.4 Test Worker sees only their assigned declarations in the priority alerts
- 4.5 Test Worker dashboard returns
isWorker: truein Inertia props - 4.6 Test Worker with no assigned declarations gets zero counts and empty declarations/alerts arrays
- 4.7 Test Worker StatCard hrefs include assignee scoping param
- 4.8 Test Owner/Manager dashboard returns
isWorker: false - 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)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 - 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 keydashboard:{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 samebaseQuerywhich includesforUser()
What IS needed:
- StatCard URLs need
assigneeparam appended for Workers so clicking navigates to a pre-filtered list - Frontend UI needs Worker-specific elements: subtitle, empty state, column hiding
isWorkerprop added to Inertia response for frontend conditional rendering- 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 keydashboard:{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:
isWorkerboolean prop to Inertia response - MODIFY: StatCard
hrefvalues — appendassigneeparam when user is Worker - The
roleLabelfor 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
isWorkerprop - ADD: EmptyState when Worker has no data
- ADD: Hide "Assigné à" column when
isWorkeris 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: booleantoDashboardProps
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:
FolderOpenorClipboardListfrom 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
<!-- 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
// 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 viaWorkspaceUser - 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 fromOwnerDashboardTest.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_datenotdeadline— architecture docs use wrong name. Always usedue_date. - Excluded statuses:
termine,mise_en_demeure,fermemust 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:
WorkspaceUserpivot needs explicitwithPivot('role', 'permissions')on relationships. - Wayfinder routes in Vue: All URLs must use Wayfinder type-safe routes. The existing
Dashboard.vueimports 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.workspaceRoleavailable viausePage(). - Cache mock gotcha: Don't use
Cache::shouldReceivemocks — they conflict with middleware Cache calls. Test cache behavior by verifying data structure directly. DeclarationTypehas alabel()method that returns French labels. Use it for display.abs()for diffInDays: Wrap withabs()when computing overdue days to avoid negative values (fixed in Story 2.2).- Story 2.1 test helper:
setupWorkspaceWithRole($role)inOwnerDashboardTest.phpcreates a user, workspace, and client with the given role. Reuse or create a similar helper in the Worker test file. - Role label mapping:
WorkspaceUserRole::Workermaps to'Collaborateur'via theroleLabels()method.
Project Structure Notes
- Tests:
tests/Feature/Dashboard/WorkerDashboardTest.php(alongsideOwnerDashboardTest.phpandPriorityAlertsPanelTest.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
/dashboardroute)
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.vuewith 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
isWorkerboolean prop to Inertia response (both normal and null workspace fallback) - Added
assigneeparam to StatCardhrefURLs for Workers (uses$user->idfor future Epic 4 FilterBar compatibility) - Added
isWorker: booleantoDashboardPropsTypeScript type - Added Worker subtitle "Mes déclarations" above KPI cards when
isWorkeris true - Added full EmptyState with
ClipboardListicon 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 — addedisWorkerprop, assignee param in StatCard hrefs)resources/js/pages/Dashboard.vue(modified — Worker subtitle, EmptyState, hidden assignee column)resources/js/types/dashboard.ts(modified — addedisWorkerto DashboardProps)tests/Feature/Dashboard/WorkerDashboardTest.php(new — 9 Pest feature tests)