Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/1-4-manager-permission-toggle-matrix.md

196 lines
12 KiB
Markdown
Raw Normal View History

# Story 1.4: Manager Permission Toggle Matrix
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## 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
- [x] Task 1: Backend — Add `updatePermissions()` method to TeamController (AC: #4, #7, #8)
- [x] 1.1 Create `UpdatePermissionsRequest` form request with authorization (Owner-only) and validation (permission keys must exist in Permission enum)
- [x] 1.2 Add `updatePermissions()` method: load WorkspaceUser by ID scoped to current workspace, verify target is Manager role, update JSON permissions column, log activity
- [x] 1.3 Add PUT route `/team/{workspaceUserId}/permissions` in `routes/web.php`
- [x] Task 2: Backend — Add permissions data to TeamController index (AC: #1, #3, #6)
- [x] 2.1 Extend `index()` to pass `permissionsUrl` per Manager member and `availablePermissions` list (from Permission enum with French labels)
- [x] 2.2 Pass each Manager member's current `permissions` array from the pivot
- [x] Task 3: Frontend — Build Permission Toggle Matrix UI (AC: #1, #2, #3, #5, #6)
- [x] 3.1 Create permission toggle slide-over or inline panel in Team Index page
- [x] 3.2 Render toggle switches for each permission with French labels
- [x] 3.3 Implement immediate save via PUT using Inertia `router.put()` on toggle change
- [x] 3.4 Show success toast on save confirmation
- [x] 3.5 Only show "Manage Permissions" action for Manager-role members when current user is Owner
- [x] Task 4: Tests — Feature tests for permission toggle endpoint (AC: #4, #5, #7, #8, #9)
- [x] 4.1 Test Owner can update Manager permissions
- [x] 4.2 Test Manager cannot update permissions (even with `can_manage_team`)
- [x] 4.3 Test Worker cannot access permissions endpoint (404)
- [x] 4.4 Test cannot update Owner's permissions (no toggles for Owners)
- [x] 4.5 Test cannot update Worker's permissions (no toggles for Workers)
- [x] 4.6 Test invalid permission key is rejected
- [x] 4.7 Test activity log entry created on permission change
- [x] 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.php``authorizePermission()` trait method
- `app/Concerns/HasWorkspaceScope.php``currentWorkspace()` and `authorizeWorkspaceAccess()`
- `app/Models/WorkspaceUser.php` — Pivot with `permissions` JSON cast
- `app/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
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)