feat(lstv2 move): moved lstv2 into this app to keep them combined and easier to maintain

This commit is contained in:
2025-09-19 22:22:05 -05:00
parent caf2315191
commit e4477402ad
847 changed files with 165801 additions and 0 deletions

View File

@@ -0,0 +1,426 @@
import { eq } from "drizzle-orm";
import { db } from "../../../../../database/dbclient.js";
import { printerData } from "../../../../../database/schema/printers.js";
import { runProdApi } from "../../../../globalUtils/runProdApi.js";
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 = {
runningNumber: number;
lotNumber: number;
originalAmount: number;
level: number;
amount: number;
type: "lot" | "eom";
};
interface PendingJob {
timeoutId: NodeJS.Timeout;
runningNumber: string | number;
data: any;
consumeLot: any;
newQty: any;
scheduledFor: Date;
}
export const pendingJobs = new Map<string | number, PendingJob>();
/**
* Move manual material to a new lot.
*
* The data sent over should be
* Running number
* Lot number
* Orignal Quantity
* level of gaylord
* amount can be sent over as a precise amount
* type what way are we lots
*/
export const lotMaterialTransfer = async (data: NewLotData) => {
// 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.runningNumber}`),
"Get label info"
)
)) as any;
if (labelError) {
createLog(
"error",
"materials",
"ocp",
"There was an error getting the label info"
);
return {
success: false,
message: "There was an error getting the label info",
data: labelError,
};
}
if (label.data.length === 0) {
createLog(
"error",
"materials",
"ocp",
`${data.runningNumber}: dose not exist or no longer in stock.`
);
return {
success: false,
message: `${data.runningNumber}: dose not exist or no longer in stock.`,
data: [],
};
}
//console.log(label);
if (label.data[0]?.stockStatus === "onStock") {
createLog(
"error",
"materials",
"ocp",
`${data.runningNumber}: currently in stock and not consumed to a lot.`
);
return {
success: false,
message: `${data.runningNumber}: currently in stock and not consumed to a lot.`,
data: [],
};
}
// get the pdf24 printer id
const { data: printer, error: printerError } = (await tryCatch(
db.select().from(printerData).where(eq(printerData.name, "PDF24"))
)) as any;
if (printerError) {
createLog(
"error",
"materials",
"ocp",
"There was an error the printer info"
);
return {
success: false,
message: "There was an error the printer info",
data: printerError,
};
}
// calculate the remaining amount bascially it will be orignal number * level sent over
// level should be sent in a decimal .25 .5 .75 .95 the 95 will allow basically the what looks to be a full gaylord but we always want to consume something
const newQty =
data.amount > 0
? data.amount
: (data.originalAmount * data.level).toFixed(0);
//console.log(data.amount);
// reprint the label and send it to pdf24
const reprintData = {
clientId: 999,
runningNo: label?.data[0].runnungNumber,
printerId: printer[0].humanReadableId,
layoutId: label?.data[0].labelLayout,
noOfCopies: 0,
quantity: newQty,
} as any;
//console.log(reprintData);
const { data: reprint, error: reprintError } = (await tryCatch(
runProdApi({
endpoint: "/public/v1.0/ProductionLabelling/ReprintLabel",
data: [reprintData],
})
)) as any;
if (!reprint.success) {
createLog(
"error",
"materials",
"ocp",
`RN:${data.runningNumber}, Reprinting Error: ${reprint.data.data.message}`
);
return {
success: false,
message: `RN:${data.runningNumber}, Reprinting Error: ${reprint.data.data.message}`,
data: reprint,
};
}
// return the label back to fm1 lane id 10001
const matReturnData = {
barcode: label?.data[0].Barcode,
laneId: 10001,
};
//console.log(matReturnData);
const { data: matReturn, error: matReturError } = (await tryCatch(
runProdApi({
endpoint:
"/public/v1.0/IssueMaterial/ReturnPartiallyConsumedManualMaterial",
data: [matReturnData],
})
)) as any;
if (!matReturn.success) {
createLog(
"error",
"materials",
"ocp",
`RN:${data.runningNumber}, Return Error ${matReturn.data.data.errors[0].message}`
);
return {
success: false,
message: `RN:${data.runningNumber}, Return Error ${matReturn.data.data.errors[0].message}`,
data: matReturn,
};
}
// consume to the lot provided.
const consumeLot = {
productionLot: data.lotNumber,
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.
const timeoutId = setTimeout(async () => {
try {
const { data: matConsume, error: matConsumeError } =
(await tryCatch(
runProdApi({
endpoint:
"/public/v1.0/IssueMaterial/ConsumeNonPreparedManualMaterial",
data: [consumeLot],
})
)) as any;
if (!matConsume?.success) {
createLog(
"error",
"materials",
"ocp",
`RN:${data.runningNumber}, Consume Error ${
matConsume?.data?.data?.errors?.[0]?.message ??
"Unknown"
}`
);
return; // still hits finally
}
createLog(
"info",
"materials",
"ocp",
`RN:${data.runningNumber}: qty: ${newQty}, was transferred to lot:${data.lotNumber}`
);
} catch (err) {
createLog(
"error",
"materials",
"ocp",
`RN:${data.runningNumber}, ${err}`
);
} finally {
// Always clear the pending entry, even if error
pendingJobs.delete(data.runningNumber);
}
}, delay);
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,
// lotNumber: 24897,
// originalAmount: 380,
// level: 0.95,
// });
// }, 5000);

View File

@@ -0,0 +1,235 @@
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
import { createLog } from "../../../logger/logger.js";
import { serverSettings } from "../../../server/controller/settings/getSettings.js";
import { query } from "../../../sqlServer/prodSqlServer.js";
import { machineCheck } from "../../../sqlServer/querys/ocp/machineId.js";
import { mmQuery } from "../../../sqlServer/querys/ocp/mainMaterial.js";
export const isMainMatStaged = async (lot: any) => {
const set = serverSettings.length === 0 ? [] : serverSettings;
// make staged false by deefault and error logged if theres an issue
let isStaged = { message: "Material is staged", success: true };
const { data, error } = (await tryCatch(
query(
machineCheck.replace("where Active = 1 and [Location] = [loc]", ""),
"check machine needs mm"
)
)) as any;
const machine = data.data.filter(
(m: any) => m.HumanReadableId === lot.machineID
);
// we have a check on ksc side to ignore the tetra machine for now as its not updating in 2.0
if (!machine[0].StagingMainMaterialMandatory) {
createLog(
"info",
"mainMaterial",
"ocp",
`The machine dose not require mm to print and book in.`
);
return {
message: "Machine dose not require material to be staged",
success: true,
};
}
// strangly the lot is not always sending over in slc so adding this in for now to see what line is cauing this issue
if (!lot) {
return isStaged;
}
if (typeof lot !== "object" || lot === null || Array.isArray(lot)) {
createLog(
"info",
"mainMaterial",
"ocp",
`The lot sent over is not an object: ${JSON.stringify(lot)}`
);
return isStaged;
}
const updateQuery = mmQuery.replaceAll("[lotNumber]", lot.lot);
try {
const r: any = await query(updateQuery, "Main Material Check");
const res: any = r.data;
// if (res[0].Staged >= 1) {
// isStaged = true;
// }
type CheckConditionArgs = {
results: any[];
filterFn: (n: any) => boolean;
failCondition: (n: any) => boolean;
failMessage: string;
successMessage: string;
lot: { lot: string | number };
};
const checkCondition = ({
results,
filterFn,
failCondition,
failMessage,
successMessage,
lot,
}: CheckConditionArgs): { message: string; success: boolean } => {
const subset = results.filter(filterFn);
if (subset.some(failCondition)) {
const failing = subset.filter(failCondition);
return {
message: `lot: ${lot.lot}, is missing: ${failing
.map(
(o: any) =>
`${o.MaterialHumanReadableId} - ${o.MaterialDescription}`
)
.join(",\n ")} ${failMessage}`,
success: false,
};
} else {
return { message: successMessage, success: true };
}
};
createLog("info", "mainMaterial", "ocp", `Maint material query ran.`);
const mainMaterial = res.find((n: any) => n.IsMainMaterial);
if (mainMaterial?.noMMShortage === "noMM") {
return {
message: `Main material: ${mainMaterial.MaterialHumanReadableId} - ${mainMaterial.MaterialDescription}: is not staged for ${lot.lot}`,
success: false,
};
}
// we need to filter the color stuff and then look for includes instead of a standard name. this way we can capture a everything and not a single type
// for manual consume color if active to check colors
const checkColorSetting = set.filter((n) => n.name === "checkColor");
// 2. Auto color
if (checkColorSetting[0].value === "1") {
// auto check
// 2. Auto color
const autoColor = checkCondition({
results: res,
lot,
filterFn: (n) =>
n.isManual &&
!("noPKGAutoShortage" in n) &&
!("noPKGManualShortage" in n), // pool = non-main, auto
failCondition: (n) => n.autoConsumeCheck === "autoConsumeNOK", // column = autoConsumeCheck
failMessage: "for autoconsume",
successMessage: "auto color is good",
});
if (!autoColor.success) return autoColor;
console.log(autoColor);
// 3. Manual color
const manualColor = checkCondition({
results: res,
lot,
filterFn: (n) =>
!n.IsMainMaterial &&
n.isManual &&
!("noPKGAutoShortage" in n) &&
!("noPKGManualShortage" in n), // pool = non-main, manual
failCondition: (n) => n.noManualShortage === "noOK", // column = noManualShortage
failMessage: "for manual material",
successMessage: "manual color is good",
});
if (!manualColor.success) return manualColor;
console.log(manualColor);
} else {
createLog(
"info",
"mainMaterial",
"ocp",
"Color check is not active."
);
}
// // if we want to check the packaging
const checkPKGSetting = set.filter((n) => n.name === "checkPKG");
if (checkPKGSetting[0].value === "1") {
const pkgAuto = checkCondition({
results: res,
lot,
filterFn: (n) =>
!n.IsMainMaterial &&
!n.isManual &&
"noPKGAutoShortage" in n,
failCondition: (n) => n.noPKGAutoShortage === "noAutoPkg",
failMessage: "for pkg",
successMessage: "auto PKG is good",
});
if (!pkgAuto.success) return pkgAuto;
console.log(pkgAuto);
// 5. Packaging manual
const pkgManual = checkCondition({
results: res,
lot,
filterFn: (n) =>
!n.IsMainMaterial &&
n.isManual &&
"noPKGManualShortage" in n,
failCondition: (n) => n.noPKGManualShortage === "noManPkg",
failMessage: "for pkg",
successMessage: "manual PKG is good",
});
if (!pkgManual.success) return pkgManual;
console.log(pkgManual);
} else {
createLog(
"info",
"mainMaterial",
"ocp",
"PKG check is not active."
);
}
// manual pkg
if (checkPKGSetting[0].value === "1") {
const packagingCheck = res.filter(
(n: any) =>
!n.IsMainMaterial &&
n.isManual &&
"noPKGManualShortage" in n
);
if (
packagingCheck.some(
(n: any) => n.noPKGManualShortage === "noManPkg"
)
) {
return (isStaged = {
message: `lot: ${lot.lot}, is missing: ${packagingCheck
.map(
(o: any) =>
`${o.MaterialHumanReadableId} - ${o.MaterialDescription}`
)
.join(",\n ")} for pkg`,
success: false,
});
}
} else {
createLog(
"info",
"mainMaterial",
"ocp",
"PKG check is not active."
);
}
} catch (err) {
createLog(
"error",
"mainMaterial",
"ocp",
`Error from running the Main Material query: ${err}`
);
}
return isStaged;
};