# Reference example: KACP-22813 Firm - Routes and page (dev review, verbatim) This is a real Developer Review Instructions field from a reviewed story, rendered to markdown. It is the calibration target for structure, depth, and voice. Note it comes from the APSCA portal repo, so its file paths and stack idioms are that repo's, only the anatomy is the standard. ## 1. Objective Add the Firm Team page shell at `/firm/team` by reusing the established full-width Firm Tasks page composition. Register Team in the Firm navigation and apply the existing Firm Membership CASL permission consistently to both the menu item and direct route access. ## 2. Current State - The Firm portal layout already authenticates the user, resolves their Firm Membership, renders the Firm sidebar, and protects the Firm portal as a whole. - `src/app/[locale]/firm/tasks/layout.tsx` already demonstrates the Firm data-grid page composition: a shared `PageHeader` followed by `MainContent` using `layoutWidth="dataGrid"`. - `src/app/[locale]/firm/tasks/page.tsx` provides the established responsive `Stack` and full-width `Box` layout for a Firm table or grid. - `src/app/[locale]/firm/tasks/_components/tasks-header.tsx` provides the closest Firm header implementation using the shared `PageHeader`. - `getFirmMenuConfig()` already registers Firm Management menu items. `createSidebarMenu()` applies CASL to each configured `subject` and renders denied items in the existing locked state. - `firmPortal.can()` already exposes a server-side permission check without requiring page code to inspect Firm roles. - No `/firm/team` route or Team menu item currently exists. - The currently implemented `FirmMembership` read rule permits every Firm portal membership. The role-aware Firm Administrator, Team Member, and Contact rules are owned by the intended review for Database, CASL policies, etc. (KACP-22817) and must be available for the required access behavior to work. ## 3. Desired State The Firm sidebar displays Team under Firm Management. - Firm Administrators and Team Members receive an enabled Team menu item and can open `/firm/team`. - Contacts see the Team menu item in the existing locked state. - A Contact who enters `/firm/team` directly is redirected to the localized Firm dashboard. The page follows the existing portal presentation: - A shared Firm page header displays the title **Team** and subtitle **Manage who can access your account and who APSCA contacts.** - The content uses the same data-grid width as Firm Tasks. - One full-width Team content surface provides the insertion point for the grid delivered by Firm - Team Members Grid (KACP-22814). The menu and page use the same `read`, `FirmMembership` CASL capability. The page does not inspect roles or duplicate authorization policy. ## 4. Scope ### In scope - Add the `/firm/team` server page. - Add the Team page header using the shared `PageHeader` and the Firm Tasks header pattern. - Compose the page with `MainContent`, `Stack`, and `Box` using the existing data-grid width layout. - Add Team under Firm Management in the Firm menu configuration. - Use `FirmMembership` as the menu item's CASL subject. - Check the same Firm Membership read capability in the page before rendering. - Redirect denied direct-route access to the localized Firm dashboard. - Add focused route and navigation authorization coverage. ### Developer acceptance criteria 1. The Team page remains a server component and resolves authorization before rendering protected content. 2. The page uses `firmPortal.can('read', 'FirmMembership')`; it does not import the CASL engine or inspect `FirmContactRole`. 3. The menu item and route guard both use the `FirmMembership` subject, preventing navigation and direct access from drifting apart. 4. A denied direct request redirects through `localePath('/firm/', locale)` so the current locale is preserved. 5. Firm portal authentication and Firm Membership resolution remain owned by the existing Firm layout. 6. The Team content uses `layoutWidth="dataGrid"` and a full-width `Box` with `flex: 1` and `minWidth: 0`, matching Firm Tasks. 7. The header uses the shared `PageHeader` with the same data-grid width, preserving its semantic heading markup and responsive spacing. 8. The page shell introduces no grid query, client state, mutation, or role-specific rendering. 9. Focused tests demonstrate the allowed page render, denied redirect, enabled authorized menu item, and locked unauthorized menu item. 10. Verification uses the role-aware CASL policy from KACP-22817 to confirm Firm Administrator and Team Member access and Contact denial. ### Explicitly out of scope - Team grid rows, columns, data loading, empty states, pagination, search, sorting, or filtering. - Add Person, Edit Person, View Person, or any other Team interaction. - Add or edit controls. - Prisma schema or migration changes. - Defining or duplicating the Firm Administrator, Team Member, or Contact CASL rules owned by KACP-22817. - Firm Membership commands or mutations. - Clerk identity or invitation behavior. - Communications, templates, or notification behavior. ### Estimate | Work | Hours | | --- | --- | | Team route, header, and data-grid-width page shell | 0.75 | | Firm menu item, CASL route check, and localized redirect | 0.5 | | Focused tests and verification | 0.75 | | **Total** | **2** | Recommended Jira original estimate: **2 engineering hours**. ## 5. Suggested Implementation ### Code paths and intended updates | Code path | Brief intended update | | --- | --- | | `src/app/[locale]/firm/team/page.tsx` | Add the server page. Resolve `params` and `firmPortal.can('read', 'FirmMembership')`, redirect denied users to the localized Firm dashboard, and compose the Team header plus the data-grid-width `MainContent`, responsive `Stack`, and full-width `Box` used by Firm Tasks. Reserve the content surface for the later grid story. | | `src/app/[locale]/firm/team/_components/team-header.tsx` | Copy the small Firm Tasks header wrapper, rename it for Team, and render the required title and subtitle through the shared `PageHeader` with `layoutWidth="dataGrid"`. Do not introduce a new generic header abstraction. | | `src/app/[locale]/firm/_components/menu-config.tsx` | Add Team under Firm Management, linking to the localized `/firm/team/` route, using the existing `Users` icon and `subject: 'FirmMembership'` so the shared menu authorization renders the locked Contact state. | | `src/app/[locale]/firm/team/page.test.tsx` | Add focused server-page coverage for authorized rendering and denied localized redirect. | | `src/app/[locale]/firm/_components/menu-config.test.tsx` | Confirm the Team item is registered under Firm Management and that the shared authorization transform enables it when `FirmMembership` read is allowed and locks it when denied. Do not test role names in the menu configuration. | The intended server-page authorization shape is: ```tsx const [{ locale }, canViewTeam] = await Promise.all([ params, firmPortal.can('read', 'FirmMembership'), ]); if (!canViewTeam) { redirect(localePath('/firm/', locale)); } ``` The page should then follow the established composition: ```tsx <> {/* KACP-22814 supplies the Team grid. */} ``` Do not add a client-side role check, new portal service, or grid read model. The shared `PageHeader`, `MainContent`, Firm Portal façade, and menu authorization already provide the required seams. ## 6. Happy Path 1. An authenticated Firm Administrator or Team Member opens the Firm portal. 2. The Firm layout resolves their current Firm Membership and builds the Firm CASL ability. 3. The sidebar applies that ability to the Team menu item's `FirmMembership` subject and renders the item as enabled. 4. The user opens Team and reaches the localized `/firm/team` route. 5. The server page checks the same Firm Membership read capability. 6. The page renders the Team header and full-width data-grid layout. 7. The Team content surface is ready for KACP-22814 to supply the grid without changing the route, header, navigation, width, or authorization composition. ## 7. Edge Cases to Consider ### Provided - A Team Member can view the Team page but cannot edit it. - A Contact sees the Team menu item in a locked state. - A Contact who enters the route directly is redirected to the Firm dashboard. - Grid content and interactions belong to a separate story. ### Added during dev review - The route must preserve the current locale when redirecting a denied user. - A user without a valid Firm Membership remains rejected by the existing Firm layout before the Team page is rendered. - The menu and direct route must not use different CASL subjects or independent role checks. - KACP-22817 must replace the current broad Firm Membership read grant before Contact denial can be verified correctly. - The header and main content must both use the data-grid width so their horizontal alignment remains consistent. - The table/grid wrapper must retain `minWidth: 0` and `width: '100%'` so wide content does not break the portal layout. - The empty shell must not invent temporary grid data, controls, or client state that KACP-22814 would later remove.