8 Commits

Author SHA1 Message Date
2ebf695526 fix(builds): fixed non used variables
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m32s
2026-06-15 17:16:29 -05:00
24af3ca403 fix(datamart): somereason it stopped working.. and added download types
there was a weird issue with the req.query that cause nothing to pull and make the excel files
lag..... now excel macros are using the csv pull from here and added in the xlsx to download to bc
why not makes it easier for later  and can have bbuttons for every thing in lst too :D
2026-06-15 17:16:00 -05:00
6fbe3a9eed chore(format): formatting changes 2026-06-15 17:14:05 -05:00
7dbc31c046 refactor(dm): mapped remainder of the forecast. will need to run backup test
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 40s
the test server wouldnt do the backup so waiting on this one
2026-06-15 01:03:50 -05:00
ba09a77f29 chore(logistics): renamed the history util to better represent what it is 2026-06-15 01:03:02 -05:00
3f04609f82 refactor(dockdoorscanning): only show a truly active loading order 0 should never show up
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m29s
2026-06-14 03:54:21 -05:00
22a7b612e1 fix(dockdoorscanner): corrections to how things get posted so it makes more sense 2026-06-14 03:53:34 -05:00
39db142db4 feat(dm): standard forecast added in rest of migration to come 2026-06-14 03:52:34 -05:00
29 changed files with 6864 additions and 52 deletions

View File

