feat(dm): migrated all the dm topics
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 4m26s

This commit is contained in:
2026-06-26 11:05:17 -05:00
parent 012a7e83b2
commit 47b149d1ea
48 changed files with 14156 additions and 44 deletions

View File

@@ -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}`)

View File

@@ -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,
},
});

View File

@@ -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(

View File

@@ -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");
}

View 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>;

View 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())
*/

View 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())
*/

View File

@@ -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);

View File

@@ -96,9 +96,18 @@ export const standardForecast = async (data: any, user: any) => {
});
}
return {
success: foreCastData[0].success,
message: foreCastData[0].message,
data: foreCastData,
};
if (foreCastData.length === 0) {
return {
success: false,
message:
"There was an error processing the forecast did you upload the correct file?",
data: [],
};
} else {
return {
success: foreCastData?.[0].success,
message: foreCastData?.[0].message,
data: foreCastData ?? [],
};
}
};

View File

@@ -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,
});
});

View 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,
};
};

View 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,
};
};

View 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,
};
};

View 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,
};
};

View 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;

View 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,
};
};

View File

@@ -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",

View File

@@ -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/*
};

View File

@@ -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",

View 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])

View 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

View File

@@ -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

View 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')

View 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;

View File

@@ -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();

View File

@@ -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>;
/*

View 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");
}
};

View File

@@ -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;
};

View 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>
);
}

View File

@@ -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]

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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'
);

View 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")
);

View File

@@ -0,0 +1,3 @@
CREATE TABLE "prod_audit_log_processed" (
"last_processed" integer DEFAULT 0
);

View 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";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
]
}

View File

@@ -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": {

View File

@@ -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