feat(dm): migrated all the dm topics
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 4m26s
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 4m26s
This commit is contained in:
@@ -171,6 +171,16 @@ export const runDatamartQuery = async (data: Data) => {
|
||||
switch (data.name) {
|
||||
case "activeArticles":
|
||||
break;
|
||||
case "orderState":
|
||||
break;
|
||||
case "invoiceAddress":
|
||||
break;
|
||||
case "bulkOrderArticleInfo":
|
||||
datamartQuery = datamartQuery.replace(
|
||||
"[articles]",
|
||||
`${data.options.articles ? data.options.articles : "0"}`,
|
||||
);
|
||||
break;
|
||||
case "deliveryByDateRange":
|
||||
datamartQuery = datamartQuery
|
||||
.replace("[startDate]", `${data.options.startDate}`)
|
||||
|
||||
@@ -3,6 +3,8 @@ import postgres from "postgres";
|
||||
import * as dockScans from "./schema/dockdoor.scans.schema.js";
|
||||
import * as logs from "./schema/logs.schema.js";
|
||||
import * as opendockAVCheck from "./schema/opendock_articleSetup.js";
|
||||
import * as prodAuditLogId from "./schema/prodAuditlog.lastProcessed.schema.js";
|
||||
import * as prodAuditLog from "./schema/prodAuditlog.schema.js";
|
||||
import * as scanUserSchema from "./schema/scanUsers.js";
|
||||
import * as settingsSchema from "./schema/settings.schema.js";
|
||||
|
||||
@@ -27,5 +29,7 @@ export const db = drizzle(queryClient, {
|
||||
...opendockAVCheck,
|
||||
...logs,
|
||||
...dockScans,
|
||||
...prodAuditLogId,
|
||||
...prodAuditLog,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import postgres from "postgres";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { bufferProcess } from "../system/system.prodAuditLog.utils.js";
|
||||
import { handleDbNotification } from "./db.router.js";
|
||||
|
||||
const log = createLogger({
|
||||
@@ -37,6 +38,11 @@ export async function startDbNotificationListener() {
|
||||
|
||||
log.info({ stack: { channel } }, `Listening for ${channel}`);
|
||||
}
|
||||
|
||||
// server side only listeners
|
||||
await sql.listen("auditLog_inserted", async (rawPayload) => {
|
||||
bufferProcess(rawPayload);
|
||||
});
|
||||
}
|
||||
|
||||
async function processNotification(
|
||||
|
||||
@@ -19,6 +19,7 @@ export async function setupDbNotifications() {
|
||||
|
||||
await setupLogsNotifications();
|
||||
await setupDockScansNotifications();
|
||||
await setupProdAuditNotifications();
|
||||
|
||||
log.info({}, "DB notifications setup complete");
|
||||
}
|
||||
@@ -97,3 +98,36 @@ async function setupDockScansNotifications() {
|
||||
|
||||
log.info({}, "Dock scan DB notification trigger ready");
|
||||
}
|
||||
|
||||
async function setupProdAuditNotifications() {
|
||||
await db.execute(sql`
|
||||
CREATE OR REPLACE FUNCTION notify_auditLog_inserted()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify(
|
||||
'auditLog_inserted',
|
||||
json_build_object(
|
||||
'table', TG_TABLE_NAME,
|
||||
'action', TG_OP,
|
||||
'id', NEW.id
|
||||
)::text
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
DROP TRIGGER IF EXISTS auditLog_inserted_notify_trigger ON prod_audit_log;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TRIGGER auditLog_inserted_notify_trigger
|
||||
AFTER INSERT ON prod_audit_log
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_auditLog_inserted();
|
||||
`);
|
||||
|
||||
log.info({}, "Audit Log DB notification trigger ready");
|
||||
}
|
||||
|
||||
23
backend/db/schema/ordersImports.schema.ts
Normal file
23
backend/db/schema/ordersImports.schema.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const orderImport = pgTable("order_import", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
receivingPlantId: text("receiving_plant_id").notNull(),
|
||||
documentName: text("documentName"),
|
||||
sender: text("sender"),
|
||||
customerId: text("customer_id"),
|
||||
invoiceAddressId: text("invoice_address_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 orderImportSchema = createSelectSchema(orderImport);
|
||||
export const newOrderImportSchema = createInsertSchema(orderImport);
|
||||
|
||||
export type OrderImport = z.infer<typeof orderImportSchema>;
|
||||
export type NewOrderImport = z.infer<typeof newOrderImportSchema>;
|
||||
39
backend/db/schema/prodAuditlog.lastProcessed.schema.ts
Normal file
39
backend/db/schema/prodAuditlog.lastProcessed.schema.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { integer, pgTable, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
export const prodAuditLogState = pgTable("prod_audit_log_state", {
|
||||
id: integer("id").primaryKey().default(1),
|
||||
|
||||
lastImportedAuditId: integer("last_imported_audit_id").notNull().default(0),
|
||||
lastProcessedAuditId: integer("last_processed_audit_id").notNull().default(0),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
});
|
||||
|
||||
/*
|
||||
if the system fails do the process we do
|
||||
and increase the retry to x max of 5 tries
|
||||
const nextRetryAt = new Date(Date.now() + Math.min(30 * retryCount, 600) * 1000);
|
||||
|
||||
Cron every 30s
|
||||
↓
|
||||
Pull ERP AuditLog by Id > lastAuditId
|
||||
↓
|
||||
Insert into prod_audit_log
|
||||
↓
|
||||
Postgres NOTIFY wakes worker
|
||||
↓
|
||||
Worker processes pending rows
|
||||
↓
|
||||
Success = success
|
||||
Failure = error + retryCount + nextRetryAt
|
||||
20 failures = dead + email
|
||||
|
||||
|
||||
for the check we want to do
|
||||
|
||||
status IN ('pending', 'error')
|
||||
AND retry_count < 20
|
||||
AND (next_retry_at IS NULL OR next_retry_at <= NOW())
|
||||
|
||||
*/
|
||||
70
backend/db/schema/prodAuditlog.schema.ts
Normal file
70
backend/db/schema/prodAuditlog.schema.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
serial,
|
||||
text,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const prodAuditLog = pgTable("prod_audit_log", {
|
||||
id: serial("id").primaryKey(),
|
||||
auditId: integer("audit_id").notNull().unique(),
|
||||
actorName: text("actor_name").notNull(),
|
||||
auditCreatedDate: timestamp("audit_created_date", {
|
||||
withTimezone: true,
|
||||
}).notNull(),
|
||||
message: text("message").notNull(), // mirrors how prod sends it over basically this is where the domain its coming from is. well split by "." and then pass it to what it needs to go to later.
|
||||
content: jsonb("content").notNull(),
|
||||
|
||||
status: text("status").notNull().default("pending"), // pending | processing | success | error | dead
|
||||
processed: boolean("processed").default(false),
|
||||
|
||||
retryCount: integer("retry_count").notNull().default(0),
|
||||
nextRetryAt: timestamp("next_retry_at", { withTimezone: true }),
|
||||
|
||||
errorMessage: text("error_message"),
|
||||
errorStack: text("error_stack"),
|
||||
|
||||
processedAt: timestamp("processed_at", { withTimezone: true }),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
});
|
||||
|
||||
export const prodAuditLogSchema = createSelectSchema(prodAuditLog);
|
||||
export const newProdAuditLogSchema = createInsertSchema(prodAuditLog);
|
||||
|
||||
export type ProdAuditLog = z.infer<typeof prodAuditLogSchema>;
|
||||
export type NewProdAuditLog = z.infer<typeof newProdAuditLogSchema>;
|
||||
|
||||
/*
|
||||
if the system fails do the process we do
|
||||
and increase the retry to x max of 5 tries
|
||||
const nextRetryAt = new Date(Date.now() + Math.min(30 * retryCount, 600) * 1000);
|
||||
|
||||
Cron every 30s
|
||||
↓
|
||||
Pull ERP AuditLog by Id > lastAuditId
|
||||
↓
|
||||
Insert into prod_audit_log
|
||||
↓
|
||||
Postgres NOTIFY wakes worker
|
||||
↓
|
||||
Worker processes pending rows
|
||||
↓
|
||||
Success = success
|
||||
Failure = error + retryCount + nextRetryAt
|
||||
20 failures = dead + email
|
||||
|
||||
|
||||
for the check we want to do
|
||||
|
||||
status IN ('pending', 'error')
|
||||
AND retry_count < 20
|
||||
AND (next_retry_at IS NULL OR next_retry_at <= NOW())
|
||||
|
||||
*/
|
||||
@@ -1,10 +1,6 @@
|
||||
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";
|
||||
@@ -55,32 +51,22 @@ export const lorealForecast = async (data: any, user: any) => {
|
||||
const ebmForecastData: any = [];
|
||||
const missingSku: any = [];
|
||||
|
||||
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
|
||||
const { data: a, error: ae } = await tryCatch(
|
||||
runDatamartQuery({ name: "activeArticles", options: {} }),
|
||||
);
|
||||
|
||||
if (!avSQLQuery.success) {
|
||||
if (ae) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "forecast",
|
||||
message: `Error getting Article info`,
|
||||
data: [avSQLQuery.message],
|
||||
data: [ae.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);
|
||||
|
||||
@@ -96,9 +96,18 @@ export const standardForecast = async (data: any, user: any) => {
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: foreCastData[0].success,
|
||||
message: foreCastData[0].message,
|
||||
data: foreCastData,
|
||||
};
|
||||
if (foreCastData.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"There was an error processing the forecast did you upload the correct file?",
|
||||
data: [],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: foreCastData?.[0].success,
|
||||
message: foreCastData?.[0].message,
|
||||
data: foreCastData ?? [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ r.post("/", requireAuth, upload.single("file"), async (req, res) => {
|
||||
? "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,
|
||||
status: result.success ? 200 : 400,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
191
backend/logistics/logistics.dm.orders.map.abbott.ts
Normal file
191
backend/logistics/logistics.dm.orders.map.abbott.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { isAfter } from "date-fns";
|
||||
import { format } from "date-fns-tz";
|
||||
import XLSX from "xlsx";
|
||||
import { runDatamartQuery } from "../datamart/datamart.controller.js";
|
||||
import { excelDateStuff } from "../utils/excelToDate.utils.js";
|
||||
import { returnFunc } from "../utils/returnHelper.utils.js";
|
||||
import { tryCatch } from "../utils/trycatch.utils.js";
|
||||
import { abbottForecast } from "./logistics.dm.forecast.map.abbott.js";
|
||||
import { postData } from "./logistics.dm.postData.js";
|
||||
|
||||
// customeris/articles stuff will be in basis once we move to iowa
|
||||
const customerID = 8;
|
||||
const invoiceID = 9;
|
||||
const articles = "118,120";
|
||||
export const abbottOrders = async (data: any, user: any) => {
|
||||
/**
|
||||
* Standard orders meaning that we get the standard file exported and fill it out and uplaod to lst.
|
||||
*/
|
||||
|
||||
// articleInfo
|
||||
const { data: article, error: ae } = await tryCatch(
|
||||
runDatamartQuery({ name: "bulkOrderArticleInfo", options: { articles } }),
|
||||
);
|
||||
|
||||
const a: any = article?.data;
|
||||
if (ae) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [ae.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const { data: o, error: oe } = await tryCatch(
|
||||
runDatamartQuery({ name: "orderState", options: {} }),
|
||||
);
|
||||
|
||||
if (oe) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [oe.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const openOrders: any = o?.data;
|
||||
|
||||
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;
|
||||
|
||||
abbottForecast(sheet, user);
|
||||
// Define custom headers
|
||||
const customHeaders = ["date", "time", "newton8oz", "newton10oz"];
|
||||
const orderData = XLSX.utils.sheet_to_json(sheet, {
|
||||
range: 5, // Start at row 5 (index 4)
|
||||
header: customHeaders,
|
||||
defval: "", // Default value for empty cells
|
||||
});
|
||||
|
||||
// the base of the import
|
||||
const predefinedObject = {
|
||||
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
|
||||
documentName: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
sender: user.username || "lst-system",
|
||||
externalRefNo: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
orders: [],
|
||||
};
|
||||
//const oOrders: any = openOrders;
|
||||
//console.log(orderData);
|
||||
|
||||
function trimAll(str: string) {
|
||||
return str.replace(/\s+/g, "");
|
||||
}
|
||||
let correctedOrders: any = orderData
|
||||
.filter(
|
||||
(o: any) =>
|
||||
(o.newton8oz && o.newton8oz.trim() !== "") ||
|
||||
(o.newton10oz && o.newton10oz.trim() !== ""),
|
||||
)
|
||||
.map((o: any) => ({
|
||||
date: excelDateStuff(o.date, o.time),
|
||||
po:
|
||||
trimAll(o.newton8oz) !== ""
|
||||
? trimAll(o.newton8oz)
|
||||
: o.newton10oz.replace(/[\s\u00A0]+/g, ""),
|
||||
customerArticleNumber:
|
||||
o.newton8oz !== ""
|
||||
? a.filter((a: any) => a.av === 118)[0].CustomerArticleNumber
|
||||
: a.filter((a: any) => a.av === 120)[0].CustomerArticleNumber,
|
||||
qty:
|
||||
o.newton8oz !== ""
|
||||
? a.filter((a: any) => a.av === 118)[0].totalTruckLoad
|
||||
: a.filter((a: any) => a.av === 120)[0].totalTruckLoad,
|
||||
}));
|
||||
|
||||
//console.log(correctedOrders);
|
||||
// now we want to make sure we only correct orders that or after now
|
||||
correctedOrders = correctedOrders.filter((o: any) => {
|
||||
//const parsedDate = parse(o.date, "M/d/yyyy, h:mm:ss a", new Date());
|
||||
return isAfter(new Date(o.date), new Date().toISOString());
|
||||
});
|
||||
//console.log(correctedOrders);
|
||||
// last map to remove orders that have already been started
|
||||
// correctedOrders = correctedOrders.filter((oo: any) =>
|
||||
// oOrders.some((o: any) => o.CustomerOrderNumber === oo.po)
|
||||
// );
|
||||
const postedOrders: any = [];
|
||||
const filterOrders: any = correctedOrders;
|
||||
|
||||
//console.log(filterOrders);
|
||||
|
||||
filterOrders.forEach((oo: any) => {
|
||||
const isMatch = openOrders.some(
|
||||
(o: any) => String(o.po).trim() === String(oo.po).trim(),
|
||||
);
|
||||
//console.log(isMatch, oo.po);
|
||||
if (!isMatch) {
|
||||
//console.log(`ok to update: ${oo.po}`);
|
||||
|
||||
// oo = {
|
||||
// ...oo,
|
||||
// CustomerOrderNumber: oo.CustomerOrderNumber.replace(" ", ""),
|
||||
// };
|
||||
postedOrders.push(oo);
|
||||
} else {
|
||||
//console.log(`Not valid order to update: ${oo.po}`);
|
||||
//console.log(oo)
|
||||
}
|
||||
});
|
||||
|
||||
// Map Excel data to predefinedObject format
|
||||
const orders = filterOrders.map((o: any) => {
|
||||
//console.log(o.po, " ", o.date, format(o.date, "M/d/yyyy HH:mm"));
|
||||
return {
|
||||
customerId: customerID,
|
||||
invoiceAddressId: invoiceID,
|
||||
customerOrderNo: o.po,
|
||||
orderDate: new Date(Date.now()).toLocaleString("en-US"),
|
||||
positions: [
|
||||
{
|
||||
deliveryAddressId: 8,
|
||||
customerArticleNo: o.customerArticleNumber,
|
||||
quantity: o.qty,
|
||||
deliveryDate: format(o.date, "M/d/yyyy HH:mm"), // addHours(format(o.date, "M/d/yyyy HH:mm"), 1), //addHours(addDays(o.date, 1), 1), // adding this in so we can over come the constant 1 day behind thing as a work around
|
||||
customerLineItemNo: 1, // this is how it is currently sent over from abbott
|
||||
customerReleaseNo: 1, // same as above
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
//console.log(orders);
|
||||
// combine it all together.
|
||||
const updatedPredefinedObject = {
|
||||
...predefinedObject,
|
||||
orders: [...predefinedObject.orders, ...orders],
|
||||
};
|
||||
|
||||
//console.log(updatedPredefinedObject);
|
||||
// post the orders to the server
|
||||
const posting: any = await postData(
|
||||
{
|
||||
type: "orders",
|
||||
endpoint: "/public/v1.0/DemandManagement/ORDERS",
|
||||
data: updatedPredefinedObject as any,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
return {
|
||||
success: posting?.success,
|
||||
message: posting?.message,
|
||||
data: posting,
|
||||
};
|
||||
};
|
||||
197
backend/logistics/logistics.dm.orders.map.energizer.ts
Normal file
197
backend/logistics/logistics.dm.orders.map.energizer.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { runDatamartQuery } from "../datamart/datamart.controller.js";
|
||||
import { returnFunc } from "../utils/returnHelper.utils.js";
|
||||
import { tryCatch } from "../utils/trycatch.utils.js";
|
||||
import { postData } from "./logistics.dm.postData.js";
|
||||
|
||||
export const energizerOrders = async (data: any, user: any) => {
|
||||
/**
|
||||
* Standard orders meaning that we get the standard file exported and fill it out and uplaod to lst.
|
||||
*/
|
||||
|
||||
/*
|
||||
get the order state.
|
||||
*/
|
||||
|
||||
const { data: o, error: oe } = await tryCatch(
|
||||
runDatamartQuery({ name: "orderState", options: {} }),
|
||||
);
|
||||
|
||||
if (oe) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [oe.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const openOrders: any = o?.data;
|
||||
|
||||
/*
|
||||
get default invoice address
|
||||
*/
|
||||
const { data: invoice, error: ie } = await tryCatch(
|
||||
runDatamartQuery({ name: "invoiceAddress", options: {} }),
|
||||
);
|
||||
|
||||
if (ie) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [ie.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const i: any = invoice?.data;
|
||||
|
||||
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;
|
||||
|
||||
// define custom headers
|
||||
const headers = [
|
||||
"ITEM",
|
||||
"PO",
|
||||
"ReleaseNo",
|
||||
"QTY",
|
||||
"DELDATE",
|
||||
"COMMENTS",
|
||||
"What changed",
|
||||
"CUSTOMERID",
|
||||
"Remark",
|
||||
];
|
||||
const orderData = XLSX.utils.sheet_to_json(sheet, {
|
||||
defval: "",
|
||||
header: headers,
|
||||
range: 1,
|
||||
});
|
||||
|
||||
// the base of the import
|
||||
const predefinedObject = {
|
||||
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
|
||||
documentName: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
sender: user.username || "lst-system",
|
||||
externalRefNo: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
orders: [],
|
||||
};
|
||||
|
||||
let newOrders: any = orderData;
|
||||
|
||||
// for orders that are in od or managed by od we want to make sure we send out an email on this so we dont over right data that could be already planned with a carrier.
|
||||
const odOrders: any = [];
|
||||
const okToUpdateOrders: any = [];
|
||||
|
||||
for (const order of openOrders) {
|
||||
if (order.AdditionalInformation1?.includes("od")) {
|
||||
odOrders.push(order);
|
||||
} else {
|
||||
okToUpdateOrders.push(order);
|
||||
}
|
||||
}
|
||||
|
||||
if (odOrders.length > 0) {
|
||||
console.log("send email for od touched orders", odOrders);
|
||||
}
|
||||
|
||||
if (okToUpdateOrders.length === 0) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `All orders have been posted to od and releases will not be updated`,
|
||||
data: [],
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
|
||||
// filter out the orders that have already been started just to reduce the risk of errors.
|
||||
newOrders.filter((oo: any) =>
|
||||
okToUpdateOrders.some(
|
||||
(o: any) => o.CustomerOrderNumber === oo.CustomerOrderNumber,
|
||||
),
|
||||
);
|
||||
|
||||
// filter out the blanks
|
||||
newOrders = newOrders.filter((z: any) => z.ITEM !== "");
|
||||
|
||||
// let postedOrders: any = [];
|
||||
// for (const [customerID, orders] of Object.entries(orderData)) {
|
||||
// // console.log(`Running for Customer ID: ${customerID}`);
|
||||
// const newOrders: any = orderData;
|
||||
|
||||
// // filter out the orders that have already been started just to reduce the risk of errors.
|
||||
// newOrders.filter((oo: any) =>
|
||||
// openOrders.some(
|
||||
// (o: any) => o.CustomerOrderNumber === oo.CustomerOrderNumber
|
||||
// )
|
||||
// );
|
||||
|
||||
// // map everything out for each order
|
||||
const nOrder = newOrders.map((o: any) => {
|
||||
const invoice = i.filter(
|
||||
(i: any) => i.deliveryAddress === parseInt(o.CUSTOMERID),
|
||||
);
|
||||
if (!invoice) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
customerId: parseInt(o.CUSTOMERID),
|
||||
invoiceAddressId: invoice[0].invoiceAddress, // matched to the default invoice address
|
||||
customerOrderNo: o.PO,
|
||||
orderDate: new Date(Date.now()).toLocaleString("en-US"),
|
||||
positions: [
|
||||
{
|
||||
deliveryAddressId: parseInt(o.CUSTOMERID),
|
||||
customerArticleNo: o.ITEM,
|
||||
quantity: parseInt(o.QTY),
|
||||
deliveryDate: o.DELDATE, //excelDateStuff(o.DELDATE),
|
||||
customerLineItemNo: o.ReleaseNo, // this is how it is currently sent over from abbott
|
||||
customerReleaseNo: o.ReleaseNo, // same as above
|
||||
remark: o.COMMENTS === "" ? null : o.COMMENTS,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// // do that fun combining thing
|
||||
const updatedPredefinedObject = {
|
||||
...predefinedObject,
|
||||
orders: [...predefinedObject.orders, ...nOrder],
|
||||
};
|
||||
|
||||
// //console.log(updatedPredefinedObject);
|
||||
|
||||
// // post the orders to the server
|
||||
const posting: any = await postData(
|
||||
{
|
||||
type: "orders",
|
||||
endpoint: "/public/v1.0/DemandManagement/ORDERS",
|
||||
data: updatedPredefinedObject as any,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
return {
|
||||
customer: nOrder[0].CUSTOMERID,
|
||||
//totalOrders: orders?.length(),
|
||||
success: posting.success,
|
||||
message: posting.message,
|
||||
data: posting.data,
|
||||
};
|
||||
};
|
||||
202
backend/logistics/logistics.dm.orders.map.macroImport.ts
Normal file
202
backend/logistics/logistics.dm.orders.map.macroImport.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import XLSX from "xlsx";
|
||||
import { runDatamartQuery } from "../datamart/datamart.controller.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 macroImportOrders = async (data: any, user: any) => {
|
||||
/**
|
||||
* Standard orders meaning that we get the standard file exported and fill it out and uplaod to lst.
|
||||
*/
|
||||
|
||||
const plantToken = process.env.PROD_PLANT_TOKEN;
|
||||
|
||||
/*
|
||||
get the order state.
|
||||
*/
|
||||
|
||||
const { data: o, error: oe } = await tryCatch(
|
||||
runDatamartQuery({ name: "orderState", options: {} }),
|
||||
);
|
||||
|
||||
if (oe) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [oe.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const openOrders: any = o?.data;
|
||||
|
||||
/*
|
||||
get default invoice address
|
||||
*/
|
||||
const { data: invoice, error: ie } = await tryCatch(
|
||||
runDatamartQuery({ name: "invoiceAddress", options: {} }),
|
||||
);
|
||||
|
||||
if (ie) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [ie.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const i: any = invoice?.data;
|
||||
|
||||
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;
|
||||
|
||||
// define custom headers
|
||||
const headers = [
|
||||
"CustomerArticleNumber",
|
||||
"CustomerOrderNumber",
|
||||
"CustomerLineNumber",
|
||||
"CustomerRealeaseNumber",
|
||||
"Quantity",
|
||||
"DeliveryDate",
|
||||
"CustomerID",
|
||||
"Remark",
|
||||
];
|
||||
const orderData = XLSX.utils.sheet_to_json(sheet, {
|
||||
defval: "",
|
||||
header: headers,
|
||||
range: 5,
|
||||
});
|
||||
|
||||
// the base of the import
|
||||
const predefinedObject = {
|
||||
receivingPlantId: plantToken ?? "test1",
|
||||
documentName: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
sender: user.username || "lst-system",
|
||||
externalRefNo: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
orders: [],
|
||||
};
|
||||
|
||||
const removeBlanks = orderData.filter(
|
||||
(n: any) => n.CustomerArticleNumber !== "",
|
||||
);
|
||||
|
||||
const groupedByCustomer: any = removeBlanks.reduce((acc: any, item: any) => {
|
||||
const id = item.CustomerID;
|
||||
if (!acc[id]) {
|
||||
acc[id] = [];
|
||||
}
|
||||
acc[id].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const postedOrders: any = [];
|
||||
for (const [customerID, orders] of Object.entries(groupedByCustomer)) {
|
||||
// console.log(`Running for Customer ID: ${customerID}`);
|
||||
const filterOrders: any = orders;
|
||||
const newOrders: any = [];
|
||||
//newOrders.filter((oo) => openOrders.some((o) => String(o.CustomerOrderNumber) === String(oo.CustomerOrderNumber)));
|
||||
//console.log(newOrders)
|
||||
filterOrders.forEach((oo: any) => {
|
||||
const isMatch = openOrders.some(
|
||||
(o: any) =>
|
||||
// check the header
|
||||
String(o.CustomerOrderNumber).trim() ===
|
||||
String(oo.CustomerOrderNumber).trim() &&
|
||||
// and check the customer release is not in here.
|
||||
String(o.CustomerReleaseNumber).trim() ===
|
||||
String(oo.CustomerReleaseNumber).trim(),
|
||||
);
|
||||
if (!isMatch) {
|
||||
//console.log(`ok to update: ${oo.CustomerOrderNumber}`);
|
||||
|
||||
newOrders.push(oo);
|
||||
} else {
|
||||
//console.log(`Not valid order to update: ${oo.CustomerOrderNumber}`);
|
||||
//console.log(oo)
|
||||
}
|
||||
});
|
||||
|
||||
// filter out the orders that have already been started just to reduce the risk of errors.
|
||||
newOrders.filter((oo: any) =>
|
||||
openOrders.some(
|
||||
(o: any) => o.CustomerOrderNumber === oo.CustomerOrderNumber,
|
||||
),
|
||||
);
|
||||
|
||||
// map everything out for each order
|
||||
const nOrder = newOrders.map((o: any) => {
|
||||
const invoice = i.find(
|
||||
(inv: any) => inv.deliveryAddress === parseInt(customerID),
|
||||
);
|
||||
if (!invoice) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
customerId: parseInt(customerID),
|
||||
invoiceAddressId: invoice.invoiceAddress, // matched to the default invoice address
|
||||
customerOrderNo: o.CustomerOrderNumber,
|
||||
orderDate: new Date(Date.now()).toLocaleString("en-US"),
|
||||
positions: [
|
||||
{
|
||||
deliveryAddressId: parseInt(customerID),
|
||||
customerArticleNo: o.CustomerArticleNumber,
|
||||
quantity: parseInt(o.Quantity),
|
||||
deliveryDate: excelDateStuff(o.DeliveryDate),
|
||||
customerLineItemNo: o.CustomerLineNumber, // this is how it is currently sent over from abbott
|
||||
customerReleaseNo: o.CustomerRealeaseNumber, // same as above
|
||||
remark: o.remark === "" ? null : o.remark,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// do that fun combining thing
|
||||
const updatedPredefinedObject = {
|
||||
...predefinedObject,
|
||||
orders: [...predefinedObject.orders, ...nOrder],
|
||||
};
|
||||
|
||||
//console.log(updatedPredefinedObject);
|
||||
|
||||
// post the orders to the server
|
||||
const posting: any = await postData(
|
||||
{
|
||||
type: "orders",
|
||||
endpoint: "/public/v1.0/DemandManagement/ORDERS",
|
||||
data: updatedPredefinedObject as any,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
postedOrders.push({
|
||||
customer: customerID,
|
||||
//totalOrders: orders?.length(),
|
||||
success: posting.success,
|
||||
message: posting.message,
|
||||
data: posting.data,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message:
|
||||
"Standard Template was just processed successfully, please check AlplaProd 2.0 to confirm no errors. ",
|
||||
data: postedOrders,
|
||||
};
|
||||
};
|
||||
258
backend/logistics/logistics.dm.orders.map.standard.ts
Normal file
258
backend/logistics/logistics.dm.orders.map.standard.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { runDatamartQuery } from "../datamart/datamart.controller.js";
|
||||
import { db } from "../db/db.controller.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 standardOrders = async (data: any, user: any) => {
|
||||
/**
|
||||
* Post a standard forecast based on the standard template.
|
||||
*/
|
||||
|
||||
const plantToken = process.env.PROD_PLANT_TOKEN;
|
||||
|
||||
/*
|
||||
get the order state.
|
||||
*/
|
||||
|
||||
const { data: o, error: oe } = await tryCatch(
|
||||
runDatamartQuery({ name: "orderState", options: {} }),
|
||||
);
|
||||
|
||||
if (oe) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [oe.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const openOrders: any = o?.data;
|
||||
|
||||
/*
|
||||
get default invoice address
|
||||
*/
|
||||
const { data: invoice, error: ie } = await tryCatch(
|
||||
runDatamartQuery({ name: "invoiceAddress", options: {} }),
|
||||
);
|
||||
|
||||
if (ie) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [ie.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const i: any = invoice?.data;
|
||||
|
||||
//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;
|
||||
|
||||
// define custom headers
|
||||
const headers = [
|
||||
"CustomerArticleNumber",
|
||||
"CustomerOrderNumber",
|
||||
"CustomerLineNumber",
|
||||
"CustomerRealeaseNumber",
|
||||
"Quantity",
|
||||
"DeliveryDate",
|
||||
"CustomerID",
|
||||
"Remark",
|
||||
];
|
||||
|
||||
const orderData = XLSX.utils.sheet_to_json(sheet, {
|
||||
defval: "",
|
||||
header: headers,
|
||||
range: 1,
|
||||
});
|
||||
|
||||
// the base of the import
|
||||
const predefinedObject = {
|
||||
receivingPlantId: plantToken ?? "test1",
|
||||
documentName: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
sender: user.username || "lst-system",
|
||||
externalRefNo: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
orders: [],
|
||||
};
|
||||
|
||||
// for orders that are in od or managed by od we want to make sure we send out an email on this so we dont over right data that could be already planned with a carrier.
|
||||
const odOrders: any = [];
|
||||
const okToUpdateOrders: any = [];
|
||||
|
||||
for (const order of openOrders) {
|
||||
if (order.AdditionalInformation1?.includes("od")) {
|
||||
odOrders.push(order);
|
||||
} else {
|
||||
okToUpdateOrders.push(order);
|
||||
}
|
||||
}
|
||||
|
||||
if (odOrders.length > 0) {
|
||||
console.log("send email for od touched orders", odOrders);
|
||||
}
|
||||
|
||||
if (okToUpdateOrders.length === 0) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `All orders have been posted to od and releases will not be updated`,
|
||||
data: [],
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
|
||||
const groupedByCustomer: any = orderData.reduce((acc: any, item: any) => {
|
||||
const id = item.CustomerID;
|
||||
if (!acc[id]) {
|
||||
acc[id] = [];
|
||||
}
|
||||
acc[id].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const postedOrders: any = [];
|
||||
|
||||
for (const [customerID, orders] of Object.entries(groupedByCustomer)) {
|
||||
// console.log(`Running for Customer ID: ${customerID}`);
|
||||
const filterOrders: any = orders;
|
||||
const newOrders: any = [];
|
||||
//newOrders.filter((oo) => openOrders.some((o) => String(o.CustomerOrderNumber) === String(oo.CustomerOrderNumber)));
|
||||
//console.log(newOrders)
|
||||
filterOrders.forEach((oo: any) => {
|
||||
const isMatch = okToUpdateOrders.some(
|
||||
(o: any) =>
|
||||
// check the header
|
||||
String(o.CustomerOrderNumber).trim() ===
|
||||
String(oo.CustomerOrderNumber).trim() &&
|
||||
// and check the customer release is not in here.
|
||||
String(o.CustomerRealeaseNumber).trim() ===
|
||||
String(oo.CustomerRealeaseNumber).trim(),
|
||||
);
|
||||
if (!isMatch) {
|
||||
//console.log(`ok to update: ${oo.CustomerOrderNumber}`);
|
||||
|
||||
newOrders.push(oo);
|
||||
} else {
|
||||
//console.log(`Not valid order to update: ${oo.CustomerOrderNumber}`);
|
||||
//console.log(oo)
|
||||
}
|
||||
});
|
||||
|
||||
// filter out the orders that have already been started just to reduce the risk of errors.
|
||||
newOrders.filter((oo: any) =>
|
||||
openOrders.some(
|
||||
(o: any) => o.CustomerOrderNumber === oo.CustomerOrderNumber,
|
||||
),
|
||||
);
|
||||
|
||||
// get the default time to put in here if an order dose not include the date
|
||||
const { data: s, error } = await tryCatch(
|
||||
db.query.settings.findFirst({
|
||||
where: (setting, { eq }) => eq(setting.name, "defaultOrderTime"),
|
||||
}),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "utils",
|
||||
subModule: "excelToDate",
|
||||
message: "Failed to get the default order time setting.",
|
||||
data: [],
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
|
||||
const hour = parseInt(s?.value ?? "8", 10);
|
||||
|
||||
const defaultOrderTime =
|
||||
Number.isNaN(hour) || hour < 1 || hour > 23 ? 800 : hour * 100;
|
||||
|
||||
// map everything out for each order
|
||||
const nOrder = newOrders.map((o: any) => {
|
||||
const invoice = i.find(
|
||||
(inv: any) => inv.deliveryAddress === parseInt(customerID),
|
||||
);
|
||||
if (!invoice) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `No invoice address found for ${customerID}`,
|
||||
data: [],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
return {
|
||||
customerId: parseInt(customerID),
|
||||
invoiceAddressId: invoice.invoiceAddress, // matched to the default invoice address
|
||||
customerOrderNo: o.CustomerOrderNumber,
|
||||
orderDate: new Date(Date.now()).toLocaleString("en-US"),
|
||||
positions: [
|
||||
{
|
||||
deliveryAddressId: parseInt(customerID),
|
||||
customerArticleNo: o.CustomerArticleNumber,
|
||||
quantity: parseInt(o.Quantity),
|
||||
deliveryDate: excelDateStuff(o.DeliveryDate, defaultOrderTime),
|
||||
customerLineItemNo: o.CustomerLineNumber, // this is how it is currently sent over from abbott
|
||||
customerReleaseNo: o.CustomerRealeaseNumber, // same as above
|
||||
remark: o.Remark === "" ? null : o.Remark,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// do that fun combining thing
|
||||
const updatedPredefinedObject = {
|
||||
...predefinedObject,
|
||||
orders: [...predefinedObject.orders, ...nOrder],
|
||||
};
|
||||
|
||||
// post the orders to the server
|
||||
const posting: any = await postData(
|
||||
{
|
||||
type: "orders",
|
||||
endpoint: "/public/v1.0/DemandManagement/ORDERS",
|
||||
data: updatedPredefinedObject as any,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
postedOrders.push({
|
||||
customer: customerID,
|
||||
//totalOrders: orders?.length(),
|
||||
success: posting.success,
|
||||
message: posting.message,
|
||||
data: posting.data,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: postedOrders[0].success,
|
||||
message: postedOrders[0].message,
|
||||
data: postedOrders,
|
||||
};
|
||||
};
|
||||
104
backend/logistics/logistics.dm.orders.route.ts
Normal file
104
backend/logistics/logistics.dm.orders.route.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Router } from "express";
|
||||
import multer from "multer";
|
||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||
|
||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||
import { abbottOrders } from "./logistics.dm.orders.map.abbott.js";
|
||||
import { energizerOrders } from "./logistics.dm.orders.map.energizer.js";
|
||||
import { macroImportOrders } from "./logistics.dm.orders.map.macroImport.js";
|
||||
import { standardOrders } from "./logistics.dm.orders.map.standard.js";
|
||||
import { scjOrders } from "./logistics.dm.orders.scj.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: "orders",
|
||||
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: "orders",
|
||||
message: "A fileType must be provided.",
|
||||
data: [],
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
//console.log("fileType:", req.body.fileType);
|
||||
|
||||
let result: ForecastResult;
|
||||
|
||||
switch (fileType) {
|
||||
case "standard":
|
||||
result = await standardOrders(req.file, req.user);
|
||||
break;
|
||||
|
||||
case "abbott":
|
||||
// daytons orders
|
||||
result = await abbottOrders(req.file, req.user);
|
||||
break;
|
||||
|
||||
case "energizer":
|
||||
// daytons orders
|
||||
result = await energizerOrders(req.file, req.user);
|
||||
break;
|
||||
|
||||
case "macro":
|
||||
result = await macroImportOrders(req.file, req.user);
|
||||
break;
|
||||
|
||||
case "scj":
|
||||
// this is for west bend orders
|
||||
result = await scjOrders(req.file, req.user);
|
||||
break;
|
||||
|
||||
default:
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "dm",
|
||||
subModule: "orders",
|
||||
message: `Invalid fileType: ${fileType}`,
|
||||
data: [],
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
return apiReturn(res, {
|
||||
success: result.success ?? false,
|
||||
level: result.success ? "info" : "error",
|
||||
module: "dm",
|
||||
subModule: "orders",
|
||||
message: result.success
|
||||
? "The orders 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;
|
||||
175
backend/logistics/logistics.dm.orders.scj.ts
Normal file
175
backend/logistics/logistics.dm.orders.scj.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { runDatamartQuery } from "../datamart/datamart.controller.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 scjOrders = async (data: any, user: any) => {
|
||||
/**
|
||||
* Standard orders meaning that we get the standard file exported and fill it out and uplaod to lst.
|
||||
*/
|
||||
|
||||
const customerID = 48;
|
||||
|
||||
const plantToken = process.env.PROD_PLANT_TOKEN;
|
||||
|
||||
/*
|
||||
get the order state.
|
||||
*/
|
||||
|
||||
const { data: o, error: oe } = await tryCatch(
|
||||
runDatamartQuery({ name: "orderState", options: {} }),
|
||||
);
|
||||
|
||||
if (oe) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [oe.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const openOrders: any = o?.data;
|
||||
|
||||
/*
|
||||
get default invoice address
|
||||
*/
|
||||
const { data: invoice, error: ie } = await tryCatch(
|
||||
runDatamartQuery({ name: "invoiceAddress", options: {} }),
|
||||
);
|
||||
|
||||
if (ie) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "logistics",
|
||||
subModule: "orders",
|
||||
message: `Error getting Article info`,
|
||||
data: [ie.message],
|
||||
notify: true,
|
||||
});
|
||||
}
|
||||
|
||||
const i: any = invoice?.data;
|
||||
|
||||
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;
|
||||
|
||||
// define custom headers
|
||||
const headers = [
|
||||
"ItemNo",
|
||||
"Description",
|
||||
"DeliveryDate",
|
||||
"Quantity",
|
||||
"PO",
|
||||
"Releases",
|
||||
"remarks",
|
||||
];
|
||||
|
||||
const orderData = XLSX.utils.sheet_to_json(sheet, {
|
||||
defval: "",
|
||||
header: headers,
|
||||
range: 1,
|
||||
});
|
||||
|
||||
// the base of the import
|
||||
const predefinedObject = {
|
||||
receivingPlantId: plantToken ?? "test1",
|
||||
documentName: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
sender: user.username || "lst-system",
|
||||
externalRefNo: `OrdersFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
orders: [],
|
||||
};
|
||||
|
||||
let newOrders: any = orderData;
|
||||
|
||||
// filter out the orders that have already been started just to reduce the risk of errors.
|
||||
newOrders.filter((oo: any) =>
|
||||
openOrders.some(
|
||||
(o: any) => o.CustomerOrderNumber === oo.CustomerOrderNumber,
|
||||
),
|
||||
);
|
||||
|
||||
// filter out the blanks
|
||||
newOrders = newOrders.filter((z: any) => z.ItemNo !== "");
|
||||
|
||||
const nOrder = newOrders.map((o: any) => {
|
||||
const invoice = i.filter((i: any) => i.deliveryAddress === customerID);
|
||||
if (!invoice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (o.Releases === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (o.PO === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = isNaN(o.DeliveryDate)
|
||||
? new Date(o.DeliveryDate)
|
||||
: excelDateStuff(o.DeliveryDate);
|
||||
return {
|
||||
customerId: customerID,
|
||||
invoiceAddressId: invoice[0].invoiceAddress, // matched to the default invoice address
|
||||
customerOrderNo: o.PO,
|
||||
orderDate: new Date(Date.now()).toLocaleString("en-US"),
|
||||
positions: [
|
||||
{
|
||||
deliveryAddressId: customerID,
|
||||
customerArticleNo: o.ItemNo,
|
||||
quantity: parseInt(o.Quantity),
|
||||
deliveryDate: date, //excelDateStuff(o.DELDATE),
|
||||
customerLineItemNo: o.PO, // this is how it is currently sent over from abbott
|
||||
customerReleaseNo: o.Releases, // same as above
|
||||
remark: o.remarks === "" ? null : o.remarks,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
//console.log(nOrder.filter((o: any) => o !== undefined));
|
||||
|
||||
// // do that fun combining thing
|
||||
const updatedPredefinedObject = {
|
||||
...predefinedObject,
|
||||
orders: [
|
||||
...predefinedObject.orders,
|
||||
...nOrder.filter((o: any) => o !== undefined),
|
||||
],
|
||||
};
|
||||
|
||||
//console.log(updatedPredefinedObject.orders[0]);
|
||||
|
||||
// // post the orders to the server
|
||||
const posting: any = await postData(
|
||||
{
|
||||
type: "orders",
|
||||
endpoint: "/public/v1.0/DemandManagement/ORDERS",
|
||||
data: updatedPredefinedObject as any,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
return {
|
||||
customer: customerID,
|
||||
//totalOrders: orders?.length(),
|
||||
success: posting.success,
|
||||
message: posting.message,
|
||||
data: posting.data,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { forecastImport } from "../db/schema/forecastImports.schema.js";
|
||||
import { orderImport } from "../db/schema/ordersImports.schema.js";
|
||||
import { runProdApi } from "../utils/prodEndpoint.utils.js";
|
||||
import { returnFunc } from "../utils/returnHelper.utils.js";
|
||||
|
||||
@@ -8,6 +9,7 @@ type PostData = {
|
||||
documentName: string;
|
||||
sender: string;
|
||||
customerId: string;
|
||||
invoiceAddressId?: string;
|
||||
positions: unknown[];
|
||||
};
|
||||
type Data = {
|
||||
@@ -52,6 +54,19 @@ export const postData = async (data: Data, user: any) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (data.type === "orders") {
|
||||
await db.insert(orderImport).values({
|
||||
receivingPlantId: data.data.receivingPlantId ?? "test1",
|
||||
documentName: data.data.documentName ?? "order-data-missing",
|
||||
sender: data.data.sender ?? "lst-user",
|
||||
customerId: data.data.customerId ?? "0",
|
||||
invoiceAddressId: data.data.invoiceAddressId ?? "0",
|
||||
rawData: data ?? [],
|
||||
add_user: user.username ?? undefined,
|
||||
upd_user: user.username ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return returnFunc({
|
||||
success: true,
|
||||
level: "info",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Express } from "express";
|
||||
import { featureCheck } from "../middleware/featureActive.middleware.js";
|
||||
import forecast from "./logistics.dm.forecast.route.js";
|
||||
import orders from "./logistics.dm.orders.route.js";
|
||||
import createTemplate from "./logistics.dm.template.route.js";
|
||||
|
||||
export const setupLogisticsRoutes = (baseUrl: string, app: Express) => {
|
||||
@@ -17,5 +18,11 @@ export const setupLogisticsRoutes = (baseUrl: string, app: Express) => {
|
||||
forecast,
|
||||
);
|
||||
|
||||
app.use(
|
||||
`${baseUrl}/api/logistics/dm/orders`,
|
||||
featureCheck("demandManagement"),
|
||||
orders,
|
||||
);
|
||||
|
||||
// all other system should be under /api/system/*
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Router } from "express";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { scanLog } from "../db/schema/scanlog.schema.js";
|
||||
import { scanUser } from "../db/schema/scanUsers.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||
|
||||
const router = Router();
|
||||
@@ -32,6 +33,8 @@ router.post("/", async (req, res) => {
|
||||
})
|
||||
.returning();
|
||||
|
||||
emitToRoom(`scanLog`, newLog[0]);
|
||||
|
||||
return apiReturn(res, {
|
||||
success: true,
|
||||
level: "info",
|
||||
|
||||
26
backend/prodSql/queries/datamart.bulkOrderArticleInfo.sql
Normal file
26
backend/prodSql/queries/datamart.bulkOrderArticleInfo.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
USE [test1_AlplaPROD2.0_Read]
|
||||
SELECT
|
||||
x.HumanReadableId as av
|
||||
,x.Name
|
||||
,Alias
|
||||
,CustomerDescription
|
||||
,CustomerArticleNumber
|
||||
,LoadingUnitPieces
|
||||
,LoadingUnitsPerTruck
|
||||
,LoadingUnitPieces * LoadingUnitsPerTruck as totalTruckLoad
|
||||
FROM [masterData].[Article] (nolock) as x
|
||||
|
||||
--get the sales price stuff
|
||||
left join
|
||||
(select * from (select *
|
||||
,ROW_NUMBER() OVER (PARTITION BY articleId ORDER BY validAfter DESC) as rn
|
||||
from [masterData].[SalesPrice] (nolock))as b
|
||||
where rn = 1) as s on
|
||||
x.id = s.ArticleId
|
||||
|
||||
-- link pkg info
|
||||
left join
|
||||
[masterData].[PackagingInstruction] (nolock) as p on
|
||||
s.PackagingId = p.id
|
||||
|
||||
where x.HumanReadableId in ([articles])
|
||||
15
backend/prodSql/queries/datamart.invoiceAddress.sql
Normal file
15
backend/prodSql/queries/datamart.invoiceAddress.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
use [test1_AlplaPROD2.0_Read]
|
||||
|
||||
SELECT deliveryAddress.humanreadableid as deliveryAddress
|
||||
,invoice.HumanReadableId as invoiceAddress
|
||||
,[Default]
|
||||
|
||||
FROM [masterData].[InvoiceAddress] (nolock) as d
|
||||
|
||||
join
|
||||
[masterData].[Address] deliveryAddress (nolock) on deliveryAddress.id = d.AddressId
|
||||
|
||||
join
|
||||
[masterData].[Address] invoice (nolock) on invoice.id = d.InvoiceAddressId
|
||||
|
||||
where [Default] = 1
|
||||
@@ -27,7 +27,15 @@ left join
|
||||
[order].Header as h (nolock) on
|
||||
h.id = l.HeaderId
|
||||
|
||||
WHERE releasestate not in (1, 2, 4)
|
||||
/*
|
||||
0 = open
|
||||
1 = shipped
|
||||
2 = customer canceled
|
||||
3 = shipped
|
||||
4 = internal canceled
|
||||
*/
|
||||
|
||||
WHERE releasestate not in (1, 2,3, 4)
|
||||
AND r.deliverydate between getDate() + -[startDay] and getdate() + [endDay]
|
||||
|
||||
order by r.deliverydate
|
||||
27
backend/prodSql/queries/datamart.orderState.sql
Normal file
27
backend/prodSql/queries/datamart.orderState.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
use [test1_AlplaPROD2.0_Read]
|
||||
|
||||
SELECT
|
||||
CustomerOrderNumber
|
||||
,r.CustomerReleaseNumber
|
||||
, OrderState
|
||||
, r.ReleaseState
|
||||
, h.CreatedByEdi
|
||||
,r.AdditionalInformation1 -- the info for od exists here, this will be mapped over to an email sending out saying someoen tryied to update an od release
|
||||
|
||||
--, *
|
||||
FROM [order].[Header] (nolock) h
|
||||
|
||||
/* get the line items to link to the headers */
|
||||
left join
|
||||
[order].[LineItem] (nolock) l on
|
||||
l.HeaderId = h.id
|
||||
|
||||
/* get the releases to link to the headers */
|
||||
left join
|
||||
[order].[Release] (nolock) r on
|
||||
r.LineItemId = l.id
|
||||
|
||||
where
|
||||
--h.CreatedByEdi = 1
|
||||
r.ReleaseState >= 1
|
||||
--and CustomerOrderNumber in ( '2358392')
|
||||
10
backend/prodSql/queries/prod.auditlog.sql
Normal file
10
backend/prodSql/queries/prod.auditlog.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
USE [test1_AlplaPROD2.0_Read]
|
||||
|
||||
DECLARE @lastId BIGINT = [lstId];
|
||||
|
||||
SELECT TOP (500)
|
||||
*
|
||||
FROM [support].[AuditLog] WITH (NOLOCK)
|
||||
WHERE ActorName NOT IN ('SCHEDULER', 'SCAN')
|
||||
AND Id > @lastId
|
||||
ORDER BY Id ASC;
|
||||
@@ -21,6 +21,7 @@ import { monitorAlplaPurchase } from "./purchase/purchase.controller.js";
|
||||
import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
|
||||
import { serversChecks } from "./system/serverData.controller.js";
|
||||
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
|
||||
import { monitorProdAuditLog } from "./system/system.prodAuditLog.utils.js";
|
||||
import { startTCPServer } from "./tcpServer/tcp.server.js";
|
||||
import {
|
||||
aggregateRouteHitsForBusinessDay,
|
||||
@@ -89,6 +90,9 @@ const start = async () => {
|
||||
);
|
||||
|
||||
createCronJob("cleanHitsUp", "0 0 7 * * *", () => cleanupOldRouteHits());
|
||||
createCronJob("ProdAuditLogMonitor", "*/30 * * * * *", () =>
|
||||
monitorProdAuditLog(),
|
||||
);
|
||||
// one shots only needed to run on server startups
|
||||
createNotifications();
|
||||
startNotifications();
|
||||
|
||||
@@ -6,7 +6,8 @@ export type RoomKey =
|
||||
| "labels"
|
||||
| "admin"
|
||||
| "inventory"
|
||||
| "dockDoorLoading";
|
||||
| "dockDoorLoading"
|
||||
| "scanLog";
|
||||
|
||||
export type SocketUser = {
|
||||
id: string;
|
||||
@@ -105,6 +106,11 @@ export const roomConfigs: Record<RoomKey, RoomConfig> = {
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
scanLog: {
|
||||
canJoin: () => true,
|
||||
buildRoom: () => "scanLog",
|
||||
},
|
||||
} satisfies Record<string, RoomConfig>;
|
||||
|
||||
/*
|
||||
|
||||
168
backend/system/system.prodAuditLog.utils.ts
Normal file
168
backend/system/system.prodAuditLog.utils.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { prodAuditLogState } from "../db/schema/prodAuditlog.lastProcessed.schema.js";
|
||||
import {
|
||||
type NewProdAuditLog,
|
||||
prodAuditLog,
|
||||
} from "../db/schema/prodAuditlog.schema.js";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
|
||||
import {
|
||||
type SqlQuery,
|
||||
sqlQuerySelector,
|
||||
} from "../prodSql/prodSqlQuerySelector.utils.js";
|
||||
import { delay } from "../utils/delay.utils.js";
|
||||
import { returnFunc } from "../utils/returnHelper.utils.js";
|
||||
import { tryCatch } from "../utils/trycatch.utils.js";
|
||||
|
||||
const log = createLogger({ module: "system", subModule: "prodAuditLog" });
|
||||
let bufferProcessInProgress = false;
|
||||
|
||||
export const monitorProdAuditLog = async () => {
|
||||
const auditLogQuery = sqlQuerySelector(`prod.auditlog`) as SqlQuery;
|
||||
|
||||
/*
|
||||
get the last processed audit log so we can only pull the newest ones.
|
||||
|
||||
as the initial go will be zero we want to look at the top 1 so we only pull the most recent one.
|
||||
|
||||
*/
|
||||
|
||||
const latestAuditId = await db.select().from(prodAuditLogState).limit(1);
|
||||
let auditQuery = auditLogQuery.query;
|
||||
if (latestAuditId.length === 0) {
|
||||
auditQuery = auditQuery
|
||||
.replace(
|
||||
"DECLARE @lastId BIGINT = [lstId];",
|
||||
"--DECLARE @lastId BIGINT = [lstId];",
|
||||
)
|
||||
.replace("TOP (500)", "TOP (1)")
|
||||
.replace("ASC", "DESC")
|
||||
.replace("AND Id > @lastId", "--AND Id > @lastId");
|
||||
} else {
|
||||
auditQuery = auditQuery.replace(
|
||||
"[lstId]",
|
||||
`${latestAuditId[0]?.lastImportedAuditId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { data: queryRun, error } = await tryCatch(
|
||||
prodQuery(auditQuery, `Running auditLog query`),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "system",
|
||||
subModule: "auditLog",
|
||||
message: `Data for: AuditLog Failed`,
|
||||
data: [error],
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (!queryRun.success) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "datamart",
|
||||
subModule: "query",
|
||||
message: queryRun.message,
|
||||
data: queryRun.data,
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
|
||||
const safeJsonParse = (value: unknown) => {
|
||||
if (typeof value !== "string") return value;
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return { raw: value };
|
||||
}
|
||||
};
|
||||
|
||||
// remap everything lst logs to keep it easy to read
|
||||
|
||||
if (queryRun.data.length > 0) {
|
||||
const auditRows = queryRun.data.map((r) => ({
|
||||
auditId: r.Id,
|
||||
actorName: r.ActorName,
|
||||
auditCreatedDate: r.CreatedDateTime,
|
||||
message: r.Message,
|
||||
content: safeJsonParse(r.Content),
|
||||
status: "pending",
|
||||
processed: false,
|
||||
retryCount: 0,
|
||||
})) as NewProdAuditLog[];
|
||||
|
||||
await db.insert(prodAuditLog).values(auditRows).onConflictDoNothing();
|
||||
|
||||
const newestAuditId = queryRun.data.at(-1).Id;
|
||||
|
||||
await db
|
||||
.insert(prodAuditLogState)
|
||||
.values({
|
||||
id: 1,
|
||||
lastImportedAuditId: newestAuditId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: prodAuditLogState.id,
|
||||
set: {
|
||||
lastImportedAuditId: newestAuditId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const bufferProcess = async (_data: unknown) => {
|
||||
if (bufferProcessInProgress) {
|
||||
log.debug({}, "[bufferProcess] already running, skipping trigger");
|
||||
return;
|
||||
}
|
||||
|
||||
bufferProcessInProgress = true;
|
||||
|
||||
try {
|
||||
log.debug({}, "[bufferProcess] started");
|
||||
|
||||
while (true) {
|
||||
const row = await db.query.prodAuditLog.findFirst({
|
||||
where: (audit, { eq }) => eq(audit.processed, false),
|
||||
orderBy: (audit, { asc }) => [asc(audit.auditId)],
|
||||
});
|
||||
|
||||
if (!row) {
|
||||
log.debug({}, "[bufferProcess] no pending rows");
|
||||
break;
|
||||
}
|
||||
|
||||
log.debug({}, "[bufferProcess] processing audit row", row.auditId);
|
||||
|
||||
// tiny delay so you can visually validate the flow
|
||||
await delay(250);
|
||||
|
||||
// TODO add in case statement to do things, if thing returns good then say good if fails then we set to error and let it retry later.
|
||||
// for items that don't fall in the case will auto set status to success
|
||||
// console.log(row.message.split("."));
|
||||
await db
|
||||
.update(prodAuditLog)
|
||||
.set({
|
||||
processed: true,
|
||||
status: "success",
|
||||
processedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(prodAuditLog.id, row.id));
|
||||
}
|
||||
} catch (error) {
|
||||
log.error({ stack: error }, "[bufferProcess] failed");
|
||||
} finally {
|
||||
bufferProcessInProgress = false;
|
||||
log.debug({}, "[bufferProcess] finished");
|
||||
}
|
||||
};
|
||||
@@ -1,21 +1,23 @@
|
||||
import { getJsDateFromExcel } from "excel-date-to-js";
|
||||
|
||||
export const excelDateStuff = (serial: number, time?: any) => {
|
||||
export const excelDateStuff = (
|
||||
serial: number,
|
||||
defaultTime = 800,
|
||||
time?: number,
|
||||
) => {
|
||||
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 excelTime = excelSerialToTime(serial);
|
||||
const finalTime = time ?? excelTime ?? defaultTime;
|
||||
|
||||
const date = getJsDateFromExcel(Math.floor(serial));
|
||||
|
||||
const localOffset = new Date().getTimezoneOffset() / 60;
|
||||
const hours = Math.floor(time / 100);
|
||||
const minutes = time % 100;
|
||||
const hours = Math.floor(finalTime / 100);
|
||||
const minutes = finalTime % 100;
|
||||
|
||||
// Set the time in UTC
|
||||
date.setUTCHours(hours + localOffset);
|
||||
@@ -26,3 +28,17 @@ export const excelDateStuff = (serial: number, time?: any) => {
|
||||
//console.log(date.toISOString(), serial, time);
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const excelSerialToTime = (serial: number): number | null => {
|
||||
const fraction = serial % 1;
|
||||
|
||||
if (fraction <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalMinutes = Math.round(fraction * 24 * 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
return hours * 100 + minutes;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user