7 Commits

Author SHA1 Message Date
3a24d62957 refactor(api docks): added api docks back into the front end and prep for docusorus
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m15s
2026-06-16 18:53:44 -05:00
6a14bab30c refactor(psi): rebuilt the delivery and planning data to 2.0 data 2026-06-16 18:52:58 -05:00
2ebf695526 fix(builds): fixed non used variables
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m32s
2026-06-15 17:16:29 -05:00
24af3ca403 fix(datamart): somereason it stopped working.. and added download types
there was a weird issue with the req.query that cause nothing to pull and make the excel files
lag..... now excel macros are using the csv pull from here and added in the xlsx to download to bc
why not makes it easier for later  and can have bbuttons for every thing in lst too :D
2026-06-15 17:16:00 -05:00
6fbe3a9eed chore(format): formatting changes 2026-06-15 17:14:05 -05:00
7dbc31c046 refactor(dm): mapped remainder of the forecast. will need to run backup test
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 40s
the test server wouldnt do the backup so waiting on this one
2026-06-15 01:03:50 -05:00
ba09a77f29 chore(logistics): renamed the history util to better represent what it is 2026-06-15 01:03:02 -05:00
28 changed files with 1114 additions and 524 deletions

View File

@@ -9,8 +9,6 @@ import os from "node:os";
import { apiReference } from "@scalar/express-api-reference";
// const port = 3000;
import type { OpenAPIV3_1 } from "openapi-types";
import { cronerActiveJobs } from "../scaler/cronerActiveJobs.spec.js";
import { cronerStatusChange } from "../scaler/cronerStatusChange.spec.js";
import { prodLoginSpec } from "../scaler/login.spec.js";
import { openDockApt } from "../scaler/opendockGetRelease.spec.js";
import { prodRestartSpec } from "../scaler/prodSqlRestart.spec.js";
@@ -125,8 +123,6 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
...prodLoginSpec,
...prodRegisterSpec,
//...mergedDatamart,
...cronerActiveJobs,
...cronerStatusChange,
...openDockApt,
// Add more specs here as you build features

View File

