FPSMS-frontend
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

633 líneas
22 KiB

  1. import { ItemLotTraceJoPrelude, ItemLotTraceMaterialInput } from "@/app/api/itemTracing";
  2. import {
  3. TraceGraphDetailField,
  4. TraceGraphDetailLabels,
  5. TraceGraphNode,
  6. } from "./buildTraceGraphNodes";
  7. import {
  8. buildProductionGraphNodes,
  9. ProductionGraphLabels,
  10. } from "./buildProductionGraphNodes";
  11. import {
  12. groupQcResultsBySession,
  13. } from "./traceLabelUtils";
  14. import { formatQty, formatSignedQty } from "./traceQtyUtils";
  15. import {
  16. createMaterialPickNode,
  17. docLinkFromOriginType,
  18. docLinkFromRefType,
  19. field,
  20. fieldIf,
  21. detailsOf,
  22. isJoProducedMaterial,
  23. parseSortKey,
  24. } from "./traceNodeFactory";
  25. import { buildPickOrderTargetDateMapFromPrelude } from "./traceDocLinkUtils";
  26. import { resolvePutawayPresentation, isTransferInboundPutaway } from "./tracePutawayUtils";
  27. export interface JoPreludeGraphLabels extends TraceGraphDetailLabels {
  28. nodeMaterialIn: string;
  29. nodeMaterialPick: string;
  30. nodePurchase: string;
  31. nodeReceipt: string;
  32. nodeJoCreated: string;
  33. pickOrder: string;
  34. }
  35. type LotPickRef = {
  36. pickOrderCode: string;
  37. pickOrderId: number | null;
  38. consoCode: string;
  39. };
  40. type PreludeBuildContext = {
  41. seenPurchaseLines: Set<number>;
  42. seenInboundLots: Set<number>;
  43. seenReceiptLots: Set<number>;
  44. seenQcKeys: Set<string>;
  45. seenPutawayKeys: Set<string>;
  46. pickOrderTargetDateMap: Map<string, string>;
  47. seq: number;
  48. };
  49. const materialLotKey = (itemCode: string, lotNo: string) => `${itemCode}::${lotNo}`;
  50. const mergeLotPickMaps = (
  51. target: Map<string, LotPickRef[]>,
  52. source: Map<string, LotPickRef[]>,
  53. ): void => {
  54. source.forEach((picks, key) => {
  55. const list = target.get(key) ?? [];
  56. picks.forEach((p) => {
  57. if (!list.some((existing) => existing.pickOrderCode === p.pickOrderCode)) {
  58. list.push(p);
  59. }
  60. });
  61. if (list.length) target.set(key, list);
  62. });
  63. };
  64. const buildLotPickOrderMap = (
  65. materialInputs: ItemLotTraceJoPrelude["materialInputs"],
  66. ): Map<string, LotPickRef[]> => {
  67. const map = new Map<string, LotPickRef[]>();
  68. materialInputs.forEach((m) => {
  69. const code = m.pickOrderCode?.trim();
  70. if (!code) return;
  71. const key = materialLotKey(m.materialItemCode, m.materialLotNo);
  72. const list = map.get(key) ?? [];
  73. if (!list.some((p) => p.pickOrderCode === code)) {
  74. list.push({
  75. pickOrderCode: code,
  76. pickOrderId: m.pickOrderId,
  77. consoCode: m.consoCode,
  78. });
  79. }
  80. map.set(key, list);
  81. const nestedInputs = m.nestedJoPrelude?.materialInputs ?? [];
  82. if (nestedInputs.length) {
  83. mergeLotPickMaps(map, buildLotPickOrderMapRecursive(nestedInputs));
  84. }
  85. });
  86. return map;
  87. };
  88. const buildLotPickOrderMapRecursive = (
  89. materialInputs: ItemLotTraceMaterialInput[],
  90. ): Map<string, LotPickRef[]> => buildLotPickOrderMap(materialInputs);
  91. const pickOrdersForLot = (
  92. map: Map<string, LotPickRef[]>,
  93. itemCode: string,
  94. lotNo: string,
  95. ): LotPickRef[] => map.get(materialLotKey(itemCode, lotNo)) ?? [];
  96. const pickOrderCodesLabel = (picks: LotPickRef[]) =>
  97. picks.map((p) => p.pickOrderCode).join(" · ");
  98. const appendPickToSubtitle = (base: string, picks: LotPickRef[], pickLabel: string): string => {
  99. if (!picks.length) return base;
  100. const suffix = `${pickLabel}: ${pickOrderCodesLabel(picks)}`;
  101. return base ? `${base} · ${suffix}` : suffix;
  102. };
  103. const pickOrderDetailFields = (
  104. picks: LotPickRef[],
  105. pickLabel: string,
  106. ): TraceGraphDetailField[] =>
  107. picks.map((p) =>
  108. field(pickLabel, p.pickOrderCode, {
  109. linkKind: "pick",
  110. linkCode: p.pickOrderCode,
  111. linkId: p.pickOrderId,
  112. consoCode: p.consoCode || p.pickOrderCode,
  113. }),
  114. );
  115. const nextSeq = (ctx: PreludeBuildContext): number => ctx.seq++;
  116. const pickCtx = (ctx: PreludeBuildContext) => ({
  117. nextSeq: () => nextSeq(ctx),
  118. pickOrderTargetDate: (pickOrderCode: string) =>
  119. ctx.pickOrderTargetDateMap.get(pickOrderCode?.trim() ?? ""),
  120. });
  121. const buildMaterialProductionNodes = (
  122. m: ItemLotTraceMaterialInput,
  123. labels: JoPreludeGraphLabels & Partial<ProductionGraphLabels>,
  124. ctx: PreludeBuildContext,
  125. ): TraceGraphNode[] => {
  126. const steps = m.productionSteps ?? [];
  127. if (!steps.length || !labels.nodeProductionStep || !labels.nodeScrap || !labels.nodeDefect) return [];
  128. const matUom = m.materialUom?.trim() || "";
  129. return buildProductionGraphNodes(steps, [], labels as ProductionGraphLabels, matUom).map((node) => ({
  130. ...node,
  131. id: `mat-prod-${m.materialLotNo}-${node.id}`,
  132. traceLotNo: m.materialLotNo,
  133. traceItemCode: m.materialItemCode,
  134. subtitle: [m.materialItemCode, m.materialLotNo, node.subtitle].filter(Boolean).join(" · "),
  135. }));
  136. };
  137. const buildMaterialStockInPreludeNodes = (
  138. m: ItemLotTraceMaterialInput,
  139. labels: JoPreludeGraphLabels,
  140. lotPicks: Map<string, LotPickRef[]>,
  141. ctx: PreludeBuildContext,
  142. ): TraceGraphNode[] => {
  143. const nodes: TraceGraphNode[] = [];
  144. const picks = pickOrdersForLot(lotPicks, m.materialItemCode, m.materialLotNo);
  145. const lotId = m.materialInventoryLotId;
  146. const origin = m.stockInOrigin;
  147. const matUom = m.materialUom?.trim() || "";
  148. (m.purchaseEvents ?? []).forEach((p) => {
  149. if (ctx.seenPurchaseLines.has(p.purchaseOrderLineId)) return;
  150. ctx.seenPurchaseLines.add(p.purchaseOrderLineId);
  151. const purchaseUnit = p.purchaseUnit;
  152. const supplierLabel = [p.supplierCode, p.supplierName].filter(Boolean).join(" ");
  153. const subtitle =
  154. [supplierLabel, p.itemCode, p.itemName].filter(Boolean).join(" · ") || m.materialItemCode;
  155. const meta = [supplierLabel].filter(Boolean).join(" · ");
  156. nodes.push({
  157. id: `mat-purchase-${p.purchaseOrderLineId}-${m.materialLotNo}`,
  158. kind: "PURCHASE",
  159. timestamp: p.orderDate,
  160. sortKey: parseSortKey(p.orderDate, nextSeq(ctx)),
  161. title: labels.nodePurchase,
  162. subtitle: appendPickToSubtitle(subtitle, picks, labels.pickOrder),
  163. qty: p.orderQty,
  164. uom: purchaseUnit,
  165. meta: meta || undefined,
  166. refType: "PO",
  167. refCode: p.purchaseOrderCode,
  168. refId: p.purchaseOrderId,
  169. docLinkKind: "po",
  170. traceLotNo: m.materialLotNo,
  171. traceItemCode: m.materialItemCode,
  172. categoryLabel: labels.categoryPurchase,
  173. details: [
  174. field(labels.detailItemCode, p.itemCode),
  175. field(labels.detailItemName, p.itemName),
  176. field(labels.detailSupplier, supplierLabel),
  177. field(labels.detailOrderQty, formatQty(p.orderQty, purchaseUnit)),
  178. field(labels.detailPurchaseUnit, p.purchaseUnit),
  179. field(labels.detailPurchaseOrderNo, p.purchaseOrderCode, {
  180. linkKind: "po",
  181. linkCode: p.purchaseOrderCode,
  182. linkId: p.purchaseOrderId,
  183. }),
  184. field(labels.detailTime, p.orderDate),
  185. field(labels.detailLot, m.materialLotNo),
  186. ...pickOrderDetailFields(picks, labels.pickOrder),
  187. ],
  188. });
  189. });
  190. if (origin && lotId != null) {
  191. const originType = origin.type.trim().toUpperCase();
  192. const linkKind = docLinkFromOriginType(origin.type);
  193. const subtitle = [
  194. m.materialLotNo,
  195. [origin.supplierCode, origin.supplierName].filter(Boolean).join(" "),
  196. ]
  197. .filter(Boolean)
  198. .join(" · ");
  199. if (originType === "PO" && !ctx.seenReceiptLots.has(lotId)) {
  200. ctx.seenReceiptLots.add(lotId);
  201. nodes.push({
  202. id: `mat-receipt-${lotId}-${origin.stockInLineId}`,
  203. kind: "RECEIPT",
  204. timestamp: origin.receiptDate,
  205. sortKey: parseSortKey(origin.receiptDate, nextSeq(ctx)),
  206. title: labels.nodeReceipt,
  207. subtitle: appendPickToSubtitle(
  208. [origin.refCode, subtitle].filter(Boolean).join(" · ") || m.materialItemCode,
  209. picks,
  210. labels.pickOrder,
  211. ),
  212. qty: origin.acceptedQty,
  213. uom: matUom,
  214. refType: origin.type,
  215. refCode: origin.refCode,
  216. refId: origin.refId,
  217. docLinkKind: linkKind,
  218. traceLotNo: m.materialLotNo,
  219. traceItemCode: m.materialItemCode,
  220. categoryLabel: labels.categoryReceipt,
  221. details: [
  222. field(labels.detailPurchaseOrderNo, origin.refCode, {
  223. linkKind,
  224. linkCode: origin.refCode,
  225. linkId: origin.refId,
  226. }),
  227. field(labels.detailSupplier, [origin.supplierCode, origin.supplierName].filter(Boolean).join(" ")),
  228. field(labels.detailItemCode, m.materialItemCode),
  229. field(labels.detailLot, m.materialLotNo),
  230. field(labels.detailQty, formatQty(origin.acceptedQty, matUom)),
  231. field(labels.detailStatus, labels.tr.stockInStatus(origin.status)),
  232. field(labels.detailTime, origin.receiptDate),
  233. ...pickOrderDetailFields(picks, labels.pickOrder),
  234. ],
  235. });
  236. } else if (
  237. originType !== "PO" &&
  238. originType !== "ADJ" &&
  239. !ctx.seenInboundLots.has(lotId)
  240. ) {
  241. // Nested JO_CREATED already represents this JO output lot (with 提料 links).
  242. const skipJoOriginBecauseNested =
  243. originType === "JO" && Boolean(m.nestedJoPrelude?.jobOrder?.jobOrderCode?.trim());
  244. if (!skipJoOriginBecauseNested) {
  245. // ADJ is emitted after putaways as ADJUSTMENT (FG-aligned: 上架 → 庫存調整).
  246. ctx.seenInboundLots.add(lotId);
  247. const isJoOrigin = originType === "JO";
  248. nodes.push({
  249. id: `mat-in-${lotId}-${origin.stockInLineId}`,
  250. kind: isJoOrigin ? "JO_CREATED" : "MATERIAL_IN",
  251. timestamp: origin.receiptDate,
  252. sortKey: parseSortKey(origin.receiptDate, nextSeq(ctx)),
  253. title: isJoOrigin
  254. ? labels.nodeJoCreated
  255. : `${labels.tr.refType(origin.type)} · ${labels.nodeMaterialIn}`,
  256. subtitle: appendPickToSubtitle(
  257. subtitle || m.materialItemCode,
  258. picks,
  259. labels.pickOrder,
  260. ),
  261. qty: origin.acceptedQty,
  262. uom: matUom,
  263. refType: origin.type,
  264. refCode: origin.refCode,
  265. refId: origin.refId,
  266. docLinkKind: linkKind,
  267. traceLotNo: m.materialLotNo,
  268. traceItemCode: m.materialItemCode,
  269. categoryLabel: isJoOrigin ? labels.categoryProduction : labels.categoryPurchase,
  270. details: [
  271. field(labels.detailType, isJoOrigin ? labels.nodeJoCreated : labels.tr.refType(origin.type)),
  272. field(
  273. isJoOrigin
  274. ? labels.jobOrder
  275. : originType === "PO"
  276. ? labels.detailPurchaseOrderNo
  277. : labels.detailSourceDoc,
  278. origin.refCode,
  279. {
  280. linkKind,
  281. linkCode: origin.refCode,
  282. linkId: origin.refId,
  283. }),
  284. field(labels.detailSupplier, [origin.supplierCode, origin.supplierName].filter(Boolean).join(" ")),
  285. field(labels.detailItemCode, m.materialItemCode),
  286. field(labels.detailLot, m.materialLotNo),
  287. field(labels.detailQty, formatQty(origin.acceptedQty, matUom)),
  288. field(labels.detailStatus, labels.tr.stockInStatus(origin.status)),
  289. field(labels.detailTime, origin.receiptDate),
  290. ...pickOrderDetailFields(picks, labels.pickOrder),
  291. ],
  292. });
  293. }
  294. }
  295. }
  296. groupQcResultsBySession(m.qcResults).forEach((group) => {
  297. const first = group[0];
  298. if (!first) return;
  299. const key = `${first.stockInLineId}-${first.created}`;
  300. if (ctx.seenQcKeys.has(key)) return;
  301. ctx.seenQcKeys.add(key);
  302. const allPassed = group.every((q) => q.qcPassed);
  303. const totalFail = group.reduce((s, q) => s + (q.failQty ?? 0), 0);
  304. const meta = [first.handledBy, ...group.map((q) => q.remarks).filter(Boolean), m.materialLotNo]
  305. .filter(Boolean)
  306. .join(" · ");
  307. const qcTypeRaw = group.map((q) => q.qcType).find((t) => t?.trim()) ?? "";
  308. nodes.push({
  309. id: `mat-qc-${key}`,
  310. kind: "MATERIAL_QC",
  311. timestamp: first.created,
  312. sortKey: parseSortKey(first.created, nextSeq(ctx)),
  313. title: allPassed ? labels.nodeQcPass : labels.nodeQcFail,
  314. subtitle: "",
  315. qty: totalFail,
  316. uom: matUom,
  317. meta: meta || undefined,
  318. traceLotNo: m.materialLotNo,
  319. traceItemCode: m.materialItemCode,
  320. categoryLabel: labels.categoryQc,
  321. qcTypeLabel: labels.tr.qcType(qcTypeRaw),
  322. details: detailsOf(
  323. field(labels.detailItemCode, m.materialItemCode),
  324. field(labels.detailLot, m.materialLotNo),
  325. field(labels.detailStatus, labels.tr.qcPassed(allPassed)),
  326. field(labels.detailQcType, labels.tr.qcType(qcTypeRaw)),
  327. field(labels.detailQcCriteria, "", {
  328. variant: "qcCriteriaList",
  329. qcCriteriaItems: group.map((q) => ({
  330. name: q.qcItemName?.trim() || q.qcItemCode?.trim() || labels.detailQcUnknownItem,
  331. description: q.qcItemDescription?.trim() || undefined,
  332. passed: q.qcPassed,
  333. failQty: q.failQty > 0 ? q.failQty : undefined,
  334. })),
  335. }),
  336. field(labels.detailAcceptedQty, formatQty(first.acceptedQty, matUom)),
  337. field(labels.detailFailQty, formatQty(totalFail, matUom)),
  338. field(labels.detailHandler, first.handledBy),
  339. field(labels.detailTime, first.created),
  340. fieldIf(labels.detailRemarks, group.map((q) => q.remarks).filter(Boolean).join(";")),
  341. ...pickOrderDetailFields(picks, labels.pickOrder),
  342. ),
  343. });
  344. });
  345. let lastMaterialPutawayWh = "";
  346. (m.putawayEvents ?? []).forEach((p) => {
  347. const key = `${p.inventoryLotLineId}-${p.timestamp}`;
  348. if (ctx.seenPutawayKeys.has(key)) return;
  349. if (isTransferInboundPutaway(p.refType)) return;
  350. ctx.seenPutawayKeys.add(key);
  351. const linkKind = docLinkFromRefType(p.refType);
  352. const pres = resolvePutawayPresentation(
  353. labels.tr,
  354. {
  355. nodePutaway: labels.nodePutaway,
  356. nodePutawayTransfer: labels.nodePutawayTransfer,
  357. putawayTransferDetail: labels.putawayTransferDetail,
  358. },
  359. p.status,
  360. p.refType,
  361. );
  362. const meta = [pres.chipLabel, p.handledBy, p.warehouseCode].filter(Boolean).join(" · ");
  363. if (p.warehouseCode?.trim()) lastMaterialPutawayWh = p.warehouseCode.trim();
  364. nodes.push({
  365. id: `mat-putaway-${p.inventoryLotLineId}-${p.timestamp}`,
  366. kind: "PUTAWAY",
  367. timestamp: p.timestamp,
  368. sortKey: parseSortKey(p.timestamp, nextSeq(ctx)),
  369. title: pres.title,
  370. subtitle: appendPickToSubtitle(
  371. [pres.chipLabel, p.refCode, p.warehouseCode, m.materialLotNo].filter(Boolean).join(" · ") ||
  372. m.materialItemCode,
  373. picks,
  374. labels.pickOrder,
  375. ),
  376. qty: p.qty,
  377. uom: matUom,
  378. meta: meta || undefined,
  379. refType: p.refType,
  380. refCode: p.refCode,
  381. refId: p.refId,
  382. docLinkKind: linkKind,
  383. putawayStatusLabel: pres.chipLabel,
  384. putawayStatusDetail: pres.statusDetail,
  385. warehouseCode: p.warehouseCode,
  386. traceLotNo: m.materialLotNo,
  387. traceItemCode: m.materialItemCode,
  388. categoryLabel: labels.categoryPutaway,
  389. details: [
  390. field(labels.detailStatus, pres.statusDetail),
  391. field(labels.detailItemCode, m.materialItemCode),
  392. field(labels.detailLot, m.materialLotNo),
  393. field(
  394. isTransferInboundPutaway(p.refType)
  395. ? labels.detailInboundSiNo
  396. : (p.refType ?? "").trim().toUpperCase() === "PO"
  397. ? labels.detailPurchaseOrderNo
  398. : (p.refType ?? "").trim().toUpperCase() === "JO"
  399. ? labels.jobOrder
  400. : labels.detailSourceDoc,
  401. p.refCode,
  402. {
  403. linkKind,
  404. linkCode: p.refCode,
  405. linkId: p.refId,
  406. }),
  407. field(labels.detailPutawayBin, p.warehouseCode),
  408. field(labels.detailQty, formatQty(p.qty, matUom)),
  409. field(labels.detailHandler, p.handledBy),
  410. field(labels.detailTime, p.timestamp),
  411. ...pickOrderDetailFields(picks, labels.pickOrder),
  412. ],
  413. });
  414. });
  415. // ADJ stock-in: match FG — 上架 → 庫存調整 (入), not orphaned 「調整 · 原料入庫」.
  416. if (origin && lotId != null) {
  417. const originType = origin.type.trim().toUpperCase();
  418. if (originType === "ADJ" && !ctx.seenInboundLots.has(lotId)) {
  419. ctx.seenInboundLots.add(lotId);
  420. const adjWh = lastMaterialPutawayWh;
  421. const meta = [labels.tr.stockInStatus(origin.status)].filter(Boolean).join(" · ");
  422. // Place at/after related putaway so chronological X and 上架 → 調整 edges match FG.
  423. const lastPutawayTs = (m.putawayEvents ?? [])
  424. .map((p) => p.timestamp)
  425. .filter((t): t is string => Boolean(t?.trim()))
  426. .sort()
  427. .at(-1);
  428. const adjTimestamp =
  429. lastPutawayTs &&
  430. parseSortKey(lastPutawayTs, 0) >= parseSortKey(origin.receiptDate, 0)
  431. ? lastPutawayTs
  432. : origin.receiptDate;
  433. // Keep sortKey strictly after matching putaway so layout X stays 上架 → 調整
  434. // (same calendar time previously ordered adj left of putaway → back-arrow).
  435. const putawaySort = lastPutawayTs ? parseSortKey(lastPutawayTs, 0) : null;
  436. const adjSortBase = parseSortKey(adjTimestamp, nextSeq(ctx));
  437. nodes.push({
  438. id: `mat-adj-${lotId}-${origin.stockInLineId}`,
  439. kind: "ADJUSTMENT",
  440. timestamp: origin.receiptDate,
  441. sortKey:
  442. putawaySort != null && adjSortBase <= putawaySort ? putawaySort + 1 : adjSortBase,
  443. title: `${labels.nodeAdjustment} (${labels.tr.direction("IN")})`,
  444. subtitle: appendPickToSubtitle(
  445. [labels.tr.adjustmentType("ADJ"), origin.refCode || m.materialLotNo]
  446. .filter(Boolean)
  447. .join(" · ") || m.materialItemCode,
  448. picks,
  449. labels.pickOrder,
  450. ),
  451. qty: origin.acceptedQty,
  452. uom: matUom,
  453. adjustmentDirection: "IN",
  454. meta: meta || undefined,
  455. refType: origin.type,
  456. refCode: origin.refCode,
  457. refId: origin.refId,
  458. warehouseCode: adjWh || undefined,
  459. traceLotNo: m.materialLotNo,
  460. traceItemCode: m.materialItemCode,
  461. categoryLabel: labels.categoryAdjustment,
  462. details: detailsOf(
  463. field(labels.detailVariance, formatSignedQty(origin.acceptedQty, "IN", matUom)),
  464. field(labels.detailAdjustmentRef, origin.refCode),
  465. field(labels.detailItemCode, m.materialItemCode),
  466. field(labels.detailLot, m.materialLotNo),
  467. ...(adjWh ? [field(labels.detailWarehouse, adjWh)] : []),
  468. field(labels.detailStatus, labels.tr.stockInStatus(origin.status)),
  469. field(labels.detailTime, origin.receiptDate),
  470. ...pickOrderDetailFields(picks, labels.pickOrder),
  471. ),
  472. });
  473. }
  474. }
  475. return nodes;
  476. };
  477. const buildNestedJoCreatedNode = (
  478. m: ItemLotTraceMaterialInput,
  479. labels: JoPreludeGraphLabels,
  480. picks: LotPickRef[],
  481. ctx: PreludeBuildContext,
  482. ): TraceGraphNode | null => {
  483. const nested = m.nestedJoPrelude;
  484. const jo = nested?.jobOrder;
  485. if (!jo?.jobOrderCode?.trim()) return null;
  486. const nestedInputs = nested?.materialInputs ?? [];
  487. const nestedPickTs = nestedInputs
  488. .map((input) => input.pickedAt)
  489. .find((ts) => ts?.trim());
  490. const nestedProdTs = (m.productionSteps ?? [])
  491. .map((step) => step.startTime)
  492. .find((ts) => ts?.trim());
  493. const origin = m.stockInOrigin;
  494. const ts =
  495. jo.createdAt?.trim() ||
  496. jo.planStart?.trim() ||
  497. nestedPickTs?.trim() ||
  498. nestedProdTs?.trim() ||
  499. origin?.receiptDate?.trim() ||
  500. null;
  501. const matUom = m.materialUom?.trim() || "";
  502. const qty = origin?.acceptedQty ?? jo.reqQty;
  503. return {
  504. id: `mat-jo-created-${jo.jobOrderId}-${m.materialLotNo}`,
  505. kind: "JO_CREATED",
  506. timestamp: ts,
  507. sortKey: parseSortKey(ts, nextSeq(ctx)),
  508. title: labels.nodeJoCreated,
  509. subtitle: appendPickToSubtitle(
  510. [m.materialLotNo, m.materialItemCode].filter(Boolean).join(" · ") || jo.jobOrderCode,
  511. picks,
  512. labels.pickOrder,
  513. ),
  514. qty,
  515. uom: matUom,
  516. refType: "JO",
  517. refCode: jo.jobOrderCode,
  518. refId: jo.jobOrderId,
  519. docLinkKind: "jo",
  520. traceLotNo: m.materialLotNo,
  521. traceItemCode: m.materialItemCode,
  522. categoryLabel: labels.categoryProduction,
  523. details: [
  524. field(labels.detailType, labels.nodeJoCreated),
  525. field(labels.jobOrder, jo.jobOrderCode, {
  526. linkKind: "jo",
  527. linkCode: jo.jobOrderCode,
  528. linkId: jo.jobOrderId,
  529. }),
  530. field(labels.detailItemCode, m.materialItemCode),
  531. field(labels.detailLot, m.materialLotNo),
  532. field(labels.detailQty, formatQty(qty, matUom)),
  533. field(labels.detailTime, ts),
  534. ...pickOrderDetailFields(picks, labels.pickOrder),
  535. ],
  536. };
  537. };
  538. const buildMaterialInputChainNodes = (
  539. materialInputs: ItemLotTraceMaterialInput[],
  540. labels: JoPreludeGraphLabels & Partial<ProductionGraphLabels>,
  541. lotPicks: Map<string, LotPickRef[]>,
  542. ctx: PreludeBuildContext,
  543. ): TraceGraphNode[] => {
  544. const nodes: TraceGraphNode[] = [];
  545. materialInputs.forEach((m) => {
  546. const joProduced = isJoProducedMaterial(m);
  547. const nestedInputs = m.nestedJoPrelude?.materialInputs ?? [];
  548. const picks = pickOrdersForLot(lotPicks, m.materialItemCode, m.materialLotNo);
  549. if (joProduced && m.nestedJoPrelude) {
  550. const nestedJoCreated = buildNestedJoCreatedNode(m, labels, picks, ctx);
  551. if (nestedJoCreated) nodes.push(nestedJoCreated);
  552. }
  553. if (joProduced && nestedInputs.length > 0) {
  554. const nestedLotPicks = buildLotPickOrderMap(nestedInputs);
  555. nodes.push(
  556. ...buildMaterialInputChainNodes(
  557. nestedInputs,
  558. labels,
  559. nestedLotPicks,
  560. ctx,
  561. ),
  562. );
  563. nestedInputs.forEach((nm, ni) => {
  564. nodes.push(createMaterialPickNode(nm, ni, labels, pickCtx(ctx), m.materialLotNo));
  565. });
  566. }
  567. if (joProduced && (m.productionSteps?.length ?? 0) > 0) {
  568. nodes.push(...buildMaterialProductionNodes(m, labels, ctx));
  569. }
  570. nodes.push(...buildMaterialStockInPreludeNodes(m, labels, lotPicks, ctx));
  571. });
  572. return nodes;
  573. };
  574. export const buildJoPreludeGraphNodes = (
  575. joPrelude: ItemLotTraceJoPrelude,
  576. labels: JoPreludeGraphLabels & Partial<ProductionGraphLabels>,
  577. ): TraceGraphNode[] => {
  578. const ctx: PreludeBuildContext = {
  579. seenPurchaseLines: new Set(),
  580. seenInboundLots: new Set(),
  581. seenReceiptLots: new Set(),
  582. seenQcKeys: new Set(),
  583. seenPutawayKeys: new Set(),
  584. pickOrderTargetDateMap: buildPickOrderTargetDateMapFromPrelude(joPrelude),
  585. seq: 0,
  586. };
  587. const lotPicks = buildLotPickOrderMap(joPrelude.materialInputs);
  588. const nodes: TraceGraphNode[] = [
  589. ...buildMaterialInputChainNodes(joPrelude.materialInputs, labels, lotPicks, ctx),
  590. ...joPrelude.materialInputs.map((m, i) => createMaterialPickNode(m, i, labels, pickCtx(ctx))),
  591. ];
  592. return nodes.sort((a, b) => {
  593. if (a.sortKey !== b.sortKey) return a.sortKey - b.sortKey;
  594. return a.id.localeCompare(b.id);
  595. });
  596. };