Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/1-4-manager-permission-toggle-matrix.md
Saad Ibn-Ezzoubayr c89d1879bf feat: complete Epic 1 — team management & permission system
- Story 1.1: Permission enum, config, AuthorizesPermissions & HasWorkspaceScope traits, member→worker migration
- Story 1.2: Team page with member list, invitation system with queued email
- Story 1.3: Role assignment (Manager/Worker) and member removal with activity logging
- Story 1.4: Owner-only permission toggle matrix for Managers (manage team, view logs, configure portal)
- Story 1.5: Role-based access enforcement — Workers see only assigned declarations/clients, sidebar scoping
- Story 1.6: Workspace switcher dropdown for multi-workspace users with session-based switching
- 83 new/modified files, 182 tests passing with zero regressions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 00:12:50 +00:00

12 KiB

Story 1.4: Manager Permission Toggle Matrix

Status: done

Story

As a firm owner, I want to configure which specific permissions each Manager has in my workspace, so that I can grant or restrict Manager capabilities based on my trust level and firm structure.

Acceptance Criteria

  1. A permissions page (or slide-over panel) displays all configurable permission toggles with descriptive labels
  2. Each toggle shows the permission name in French (e.g., "Gerer l'equipe", "Voir les journaux d'activite", "Configurer le portail client")
  3. Toggles reflect the Manager's current permission state (from JSON column)
  4. Toggling a permission immediately saves via PUT request to /team/{workspaceUser}/permissions
  5. A success toast confirms "Permissions updated"
  6. The permission toggles are only visible for Manager role members (not Workers or Owners)
  7. Only Owners can access the permissions management (Managers with can_manage_team cannot modify other Managers' permissions)
  8. Permission changes are logged via Spatie Activity Log
  9. If a Manager's can_manage_team is toggled off, the "Invite Member" button disappears from their Team page view on next load

Tasks / Subtasks

  • Task 1: Backend — Add updatePermissions() method to TeamController (AC: #4, #7, #8)
    • 1.1 Create UpdatePermissionsRequest form request with authorization (Owner-only) and validation (permission keys must exist in Permission enum)
    • 1.2 Add updatePermissions() method: load WorkspaceUser by ID scoped to current workspace, verify target is Manager role, update JSON permissions column, log activity
    • 1.3 Add PUT route /team/{workspaceUserId}/permissions in routes/web.php
  • Task 2: Backend — Add permissions data to TeamController index (AC: #1, #3, #6)
    • 2.1 Extend index() to pass permissionsUrl per Manager member and availablePermissions list (from Permission enum with French labels)
    • 2.2 Pass each Manager member's current permissions array from the pivot
  • Task 3: Frontend — Build Permission Toggle Matrix UI (AC: #1, #2, #3, #5, #6)
    • 3.1 Create permission toggle slide-over or inline panel in Team Index page
    • 3.2 Render toggle switches for each permission with French labels
    • 3.3 Implement immediate save via PUT using Inertia router.put() on toggle change
    • 3.4 Show success toast on save confirmation
    • 3.5 Only show "Manage Permissions" action for Manager-role members when current user is Owner
  • Task 4: Tests — Feature tests for permission toggle endpoint (AC: #4, #5, #7, #8, #9)
    • 4.1 Test Owner can update Manager permissions
    • 4.2 Test Manager cannot update permissions (even with can_manage_team)
    • 4.3 Test Worker cannot access permissions endpoint (404)
    • 4.4 Test cannot update Owner's permissions (no toggles for Owners)
    • 4.5 Test cannot update Worker's permissions (no toggles for Workers)
    • 4.6 Test invalid permission key is rejected
    • 4.7 Test activity log entry created on permission change
    • 4.8 Test toggling can_manage_team off removes invite capability on next page load

Dev Notes

Critical Architecture Patterns (from Stories 1.1-1.3)

  • Authorization: abort(404) NEVER abort(403) — hides workspace existence per project convention
  • Enum comparisons: use ->is() with bensampo/laravel-enum, NOT ===
  • WorkspaceUser extends Pivot — route model binding does NOT work; use manual WorkspaceUser::where() lookup scoped to current workspace via session
  • Permission checking: Role-based with JSON column on workspace_user pivot. NO Spatie policies/gates
  • Activity logging: Use activity() helper for business operations (non-model-level)
  • Frontend URLs: All URLs via route() helper passed as Inertia props — never hardcode
  • Form handling: For simple toggle saves, use router.put() (not useForm) since no complex form state needed
  • Transactions: Wrap permission update + activity log in DB::transaction()
  • Model casts: Use protected function casts(): array method, NOT $casts property
  • Flash messages: HandleInertiaRequests shares success/error flash keys; AppSidebarLayout displays toast

Permission Keys & French Labels

Permission Key (snake_case) Enum Value French Label
can_manage_team Permission::CanManageTeam Gerer l'equipe
can_view_activity_logs Permission::CanViewActivityLogs Voir les journaux d'activite
can_configure_portal Permission::CanConfigurePortal Configurer le portail client

Existing Permission Infrastructure

The permission system is fully built from Stories 1.1-1.3:

  • config/permissions.php — Default permissions per role (Manager defaults: can_manage_team: false, can_view_activity_logs: true, can_configure_portal: false)
  • app/Enums/Permission.php — Three permission cases
  • app/Enums/WorkspaceUserRole.php — Owner, Manager, Worker
  • app/Concerns/AuthorizesPermissions.phpauthorizePermission() trait method
  • app/Concerns/HasWorkspaceScope.phpcurrentWorkspace() and authorizeWorkspaceAccess()
  • app/Models/WorkspaceUser.php — Pivot with permissions JSON cast
  • app/Http/Controllers/TeamController.phpindex(), invite(), updateRole(), remove() already implemented

What This Story Adds

This is the only missing CRUD operation on the permission system: the ability for Owners to toggle individual Manager permissions. Everything else (reading permissions for authorization, resetting on role change, default application) is already implemented.

Key Implementation Decisions

  1. UI Pattern: Add a "Manage Permissions" option to the existing DropdownMenu on Manager rows (same pattern as "Change Role" and "Remove"). Opens a Dialog/slide-over with toggle switches.
  2. Save Strategy: Immediate save per toggle (no "Save All" button) — each toggle fires a PUT request independently. Use router.put() with preserveScroll: true.
  3. Validation: Backend must validate that only known permission keys (from Permission enum) are submitted. Reject unknown keys.
  4. Owner-Only Access: This is stricter than other team actions — even Managers with can_manage_team CANNOT modify permissions. Only Owners.

Project Structure Notes

  • Alignment with existing team management patterns established in Stories 1.2-1.3
  • New route follows existing pattern: /team/{workspaceUserId}/permissions (PUT)
  • No new Vue pages needed — extend existing resources/js/pages/team/Index.vue
  • New form request: app/Http/Requests/UpdatePermissionsRequest.php
  • No new models or migrations needed — uses existing permissions JSON column

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Epic 1 - Story 1.4]
  • [Source: _bmad-output/planning-artifacts/architecture.md#RBAC Implementation]
  • [Source: _bmad-output/planning-artifacts/prd.md#Permission Matrix]
  • [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Team Interaction Patterns]
  • [Source: _bmad-output/implementation-artifacts/1-1-permission-configuration-and-controller-traits.md]
  • [Source: _bmad-output/implementation-artifacts/1-2-team-management-page-view-and-invite-members.md]
  • [Source: _bmad-output/implementation-artifacts/1-3-role-assignment-and-member-removal.md]
  • [Source: config/permissions.php]
  • [Source: app/Enums/Permission.php]
  • [Source: app/Http/Controllers/TeamController.php]

Previous Story Intelligence

From Story 1.3 (most recent):

  • WorkspaceUser route model binding doesn't work — use manual query: WorkspaceUser::where('workspace_id', $workspace->id)->where('id', $id)->firstOrFail()
  • Role change resets permissions to config('permissions.defaults.{newRole}') — this pattern confirms the config structure works
  • Activity logging pattern: activity()->performedOn($workspaceUser)->causedBy(auth()->user())->withProperties([...])->log('description')
  • Frontend dialog pattern: Dialog + DialogContent + DialogHeader + DialogFooter from shadcn/ui
  • Loading states on action buttons to prevent double-click
  • Wrapped operations in DB::transaction()

From Story 1.2:

  • canManageTeam prop already passed to frontend — reuse pattern for isOwner check
  • Flash message toast pattern already working via HandleInertiaRequests
  • Team member data structure includes workspace_user_id for URL construction

Git Intelligence:

  • Last commits: Epic 0 retrospective and foundation setup complete
  • Stories 1.1-1.3 are implemented but not yet committed to main (on feature branch l-ami-fiduciaire-v1.0.0)

Technology Versions

  • Laravel 12, PHP 8.2+, Vue 3.5, TypeScript 5.2 (strict), Inertia.js 2.0
  • Pest 4.4 for testing
  • bensampo/laravel-enum for enums
  • Spatie Activity Log for audit trail
  • shadcn-vue for UI components (Dialog, DropdownMenu, Switch/Toggle, etc.)

Dev Agent Record

Agent Model Used

Claude Opus 4.6

Debug Log References

No blockers or debug issues encountered.

Completion Notes List

  • Created UpdatePermissionsRequest with Owner-only authorization and Permission enum key validation
  • Added updatePermissions() method to TeamController with DB::transaction wrapping permission update + Spatie activity log
  • Added PUT route /team/{workspaceUserId}/permissions following existing team route patterns
  • Extended index() to pass isOwner, availablePermissions (French labels), permissions, and permissionsUrl per Manager member
  • Added permissionLabels() protected method returning French labels for all 3 permissions
  • Installed shadcn-vue Switch component for toggle UI
  • Added permissions Dialog with Switch toggles per permission, opened from "Gérer les permissions" dropdown item (Shield icon)
  • Immediate save per toggle via router.put() with preserveScroll: true
  • Success toast via existing flash message infrastructure ("Permissions mises à jour")
  • "Gérer les permissions" dropdown item only visible for Manager-role rows when current user is Owner
  • 8 comprehensive Pest feature tests covering all ACs: owner can update, manager/worker cannot, owner/worker targets rejected, invalid keys rejected, activity log verified, can_manage_team toggle affects invite capability
  • All 143 tests pass (380 assertions), zero regressions
  • PHP Pint linting passes clean
  • ESLint errors all pre-existing (not in changed files)

Code Review Fixes (AI)

  • [H1] Removed dead $validKeys variable in UpdatePermissionsRequest::rules()
  • [H2] Added completeness validation: all permission keys must be present in request to prevent silent permission loss on partial submissions
  • [M1] Fixed enum comparison in TeamController::index() to use ->is() with instanceof check per project convention
  • [M2] Added cross-workspace isolation test (owner of workspace A cannot update manager in workspace B)
  • [M3] Replaced magic role strings 'manager'/'owner' with ROLE_MANAGER/ROLE_OWNER constants in Index.vue
  • [M4] Added watch() on props.members to keep permissionsMember ref in sync during Inertia partial reloads
  • Updated tests 4.4 and 4.5 to send all 3 permission keys (required by H2 completeness validation)
  • All 144 tests pass (382 assertions) after review fixes, zero regressions

Change Log

  • 2026-03-15: Implemented Story 1.4 — Manager Permission Toggle Matrix (all tasks complete)
  • 2026-03-15: Code review fixes — 2 HIGH, 4 MEDIUM issues resolved (6 fixes applied)

File List

  • app/Http/Requests/UpdatePermissionsRequest.php (new, review-fixed — removed dead code, added completeness validation)
  • app/Http/Controllers/TeamController.php (modified — added updatePermissions(), extended index(), added permissionLabels(); review-fixed enum comparison)
  • routes/web.php (modified — added PUT team/{workspaceUserId}/permissions route)
  • resources/js/types/team.ts (modified — added permissions?, permissionsUrl?, isOwner, availablePermissions)
  • resources/js/pages/team/Index.vue (modified — added permissions dialog, Switch toggles, dropdown item; review-fixed role constants + props watcher)
  • resources/js/components/ui/switch/Switch.vue (new — shadcn-vue component)
  • resources/js/components/ui/switch/index.ts (new — shadcn-vue barrel)
  • tests/Feature/Team/PermissionToggleTest.php (new — 9 tests; review-added cross-workspace test + fixed partial permission payloads)