@@ -37,6 +37,7 @@ const lstDbRun = async (data: Data) => {
if (data.options) {
if (data.name === "psiInventory") {
const ids = data.options.articles.split(",").map((id: any) => id.trim());
const whse = data.options.whseToInclude
? data.options.whseToInclude
.split(",")

View File

@@ -1,4 +1,5 @@
import { Router } from "express";
import * as XLSX from "xlsx";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { runDatamartQuery } from "./datamart.controller.js";
@@ -7,13 +8,73 @@ const r = Router();
type Options = {
name: string;
value: string;
format: string;
};
r.get("/:name", async (req, res) => {
const { name } = req.params;
const options = req.query as Options;
const options = { ...req.query } as Options;
const dataRan = await runDatamartQuery({ name, options });
if (!dataRan.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: dataRan.message,
status: 500,
});
}
// XLSX Export
if (options.format?.toLowerCase() === "xlsx") {
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(dataRan.data);
XLSX.utils.book_append_sheet(wb, ws, name);
const buffer = XLSX.write(wb, {
type: "buffer",
bookType: "xlsx",
});
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
);
res.setHeader("Content-Disposition", `attachment; filename="${name}.xlsx"`);
return res.send(buffer);
}
// CSV Export
if (options.format?.toLowerCase() === "csv") {
const rows = dataRan.data as any;
if (!rows.length) {
return res.status(200).send("");
}
const headers = Object.keys(rows[0]);
const csv = [
headers.join(","),
...rows.map((row: any) =>
headers
.map((h) => `"${String(row[h] ?? "").replace(/"/g, '""')}"`)
.join(","),
),
].join("\r\n");
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", `attachment; filename="${name}.csv"`);
return res.send(csv);
}
return apiReturn(res, {
success: dataRan.success,
level: "info",

View File

@@ -0,0 +1,22 @@
import { jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const forecastImport = pgTable("forecast_import", {
id: uuid("id").defaultRandom().primaryKey(),
receivingPlantId: text("receiving_plant_id").notNull(),
documentName: text("documentName"),
sender: text("sender"),
customerId: text("customer_id"),
rawData: jsonb("raw_data").default([]),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});
export const forecastImportSchema = createSelectSchema(forecastImport);
export const newForecastImportSchema = createInsertSchema(forecastImport);
export type ForecastImport = z.infer<typeof forecastImportSchema>;
export type NeworecastImport = z.infer<typeof newForecastImportSchema>;

View File

@@ -27,6 +27,7 @@ const postScan = async (data: any) => {
loadingUnit: data.loadingUnit.sscc ?? data.loadingUnit.runningNo, // can be running number or sscc depending on where it came from
loadingUnitStatus: data.loadingUnitStatus, // TODO: add enums on the status of each load.
message: data.message, // the response it gave when scanning
status: data.status ?? undefined,
})
.returning()) as any;
@@ -76,6 +77,7 @@ const loadUnit = async (data: Data) => {
loadingUnitStatus: "notLoaded",
message:
"There are know current active loading orders please start one and try again.",
status: "error",
});
return returnFunc({
success: true,
@@ -86,7 +88,6 @@ const loadUnit = async (data: Data) => {
"There are know current active loading orders please start one and try again.",
data: [],
notify: false,
room: `dockDoorLoading:${data.dockId}`,
});
}
// check if its a valids an sscc
@@ -99,6 +100,7 @@ const loadUnit = async (data: Data) => {
loadingUnitStatus: "noread",
message:
"Failed to load the unit to the truck, there was no pallet read.",
status: "error",
});
return returnFunc({
@@ -110,7 +112,6 @@ const loadUnit = async (data: Data) => {
"Failed to load the unit to the truck, there was no pallet read.",
data: [],
notify: false,
room: `dockDoorLoading:${data.dockId}`,
});
}
@@ -137,6 +138,7 @@ const loadUnit = async (data: Data) => {
loadingUnit: unitToScan,
loadingUnitStatus: "notLoaded",
message: prod?.data.errors[0].message,
status: "error",
});
return returnFunc({

View File

@@ -12,7 +12,9 @@ export const getRecentDockScans = ({
where: (scans, { and, eq }) =>
and(
eq(scans.status, "active"),
loadingOrder ? eq(scans.loadingOrder, loadingOrder) : undefined,
loadingOrder
? eq(scans.loadingOrder, loadingOrder)
: eq(scans.loadingOrder, "0"),
),
orderBy: (scans, { desc }) => [desc(scans.upd_date)],
limit,

View File

@@ -0,0 +1,92 @@
import XLSX from "xlsx";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { postData } from "./logistics.dm.postData.js";
export const abbottForecast = async (sheet: any, user: any) => {
/*
This is the forecast but will only be triggered when the actual sheet is passed over from the orders in. this is being done this way due to the truck list being sent over as well.
*/
const customerId = 8;
const posting: any = [];
const customHeaders = [
"date",
"time",
"newton8oz",
"newton10oz",
"E",
"F",
"fDate",
"f8ozqty",
"I",
"J",
"K",
"L",
"M",
"f10ozqty",
];
const forecastData = XLSX.utils.sheet_to_json(sheet, {
range: 5, // Start at row 5 (index 4)
header: customHeaders,
defval: "", // Default value for empty cells
});
for (let i = 1; i < forecastData.length; i++) {
const row: any = forecastData[i];
//console.log(row);
//if (row.fDate == undefined) continue;
if (row.fDate !== "") {
const date = isNaN(row.fDate)
? new Date(row.fDate)
: excelDateStuff(row.fDate);
// for 8oz do
if (row.f8ozqty > 0) {
posting.push({
customerArticleNo: "45300DA",
quantity: row.f8ozqty,
requirementDate: date,
});
}
if (row.f10ozqty > 0) {
posting.push({
customerArticleNo: "43836DA",
quantity: row.f10ozqty,
requirementDate: date,
});
}
}
}
// the predefined data that will never change
const predefinedObject = {
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: customerId,
positions: [],
};
// add the new forecast to the predefined data
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...posting],
};
const forecast: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
return {
success: forecast.success,
message: forecast.message,
data: forecast.data,
};
};

View File

@@ -0,0 +1,86 @@
import XLSX from "xlsx";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { postData } from "./logistics.dm.postData.js";
export const energizerForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
*/
const buffer = Buffer.from(data.buffer);
const workbook = XLSX.read(buffer, { type: "buffer" });
const sheet: any = workbook.Sheets.Sheet1;
// const range = XLSX.utils.decode_range(sheet["!ref"]);
// const headers = [
// "CustomerArticleNumber",
// "Quantity",
// "RequirementDate",
// "CustomerID",
// ];
// formatting the data
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 }) as any;
const posting: any = [];
const customerId = 44;
for (let i = 1; i < rows.length; i++) {
const row: any = rows[i];
const material = row[0];
if (material === undefined) continue;
for (let j = 1; j < row.length; j++) {
const qty = row[j];
if (qty && qty > 0) {
const requirementDate = rows[0][j]; // first row is dates
const date = Number.isNaN(requirementDate)
? new Date(requirementDate)
: excelDateStuff(requirementDate);
posting.push({
customerArticleNo: material,
quantity: qty,
requirementDate: date,
});
}
}
}
//console.log(posting);
// the predefined data that will never change
const predefinedObject = {
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: customerId,
positions: [],
};
// add the new forecast to the predefined data
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...posting],
};
//post it
const forecastData: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
return {
success: forecastData.success,
message: forecastData.message,
data: forecastData.data,
};
};

