|
- import {
- DO_GROUP_CHILD_WIDTH,
- DO_GROUP_HEADER,
- DO_GROUP_INNER_COLS_MAX,
- DO_GROUP_MIN_SIZE,
- DO_GROUP_PAD,
- PICK_GROUP_MIN_SIZE,
- NODE_GAP,
- NODE_HEIGHT,
- NODE_WIDTH,
- } from "./traceFlowConstants";
- import { TraceGraphLayoutNode, sortNodesInPhase } from "./traceGraphLayout";
- import { TraceGraphNode } from "./buildTraceGraphNodes";
-
- export type DoGroupChildPlacement = { x: number; y: number; compact: boolean };
-
- /** Fewer columns for large groups — wider cards, no horizontal cramming. */
- const preferredDoGroupCols = (memberCount: number): number => {
- if (memberCount <= 1) return 1;
- if (memberCount >= 15) return 2;
- if (memberCount >= 6) return 3;
- return 2;
- };
-
- const sumDoGroupQty = (
- children: TraceGraphLayoutNode[],
- ): { totalQty: number; uom?: string } | null => {
- let totalQty = 0;
- let hasQty = false;
- let uom: string | undefined;
- children.forEach((child) => {
- const qty = child.qty;
- if (qty != null && Number.isFinite(Number(qty))) {
- totalQty += Number(qty);
- hasQty = true;
- }
- if (!uom && child.uom?.trim()) uom = child.uom.trim();
- });
- return hasQty ? { totalQty, uom } : null;
- };
-
- export const computeDoGroupBoxLayout = (
- memberCount: number,
- ): { width: number; height: number; cols: number } => {
- const childW = DO_GROUP_CHILD_WIDTH;
- const cols = Math.min(DO_GROUP_INNER_COLS_MAX, memberCount, preferredDoGroupCols(memberCount));
- const rows = Math.ceil(memberCount / cols);
- const gridW = cols * childW + Math.max(0, cols - 1) * NODE_GAP;
- const gridH = rows * NODE_HEIGHT + Math.max(0, rows - 1) * NODE_GAP;
- return {
- width: gridW + DO_GROUP_PAD * 2,
- height: DO_GROUP_HEADER + gridH + DO_GROUP_PAD * 2,
- cols,
- };
- };
-
- export const layoutDoGroupChildPlacements = (
- children: TraceGraphLayoutNode[],
- ): Map<string, DoGroupChildPlacement> => {
- const sorted = [...children].sort(sortNodesInPhase);
- const { cols } = computeDoGroupBoxLayout(sorted.length);
- const childW = DO_GROUP_CHILD_WIDTH;
- const result = new Map<string, DoGroupChildPlacement>();
-
- sorted.forEach((child, index) => {
- const col = index % cols;
- const row = Math.floor(index / cols);
- result.set(child.id, {
- x: DO_GROUP_PAD + col * (childW + NODE_GAP),
- y: DO_GROUP_HEADER + DO_GROUP_PAD + row * (NODE_HEIGHT + NODE_GAP),
- compact: false,
- });
- });
-
- return result;
- };
-
- export const applyDoOutboundGrouping = (
- nodes: TraceGraphLayoutNode[],
- groupTitle: (count: number) => string,
- minSize = DO_GROUP_MIN_SIZE,
- ): TraceGraphLayoutNode[] => {
- const doByColumnAndWarehouse = new Map<string, TraceGraphLayoutNode[]>();
-
- nodes.forEach((node) => {
- if (node.kind !== "DO_OUT" || node.doGroupId) return;
- const whKey = (node.warehouseCode ?? "").trim().toUpperCase() || "__none__";
- const key = `${node.column}::${whKey}`;
- const list = doByColumnAndWarehouse.get(key) ?? [];
- list.push(node);
- doByColumnAndWarehouse.set(key, list);
- });
-
- const groupNodes: TraceGraphLayoutNode[] = [];
- const childGroupIds = new Map<string, string>();
-
- doByColumnAndWarehouse.forEach((columnNodes, key) => {
- if (columnNodes.length < minSize) return;
-
- const column = columnNodes[0]!.column;
- const warehouseCode = columnNodes[0]!.warehouseCode?.trim() || undefined;
- const whSlug = (warehouseCode ?? "none").replace(/[^a-zA-Z0-9_-]/g, "_");
- const groupId = `do-group-col-${column}-${whSlug}`;
- const sorted = [...columnNodes].sort(sortNodesInPhase);
- const { width, height } = computeDoGroupBoxLayout(sorted.length);
- const minSort = sorted.reduce((min, n) => Math.min(min, n.sortKey), sorted[0]!.sortKey);
- const qtySummary = sumDoGroupQty(sorted);
-
- sorted.forEach((child) => childGroupIds.set(child.id, groupId));
-
- const groupNode: TraceGraphLayoutNode = {
- id: groupId,
- kind: "DO_GROUP",
- timestamp: sorted[0]?.timestamp ?? null,
- sortKey: minSort,
- title: groupTitle(sorted.length),
- subtitle: [sorted[0]?.dayKey, warehouseCode].filter(Boolean).join(" · ") || "",
- categoryLabel: sorted[0]?.categoryLabel,
- warehouseCode,
- details: [],
- phase: sorted[0]!.phase,
- column,
- dayKey: sorted[0]!.dayKey,
- laneIndex: sorted[0]!.laneIndex,
- sequenceIndex: sorted[0]!.sequenceIndex,
- branchIndex: 0,
- branchSize: 1,
- dayPhaseStaggerIndex: sorted[0]!.dayPhaseStaggerIndex,
- groupBoxWidth: width,
- groupBoxHeight: height,
- groupMemberCount: sorted.length,
- groupTotalQty: qtySummary?.totalQty,
- groupUom: qtySummary?.uom,
- };
- groupNodes.push(groupNode);
- });
-
- if (!groupNodes.length) return nodes;
-
- return [
- ...nodes.map((node) => {
- const groupId = childGroupIds.get(node.id);
- if (!groupId) return node;
- return { ...node, doGroupId: groupId };
- }),
- ...groupNodes,
- ].sort((a, b) => {
- if (a.sortKey !== b.sortKey) return a.sortKey - b.sortKey;
- return a.id.localeCompare(b.id);
- });
- };
-
- export const applyMaterialPickGrouping = (
- nodes: TraceGraphLayoutNode[],
- groupTitle: (pickOrderCode: string, count: number) => string,
- minSize = PICK_GROUP_MIN_SIZE,
- ): TraceGraphLayoutNode[] => {
- const picksByKey = new Map<string, TraceGraphLayoutNode[]>();
-
- nodes.forEach((node) => {
- if (node.kind !== "MATERIAL_PICK" || node.doGroupId) return;
- const pickCode = node.refCode?.trim();
- if (!pickCode) return;
- const key = `${node.column}::${pickCode}`;
- const list = picksByKey.get(key) ?? [];
- list.push(node);
- picksByKey.set(key, list);
- });
-
- const groupNodes: TraceGraphLayoutNode[] = [];
- const childGroupIds = new Map<string, string>();
-
- picksByKey.forEach((columnNodes, key) => {
- if (columnNodes.length < minSize) return;
-
- const pickCode = key.split("::").slice(1).join("::");
- const groupId = `pick-group-${key.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
- const sorted = [...columnNodes].sort(sortNodesInPhase);
- const { width, height } = computeDoGroupBoxLayout(sorted.length);
- const minSort = sorted.reduce((min, n) => Math.min(min, n.sortKey), sorted[0]!.sortKey);
-
- sorted.forEach((child) => childGroupIds.set(child.id, groupId));
-
- const groupNode: TraceGraphLayoutNode = {
- id: groupId,
- kind: "PICK_GROUP",
- timestamp: sorted[0]?.timestamp ?? null,
- sortKey: minSort,
- title: groupTitle(pickCode, sorted.length),
- subtitle: sorted[0]?.dayKey ?? "",
- refCode: pickCode,
- refId: sorted[0]?.refId,
- docLinkKind: "pick",
- consoCode: sorted[0]?.consoCode,
- jobOrderCode: sorted[0]?.jobOrderCode,
- categoryLabel: sorted[0]?.categoryLabel,
- details: [],
- phase: sorted[0]!.phase,
- column: sorted[0]!.column,
- dayKey: sorted[0]!.dayKey,
- laneIndex: sorted[0]!.laneIndex,
- sequenceIndex: sorted[0]!.sequenceIndex,
- branchIndex: 0,
- branchSize: 1,
- dayPhaseStaggerIndex: sorted[0]!.dayPhaseStaggerIndex,
- groupBoxWidth: width,
- groupBoxHeight: height,
- groupMemberCount: sorted.length,
- };
- groupNodes.push(groupNode);
- });
-
- if (!groupNodes.length) return nodes;
-
- return [
- ...nodes.map((node) => {
- const groupId = childGroupIds.get(node.id);
- if (!groupId) return node;
- return { ...node, doGroupId: groupId };
- }),
- ...groupNodes,
- ].sort((a, b) => {
- if (a.sortKey !== b.sortKey) return a.sortKey - b.sortKey;
- return a.id.localeCompare(b.id);
- });
- };
-
- export const applyFlowNodeGrouping = (
- nodes: TraceGraphLayoutNode[],
- labels: {
- flowDoGroupTitle: (count: number) => string;
- flowPickGroupTitle: (pickOrderCode: string, count: number) => string;
- },
- ): TraceGraphLayoutNode[] =>
- applyMaterialPickGrouping(
- applyDoOutboundGrouping(nodes, labels.flowDoGroupTitle),
- labels.flowPickGroupTitle,
- );
-
- export const regroupLayoutCells = (
- nodes: TraceGraphLayoutNode[],
- ): TraceGraphLayoutNode[][] => {
- const visible = nodes.filter((n) => !isDoGroupChild(n));
- const groups = new Map<string, TraceGraphLayoutNode[]>();
-
- visible.forEach((node) => {
- const key = `${node.column}:${node.laneIndex}`;
- const list = groups.get(key) ?? [];
- list.push(node);
- groups.set(key, list);
- });
-
- groups.forEach((list) => {
- list.sort(sortNodesInPhase);
- const size = list.length;
- list.forEach((node, idx) => {
- node.branchIndex = idx;
- node.branchSize = size;
- });
- });
-
- return Array.from(groups.values()).sort((a, b) => {
- const na = a[0]!;
- const nb = b[0]!;
- if (na.column !== nb.column) return na.column - nb.column;
- return na.laneIndex - nb.laneIndex;
- });
- };
-
- export const isFlowGroupContainer = (node: TraceGraphNode | TraceGraphLayoutNode): boolean =>
- node.kind === "DO_GROUP" || node.kind === "PICK_GROUP";
-
- export const isDoGroupChild = (node: TraceGraphNode | TraceGraphLayoutNode): boolean =>
- Boolean(node.doGroupId?.trim());
|