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) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (foreCastData.length === 0) {
|
||||
return {
|
||||
success: foreCastData[0].success,
|
||||
message: foreCastData[0].message,
|
||||
data: foreCastData,
|
||||
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;
|
||||
};
|
||||
|
||||
53
frontend/src/components/Sidebar/LogisticsBar.tsx
Normal file
53
frontend/src/components/Sidebar/LogisticsBar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Truck } from "lucide-react";
|
||||
import { getSettings } from "../../lib/queries/getSettings";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "../ui/sidebar";
|
||||
|
||||
export default function LogisticsSidebar({ session }: any) {
|
||||
const { setOpen } = useSidebar();
|
||||
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
|
||||
const items = [
|
||||
{
|
||||
title: "Demand Management",
|
||||
url: "/logistics/dm",
|
||||
icon: Truck,
|
||||
role: ["systemAdmin", "admin", "warehouse", "transport"],
|
||||
module: "logistics",
|
||||
active:
|
||||
!isLoading &&
|
||||
settings.filter((n: any) => n.name === "demandManagement")[0].active,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Logistics</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<div key={item.title}>
|
||||
{item.role.includes(session.user.role) && item.active && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link to={item.url} onClick={() => setOpen(false)}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { getSettings } from "../../lib/queries/getSettings";
|
||||
import { permissionQuery } from "../../lib/queries/permsCheck";
|
||||
import AdminSidebar from "./AdminBar";
|
||||
import DocBar from "./DocBar";
|
||||
import LogisticsSidebar from "./LogisticsBar";
|
||||
import MobileBar from "./MobileBar";
|
||||
import TransportationBar from "./TransportationBar";
|
||||
import WarehouseBar from "./Warhouse";
|
||||
@@ -27,6 +28,12 @@ export function AppSidebar() {
|
||||
openDock: ["read"],
|
||||
}),
|
||||
);
|
||||
|
||||
const { data: canReadWarehouse = false } = useQuery(
|
||||
permissionQuery({
|
||||
warehouse: ["update"],
|
||||
}),
|
||||
);
|
||||
const { setOpen } = useSidebar();
|
||||
|
||||
// const { data: canReadWarehouse = false } = useQuery(
|
||||
@@ -47,9 +54,11 @@ export function AppSidebar() {
|
||||
<SidebarContent>
|
||||
<DocBar />
|
||||
{!isLoading &&
|
||||
canReadWarehouse &&
|
||||
settings.filter((n: any) => n.name === "mobile")[0].active && (
|
||||
<MobileBar />
|
||||
)}
|
||||
{!isLoading && session && <LogisticsSidebar session={session} />}
|
||||
|
||||
{!isLoading &&
|
||||
settings.filter((n: any) => n.name === "opendock_sync")[0]
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Route as ApidocsRouteImport } from './routes/apidocs'
|
||||
import { Route as AboutRouteImport } from './routes/about'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DocsIndexRouteImport } from './routes/docs/index'
|
||||
import { Route as LogisticsDmRouteImport } from './routes/logistics/dm'
|
||||
import { Route as DocsDatamartRouteImport } from './routes/docs/datamart'
|
||||
import { Route as DocsSplatRouteImport } from './routes/docs/$'
|
||||
import { Route as AdminUsersRouteImport } from './routes/admin/users'
|
||||
@@ -63,6 +64,11 @@ const DocsIndexRoute = DocsIndexRouteImport.update({
|
||||
path: '/docs/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const LogisticsDmRoute = LogisticsDmRouteImport.update({
|
||||
id: '/logistics/dm',
|
||||
path: '/logistics/dm',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DocsDatamartRoute = DocsDatamartRouteImport.update({
|
||||
id: '/docs/datamart',
|
||||
path: '/docs/datamart',
|
||||
@@ -169,6 +175,7 @@ export interface FileRoutesByFullPath {
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/datamart': typeof DocsDatamartRoute
|
||||
'/logistics/dm': typeof LogisticsDmRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
'/user/profile': typeof authUserProfileRoute
|
||||
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||
@@ -194,6 +201,7 @@ export interface FileRoutesByTo {
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/datamart': typeof DocsDatamartRoute
|
||||
'/logistics/dm': typeof LogisticsDmRoute
|
||||
'/docs': typeof DocsIndexRoute
|
||||
'/user/profile': typeof authUserProfileRoute
|
||||
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||
@@ -220,6 +228,7 @@ export interface FileRoutesById {
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/datamart': typeof DocsDatamartRoute
|
||||
'/logistics/dm': typeof LogisticsDmRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
'/(auth)/user/profile': typeof authUserProfileRoute
|
||||
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
|
||||
@@ -247,6 +256,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/users'
|
||||
| '/docs/$'
|
||||
| '/docs/datamart'
|
||||
| '/logistics/dm'
|
||||
| '/docs/'
|
||||
| '/user/profile'
|
||||
| '/user/resetpassword'
|
||||
@@ -272,6 +282,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/users'
|
||||
| '/docs/$'
|
||||
| '/docs/datamart'
|
||||
| '/logistics/dm'
|
||||
| '/docs'
|
||||
| '/user/profile'
|
||||
| '/user/resetpassword'
|
||||
@@ -297,6 +308,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/users'
|
||||
| '/docs/$'
|
||||
| '/docs/datamart'
|
||||
| '/logistics/dm'
|
||||
| '/docs/'
|
||||
| '/(auth)/user/profile'
|
||||
| '/(auth)/user/resetpassword'
|
||||
@@ -323,6 +335,7 @@ export interface RootRouteChildren {
|
||||
AdminUsersRoute: typeof AdminUsersRoute
|
||||
DocsSplatRoute: typeof DocsSplatRoute
|
||||
DocsDatamartRoute: typeof DocsDatamartRoute
|
||||
LogisticsDmRoute: typeof LogisticsDmRoute
|
||||
DocsIndexRoute: typeof DocsIndexRoute
|
||||
authUserProfileRoute: typeof authUserProfileRoute
|
||||
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
|
||||
@@ -378,6 +391,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof DocsIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/logistics/dm': {
|
||||
id: '/logistics/dm'
|
||||
path: '/logistics/dm'
|
||||
fullPath: '/logistics/dm'
|
||||
preLoaderRoute: typeof LogisticsDmRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/docs/datamart': {
|
||||
id: '/docs/datamart'
|
||||
path: '/docs/datamart'
|
||||
@@ -515,6 +535,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
AdminUsersRoute: AdminUsersRoute,
|
||||
DocsSplatRoute: DocsSplatRoute,
|
||||
DocsDatamartRoute: DocsDatamartRoute,
|
||||
LogisticsDmRoute: LogisticsDmRoute,
|
||||
DocsIndexRoute: DocsIndexRoute,
|
||||
authUserProfileRoute: authUserProfileRoute,
|
||||
authUserResetpasswordRoute: authUserResetpasswordRoute,
|
||||
|
||||
@@ -248,13 +248,13 @@ function RouteComponent() {
|
||||
};
|
||||
//console.log(logs);
|
||||
return (
|
||||
<div className="flex flex-col gap-1 max-w-7xl">
|
||||
<div className="flex flex-col gap-1 w-7xl">
|
||||
<div className="flex gap-1 justify-end">
|
||||
<Button onClick={triggerBuild}>Trigger Build</Button>
|
||||
<Button onClick={() => clearRoom()}>Clear Logs</Button>
|
||||
</div>
|
||||
<div className="flex gap-1 w-full">
|
||||
<div className="w-full">
|
||||
<div className="w-3/4">
|
||||
<Suspense fallback={<SkellyTable />}>
|
||||
<ServerTable />
|
||||
</Suspense>
|
||||
|
||||
@@ -178,10 +178,10 @@ function SettingsTableCard() {
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 w-7xl flex flex-col justify-center">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold">Settings</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h1 className="text-2xl font-semibold text-center">Settings</h1>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Manage your settings and related data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
146
frontend/src/routes/logistics/-components/dm.ForecastUpload.tsx
Normal file
146
frontend/src/routes/logistics/-components/dm.ForecastUpload.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from "../../../components/ui/card";
|
||||
import { Separator } from "../../../components/ui/separator";
|
||||
import { api } from "../../../lib/apiHelper";
|
||||
import { useSession } from "../../../lib/auth-client";
|
||||
|
||||
export default function ForecastUpload({
|
||||
server,
|
||||
responseData,
|
||||
}: {
|
||||
server: string;
|
||||
responseData: any;
|
||||
}) {
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [posting, setPosting] = useState(false);
|
||||
const [selectedFileType, setSelectedFileType] = useState<string>("");
|
||||
const { data: session } = useSession();
|
||||
|
||||
const importOrders = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
console.log("clicked import");
|
||||
responseData([]);
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) {
|
||||
toast.error("Missing or no file was selected please try again");
|
||||
setPosting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// create the form data with the correct fileType
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("fileType", selectedFileType); // extra field
|
||||
formData.append("username", `${session?.user.username}`);
|
||||
|
||||
toast.success("Import started.");
|
||||
try {
|
||||
const response = await api.post("/logistics/dm/forecast", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
validateStatus: (status) => status < 500,
|
||||
});
|
||||
//console.log("Upload successful:", response.data);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
setPosting(false);
|
||||
|
||||
if (response.status === 200) {
|
||||
toast.success(response?.data?.message);
|
||||
|
||||
responseData(response.data.data);
|
||||
} else {
|
||||
toast.error(response?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Upload failed");
|
||||
}
|
||||
setPosting(false);
|
||||
};
|
||||
|
||||
const handleButtonClick = (type: string) => {
|
||||
setPosting(true);
|
||||
|
||||
const handleFocus = () => {
|
||||
setTimeout(() => {
|
||||
if (!fileInputRef.current?.files?.length) {
|
||||
setPosting(false);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
window.addEventListener("focus", handleFocus, { once: true });
|
||||
setSelectedFileType(type);
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const ForecastButton = ({ name, type }: { name: string; type: string }) => {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={() => handleButtonClick(type)} disabled={posting}>
|
||||
{name}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// For plants that basically use the same import set like this so we dont have weird looking cards
|
||||
const pngForecast = ["usiow1", "usiow2", "usksc1"];
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Card>
|
||||
<CardTitle>
|
||||
<p className="text-center">Forecast</p>
|
||||
</CardTitle>
|
||||
<CardDescription className="w-64 p-2 ">
|
||||
<p className="text-xs">
|
||||
When clicking on one of the below options you will need to upload
|
||||
the respective file to be processed to 2.0
|
||||
</p>
|
||||
<Separator />
|
||||
</CardDescription>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<ForecastButton name={"Standard"} type={"standard"} />
|
||||
{server === "usday1" ||
|
||||
(server.includes("test") && (
|
||||
<ForecastButton
|
||||
name={"Energizer Forecast"}
|
||||
type={"energizer"}
|
||||
/>
|
||||
))}
|
||||
|
||||
{pngForecast.includes(server) ||
|
||||
(server.includes("test") && (
|
||||
<ForecastButton name={"PnG"} type={"pg"} />
|
||||
))}
|
||||
|
||||
{server === "usflo1" ||
|
||||
(server.includes("test") && (
|
||||
<ForecastButton name={"VMI Import"} type={"loreal"} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xlsx, .xls, .xlsm"
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={importOrders}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
139
frontend/src/routes/logistics/-components/dm.OrdersUpload.tsx
Normal file
139
frontend/src/routes/logistics/-components/dm.OrdersUpload.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from "../../../components/ui/card";
|
||||
import { Separator } from "../../../components/ui/separator";
|
||||
import { api } from "../../../lib/apiHelper";
|
||||
import { useSession } from "../../../lib/auth-client";
|
||||
|
||||
export default function OrdersUpload({
|
||||
server,
|
||||
responseData,
|
||||
}: {
|
||||
server: string;
|
||||
responseData: any;
|
||||
}) {
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [posting, setPosting] = useState(false);
|
||||
const [selectedFileType, setSelectedFileType] = useState<string>("");
|
||||
const { data: session } = useSession();
|
||||
|
||||
const importOrders = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
responseData([]);
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) {
|
||||
toast.error("Missing or no file was selected please try again");
|
||||
setPosting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// create the form data with the correct fileType
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("fileType", selectedFileType); // extra field
|
||||
formData.append("username", `${session?.user.username}`);
|
||||
|
||||
toast.success("Import started.");
|
||||
try {
|
||||
const response = await api.post("/logistics/dm/orders", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
validateStatus: (status) => status < 500,
|
||||
});
|
||||
//console.log("Upload successful:", response.data);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
setPosting(false);
|
||||
|
||||
if (response.status === 200) {
|
||||
toast.success(response?.data?.message);
|
||||
//console.log(response.data);
|
||||
responseData(response.data.data);
|
||||
} else {
|
||||
toast.error(response?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Upload failed");
|
||||
}
|
||||
setPosting(false);
|
||||
};
|
||||
|
||||
const handleButtonClick = (type: string) => {
|
||||
setPosting(true);
|
||||
|
||||
const handleFocus = () => {
|
||||
setTimeout(() => {
|
||||
if (!fileInputRef.current?.files?.length) {
|
||||
setPosting(false);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
window.addEventListener("focus", handleFocus, { once: true });
|
||||
setSelectedFileType(type);
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const OrderButton = ({ name, type }: { name: string; type: string }) => {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={() => handleButtonClick(type)} disabled={posting}>
|
||||
{name}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Card>
|
||||
<CardTitle>
|
||||
<p className="text-center">Orders</p>
|
||||
</CardTitle>
|
||||
<CardDescription className="w-64 p-2 ">
|
||||
<p className="text-xs">
|
||||
When clicking on one of the below options you will need to upload
|
||||
the respective file to be processed to 2.0
|
||||
</p>
|
||||
<Separator />
|
||||
</CardDescription>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<OrderButton name={"Standard"} type={"standard"} />
|
||||
{server === "usweb1" ||
|
||||
(server.includes("test") && (
|
||||
<OrderButton name={"SCJ Orders"} type={"scj"} />
|
||||
))}
|
||||
{server === "usday1" ||
|
||||
(server.includes("test") && (
|
||||
<>
|
||||
<OrderButton name={"Abbott"} type={"abbott"} />
|
||||
<OrderButton
|
||||
name={"Energizer Truck List"}
|
||||
type={"energizer"}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xlsx, .xls, .xlsm"
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={importOrders}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
frontend/src/routes/logistics/-components/dm.Templates.tsx
Normal file
72
frontend/src/routes/logistics/-components/dm.Templates.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { format } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from "../../../components/ui/card";
|
||||
import { Separator } from "../../../components/ui/separator";
|
||||
import { api } from "../../../lib/apiHelper";
|
||||
|
||||
export default function Templates() {
|
||||
const [template, setTemplate] = useState(false);
|
||||
|
||||
const getTemplate = async (type: "orders" | "forecast") => {
|
||||
setTemplate(true);
|
||||
try {
|
||||
const res = await api.get(`/logistics/dm/template?filename=${type}`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
|
||||
const blob = new Blob([res.data], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = `Bulk${type.charAt(0).toUpperCase() + type.slice(1)}_Template-${format(new Date(Date.now()), "M-d-yyyy")}.xlsx`; // You can make this dynamic
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(link.href);
|
||||
toast.success(`Template created for ${type}`);
|
||||
setTemplate(false);
|
||||
} catch {
|
||||
setTemplate(false);
|
||||
toast.error("There was an error getting the template");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<CardTitle>
|
||||
<p className="text-center">Templates</p>
|
||||
</CardTitle>
|
||||
<CardDescription className="w-64 p-2 ">
|
||||
<p>
|
||||
Clicking one of the template's below will generate an excel file you
|
||||
can fill out and re-upload, using the standard orders or standard
|
||||
forecast button
|
||||
</p>
|
||||
<Separator />
|
||||
</CardDescription>
|
||||
<CardContent>
|
||||
<div className="flex flex-rol justify-center gap-2">
|
||||
<Button onClick={() => getTemplate("orders")} disabled={template}>
|
||||
Orders
|
||||
</Button>
|
||||
<Button onClick={() => getTemplate("forecast")} disabled={template}>
|
||||
Forecast
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
127
frontend/src/routes/logistics/dm.tsx
Normal file
127
frontend/src/routes/logistics/dm.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { useState } from "react";
|
||||
import { Separator } from "../../components/ui/separator";
|
||||
import LstTable from "../../lib/tableStuff/LstTable";
|
||||
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
|
||||
import { runtimeConfig } from "../../lib/umami.utils";
|
||||
import ForecastUpload from "./-components/dm.ForecastUpload";
|
||||
import OrdersUpload from "./-components/dm.OrdersUpload";
|
||||
import Templates from "./-components/dm.Templates";
|
||||
|
||||
export const Route = createFileRoute("/logistics/dm")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const server = runtimeConfig.server;
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
const [uploadInfo, setUploadInfo] = useState<any>([]);
|
||||
|
||||
console.log(uploadInfo?.data?.orders);
|
||||
const columns = [
|
||||
columnHelper.accessor("customerId", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader
|
||||
column={column}
|
||||
title="Customer ID"
|
||||
searchable={true}
|
||||
/>
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => <>{i.getValue()}</>,
|
||||
}),
|
||||
columnHelper.accessor("customerOrderNo", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader
|
||||
column={column}
|
||||
title="Customer Order Number"
|
||||
searchable={true}
|
||||
/>
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => <>{i.getValue()}</>,
|
||||
}),
|
||||
columnHelper.accessor((row) => row.positions?.[0]?.customerLineItemNo, {
|
||||
id: "customerLineItemNo",
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader
|
||||
column={column}
|
||||
title="Customer Line Item No"
|
||||
searchable={true}
|
||||
/>
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => <>{i.getValue()}</>,
|
||||
}),
|
||||
columnHelper.accessor((row) => row.positions?.[0]?.deliveryDate, {
|
||||
id: "deliveryDate",
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader
|
||||
column={column}
|
||||
title="Delivery date"
|
||||
searchable={true}
|
||||
/>
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => <>{i.getValue()}</>,
|
||||
}),
|
||||
columnHelper.accessor((row) => row.positions?.[0]?.quantity, {
|
||||
id: "quantity",
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Quantity" searchable={true} />
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => <>{i.getValue()}</>,
|
||||
}),
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
<div className="w-7x1 mt-2 flex flex-row gap-2 justify-center">
|
||||
<div>
|
||||
<Templates />
|
||||
</div>
|
||||
<div>
|
||||
<ForecastUpload server={server} responseData={setUploadInfo} />
|
||||
</div>
|
||||
<div>
|
||||
<OrdersUpload server={server} responseData={setUploadInfo} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<p>
|
||||
As a reminder, All forecast and orders, uploaded will always show
|
||||
processed as good or error, you will still need to go into AlplaPROD
|
||||
2.0 and validate the file processed all orders with no errors.
|
||||
</p>
|
||||
<p>
|
||||
This is due to AlplaPROD basically saying it has accepted your file
|
||||
only.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<p className="text-sm">
|
||||
The info in the table below is what was processed and sent over to
|
||||
AlplaPROD, if releases were in any other state than planned they would
|
||||
be skipped and not show up below.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
We only pass back the data you sent in no releases you will need to
|
||||
validate this on your own.
|
||||
</p>
|
||||
<Separator className="m-2" />
|
||||
<p className="text-center">
|
||||
The below table will show orders only forecast will be added in later
|
||||
</p>
|
||||
<div className="w-7xl flex justify-center mt-3">
|
||||
<LstTable
|
||||
data={uploadInfo?.data?.orders ?? []}
|
||||
columns={columns}
|
||||
pageSize={50}
|
||||
className="max-h-96 "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
migrations/0066_milky_bedlam.sql
Normal file
13
migrations/0066_milky_bedlam.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE "order_import" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"receiving_plant_id" text NOT NULL,
|
||||
"documentName" text,
|
||||
"sender" text,
|
||||
"customer_id" text,
|
||||
"invoice_address_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'
|
||||
);
|
||||
18
migrations/0067_low_bullseye.sql
Normal file
18
migrations/0067_low_bullseye.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE "prod_audit_log" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"audit_id" integer NOT NULL,
|
||||
"actor_name" text NOT NULL,
|
||||
"audit_created_date" timestamp with time zone NOT NULL,
|
||||
"message" text NOT NULL,
|
||||
"content" jsonb NOT NULL,
|
||||
"status" text DEFAULT 'pending' NOT NULL,
|
||||
"processed" boolean DEFAULT false,
|
||||
"retry_count" integer DEFAULT 0 NOT NULL,
|
||||
"next_retry_at" timestamp with time zone,
|
||||
"error_message" text,
|
||||
"error_stack" text,
|
||||
"processed_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now(),
|
||||
"updated_at" timestamp with time zone DEFAULT now(),
|
||||
CONSTRAINT "prod_audit_log_audit_id_unique" UNIQUE("audit_id")
|
||||
);
|
||||
3
migrations/0068_flaky_retro_girl.sql
Normal file
3
migrations/0068_flaky_retro_girl.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
CREATE TABLE "prod_audit_log_processed" (
|
||||
"last_processed" integer DEFAULT 0
|
||||
);
|
||||
7
migrations/0070_broad_revanche.sql
Normal file
7
migrations/0070_broad_revanche.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE "prod_audit_log_processed" RENAME TO "prod_audit_log_state";--> statement-breakpoint
|
||||
ALTER TABLE "prod_audit_log_state" ADD COLUMN "id" integer PRIMARY KEY DEFAULT 1 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "prod_audit_log_state" ADD COLUMN "last_imported_audit_id" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "prod_audit_log_state" ADD COLUMN "last_processed_audit_id" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "prod_audit_log_state" ADD COLUMN "created_at" timestamp with time zone DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "prod_audit_log_state" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "prod_audit_log_state" DROP COLUMN "last_processed";
|
||||
2860
migrations/meta/0066_snapshot.json
Normal file
2860
migrations/meta/0066_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2976
migrations/meta/0067_snapshot.json
Normal file
2976
migrations/meta/0067_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2996
migrations/meta/0068_snapshot.json
Normal file
2996
migrations/meta/0068_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3024
migrations/meta/0070_snapshot.json
Normal file
3024
migrations/meta/0070_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -463,6 +463,41 @@
|
||||
"when": 1781426193735,
|
||||
"tag": "0065_jittery_ares",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 66,
|
||||
"version": "7",
|
||||
"when": 1782292405098,
|
||||
"tag": "0066_milky_bedlam",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 67,
|
||||
"version": "7",
|
||||
"when": 1782400069152,
|
||||
"tag": "0067_low_bullseye",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 68,
|
||||
"version": "7",
|
||||
"when": 1782401046031,
|
||||
"tag": "0068_flaky_retro_girl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 69,
|
||||
"version": "7",
|
||||
"when": 1782403148642,
|
||||
"tag": "0069_slow_speedball",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 70,
|
||||
"version": "7",
|
||||
"when": 1782403232037,
|
||||
"tag": "0070_broad_revanche",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "lst_v3",
|
||||
"version": "0.1.0-alpha.4",
|
||||
"build": "181",
|
||||
"lastBuildDate": "6/22/2026 06:50",
|
||||
"build": "186",
|
||||
"lastBuildDate": "6/26/2026 10:47",
|
||||
"description": "The tool that supports us in our everyday alplaprod",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -16,7 +16,7 @@ param (
|
||||
# server migrations get - reminder to add to old version in pkg "start:lst": "cd lstV2 && npm start",
|
||||
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LST_app" -option "install" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start"
|
||||
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LSTV3_app" -option "install" -appPath "D:\LST_V3" -description "Logistics Support Tool" -command "run start"
|
||||
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LST_ctl" -option "delete" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start:lst"
|
||||
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LSTV2" -option "install" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start:lst"
|
||||
|
||||
$nssmPath = $AppPath + "\nssm.exe"
|
||||
$npmPath = "C:\Program Files\nodejs\npm.cmd" # Path to npm.cmd
|
||||
|
||||
Reference in New Issue
Block a user