strategic-overview-layout.ts3.2 KBView on GitHub /**
* The Strategic Overview top row — the single definition of which native Cedar
* fields can sit in it, what each one is called, and how a stored layout is
* normalized before anything renders it.
*
* This lives here because two surfaces read it: the rendered Overview tab
* (`StrategicOverviewTab`) and the AOP Playbook's Overview config editor
* (`StrategicOverviewConfigEditor`). They used to carry their own copies of the
* cedar-field label map, which is how the editor came to show "Next step quality"
* over a cell the tab renders as "Stage".
*/
import type { StrategicOverviewCell, StrategicOverviewConfig } from '@/modules/crm/types';
/** Native Cedar fields (columns on the conversation) that can occupy a top-row cell. */
export const CEDAR_TOP_ROW_CELLS = [
{ fieldId: 'status', label: 'Stage' },
{ fieldId: 'nextStepDate', label: 'Next step' },
{ fieldId: 'lastContactedAt', label: 'Last touch' },
{ fieldId: 'dealValue', label: 'ACV' },
{ fieldId: 'risk', label: 'Risk' },
] as const;
export const CEDAR_CELL_IDS: ReadonlySet<string> = new Set(
CEDAR_TOP_ROW_CELLS.map((c) => c.fieldId),
);
export const CEDAR_CELL_LABELS: Readonly<Record<string, string>> = Object.fromEntries(
CEDAR_TOP_ROW_CELLS.map((c) => [c.fieldId, c.label]),
);
/**
* Custom fields that a stored layout may still name but that nothing renders any
* more — `normalizeLegacyLayout` rewrites or drops them, so no picker should offer
* them either.
*/
export const SUPERSEDED_CUSTOM_FIELD_IDS: ReadonlySet<string> = new Set(['next_step_quality']);
/** How many cells the top row holds. The row is a fixed grid; a sixth cell squeezes all six. */
export const TOP_ROW_SLOTS = 5;
/**
* The top row every AOP gets: Stage, Next step, Last touch, Forecast, ACV.
* `forecast` is a seeded custom field (see the server's STRATEGIC_OVERVIEW_FIELDS).
*/
export const DEFAULT_TOP_ROW: StrategicOverviewCell[] = [
{ fieldId: 'status', kind: 'cedar' },
{ fieldId: 'nextStepDate', kind: 'cedar' },
{ fieldId: 'lastContactedAt', kind: 'cedar' },
{ fieldId: 'forecast', kind: 'custom' },
{ fieldId: 'dealValue', kind: 'cedar' },
];
/**
* Rewrites a stored layout into what actually renders:
*
* - `next_step_quality` was replaced by the native `status` field. Old AOPs still
* carry it, so swap it for `status` (or drop it when `status` is already there).
* - A field that has since become native (e.g. `status` stored as `kind: 'custom'`)
* is re-tagged from `CEDAR_CELL_IDS`, so it renders as the native control.
* - Duplicates are dropped and the row is capped at `TOP_ROW_SLOTS`.
*/
export function normalizeLegacyLayout(layout: StrategicOverviewConfig): StrategicOverviewConfig {
const topRow: StrategicOverviewCell[] = [];
const seen = new Set<string>();
for (const cell of layout.topRow ?? []) {
const fieldId = cell.fieldId === 'next_step_quality' ? 'status' : cell.fieldId;
if (seen.has(fieldId)) continue;
seen.add(fieldId);
topRow.push({ fieldId, kind: CEDAR_CELL_IDS.has(fieldId) ? 'cedar' : 'custom' });
}
const sections = (layout.sections ?? []).map((section) => ({
...section,
fieldIds: section.fieldIds.filter((id) => !SUPERSEDED_CUSTOM_FIELD_IDS.has(id)),
}));
return { ...layout, topRow: topRow.slice(0, TOP_ROW_SLOTS), sections };
}