Documentation
Dashboard

Client Portal

The Client Hub is the relationship layer. Ten sub-tabs covering onboarding, strategy, deliverables, content, meeting notes and billing.

Naming clarification
The brief calls this the Client Hub in the new navigation. The codebase still has the older host component named ClientPortalTab.tsx — until Phase 4-12 progressively pull each sub-tab into its own dedicated section, the legacy host is reused with a forcedSubTab prop that hides its internal sub-nav and renders only the requested sub-section.

Entry

The host component is at src/components/portal/ClientPortalTab.tsx. It receives every metric prop from ClientDashboard (so the metric-driven sub-tabs can render without their own fetch logic) and on mount it pulls editable section data from /api/portal/[clientId]. The new SubNav at the top drives which sub-section is visible via the forcedSubTab prop.

Sub-tabs

Sub-tabSource of truthComponent
SummaryLive metrics (GHL + Google Ads + Meta + GBP + GSC + ext)SummarySection.tsx
Historical RevenueGoogle Sheets RFMS via /api/metrics/sheets-rfmsHistoricalRevenueSection.tsx
Snapshot & GoalsEditable JSONB in portal_data.snapshotSnapshotSection.tsx + SnapshotEditor
Pipeline OverviewLive GHL + Google Ads + Meta + GBPPipelineSection.tsx
ScorecardEditable JSONB in portal_data.scorecardScorecardSection.tsx + ScorecardEditor
90-Day Game PlanEditable JSONB in portal_data.gameplanGamePlanSection.tsx + GamePlanEditor
Journey & DeliverablesEditable JSONB in portal_data.deliverablesDeliverablesSection.tsx + DeliverablesEditor
KPI ReportingEditable JSONB in portal_data.kpi_reportingKpiReportingSection.tsx + KpiReportingEditor
Content & WebsiteEditable JSONB in portal_data.content_trackerContentTrackerSection.tsx + ContentTrackerEditor
DocumentsEditable JSONB in portal_data.documents (file URLs)DocumentsSection.tsx + DocumentsEditor

Editable section data shape

All editable sections live in one row in portal_data:

portal_data
├── id (uuid)
├── client_id (uuid, references clients)
├── snapshot (jsonb)         — PortalSnapshot
├── scorecard (jsonb)        — PortalScorecard
├── gameplan (jsonb)         — PortalGameplan
├── deliverables (jsonb)     — PortalDeliverables
├── kpi_reporting (jsonb)    — PortalKpiReporting
├── content_tracker (jsonb)  — PortalContentTracker
└── documents (jsonb)        — PortalDocuments

Each JSONB column has a TypeScript type defined in src/lib/types/portal.ts. The exact shape varies by section — Snapshot has goals + current state, Scorecard has scored pillars, Game Plan has 30/60/90 sections, etc.

How editing works (admin only)

  • Admin clicks the pencil icon in a sub-tab header.
  • setEditing('snapshot') opens the corresponding editor modal.
  • The editor is a dynamically imported component (SnapshotEditor etc.) that takes the current data, lets the admin edit fields, and calls onSave(newData).
  • The host calls handleSave(section, data) which PUTs to /api/portal/[clientId].
  • The route validates admin role server-side, validates the JSON against the section's schema, then upserts the column.
  • On success, the host re-fetches and closes the modal.
Editor lazy-load
Editors are imported via next/dynamic with ssr: false to avoid bloating the initial bundle for client users (who never see them). Don't change to static imports without considering the bundle impact.

Empty state

When a client has no portal_data row yet, the host falls back to EMPTY_PORTAL (defined in src/lib/types/portal.ts) so the read-only sections still render with placeholder copy. Saving any section creates the row.

Historical Revenue (RFMS)

Historical Revenue is the only sub-tab driven by Google Sheets. It expects a sheet ID stored at clients.sheets_rfms_id, with a specific column layout (date, revenue, units sold, etc.). The route that reads it is /api/metrics/sheets-rfms and the component is HistoricalRevenueSection.tsx.

This pattern exists because most flooring/contractor clients still keep their installed-revenue records in spreadsheets they own.