CumulativeFieldsSection.tsx4.8 KBView on GitHub /**
* CumulativeFieldsSection — pick which roll-up tiles a canvas shows.
*
* Lives inside the Fields popover next to column selection, because that is the same
* question in a different direction: Fields chooses what each ROW shows, this chooses what
* the WHOLE SET shows. Keeping them in one place means there is one answer to "how do I
* change what this view displays".
*
* Writes `summaries` on the canvas viewConfig; the bar itself renders nothing when the list
* is empty, so unchecking everything is the same as turning it off.
*/
import type {
CanvasSummaryMetric,
CanvasSummarySpec,
ConversationViewConfig,
} from '@/modules/canvas/types/canvas-types';
import { Checkbox } from '@/components/ui/checkbox';
import { useCedarStore } from '@/modules/store';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useState } from 'react';
/**
* The offerable tiles, in the order HubSpot lists them — the bar gets compared against
* theirs, so matching the order costs nothing and removes a point of confusion.
*
* `showAveragePerDeal` mirrors HubSpot too: the three amount tiles carry the sub-line, the
* summary tiles do not.
*/
const AVAILABLE: { id: string; label: string; metric: CanvasSummaryMetric; avg?: boolean }[] = [
{ id: 'total', label: 'Total deal amount', metric: { kind: 'totalDealAmount' }, avg: true },
{ id: 'weighted', label: 'Weighted deal amount', metric: { kind: 'weightedDealAmount' }, avg: true },
{ id: 'open', label: 'Open deal amount', metric: { kind: 'openDealAmount' }, avg: true },
{ id: 'closed', label: 'Closed deal amount', metric: { kind: 'closedDealAmount' } },
{ id: 'new', label: 'New deal amount', metric: { kind: 'newDealAmount' } },
{ id: 'age', label: 'Average deal age', metric: { kind: 'averageDealAge' } },
{ id: 'count', label: 'Deal count', metric: { kind: 'count' } },
];
interface CumulativeFieldsSectionProps {
canvasId: string;
}
export function CumulativeFieldsSection({ canvasId }: CumulativeFieldsSectionProps) {
const summaries = useCedarStore((state) => {
const c = state.canvasesById[canvasId];
return (c?.viewConfig as ConversationViewConfig | undefined)?.summaries;
});
const updateCanvasViewConfig = useCedarStore((state) => state.updateCanvasViewConfig);
const saveCanvasViewConfig = useCedarStore((state) => state.saveCanvasViewConfig);
const enabled = new Set((summaries ?? []).map((s) => s.id));
const toggle = (id: string) => {
const current = (useCedarStore.getState().canvasesById[canvasId]?.viewConfig ??
{}) as ConversationViewConfig;
const existing = current.summaries ?? [];
const isOn = existing.some((s) => s.id === id);
// Rebuild from AVAILABLE rather than push/splice so the tiles always come out in the
// canonical order no matter what sequence they were ticked in.
const nextIds = new Set(existing.map((s) => s.id));
if (isOn) nextIds.delete(id);
else nextIds.add(id);
const next: CanvasSummarySpec[] = AVAILABLE.filter((a) => nextIds.has(a.id)).map((a) => ({
id: a.id,
metric: a.metric,
...(a.avg ? { showAveragePerDeal: true } : {}),
}));
updateCanvasViewConfig(canvasId, { ...current, summaries: next });
void saveCanvasViewConfig(canvasId);
};
// Collapsed by default: the tile set is a set-once decision, while the column list beside
// it gets touched constantly. Leaving seven checkboxes permanently expanded pushed the
// presets — the thing people actually reach for — below the fold.
const [open, setOpen] = useState(false);
const activeCount = enabled.size;
return (
<div className="border-b border-border/40">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left hover:bg-muted/50"
aria-expanded={open}
>
{open ? (
<ChevronDown className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
) : (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
)}
<span className="flex-1 select-none text-sm">Cumulative</span>
{activeCount > 0 && (
<span className="bg-muted-foreground/20 rounded-full px-1.5 py-0.5 text-xs font-medium leading-none">
{activeCount}
</span>
)}
</button>
{open &&
AVAILABLE.map((item) => (
<div
key=[redacted]
className="flex cursor-pointer items-center gap-2.5 py-1.5 pl-8 pr-3 hover:bg-muted/50"
onClick={() => toggle(item.id)}
>
<Checkbox checked={enabled.has(item.id)} tabIndex={-1} className="pointer-events-none" />
<span className="flex-1 select-none text-sm">{item.label}</span>
</div>
))}
</div>
);
}