Files
pn-new-crm/src/components/dashboard/dashboard-shell.tsx
Matt 3f86baeb0f chore(ui): drop unused dashboard KPIs + soften membership wording
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>
2026-05-09 04:11:15 +02:00

116 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}