View File

@@ -0,0 +1,245 @@
import { addDays } from "date-fns";
import XLSX from "xlsx";
import { runDatamartQuery } from "../datamart/datamart.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
//import { sendEmail } from "../utils/sendEmail.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { postData } from "./logistics.dm.postData.js";
const customerID = 4;
export const lorealForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
*/
const buffer = Buffer.from(data.buffer);
const workbook = XLSX.read(buffer, { type: "buffer" });
const sheet: any = workbook.Sheets["Alpla HDPE"];
const range = XLSX.utils.decode_range(sheet["!ref"]);
const psheet: any = workbook.Sheets["Alpla PET"];
const prange = XLSX.utils.decode_range(psheet["!ref"]);
const headers = [];
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: 1, c: C }); // row 0 = Excel row 1
const cell = sheet[cellAddress];
headers.push(cell ? cell.v : undefined);
}
const pheaders = [];
for (let C = prange.s.c; C <= prange.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: 1, c: C }); // row 0 = Excel row 1
const cell = psheet[cellAddress];
pheaders.push(cell ? cell.v : undefined);
}
const ebmForeCastData: any = XLSX.utils.sheet_to_json(sheet, {
defval: "",
header: headers,
range: 3,
});
const petForeCastData: any = XLSX.utils.sheet_to_json(psheet, {
defval: "",
header: pheaders,
range: 3,
});
const ebmForecastData: any = [];
const missingSku: any = [];
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
if (!avSQLQuery.success) {
return returnFunc({
success: false,
level: "error",
module: "logistics",
subModule: "forecast",
message: `Error getting Article info`,
data: [avSQLQuery.message],
notify: true,
});
}
const { data: a, error: ae } = await tryCatch(
runDatamartQuery({ name: "activeArticles", options: {} }),
);
if (ae) {
return {
success: false,
message: "Error getting active av",
data: [],
};
}
const article: any = a?.data;
//console.log(article);
// process the ebm forcast
for (let i = 0; i < ebmForeCastData.length; i++) {
// bottle code
const sku = ebmForeCastData[i]["HDPE Bottle Code"];
// ignore the blanks
if (sku === "") continue;
// ignore zero qty
// if (ebmForeCastData[i][`Day ${i}`]) continue;
for (let f = 0; f <= 90; f++) {
const day = `Day ${f + 1}`;
// if (ebmForeCastData[i][day] === 0) continue;
const forcast = {
customerArticleNo: sku,
requirementDate: addDays(new Date(Date.now()), f), //excelDateStuff(parseInt(date)),
quantity: ebmForeCastData[i][day] ?? 0,
};
if (forcast.quantity === 0) continue;
// checking to make sure there is a real av to add to.
const activeAV = article.filter(
(c: any) =>
c?.CustomerArticleNumber === forcast.customerArticleNo.toString(),
);
if (activeAV.length === 0) {
if (typeof forcast.customerArticleNo === "number") {
missingSku.push(forcast);
}
continue;
}
ebmForecastData.push(forcast);
}
//console.log(ebmForeCastData.length);
}
// pet forecast
for (let i = 0; i < petForeCastData.length; i++) {
// bottle code
const sku = petForeCastData[i]["South PET Bottle Code"];
// ignore the blanks
if (sku === "") continue;
// ignore zero qty
// if (ebmForeCastData[i][`Day ${i}`]) continue;
for (let f = 0; f <= 90; f++) {
const day = `Day ${f + 1}`;
// if (ebmForeCastData[i][day] === 0) continue;
const forcast = {
customerArticleNo: sku,
requirementDate: addDays(new Date(Date.now()), f), //excelDateStuff(parseInt(date)),
quantity: petForeCastData[i][day] ?? 0,
};
if (forcast.quantity === 0 || forcast.quantity === "") continue;
if (forcast.customerArticleNo < 99999) {
//console.log(`Sku a normal av ${forcast.customerArticleNo}`);
continue;
}
// checking to make sure there is a real av to add to.
const activeAV = article.filter(
(c: any) =>
c?.CustomerArticleNumber === forcast.customerArticleNo.toString(),
);
if (activeAV.length === 0) {
if (typeof forcast.customerArticleNo === "number") {
missingSku.push(forcast);
}
continue;
}
ebmForecastData.push(forcast);
}
}
//console.log(comForecast);
// email the for the missing ones
// const missedGrouped = Object.values(
// missingSku.reduce((acc: any, item: any) => {
// const key = item.customerArticleNo;
// if (!acc[key]) {
// // first time we see this customer
// acc[key] = item;
// } else {
// // compare dates and keep the earliest
// if (
// new Date(item.requirementDate) < new Date(acc[key].requirementDate)
// ) {
// acc[key] = item;
// }
// }
// return acc;
// }, {}),
// );
// TODO: change this to a flagged notification so that he users can subscribe or leave it. this removes the hardcody shit.
// const emailSetup = {
// email:
// "Blake.matthes@alpla.com; Stuart.Gladney@alpla.com; Harold.Mccalister@alpla.com; Jenn.Osbourn@alpla.com",
// subject:
// missedGrouped.length > 0
// ? `Alert! There are ${missedGrouped.length}, missing skus.`
// : `Alert! There is a missing SKU.`,
// template: "missingLorealSkus",
// context: {
// items: missedGrouped,
// },
// };
// sendEmail(emailSetup);
// if the customerarticle number is not matching just ignore it
const predefinedObject = {
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: customerID,
positions: [],
};
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...ebmForecastData],
};
// console.log(updatedPredefinedObject);
// posting the data to the new backend so we can store the data.
const posting: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
return {
success: posting.success,
message: posting.message,
data: posting.data === "" ? ebmForecastData : posting.data,
};
};

