TenantAtlas/specs/107-workspace-chooser/tasks.md
ahmido e15eee8f26 fix: consolidate tenant creation + harden selection flows (#131)
## Summary
- Removes the legacy Tenant CRUD create page (`/admin/tenants/create`) so tenant creation is handled exclusively via the onboarding wizard.
- Updates tenant selection flows and pages to prevent Livewire polling/notification-related 404s on workspace-scoped routes.
- Aligns empty-state UX with enterprise patterns (avoid duplicate CTAs).

## Key changes
- Tenant creation
  - Removed `CreateTenant` page + route from `TenantResource`.
  - `TenantResource::canCreate()` now returns `false` (CRUD creation disabled).
  - Tenants list now surfaces an **Add tenant** action that links to onboarding (`admin.onboarding`).
- Onboarding wizard
  - Removed redundant legacy step-cards from the blade view (Wizard schema is the source of truth).
  - Disabled topbar on the onboarding page to avoid lazy-loaded notifications.
- Choose tenant
  - Enterprise UI redesign + workspace context.
  - Uses Livewire `selectTenant()` instead of a form POST.
  - Disabled topbar and gated BODY_END hook to avoid background polling.
- Baseline profiles
  - Hide header create action when table is empty to avoid duplicate CTAs.

## Tests
- `vendor/bin/sail artisan test --compact --filter='Onboarding|ManagedTenantOnboarding'`
- `vendor/bin/sail artisan test --compact --filter='ManagedTenantsLivewireUpdate'`
- `vendor/bin/sail artisan test --compact --filter='TenantSetup|TenantResourceAuth|TenantAdminAuth|ListTenants'`
- `vendor/bin/sail artisan test --compact --filter='BaselineProfile'`
- `vendor/bin/sail artisan test --compact --filter='ChooseTenant|TenantMake|TenantScoping|AdminTenantScoped|AdminHomeRedirect|WorkspaceContext'`

## Notes
- Filament v5 / Livewire v4 compatible.
- No new assets introduced; no deploy pipeline changes required.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #131
2026-02-22 19:54:24 +00:00

288 lines
19 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tasks: Workspace Chooser v1 (Enterprise) + In-App Switch Entry Point
**Input**: Design documents from `/specs/107-workspace-chooser/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/routes.md, quickstart.md
**Tests**: REQUIRED (Pest) — all changes involve runtime behavior. 17 test cases from spec + additional integration tests.
**Operations**: No `OperationRun` needed — workspace selection is synchronous, DB-only, < 2s. Audit entries via `WorkspaceAuditLogger`.
**RBAC**:
- Authorization plane: admin `/admin`
- Membership = switch-right (any workspace member may select/switch)
- Non-member selection attempt 404 (deny-as-not-found) via `WorkspaceContext::isMember()`
- `Capabilities::WORKSPACE_MANAGE` gates "Manage workspaces" link visibility (canonical registry constant)
- Positive test: member selects workspace success
- Negative test: non-member attempt 404
**Filament UI Action Surfaces**: ChooseWorkspace is a custom context-selector page (not CRUD Resource). UI Action Matrix in spec no header actions (v1), "Open" per workspace, empty state with specific title + CTA. Exemption from UX-001 documented.
**Badges**: Workspace membership role badge is a tag/category (owner/admin/member), exempt from BADGE-001 per R7 decision. Simple color-mapped `<x-filament::badge>`, no `BadgeCatalog`.
**Organization**: Tasks grouped by user story. Stories map to quickstart phases:
- Foundation Phase A (enum + redirect resolver)
- US1+US2 Phase B (middleware refactor, incremental)
- US3 Phase C (chooser page upgrade)
- US4 Phase B enhancement (stale detection)
- US5 Phase C+D (chooser audit + user menu)
- US6 Verification (audit payloads)
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (US1US6)
- Exact file paths included in all descriptions
---
## Phase 1: Setup
**Purpose**: No project initialization needed existing Laravel monolith with Filament v5.
_(No tasks — project structure, dependencies, and all target directories already exist.)_
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented.
** CRITICAL**: No user story work can begin until this phase is complete.
- [X] T001 [P] Add `WorkspaceAutoSelected` and `WorkspaceSelected` enum cases to `app/Support/Audit/AuditActionId.php`
- [X] T002 [P] Create `WorkspaceRedirectResolver` service with tenant-count branching logic (0managed tenants, 1tenant dashboard, >1→choose tenant) in `app/Support/Workspaces/WorkspaceRedirectResolver.php`
- [X] T003 Write tests for `WorkspaceRedirectResolver` covering 0/1/>1 tenant branching in `tests/Feature/Workspaces/WorkspaceRedirectResolverTest.php`
**Checkpoint**: Foundation ready — AuditActionId enum extended, tenant-count branching deduplicated into resolver. User story implementation can now begin.
---
## Phase 3: User Story 1 — Auto-Resume: Single Workspace (Priority: P1) 🎯 MVP
**Goal**: A user with exactly one workspace membership is taken directly to their workspace dashboard without seeing the chooser.
**Independent Test**: Create a user with one workspace membership, hit `/admin`, verify redirect to workspace dashboard without chooser.
### Implementation for User Story 1
- [X] T004 [US1] Refactor `EnsureWorkspaceSelected` middleware: implement step 1 (workspace-optional bypass), step 2 (`?choose=1` redirect), step 3 (basic session validation — allow if valid membership), step 4 (load selectable memberships), step 5 (single membership auto-resume with audit via `WorkspaceAuditLogger`), step 7 (fallback redirect to chooser) in `app/Http/Middleware/EnsureWorkspaceSelected.php`
- [X] T005 [US1] Write test `it_skips_chooser_when_single_workspace_membership` — verify direct redirect to workspace dashboard in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [X] T006 [US1] Write test `it_emits_audit_event_on_auto_selection_single_membership` — verify `workspace.auto_selected` audit log with reason `single_membership` in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [X] T007 [US1] Write test `it_redirects_via_tenant_count_branching_after_single_auto_resume` — verify 0/1/>1 tenant routing after auto-resume in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [X] T008 [US1] Write test `it_allows_request_when_session_workspace_is_valid` — verify middleware passes through when session has valid membership in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
**Checkpoint**: Single-workspace users bypass the chooser entirely. Audit event emitted. Middleware skeleton (7 steps) in place with steps 15, 7 active.
---
## Phase 4: User Story 2 — Auto-Resume: Last Used Workspace (Priority: P1)
**Goal**: A user with multiple workspaces who has a valid `last_workspace_id` is taken directly to that workspace without the chooser.
**Independent Test**: Create a user with 2+ workspaces and a valid `last_workspace_id`, hit `/admin`, verify direct entry.
### Implementation for User Story 2
- [X] T009 [US2] Add step 6 to `EnsureWorkspaceSelected` middleware: `last_workspace_id` auto-resume with membership validation and audit logging in `app/Http/Middleware/EnsureWorkspaceSelected.php`
- [X] T010 [US2] Write test `it_auto_resumes_to_last_used_workspace_when_membership_valid` — verify direct redirect via last_workspace_id in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [X] T011 [US2] Write test `it_emits_audit_event_on_auto_selection_last_used` — verify `workspace.auto_selected` audit log with reason `last_used` in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [X] T012 [US2] Write test `it_falls_back_to_chooser_when_multiple_workspaces_and_no_last_used` — verify redirect to chooser when no default in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
**Checkpoint**: Multi-workspace users with a valid last-used preference bypass the chooser. Both P1 auto-resume paths (single + last-used) are functional.
---
## Phase 5: User Story 3 — Chooser Fallback: Multiple Workspaces, No Default (Priority: P1)
**Goal**: A user with multiple workspaces and no valid `last_workspace_id` sees the chooser with enterprise metadata (name, role badge, tenant count).
**Independent Test**: Create a user with 3 workspaces (varying roles, tenant counts), clear `last_workspace_id`, visit `/admin`, verify chooser renders with metadata.
### Implementation for User Story 3
- [X] T013 [US3] Refactor `ChooseWorkspace::getWorkspaces()` to add `withCount('tenants')` and load membership roles keyed by workspace_id; expose `$this->workspaceRoles` for Blade in `app/Filament/Pages/ChooseWorkspace.php`
- [X] T014 [US3] Remove "Create workspace" header action from `ChooseWorkspace` page (FR-006) in `app/Filament/Pages/ChooseWorkspace.php`
- [X] T015 [US3] Update Blade template: add role badge (`<x-filament::badge>` with color mapping for owner/admin/member), tenant count display, "Manage workspaces" link (gated by `Capabilities::WORKSPACE_MANAGE`), updated empty state copy per spec terminology in `resources/views/filament/pages/choose-workspace.blade.php`
- [X] T016 [US3] Write test `it_only_lists_workspaces_user_is_member_of` — create workspaces user is and isn't a member of, verify only member workspaces shown in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [X] T017 [US3] Write test `it_shows_name_role_and_tenants_count_per_workspace` — verify metadata rendered in chooser cards in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [X] T018 [US3] Write test `it_shows_empty_state_when_no_memberships` — verify "You don't have access to any workspace yet." message in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [X] T019 [US3] Write test `it_hides_manage_link_without_workspace_manage_capability` and `it_shows_manage_link_with_workspace_manage_capability` — positive + negative authorization in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [X] T020 [US3] Write test `it_has_no_n_plus_1_queries_in_chooser` — assert query count with 5+ workspaces in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
**Checkpoint**: Chooser page displays enterprise metadata. All three P1 stories are functional — auto-resume (single + last-used) and chooser fallback with metadata.
---
## Phase 6: User Story 4 — Stale Session / Revoked Membership (Priority: P2)
**Goal**: A user whose workspace membership was revoked between sessions sees a clear warning and is redirected to the chooser.
**Independent Test**: Set session to a workspace, delete the membership, visit `/admin`, verify warning + chooser.
### Implementation for User Story 4
- [X] T021 [US4] Enhance middleware step 3: detect stale session (revoked membership or archived workspace), clear session, emit Filament `Notification::make()->danger()` with "Your access to {workspace_name} was removed." flash, redirect to chooser in `app/Http/Middleware/EnsureWorkspaceSelected.php`
- [X] T022 [US4] Enhance middleware step 6 error path: detect stale `last_workspace_id` (revoked or archived), clear `last_workspace_id` on user record, emit flash warning, redirect to chooser in `app/Http/Middleware/EnsureWorkspaceSelected.php`
- [X] T023 [US4] Write test `it_clears_session_when_active_workspace_membership_revoked` — verify session cleared + warning notification + chooser redirect in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [X] T024 [US4] Write test `it_redirects_to_chooser_when_last_workspace_membership_revoked_and_shows_warning` — verify `last_workspace_id` cleared + warning + chooser, including archived workspace scenario (edge case EC2) in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [X] T025 [US4] Write test `it_handles_archived_workspace_in_session` — verify archived workspace treated as stale in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
**Checkpoint**: Stale/revoked membership detection is active. Users see clear warning notifications instead of broken states.
---
## Phase 7: User Story 5 — Manual Workspace Switch (Priority: P2)
**Goal**: A user can switch workspaces from within the app via the user menu, which takes them to the chooser.
**Independent Test**: As a logged-in user with active workspace, click "Switch workspace" in user menu, verify chooser loads with `?choose=1`.
### Implementation for User Story 5
- [X] T026 [US5] Add audit logging in `ChooseWorkspace::selectWorkspace()` — emit `workspace.selected` via `WorkspaceAuditLogger` with metadata `{method: "manual", reason: "chooser", prev_workspace_id}` in `app/Filament/Pages/ChooseWorkspace.php`
- [X] T027 [US5] Replace form POST with `wire:click="selectWorkspace({{ $workspace->id }})"` in chooser Blade template in `resources/views/filament/pages/choose-workspace.blade.php`
- [X] T028 [US5] Use `WorkspaceRedirectResolver` in `ChooseWorkspace::redirectAfterWorkspaceSelected()` for tenant-count branching in `app/Filament/Pages/ChooseWorkspace.php`
- [X] T029 [US5] Register "Switch workspace" user menu item via `->userMenuItems()` with `MenuItem::make()->url('/admin/choose-workspace?choose=1')->icon('heroicon-o-arrows-right-left')` and `->visible()` callback (>1 workspace membership) in `app/Providers/Filament/AdminPanelProvider.php`
- [X] T030 [US5] Write test `it_forces_chooser_with_choose_param` — verify `?choose=1` bypasses auto-resume, including the single-workspace sub-case (edge case EC3: forced chooser shown even with 1 membership) in `tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php`
- [X] T031 [US5] Write test `it_persists_last_used_workspace_on_manual_selection` and `it_emits_audit_event_on_manual_selection` — verify `last_workspace_id` update + `workspace.selected` audit log in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
- [X] T032 [US5] Write test `it_shows_switch_workspace_menu_when_multiple_workspaces` and `it_hides_switch_workspace_menu_when_single_workspace` in `tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php`
- [X] T033 [US5] Write test `it_rejects_non_member_workspace_selection_with_404` — verify deny-as-not-found for non-member attempt in `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`
**Checkpoint**: Manual switch flow complete — user menu entry, Livewire selection, audit logging, and 404 for non-members.
---
## Phase 8: User Story 6 — Audit Trail for Workspace Context Changes (Priority: P2)
**Goal**: Every workspace selection (auto or manual) produces an audit log entry with correct payloads for compliance.
**Independent Test**: Trigger auto-resume and manual selection, verify audit log entries with correct payloads.
### Implementation for User Story 6
- [X] T034 [US6] Write comprehensive audit payload verification test covering all four audit scenarios (auto/single_membership, auto/last_used, manual/chooser, manual/context_bar) with full metadata assertion (`method`, `reason`, `prev_workspace_id`, `resource_type`, `resource_id`) in `tests/Feature/Workspaces/WorkspaceAuditTrailTest.php`
- [X] T035 [US6] Write test `it_includes_prev_workspace_id_when_switching_from_active_workspace` — verify previous workspace context is captured in audit metadata in `tests/Feature/Workspaces/WorkspaceAuditTrailTest.php`
**Checkpoint**: All six user stories are implemented and tested. Audit trail is verified for compliance.
---
## Phase 9: Polish & Cross-Cutting Concerns
**Purpose**: Deduplicate remaining tenant-branching copies, full suite validation, formatting.
- [X] T036 [US6] Replace inline tenant-count branching in `SwitchWorkspaceController::__invoke()` with `WorkspaceRedirectResolver` AND add `WorkspaceAuditLogger::log()` for `workspace.selected` (method: `manual`, reason: `context_bar`) to satisfy FR-005 audit coverage for the context-bar switch path, in `app/Http/Controllers/SwitchWorkspaceController.php`
- [X] T037 Replace inline tenant-count branching in `/admin` route handler with `WorkspaceRedirectResolver` in `routes/web.php`
- [X] T038 Run full test suite via `vendor/bin/sail artisan test --compact` and verify no regressions
- [X] T039 Run Pint formatting via `vendor/bin/sail bin pint --dirty --format agent`
- [X] T040 Final commit and push to branch `107-workspace-chooser`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: N/A — project already initialized
- **Foundational (Phase 2)**: No dependencies — can start immediately. BLOCKS all user stories
- **US1 (Phase 3)**: Depends on Phase 2 (AuditActionId enum + WorkspaceRedirectResolver)
- **US2 (Phase 4)**: Depends on Phase 3 (middleware skeleton from US1)
- **US3 (Phase 5)**: Depends on Phase 2 only — can run in parallel with US1/US2
- **US4 (Phase 6)**: Depends on Phase 3 (middleware skeleton from US1)
- **US5 (Phase 7)**: Depends on Phase 5 (chooser page from US3) + Phase 3 (middleware `?choose=1`)
- **US6 (Phase 8)**: Depends on Phases 37 (all audit-emitting code must exist)
- **Polish (Phase 9)**: Depends on all user stories being complete
### User Story Dependencies
- **US1 (P1)**: After Foundational → provides middleware skeleton for US2 + US4
- **US2 (P1)**: After US1 → extends middleware with step 6
- **US3 (P1)**: After Foundational → independent from US1/US2 (different files)
- **US4 (P2)**: After US1 → enhances middleware error paths
- **US5 (P2)**: After US3 (chooser page) + US1 (middleware ?choose=1)
- **US6 (P2)**: After US1US5 → verifies audit payloads across all paths
### Within Each User Story
- Implementation before tests (refactoring existing code — not greenfield TDD)
- Core changes before edge-case handling
- Story complete before moving to next priority
### Parallel Opportunities
- **Phase 2**: T001 and T002 can run in parallel (different files)
- **After Phase 2**: US3 (chooser page) can start in parallel with US1 (middleware)
- **After Phase 3**: US2 and US4 can start in parallel (US4 enhances middleware error paths, US2 adds step 6)
- **Tests within same file**: Sequential (same file), but different test files can run in parallel
---
## Parallel Example: After Foundational
```
# Developer A: Middleware track (US1 → US2 → US4)
T004 Refactor EnsureWorkspaceSelected (US1)
T009 Add step 6 to middleware (US2)
T021T022 Enhance error paths (US4)
# Developer B: Chooser page track (US3 → US5)
T013T015 Upgrade ChooseWorkspace page + Blade (US3)
T026T029 Add audit + wire:click + user menu (US5)
# Both tracks converge at:
T034T035 Audit trail verification (US6)
T036T040 Polish & cross-cutting
```
---
## Implementation Strategy
### MVP First (US1 Only)
1. Complete Phase 2: Foundational (T001T003)
2. Complete Phase 3: US1 — Single workspace auto-resume (T004T008)
3. **STOP and VALIDATE**: Single-workspace users bypass chooser, audit logged
4. Deploy/demo if ready → immediate UX improvement for majority of users
### Incremental Delivery
1. Foundation (T001T003) → Core infrastructure ready
2. US1 (T004T008) → Single workspace auto-resume → **MVP!**
3. US2 (T009T012) → Last-used auto-resume → Multi-workspace friction reduced
4. US3 (T013T020) → Enterprise metadata in chooser → Better selection UX
5. US4 (T021T025) → Stale session handling → Governance safety net
6. US5 (T026T033) → Manual switch via user menu → Full switch flow
7. US6 (T034T035) → Audit verification → Compliance confidence
8. Polish (T036T040) → DRY codebase, full suite green
Each story adds value without breaking previous stories.
---
## Summary
| Metric | Value |
|--------|-------|
| **Total tasks** | 40 |
| **Phase 2 (Foundational)** | 3 tasks |
| **US1 (Auto-resume single)** | 5 tasks |
| **US2 (Auto-resume last-used)** | 4 tasks |
| **US3 (Chooser metadata)** | 8 tasks |
| **US4 (Stale session)** | 5 tasks |
| **US5 (Manual switch)** | 8 tasks |
| **US6 (Audit verification)** | 2 tasks |
| **Polish** | 5 tasks |
| **Parallel opportunities** | 2 independent tracks (middleware + chooser page) after foundation |
| **MVP scope** | Foundation + US1 (8 tasks) |
| **New files** | 6 (1 service + 5 test files) |
| **Modified files** | 6 (middleware, page, blade, enum, provider, controller + routes) |
---
## Notes
- All tasks reference exact file paths from plan.md project structure
- Audit logging uses direct `WorkspaceAuditLogger::log()` calls (decision R2)
- Middleware is refactored in-place (decision R1) — alias `ensure-workspace-selected` unchanged
- Chooser migrates to `wire:click` (decision R9) — `SwitchWorkspaceController` retained for context-bar
- Flash warnings use Filament `Notification::make()->danger()` (decision R10)
- Role badge uses simple color mapping, exempt from BadgeCatalog (decision R7)
- `WorkspaceRedirectResolver` deduplicates 4 copies of tenant-count branching (decision R4)