2026-05-12 14:50:58 +02:00
|
|
|
|
/**
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
|
* Dashboard widget registry - the single source of truth for which
|
2026-05-12 14:50:58 +02:00
|
|
|
|
* widgets exist, what they're called, where they live, and what they
|
|
|
|
|
|
* default to. The DashboardShell loops over this; the settings UI also
|
|
|
|
|
|
* loops over this. Adding a new widget = adding one entry here.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Widget visibility is persisted per-user in
|
|
|
|
|
|
* `user_profiles.preferences.dashboardWidgets` as `{ [id]: boolean }`.
|
|
|
|
|
|
* Missing entries default to `defaultVisible`, so a brand-new widget
|
|
|
|
|
|
* surfaces for existing users automatically.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import type { ReactNode } from 'react';
|
audit: Tier 1/3/6/7 batch — PII redaction, mobile safe-area, perf, build hardening
Tier 1.4: error_events.request_body_excerpt sanitizer now redacts
GDPR-relevant fields (email, phone, dob, address, fullName, firstName,
lastName, postcode, nationalId, etc.) on top of the existing
credential list. A 5xx in /api/v1/clients no longer lands full client
PII in the super-admin inspector.
Tier 3.10: ScanShell <main> now adds pb-[max(1.5rem, env(safe-area-
inset-bottom))]. Mobile-pwa audit caught the Save expense button sitting
flush against the iPhone 14/15 home indicator in standalone PWA mode.
Tier 6.2: dashboard widget-registry now dynamic-imports every
recharts-backed chart widget (berth status, lead source, occupancy
timeline, pipeline funnel, revenue breakdown, source conversion).
~80-150KB initial-bundle savings when reps have charts disabled.
ssr:false because recharts needs window.
Tier 6.3: DataTable wraps the assembled columns in useMemo keyed on
(columns, hasBulkActions). TanStack docs explicitly warn that
rebuilding columns every render resets the table's internal state.
Tier 7.1: Added .dockerignore (was missing — 7.6 GB context with
.env reachable via COPY . .). Excludes git, env files, node_modules,
build artefacts, IDE config, test artefacts, audit docs.
Tier 7.4: Dockerfile.dev now runs as the node user (uid 1000) — was
root. Working dir moves to /home/node/app.
Tier 7.5: docker-compose.prod.yml adds memory limits (2g postgres,
512m redis, 1g crm-app, 1g crm-worker) and json-file log rotation
(max-size, max-file) to every service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:18:35 +02:00
|
|
|
|
import dynamic from 'next/dynamic';
|
2026-05-12 14:50:58 +02:00
|
|
|
|
|
|
|
|
|
|
import { ActiveDealsTile } from './active-deals-tile';
|
|
|
|
|
|
import { ActivityFeed } from './activity-feed';
|
2026-05-25 03:40:37 +02:00
|
|
|
|
import { OnboardingTile } from './onboarding-tile';
|
2026-05-14 15:47:49 +02:00
|
|
|
|
import { BerthHeatWidget } from './berth-heat-widget';
|
2026-05-21 23:32:21 +02:00
|
|
|
|
import { ClientsByCountryWidget } from './clients-by-country-widget';
|
2026-05-12 14:50:58 +02:00
|
|
|
|
import { HotDealsCard } from './hot-deals-card';
|
|
|
|
|
|
import { PipelineValueTile } from './pipeline-value-tile';
|
|
|
|
|
|
import { WebsiteGlanceTile } from './website-glance-tile';
|
|
|
|
|
|
import { MyRemindersRail } from './my-reminders-rail';
|
|
|
|
|
|
import { AlertRail } from '@/components/alerts/alert-rail';
|
|
|
|
|
|
import type { DateRange } from '@/lib/analytics/range';
|
|
|
|
|
|
|
audit: Tier 1/3/6/7 batch — PII redaction, mobile safe-area, perf, build hardening
Tier 1.4: error_events.request_body_excerpt sanitizer now redacts
GDPR-relevant fields (email, phone, dob, address, fullName, firstName,
lastName, postcode, nationalId, etc.) on top of the existing
credential list. A 5xx in /api/v1/clients no longer lands full client
PII in the super-admin inspector.
Tier 3.10: ScanShell <main> now adds pb-[max(1.5rem, env(safe-area-
inset-bottom))]. Mobile-pwa audit caught the Save expense button sitting
flush against the iPhone 14/15 home indicator in standalone PWA mode.
Tier 6.2: dashboard widget-registry now dynamic-imports every
recharts-backed chart widget (berth status, lead source, occupancy
timeline, pipeline funnel, revenue breakdown, source conversion).
~80-150KB initial-bundle savings when reps have charts disabled.
ssr:false because recharts needs window.
Tier 6.3: DataTable wraps the assembled columns in useMemo keyed on
(columns, hasBulkActions). TanStack docs explicitly warn that
rebuilding columns every render resets the table's internal state.
Tier 7.1: Added .dockerignore (was missing — 7.6 GB context with
.env reachable via COPY . .). Excludes git, env files, node_modules,
build artefacts, IDE config, test artefacts, audit docs.
Tier 7.4: Dockerfile.dev now runs as the node user (uid 1000) — was
root. Working dir moves to /home/node/app.
Tier 7.5: docker-compose.prod.yml adds memory limits (2g postgres,
512m redis, 1g crm-app, 1g crm-worker) and json-file log rotation
(max-size, max-file) to every service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:18:35 +02:00
|
|
|
|
// Recharts-backed widgets are dynamic-imported so the recharts bundle
|
|
|
|
|
|
// (~80-150KB) doesn't ship on every dashboard load when the rep has
|
|
|
|
|
|
// disabled charts. perf-test-auditor HIGH H3 caught the static import.
|
|
|
|
|
|
// Each one gets a placeholder loading state matching its grid slot.
|
|
|
|
|
|
const ChartFallback = () => (
|
|
|
|
|
|
<div className="rounded-lg border bg-muted/30 p-8 text-center text-sm text-muted-foreground">
|
|
|
|
|
|
Loading chart…
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
const BerthStatusChart = dynamic(
|
|
|
|
|
|
() => import('./berth-status-chart').then((m) => ({ default: m.BerthStatusChart })),
|
|
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
const LeadSourceChart = dynamic(
|
|
|
|
|
|
() => import('./lead-source-chart').then((m) => ({ default: m.LeadSourceChart })),
|
|
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
const OccupancyTimelineChart = dynamic(
|
2026-05-12 17:19:24 +02:00
|
|
|
|
() => import('./occupancy-timeline-chart').then((m) => ({ default: m.OccupancyTimelineChart })),
|
audit: Tier 1/3/6/7 batch — PII redaction, mobile safe-area, perf, build hardening
Tier 1.4: error_events.request_body_excerpt sanitizer now redacts
GDPR-relevant fields (email, phone, dob, address, fullName, firstName,
lastName, postcode, nationalId, etc.) on top of the existing
credential list. A 5xx in /api/v1/clients no longer lands full client
PII in the super-admin inspector.
Tier 3.10: ScanShell <main> now adds pb-[max(1.5rem, env(safe-area-
inset-bottom))]. Mobile-pwa audit caught the Save expense button sitting
flush against the iPhone 14/15 home indicator in standalone PWA mode.
Tier 6.2: dashboard widget-registry now dynamic-imports every
recharts-backed chart widget (berth status, lead source, occupancy
timeline, pipeline funnel, revenue breakdown, source conversion).
~80-150KB initial-bundle savings when reps have charts disabled.
ssr:false because recharts needs window.
Tier 6.3: DataTable wraps the assembled columns in useMemo keyed on
(columns, hasBulkActions). TanStack docs explicitly warn that
rebuilding columns every render resets the table's internal state.
Tier 7.1: Added .dockerignore (was missing — 7.6 GB context with
.env reachable via COPY . .). Excludes git, env files, node_modules,
build artefacts, IDE config, test artefacts, audit docs.
Tier 7.4: Dockerfile.dev now runs as the node user (uid 1000) — was
root. Working dir moves to /home/node/app.
Tier 7.5: docker-compose.prod.yml adds memory limits (2g postgres,
512m redis, 1g crm-app, 1g crm-worker) and json-file log rotation
(max-size, max-file) to every service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:18:35 +02:00
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
const PipelineFunnelChart = dynamic(
|
|
|
|
|
|
() => import('./pipeline-funnel-chart').then((m) => ({ default: m.PipelineFunnelChart })),
|
|
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
const SourceConversionChart = dynamic(
|
|
|
|
|
|
() => import('./source-conversion-chart').then((m) => ({ default: m.SourceConversionChart })),
|
|
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
2026-05-25 15:34:43 +02:00
|
|
|
|
const TenancyOccupancyHeatmapWidget = dynamic(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
import('./tenancy-occupancy-heatmap').then((m) => ({
|
|
|
|
|
|
default: m.TenancyOccupancyHeatmapWidget,
|
|
|
|
|
|
})),
|
|
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
const TenancyRenewalsAtRiskWidget = dynamic(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
import('./tenancy-renewals-at-risk').then((m) => ({
|
|
|
|
|
|
default: m.TenancyRenewalsAtRiskWidget,
|
|
|
|
|
|
})),
|
|
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
const TenancyRevenueForecastWidget = dynamic(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
import('./tenancy-revenue-forecast').then((m) => ({
|
|
|
|
|
|
default: m.TenancyRevenueForecastWidget,
|
|
|
|
|
|
})),
|
|
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
const TenancyByTenureTypeWidget = dynamic(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
import('./tenancy-by-tenure-type').then((m) => ({
|
|
|
|
|
|
default: m.TenancyByTenureTypeWidget,
|
|
|
|
|
|
})),
|
|
|
|
|
|
{ loading: ChartFallback, ssr: false },
|
|
|
|
|
|
);
|
audit: Tier 1/3/6/7 batch — PII redaction, mobile safe-area, perf, build hardening
Tier 1.4: error_events.request_body_excerpt sanitizer now redacts
GDPR-relevant fields (email, phone, dob, address, fullName, firstName,
lastName, postcode, nationalId, etc.) on top of the existing
credential list. A 5xx in /api/v1/clients no longer lands full client
PII in the super-admin inspector.
Tier 3.10: ScanShell <main> now adds pb-[max(1.5rem, env(safe-area-
inset-bottom))]. Mobile-pwa audit caught the Save expense button sitting
flush against the iPhone 14/15 home indicator in standalone PWA mode.
Tier 6.2: dashboard widget-registry now dynamic-imports every
recharts-backed chart widget (berth status, lead source, occupancy
timeline, pipeline funnel, revenue breakdown, source conversion).
~80-150KB initial-bundle savings when reps have charts disabled.
ssr:false because recharts needs window.
Tier 6.3: DataTable wraps the assembled columns in useMemo keyed on
(columns, hasBulkActions). TanStack docs explicitly warn that
rebuilding columns every render resets the table's internal state.
Tier 7.1: Added .dockerignore (was missing — 7.6 GB context with
.env reachable via COPY . .). Excludes git, env files, node_modules,
build artefacts, IDE config, test artefacts, audit docs.
Tier 7.4: Dockerfile.dev now runs as the node user (uid 1000) — was
root. Working dir moves to /home/node/app.
Tier 7.5: docker-compose.prod.yml adds memory limits (2g postgres,
512m redis, 1g crm-app, 1g crm-worker) and json-file log rotation
(max-size, max-file) to every service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:18:35 +02:00
|
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Where a widget lives on the dashboard. The shell renders three
|
|
|
|
|
|
* separate auto-fit regions so charts and rails don't compete for the
|
|
|
|
|
|
* same horizontal slots (preserves the visual hierarchy the team has
|
|
|
|
|
|
* gotten used to).
|
|
|
|
|
|
*
|
|
|
|
|
|
* - 'chart' → main analytics region (wider min-col)
|
|
|
|
|
|
* - 'rail' → side-rail region (narrower min-col)
|
|
|
|
|
|
* - 'feed' → full-width row underneath everything else
|
|
|
|
|
|
*/
|
|
|
|
|
|
export type WidgetGroup = 'chart' | 'rail' | 'feed';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* External integrations a widget can depend on. When the corresponding
|
|
|
|
|
|
* integration isn't connected for the active port, the widget is hidden
|
|
|
|
|
|
* from the picker AND from the rendered dashboard so reps can't toggle
|
|
|
|
|
|
* something that would render nothing. Wire new integrations through
|
|
|
|
|
|
* `useDashboardIntegrations()`.
|
|
|
|
|
|
*/
|
2026-05-25 15:34:43 +02:00
|
|
|
|
export type WidgetIntegration = 'umami' | 'documenso' | 'tenancies_module';
|
2026-05-12 14:50:58 +02:00
|
|
|
|
|
|
|
|
|
|
export interface DashboardWidget {
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
|
/** Stable persistence key. Don't rename - old preferences would break. */
|
2026-05-12 14:50:58 +02:00
|
|
|
|
id: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Renders the widget. Receives the active date-range so chart widgets
|
|
|
|
|
|
* can react; non-chart widgets simply ignore it. Keeping this a
|
|
|
|
|
|
* function instead of a `ComponentType` lets each widget pick its own
|
|
|
|
|
|
* prop shape without leaking the union into the registry type.
|
|
|
|
|
|
*/
|
|
|
|
|
|
render: (range: DateRange) => ReactNode;
|
|
|
|
|
|
group: WidgetGroup;
|
|
|
|
|
|
defaultVisible: boolean;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Some widgets self-gate (e.g. WebsiteGlanceTile renders null when
|
|
|
|
|
|
* Umami isn't configured). When `true`, the settings UI still shows
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
|
* the toggle so admins can enable it once the integration is wired -
|
2026-05-12 14:50:58 +02:00
|
|
|
|
* but the widget itself decides whether to render content.
|
|
|
|
|
|
*/
|
|
|
|
|
|
selfGates?: boolean;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Names the external integration this widget depends on. When the
|
|
|
|
|
|
* integration isn't connected for the active port, the widget is
|
|
|
|
|
|
* filtered out of both the picker and the rendered dashboard.
|
|
|
|
|
|
*/
|
|
|
|
|
|
requires?: WidgetIntegration;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const DASHBOARD_WIDGETS: readonly DashboardWidget[] = [
|
2026-05-25 03:40:37 +02:00
|
|
|
|
// ── Onboarding (rail, super_admin-only) ─────────────────────────────
|
|
|
|
|
|
// Self-collapses when the checklist hits 100% and self-hides for
|
|
|
|
|
|
// non-super-admin users so most reps never see this tile at all.
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'onboarding_checklist',
|
|
|
|
|
|
label: 'Setup checklist',
|
|
|
|
|
|
description:
|
|
|
|
|
|
'Progress + next-step nudge while the org is still going through Documenso / branding / users setup. Hidden once 100% complete.',
|
|
|
|
|
|
render: () => <OnboardingTile />,
|
|
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
|
// ── KPI tiles (rail) ────────────────────────────────────────────────
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
|
// Off by default - keep the existing dashboard layout unchanged for
|
2026-05-12 14:50:58 +02:00
|
|
|
|
// users on first paint after the upgrade; reps can flip them on from
|
|
|
|
|
|
// the Customize menu.
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'kpi_active_deals',
|
|
|
|
|
|
label: 'Active Deals',
|
|
|
|
|
|
description: 'Compact tile: count of in-flight interests.',
|
|
|
|
|
|
render: () => <ActiveDealsTile />,
|
|
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'kpi_pipeline_value',
|
|
|
|
|
|
label: 'Pipeline Value',
|
2026-05-20 15:56:11 +02:00
|
|
|
|
description:
|
|
|
|
|
|
'Gross + weighted forecast, broken down by pipeline stage so leadership can see what is near-close vs speculative.',
|
2026-06-03 18:27:56 +02:00
|
|
|
|
// Current-state snapshot: pipeline value = sum across ALL active deals,
|
|
|
|
|
|
// not "added in the selected window". Don't thread the range (UAT
|
|
|
|
|
|
// 2026-06-03 — windowing it dropped older deals + confused the headline).
|
|
|
|
|
|
render: () => <PipelineValueTile />,
|
2026-05-20 15:56:11 +02:00
|
|
|
|
// Lives in the chart grid (not the narrow rail) so the per-stage
|
|
|
|
|
|
// breakdown rows have room to breathe alongside the headline numbers,
|
|
|
|
|
|
// and the rail stays reserved for reminders / alerts / glance tiles.
|
|
|
|
|
|
group: 'chart',
|
2026-05-14 15:47:49 +02:00
|
|
|
|
defaultVisible: true,
|
2026-05-12 14:50:58 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── Charts (main area) ──────────────────────────────────────────────
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'pipeline_funnel',
|
|
|
|
|
|
label: 'Pipeline Funnel',
|
|
|
|
|
|
description: 'Interests by stage with conversion-rate vs open.',
|
|
|
|
|
|
render: (range) => <PipelineFunnelChart range={range} />,
|
|
|
|
|
|
group: 'chart',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'occupancy_timeline',
|
|
|
|
|
|
label: 'Occupancy Timeline',
|
|
|
|
|
|
description: 'Daily berth occupancy across the range.',
|
|
|
|
|
|
render: (range) => <OccupancyTimelineChart range={range} />,
|
|
|
|
|
|
group: 'chart',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'lead_source',
|
|
|
|
|
|
label: 'Lead Source Attribution',
|
|
|
|
|
|
description: 'Where new interests came from.',
|
|
|
|
|
|
render: (range) => <LeadSourceChart range={range} />,
|
|
|
|
|
|
group: 'chart',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'berth_status',
|
|
|
|
|
|
label: 'Berth Status',
|
|
|
|
|
|
description: 'Donut: available / under offer / sold split.',
|
|
|
|
|
|
render: () => <BerthStatusChart />,
|
|
|
|
|
|
group: 'chart',
|
|
|
|
|
|
defaultVisible: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'source_conversion',
|
|
|
|
|
|
label: 'Source Conversion',
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
|
description: 'Win rate per lead source - which channels deliver buyers, not just leads.',
|
2026-05-12 14:50:58 +02:00
|
|
|
|
render: () => <SourceConversionChart />,
|
|
|
|
|
|
group: 'chart',
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
|
// Flipped on 2026-05-14 - investor-facing conversion-funnel-by-source
|
2026-05-14 15:47:49 +02:00
|
|
|
|
// surface (PRE-DEPLOY-PLAN § 1.6.23). Reads inquiry → client linkage
|
|
|
|
|
|
// (clients.source_inquiry_id) added in migration 0065.
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'berth_heat',
|
2026-05-20 15:56:11 +02:00
|
|
|
|
label: 'Berth Demand',
|
|
|
|
|
|
description:
|
|
|
|
|
|
'Ranks berths by active interest. Surfaces the leading mooring with its runners-up.',
|
2026-05-14 15:47:49 +02:00
|
|
|
|
render: () => <BerthHeatWidget />,
|
|
|
|
|
|
group: 'chart',
|
|
|
|
|
|
defaultVisible: true,
|
2026-05-12 14:50:58 +02:00
|
|
|
|
},
|
2026-05-21 23:32:21 +02:00
|
|
|
|
{
|
|
|
|
|
|
id: 'clients_by_country',
|
|
|
|
|
|
label: 'Clients by country',
|
|
|
|
|
|
description:
|
|
|
|
|
|
'Per-country distribution of the active client book. Click a row to filter the clients list by country.',
|
|
|
|
|
|
render: () => <ClientsByCountryWidget />,
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
|
// Same rail-tile idiom as BerthHeatWidget + HotDealsCard - compact
|
2026-05-21 23:32:21 +02:00
|
|
|
|
// ranked list with mini-bars. Variant (a) per the master-doc design;
|
|
|
|
|
|
// the world-map variant lands alongside the recharts→ECharts pass.
|
|
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
2026-05-12 14:50:58 +02:00
|
|
|
|
{
|
|
|
|
|
|
id: 'website_analytics',
|
|
|
|
|
|
label: 'Website Analytics',
|
|
|
|
|
|
description: 'Quick glance at marketing site traffic. Requires Umami.',
|
2026-05-20 15:56:11 +02:00
|
|
|
|
render: (range) => <WebsiteGlanceTile range={range} />,
|
2026-05-12 14:50:58 +02:00
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
selfGates: true,
|
|
|
|
|
|
requires: 'umami',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'my_reminders',
|
|
|
|
|
|
label: 'My Reminders',
|
|
|
|
|
|
description: 'Your upcoming and overdue reminders.',
|
|
|
|
|
|
render: () => <MyRemindersRail />,
|
|
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'alerts',
|
|
|
|
|
|
label: 'Alerts',
|
|
|
|
|
|
description: 'System-flagged action items.',
|
|
|
|
|
|
render: () => <AlertRail />,
|
|
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'hot_deals',
|
|
|
|
|
|
label: 'Hot Deals',
|
|
|
|
|
|
description: 'Top 5 active interests closest to closing.',
|
|
|
|
|
|
render: () => <HotDealsCard />,
|
|
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'activity_feed',
|
|
|
|
|
|
label: 'Recent Activity',
|
|
|
|
|
|
description: 'Audit log of changes across the port.',
|
|
|
|
|
|
render: () => <ActivityFeed />,
|
|
|
|
|
|
group: 'feed',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
},
|
2026-05-25 15:34:43 +02:00
|
|
|
|
|
|
|
|
|
|
// ── Tenancies module widgets ───────────────────────────────────────────
|
|
|
|
|
|
// All four self-gate on `tenancies_module`. Hidden from picker + render
|
|
|
|
|
|
// when the module isn't enabled for the active port.
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'tenancy_occupancy_heatmap',
|
|
|
|
|
|
label: 'Occupancy heatmap',
|
|
|
|
|
|
description: 'Per-(berth area × month) occupancy across the year.',
|
|
|
|
|
|
render: () => <TenancyOccupancyHeatmapWidget />,
|
|
|
|
|
|
group: 'chart',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
selfGates: true,
|
|
|
|
|
|
requires: 'tenancies_module',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'tenancy_renewals_at_risk',
|
|
|
|
|
|
label: 'Renewals at risk',
|
|
|
|
|
|
description: 'Active tenancies expiring in the next 90 days without a successor.',
|
|
|
|
|
|
render: () => <TenancyRenewalsAtRiskWidget />,
|
|
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
selfGates: true,
|
|
|
|
|
|
requires: 'tenancies_module',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'tenancy_revenue_forecast',
|
|
|
|
|
|
label: 'Tenancy revenue forecast',
|
|
|
|
|
|
description: 'Berth value tied to tenancies ending each quarter, projected forward.',
|
|
|
|
|
|
render: () => <TenancyRevenueForecastWidget />,
|
|
|
|
|
|
group: 'chart',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
selfGates: true,
|
|
|
|
|
|
requires: 'tenancies_module',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'tenancy_by_tenure_type',
|
|
|
|
|
|
label: 'Tenancies by tenure type',
|
|
|
|
|
|
description: 'Active tenancy mix by tenure (permanent / fixed-term / seasonal …).',
|
|
|
|
|
|
render: () => <TenancyByTenureTypeWidget />,
|
|
|
|
|
|
group: 'rail',
|
|
|
|
|
|
defaultVisible: true,
|
|
|
|
|
|
selfGates: true,
|
|
|
|
|
|
requires: 'tenancies_module',
|
|
|
|
|
|
},
|
2026-05-12 14:50:58 +02:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/** Lookup helper so consumers don't have to scan the array. */
|
|
|
|
|
|
export const WIDGETS_BY_ID: Record<string, DashboardWidget> = Object.fromEntries(
|
|
|
|
|
|
DASHBOARD_WIDGETS.map((w) => [w.id, w]),
|
|
|
|
|
|
);
|