View File

@@ -0,0 +1,182 @@
import XLSX from "xlsx";
import { runDatamartQuery } from "../datamart/datamart.controller.js";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.schema.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { postData } from "./logistics.dm.postData.js";
export const pNgForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
*/
const { data: s, error: e } = await tryCatch(db.select().from(settings));
if (e) {
return {
sucess: false,
message: `Error getting settings`,
data: e,
};
}
const pNg = s.filter((n: any) => n.name === "pNgAddress");
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
if (!avSQLQuery.success) {
return returnFunc({
success: false,
level: "error",
module: "logistics",
subModule: "forecast",
message: `Error getting Article info`,
data: [avSQLQuery.message],
notify: true,
});
}
const { data: a, error: ae } = await tryCatch(
runDatamartQuery({ name: "activeArticles", options: {} }),
);
if (ae) {
return {
success: false,
message: "Error getting active av",
data: [],
};
}
const article: any = a?.data;
const buffer = Buffer.from(data.buffer);
const workbook = XLSX.read(buffer, { type: "buffer" });
//const sheet: any = workbook.Sheets[sheetName];
const sheet: any = workbook.Sheets["SchedAgreementUIConfigSpreadshe"];
const range = XLSX.utils.decode_range(sheet["!ref"]);
const headers = [];
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: 0, c: C }); // row 0 = Excel row 1
const cell = sheet[cellAddress];
headers.push(cell ? cell.v : undefined);
}
//console.log(headers);
const forecastData: any = XLSX.utils.sheet_to_json(sheet, {
defval: "",
header: headers,
range: 1,
});
const groupedByCustomer: any = forecastData.reduce((acc: any, item: any) => {
const id = item.CustomerID;
if (!acc[id]) {
acc[id] = [];
}
acc[id].push(item);
return acc;
}, {});
const foreCastData: any = [];
for (const [customerID, forecast] of Object.entries(groupedByCustomer)) {
//console.log(`Running for Customer ID: ${customerID}`);
const newForecast: any = forecast;
const predefinedObject = {
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: pNg[0]?.value,
positions: [],
};
// map everything out for each order
const nForecast = newForecast.map((o: any) => {
// const invoice = i.filter(
// (i: any) => i.deliveryAddress === parseInt(customerID)
// );
// if (!invoice) {
// return;
// }
return {
customerArticleNo: parseInt(o["Customer Item No."] ?? "0", 10),
requirementDate: excelDateStuff(parseInt(o["Request Date"] ?? "0", 10)),
quantity: o["Remaining Qty to be Shipped"],
};
});
// check to make sure the av belongs in this plant.
const onlyNumbers = nForecast.filter((n: any) => n.quantity > 0);
const filteredForecast: any = [];
for (let i = 0; i < nForecast.length; i++) {
//console.log(nForecast[i].customerArticleNo);
const activeAV = article.filter(
(c: any) =>
c?.CustomerArticleNumber ===
nForecast[i]?.customerArticleNo.toString() &&
// validate it works via the default address
c?.IdAdresse === parseInt(pNg[0]?.value ?? "139", 10),
);
if (activeAV.length > 0) {
filteredForecast.push(onlyNumbers[i]);
}
}
if (filteredForecast.length === 0) {
console.log("Nothing to post");
return {
success: true,
message: "No forecast to be posted",
data: foreCastData,
};
}
// do that fun combining thing
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...filteredForecast],
};
//console.log(updatedPredefinedObject);
// post the orders to the server
const posting: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
foreCastData.push({
customer: customerID,
//totalOrders: orders?.length(),
success: posting.success,
message: posting.message,
data: posting.data,
});
}
return {
success: foreCastData[0].success,
message: foreCastData[0].message,
data: foreCastData,
};
};

