Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/2-3-worker-scoped-dashboard.md
Saad Zoubir a02b5f12d8 feat: implement Story 2.4 — Dashboard Activity Feed with review fixes
Add role-scoped activity feed to the dashboard showing the 20 most recent
workspace events. Owners/Managers see all activity (declarations, clients,
team changes); Workers see only their assigned declarations. Includes
French descriptions, relative timestamps, responsive layout (desktop
sidebar, tablet inline, mobile collapsible), and 7 passing Pest tests.

Review fixes applied: batch-load declarations/clients/users to eliminate
N+1 queries, consistent soft-delete handling in URL resolution, French
grammar singular/plural fix, missing icon map entry, and corrected tablet
breakpoint per spec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:21:07 +01:00

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

  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={user.id}&status={status} (using the Worker's actual user ID to pre-filter)

  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

  • Task 1: Verify and enhance Worker scoping in DashboardController (AC: #1, #2, #3, #7)

    • 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.
    • 1.2 Add assignee={user.id} 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
    • 1.3 Add isWorker boolean flag to Inertia props to enable frontend conditional rendering
  • Task 2: Update Dashboard.vue for Worker-specific UI (AC: #4, #5, #6, #8)

    • 2.1 Add a scoped view subtitle/header that displays when isWorker is true: "Mes déclarations" or "Tableau de bord — {userName}"
    • 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)
    • 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)
    • 2.4 Ensure "Relancer" (Nudge) and "Réassigner" dropdown actions remain disabled/hidden for Workers (they already are disabled — verify they stay that way)
  • Task 3: Extend StatCard href for Worker context (AC: #4)

    • 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.
    • 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)
  • 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: true in 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) 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

<!-- 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 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)