@@ -37,6 +37,7 @@ const lstDbRun = async (data: Data) => {
if (data.options) {
if (data.name === "psiInventory") {
const ids = data.options.articles.split(",").map((id: any) => id.trim());
const whse = data.options.whseToInclude
? data.options.whseToInclude
.split(",")
@@ -274,10 +275,10 @@ export const runDatamartQuery = async (data: Data) => {
.replace("[startDate]", `${data.options.startDate}`)
.replace("[endDate]", `${data.options.endDate}`)
.replace(
"and p.IdArtikelvarianten in ([articles])",
"and pl.ArticleHumanReadableId IN ([articles]) ",
data.options.articles
? `and p.IdArtikelvarianten in (${data.options.articles})`
: "--and p.IdArtikelvarianten in ([articles])",
? `and pl.ArticleHumanReadableId IN (${data.options.articles})`
: "--and pl.ArticleHumanReadableId IN ([articles])",
);
break;
default:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import * as XLSX from "xlsx";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { postForecast } from "./logistics.dm.postForecast.js";
import { postData } from "./logistics.dm.postData.js";
export const standardForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
@@ -78,7 +78,14 @@ export const standardForecast = async (data: any, user: any) => {
//console.log(updatedPredefinedObject);
// post the orders to the server
const posting: any = await postForecast(updatedPredefinedObject, user);
const posting: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
foreCastData.push({
customer: customerID,

View File

@@ -2,7 +2,10 @@ import { Router } from "express";
import multer from "multer";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { standardForecast } from "./logsitcs.dm.forecast.map.standard.js";
import { energizerForecast } from "./logistics.dm.forecast.map.energizer.js";
import { lorealForecast } from "./logistics.dm.forecast.map.loreal.js";
import { pNgForecast } from "./logistics.dm.forecast.map.pNg.js";
import { standardForecast } from "./logistics.dm.forecast.map.standard.js";
type ForecastResult = {
success?: boolean;
@@ -53,18 +56,15 @@ r.post("/", requireAuth, upload.single("file"), async (req, res) => {
break;
case "loreal":
`result = await lorealForecast(req.file, req.user);`;
result = { success: true, message: "standardForecast", data: [] };
result = await lorealForecast(req.file, req.user);
break;
case "pg":
`result = await pNgForecast(req.file, req.user);`;
result = { success: true, message: "standardForecast", data: [] };
result = await pNgForecast(req.file, req.user);
break;
case "energizer":
`result = await energizerForecast(req.file, req.user);`;
result = { success: true, message: "standardForecast", data: [] };
result = await energizerForecast(req.file, req.user);
break;
default:

View File

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

View File

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

View File

@@ -1,74 +1,73 @@
use [test1_AlplaPROD2.0_Read]
DECLARE @StartDate DATE = '[startDate]' -- 2025-1-1
DECLARE @EndDate DATE = '[endDate]' -- 2025-1-31
SELECT
r.[ArticleHumanReadableId]
,[ReleaseNumber]
,h.CustomerOrderNumber
,x.CustomerLineItemNumber
,[CustomerReleaseNumber]
,[ReleaseState]
,[DeliveryState]
,ea.JournalNummer as BOL_Number
,[ReleaseConfirmationState]
,[PlanningState]
DECLARE @StartDate DATE = '[startDate]'
DECLARE @EndDate DATE = '[endDate]'
;WITH bol_20 AS ( -- 2.0 BOL, one per release (newest doc wins)
SELECT pos.ReleaseId,
dd.JournalNumber,
ROW_NUMBER() OVER (PARTITION BY pos.ReleaseId
ORDER BY dd.ShippingDate DESC) AS rn
FROM [outboundDelivery].[DeliveryDocumentPosition] (nolock) pos
JOIN [outboundDelivery].[DeliveryDocument] (nolock) dd
ON dd.Id = pos.DeliveryDocumentId
-- WHERE dd.DocumentType = <BOL value> -- see note below
)
SELECT
r.[ArticleHumanReadableId]
,[ReleaseNumber]
,h.CustomerOrderNumber
,x.CustomerLineItemNumber
,[CustomerReleaseNumber]
,[ReleaseState]
,[DeliveryState]
,COALESCE(ea.JournalNummer, bol_20.JournalNumber) AS BOL_Number -- 1.0 or 2.0
,[ReleaseConfirmationState]
,[PlanningState]
,format(r.[OrderDate], 'yyyy-MM-dd HH:mm') as OrderDate
--,r.[OrderDate]
,FORMAT(r.[DeliveryDate], 'yyyy-MM-dd HH:mm') as DeliveryDate
--,r.[DeliveryDate]
,FORMAT(r.[LoadingDate], 'yyyy-MM-dd HH:mm') as LoadingDate
--,r.[LoadingDate]
,[Quantity]
,[DeliveredQuantity]
,r.[AdditionalInformation1]
,r.[AdditionalInformation2]
,[TradeUnits]
,[LoadingUnits]
,[Trucks]
,[LoadingToleranceType]
,[SalesPrice]
,[Currency]
,[QuantityUnit]
,[SalesPriceRemark]
,r.[Remark]
,[Irradiated]
,r.[CreatedByEdi]
,[DeliveryAddressHumanReadableId]
,DeliveryAddressDescription
,[CustomerArtNo]
,[TotalPrice]
,r.[ArticleAlias]
,[Quantity]
,[DeliveredQuantity]
,r.[AdditionalInformation1]
,r.[AdditionalInformation2]
,[TradeUnits]
,[LoadingUnits]
,[Trucks]
,[LoadingToleranceType]
,[SalesPrice]
,[Currency]
,[QuantityUnit]
,[SalesPriceRemark]
,r.[Remark]
,[Irradiated]
,r.[CreatedByEdi]
,[DeliveryAddressHumanReadableId]
,DeliveryAddressDescription
,[CustomerArtNo]
,[TotalPrice]
,r.[ArticleAlias]
FROM [order].[Release] (nolock) AS r
LEFT JOIN [order].LineItem AS x ON r.LineItemId = x.id
LEFT JOIN [order].Header AS h ON x.HeaderId = h.id
FROM [order].[Release] (nolock) as r
-- 1.0 BOL (legacy) — unchanged
LEFT JOIN AlplaPROD_test1.dbo.V_LadePlanungenLadeAuftragAbruf (nolock) AS zz
ON zz.AbrufIdAuftragsAbruf = r.ReleaseNumber
LEFT JOIN (
SELECT * FROM (
SELECT ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum, *
FROM [AlplaPROD_test1].[dbo].[T_Lieferungen] (nolock)
) t WHERE RowNum = 1
) AS ea ON zz.IdLieferschein = ea.IdJournal
left join
[order].LineItem as x on
-- 2.0 BOL (new)
LEFT JOIN bol_20 ON bol_20.ReleaseId = r.Id AND bol_20.rn = 1
r.LineItemId = x.id
left join
[order].Header as h on
x.HeaderId = h.id
--bol stuff
left join
AlplaPROD_test1.dbo.V_LadePlanungenLadeAuftragAbruf (nolock) as zz
on zz.AbrufIdAuftragsAbruf = r.ReleaseNumber
left join
(select * from (SELECT
ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum
,*
FROM [AlplaPROD_test1].[dbo].[T_Lieferungen] (nolock)) x
where RowNum = 1) as ea on
zz.IdLieferschein = ea.IdJournal
where
--r.ReleaseNumber = 1452
r.DeliveryDate between @StartDate AND @EndDate
WHERE r.DeliveryDate BETWEEN @StartDate AND @EndDate
and DeliveredQuantity > 0
--and r.ArticleHumanReadableId in ([articles])
--and Journalnummer = 169386

View File

@@ -1,79 +0,0 @@
use AlplaPROD_test1
/**
move this over to the delivery date range query once we have the shift data mapped over correctly.
update the psi stuff on this as well.
**/
DECLARE @StartDate DATE = '[startDate]' -- 2025-1-1
DECLARE @EndDate DATE = '[endDate]' -- 2025-1-31
SELECT
r.[ArticleHumanReadableId]
,[ReleaseNumber]
,h.CustomerOrderNumber
,x.CustomerLineItemNumber
,[CustomerReleaseNumber]
,[ReleaseState]
,[DeliveryState]
,ea.JournalNummer as BOL_Number
,[ReleaseConfirmationState]
,[PlanningState]
--,format(r.[OrderDate], 'yyyy-MM-dd HH:mm') as OrderDate
,r.[OrderDate]
--,FORMAT(r.[DeliveryDate], 'yyyy-MM-dd HH:mm') as DeliveryDate
,r.[DeliveryDate]
--,FORMAT(r.[LoadingDate], 'yyyy-MM-dd HH:mm') as LoadingDate
,r.[LoadingDate]
,[Quantity]
,[DeliveredQuantity]
,r.[AdditionalInformation1]
,r.[AdditionalInformation2]
,[TradeUnits]
,[LoadingUnits]
,[Trucks]
,[LoadingToleranceType]
,[SalesPrice]
,[Currency]
,[QuantityUnit]
,[SalesPriceRemark]
,r.[Remark]
,[Irradiated]
,r.[CreatedByEdi]
,[DeliveryAddressHumanReadableId]
,DeliveryAddressDescription
,[CustomerArtNo]
,[TotalPrice]
,r.[ArticleAlias]
FROM [order].[Release] (nolock) as r
left join
[order].LineItem as x on
r.LineItemId = x.id
left join
[order].Header as h on
x.HeaderId = h.id
--bol stuff
left join
AlplaPROD_test1.dbo.V_LadePlanungenLadeAuftragAbruf (nolock) as zz
on zz.AbrufIdAuftragsAbruf = r.ReleaseNumber
left join
(select * from (SELECT
ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum
,*
FROM [AlplaPROD_test1].[dbo].[T_Lieferungen] (nolock)) x
where RowNum = 1) as ea on
zz.IdLieferschein = ea.IdJournal
where
r.ArticleHumanReadableId in ([articles])
--r.ReleaseNumber = 1452
and r.DeliveryDate between @StartDate AND @EndDate
--and DeliveredQuantity > 0
--and Journalnummer = 169386

View File

@@ -1,32 +1,72 @@
use AlplaPROD_test1
declare @start_date nvarchar(30) = '[startDate]' --'2025-01-01'
declare @end_date nvarchar(30) = '[endDate]' --'2025-08-09'
/*
articles will need to be passed over as well as the date structure we want to see
*/
use [test1_AlplaPROD2.0_Read]
select x.IdArtikelvarianten As Article,
ProduktionAlias as Description,
standort as MachineId,
MaschinenBezeichnung as MachineName,
--MaschZyklus as PlanningCycleTime,
x.IdProdPlanung as LotNumber,
FORMAT(ProdTag, 'MM/dd/yyyy') as ProductionDay,
x.planMenge as TotalPlanned,
ProduktionMenge as QTYPerDay,
round(ProduktionMengeVPK, 2) PalDay,
Status as finished
--MaschStdAuslastung as nee
from dbo.V_ProdLosProduktionJeProdTag_PLANNING (nolock) as x
left join
dbo.V_ProdPlanung (nolock) as p on
x.IdProdPlanung = p.IdProdPlanung
where ProdTag between @start_date and @end_date
and p.IdArtikelvarianten in ([articles])
--and V_ProdLosProduktionJeProdTag_PLANNING.IdKunde = 10
--and IdProdPlanung = 18442
order by ProdTag desc
DECLARE @start_date date = '[startDate]'; --'2025-01-01'
DECLARE @end_date date = '[endDate]'; --'2025-08-09'
DECLARE @tz sysname = 'Eastern Standard Time'; -- usday1; use 'Central Standard Time' for usksc1
DECLARE @shiftSeconds int = 7*3600; -- 07:00 production-day anchor
--TODO: add in the proper time zone based on the env, get correcr shift time as well
;WITH src AS (
SELECT
pl.RunningNumber, pl.ArticleHumanReadableId, pl.ArticleAlias, pl.ArticleDescription,
pl.MachineLocation, pl.MachineDescription, pl.ProductionLotState, pl.ProductionInterrupt,
pl.PlanQuantityPieces, pl.PlanQuantityLoadingUnit,
CAST(pl.ProdStart AT TIME ZONE @tz AS datetime2(0)) AS StartLocal,
CAST(pl.PlanEnd AT TIME ZONE @tz AS datetime2(0)) AS EndLocal
FROM productionScheduling.ProductionLot AS pl
WHERE pl.PlanEnd > pl.ProdStart
and pl.publishState = 1
and pl.ArticleHumanReadableId IN ([articles]) -- <-- article filter (was IdArtikelvarianten)
--AND pl.RunningNumber = 28094 -- <-- lot filter (was IdProdPlanung); comment out for all
),
calc AS (
SELECT *,
DATEADD(SECOND, @shiftSeconds,
CAST(CASE WHEN DATEDIFF(SECOND, CAST(StartLocal AS date), StartLocal) >= @shiftSeconds
THEN CAST(StartLocal AS date)
ELSE DATEADD(DAY,-1, CAST(StartLocal AS date)) END AS datetime2(0))) AS FirstBoundary
FROM src
),
days AS ( -- one row per production day the lot touches
SELECT RunningNumber, ArticleHumanReadableId, ArticleAlias, ArticleDescription, MachineLocation,
MachineDescription, ProductionLotState, PlanQuantityPieces, PlanQuantityLoadingUnit,
StartLocal, EndLocal, FirstBoundary AS DayStart
FROM calc
UNION ALL
SELECT RunningNumber, ArticleHumanReadableId, ArticleAlias, ArticleDescription, MachineLocation,
MachineDescription, ProductionLotState, PlanQuantityPieces, PlanQuantityLoadingUnit,
StartLocal, EndLocal, DATEADD(DAY,1,DayStart)
FROM days
WHERE DATEADD(DAY,1,DayStart) < EndLocal
),
seg AS ( -- working seconds inside each production day
SELECT *,
DATEDIFF(SECOND,
CASE WHEN StartLocal > DayStart THEN StartLocal ELSE DayStart END,
CASE WHEN EndLocal < DATEADD(DAY,1,DayStart) THEN EndLocal ELSE DATEADD(DAY,1,DayStart) END
) AS SegSec
FROM days
),
cum AS ( -- cumulative seconds for telescoping round
SELECT *,
SUM(SegSec) OVER (PARTITION BY RunningNumber ORDER BY DayStart ROWS UNBOUNDED PRECEDING) AS CumEnd,
SUM(SegSec) OVER (PARTITION BY RunningNumber ORDER BY DayStart ROWS UNBOUNDED PRECEDING) - SegSec AS CumStart,
SUM(SegSec) OVER (PARTITION BY RunningNumber) AS DenomSec
FROM seg
)
SELECT
ArticleHumanReadableId AS Article,
ArticleAlias AS Description,
MachineLocation AS MachineId,
MachineDescription AS MachineName,
RunningNumber AS LotNumber,
FORMAT(DayStart, 'MM/dd/yyyy') AS ProductionDay,
PlanQuantityPieces AS TotalPlanned,
ROUND(PlanQuantityPieces * 1.0 * CumEnd / DenomSec, 0)
- ROUND(PlanQuantityPieces * 1.0 * CumStart / DenomSec, 0) AS QTYPerDay,
ROUND(PlanQuantityLoadingUnit * CumEnd / DenomSec, 2)
- ROUND(PlanQuantityLoadingUnit * CumStart / DenomSec, 2) AS PalDay,
ProductionLotState AS finished
FROM cum
WHERE CAST(DayStart AS date) BETWEEN @start_date AND @end_date -- filter AFTER cumulative calc
ORDER BY RunningNumber, DayStart DESC
OPTION (MAXRECURSION 366);

View File

@@ -19,13 +19,13 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
export const setupRoutes = (baseUrl: string, app: Express) => {
//routes that are on by default
setupDatamartRoutes(baseUrl, app);
setupMobileRoutes(baseUrl, app);
setupSystemRoutes(baseUrl, app);
setupAdminRoutes(baseUrl, app);
setupApiDocsRoutes(baseUrl, app);
setupProdSqlRoutes(baseUrl, app);
setupGPSqlRoutes(baseUrl, app);
setupDatamartRoutes(baseUrl, app);
setupAuthRoutes(baseUrl, app);
setupUtilsRoutes(baseUrl, app);
setupOpendockRoutes(baseUrl, app);

View File

@@ -1,43 +0,0 @@
import type { OpenAPIV3_1 } from "openapi-types";
export const cronerActiveJobs: OpenAPIV3_1.PathsObject = {
"/api/utils/croner": {
get: {
summary: "Cron jobs",
description: "Returns all jobs on the server.",
tags: ["Utils"],
responses: {
"200": {
description: "Jobs returned",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
format: "boolean",
example: true,
},
uptime: {
type: "number",
format: "3454.34",
example: 3454.34,
},
memoryUsage: {
type: "string",
format: "Heap: 11.62 MB / RSS: 86.31 MB",
},
sqlServerStats: {
type: "number",
format: "442127",
},
},
},
},
},
},
},
},
},
};

View File

@@ -1,94 +0,0 @@
import type { OpenAPIV3_1 } from "openapi-types";
export const cronerStatusChange: OpenAPIV3_1.PathsObject = {
"/api/utils/croner/{status}": {
patch: {
summary: "Pauses or Resume the Job",
description:
"When sending start or stop with job name it will resume or stop the job",
tags: ["Utils"],
parameters: [
{
name: "status",
in: "path",
required: true,
description: "Status change",
schema: {
type: "string",
},
example: "start",
},
{
name: "limit",
in: "query",
required: false, // 👈 optional
description: "Maximum number of records to return",
schema: {
type: "integer",
minimum: 1,
maximum: 100,
},
example: 10,
},
],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["name"],
properties: {
name: {
type: "string",
example: "start",
},
},
},
},
},
},
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", example: true },
data: {
type: "object",
example: {
name: "exampleName",
value: "some value",
},
},
},
},
},
},
},
"400": {
description: "Bad request",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", example: false },
message: {
type: "string",
example: "Invalid name parameter",
},
},
},
},
},
},
},
},
},
};