View File

@@ -0,0 +1,104 @@
import * as XLSX from "xlsx";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { postData } from "./logistics.dm.postData.js";
export const standardForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
*/
const plantToken = process.env.PROD_PLANT_TOKEN;
//const arrayBuffer = await data.arrayBuffer();
const buffer = Buffer.from(data.buffer);
const workbook = XLSX.read(buffer, { type: "buffer" });
const sheetName = workbook.SheetNames[0] as string;
const sheet = workbook.Sheets[sheetName] as any;
const headers = [
"CustomerArticleNumber",
"Quantity",
"RequirementDate",
"CustomerID",
];
const forecastData: any = XLSX.utils.sheet_to_json(sheet, {
defval: "",
header: headers,
range: 1,
});
const groupedByCustomer: any = forecastData.reduce((acc: any, item: any) => {
const id = item.CustomerID;
if (!acc[id]) {
acc[id] = [];
}
acc[id].push(item);
return acc;
}, {});
const foreCastData: any = [];
for (const [customerID, forecast] of Object.entries(groupedByCustomer)) {
//console.log(`Running for Customer ID: ${customerID}`);
const newForecast: any = forecast;
const predefinedObject = {
receivingPlantId: plantToken,
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: customerID,
positions: [],
};
// map everything out for each order
const nForecast = newForecast.map((o: any) => {
// const invoice = i.filter(
// (i: any) => i.deliveryAddress === parseInt(customerID)
// );
// if (!invoice) {
// return;
// }
return {
customerArticleNo: o.CustomerArticleNumber,
requirementDate: excelDateStuff(parseInt(o.RequirementDate)),
quantity: o.Quantity,
};
});
// do that fun combining thing
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...nForecast],
};
//console.log(updatedPredefinedObject);
// post the orders to the server
const posting: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
foreCastData.push({
customer: customerID,
//totalOrders: orders?.length(),
success: posting.success,
message: posting.message,
data: posting.data,
});
}
return {
success: foreCastData[0].success,
message: foreCastData[0].message,
data: foreCastData,
};
};

View File

@@ -0,0 +1,95 @@
import { Router } from "express";
import multer from "multer";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { energizerForecast } from "./logistics.dm.forecast.map.energizer.js";
import { lorealForecast } from "./logistics.dm.forecast.map.loreal.js";
import { pNgForecast } from "./logistics.dm.forecast.map.pNg.js";
import { standardForecast } from "./logistics.dm.forecast.map.standard.js";
type ForecastResult = {
success?: boolean;
message?: string;
data?: unknown;
};
const r = Router();
const upload = multer({
storage: multer.memoryStorage(),
});
r.post("/", requireAuth, upload.single("file"), async (req, res) => {
if (!req.file) {
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "forecast",
message: "A file must be added to be able to run the forecast.",
data: [],
status: 400,
});
}
const { fileType } = req.body;
if (typeof fileType !== "string") {
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "forecast",
message: "A fileType must be provided.",
data: [],
status: 400,
});
}
//console.log("fileType:", req.body.fileType);
let result: ForecastResult;
switch (fileType) {
case "standard":
result = await standardForecast(req.file, req.user);
break;
case "loreal":
result = await lorealForecast(req.file, req.user);
break;
case "pg":
result = await pNgForecast(req.file, req.user);
break;
case "energizer":
result = await energizerForecast(req.file, req.user);
break;
default:
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "forecast",
message: `Invalid fileType: ${fileType}`,
data: [],
status: 400,
});
}
return apiReturn(res, {
success: result.success ?? false,
level: result.success ? "info" : "error",
module: "dm",
subModule: "forecast",
message: result.success
? "The forecast was accepted by Alplaprod 2.0 please check to make sure everything processed properly."
: (result.message as string),
data: result.data ?? ([] as any),
status: result.success ? 200 : 500,
});
});
export default r;

