FPSMS-frontend
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

275 linhas
8.7 KiB

  1. import {
  2. DO_GROUP_CHILD_WIDTH,
  3. DO_GROUP_HEADER,
  4. DO_GROUP_INNER_COLS_MAX,
  5. DO_GROUP_MIN_SIZE,
  6. DO_GROUP_PAD,
  7. PICK_GROUP_MIN_SIZE,
  8. NODE_GAP,
  9. NODE_HEIGHT,
  10. NODE_WIDTH,
  11. } from "./traceFlowConstants";
  12. import { TraceGraphLayoutNode, sortNodesInPhase } from "./traceGraphLayout";
  13. import { TraceGraphNode } from "./buildTraceGraphNodes";
  14. export type DoGroupChildPlacement = { x: number; y: number; compact: boolean };
  15. /** Fewer columns for large groups — wider cards, no horizontal cramming. */
  16. const preferredDoGroupCols = (memberCount: number): number => {
  17. if (memberCount <= 1) return 1;
  18. if (memberCount >= 15) return 2;
  19. if (memberCount >= 6) return 3;
  20. return 2;
  21. };
  22. const sumDoGroupQty = (
  23. children: TraceGraphLayoutNode[],
  24. ): { totalQty: number; uom?: string } | null => {
  25. let totalQty = 0;
  26. let hasQty = false;
  27. let uom: string | undefined;
  28. children.forEach((child) => {
  29. const qty = child.qty;
  30. if (qty != null && Number.isFinite(Number(qty))) {
  31. totalQty += Number(qty);
  32. hasQty = true;
  33. }
  34. if (!uom && child.uom?.trim()) uom = child.uom.trim();
  35. });
  36. return hasQty ? { totalQty, uom } : null;
  37. };
  38. export const computeDoGroupBoxLayout = (
  39. memberCount: number,
  40. ): { width: number; height: number; cols: number } => {
  41. const childW = DO_GROUP_CHILD_WIDTH;
  42. const cols = Math.min(DO_GROUP_INNER_COLS_MAX, memberCount, preferredDoGroupCols(memberCount));
  43. const rows = Math.ceil(memberCount / cols);
  44. const gridW = cols * childW + Math.max(0, cols - 1) * NODE_GAP;
  45. const gridH = rows * NODE_HEIGHT + Math.max(0, rows - 1) * NODE_GAP;
  46. return {
  47. width: gridW + DO_GROUP_PAD * 2,
  48. height: DO_GROUP_HEADER + gridH + DO_GROUP_PAD * 2,
  49. cols,
  50. };
  51. };
  52. export const layoutDoGroupChildPlacements = (
  53. children: TraceGraphLayoutNode[],
  54. ): Map<string, DoGroupChildPlacement> => {
  55. const sorted = [...children].sort(sortNodesInPhase);
  56. const { cols } = computeDoGroupBoxLayout(sorted.length);
  57. const childW = DO_GROUP_CHILD_WIDTH;
  58. const result = new Map<string, DoGroupChildPlacement>();
  59. sorted.forEach((child, index) => {
  60. const col = index % cols;
  61. const row = Math.floor(index / cols);
  62. result.set(child.id, {
  63. x: DO_GROUP_PAD + col * (childW + NODE_GAP),
  64. y: DO_GROUP_HEADER + DO_GROUP_PAD + row * (NODE_HEIGHT + NODE_GAP),
  65. compact: false,
  66. });
  67. });
  68. return result;
  69. };
  70. export const applyDoOutboundGrouping = (
  71. nodes: TraceGraphLayoutNode[],
  72. groupTitle: (count: number) => string,
  73. minSize = DO_GROUP_MIN_SIZE,
  74. ): TraceGraphLayoutNode[] => {
  75. const doByColumnAndWarehouse = new Map<string, TraceGraphLayoutNode[]>();
  76. nodes.forEach((node) => {
  77. if (node.kind !== "DO_OUT" || node.doGroupId) return;
  78. const whKey = (node.warehouseCode ?? "").trim().toUpperCase() || "__none__";
  79. const key = `${node.column}::${whKey}`;
  80. const list = doByColumnAndWarehouse.get(key) ?? [];
  81. list.push(node);
  82. doByColumnAndWarehouse.set(key, list);
  83. });
  84. const groupNodes: TraceGraphLayoutNode[] = [];
  85. const childGroupIds = new Map<string, string>();
  86. doByColumnAndWarehouse.forEach((columnNodes, key) => {
  87. if (columnNodes.length < minSize) return;
  88. const column = columnNodes[0]!.column;
  89. const warehouseCode = columnNodes[0]!.warehouseCode?.trim() || undefined;
  90. const whSlug = (warehouseCode ?? "none").replace(/[^a-zA-Z0-9_-]/g, "_");
  91. const groupId = `do-group-col-${column}-${whSlug}`;
  92. const sorted = [...columnNodes].sort(sortNodesInPhase);
  93. const { width, height } = computeDoGroupBoxLayout(sorted.length);
  94. const minSort = sorted.reduce((min, n) => Math.min(min, n.sortKey), sorted[0]!.sortKey);
  95. const qtySummary = sumDoGroupQty(sorted);
  96. sorted.forEach((child) => childGroupIds.set(child.id, groupId));
  97. const groupNode: TraceGraphLayoutNode = {
  98. id: groupId,
  99. kind: "DO_GROUP",
  100. timestamp: sorted[0]?.timestamp ?? null,
  101. sortKey: minSort,
  102. title: groupTitle(sorted.length),
  103. subtitle: [sorted[0]?.dayKey, warehouseCode].filter(Boolean).join(" · ") || "",
  104. categoryLabel: sorted[0]?.categoryLabel,
  105. warehouseCode,
  106. details: [],
  107. phase: sorted[0]!.phase,
  108. column,
  109. dayKey: sorted[0]!.dayKey,
  110. laneIndex: sorted[0]!.laneIndex,
  111. sequenceIndex: sorted[0]!.sequenceIndex,
  112. branchIndex: 0,
  113. branchSize: 1,
  114. dayPhaseStaggerIndex: sorted[0]!.dayPhaseStaggerIndex,
  115. groupBoxWidth: width,
  116. groupBoxHeight: height,
  117. groupMemberCount: sorted.length,
  118. groupTotalQty: qtySummary?.totalQty,
  119. groupUom: qtySummary?.uom,
  120. };
  121. groupNodes.push(groupNode);
  122. });
  123. if (!groupNodes.length) return nodes;
  124. return [
  125. ...nodes.map((node) => {
  126. const groupId = childGroupIds.get(node.id);
  127. if (!groupId) return node;
  128. return { ...node, doGroupId: groupId };
  129. }),
  130. ...groupNodes,
  131. ].sort((a, b) => {
  132. if (a.sortKey !== b.sortKey) return a.sortKey - b.sortKey;
  133. return a.id.localeCompare(b.id);
  134. });
  135. };
  136. export const applyMaterialPickGrouping = (
  137. nodes: TraceGraphLayoutNode[],
  138. groupTitle: (pickOrderCode: string, count: number) => string,
  139. minSize = PICK_GROUP_MIN_SIZE,
  140. ): TraceGraphLayoutNode[] => {
  141. const picksByKey = new Map<string, TraceGraphLayoutNode[]>();
  142. nodes.forEach((node) => {
  143. if (node.kind !== "MATERIAL_PICK" || node.doGroupId) return;
  144. const pickCode = node.refCode?.trim();
  145. if (!pickCode) return;
  146. const key = `${node.column}::${pickCode}`;
  147. const list = picksByKey.get(key) ?? [];
  148. list.push(node);
  149. picksByKey.set(key, list);
  150. });
  151. const groupNodes: TraceGraphLayoutNode[] = [];
  152. const childGroupIds = new Map<string, string>();
  153. picksByKey.forEach((columnNodes, key) => {
  154. if (columnNodes.length < minSize) return;
  155. const pickCode = key.split("::").slice(1).join("::");
  156. const groupId = `pick-group-${key.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
  157. const sorted = [...columnNodes].sort(sortNodesInPhase);
  158. const { width, height } = computeDoGroupBoxLayout(sorted.length);
  159. const minSort = sorted.reduce((min, n) => Math.min(min, n.sortKey), sorted[0]!.sortKey);
  160. sorted.forEach((child) => childGroupIds.set(child.id, groupId));
  161. const groupNode: TraceGraphLayoutNode = {
  162. id: groupId,
  163. kind: "PICK_GROUP",
  164. timestamp: sorted[0]?.timestamp ?? null,
  165. sortKey: minSort,
  166. title: groupTitle(pickCode, sorted.length),
  167. subtitle: sorted[0]?.dayKey ?? "",
  168. refCode: pickCode,
  169. refId: sorted[0]?.refId,
  170. docLinkKind: "pick",
  171. consoCode: sorted[0]?.consoCode,
  172. jobOrderCode: sorted[0]?.jobOrderCode,
  173. categoryLabel: sorted[0]?.categoryLabel,
  174. details: [],
  175. phase: sorted[0]!.phase,
  176. column: sorted[0]!.column,
  177. dayKey: sorted[0]!.dayKey,
  178. laneIndex: sorted[0]!.laneIndex,
  179. sequenceIndex: sorted[0]!.sequenceIndex,
  180. branchIndex: 0,
  181. branchSize: 1,
  182. dayPhaseStaggerIndex: sorted[0]!.dayPhaseStaggerIndex,
  183. groupBoxWidth: width,
  184. groupBoxHeight: height,
  185. groupMemberCount: sorted.length,
  186. };
  187. groupNodes.push(groupNode);
  188. });
  189. if (!groupNodes.length) return nodes;
  190. return [
  191. ...nodes.map((node) => {
  192. const groupId = childGroupIds.get(node.id);
  193. if (!groupId) return node;
  194. return { ...node, doGroupId: groupId };
  195. }),
  196. ...groupNodes,
  197. ].sort((a, b) => {
  198. if (a.sortKey !== b.sortKey) return a.sortKey - b.sortKey;
  199. return a.id.localeCompare(b.id);
  200. });
  201. };
  202. export const applyFlowNodeGrouping = (
  203. nodes: TraceGraphLayoutNode[],
  204. labels: {
  205. flowDoGroupTitle: (count: number) => string;
  206. flowPickGroupTitle: (pickOrderCode: string, count: number) => string;
  207. },
  208. ): TraceGraphLayoutNode[] =>
  209. applyMaterialPickGrouping(
  210. applyDoOutboundGrouping(nodes, labels.flowDoGroupTitle),
  211. labels.flowPickGroupTitle,
  212. );
  213. export const regroupLayoutCells = (
  214. nodes: TraceGraphLayoutNode[],
  215. ): TraceGraphLayoutNode[][] => {
  216. const visible = nodes.filter((n) => !isDoGroupChild(n));
  217. const groups = new Map<string, TraceGraphLayoutNode[]>();
  218. visible.forEach((node) => {
  219. const key = `${node.column}:${node.laneIndex}`;
  220. const list = groups.get(key) ?? [];
  221. list.push(node);
  222. groups.set(key, list);
  223. });
  224. groups.forEach((list) => {
  225. list.sort(sortNodesInPhase);
  226. const size = list.length;
  227. list.forEach((node, idx) => {
  228. node.branchIndex = idx;
  229. node.branchSize = size;
  230. });
  231. });
  232. return Array.from(groups.values()).sort((a, b) => {
  233. const na = a[0]!;
  234. const nb = b[0]!;
  235. if (na.column !== nb.column) return na.column - nb.column;
  236. return na.laneIndex - nb.laneIndex;
  237. });
  238. };
  239. export const isFlowGroupContainer = (node: TraceGraphNode | TraceGraphLayoutNode): boolean =>
  240. node.kind === "DO_GROUP" || node.kind === "PICK_GROUP";
  241. export const isDoGroupChild = (node: TraceGraphNode | TraceGraphLayoutNode): boolean =>
  242. Boolean(node.doGroupId?.trim());