From c89d1879bf66c36fcc6943f4c7f511ed0ff6e3d0 Mon Sep 17 00:00:00 2001 From: Saad Ibn-Ezzoubayr Date: Wed, 18 Mar 2026 00:12:50 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20complete=20Epic=201=20=E2=80=94=20team?= =?UTF-8?q?=20management=20&=20permission=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ...ion-configuration-and-controller-traits.md | 230 +++++++ ...management-page-view-and-invite-members.md | 420 ++++++++++++ .../1-3-role-assignment-and-member-removal.md | 458 +++++++++++++ .../1-4-manager-permission-toggle-matrix.md | 195 ++++++ ...e-based-access-enforcement-across-views.md | 324 ++++++++++ ...ce-switching-for-multi-workspace-owners.md | 231 +++++++ .../sprint-status.yaml | 14 +- app/Concerns/AuthorizesPermissions.php | 34 + app/Concerns/HasWorkspaceScope.php | 30 + app/Enums/Permission.php | 14 + app/Enums/WorkspaceUserRole.php | 2 +- app/Http/Controllers/ClientController.php | 102 ++- .../Controllers/DeclarationController.php | 83 ++- app/Http/Controllers/TeamController.php | 261 ++++++++ app/Http/Controllers/WorkspaceController.php | 4 +- .../Controllers/WorkspaceSwitchController.php | 26 +- app/Http/Middleware/HandleInertiaRequests.php | 12 +- app/Http/Requests/InviteTeamMemberRequest.php | 83 +++ app/Http/Requests/SwitchWorkspaceRequest.php | 29 + .../Requests/UpdatePermissionsRequest.php | 71 ++ .../Requests/UpdateTeamMemberRoleRequest.php | 48 ++ app/Mail/TeamInvitationMail.php | 56 ++ app/Models/Declaration.php | 14 + app/Models/TeamInvitation.php | 98 +++ app/Models/User.php | 22 +- app/Models/Workspace.php | 12 +- config/permissions.php | 31 + ..._26_220354_create_workspace_user_table.php | 2 +- ...ame_member_to_worker_in_workspace_user.php | 27 + ...5_000001_create_team_invitations_table.php | 36 ++ database/seeders/DatabaseSeeder.php | 4 +- package-lock.json | 32 +- package.json | 2 +- resources/css/app.css | 127 ++-- resources/js/components/AppHeader.vue | 2 +- resources/js/components/AppLogo.vue | 6 +- resources/js/components/AppSidebar.vue | 42 +- resources/js/components/DeleteUser.vue | 2 +- resources/js/components/Pagination.vue | 61 +- .../js/components/TwoFactorSetupModal.vue | 2 +- resources/js/components/UserForm.vue | 8 +- resources/js/components/UserMenuContent.vue | 2 +- resources/js/components/WorkspaceForm.vue | 24 +- resources/js/components/WorkspaceSwitcher.vue | 109 +++- resources/js/components/ui/switch/Switch.vue | 38 ++ resources/js/components/ui/switch/index.ts | 1 + resources/js/layouts/app/AppSidebarLayout.vue | 66 ++ resources/js/layouts/settings/Layout.vue | 2 +- .../js/pages/auth/TwoFactorChallenge.vue | 2 +- resources/js/pages/clients/Create.vue | 2 +- resources/js/pages/clients/Edit.vue | 7 +- resources/js/pages/clients/Index.vue | 64 +- resources/js/pages/declarations/Index.vue | 11 +- resources/js/pages/declarations/Show.vue | 4 +- resources/js/pages/settings/Appearance.vue | 2 +- resources/js/pages/settings/Password.vue | 4 +- resources/js/pages/settings/Profile.vue | 4 +- resources/js/pages/settings/TwoFactor.vue | 2 +- resources/js/pages/team/Index.vue | 606 ++++++++++++++++++ resources/js/pages/users/Create.vue | 2 +- resources/js/pages/users/Edit.vue | 2 +- resources/js/pages/users/Index.vue | 36 +- resources/js/pages/workspaces/Create.vue | 2 +- resources/js/pages/workspaces/Edit.vue | 2 +- resources/js/pages/workspaces/Index.vue | 42 +- resources/js/types/auth.ts | 2 + resources/js/types/index.ts | 1 + resources/js/types/team.ts | 32 + .../views/emails/team-invitation.blade.php | 17 + routes/web.php | 6 + tests/Feature/Client/RoleBasedAccessTest.php | 245 +++++++ .../Database/MemberToWorkerMigrationTest.php | 64 ++ .../RenameFoldersToDeclarationsTest.php | 4 +- .../Feature/Declaration/MediaDownloadTest.php | 2 +- .../Declaration/RoleBasedAccessTest.php | 377 +++++++++++ .../Notification/DeclarationMentionTest.php | 6 +- tests/Feature/Team/ManageTeamTest.php | 214 +++++++ tests/Feature/Team/PermissionToggleTest.php | 248 +++++++ tests/Feature/Team/TeamMemberRemovalTest.php | 153 +++++ tests/Feature/Team/TeamRoleAssignmentTest.php | 217 +++++++ tests/Feature/Team/WorkspaceScopeTest.php | 68 ++ .../Feature/Workspace/WorkspaceSwitchTest.php | 123 ++++ tests/Unit/PermissionCheckTest.php | 92 +++ 83 files changed, 5850 insertions(+), 314 deletions(-) create mode 100644 _bmad-output/implementation-artifacts/1-1-permission-configuration-and-controller-traits.md create mode 100644 _bmad-output/implementation-artifacts/1-2-team-management-page-view-and-invite-members.md create mode 100644 _bmad-output/implementation-artifacts/1-3-role-assignment-and-member-removal.md create mode 100644 _bmad-output/implementation-artifacts/1-4-manager-permission-toggle-matrix.md create mode 100644 _bmad-output/implementation-artifacts/1-5-role-based-access-enforcement-across-views.md create mode 100644 _bmad-output/implementation-artifacts/1-6-workspace-switching-for-multi-workspace-owners.md create mode 100644 app/Concerns/AuthorizesPermissions.php create mode 100644 app/Concerns/HasWorkspaceScope.php create mode 100644 app/Enums/Permission.php create mode 100644 app/Http/Controllers/TeamController.php create mode 100644 app/Http/Requests/InviteTeamMemberRequest.php create mode 100644 app/Http/Requests/SwitchWorkspaceRequest.php create mode 100644 app/Http/Requests/UpdatePermissionsRequest.php create mode 100644 app/Http/Requests/UpdateTeamMemberRoleRequest.php create mode 100644 app/Mail/TeamInvitationMail.php create mode 100644 app/Models/TeamInvitation.php create mode 100644 config/permissions.php create mode 100644 database/migrations/2026_03_14_000001_rename_member_to_worker_in_workspace_user.php create mode 100644 database/migrations/2026_03_15_000001_create_team_invitations_table.php create mode 100644 resources/js/components/ui/switch/Switch.vue create mode 100644 resources/js/components/ui/switch/index.ts create mode 100644 resources/js/pages/team/Index.vue create mode 100644 resources/js/types/team.ts create mode 100644 resources/views/emails/team-invitation.blade.php create mode 100644 tests/Feature/Client/RoleBasedAccessTest.php create mode 100644 tests/Feature/Database/MemberToWorkerMigrationTest.php create mode 100644 tests/Feature/Declaration/RoleBasedAccessTest.php create mode 100644 tests/Feature/Team/ManageTeamTest.php create mode 100644 tests/Feature/Team/PermissionToggleTest.php create mode 100644 tests/Feature/Team/TeamMemberRemovalTest.php create mode 100644 tests/Feature/Team/TeamRoleAssignmentTest.php create mode 100644 tests/Feature/Team/WorkspaceScopeTest.php create mode 100644 tests/Feature/Workspace/WorkspaceSwitchTest.php create mode 100644 tests/Unit/PermissionCheckTest.php diff --git a/_bmad-output/implementation-artifacts/1-1-permission-configuration-and-controller-traits.md b/_bmad-output/implementation-artifacts/1-1-permission-configuration-and-controller-traits.md new file mode 100644 index 0000000..d34c0da --- /dev/null +++ b/_bmad-output/implementation-artifacts/1-1-permission-configuration-and-controller-traits.md @@ -0,0 +1,230 @@ +# Story 1.1: Permission Configuration & Controller Traits + +Status: done + + + +## Story + +As a developer, +I want a centralized permission configuration and reusable controller traits for workspace scoping and permission checking, +So that all future controllers can enforce role-based access consistently with minimal code duplication. + +## Acceptance Criteria + +1. **Permission config file** — `config/permissions.php` defines default permissions per role: + - Owner: `['*']` (all permissions) + - Manager: configurable defaults (`can_manage_team: false`, `can_view_activity_logs: true`, `can_configure_portal: false`) + - Worker: `[]` (no configurable permissions) + +2. **Permission enum** — `app/Enums/Permission.php` exists with all permission keys as snake_case values (`can_manage_team`, `can_view_activity_logs`, `can_configure_portal`) + +3. **Rename `Member` to `Worker`** — The existing `WorkspaceUserRole` enum value `Member` must be renamed to `Worker` across models, migrations, seeders, and any references. A data migration updates existing `member` values in `workspace_user.role` to `worker`. + +4. **`HasWorkspaceScope` trait** — `app/Concerns/HasWorkspaceScope.php` provides: + - `currentWorkspace(): Workspace` — resolves workspace from session `current_workspace_id` + - `authorizeWorkspaceAccess(): void` — verifies resource belongs to current workspace, `abort(404)` if not + +5. **`AuthorizesPermissions` trait** — `app/Concerns/AuthorizesPermissions.php` provides: + - `authorizePermission(string $permission): void` + - Owner: always passes (returns immediately) + - Worker: always fails (`abort(404)`) + - Manager: checks `workspace_user.permissions` JSON column — `abort(404)` if key is `false` or absent + - Unknown permission keys default to `false` + +6. **Authorization failures return `abort(404)`** — never `abort(403)`, per architecture convention (NFR9) + +7. **Unit tests** verify: + - Permission checking logic for all three roles (Owner passes, Worker fails, Manager checks JSON) + - Unknown permission keys default to `false` for Managers + - `HasWorkspaceScope` resolves workspace correctly from session + - Data migration renames `member` → `worker` in `workspace_user.role` + +## Tasks / Subtasks + +- [x] Task 1: Rename `Member` to `Worker` in `WorkspaceUserRole` enum (AC: #3) + - [x] 1.1 Update `app/Enums/WorkspaceUserRole.php`: rename `Member` case to `Worker` with value `'worker'` + - [x] 1.2 Create data migration `database/migrations/2026_03_14_000001_rename_member_to_worker_in_workspace_user.php` — updates existing `member` values to `worker` + - [x] 1.3 Update `WorkspaceUser` model default role to `WorkspaceUserRole::Worker` (N/A — no default role in model; updated in original migration) + - [x] 1.4 Update `WorkspaceUserFactory` default role to `WorkspaceUserRole::Worker` (N/A — no WorkspaceUserFactory exists) + - [x] 1.5 Update `DatabaseSeeder` — change any `member` references to `worker` + - [x] 1.6 Search codebase for any `Member` / `member` references in role context and update (updated: WorkspaceController, tests, WorkspaceForm.vue) +- [x] Task 2: Create Permission enum (AC: #2) + - [x] 2.1 Create `app/Enums/Permission.php` using `bensampo/laravel-enum` with values: `CanManageTeam = 'can_manage_team'`, `CanViewActivityLogs = 'can_view_activity_logs'`, `CanConfigurePortal = 'can_configure_portal'` +- [x] Task 3: Create permission config file (AC: #1) + - [x] 3.1 Create `config/permissions.php` with `defaults` array keyed by role +- [x] Task 4: Create `HasWorkspaceScope` trait (AC: #4) + - [x] 4.1 Create `app/Concerns/HasWorkspaceScope.php` with `currentWorkspace()` and `authorizeWorkspaceAccess()` methods + - [x] 4.2 `currentWorkspace()` retrieves workspace via `session('current_workspace_id')` and `auth()->user()->workspaces()` relationship + - [x] 4.3 `authorizeWorkspaceAccess()` compares resource `workspace_id` to current workspace, `abort(404)` on mismatch +- [x] Task 5: Create `AuthorizesPermissions` trait (AC: #5, #6) + - [x] 5.1 Create `app/Concerns/AuthorizesPermissions.php` + - [x] 5.2 Implement `authorizePermission(string $permission)` with Owner/Worker/Manager branching + - [x] 5.3 Manager branch reads `permissions` JSON column from `WorkspaceUser` pivot — `abort(404)` if key missing or `false` +- [x] Task 6: Write tests (AC: #7) + - [x] 6.1 Create `tests/Unit/PermissionCheckTest.php` — test `AuthorizesPermissions` logic for all 3 roles + unknown keys + - [x] 6.2 Create `tests/Feature/Team/WorkspaceScopeTest.php` — test `HasWorkspaceScope` trait + - [x] 6.3 Create `tests/Feature/Database/MemberToWorkerMigrationTest.php` — test data migration + - [x] 6.4 Run full test suite: `composer test` — 105 passed, 0 failures + +## Dev Notes + +### Architecture Constraints (MUST FOLLOW) + +- **Enum library**: Use `bensampo/laravel-enum` ^6.12 (NOT native PHP enums). All existing enums follow this pattern. +- **Model casts**: Use method-based `protected function casts(): array` (NEVER `$casts` property) +- **Authorization**: Always `abort(404)` for permission failures (NEVER `abort(403)`) — intentional to hide workspace existence +- **No Policies/Gates**: This project uses custom `authorizeXxx()` methods in traits/controllers, NOT Laravel Policies or Gates +- **No Spatie Permission package**: Permissions use the JSON column on `workspace_user` pivot — do NOT install `spatie/laravel-permission` +- **Mass assignment**: Explicit `$fillable` arrays (NEVER `$guarded = []`) +- **Workspace scoping**: Always from `session('current_workspace_id')`, never from request params + +### Existing Code to Build On + +- **`WorkspaceUser` model** (`app/Models/WorkspaceUser.php`): Already extends `Pivot`, has `role` column, and `permissions` JSON column (added in Story 0.5). Cast `permissions` to `array` if not already done. +- **`WorkspaceUserRole` enum** (`app/Enums/WorkspaceUserRole.php`): Currently has `Owner`, `Manager`, `Member` — rename `Member` → `Worker` +- **`EnsureUserHasWorkspace` middleware** (`app/Http/Middleware/EnsureUserHasWorkspace.php`): Already validates workspace access via session. The new `HasWorkspaceScope` trait complements this (middleware checks access, trait provides controller helpers). +- **`DeclarationController`/`ClientController`**: Already use `abort(404)` pattern with `authorizeDeclaration()`/`authorizeClient()` methods — the new traits will replace these inline patterns. +- **Existing traits location**: `app/Concerns/` (contains `PasswordValidationRules.php`, `ProfileValidationRules.php`) + +### Permission Checking Implementation Pattern + +```php +// app/Concerns/AuthorizesPermissions.php +protected function authorizePermission(string $permission): void +{ + $workspaceUser = auth()->user()->currentWorkspaceUser(); + + if ($workspaceUser->role === WorkspaceUserRole::Owner) { + return; // Owners can do everything + } + + if ($workspaceUser->role === WorkspaceUserRole::Worker) { + abort(404); // Workers never have configurable permissions + } + + // Manager: check JSON permissions column + if (!($workspaceUser->permissions[$permission] ?? false)) { + abort(404); // 404 not 403 per project convention + } +} +``` + +### User Model Requirements + +The `User` model needs a `currentWorkspaceUser(): WorkspaceUser` method (if not already present) that returns the pivot record for the current workspace. Check if this exists; if not, add it: + +```php +public function currentWorkspaceUser(): WorkspaceUser +{ + return WorkspaceUser::where('user_id', $this->id) + ->where('workspace_id', session('current_workspace_id')) + ->firstOrFail(); +} +``` + +### Data Migration for Member → Worker Rename + +```php +// Migration up(): Update existing 'member' values to 'worker' +DB::table('workspace_user') + ->where('role', 'member') + ->update(['role' => 'worker']); + +// Migration down(): Revert 'worker' back to 'member' +DB::table('workspace_user') + ->where('role', 'worker') + ->update(['role' => 'member']); +``` + +### Project Structure Notes + +- Traits go in `app/Concerns/` (established pattern) +- Enums go in `app/Enums/` (established pattern) +- Config files go in `config/` (standard Laravel) +- Feature tests in `tests/Feature/Team/` and `tests/Feature/Database/` +- Unit tests in `tests/Unit/` +- No new middleware needed — traits are used inside controllers, not as middleware + +### Testing Standards + +- **Framework**: Pest 4 with `test()` closures and `expect()` assertions +- **`RefreshDatabase`**: Auto-applied via `Pest.php` — do NOT add manually +- **Run command**: `composer test` (clears config → runs Pint → runs tests) +- **Test both**: Happy path (authorized) AND sad path (unauthorized → 404) +- **Factory usage**: Use `WorkspaceUser::factory()` to create test users with specific roles and permissions +- **Route helper**: Use `route()` helper, never hardcoded URLs + +### References + +- [Source: _bmad-output/planning-artifacts/epics.md#Epic-1 — Story 1.1 requirements] +- [Source: _bmad-output/planning-artifacts/architecture.md#D1-Permission-Toggle-Storage — JSON column decision] +- [Source: _bmad-output/planning-artifacts/architecture.md#Permission-Checking-Pattern — authorizePermission() code pattern] +- [Source: _bmad-output/planning-artifacts/architecture.md#Phase-1-Files — file locations] +- [Source: _bmad-output/implementation-artifacts/0-5-*.md — previous story patterns and learnings] + +### Previous Story Intelligence (Epic 0 Learnings) + +- **Enum convention**: `bensampo/laravel-enum` with `labels(): array` for French display and custom methods +- **Observer pattern**: Register in `AppServiceProvider::boot()` using `Model::observe()` +- **FK constraints**: Use explicit `->on('table_name')` (never bare `->constrained()`) +- **Scope discipline**: Only modify files directly required by acceptance criteria — no cosmetic changes +- **Test organization**: Group by domain (`tests/Feature/Team/`, `tests/Feature/Database/`) +- **Code review feedback**: Business logic belongs in models/observers/traits, not controllers + +### Git Intelligence + +Recent commits show Epic 0 is complete. Branch `l-ami-fiduciaire-v1.0.0` has 4 commits. All Epic 0 stories implemented and retrospective done. + +## Dev Agent Record + +### Agent Model Used + +Claude Opus 4.6 + +### Debug Log References + +- bensampo/laravel-enum uses `->is()` for comparisons, not `===` (Enum instance vs string constant). Fixed in AuthorizesPermissions trait. +- `permissions` column already cast to `array` on WorkspaceUser model — do not `json_encode()` when attaching via relationship (causes double-encoding). +- Unit tests in `tests/Unit/` need explicit `uses(Tests\TestCase::class, RefreshDatabase::class)` since Pest.php only auto-applies these for Feature tests. +- Added new migration shifted rollback step count in RenameFoldersToDeclarationsTest (5 → 6). + +### Completion Notes List + +- Ultimate context engine analysis completed — comprehensive developer guide created +- Renamed `Member` → `Worker` across enum, migrations, seeders, controllers, tests, and frontend +- Created `Permission` enum with 3 permission keys (CanManageTeam, CanViewActivityLogs, CanConfigurePortal) +- Created `config/permissions.php` with role-based default permissions +- Created `HasWorkspaceScope` trait for workspace resolution and resource access verification +- Created `AuthorizesPermissions` trait for role-based permission checking (Owner/Worker/Manager) +- Added `currentWorkspaceUser()` method to User model +- Created 12 new tests (6 unit + 4 feature/team + 2 feature/database), all passing +- Full test suite: 105 tests, 255 assertions, 0 failures + +### File List + +**New files:** +- app/Enums/Permission.php +- app/Concerns/HasWorkspaceScope.php +- app/Concerns/AuthorizesPermissions.php +- config/permissions.php +- database/migrations/2026_03_14_000001_rename_member_to_worker_in_workspace_user.php +- tests/Unit/PermissionCheckTest.php +- tests/Feature/Team/WorkspaceScopeTest.php +- tests/Feature/Database/MemberToWorkerMigrationTest.php + +**Modified files:** +- app/Enums/WorkspaceUserRole.php (Member → Worker) +- app/Models/User.php (added currentWorkspaceUser() method, added 'permissions' to withPivot) +- app/Http/Controllers/WorkspaceController.php (Member → Worker references) +- database/migrations/2026_02_26_220354_create_workspace_user_table.php (default role Member → Worker) +- database/seeders/DatabaseSeeder.php (Member → Worker references) +- resources/js/components/WorkspaceForm.vue (defaultRole 'member' → 'worker') +- tests/Feature/Notification/DeclarationMentionTest.php ('member' → 'worker' role strings) +- tests/Feature/Declaration/MediaDownloadTest.php ('member' → 'worker' role string) +- tests/Feature/Database/RenameFoldersToDeclarationsTest.php (rollback step count 5 → 6) + +## Change Log + +- 2026-03-14: Story 1.1 implemented — Permission configuration, controller traits (HasWorkspaceScope, AuthorizesPermissions), Permission enum, Member→Worker rename with data migration. All 105 tests passing. +- 2026-03-15: Code review fixes — [H1] Added 'permissions' to withPivot() in User→workspaces relationship. [M1] Rewrote MemberToWorkerMigrationTest to invoke actual migration file instead of duplicating SQL inline. [M2] Noted: config/permissions.php defaults are defined but not consumed — by design, future stories will apply defaults. All 105 tests passing. diff --git a/_bmad-output/implementation-artifacts/1-2-team-management-page-view-and-invite-members.md b/_bmad-output/implementation-artifacts/1-2-team-management-page-view-and-invite-members.md new file mode 100644 index 0000000..7010c01 --- /dev/null +++ b/_bmad-output/implementation-artifacts/1-2-team-management-page-view-and-invite-members.md @@ -0,0 +1,420 @@ +# Story 1.2: Team Management Page — View & Invite Members + +Status: done + + + +## Story + +As a firm owner, +I want to view all team members in my workspace and invite new members via email, +So that I can build my team and see who has access to my firm's data. + +## Acceptance Criteria + +1. **Team index page** — A route `GET /team` renders a team page displaying all workspace members in a table with columns: name, email, role (as Badge), join date, and status (active/pending). + +2. **Pending invitations** — Invited users who have not yet accepted show a "Pending" StatusBadge in the status column. + +3. **Invite Member button** — An "Inviter un membre" button opens a Dialog/Sheet form with: + - Email input (required, valid email format) + - Role selection dropdown (Manager or Worker — Owner is never assignable) + +4. **Invitation email** — Submitting the invite form sends an invitation email to the specified address with a link to register/join the workspace. + +5. **Immediate feedback** — The invitation appears in the team list as "Pending" immediately after submission. A success toast confirms: "Invitation envoyée". + +6. **Workspace scoping** — The `TeamController` uses the `HasWorkspaceScope` trait to scope members to the current workspace. + +7. **Worker access denied** — Workers cannot access the Team page (`abort(404)` via `AuthorizesPermissions`). + +8. **Manager conditional access** — Managers can view the team list but only see the "Inviter un membre" button if they have `can_manage_team` permission. + +9. **Layout & breadcrumbs** — The page uses `AppLayout` with breadcrumbs: `Dashboard / Équipe`. + +10. **EmptyState** — When the workspace has only the owner, an EmptyState is shown: "Invitez votre premier membre d'équipe" with the invite action button. + +## Tasks / Subtasks + +- [x] Task 1: Create `TeamController` with `index` and `invite` methods (AC: #1, #6, #7, #8) + - [x] 1.1 Create `app/Http/Controllers/TeamController.php` using `HasWorkspaceScope` and `AuthorizesPermissions` traits + - [x] 1.2 `index()` method: block Workers (`authorizePermission` with early abort for Worker role), load workspace members with pivot data, load pending invitations, render Inertia page `team/Index` + - [x] 1.3 `invite()` method: validate with `InviteTeamMemberRequest`, create invitation record, send invitation email, redirect back with success toast + - [x] 1.4 Pass `canManageTeam` boolean prop to frontend (true for Owner, check permission for Manager) + +- [x] Task 2: Create `InviteTeamMemberRequest` form request (AC: #3) + - [x] 2.1 Create `app/Http/Requests/InviteTeamMemberRequest.php` + - [x] 2.2 `authorize()`: verify user is Owner OR Manager with `can_manage_team` permission + - [x] 2.3 `rules()`: validate `email` (required, email format, not already in workspace), `role` (required, in: manager, worker) + +- [x] Task 3: Create `TeamInvitation` model and migration (AC: #2, #4) + - [x] 3.1 Create migration `create_team_invitations_table`: `id`, `workspace_id` (FK), `email`, `role` (string), `token` (uuid, unique), `invited_by` (FK to users), `accepted_at` (nullable datetime), `expires_at` (datetime), timestamps + - [x] 3.2 Create `app/Models/TeamInvitation.php` with fillable, casts, relationships (`workspace`, `invitedBy`), `isValid()` method, auto-generate token on creating + - [x] 3.3 Add `teamInvitations(): HasMany` relationship to `Workspace` model + +- [x] Task 4: Create `TeamInvitationMail` mailable (AC: #4) + - [x] 4.1 Create `app/Mail/TeamInvitationMail.php` following existing mailable pattern (envelope + content + markdown) + - [x] 4.2 Create markdown email template `resources/views/emails/team-invitation.blade.php` with workspace name, role, and registration/accept link + - [x] 4.3 Queue the mail dispatch (use `ShouldQueue` or dispatch via queue) + +- [x] Task 5: Create routes (AC: #1) + - [x] 5.1 Add to `routes/web.php` inside the workspace middleware group: + - `Route::get('team', [TeamController::class, 'index'])->name('team.index')` + - `Route::post('team/invite', [TeamController::class, 'invite'])->name('team.invite')` + +- [x] Task 6: Create `team/Index.vue` page (AC: #1, #2, #3, #5, #8, #9, #10) + - [x] 6.1 Create `resources/js/pages/team/Index.vue` with ` diff --git a/resources/js/components/Pagination.vue b/resources/js/components/Pagination.vue index 0e786b4..37df22a 100644 --- a/resources/js/components/Pagination.vue +++ b/resources/js/components/Pagination.vue @@ -1,6 +1,12 @@