Remove the "Total Clients / Active Interests / Pipeline Value / Occupancy Rate" KPI grid from the dashboard — duplicated by the charts below and rarely scanned. Pipeline funnel, occupancy timeline, revenue breakdown, lead source charts and the activity feed remain. Rename the company-members dropdown action "End Membership" → "Remove from company" — matches how reps describe the action. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
|
||
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
||
import { usePortContext } from '@/providers/port-provider';
|
||
import { PageHeader } from '@/components/shared/page-header';
|
||
import { ActivityFeed } from './activity-feed';
|
||
import { DateRangePicker } from './date-range-picker';
|
||
import { PipelineFunnelChart } from './pipeline-funnel-chart';
|
||
import { OccupancyTimelineChart } from './occupancy-timeline-chart';
|
||
import { RevenueBreakdownChart } from './revenue-breakdown-chart';
|
||
import { LeadSourceChart } from './lead-source-chart';
|
||
import { MyRemindersRail } from './my-reminders-rail';
|
||
import { WebsiteGlanceTile } from './website-glance-tile';
|
||
import { WidgetErrorBoundary } from './widget-error-boundary';
|
||
import { AlertRail } from '@/components/alerts/alert-rail';
|
||
import { isCustomRange, type DateRange } from '@/lib/analytics/range';
|
||
|
||
const PRESET_LABELS: Record<'today' | '7d' | '30d' | '90d', string> = {
|
||
today: 'Today',
|
||
'7d': 'Last 7 days',
|
||
'30d': 'Last 30 days',
|
||
'90d': 'Last 90 days',
|
||
};
|
||
|
||
function rangeLabel(range: DateRange): string {
|
||
if (isCustomRange(range)) {
|
||
const fmt: Intl.DateTimeFormatOptions = {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
timeZone: 'UTC',
|
||
};
|
||
const from = new Date(`${range.from}T00:00:00.000Z`).toLocaleDateString('en-US', fmt);
|
||
const to = new Date(`${range.to}T00:00:00.000Z`).toLocaleDateString('en-US', fmt);
|
||
return `${from} – ${to}`;
|
||
}
|
||
return PRESET_LABELS[range];
|
||
}
|
||
|
||
export function DashboardShell() {
|
||
const [range, setRange] = useState<DateRange>('30d');
|
||
const { currentPort } = usePortContext();
|
||
const portName = currentPort?.name ?? 'this port';
|
||
|
||
// Use a partial query-key prefix (no range segment) for invalidations.
|
||
// Reading: "any cached analytics result, regardless of range, please
|
||
// refetch on this event." This avoids any chance that a custom-range
|
||
// object literal hashes differently than the one stored in the cache,
|
||
// and keeps the invalidation surface broad enough to refresh whichever
|
||
// range the user is currently looking at.
|
||
useRealtimeInvalidation({
|
||
'interest:stageChanged': [
|
||
['analytics', 'pipeline_funnel'],
|
||
['analytics', 'lead_source_attribution'],
|
||
['dashboard', 'kpis'],
|
||
],
|
||
'client:created': [['dashboard', 'kpis']],
|
||
'berth:statusChanged': [
|
||
['analytics', 'occupancy_timeline'],
|
||
['dashboard', 'kpis'],
|
||
],
|
||
});
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<PageHeader
|
||
title="Dashboard"
|
||
eyebrow="Overview"
|
||
description={`Live snapshot of ${portName} activity`}
|
||
kpiLine={<span>{rangeLabel(range)}</span>}
|
||
variant="gradient"
|
||
actions={<DateRangePicker value={range} onChange={setRange} />}
|
||
/>
|
||
|
||
{/* `items-start` is critical: without it, the right-column aside is
|
||
stretched to match the chart column's row height, which forces
|
||
MyRemindersRail (or any other child with `h-full`) to push later
|
||
children out of the aside's box and into the rows below where
|
||
ActivityFeed renders. */}
|
||
<div className="grid gap-4 grid-cols-1 items-start xl:grid-cols-[minmax(0,1fr)_320px]">
|
||
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2">
|
||
<WidgetErrorBoundary>
|
||
<PipelineFunnelChart range={range} />
|
||
</WidgetErrorBoundary>
|
||
<WidgetErrorBoundary>
|
||
<OccupancyTimelineChart range={range} />
|
||
</WidgetErrorBoundary>
|
||
<WidgetErrorBoundary>
|
||
<RevenueBreakdownChart range={range} />
|
||
</WidgetErrorBoundary>
|
||
<WidgetErrorBoundary>
|
||
<LeadSourceChart range={range} />
|
||
</WidgetErrorBoundary>
|
||
</div>
|
||
<aside className="min-w-0 space-y-4">
|
||
{/* Soft-fail tile linking to /website-analytics. Hidden if Umami
|
||
isn't configured for this port. */}
|
||
<WidgetErrorBoundary>
|
||
<WebsiteGlanceTile />
|
||
</WidgetErrorBoundary>
|
||
<WidgetErrorBoundary>
|
||
<MyRemindersRail />
|
||
</WidgetErrorBoundary>
|
||
<WidgetErrorBoundary>
|
||
<AlertRail />
|
||
</WidgetErrorBoundary>
|
||
</aside>
|
||
</div>
|
||
|
||
<ActivityFeed />
|
||
</div>
|
||
);
|
||
}
|