View File

@@ -0,0 +1,65 @@
import { db } from "../db/db.controller.js";
import { forecastImport } from "../db/schema/forecastImports.schema.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
type PostData = {
receivingPlantId: string;
documentName: string;
sender: string;
customerId: string;
positions: unknown[];
};
type Data = {
type: "orders" | "forecast";
endpoint: string;
data: PostData;
};
export const postData = async (data: Data, user: any) => {
const posting = await runProdApi(
{
method: "post",
endpoint: data.endpoint,
data: [data.data],
},
"Forecast post",
);
if (!posting?.success) {
return returnFunc({
success: false,
level: "error",
module: "dm",
subModule: data.type === "orders" ? "orders" : "forecast",
message:
posting?.message ??
`Error in posting the ${data.type === "orders" ? "orders" : "forecast"} data`,
data: posting?.data ?? [],
notify: false,
});
}
if (posting.success) {
if (data.type === "forecast") {
await db.insert(forecastImport).values({
receivingPlantId: data.data.receivingPlantId ?? "test1",
documentName: data.data.documentName ?? "forecast-data-missing",
sender: data.data.sender ?? "lst-user",
customerId: data.data.customerId ?? "0",
rawData: data ?? [],
add_user: user.username ?? undefined,
upd_user: user.username ?? undefined,
});
}
return returnFunc({
success: true,
level: "info",
module: "dm",
subModule: data.type === "orders" ? "orders" : "forecast",
message: posting?.message ?? "",
data: (data.data as any) ?? [],
notify: false,
});
}
};

View File

@@ -0,0 +1,79 @@
import { format } from "date-fns";
import { Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { excelTemplate, type Template } from "../utils/excelTemplates.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", requireAuth, async (req, res) => {
const { filename } = req.query;
const templateNames = ["orders", "forecast"];
if (typeof filename !== "string" || !templateNames.includes(filename)) {
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "template",
message: "You are required to pass over the template name.",
data: [],
status: 500,
});
}
const name = `${filename}-Template-${format(
new Date(Date.now()),
"M-d-yyyy",
)}.xlsx`;
const standardHeaders = [
"CustomerArticleNumber",
"CustomerOrderNumber",
"CustomerLineNumber",
"CustomerRealeaseNumber",
"Quantity",
"DeliveryDate",
"CustomerID",
"Remark",
// "InvoiceID",
];
const forecastHeaders = [
"CustomerArticleNumber",
"Quantity",
"RequirementDate",
"CustomerID",
];
const template = {
name,
headers: filename === "orders" ? standardHeaders : forecastHeaders,
} as Template;
//console.log(template);
const { data, error } = await tryCatch(excelTemplate(template));
if (error || !data) {
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "template",
message: "There was an error creating the Excel template.",
data: [],
status: 500,
});
}
res.set({
"Content-Type":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition": `attachment; filename="${name}"`,
});
return res.status(200).send(data);
});
export default r;

View File

@@ -0,0 +1,21 @@
import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import forecast from "./logistics.dm.forecast.route.js";
import createTemplate from "./logistics.dm.template.route.js";
export const setupLogisticsRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(
`${baseUrl}/api/logistics/dm/template`,
featureCheck("demandManagement"),
createTemplate,
);
app.use(
`${baseUrl}/api/logistics/dm/forecast`,
featureCheck("demandManagement"),
forecast,
);
// all other system should be under /api/system/*
};

View File

@@ -7,6 +7,7 @@ import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupDockDoorRoutes } from "./dockdoorScanning/dockdoor.routes.js";
import { setupEomRoutes } from "./eom/eom.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupLogisticsRoutes } from "./logistics/logistics.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js";
@@ -18,13 +19,13 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
export const setupRoutes = (baseUrl: string, app: Express) => {
//routes that are on by default
setupDatamartRoutes(baseUrl, app);
setupMobileRoutes(baseUrl, app);
setupSystemRoutes(baseUrl, app);
setupAdminRoutes(baseUrl, app);
setupApiDocsRoutes(baseUrl, app);
setupProdSqlRoutes(baseUrl, app);
setupGPSqlRoutes(baseUrl, app);
setupDatamartRoutes(baseUrl, app);
setupAuthRoutes(baseUrl, app);
setupUtilsRoutes(baseUrl, app);
setupOpendockRoutes(baseUrl, app);
@@ -33,4 +34,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupTCPRoutes(baseUrl, app);
setupDockDoorRoutes(baseUrl, app);
setupEomRoutes(baseUrl, app);
setupLogisticsRoutes(baseUrl, app);
};

