- 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>
12 KiB
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
- A permissions page (or slide-over panel) displays all configurable permission toggles with descriptive labels
- Each toggle shows the permission name in French (e.g., "Gerer l'equipe", "Voir les journaux d'activite", "Configurer le portail client")
- Toggles reflect the Manager's current permission state (from JSON column)
- Toggling a permission immediately saves via PUT request to
/team/{workspaceUser}/permissions - A success toast confirms "Permissions updated"
- The permission toggles are only visible for Manager role members (not Workers or Owners)
- Only Owners can access the permissions management (Managers with
can_manage_teamcannot modify other Managers' permissions) - Permission changes are logged via Spatie Activity Log
- If a Manager's
can_manage_teamis 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
UpdatePermissionsRequestform 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}/permissionsinroutes/web.php
- 1.1 Create
- Task 2: Backend — Add permissions data to TeamController index (AC: #1, #3, #6)
- 2.1 Extend
index()to passpermissionsUrlper Manager member andavailablePermissionslist (from Permission enum with French labels) - 2.2 Pass each Manager member's current
permissionsarray from the pivot
- 2.1 Extend
- 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_teamoff removes invite capability on next page load
Dev Notes
Critical Architecture Patterns (from Stories 1.1-1.3)
- Authorization:
abort(404)NEVERabort(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_userpivot. 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()(notuseForm) since no complex form state needed - Transactions: Wrap permission update + activity log in
DB::transaction() - Model casts: Use
protected function casts(): arraymethod, NOT$castsproperty - Flash messages:
HandleInertiaRequestssharessuccess/errorflash keys;AppSidebarLayoutdisplays 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 casesapp/Enums/WorkspaceUserRole.php— Owner, Manager, Workerapp/Concerns/AuthorizesPermissions.php—authorizePermission()trait methodapp/Concerns/HasWorkspaceScope.php—currentWorkspace()andauthorizeWorkspaceAccess()app/Models/WorkspaceUser.php— Pivot withpermissionsJSON castapp/Http/Controllers/TeamController.php—index(),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
- 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.
- Save Strategy: Immediate save per toggle (no "Save All" button) — each toggle fires a PUT request independently. Use
router.put()withpreserveScroll: true. - Validation: Backend must validate that only known permission keys (from Permission enum) are submitted. Reject unknown keys.
- Owner-Only Access: This is stricter than other team actions — even Managers with
can_manage_teamCANNOT 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
permissionsJSON 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:
canManageTeamprop already passed to frontend — reuse pattern forisOwnercheck- Flash message toast pattern already working via
HandleInertiaRequests - Team member data structure includes
workspace_user_idfor 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
UpdatePermissionsRequestwith 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}/permissionsfollowing existing team route patterns - Extended
index()to passisOwner,availablePermissions(French labels),permissions, andpermissionsUrlper 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()withpreserveScroll: 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
$validKeysvariable inUpdatePermissionsRequest::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()withinstanceofcheck 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'withROLE_MANAGER/ROLE_OWNERconstants inIndex.vue - [M4] Added
watch()onprops.membersto keeppermissionsMemberref 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)