View File

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

View File

@@ -1,10 +1,14 @@
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { LaptopMinimal } from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarFooter,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { useSession } from "@/lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings";
@@ -23,6 +27,7 @@ export function AppSidebar() {
openDock: ["read"],
}),
);
const { setOpen } = useSidebar();
// const { data: canReadWarehouse = false } = useQuery(
// permissionQuery({
@@ -36,7 +41,7 @@ export function AppSidebar() {
collapsible="offcanvas"
className="top-(--header-height) h-[calc(100svh-var(--header-height))]!"
>
<SidebarHeader>
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarContent>
@@ -64,7 +69,24 @@ export function AppSidebar() {
</SidebarContent>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
</SidebarContent>
{session &&
(session.user.role === "admin" ||
session.user.role === "systemAdmin" ||
session.user.role === "manager") && (
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link to={"/apidocs"} onClick={() => setOpen(false)}>
<LaptopMinimal />
<span>Api docs</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
)}
</Sidebar>
);
}

View File

@@ -11,9 +11,11 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as ForbiddenRouteImport } from './routes/forbidden'
import { Route as AppDownRouteImport } from './routes/app-down'
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 DocsDatamartRouteImport } from './routes/docs/datamart'
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminUsersRouteImport } from './routes/admin/users'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
@@ -41,6 +43,11 @@ const AppDownRoute = AppDownRouteImport.update({
path: '/app-down',
getParentRoute: () => rootRouteImport,
} as any)
const ApidocsRoute = ApidocsRouteImport.update({
id: '/apidocs',
path: '/apidocs',
getParentRoute: () => rootRouteImport,
} as any)
const AboutRoute = AboutRouteImport.update({
id: '/about',
path: '/about',
@@ -56,6 +63,11 @@ const DocsIndexRoute = DocsIndexRouteImport.update({
path: '/docs/',
getParentRoute: () => rootRouteImport,
} as any)
const DocsDatamartRoute = DocsDatamartRouteImport.update({
id: '/docs/datamart',
path: '/docs/datamart',
getParentRoute: () => rootRouteImport,
} as any)
const DocsSplatRoute = DocsSplatRouteImport.update({
id: '/docs/$',
path: '/docs/$',
@@ -145,6 +157,7 @@ const WarehouseDockdoorscanningScansDockScansRoute =
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/apidocs': typeof ApidocsRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/login': typeof authLoginRoute
@@ -155,6 +168,7 @@ export interface FileRoutesByFullPath {
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs/datamart': typeof DocsDatamartRoute
'/docs/': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
@@ -168,6 +182,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/apidocs': typeof ApidocsRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/login': typeof authLoginRoute
@@ -178,6 +193,7 @@ export interface FileRoutesByTo {
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs/datamart': typeof DocsDatamartRoute
'/docs': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
@@ -192,6 +208,7 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/apidocs': typeof ApidocsRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/(auth)/login': typeof authLoginRoute
@@ -202,6 +219,7 @@ export interface FileRoutesById {
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs/datamart': typeof DocsDatamartRoute
'/docs/': typeof DocsIndexRoute
'/(auth)/user/profile': typeof authUserProfileRoute
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
@@ -217,6 +235,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/about'
| '/apidocs'
| '/app-down'
| '/forbidden'
| '/login'
@@ -227,6 +246,7 @@ export interface FileRouteTypes {
| '/admin/settings'
| '/admin/users'
| '/docs/$'
| '/docs/datamart'
| '/docs/'
| '/user/profile'
| '/user/resetpassword'
@@ -240,6 +260,7 @@ export interface FileRouteTypes {
to:
| '/'
| '/about'
| '/apidocs'
| '/app-down'
| '/forbidden'
| '/login'
@@ -250,6 +271,7 @@ export interface FileRouteTypes {
| '/admin/settings'
| '/admin/users'
| '/docs/$'
| '/docs/datamart'
| '/docs'
| '/user/profile'
| '/user/resetpassword'
@@ -263,6 +285,7 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/about'
| '/apidocs'
| '/app-down'
| '/forbidden'
| '/(auth)/login'
@@ -273,6 +296,7 @@ export interface FileRouteTypes {
| '/admin/settings'
| '/admin/users'
| '/docs/$'
| '/docs/datamart'
| '/docs/'
| '/(auth)/user/profile'
| '/(auth)/user/resetpassword'
@@ -287,6 +311,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
ApidocsRoute: typeof ApidocsRoute
AppDownRoute: typeof AppDownRoute
ForbiddenRoute: typeof ForbiddenRoute
authLoginRoute: typeof authLoginRoute
@@ -297,6 +322,7 @@ export interface RootRouteChildren {
AdminSettingsRoute: typeof AdminSettingsRoute
AdminUsersRoute: typeof AdminUsersRoute
DocsSplatRoute: typeof DocsSplatRoute
DocsDatamartRoute: typeof DocsDatamartRoute
DocsIndexRoute: typeof DocsIndexRoute
authUserProfileRoute: typeof authUserProfileRoute
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
@@ -324,6 +350,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppDownRouteImport
parentRoute: typeof rootRouteImport
}
'/apidocs': {
id: '/apidocs'
path: '/apidocs'
fullPath: '/apidocs'
preLoaderRoute: typeof ApidocsRouteImport
parentRoute: typeof rootRouteImport
}
'/about': {
id: '/about'
path: '/about'
@@ -345,6 +378,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/docs/datamart': {
id: '/docs/datamart'
path: '/docs/datamart'
fullPath: '/docs/datamart'
preLoaderRoute: typeof DocsDatamartRouteImport
parentRoute: typeof rootRouteImport
}
'/docs/$': {
id: '/docs/$'
path: '/docs/$'
@@ -463,6 +503,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
ApidocsRoute: ApidocsRoute,
AppDownRoute: AppDownRoute,
ForbiddenRoute: ForbiddenRoute,
authLoginRoute: authLoginRoute,
@@ -473,6 +514,7 @@ const rootRouteChildren: RootRouteChildren = {
AdminSettingsRoute: AdminSettingsRoute,
AdminUsersRoute: AdminUsersRoute,
DocsSplatRoute: DocsSplatRoute,
DocsDatamartRoute: DocsDatamartRoute,
DocsIndexRoute: DocsIndexRoute,
authUserProfileRoute: authUserProfileRoute,
authUserResetpasswordRoute: authUserResetpasswordRoute,

View File

@@ -19,8 +19,10 @@ const RootLayout = () => {
<div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar />
<main className="w-full p-4">
<div className="mx-auto w-full max-w-7xl">
<main className="w-full">
<div className="mx-auto w-full flex justify-center">
{" "}
{/* className="mx-auto w-full max-w-7xl" use this for dashboards and stuff*/}
<Outlet />
</div>
</main>
@@ -31,7 +33,7 @@ const RootLayout = () => {
</SidebarProvider>
</ThemeProvider>
{session && session.user.role === "systemAdmin" && (
<TanStackRouterDevtools />
<TanStackRouterDevtools position="bottom-right" />
)}
</div>
);

View File

@@ -248,7 +248,7 @@ function RouteComponent() {
};
//console.log(logs);
return (
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 max-w-7xl">
<div className="flex gap-1 justify-end">
<Button onClick={triggerBuild}>Trigger Build</Button>
<Button onClick={() => clearRoom()}>Clear Logs</Button>

View File

@@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/apidocs")({
component: RouteComponent,
});
function RouteComponent() {
return (
<iframe
src="/lst/api/docs"
className="h-[calc(100vh-64px)] w-full border-0"
title="LST API Docs"
/>
);
}

View File

@@ -1,18 +1,18 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import { Trash } from "lucide-react";
import { Suspense, useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import { Spinner } from "../../../components/ui/spinner";
import { api } from "../../../lib/apiHelper";
import { authClient } from "../../../lib/auth-client";
import { getArticleLinks } from "../../../lib/queries/getArticleLinks";
import LstTable from "../../../lib/tableStuff/LstTable";
import SearchableHeader from "../../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../../lib/tableStuff/SkellyTable";
import NewArticleLink from "./-components/NewArticleLink";
import { api } from "../../../lib/apiHelper";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import { Spinner } from "../../../components/ui/spinner";
import { Trash } from "lucide-react";
export const Route = createFileRoute("/transportation/opendock/")({
beforeLoad: async ({ location }) => {
@@ -97,77 +97,77 @@ const ArticleLinkTable = () => {
cell: (i) => i.getValue(),
}),
columnHelper.accessor("deleteUser", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Delete Link"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => {
// biome-ignore lint: just removing the lint for now to get this going will maybe fix later
const [activeToggle, setActiveToggle] = useState(false);
const onTrigger = async () => {
setActiveToggle(true);
try {
const res = await api.delete(
`/opendock/articleCheck/${i.row.original.id}`,
{
withCredentials: true,
timeout: 5000,
validateStatus: () => true,
},
);
if (res.data.success) {
toast.success(`AV: ${i.row.original.av} was deleted.`);
refetch();
setActiveToggle(false);
}
if (!res.data.success) {
toast.error(
`AV: ${i.row.original.av} encountered an error when trying to delete: ${res.data.message}`,
);
refetch();
setActiveToggle(false);
}
} catch (error) {
setActiveToggle(false);
console.error(error);
}
};
return (
<div>
<div className="flex items-center space-x-2">
<Button
variant="destructive"
disabled={activeToggle}
onClick={onTrigger}
>
{activeToggle ? (
<span>
<Spinner />
</span>
) : (
<span>
<Trash />
</span>
)}
</Button>
</div>
</div>
header: ({ column }) => (
<SearchableHeader
column={column}
title="Delete Link"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => {
// biome-ignore lint: just removing the lint for now to get this going will maybe fix later
const [activeToggle, setActiveToggle] = useState(false);
const onTrigger = async () => {
setActiveToggle(true);
try {
const res = await api.delete(
`/opendock/articleCheck/${i.row.original.id}`,
{
withCredentials: true,
timeout: 5000,
validateStatus: () => true,
},
);
},
}),
if (res.data.success) {
toast.success(`AV: ${i.row.original.av} was deleted.`);
refetch();
setActiveToggle(false);
}
if (!res.data.success) {
toast.error(
`AV: ${i.row.original.av} encountered an error when trying to delete: ${res.data.message}`,
);
refetch();
setActiveToggle(false);
}
} catch (error) {
setActiveToggle(false);
console.error(error);
}
};
return (
<div>
<div className="flex items-center space-x-2">
<Button
variant="destructive"
disabled={activeToggle}
onClick={onTrigger}
>
{activeToggle ? (
<span>
<Spinner />
</span>
) : (
<span>
<Trash />
</span>
)}
</Button>
</div>
</div>
);
},
}),
];
return (
<div>
<div className="">
<div>
<div className="flex justify-end m-2">
<Suspense

90
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"license": "ISC",
"dependencies": {
"@dotenvx/dotenvx": "^1.57.0",
"@scalar/express-api-reference": "^0.9.4",
"@scalar/express-api-reference": "^0.9.20",
"@socket.io/admin-ui": "^0.5.1",
"archiver": "^7.0.1",
"axios": "^1.13.6",
@@ -44,7 +44,8 @@
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"xlsx": "^0.18.5",
"zod": "^4.3.6"
"zod": "^4.3.6",
"zod-openapi": "^6.0.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.8",
@@ -2424,46 +2425,61 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@scalar/core": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@scalar/core/-/core-0.4.4.tgz",
"integrity": "sha512-eXIG0opyQn45FzpTp0dAWFP1Vjcx+helgUAsa0uN36tyUR7DSmz2kRwHqqedzvPWryeRCKPz7/vwzKpETZp5lg==",
"node_modules/@scalar/client-side-rendering": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/@scalar/client-side-rendering/-/client-side-rendering-0.1.13.tgz",
"integrity": "sha512-p8V4HgEWjaCpqsnhclg1pTfjE9JA0AWRr0ocBQHexoHo+pqnSs1d83Mv9rjH7R0FZJrlCSandZZeY3DMX2gYXQ==",
"license": "MIT",
"dependencies": {
"@scalar/types": "0.7.4"
"@scalar/schemas": "0.3.3",
"@scalar/types": "0.12.3",
"@scalar/validation": "0.6.0"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/express-api-reference": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/@scalar/express-api-reference/-/express-api-reference-0.9.4.tgz",
"integrity": "sha512-KXG+VaMArCGcWhzDV2rfkHd+UF1HYevIFbO6cqFpd+az7QHvVT99BU8Yh60T1dmtCp504s0Pl/vcTyJ91fK1Ug==",
"version": "0.9.20",
"resolved": "https://registry.npmjs.org/@scalar/express-api-reference/-/express-api-reference-0.9.20.tgz",
"integrity": "sha512-J0P6qpYoL0kXvs/A/vuAwCqQFCYnErbXSB5/3lEGTbARuK0oGyMvl55dQyW5Ucq3CX1npuRejlTX6bxEprSvJA==",
"license": "MIT",
"dependencies": {
"@scalar/core": "0.4.4"
"@scalar/client-side-rendering": "0.1.13"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/helpers": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.4.2.tgz",
"integrity": "sha512-IrgrGVSahCfYDNWITazz4Q1BOndp5eEzlimRkfxiYn++KqeWyLfALyym1omqcdKGYtiSx1KIbKaUJL9vkjaN7w==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.8.1.tgz",
"integrity": "sha512-yuiuBCadP5bjAnIv23QvifVN/NaMi9xBF6b8Wdk4QOzwzLPJmp699MAdf33J0A5i2qKcvnu32iz/VkEJmQRe5g==",
"license": "MIT",
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/types": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.7.4.tgz",
"integrity": "sha512-1o9uf42lZ9YD0XP/HMWrwXN0unx6vFTTgtduA1F28Yloea9Pfv9N2R/t0wO91iSIzw4+NubEFolunbdb2QcgHA==",
"node_modules/@scalar/schemas": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@scalar/schemas/-/schemas-0.3.3.tgz",
"integrity": "sha512-qDcgFu6ta5Z90L9D2P6DFKzYesU+FW5+m55SGmdI4iRMRCwj5umHpec2Y2W/SJcCF6bbUZawMxuOH2Ja6rUNpQ==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.4.2",
"@scalar/helpers": "0.8.1",
"@scalar/validation": "0.6.0"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/types": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.12.3.tgz",
"integrity": "sha512-7zaXafbgTFmsJ/9AwYeExUWzXoZNyKOL0SEVAUWRaOndcjxpFCtwzuPrc1elMEWdHopWbY1Qe5pWKbE2aqG2HA==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.8.1",
"nanoid": "^5.1.6",
"type-fest": "^5.3.1",
"zod": "^4.3.5"
@@ -2472,6 +2488,15 @@
"node": ">=22"
}
},
"node_modules/@scalar/validation": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@scalar/validation/-/validation-0.6.0.tgz",
"integrity": "sha512-tpmmG+/xRE2Kn9RpflU3AIyZv08v10+E1ZrJCx7z6+/91zHVxy0M73kC1LT4/8PbYNt85ywyC8+n+D99JdMcGA==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/@serialport/binding-mock": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
@@ -10566,9 +10591,9 @@
"license": "ISC"
},
"node_modules/nanoid": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz",
"integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==",
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
"integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
"funding": [
{
"type": "github",
@@ -13252,9 +13277,9 @@
}
},
"node_modules/type-fest": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz",
"integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz",
"integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==",
"license": "(MIT OR CC0-1.0)",
"dependencies": {
"tagged-tag": "^1.0.0"
@@ -13937,6 +13962,21 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-openapi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zod-openapi/-/zod-openapi-6.0.0.tgz",
"integrity": "sha512-mS4eRJ4DGCPrg6elRbJqc/3nLe4EPVi8KiHRKZ7dcTR5m5orPy8EfoWmceAyGZAq71MAWuyrTTOag7W5N61ZPQ==",
"license": "MIT",
"engines": {
"node": ">=22.14.0"
},
"funding": {
"url": "https://github.com/samchungy/zod-openapi?sponsor=1"
},
"peerDependencies": {
"zod": "^4.0.0"
}
}
}
}

View File

@@ -68,7 +68,7 @@
},
"dependencies": {
"@dotenvx/dotenvx": "^1.57.0",
"@scalar/express-api-reference": "^0.9.4",
"@scalar/express-api-reference": "^0.9.20",
"@socket.io/admin-ui": "^0.5.1",
"archiver": "^7.0.1",
"axios": "^1.13.6",
@@ -102,7 +102,8 @@
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"xlsx": "^0.18.5",
"zod": "^4.3.6"
"zod": "^4.3.6",
"zod-openapi": "^6.0.0"
},
"config": {
"commitizen": {

View File

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