View File

@@ -8,7 +8,7 @@ import { dbCleanup } from "./db/dbCleanup.controller.js";
import { type Setting, settings } from "./db/schema/settings.schema.js";
import { connectGPSql } from "./gpSql/gpSqlConnection.controller.js";
import { createLogger } from "./logger/logger.controller.js";
import { historicalSchedule } from "./logistics/logistics.historicalInv.js";
import { historicalSchedule } from "./logistics/logistics.utils.historicalInv.js";
import { startNotifications } from "./notification/notification.controller.js";
import { sqlJobCleanUp } from "./notification/notification.SqlJobCleanUp.js";
import { createNotifications } from "./notification/notifications.master.js";

View File

@@ -0,0 +1,49 @@
import * as XLSX from "xlsx";
export type Template = {
name: string;
headers: string[];
};
export const excelTemplate = async (data: Template) => {
/**
* Creates the standard Template for bulk orders in
*/
// const headers = [
// [
// "CustomerArticleNumber",
// "CustomerOrderNumber",
// "CustomerLineNumber",
// "CustomerRealeaseNumber",
// "Quantity",
// "DeliveryDate",
// "CustomerID",
// "Remark",
// // "InvoiceID",
// ],
// ];
// create a new workbook
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet([data.headers]);
//const ws2 = XLSX.utils.aoa_to_sheet(headers2);
const columnWidths = data.headers.map((header) => ({
width: header.length + 2,
}));
ws["!cols"] = columnWidths;
// append the worksheet to the workbook
XLSX.utils.book_append_sheet(wb, ws, `Sheet1`);
//XLSX.utils.book_append_sheet(wb, ws2, `Sheet2`);
// Creates the file to disk'
// XLSX.writeFile(wb, data.name);
// Write the workbook to a buffer and return it
const excelBuffer = XLSX.write(wb, { bookType: "xlsx", type: "buffer" });
return excelBuffer;
};

View File

@@ -0,0 +1,28 @@
import { getJsDateFromExcel } from "excel-date-to-js";
export const excelDateStuff = (serial: number, time?: any) => {
if (typeof serial !== "number" || serial <= 0) {
return "invalid Date";
}
// Default time to 8:00 AM if not provided
if (!time) {
time = 800;
}
// Get base date from Excel serial (this gives you UTC midnight)
const date = getJsDateFromExcel(serial);
const localOffset = new Date().getTimezoneOffset() / 60;
const hours = Math.floor(time / 100);
const minutes = time % 100;
// Set the time in UTC
date.setUTCHours(hours + localOffset);
date.setUTCMinutes(minutes);
date.setUTCSeconds(0);
date.setUTCMilliseconds(0);
//console.log(date.toISOString(), serial, time);
return date.toISOString();
};

View File

