196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
|
|
# 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)
|