In traditional single-page architectures, creating a data management dashboard page frequently introduces severe maintenance overhead: frontend developers manually transcribe Swagger definitions into hand-crafted TypeScript interfaces and ad-hoc fetch functions. When backend models change, the mismatch often escapes undetected until a runtime white screen occurs in production. Worse, developers often stash roles and permissions in localStorage and perform client-only checks—violating the cardinal rule that the backend is the sole source of truth and opening security vulnerabilities.
BitzOrcas.Modern frontend architecture (located at frontend/apps/app) eliminates these pitfalls through strict contract-driven design and governed systems:
- Zero-Handwritten OpenAPI SDK (
@bitz/platform-sdk): Backend Minimal APIs compile down to OpenAPI 3.0 artifacts, from which a typed client and TypeScript DTOs are synchronized via a single script; - Tamper-Proof Credential Lifecycle: Web refresh tokens are managed via backend
HttpOnlycookies (WebRefreshCookieService), while access tokens reside purely in JavaScript memory—eliminating XSS theft and client privilege escalation; - Automated Metadata Hydration: Tables leverage
__metaprojections attached to API responses to resolve localized dictionary labels and related entity titles with zero additional N+1 queries; - Frozen Design System v1: All pages strictly consume
@bitz/componentsand@bitz/widgets, prohibiting arbitrary CSS variables or undocumented UI patterns.
This guide demonstrates building a production-ready Civil Litigation Matter Intake Dashboard (MatterIntakeDashboardPage) from route declaration and SDK synchronization to metadata hydration and fine-grained RBAC action gates.
Frontend Page Development Sequence
Step 1: Declare Route Metadata and Permission Boundaries
Register the new page in frontend/apps/app/src/routes.tsx. BitzOrcas uses declarative route configuration where business pages reside within the authenticated workspace shell (authenticated-workspace-shell), explicitly declaring navigation ownership and RBAC permissions.
import { Navigate } from 'react-router-dom';import type { RouteObject } from 'react-router-dom';import App from './App';import { RouteError } from './components/route-error';import { RouteGuard } from './components/route-guard';import { RouteLoading } from './components/route-loading';
export type AppPermission = string;export type AppSurface = 'tenant' | 'host' | 'shared';
export type AppRouteMeta = { /** Parent menu code for workspace shell highlight and context persistence */ navigationCode?: string; /** Stable guidance key for interactive product tours */ guidanceKey?: string; /** Granular RBAC action codes required to access this route */ permission?: AppPermission[]; /** Permission evaluation mode: 'all' requires every action, 'any' requires at least one */ permissionMode?: 'all' | 'any'; /** Whether the route requires an authenticated session */ requiresAuth?: boolean; /** Layout profile: 'identity' for standalone auth screens, 'workspace' for operational app */ layout?: 'identity' | 'workspace'; /** Operating surface: 'tenant' client, 'host' SaaS administration, or 'shared' */ surface?: AppSurface; /** Human-readable page title for browser tabs and breadcrumbs */ title?: string;};
export type AppRouteConfig = Omit<RouteObject, 'children' | 'lazy'> & { lazy?: RouteObject['lazy'] | string; meta?: AppRouteMeta; hide?: boolean; children?: AppRouteConfig[];};
export const routes: AppRouteConfig[] = [ { path: '/', element: <App />, errorElement: <RouteError />, HydrateFallback: RouteLoading, children: [ { element: <RouteGuard />, meta: { requiresAuth: true }, children: [ { // AppShell layout: renders sidebar, header tenant switcher, and active user state lazy: './components/authenticated-workspace-shell', children: [ // --- Legal Practice Management Sub-tree --- { path: 'legal/matters', lazy: async () => { const { MatterIntakeDashboardPage } = await import( './pages/legal/matters/matter-intake-dashboard-page' ); return { Component: MatterIntakeDashboardPage }; }, meta: { title: 'Matter Intake Dashboard', requiresAuth: true, surface: 'tenant', navigationCode: 'workspace.legal-matters', // Backend Authorization remains the sole source of truth permission: ['legal.matters.view'], guidanceKey: 'legal-matters-intake-dashboard', }, }, ], }, ], }, ], },];Step 2: Synchronize the OpenAPI SDK Contract
Whenever the backend introduces a new Minimal API endpoint (e.g. api/legal/matters), run the synchronization script:
# Export OpenAPI specification and regenerate @bitz/platform-sdk typed client./scripts/build/sync-openapi-sdk.shThis script reads artifacts/openapi/openapi-v1.json and updates frontend/packages/platform-sdk with full TypeScript definitions:
api.listLegalMatters(params)interface LegalMatterSummaryDtointerface LegalMatterFilterRequest
[!CAUTION] Strict Architecture Red Lines:
- Never manually edit generated code inside
packages/platform-sdk;- Never manually declare backend DTO
interfaceshapes in page code;- Never execute raw
fetch()oraxioscalls directly from React components. All network requests must flow throughusePlatform().api.
Step 3: Implement the Production-Grade Dashboard Page
Create frontend/apps/app/src/pages/legal/matters/matter-intake-dashboard-page.tsx. This implementation strictly follows Frozen Design System v1 and @bitz/widgets compound patterns:
import { useEffect, useMemo, useState, type JSX } from 'react';import { useQuery, useQueryClient } from '@tanstack/react-query';import { usePlatform, type LegalMatterSummaryDto } from '@bitz/platform-sdk';import { useT } from '@bitz/i18n';import { DataTableObjectCell, InlineNotice, OperationField, PermissionGate, QueryToolbar, ServerDataTable, StatusBadge, WorkspacePage, type DataTableColumn, type StatusTone,} from '@bitz/widgets';import { Button, Input, NativeSelect, NativeSelectOption } from '@bitz/components';import { MetadataListSearchPanel, readMetadataString, useMetadataListSearchPanel,} from '../../../components/metadata-list-search-panel';import { WorkspaceContextHeader } from '../../../components/workspace-context-header';import { useListQueryState } from '../../../utils/list-query-state';
/** Query key constant for TanStack Query caching and localized cache invalidation */const MATTERS_QUERY_KEY = ['legal', 'matters', 'dashboard'] as const;
/** Allowed matter status filter values */const MATTER_STATUS_OPTIONS = ['Draft', 'PendingReview', 'Active', 'Closed'] as const;
/** Parameter mapping configuration for metadata-driven search panel */const MATTER_SEARCH_PANEL_OPTIONS = { paramNames: { SearchText: 'q', Status: 'status', }, defaultPageSize: 20,} as const;
/** * Civil Litigation Matter Intake Dashboard Component */export function MatterIntakeDashboardPage(): JSX.Element { const { api, currentUser } = usePlatform(); const t = useT(); const queryClient = useQueryClient();
// 1. Pagination and URL query state synchronization const { state: listState, update: updateListState } = useListQueryState({ allowedStatuses: MATTER_STATUS_OPTIONS, });
// 2. Metadata-driven search panel hook const metadataSearch = useMetadataListSearchPanel( 'Legal.MatterSummaryList', MATTER_SEARCH_PANEL_OPTIONS, );
const activeKeyword = metadataSearch.panelEnabled ? readMetadataString(metadataSearch.values, 'SearchText') : listState.keyword; const activeStatus = metadataSearch.panelEnabled ? readMetadataString(metadataSearch.values, 'Status') : listState.status; const pageIndex = metadataSearch.panelEnabled ? metadataSearch.pageIndex : listState.pageIndex; const pageSize = metadataSearch.panelEnabled ? metadataSearch.pageSize : listState.pageSize;
const [keywordDraft, setKeywordDraft] = useState(activeKeyword); const [statusDraft, setStatusDraft] = useState(activeStatus); const [selectedMatterId, setSelectedMatterId] = useState<string | null>(null);
// 3. Current user permissions evaluation (read-only client reflection of backend claims) const permissions = useMemo(() => new Set(currentUser?.permissions ?? []), [currentUser]); const canCreate = permissions.has('legal.matters.create'); const canAdvanceStage = permissions.has('legal.matters.advance-stage');
useEffect(() => { setKeywordDraft(activeKeyword); setStatusDraft(activeStatus); }, [activeKeyword, activeStatus]);
// 4. Fetch paginated data using TanStack Query const mattersQuery = useQuery({ queryKey: [...MATTERS_QUERY_KEY, pageIndex, pageSize, activeKeyword, activeStatus], queryFn: () => api.listLegalMatters({ pageIndex, pageSize, searchText: activeKeyword.trim() || undefined, status: activeStatus || undefined, }), staleTime: 30_000, // 30-second stale time });
// 5. Map status string to visual status tone const resolveStatusTone = (status: string): StatusTone => { switch (status) { case 'Active': return 'success'; case 'PendingReview': return 'warning'; case 'Closed': return 'neutral'; default: return 'neutral'; } };
// 6. Strongly-typed column definitions with metadata hydration const columns: DataTableColumn<LegalMatterSummaryDto>[] = [ { id: 'matterCode', header: 'Tracking Code', cell: (row) => ( <span className="font-mono text-sm font-semibold tracking-tight text-neutral-900 dark:text-neutral-100"> {row.matterCode} </span> ), width: '180px', }, { id: 'title', header: 'Matter Title', cell: (row) => ( <div className="flex flex-col gap-0.5"> <span className="font-medium text-neutral-900 dark:text-neutral-100">{row.title}</span> <span className="text-xs text-neutral-500">{row.caseNumber || 'Unassigned Official Docket'}</span> </div> ), }, { id: 'client', header: 'Client / Retainer', cell: (row) => ( // Metadata Hydration: zero-latency lookup via __meta projection without N+1 queries <DataTableObjectCell primaryText={row.__meta?.clientName ?? row.clientId} secondaryText={row.__meta?.clientUnifiedSocialCreditCode} /> ), width: '220px', }, { id: 'claimAmount', header: 'Claim Amount', cell: (row) => ( <span className="font-mono text-sm text-right tabular-nums"> ¥ {row.claimAmount.toLocaleString('en-US', { minimumFractionDigits: 2 })} </span> ), align: 'right', width: '150px', }, { id: 'status', header: 'Status', cell: (row) => ( <StatusBadge tone={resolveStatusTone(row.status)} label={row.__meta?.statusDisplayText ?? row.status} /> ), width: '130px', }, { id: 'actions', header: 'Actions', cell: (row) => ( <OperationField> <Button variant="ghost" size="sm" onClick={() => setSelectedMatterId(row.id)} > View Dossier </Button> {/* Fine-grained RBAC action gate */} <PermissionGate permission="legal.matters.advance-stage"> <Button variant="outline" size="sm" disabled={row.status !== 'Active'} onClick={() => { // Open stage advancement modal dialog }} > Advance Stage </Button> </PermissionGate> </OperationField> ), width: '160px', }, ];
return ( <WorkspacePage> {/* Workspace Context Header */} <WorkspaceContextHeader title="Matter Intake Dashboard" description="Search litigation matters across legal entities and practice groups, conduct conflict checks, and track court milestones." actions={ <div className="flex items-center gap-3"> <Button variant="outline" onClick={() => queryClient.invalidateQueries({ queryKey: MATTERS_QUERY_KEY })} > Refresh </Button> {/* Declarative Create Button RBAC Gate */} <PermissionGate permission="legal.matters.create"> <Button variant="primary" onClick={() => { // Open create matter intake sheet }} > New Matter Intake </Button> </PermissionGate> </div> } />
{/* Query and Filter Toolbar */} <QueryToolbar search={ <Input placeholder="Search by title, docket code, or client..." value={keywordDraft} onChange={(e) => setKeywordDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { updateListState({ keyword: keywordDraft, pageIndex: 1 }); } }} /> } filters={ <NativeSelect value={statusDraft} onChange={(e) => { const val = e.target.value; setStatusDraft(val); updateListState({ status: val, pageIndex: 1 }); }} > <NativeSelectOption value="">All Statuses</NativeSelectOption> <NativeSelectOption value="Draft">Draft</NativeSelectOption> <NativeSelectOption value="PendingReview">Pending Review</NativeSelectOption> <NativeSelectOption value="Active">Active Litigation</NativeSelectOption> <NativeSelectOption value="Closed">Closed & Archived</NativeSelectOption> </NativeSelect> } />
{/* Inline Error Notice */} {mattersQuery.isError && ( <InlineNotice tone="critical" title="Data Loading Failed" message={mattersQuery.error instanceof Error ? mattersQuery.error.message : 'Network error occurred. Please retry.'} /> )}
{/* Server-Side Paginated Table */} <ServerDataTable rowKey="id" columns={columns} data={mattersQuery.data?.items ?? []} isLoading={mattersQuery.isLoading} pagination={{ pageIndex, pageSize, totalCount: mattersQuery.data?.totalCount ?? 0, onPageChange: (newPageIndex) => updateListState({ pageIndex: newPageIndex }), onPageSizeChange: (newPageSize) => updateListState({ pageSize: newPageSize, pageIndex: 1 }), }} /> </WorkspacePage> );}Step 4: Automated CI and Quality Gate Assertions
Before committing changes, execute the full suite of automated assertions:
# 1. Enforce strict lockfile immutabilityyarn install --immutable
# 2. Check peer dependency integrityyarn peer:check
# 3. Validate Prettier baseline formattingyarn format:check
# 4. Run workspace-wide ESLint static analysisyarn lint
# 5. Execute TypeScript strict type checking (0 errors)yarn typecheck
# 6. Build Web application and verify bundle budget gatesyarn workspace @bitz/app buildSummary
Following the BitzOrcas.Modern frontend engineering standard yields enterprise-level quality and reliability:
- Compile-Time Contract Integrity: Synchronized via
sync-openapi-sdk.sh, backend model changes immediately flag compile errors intsc, preventing runtime failures; - Defense in Depth: Eliminates insecure
localStoragepermission patterns; client-sidePermissionGateenhances user experience while backend pipelines strictly enforce authorization; - Automated Metadata Hydration: Consuming
__metaprojections viaServerDataTableresolves multi-lingual dictionary texts and foreign keys without extra API calls; - Design Uniformity: Composing standard
@bitz/widgetsand@bitz/componentsguarantees visual coherence and prevents rogue styles.