From 53ed2c4e6a0d3fafafbcb655b5d06cff6324363d Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Thu, 28 Aug 2025 10:14:41 -0500 Subject: [PATCH] feat(materials): added in a bigger window on eom transfer lots --- .../ocp/controller/materials/lotTransfer.ts | 264 +++++++++++++++--- server/services/ocp/ocpService.ts | 2 + .../ocp/routes/materials/currentPending.ts | 51 ++++ .../ocp/routes/materials/lotTransfer.ts | 4 +- .../sqlServer/querys/misc/shiftChange.ts | 4 + 5 files changed, 286 insertions(+), 39 deletions(-) create mode 100644 server/services/ocp/routes/materials/currentPending.ts create mode 100644 server/services/sqlServer/querys/misc/shiftChange.ts diff --git a/server/services/ocp/controller/materials/lotTransfer.ts b/server/services/ocp/controller/materials/lotTransfer.ts index 7b75687..69144c3 100644 --- a/server/services/ocp/controller/materials/lotTransfer.ts +++ b/server/services/ocp/controller/materials/lotTransfer.ts @@ -6,9 +6,12 @@ import { tryCatch } from "../../../../globalUtils/tryCatch.js"; import { createLog } from "../../../logger/logger.js"; import { query } from "../../../sqlServer/prodSqlServer.js"; import { labelInfo } from "../../../sqlServer/querys/warehouse/labelInfo.js"; +import { format, formatDuration, intervalToDuration } from "date-fns"; +import { shiftChange } from "../../../sqlServer/querys/misc/shiftChange.js"; +import { success } from "zod/v4"; type NewLotData = { - runnungNumber: number; + runningNumber: number; lotNumber: number; originalAmount: number; level: number; @@ -16,6 +19,17 @@ type NewLotData = { type: "lot" | "eom"; }; +interface PendingJob { + timeoutId: NodeJS.Timeout; + runningNumber: string | number; + data: any; + consumeLot: any; + newQty: any; + scheduledFor: Date; +} + +export const pendingJobs = new Map(); + /** * Move manual material to a new lot. * @@ -28,11 +42,116 @@ type NewLotData = { * type what way are we lots */ export const lotMaterialTransfer = async (data: NewLotData) => { - let timeoutTrans: number = data.type === "lot" ? 1 : 10; + // check if we already have this running number scheduled + if (pendingJobs.has(data.runningNumber)) { + const job = pendingJobs.get(data.runningNumber) as PendingJob; + + const duration = intervalToDuration({ + start: new Date(), + end: job.scheduledFor, + }); + createLog( + "error", + "materials", + "ocp", + `${ + data.runningNumber + } is pending to be transfered already, remaining time ${formatDuration( + duration, + { format: ["hours", "minutes", "seconds"] } + )}` + ); + return { + success: false, + message: `${ + data.runningNumber + } is pending to be transfered already, remaining time ${formatDuration( + duration, + { format: ["hours", "minutes", "seconds"] } + )}`, + data: [], + }; + } + // get the shift time + const { data: shift, error: shiftError } = (await tryCatch( + query(shiftChange, "shift change from material.") + )) as any; + + if (shiftError) { + createLog( + "error", + "materials", + "ocp", + "There was an error getting the shift times will use fallback times" + ); + } + + // shift split + const shiftTimeSplit = shift?.data[0]?.shiftChange.split(":"); + //console.log(shiftTimeSplit); + // Current time + const now = new Date(); + + // Target time: today at 06:35 + const target = new Date( + now.getFullYear(), + now.getMonth(), + 1, //now.getDate(), + shiftTimeSplit.length > 0 ? parseInt(shiftTimeSplit[0]) - 1 : 5, // this will parse the hour to remove teh zero + shiftTimeSplit.length > 0 ? parseInt(shiftTimeSplit[1]) + 3 : 3, + 0, + 0 + ); + + // to early time + const early = new Date( + now.getFullYear(), + now.getMonth(), + 1, //now.getDate(), + shiftTimeSplit.length > 0 ? parseInt(shiftTimeSplit[0]) - 1 : 5, // this will parse the hour to remove teh zero + shiftTimeSplit.length > 0 ? parseInt(shiftTimeSplit[1]) : 0, + 0, + 0 + ); + + // next month just to be here + const nextMonth = new Date( + now.getFullYear(), + now.getMonth() + 1, + 1, //now.getDate(), + shiftTimeSplit.length > 0 ? parseInt(shiftTimeSplit[0]) - 1 : 5, // this will parse the hour to remove teh zero + shiftTimeSplit.length > 0 ? parseInt(shiftTimeSplit[1]) : 0, + 0, + 0 + ); + + // console.log(early, target); + // if we are to early return early only if we are sending over eom + if (data.type === "eom" && (early > now || target < now)) { + createLog( + "error", + "materials", + "ocp", + `Eom transfers is not allowed right now please try again at ${format( + nextMonth, + "M/d/yyyy hh:mm" + )} ` + ); + return { + success: false, + message: `Eom transfers is not allowed right now please try again at ${format( + nextMonth, + "M/d/yyyy hh:mm" + )} `, + data: [], + }; + } + + let timeoutTrans: number = data.type === "lot" ? 30 : 10; // get the barcode, and layoutID from the running number const { data: label, error: labelError } = (await tryCatch( query( - labelInfo.replace("[runningNr]", `${data.runnungNumber}`), + labelInfo.replace("[runningNr]", `${data.runningNumber}`), "Get label info" ) )) as any; @@ -56,11 +175,11 @@ export const lotMaterialTransfer = async (data: NewLotData) => { "error", "materials", "ocp", - `${data.runnungNumber}: dose not exist or no longer in stock.` + `${data.runningNumber}: dose not exist or no longer in stock.` ); return { success: false, - message: `${data.runnungNumber}: dose not exist or no longer in stock.`, + message: `${data.runningNumber}: dose not exist or no longer in stock.`, data: [], }; } @@ -72,11 +191,11 @@ export const lotMaterialTransfer = async (data: NewLotData) => { "error", "materials", "ocp", - `${data.runnungNumber}: currently in stock and not consumed to a lot.` + `${data.runningNumber}: currently in stock and not consumed to a lot.` ); return { success: false, - message: `${data.runnungNumber}: currently in stock and not consumed to a lot.`, + message: `${data.runningNumber}: currently in stock and not consumed to a lot.`, data: [], }; } @@ -132,11 +251,11 @@ export const lotMaterialTransfer = async (data: NewLotData) => { "error", "materials", "ocp", - `RN:${data.runnungNumber}, Reprinting Error: ${reprint.data.data.message}` + `RN:${data.runningNumber}, Reprinting Error: ${reprint.data.data.message}` ); return { success: false, - message: `RN:${data.runnungNumber}, Reprinting Error: ${reprint.data.data.message}`, + message: `RN:${data.runningNumber}, Reprinting Error: ${reprint.data.data.message}`, data: reprint, }; } @@ -161,11 +280,11 @@ export const lotMaterialTransfer = async (data: NewLotData) => { "error", "materials", "ocp", - `RN:${data.runnungNumber}, Return Error ${matReturn.data.data.errors[0].message}` + `RN:${data.runningNumber}, Return Error ${matReturn.data.data.errors[0].message}` ); return { success: false, - message: `RN:${data.runnungNumber}, Return Error ${matReturn.data.data.errors[0].message}`, + message: `RN:${data.runningNumber}, Return Error ${matReturn.data.data.errors[0].message}`, data: matReturn, }; } @@ -175,9 +294,66 @@ export const lotMaterialTransfer = async (data: NewLotData) => { barcode: label?.data[0].Barcode, }; + const delay = + data.type === "lot" + ? timeoutTrans * 1000 + : target.getTime() - now.getTime(); + + const transfer = await transferMaterial(delay, data, consumeLot, newQty); + + if (!transfer.success) { + return { + success: transfer.success, + message: transfer.message, + data: transfer.data, + }; + } + + const duration = intervalToDuration({ start: now, end: target }); + const pretty = formatDuration(duration, { + format: ["hours", "minutes", "seconds"], + }); + + if (data.type === "eom") { + return { + success: true, + message: `RN:${data.runningNumber}: qty: ${newQty}, will be transfered to lot: ${data.lotNumber}, in ${pretty} `, + data: [], + }; + } else { + return { + success: true, + message: `RN:${data.runningNumber}: qty: ${newQty}, was transfered to lot: ${data.lotNumber}`, + data: [], + }; + } +}; + +const transferMaterial = async ( + delay: number, + data: any, + consumeLot: any, + newQty: any +) => { + //console.log(data); + if (pendingJobs.has(data.runningNumber)) { + createLog( + "error", + "materials", + "ocp", + `${data.runningNumber} is pending to be transfered already` + ); + return { + success: false, + message: `${data.runningNumber} is pending to be transfered already`, + data: [], + }; + } + + const scheduledFor = new Date(Date.now() + delay); // sets the time out based on the type of transfer sent over. - setTimeout( - async () => { + const timeoutId = setTimeout(async () => { + try { const { data: matConsume, error: matConsumeError } = (await tryCatch( runProdApi({ @@ -187,45 +363,59 @@ export const lotMaterialTransfer = async (data: NewLotData) => { }) )) as any; - if (!matConsume.success) { + if (!matConsume?.success) { createLog( "error", "materials", "ocp", - `RN:${data.runnungNumber}, Consume Error ${matConsume.data.data.errors[0].message}` + `RN:${data.runningNumber}, Consume Error ${ + matConsume?.data?.data?.errors?.[0]?.message ?? + "Unknown" + }` ); - return { - success: false, - message: `RN:${data.runnungNumber}, Consume Error ${matConsume.data.data.errors[0].message}`, - data: matConsume, - }; + return; // still hits finally } createLog( "info", "materials", "ocp", - `RN:${data.runnungNumber}: qty: ${newQty}, was transfered to lot:${data.lotNumber}` + `RN:${data.runningNumber}: qty: ${newQty}, was transferred to lot:${data.lotNumber}` ); - }, - data.type === "lot" ? timeoutTrans * 1000 : timeoutTrans * 1000 * 60 - ); + } catch (err) { + createLog( + "error", + "materials", + "ocp", + `RN:${data.runningNumber}, ${err}` + ); + } finally { + // Always clear the pending entry, even if error + pendingJobs.delete(data.runningNumber); + } + }, delay); - if (data.type === "eom") { - return { - success: true, - message: `RN:${data.runnungNumber}: qty: ${newQty}, will be transfered to:${data.lotNumber}, in ${timeoutTrans}min`, - data: [], - }; - } else { - return { - success: true, - message: `RN:${data.runnungNumber}: qty: ${newQty}, was transfered to lot:${data.lotNumber}`, - data: [], - }; - } + pendingJobs.set(data.runningNumber, { + timeoutId, + runningNumber: data.runningNumber, + data, + consumeLot, + newQty, + scheduledFor, + }); + + // Immediately say we scheduled it + return { + success: true, + message: `Transfer for ${data.runningNumber} scheduled`, + data: [], + }; }; +// setInterval(() => { +// console.log(pendingJobs); +// }, 5000); + // setTimeout(async () => { // lotMaterialTransfer({ // runnungNumber: 603468, diff --git a/server/services/ocp/ocpService.ts b/server/services/ocp/ocpService.ts index c895e2b..4460ded 100644 --- a/server/services/ocp/ocpService.ts +++ b/server/services/ocp/ocpService.ts @@ -25,6 +25,7 @@ import bookInLabel from "./routes/labeling/bookIn.js"; import labelRatio from "./routes/labeling/getLabelRatio.js"; import resetRatio from "./routes/labeling/resetLabelRatio.js"; import materialTransferLot from "./routes/materials/lotTransfer.js"; +import pendingTransfers from "./routes/materials/currentPending.js"; import { zechitti1Connect } from "./controller/specialProcesses/zechettis/zechetti1.js"; const app = new OpenAPIHono(); @@ -51,6 +52,7 @@ const routes = [ dycoClose, // materials materialTransferLot, + pendingTransfers, ] as const; const setting = await db.select().from(settings); diff --git a/server/services/ocp/routes/materials/currentPending.ts b/server/services/ocp/routes/materials/currentPending.ts new file mode 100644 index 0000000..7c018dd --- /dev/null +++ b/server/services/ocp/routes/materials/currentPending.ts @@ -0,0 +1,51 @@ +// an external way to creating logs +import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; +import { responses } from "../../../../globalUtils/routeDefs/responses.js"; +import { tryCatch } from "../../../../globalUtils/tryCatch.js"; +import { apiHit } from "../../../../globalUtils/apiHits.js"; + +import { pendingJobs } from "../../controller/materials/lotTransfer.js"; +import { format, formatDuration, intervalToDuration } from "date-fns"; + +const app = new OpenAPIHono({ strict: false }); + +app.openapi( + createRoute({ + tags: ["ocp"], + summary: "Returns pending transfers", + method: "get", + path: "/pendingtransfers", + + responses: responses(), + }), + async (c) => { + apiHit(c, { endpoint: "/pendingtransfers" }); + + const pending = Array.from(pendingJobs.entries()).map( + ([runningNumber, job]) => { + const duration = intervalToDuration({ + start: new Date(), + end: job.scheduledFor, + }); + return { + runningNumber, + lot: job.data?.lotNumber, + newQty: job.newQty, + consumeLot: job.consumeLot, + scheduledFor: format(job.scheduledFor, "M/d/yyyy HH:mm"), + remainingMs: formatDuration(duration, { + format: ["hours", "minutes", "seconds"], + }), + }; + } + ); + + //console.log(pending); + return c.json({ + success: true, + message: "Current Pending trnasfers", + data: [pending], + }); + } +); +export default app; diff --git a/server/services/ocp/routes/materials/lotTransfer.ts b/server/services/ocp/routes/materials/lotTransfer.ts index 991f63f..c2999ab 100644 --- a/server/services/ocp/routes/materials/lotTransfer.ts +++ b/server/services/ocp/routes/materials/lotTransfer.ts @@ -9,7 +9,7 @@ import { lotMaterialTransfer } from "../../controller/materials/lotTransfer.js"; const app = new OpenAPIHono({ strict: false }); const LotTransfer = z.object({ - runnungNumber: z.number().openapi({ example: 1234 }), + runningNumber: z.number().openapi({ example: 1234 }), lotNumber: z.number().openapi({ example: 1235 }), originalAmount: z.number().openapi({ example: 457 }), level: z.number().openapi({ examples: [0.24, 0.5, 0.75, 0.95] }), @@ -45,7 +45,7 @@ app.openapi( ); if (transferError) { - console.log(transferError); + //console.log(transferError); return c.json({ success: false, message: diff --git a/server/services/sqlServer/querys/misc/shiftChange.ts b/server/services/sqlServer/querys/misc/shiftChange.ts new file mode 100644 index 0000000..7e36780 --- /dev/null +++ b/server/services/sqlServer/querys/misc/shiftChange.ts @@ -0,0 +1,4 @@ +export const shiftChange = ` +select top(1) convert(varchar(8) ,convert(time,startdate), 108) as shiftChange + from [test1_AlplaPROD2.0_Read].[masterData].[ShiftDefinition] where teamNumber = 1 +`;