--- phase: 04-marketplace-portal-navigation plan: 02 type: execute wave: 2 depends_on: ["04-01"] files_modified: - apps/web/src/app/(portal)/marketplace/page.tsx - apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx - apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx - apps/web/src/app/(portal)/marketplace/components/StatusFilter.tsx - apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx - apps/web/src/app/(portal)/marketplace/components/ActivationDialog.tsx - apps/web/src/app/(portal)/marketplace/components/Toast.tsx - apps/web/src/app/(portal)/marketplace/[slug]/page.tsx - apps/web/src/app/(portal)/marketplace/marketplace-filters.test.tsx autonomous: true requirements: [MRKT-01, MRKT-02, MRKT-04] must_haves: truths: - "User can type in the marketplace search box and the card grid live-filters by module name and description (debounced)" - "User can filter the grid by category chip and by status tab (Alle / Aktiviert / Verfuegbar)" - "Super-Admin sees a tenant-context dropdown; switching tenant re-fetches activation status for that tenant and activation targets that tenant (D-04)" - "Deactivating a module shows a confirmation dialog before the deactivate call; activation shows a success toast (no dialog)" - "User can open a module detail page at /marketplace/[slug] showing the full description, status, and an activation button" artifacts: - path: "apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx" provides: "Debounced (300ms) search input with clear button" min_lines: 20 - path: "apps/web/src/app/(portal)/marketplace/components/StatusFilter.tsx" provides: "Tab bar: Alle / Aktiviert / Verfuegbar with counts" min_lines: 20 - path: "apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx" provides: "Horizontal category chip row including 'Alle'" min_lines: 20 - path: "apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx" provides: "Super-Admin-only tenant dropdown writing selectedTenantId to marketplace-store (D-04)" contains: "SUPER_ADMIN" - path: "apps/web/src/app/(portal)/marketplace/components/ActivationDialog.tsx" provides: "Deactivation confirmation modal with focus trap" min_lines: 20 - path: "apps/web/src/app/(portal)/marketplace/components/Toast.tsx" provides: "Hand-rolled toast system (Zustand store + toast container)" contains: "useToastStore" - path: "apps/web/src/app/(portal)/marketplace/[slug]/page.tsx" provides: "Compact module detail page" min_lines: 40 key_links: - from: "apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx" to: "/tenants" via: "fetch tenant list (SUPER_ADMIN)" pattern: "/tenants" - from: "apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx" to: "marketplace-store selectedTenantId" via: "setSelectedTenantId on change" pattern: "setSelectedTenantId" - from: "apps/web/src/app/(portal)/marketplace/page.tsx" to: "useMemo filtered modules" via: "search AND status AND category composition" pattern: "filter" --- Refine the marketplace slice from Plan 01 with the full filtering experience (live search, status tabs, category chips), the Super-Admin tenant-context selector (D-04), asymmetric activation UX (immediate activate + toast, confirm dialog for deactivate), and the compact module detail page at `/marketplace/[slug]`. Purpose: Completes MRKT-01 (full discovery with filters), MRKT-02 (Super-Admin per-tenant activation via context switch, D-04/D-05), and MRKT-04 (category filtering). Honors all Claude's-Discretion decisions locked in 04-UI-SPEC. Output: Search/CategoryFilter/StatusFilter/TenantContextSelector/ActivationDialog/Toast components, the detail page, and the filtered marketplace page. ## Phase Goal **As a** Tessera admin (or Super-Admin), **I want to** search, filter, and inspect modules — and as a Super-Admin choose which tenant I am managing — **so that** I can confidently activate the right modules for the right tenant. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/phases/04-marketplace-portal-navigation/04-CONTEXT.md @.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md @.planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md @.planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md @.planning/phases/04-marketplace-portal-navigation/04-01-SUMMARY.md Task 1: Build filter components and Toast, wire client-side filtering into the marketplace page - apps/web/src/app/(portal)/marketplace/page.tsx (created Plan 01 — extend with filter state + useMemo) - apps/web/src/lib/stores/marketplace-store.ts (created Plan 01 — selectedTenantId, bumpSidebarRefresh) - apps/web/src/app/(portal)/admin/tenants/page.tsx (form input styling `flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm`; badge styling for chips) - .planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md (Interaction Contracts: Status Tabs, Category Chips, Search; Toast Notifications; Copywriting Contract) - .planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md (Pattern 1 client-side multi-dimension filter; Pattern 4 toast store) - apps/web/src/messages/de.json (marketplace namespace added in Plan 01) - Test 1: Given modules A and B, typing "A" in MarketplaceSearch filters the grid to only module A (after debounce) - Test 2: Selecting the "Aktiviert" status tab shows only modules present in the activation map - Test 3: Selecting a specific category chip shows only modules of that category; "Alle" shows all - Test 4: Search + status + category filters compose (AND) together - Test 5: When filters produce zero results, the page renders the filtered-empty heading (`filteredEmptyTitle`), distinct from the no-modules empty state Create `apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx`: controlled `` with 300ms debounce (simple setTimeout/clearTimeout, no lodash per 04-RESEARCH), placeholder from `marketplace.searchPlaceholder`, a clear (X) button visible when text present, calling an `onChange(value: string)` prop with the debounced value. Use input styling from tenants page. Create `apps/web/src/app/(portal)/marketplace/components/StatusFilter.tsx`: three tabs "Alle"/"Aktiviert"/"Verfuegbar" (labels from marketplace namespace), props `{ value: 'all'|'active'|'available'; counts: {all:number;active:number;available:number}; onChange }`. Active tab `text-foreground border-b-2 border-primary`, inactive `text-muted-foreground hover:text-foreground`, inline count badge. Create `apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx`: horizontal pill row, first chip "Alle" then one per unique category, props `{ categories: string[]; value: string; onChange }`. Selected `bg-primary text-primary-foreground`, unselected `bg-muted text-foreground hover:bg-muted/80`, `rounded-full px-3 py-1 text-xs font-medium`, container `overflow-x-auto`. Create `apps/web/src/app/(portal)/marketplace/components/Toast.tsx`: a Zustand `useToastStore` (per 04-RESEARCH Pattern 4 — toasts array, `addToast(type, message)` auto-removing after 4000ms via setTimeout, `removeToast`) AND a `ToastContainer` component rendering bottom-right stacked toasts (`bg-card border border-border shadow-lg rounded-lg p-4`, success green check / error red X, `motion-safe:` slide animation). Export both `useToastStore` and `ToastContainer`. Modify `apps/web/src/app/(portal)/marketplace/page.tsx`: add state `searchQuery`, `statusFilter` ('all'), `categoryFilter` ('all'). Compute `filteredModules` via `useMemo` composing all three dimensions (search matches name + localized description; status against activation Map; category against `mod.category`) exactly per 04-RESEARCH Pattern 1. Derive unique categories and per-status counts. Render ``, ``, `` above the grid; map `filteredModules` instead of all modules. Add a filtered-empty branch (`filteredEmptyTitle`/`filteredEmptyBody`) distinct from the no-modules empty state. Mount `` and call `addToast('success', t('toastActivated'))` / `addToast('error', t('toastActivateError'))` in toggleModule for the activate path. Create `apps/web/src/app/(portal)/marketplace/marketplace-filters.test.tsx` implementing the five tests in (use fake timers for debounce). cd apps/web && pnpm vitest run "src/app/(portal)/marketplace/marketplace-filters.test.tsx" 2>&1 | grep -qiE "passed" && echo FILTER_TESTS_OK - MarketplaceSearch.tsx contains a debounce (setTimeout/clearTimeout) and a clear button - StatusFilter.tsx renders three tabs and uses `border-b-2 border-primary` for the active tab - CategoryFilter.tsx includes an "Alle" chip and renders `rounded-full` - Toast.tsx exports `useToastStore` and `ToastContainer`; auto-dismiss uses 4000 - page.tsx contains `useMemo` composing search, status, and category filters - page.tsx renders a distinct filtered-empty branch using `filteredEmptyTitle` - All 5 filter tests pass; `pnpm type-check` exits 0 Search, status tabs, and category chips live-filter the grid (composing AND); zero-result filtering shows the filtered-empty state; activation fires a success toast; all 5 tests pass. Task 2: Add Super-Admin TenantContextSelector and deactivation confirmation dialog (D-04, D-05) - apps/web/src/app/(portal)/marketplace/page.tsx (extended in Task 1 — add selector + dialog wiring) - apps/web/src/lib/stores/marketplace-store.ts (selectedTenantId / setSelectedTenantId — created Plan 01) - apps/web/src/lib/stores/auth-store.ts (role: SUPER_ADMIN / ADMIN / USER) - apps/web/src/app/(portal)/admin/tenants/page.tsx (Tenant interface {id,name,slug,isActive,createdAt,userCount}; GET /tenants fetch; delete-confirm modal pattern with overlay bg-black/50, max-w-sm card, cancel/confirm buttons) - .planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md (Tenant Context Selector section; Deactivation flow + dialog copy; Accessibility focus management) - .planning/phases/04-marketplace-portal-navigation/04-CONTEXT.md (D-04 Super-Admin context switch, D-05 regular admin direct, deferred FEAT-V2-04 full impersonation) - Test 1: For a SUPER_ADMIN user, TenantContextSelector fetches /tenants and renders the tenant options; for a non-super-admin it renders nothing - Test 2: Changing the selector value calls setSelectedTenantId with the chosen tenant id - Test 3: Clicking deactivate on an active module opens ActivationDialog with the module name in the body (does NOT immediately call the API) - Test 4: Confirming the dialog calls the deactivate handler; cancelling closes the dialog with no API call Per D-04, build ONLY a lightweight tenant-context selector — NOT a full impersonation system (FEAT-V2-04 is deferred, out of scope). Create `apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx`: `'use client'`, render nothing unless `currentUser?.role === 'SUPER_ADMIN'` (from auth-store). Fetch `GET /tenants` with `credentials: 'include'` (Tenant interface per tenants page). Container `bg-muted/50 rounded-lg p-3 mb-4 border border-border`, label `tenantContextLabel` (12px semibold muted), native `