@@ -1,5 +1,6 @@
import https from "node:https";
import axios from "axios";
import { createLogger } from "../logger/logger.controller.js";
import { returnFunc } from "./returnHelper.utils.js";
import { tryCatch } from "./trycatch.utils.js";
@@ -59,9 +60,14 @@ export const prodEndpointCreation = async (endpoint: string) => {
* @param timeoutDelay
* @returns
*/
export const runProdApi = async (data: Data) => {
export const runProdApi = async (data: Data, name?: string) => {
const log = createLogger({ module: "utils", subModule: "prodEndpoints" });
const url = await prodEndpointCreation(data.endpoint);
log.debug(
{ stack: data },
`Info passed over for ${name ? name : "Missing name"}`,
);
const { data: d, error } = await tryCatch(
axios({
method: data.method as string,
@@ -94,7 +100,7 @@ export const runProdApi = async (data: Data) => {
level: "error",
module: "utils",
subModule: "prodEndpoint",
message: "Data from prod endpoint",
message: "Error data from prod endpoint",
data: d.data,
notify: false,
});
@@ -104,10 +110,30 @@ export const runProdApi = async (data: Data) => {
level: "error",
module: "utils",
subModule: "prodEndpoint",
message: "Data from prod endpoint",
message: "Error data from prod endpoint",
data: d.data,
notify: false,
});
case 500:
return returnFunc({
success: false,
level: "error",
module: "utils",
subModule: "prodEndpoint",
message: "Error data from prod endpoint",
data: d.data,
notify: false,
});
default:
returnFunc({
success: false,
level: "error",
module: "utils",
subModule: "prodEndpoint",
message: "Unknown error encountered",
data: d?.data as any,
notify: false,
});
}
if (error) {

View File

@@ -18,7 +18,8 @@ export interface ReturnHelper<T = unknown[]> {
| "admin"
| "mobile"
| "dockdoor"
| "eom";
| "eom"
| "dm";
subModule: string;
level: "info" | "error" | "debug" | "fatal" | "warn";

View File

@@ -0,0 +1,13 @@
CREATE TABLE "forecast_import" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"receiving_plant_id" text NOT NULL,
"documentName" text,
"sender" text,
"customer_id" text,
"raw_data" jsonb DEFAULT '[]'::jsonb,
"add_date" timestamp with time zone DEFAULT now(),
"add_user" text DEFAULT 'lst-system',
"upd_date" timestamp with time zone DEFAULT now(),
"upd_user" text DEFAULT 'lst-system',
CONSTRAINT "forecast_import_documentName_unique" UNIQUE("documentName")
);

View File

@@ -0,0 +1 @@
ALTER TABLE "forecast_import" DROP CONSTRAINT "forecast_import_documentName_unique";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -449,6 +449,20 @@
"when": 1781045714275,
"tag": "0063_illegal_mauler",
"breakpoints": true
},
{
"idx": 64,
"version": "7",
"when": 1781425987022,
"tag": "0064_magical_lady_mastermind",
"breakpoints": true
},
{
"idx": 65,
"version": "7",
"when": 1781426193735,
"tag": "0065_jittery_ares",
"breakpoints": true
}
]
}

102
package-lock.json generated
View File

@@ -25,6 +25,7 @@
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"excel-date-to-js": "^1.1.5",
"express": "^5.2.1",
"husky": "^9.1.7",
"ldapts": "^8.1.7",
@@ -42,6 +43,7 @@
"powershell": "^2.3.3",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"xlsx": "^0.18.5",
"zod": "^4.3.6"
},
"devDependencies": {
@@ -3412,6 +3414,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -4301,6 +4312,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -4402,6 +4426,15 @@
"node": ">=0.8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -7634,6 +7667,15 @@
"bare-events": "^2.7.0"
}
},
"node_modules/excel-date-to-js": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/excel-date-to-js/-/excel-date-to-js-1.1.5.tgz",
"integrity": "sha512-grZW0MPye0VGCzLNljI7H22QWgrI8/hkTCvIUczYsQTTSaPQU/UTcz1fBPHNxWKpiv8Zu2I/98z+aAnlp6STNw==",
"license": "MIT",
"engines": {
"node": ">=4.2.4"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -8176,6 +8218,15 @@
"node": ">= 0.6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
@@ -12461,6 +12512,18 @@
"node": ">= 10.x"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -13581,6 +13644,24 @@
"node": ">=8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -13674,6 +13755,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",

View File

@@ -83,6 +83,7 @@
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"excel-date-to-js": "^1.1.5",
"express": "^5.2.1",
"husky": "^9.1.7",
"ldapts": "^8.1.7",
@@ -100,6 +101,7 @@
"powershell": "^2.3.3",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"xlsx": "^0.18.5",
"zod": "^4.3.6"
},
"config": {

View File

@@ -1,41 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "nodenext",
"strict": true,
"verbatimModuleSyntax": true,
"types": ["node", "better-auth"],
"jsx": "react-jsx",
"outDir": "./dist",
"removeComments": true,
"allowJs": false,
"rootDir": "./backend",
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"baseUrl": ".",
"paths": {
"@/*": ["backend/*"],
"@features/*": ["backend/features/*"],
"@shared/*": ["backend/shared/*"],
"@config/*": ["backend/config/*"]
},
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
//"allowImportingTsExtensions": true,
"noEmit": false
},
"include": ["backend/**/*"],
"exclude": [
"node_modules",
"frontend",
"dist",
"lstDocs",
"database/testFiles",
"scripts"
]}