Compare commits
23 Commits
a2d9a6c127
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e8417dcff | |||
| 1b1918dcd0 | |||
| 4ff10dfcb2 | |||
| 9a0bb18c5b | |||
| 1838c6f1e9 | |||
| 3a24d62957 | |||
| 6a14bab30c | |||
| 2ebf695526 | |||
| 24af3ca403 | |||
| 6fbe3a9eed | |||
| 7dbc31c046 | |||
| ba09a77f29 | |||
| 3f04609f82 | |||
| 22a7b612e1 | |||
| 39db142db4 | |||
| d85f08cb19 | |||
| 15c939ebe8 | |||
| 9ff428f5ea | |||
| 2e460c7f8a | |||
| 7c4c5f980a | |||
| 86e1237509 | |||
| 706ab8b448 | |||
| 9440b44f3b |
79
CHANGELOG.md
79
CHANGELOG.md
@@ -1,5 +1,84 @@
|
||||
# All Changes to LST can be found below.
|
||||
|
||||
## [0.1.0-alpha.3](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.2...v0.1.0-alpha.3) (2026-06-10)
|
||||
|
||||
|
||||
### 🌟 Enhancements
|
||||
|
||||
* **eom:** migrated eom endpoints from old version validated working ([e909e8d](https://git.tuffraid.net/cowch/lst_v3/commits/e909e8deecb54a3e4c39789609b0aa7435b9e08a))
|
||||
* **mobile:** added auto download of latest ([6d0fb8a](https://git.tuffraid.net/cowch/lst_v3/commits/6d0fb8aee45c8b5c56ccd7d8a010e1dc803408bf))
|
||||
* **mobile:** dock door scanning backend added ([fe0b157](https://git.tuffraid.net/cowch/lst_v3/commits/fe0b1573f3ba6fd220f181088b994588c52af139)), closes [#12](https://git.tuffraid.net/cowch/lst_v3/issues/12)
|
||||
* **opendock:** added delete button in the article tab ([4464cea](https://git.tuffraid.net/cowch/lst_v3/commits/4464cea022ba48744b884b83fef0fc3f3421dea5)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
|
||||
* **opendock:** added in a new article link system ([bb7931d](https://git.tuffraid.net/cowch/lst_v3/commits/bb7931d6c8d0f5ce4065955491e9ee1247b5e92d))
|
||||
* **warehousing:** ppoo monitoring added ([8b07694](https://git.tuffraid.net/cowch/lst_v3/commits/8b076949a7f8e723bc87619f729082d2c1991b2d)), closes [#13](https://git.tuffraid.net/cowch/lst_v3/issues/13)
|
||||
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
* **app:** type in the templates... they all looked the same ([2a648f6](https://git.tuffraid.net/cowch/lst_v3/commits/2a648f63064a300b0a2888bae3322b857af6a238))
|
||||
* **dock door scanning:** correction to how the data is posted ([2f49573](https://git.tuffraid.net/cowch/lst_v3/commits/2f495739e653c462eff7f10ff6343d9572f25563))
|
||||
* **dockscanner:** removed console log ([4249e90](https://git.tuffraid.net/cowch/lst_v3/commits/4249e90307ba1a1992753803e1dc3ab7dd7ac95e))
|
||||
* **eom:** removed un needed imports ([05c553e](https://git.tuffraid.net/cowch/lst_v3/commits/05c553e9279c6e8384d61073781bf915733b0ab5))
|
||||
* **logistics:** historical checks for no data errors when feature is activeed ([4f848bb](https://git.tuffraid.net/cowch/lst_v3/commits/4f848bb649f350c9d370daa09c6fc48f7b76e2e2))
|
||||
* **mobile users:** corrected and endpoint that prevented us from change the pin number ([347edb7](https://git.tuffraid.net/cowch/lst_v3/commits/347edb7078fb4ce959975cd968a6f026bacc98bf))
|
||||
* **mobile:** scan log incorrect user ref ([9c0ef1f](https://git.tuffraid.net/cowch/lst_v3/commits/9c0ef1f5dfa13e8f7e1f72d47d0d5842b3da3c87))
|
||||
* **mobile:** temp removed the autodownload as its causing issues ([3ef0f23](https://git.tuffraid.net/cowch/lst_v3/commits/3ef0f230ddbaef4c3d738cc531e7afee25b210dd))
|
||||
* **mobile:** ui over lapping ([db28635](https://git.tuffraid.net/cowch/lst_v3/commits/db28635c8c260d0f378e109755d32201acdb2328)), closes [#25](https://git.tuffraid.net/cowch/lst_v3/issues/25)
|
||||
* **notifications:** missed api converstion in the front end for updating ([5281118](https://git.tuffraid.net/cowch/lst_v3/commits/52811185965cb0fe4c9b42e73c447b67e917a5ca))
|
||||
* **opendock:** changed the header of delete to be properly named ([865f280](https://git.tuffraid.net/cowch/lst_v3/commits/865f280cfaf82a7126ca4b57d658aed50cf19a73)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
|
||||
* **opendock:** correction to article link success on delete ([a717260](https://git.tuffraid.net/cowch/lst_v3/commits/a717260b8d5cf72534b055721de2b79901506875)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
|
||||
* **opendock:** ref wrong field oops ([f635415](https://git.tuffraid.net/cowch/lst_v3/commits/f635415b751e11d0e7beb557d18b83915155428a))
|
||||
* **scanner:** corrected teh endpoint to delete the user if needed ([6dd1a5b](https://git.tuffraid.net/cowch/lst_v3/commits/6dd1a5b332a853f0e2c7226ac1f64e403ef752d7))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* **app:** updated last updated to the readme to show current progress ([78f7b8a](https://git.tuffraid.net/cowch/lst_v3/commits/78f7b8a179d078fc1b2740fd2bf4af71a3df8292))
|
||||
* **app:** updated readme ([1a3d8a7](https://git.tuffraid.net/cowch/lst_v3/commits/1a3d8a7ddcb7a2e51da4b77968ffdefb7644a0b1))
|
||||
|
||||
|
||||
### 🛠️ Code Refactor
|
||||
|
||||
* **backend:** dock door scanning socket and perms ([f8335f5](https://git.tuffraid.net/cowch/lst_v3/commits/f8335f5217c339a7dc09883fa8ba9c60aa4c35b1))
|
||||
* **datamart:** if added to query will include plant token now ([7d722c4](https://git.tuffraid.net/cowch/lst_v3/commits/7d722c4aac16bc1d2cfbfbc70539783f34003f77))
|
||||
* **db:** added in notifications vs pulling from the db makes it easier on the system ([706ab8b](https://git.tuffraid.net/cowch/lst_v3/commits/706ab8b448aafb81da94a76fbf8d3d400cba616b))
|
||||
* **db:** added timezone check in so it comes over correct based on the backend timezone ([2558b2e](https://git.tuffraid.net/cowch/lst_v3/commits/2558b2e5bb68be7a3f46de09bb509c99423adeb6))
|
||||
* **dock door scanning:** fixes and final writes for the intial trial went smooth ([86e1237](https://git.tuffraid.net/cowch/lst_v3/commits/86e1237509b81722dee7b42762d0bfced8d26fa3))
|
||||
* **dock scanner:** more work on dock scanner and semi finished ([6eaae0f](https://git.tuffraid.net/cowch/lst_v3/commits/6eaae0f5378e39c4002dadd8325833698dd960e7))
|
||||
* **dockscanning:** more work on the dock door scanning ([7671172](https://git.tuffraid.net/cowch/lst_v3/commits/7671172d975355b5d245a482b481726b49153578)), closes [#12](https://git.tuffraid.net/cowch/lst_v3/issues/12)
|
||||
* **logger:** included error in the stack version so we dont have to remove it all ([da87e2e](https://git.tuffraid.net/cowch/lst_v3/commits/da87e2e1d322a45ca9a7b500d77499fe4c7b999e))
|
||||
* **logs:** refactored to show the submodule and stack as well to make it more easy to watch ([45a0dee](https://git.tuffraid.net/cowch/lst_v3/commits/45a0dee9caae14df639439b905fa81b340791197))
|
||||
* **logs:** socket io setup to be properly logging ([9ff428f](https://git.tuffraid.net/cowch/lst_v3/commits/9ff428f5ea1051e62521632947092f74eb93944d))
|
||||
* **mobile:** intial addin of dockdoor scanning on mobile ([2a35381](https://git.tuffraid.net/cowch/lst_v3/commits/2a35381fe400fc46e6f10c4e72c3ab9ac435e0e5))
|
||||
* **mobile:** moved logout to the tab bar ([bcdf956](https://git.tuffraid.net/cowch/lst_v3/commits/bcdf9566bcf721a085f46ce3befe783eb7b91949))
|
||||
* **mobile:** new error found and added in ([7bbdd4e](https://git.tuffraid.net/cowch/lst_v3/commits/7bbdd4e5552889434aff81ed10ea7c64174f7d34))
|
||||
* **mobile:** setup - added button to go home as it caused confustion ([c15ee07](https://git.tuffraid.net/cowch/lst_v3/commits/c15ee070e7057ad8a5e3d42f51c230d680db9e21))
|
||||
* **new role:** added in warehouse role ([55418e2](https://git.tuffraid.net/cowch/lst_v3/commits/55418e2f098193b0891129a19532608dce2abd9c))
|
||||
* **opend dock:** added in default dock so it uses the default setup as a backup ([8fc3129](https://git.tuffraid.net/cowch/lst_v3/commits/8fc3129f7d687d45dc242aca9a6e71f43d0352ab))
|
||||
* **opendock:** added in check for really using the article link ([cfc497c](https://git.tuffraid.net/cowch/lst_v3/commits/cfc497c1f2254dc12a6e7bb15cfc0dce2a64c1e6))
|
||||
* **opendock:** added in proper complete and ignore of picksheets ([3ad84da](https://git.tuffraid.net/cowch/lst_v3/commits/3ad84dab71ceea081efc824e97d075c6f33807ad))
|
||||
* **opendock:** added some new goodies to the app to help manage releases ([c0a7d4a](https://git.tuffraid.net/cowch/lst_v3/commits/c0a7d4a1252e3f14240d14288a3ac4add44a6463))
|
||||
* **opendock:** changed the article to look at the label to match all plants ([a2d9a6c](https://git.tuffraid.net/cowch/lst_v3/commits/a2d9a6c127cae3b6dc5beb1a996558ab1383fa8c))
|
||||
* **opendock:** changed the subModule for better logging ([7c4c5f9](https://git.tuffraid.net/cowch/lst_v3/commits/7c4c5f980a15e13e2a43a8583a652622370ec1dd))
|
||||
* **opendock:** if load type is drop we will not do anything unless its already scanned ([5a7046b](https://git.tuffraid.net/cowch/lst_v3/commits/5a7046bacd9323627a37c89d7a9a329383e8b1be))
|
||||
* **server builds:** added in proper logging will still need fine tuned though ([15c939e](https://git.tuffraid.net/cowch/lst_v3/commits/15c939ebe8b808b3a9a46a4c1e62fb0488d3649b))
|
||||
* **servers:** all remaining servers added ([5f2148f](https://git.tuffraid.net/cowch/lst_v3/commits/5f2148f5f0f7688cf71e1dad99d4a5f6c34241dd))
|
||||
* **socket.io:** complete rewrite to manage dynamic rooms and seeding better ([9440b44](https://git.tuffraid.net/cowch/lst_v3/commits/9440b44f3bb1842d0e6ffc479d340ae9c5b84656))
|
||||
* **socketio:** changes that if we are in dock room to constant reseed the room ([8fcb2c6](https://git.tuffraid.net/cowch/lst_v3/commits/8fcb2c66ed25c746dc1d63c5290e11fe6a56328e))
|
||||
* **times:** added a better view for times as we save all db as there respective timezone ([40edbc3](https://git.tuffraid.net/cowch/lst_v3/commits/40edbc3eae9000afea447f81ec127884712b38da))
|
||||
* **warehouse:** fixed ppoo check to match the new changes ([2e460c7](https://git.tuffraid.net/cowch/lst_v3/commits/2e460c7f8aeb7de1d641c42357001704a4d8d8b2))
|
||||
|
||||
|
||||
### 📝 Testing Code
|
||||
|
||||
* **test:** added in vitest to start doing more testing before deploying ([e6b92ae](https://git.tuffraid.net/cowch/lst_v3/commits/e6b92aeb109394691dd2a7d548ddadd02769a159))
|
||||
|
||||
|
||||
### 📈 Project changes
|
||||
|
||||
* **app:** added more labels to the templates ([69a9a81](https://git.tuffraid.net/cowch/lst_v3/commits/69a9a81a889aca010ec8835ffab7084f7cd4a9b0))
|
||||
* **app:** changed the issues templates to be easier to add to the part of the app needed ([188fdd0](https://git.tuffraid.net/cowch/lst_v3/commits/188fdd0eb7c6bedf52ae1dcc9109a0fdbe969a21))
|
||||
|
||||
## [0.1.0-alpha.2](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.1...v0.1.0-alpha.2) (2026-05-23)
|
||||
|
||||
|
||||
|
||||
90
README.md
90
README.md
@@ -7,7 +7,7 @@
|
||||
Quick summary of current rewrite/migration goal.
|
||||
|
||||
- **Phase:** Backend rewrite
|
||||
- **Last updated:** 2026-05-27
|
||||
- **Last updated:** 2026-06-17
|
||||
|
||||
---
|
||||
|
||||
@@ -39,21 +39,91 @@ _Status legend:_
|
||||
|
||||
---
|
||||
|
||||
## Setup / Installation
|
||||
# Install
|
||||
|
||||
How to run the current version of the app.
|
||||
## Files needed to be downloaded before install.
|
||||
|
||||
### To run the server
|
||||
|
||||
- [PostgresSQL](https://www.postgresql.org/download/windows/) - current version using is 17
|
||||
- [NodeJS](https://nodejs.org)
|
||||
- [NSSM](https://nssm.cc/)
|
||||
|
||||
### To manage the server
|
||||
|
||||
- [VSCODE](https://code.visualstudio.com/)
|
||||
|
||||
## Creating directories needed
|
||||
|
||||
- Create a new folder where we will host the server files.
|
||||
- Copy the nssm.exe into this folder
|
||||
- Copy the get the build from the releases and extract.
|
||||
- This will house all the compiles and minified files needed to start the server up, this includes the frontend.
|
||||
- Save the nssm.exe into this folder as well, this will be used to control the service.
|
||||
|
||||
## Do the initial install
|
||||
|
||||
### DB instal setup
|
||||
|
||||
1. Install postgres
|
||||
2. Open pgAdmin
|
||||
3. create a new Database named lst_db_v3. this can also be to your liking
|
||||
|
||||
### Initial server setup
|
||||
|
||||
1. Open VSCode and navigate to the folder where you extracted the files.
|
||||
2. Click trusted when it pops up.
|
||||
3. Open a terminal window inside vscode.
|
||||
4. Run the install script this will install all dependence's needed as well as do all the database migrations
|
||||
|
||||
|
||||
### Create the .env file
|
||||
|
||||
In the root of the folder create a new .env file by renaming .env-example to .env
|
||||
|
||||
change all the parameters to your desired server
|
||||
|
||||
```bash
|
||||
git clone https://git.tuffraid.net/cowch/lst_v3.git
|
||||
cd lst_v3
|
||||
npm install
|
||||
npm run install --omit=dev
|
||||
```
|
||||
|
||||
Rename the .env-example to .env
|
||||
|
||||
Update all the fields
|
||||
Next we want to do an initial db
|
||||
|
||||
```bash
|
||||
npm run dev:db:migrate
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Run the start command to get all the basic settings and modules installed
|
||||
|
||||
1. Run the below
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### Creating first user.
|
||||
|
||||
Open http://[SERVER]:[PORT]/api/docs or postman and create a user.
|
||||
- Please do not try to manually enter a new user this is due to how the password is hashed, as well as setting systemAdmin for the first user.
|
||||
- Change the server and port to what you changed in the DB.
|
||||
|
||||
### Running as a serivice.
|
||||
|
||||
You want to CD into the scripts folder.
|
||||
|
||||
```bash
|
||||
cd .\scripts\
|
||||
```
|
||||
|
||||
Next use the example command below to get the service up and running.
|
||||
|
||||
- Options legend
|
||||
- serviceName = not recommended to change to reduce issues with the update process
|
||||
- option = use install for the install, but you can use this script later to stop, start, restart the service.
|
||||
- appPath = where did you extract the server files
|
||||
- description = no need to change this unless you want it to be something else
|
||||
- command = do not change this unless you know what your doing and really need to change this.
|
||||
|
||||
```powershell
|
||||
.\services.ps1 -serviceName "LSTV3_app" -option "install" -appPath "D:\LS_V3T" -description "Logistics Support Tool V3" -command "run start"
|
||||
```
|
||||
@@ -9,6 +9,7 @@ import { routeHitMiddleware } from "./middleware/routeHit.middleware.js";
|
||||
import { setupRoutes } from "./routeHandler.routes.js";
|
||||
import { auth } from "./utils/auth.utils.js";
|
||||
import { lstCors } from "./utils/cors.utils.js";
|
||||
import { getAppVersion } from "./utils/version.utils.js";
|
||||
|
||||
const createApp = async () => {
|
||||
const log = createLogger({ module: "system", subModule: "main start" });
|
||||
@@ -35,6 +36,7 @@ const createApp = async () => {
|
||||
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
|
||||
app.use(express.json());
|
||||
|
||||
const version = await getAppVersion();
|
||||
app.get(`${baseUrl}/api/lst-config.js`, (_, res) => {
|
||||
res.type("application/javascript");
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
@@ -47,7 +49,9 @@ const createApp = async () => {
|
||||
appVersion: ${JSON.stringify(umamiConfig.appVersion ?? "dev")},
|
||||
umamiHost: ${JSON.stringify(umamiConfig.umamiHost ?? "")},
|
||||
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")},
|
||||
timezone: ${JSON.stringify(process.env.TIMEZONE ?? "America/Chicago")}
|
||||
timezone: ${JSON.stringify(process.env.TIMEZONE ?? "America/Chicago")},
|
||||
version: ${JSON.stringify(version.version)},
|
||||
lastBuildTIme: ${JSON.stringify(version.lastBuildTime)}
|
||||
};
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
@@ -19,12 +17,13 @@ import { prodStopSpec } from "../scaler/prodSqlStop.spec.js";
|
||||
import { prodRegisterSpec } from "../scaler/register.spec.js";
|
||||
// all the specs
|
||||
import { statusSpec } from "../scaler/stats.spec.js";
|
||||
import { getAppVersion } from "../utils/version.utils.js";
|
||||
|
||||
export const openApiBase: OpenAPIV3_1.Document = {
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "LST API",
|
||||
version: "3.0.0",
|
||||
version: (await getAppVersion()).version ?? "",
|
||||
description: "Label System Tracking API",
|
||||
},
|
||||
servers: [
|
||||
@@ -125,8 +124,6 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
|
||||
...prodLoginSpec,
|
||||
...prodRegisterSpec,
|
||||
//...mergedDatamart,
|
||||
...cronerActiveJobs,
|
||||
...cronerStatusChange,
|
||||
...openDockApt,
|
||||
|
||||
// Add more specs here as you build features
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
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 scanUserSchema from "./schema/scanUsers.js";
|
||||
import * as settingsSchema from "./schema/settings.schema.js";
|
||||
@@ -23,5 +25,7 @@ export const db = drizzle(queryClient, {
|
||||
...scanUserSchema,
|
||||
...settingsSchema,
|
||||
...opendockAVCheck,
|
||||
...logs,
|
||||
...dockScans,
|
||||
},
|
||||
});
|
||||
|
||||
59
backend/db/db.listener.ts
Normal file
59
backend/db/db.listener.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import postgres from "postgres";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { handleDbNotification } from "./db.router.js";
|
||||
|
||||
const log = createLogger({
|
||||
module: "db",
|
||||
subModule: "notifications",
|
||||
});
|
||||
|
||||
const CHANNELS = [
|
||||
"logs_inserted",
|
||||
// "labels_inserted",
|
||||
// "dock_scans_inserted",
|
||||
] as const;
|
||||
|
||||
type DbNotificationChannel = (typeof CHANNELS)[number];
|
||||
|
||||
type DbNotificationPayload = {
|
||||
table: string;
|
||||
action: "INSERT" | "UPDATE" | "DELETE";
|
||||
id: string;
|
||||
};
|
||||
|
||||
export async function startDbNotificationListener() {
|
||||
const sql = postgres({
|
||||
host: `${process.env.DATABASE_HOST}`,
|
||||
port: Number(process.env.DATABASE_PORT),
|
||||
database: `${process.env.DATABASE_DB}`,
|
||||
username: process.env.DATABASE_USER,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
});
|
||||
|
||||
for (const channel of CHANNELS) {
|
||||
await sql.listen(channel, async (rawPayload) => {
|
||||
await processNotification(channel, rawPayload);
|
||||
});
|
||||
|
||||
log.info({ stack: { channel } }, `Listening for ${channel}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function processNotification(
|
||||
channel: DbNotificationChannel,
|
||||
rawPayload: string,
|
||||
) {
|
||||
try {
|
||||
const payload = JSON.parse(rawPayload) as DbNotificationPayload;
|
||||
|
||||
await handleDbNotification({
|
||||
channel,
|
||||
payload,
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(
|
||||
{ stack: { channel, rawPayload, e }, notify: true },
|
||||
"Failed processing DB notification",
|
||||
);
|
||||
}
|
||||
}
|
||||
41
backend/db/db.router.ts
Normal file
41
backend/db/db.router.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { handleDockScanInsertedNotification } from "../dockdoorScanning/dockdoor.socket.notifications.js";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { handleLogInsertedNotification } from "../logger/logger.socket.notifications.js";
|
||||
|
||||
const log = createLogger({
|
||||
module: "db",
|
||||
subModule: "notifications-router",
|
||||
});
|
||||
|
||||
type DbNotification = {
|
||||
channel: string;
|
||||
payload: {
|
||||
table: string;
|
||||
action: "INSERT" | "UPDATE" | "DELETE";
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function handleDbNotification(notification: DbNotification) {
|
||||
const { channel, payload } = notification;
|
||||
|
||||
switch (channel) {
|
||||
case "logs_inserted":
|
||||
await handleLogInsertedNotification(payload.id);
|
||||
return;
|
||||
|
||||
// case "labels_inserted":
|
||||
// await handleLabelInsertedNotification(payload.id);
|
||||
// return;
|
||||
|
||||
case "dock_scan_inserted":
|
||||
await handleDockScanInsertedNotification(payload.id);
|
||||
return;
|
||||
|
||||
default:
|
||||
log.warn(
|
||||
{ stack: notification },
|
||||
`Unhandled DB notification channel: ${channel}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
99
backend/db/db.setupNotifications.ts
Normal file
99
backend/db/db.setupNotifications.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
|
||||
const log = createLogger({
|
||||
module: "db",
|
||||
subModule: "notifications",
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates/updates Postgres notification functions + triggers.
|
||||
*
|
||||
* Safe to run on every app startup.
|
||||
* CREATE OR REPLACE updates the function.
|
||||
* DROP TRIGGER IF EXISTS prevents duplicate triggers.
|
||||
*/
|
||||
export async function setupDbNotifications() {
|
||||
log.info({}, "Setting up DB notifications");
|
||||
|
||||
await setupLogsNotifications();
|
||||
await setupDockScansNotifications();
|
||||
|
||||
log.info({}, "DB notifications setup complete");
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs notification setup.
|
||||
*
|
||||
* Flow:
|
||||
* 1. app inserts into logs table
|
||||
* 2. trigger runs after insert
|
||||
* 3. Postgres sends NOTIFY logs_inserted with the new log id
|
||||
* 4. Node listener receives id and fetches/emits full row
|
||||
*/
|
||||
async function setupLogsNotifications() {
|
||||
await db.execute(sql`
|
||||
CREATE OR REPLACE FUNCTION notify_logs_inserted()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify(
|
||||
'logs_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 logs_inserted_notify_trigger ON logs;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TRIGGER logs_inserted_notify_trigger
|
||||
AFTER INSERT ON logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_logs_inserted();
|
||||
`);
|
||||
|
||||
log.info({}, "Logs DB notification trigger ready");
|
||||
}
|
||||
|
||||
async function setupDockScansNotifications() {
|
||||
await db.execute(sql`
|
||||
CREATE OR REPLACE FUNCTION notify_dock_scan_inserted()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify(
|
||||
'dock_scan_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 dock_scan_inserted_notify_trigger ON dock_door_scans;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TRIGGER dock_scan_inserted_notify_trigger
|
||||
AFTER INSERT ON dock_door_scans
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_dock_scan_inserted();
|
||||
`);
|
||||
|
||||
log.info({}, "Dock scan DB notification trigger ready");
|
||||
}
|
||||
21
backend/db/db.socketSeed.ts
Normal file
21
backend/db/db.socketSeed.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { db } from "./db.controller.js";
|
||||
|
||||
export const getRecentLogs = ({
|
||||
module,
|
||||
submodule,
|
||||
limit = 200,
|
||||
}: {
|
||||
module?: string | undefined;
|
||||
submodule?: string | undefined;
|
||||
limit?: number | undefined;
|
||||
}) => {
|
||||
return db.query.logs.findMany({
|
||||
where: (logs, { and, eq }) =>
|
||||
and(
|
||||
module ? eq(logs.module, module) : undefined,
|
||||
submodule ? eq(logs.subModule, submodule) : undefined,
|
||||
),
|
||||
orderBy: (logs, { desc }) => [desc(logs.createdAt)],
|
||||
limit,
|
||||
});
|
||||
};
|
||||
22
backend/db/schema/forecastImports.schema.ts
Normal file
22
backend/db/schema/forecastImports.schema.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const forecastImport = pgTable("forecast_import", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
receivingPlantId: text("receiving_plant_id").notNull(),
|
||||
documentName: text("documentName"),
|
||||
sender: text("sender"),
|
||||
customerId: text("customer_id"),
|
||||
rawData: jsonb("raw_data").default([]),
|
||||
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
|
||||
add_user: text("add_user").default("lst-system"),
|
||||
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
|
||||
upd_user: text("upd_user").default("lst-system"),
|
||||
});
|
||||
|
||||
export const forecastImportSchema = createSelectSchema(forecastImport);
|
||||
export const newForecastImportSchema = createInsertSchema(forecastImport);
|
||||
|
||||
export type ForecastImport = z.infer<typeof forecastImportSchema>;
|
||||
export type NeworecastImport = z.infer<typeof newForecastImportSchema>;
|
||||
@@ -16,7 +16,58 @@ const endLoading = z.object({
|
||||
});
|
||||
|
||||
r.post("/", async (req, res) => {
|
||||
// TODO: setup the emitter to just emit the data when we post to the db
|
||||
if (req.body.clear) {
|
||||
// just clear the loading order and clear out all the pallets to keep it clean.
|
||||
await tryCatch(
|
||||
db
|
||||
.update(dockDoorScans)
|
||||
.set({
|
||||
status: "completed",
|
||||
upd_date: sql`NOW()`,
|
||||
upd_user: req.user?.username ?? "lst-dock-system",
|
||||
})
|
||||
.where(
|
||||
req.body.loadingOrder
|
||||
? eq(dockDoorScanners.currentLoadingOrder, req.body.loadingOrder)
|
||||
: undefined,
|
||||
)
|
||||
.returning(),
|
||||
);
|
||||
|
||||
const { data, error } = await tryCatch(
|
||||
db
|
||||
.update(dockDoorScanners)
|
||||
.set({
|
||||
currentLoadingOrder: "",
|
||||
upd_date: sql`NOW()`,
|
||||
upd_user: req.user?.username ?? "lst-dock-system",
|
||||
})
|
||||
.where(eq(dockDoorScanners.dockId, req.body.dockId))
|
||||
.returning(),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "dockdoor",
|
||||
subModule: "loadingOrder",
|
||||
message: `Failed to updating the dock.`,
|
||||
data: (error as any) ?? [],
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
return apiReturn(res, {
|
||||
success: true,
|
||||
level: "info",
|
||||
module: "dockdoor",
|
||||
subModule: "loadingOrder",
|
||||
message: `Loading order: ${req.body.loadingOrder} was just cleared out do to the process being completed in some other means. \nThis includes any scanned pallets as well.`,
|
||||
data: data ?? [],
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const validated = endLoading.parse(req.body);
|
||||
@@ -73,7 +124,9 @@ r.post("/", async (req, res) => {
|
||||
upd_date: sql`NOW()`,
|
||||
upd_user: req.user?.username ?? "lst-dock-system",
|
||||
})
|
||||
.where(eq(dockDoorScanners.currentLoadingOrder, validated.loadingOrder))
|
||||
.where(
|
||||
eq(dockDoorScans.loadingOrder, validated.loadingOrder.toString()),
|
||||
)
|
||||
.returning(),
|
||||
);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const postScan = async (data: any) => {
|
||||
loadingUnit: data.loadingUnit.sscc ?? data.loadingUnit.runningNo, // can be running number or sscc depending on where it came from
|
||||
loadingUnitStatus: data.loadingUnitStatus, // TODO: add enums on the status of each load.
|
||||
message: data.message, // the response it gave when scanning
|
||||
status: data.status ?? undefined,
|
||||
})
|
||||
.returning()) as any;
|
||||
|
||||
@@ -76,6 +77,7 @@ const loadUnit = async (data: Data) => {
|
||||
loadingUnitStatus: "notLoaded",
|
||||
message:
|
||||
"There are know current active loading orders please start one and try again.",
|
||||
status: "error",
|
||||
});
|
||||
return returnFunc({
|
||||
success: true,
|
||||
@@ -86,7 +88,6 @@ const loadUnit = async (data: Data) => {
|
||||
"There are know current active loading orders please start one and try again.",
|
||||
data: [],
|
||||
notify: false,
|
||||
room: `dockDoorLoading:${data.dockId}`,
|
||||
});
|
||||
}
|
||||
// check if its a valids an sscc
|
||||
@@ -99,6 +100,7 @@ const loadUnit = async (data: Data) => {
|
||||
loadingUnitStatus: "noread",
|
||||
message:
|
||||
"Failed to load the unit to the truck, there was no pallet read.",
|
||||
status: "error",
|
||||
});
|
||||
|
||||
return returnFunc({
|
||||
@@ -110,7 +112,6 @@ const loadUnit = async (data: Data) => {
|
||||
"Failed to load the unit to the truck, there was no pallet read.",
|
||||
data: [],
|
||||
notify: false,
|
||||
room: `dockDoorLoading:${data.dockId}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,6 +138,7 @@ const loadUnit = async (data: Data) => {
|
||||
loadingUnit: unitToScan,
|
||||
loadingUnitStatus: "notLoaded",
|
||||
message: prod?.data.errors[0].message,
|
||||
status: "error",
|
||||
});
|
||||
|
||||
return returnFunc({
|
||||
|
||||
17
backend/dockdoorScanning/dockdoor.socket.notifications.ts
Normal file
17
backend/dockdoorScanning/dockdoor.socket.notifications.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { logs } from "../db/schema/logs.schema.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
|
||||
export async function handleDockScanInsertedNotification(id: string) {
|
||||
const row = await db.query.dockDoorScans.findFirst({
|
||||
where: eq(logs.id, id),
|
||||
});
|
||||
|
||||
if (!row) return;
|
||||
|
||||
// send only to the current dock door
|
||||
if (row.dockId) {
|
||||
emitToRoom(`dockDoorLoading:${row.dockId}`, row);
|
||||
}
|
||||
}
|
||||
22
backend/dockdoorScanning/dockdoor.socket.seed.ts
Normal file
22
backend/dockdoorScanning/dockdoor.socket.seed.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { db } from "../db/db.controller.js";
|
||||
|
||||
export const getRecentDockScans = ({
|
||||
loadingOrder,
|
||||
limit = 200,
|
||||
}: {
|
||||
loadingOrder: string;
|
||||
limit?: number | undefined;
|
||||
}) => {
|
||||
return db.query.dockDoorScans.findMany({
|
||||
//where: (scans, { eq }) => eq(scans.status, "active"),
|
||||
where: (scans, { and, eq }) =>
|
||||
and(
|
||||
eq(scans.status, "active"),
|
||||
loadingOrder
|
||||
? eq(scans.loadingOrder, loadingOrder)
|
||||
: eq(scans.loadingOrder, "0"),
|
||||
),
|
||||
orderBy: (scans, { desc }) => [desc(scans.upd_date)],
|
||||
limit,
|
||||
});
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import { Writable } from "node:stream";
|
||||
import pino, { type Logger } from "pino";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { logs } from "../db/schema/logs.schema.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
import { tryCatch } from "../utils/trycatch.utils.js";
|
||||
import { notifySystemIssue } from "./logger.notify.js";
|
||||
//import build from "pino-abstract-transport";
|
||||
@@ -50,10 +49,10 @@ const dbStream = new Writable({
|
||||
notifySystemIssue(obj);
|
||||
}
|
||||
|
||||
if (obj.room) {
|
||||
emitToRoom(obj.room, res.data ? res.data[0] : obj);
|
||||
}
|
||||
emitToRoom("logs", res.data ? res.data[0] : obj);
|
||||
// if (obj.room) {
|
||||
// emitToRoom(obj.room, res.data ? res.data[0] : obj);
|
||||
// }
|
||||
// emitToRoom("logs", res.data ? res.data[0] : obj);
|
||||
callback();
|
||||
} catch (err) {
|
||||
console.error("DB log insert error:", err);
|
||||
|
||||
24
backend/logger/logger.socket.notifications.ts
Normal file
24
backend/logger/logger.socket.notifications.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { logs } from "../db/schema/logs.schema.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
|
||||
export async function handleLogInsertedNotification(id: string) {
|
||||
const row = await db.query.logs.findFirst({
|
||||
where: eq(logs.id, id),
|
||||
});
|
||||
|
||||
if (!row) return;
|
||||
|
||||
// More targeted rooms.
|
||||
if (row.module) {
|
||||
emitToRoom(`logs:${row.module}`, row);
|
||||
}
|
||||
|
||||
if (row.subModule) {
|
||||
emitToRoom(`logs:${row.subModule}`, row);
|
||||
}
|
||||
|
||||
// Everyone listening to all logs.
|
||||
emitToRoom("logs", row);
|
||||
}
|
||||
92
backend/logistics/logistics.dm.forecast.map.abbott.ts
Normal file
92
backend/logistics/logistics.dm.forecast.map.abbott.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
86
backend/logistics/logistics.dm.forecast.map.energizer.ts
Normal file
86
backend/logistics/logistics.dm.forecast.map.energizer.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
245
backend/logistics/logistics.dm.forecast.map.loreal.ts
Normal file
245
backend/logistics/logistics.dm.forecast.map.loreal.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
182
backend/logistics/logistics.dm.forecast.map.pNg.ts
Normal file
182
backend/logistics/logistics.dm.forecast.map.pNg.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
104
backend/logistics/logistics.dm.forecast.map.standard.ts
Normal file
104
backend/logistics/logistics.dm.forecast.map.standard.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import { excelDateStuff } from "../utils/excelToDate.utils.js";
|
||||
import { postData } from "./logistics.dm.postData.js";
|
||||
export const standardForecast = async (data: any, user: any) => {
|
||||
/**
|
||||
* Post a standard forecast based on the standard template.
|
||||
*/
|
||||
|
||||
const plantToken = process.env.PROD_PLANT_TOKEN;
|
||||
|
||||
//const arrayBuffer = await data.arrayBuffer();
|
||||
const buffer = Buffer.from(data.buffer);
|
||||
|
||||
const workbook = XLSX.read(buffer, { type: "buffer" });
|
||||
|
||||
const sheetName = workbook.SheetNames[0] as string;
|
||||
const sheet = workbook.Sheets[sheetName] as any;
|
||||
|
||||
const headers = [
|
||||
"CustomerArticleNumber",
|
||||
"Quantity",
|
||||
"RequirementDate",
|
||||
"CustomerID",
|
||||
];
|
||||
|
||||
const forecastData: any = XLSX.utils.sheet_to_json(sheet, {
|
||||
defval: "",
|
||||
header: headers,
|
||||
range: 1,
|
||||
});
|
||||
|
||||
const groupedByCustomer: any = forecastData.reduce((acc: any, item: any) => {
|
||||
const id = item.CustomerID;
|
||||
if (!acc[id]) {
|
||||
acc[id] = [];
|
||||
}
|
||||
acc[id].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const foreCastData: any = [];
|
||||
|
||||
for (const [customerID, forecast] of Object.entries(groupedByCustomer)) {
|
||||
//console.log(`Running for Customer ID: ${customerID}`);
|
||||
const newForecast: any = forecast;
|
||||
|
||||
const predefinedObject = {
|
||||
receivingPlantId: plantToken,
|
||||
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
|
||||
"en-US",
|
||||
)}`,
|
||||
sender: user.username || "lst-system",
|
||||
customerId: customerID,
|
||||
positions: [],
|
||||
};
|
||||
|
||||
// map everything out for each order
|
||||
const nForecast = newForecast.map((o: any) => {
|
||||
// const invoice = i.filter(
|
||||
// (i: any) => i.deliveryAddress === parseInt(customerID)
|
||||
// );
|
||||
// if (!invoice) {
|
||||
// return;
|
||||
// }
|
||||
return {
|
||||
customerArticleNo: o.CustomerArticleNumber,
|
||||
requirementDate: excelDateStuff(parseInt(o.RequirementDate)),
|
||||
quantity: o.Quantity,
|
||||
};
|
||||
});
|
||||
|
||||
// do that fun combining thing
|
||||
const updatedPredefinedObject = {
|
||||
...predefinedObject,
|
||||
positions: [...predefinedObject.positions, ...nForecast],
|
||||
};
|
||||
|
||||
//console.log(updatedPredefinedObject);
|
||||
|
||||
// post the orders to the server
|
||||
const posting: any = await postData(
|
||||
{
|
||||
type: "forecast",
|
||||
endpoint: "/public/v1.0/DemandManagement/DELFOR",
|
||||
data: updatedPredefinedObject as any,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
foreCastData.push({
|
||||
customer: customerID,
|
||||
//totalOrders: orders?.length(),
|
||||
success: posting.success,
|
||||
message: posting.message,
|
||||
data: posting.data,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: foreCastData[0].success,
|
||||
message: foreCastData[0].message,
|
||||
data: foreCastData,
|
||||
};
|
||||
};
|
||||
95
backend/logistics/logistics.dm.forecast.route.ts
Normal file
95
backend/logistics/logistics.dm.forecast.route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Router } from "express";
|
||||
import multer from "multer";
|
||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||
import { energizerForecast } from "./logistics.dm.forecast.map.energizer.js";
|
||||
import { lorealForecast } from "./logistics.dm.forecast.map.loreal.js";
|
||||
import { pNgForecast } from "./logistics.dm.forecast.map.pNg.js";
|
||||
import { standardForecast } from "./logistics.dm.forecast.map.standard.js";
|
||||
|
||||
type ForecastResult = {
|
||||
success?: boolean;
|
||||
message?: string;
|
||||
data?: unknown;
|
||||
};
|
||||
|
||||
const r = Router();
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
});
|
||||
|
||||
r.post("/", requireAuth, upload.single("file"), async (req, res) => {
|
||||
if (!req.file) {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "dm",
|
||||
subModule: "forecast",
|
||||
message: "A file must be added to be able to run the forecast.",
|
||||
data: [],
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const { fileType } = req.body;
|
||||
|
||||
if (typeof fileType !== "string") {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "dm",
|
||||
subModule: "forecast",
|
||||
message: "A fileType must be provided.",
|
||||
data: [],
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
//console.log("fileType:", req.body.fileType);
|
||||
|
||||
let result: ForecastResult;
|
||||
|
||||
switch (fileType) {
|
||||
case "standard":
|
||||
result = await standardForecast(req.file, req.user);
|
||||
break;
|
||||
|
||||
case "loreal":
|
||||
result = await lorealForecast(req.file, req.user);
|
||||
break;
|
||||
|
||||
case "pg":
|
||||
result = await pNgForecast(req.file, req.user);
|
||||
break;
|
||||
|
||||
case "energizer":
|
||||
result = await energizerForecast(req.file, req.user);
|
||||
break;
|
||||
|
||||
default:
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "dm",
|
||||
subModule: "forecast",
|
||||
message: `Invalid fileType: ${fileType}`,
|
||||
data: [],
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
return apiReturn(res, {
|
||||
success: result.success ?? false,
|
||||
level: result.success ? "info" : "error",
|
||||
module: "dm",
|
||||
subModule: "forecast",
|
||||
message: result.success
|
||||
? "The forecast was accepted by Alplaprod 2.0 please check to make sure everything processed properly."
|
||||
: (result.message as string),
|
||||
data: result.data ?? ([] as any),
|
||||
status: result.success ? 200 : 500,
|
||||
});
|
||||
});
|
||||
|
||||
export default r;
|
||||
65
backend/logistics/logistics.dm.postData.ts
Normal file
65
backend/logistics/logistics.dm.postData.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
57
backend/logistics/logistics.dm.repost.route.ts
Normal file
57
backend/logistics/logistics.dm.repost.route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||
import { postData } from "./logistics.dm.postData.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
r.post("/", requireAuth, async (req, res) => {
|
||||
let posting: any;
|
||||
|
||||
|
||||
if (req.body.type !== "forecast" || req.body.type !== "orders") {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "dm",
|
||||
subModule: "repost",
|
||||
message: "You must pass over a proper type.",
|
||||
data: [],
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.type === "forecast") {
|
||||
posting = await postData(
|
||||
{
|
||||
type: "forecast",
|
||||
endpoint: "/public/v1.0/DemandManagement/DELFOR",
|
||||
data: req.body.data as any,
|
||||
},
|
||||
req.user,
|
||||
);
|
||||
}
|
||||
|
||||
if (req.body.type === "orders") {
|
||||
posting = await postData(
|
||||
{
|
||||
type: "orders",
|
||||
endpoint: "/public/v1.0/DemandManagement/ORDERS",
|
||||
data: req.body.data as any,
|
||||
},
|
||||
req.user,
|
||||
);
|
||||
}
|
||||
|
||||
return apiReturn(res, {
|
||||
success: posting.success,
|
||||
level: posting.success ? "info" : "error",
|
||||
module: "dm",
|
||||
subModule: "repost",
|
||||
message: posting.message,
|
||||
data: [],
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
|
||||
export default r;
|
||||
79
backend/logistics/logistics.dm.template.route.ts
Normal file
79
backend/logistics/logistics.dm.template.route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { format } from "date-fns";
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||
import { excelTemplate, type Template } from "../utils/excelTemplates.utils.js";
|
||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||
import { tryCatch } from "../utils/trycatch.utils.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
r.get("/", requireAuth, async (req, res) => {
|
||||
const { filename } = req.query;
|
||||
|
||||
const templateNames = ["orders", "forecast"];
|
||||
if (typeof filename !== "string" || !templateNames.includes(filename)) {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "dm",
|
||||
subModule: "template",
|
||||
message: "You are required to pass over the template name.",
|
||||
data: [],
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
||||
const name = `${filename}-Template-${format(
|
||||
new Date(Date.now()),
|
||||
"M-d-yyyy",
|
||||
)}.xlsx`;
|
||||
|
||||
const standardHeaders = [
|
||||
"CustomerArticleNumber",
|
||||
"CustomerOrderNumber",
|
||||
"CustomerLineNumber",
|
||||
"CustomerRealeaseNumber",
|
||||
"Quantity",
|
||||
"DeliveryDate",
|
||||
"CustomerID",
|
||||
"Remark",
|
||||
// "InvoiceID",
|
||||
];
|
||||
|
||||
const forecastHeaders = [
|
||||
"CustomerArticleNumber",
|
||||
"Quantity",
|
||||
"RequirementDate",
|
||||
"CustomerID",
|
||||
];
|
||||
|
||||
const template = {
|
||||
name,
|
||||
headers: filename === "orders" ? standardHeaders : forecastHeaders,
|
||||
} as Template;
|
||||
|
||||
//console.log(template);
|
||||
const { data, error } = await tryCatch(excelTemplate(template));
|
||||
|
||||
if (error || !data) {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "dm",
|
||||
subModule: "template",
|
||||
message: "There was an error creating the Excel template.",
|
||||
data: [],
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
||||
res.set({
|
||||
"Content-Type":
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"Content-Disposition": `attachment; filename="${name}"`,
|
||||
});
|
||||
|
||||
return res.status(200).send(data);
|
||||
});
|
||||
|
||||
export default r;
|
||||
21
backend/logistics/logistics.routes.ts
Normal file
21
backend/logistics/logistics.routes.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Express } from "express";
|
||||
import { featureCheck } from "../middleware/featureActive.middleware.js";
|
||||
import forecast from "./logistics.dm.forecast.route.js";
|
||||
import createTemplate from "./logistics.dm.template.route.js";
|
||||
|
||||
export const setupLogisticsRoutes = (baseUrl: string, app: Express) => {
|
||||
//stats will be like this as we dont need to change this
|
||||
|
||||
app.use(
|
||||
`${baseUrl}/api/logistics/dm/template`,
|
||||
featureCheck("demandManagement"),
|
||||
createTemplate,
|
||||
);
|
||||
app.use(
|
||||
`${baseUrl}/api/logistics/dm/forecast`,
|
||||
featureCheck("demandManagement"),
|
||||
forecast,
|
||||
);
|
||||
|
||||
// all other system should be under /api/system/*
|
||||
};
|
||||
@@ -12,7 +12,7 @@ export let odToken: ODToken = {
|
||||
};
|
||||
|
||||
export const getToken = async () => {
|
||||
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
|
||||
const log = createLogger({ module: "opendock", subModule: "auth" });
|
||||
try {
|
||||
const { status, data } = await axios.post(
|
||||
`${process.env.OPENDOCK_URL}/auth/login`,
|
||||
@@ -29,7 +29,8 @@ export const getToken = async () => {
|
||||
|
||||
odToken = { odToken: data.access_token, tokenDate: new Date() };
|
||||
log.info({ odToken }, "Token added");
|
||||
return;
|
||||
} catch (e) {
|
||||
log.error({ error: e }, "Error getting/refreshing token");
|
||||
log.error({ stack: e }, "Error getting/refreshing token");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,74 +1,73 @@
|
||||
use [test1_AlplaPROD2.0_Read]
|
||||
|
||||
DECLARE @StartDate DATE = '[startDate]' -- 2025-1-1
|
||||
DECLARE @EndDate DATE = '[endDate]' -- 2025-1-31
|
||||
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]
|
||||
,ea.JournalNummer as BOL_Number
|
||||
,[ReleaseConfirmationState]
|
||||
,[PlanningState]
|
||||
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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
|
||||
import { setupDockDoorRoutes } from "./dockdoorScanning/dockdoor.routes.js";
|
||||
import { setupEomRoutes } from "./eom/eom.routes.js";
|
||||
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
|
||||
import { setupLogisticsRoutes } from "./logistics/logistics.routes.js";
|
||||
import { setupMobileRoutes } from "./mobile/mobile.routes.js";
|
||||
import { setupNotificationRoutes } from "./notification/notification.routes.js";
|
||||
import { setupOCPRoutes } from "./ocp/ocp.routes.js";
|
||||
@@ -18,13 +19,13 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
|
||||
|
||||
export const setupRoutes = (baseUrl: string, app: Express) => {
|
||||
//routes that are on by default
|
||||
setupDatamartRoutes(baseUrl, app);
|
||||
setupMobileRoutes(baseUrl, app);
|
||||
setupSystemRoutes(baseUrl, app);
|
||||
setupAdminRoutes(baseUrl, app);
|
||||
setupApiDocsRoutes(baseUrl, app);
|
||||
setupProdSqlRoutes(baseUrl, app);
|
||||
setupGPSqlRoutes(baseUrl, app);
|
||||
setupDatamartRoutes(baseUrl, app);
|
||||
setupAuthRoutes(baseUrl, app);
|
||||
setupUtilsRoutes(baseUrl, app);
|
||||
setupOpendockRoutes(baseUrl, app);
|
||||
@@ -33,4 +34,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
|
||||
setupTCPRoutes(baseUrl, app);
|
||||
setupDockDoorRoutes(baseUrl, app);
|
||||
setupEomRoutes(baseUrl, app);
|
||||
setupLogisticsRoutes(baseUrl, app);
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,15 @@
|
||||
import { createServer } from "node:http";
|
||||
import os from "node:os";
|
||||
|
||||
import createApp from "./app.js";
|
||||
import { db } from "./db/db.controller.js";
|
||||
import { startDbNotificationListener } from "./db/db.listener.js";
|
||||
import { setupDbNotifications } from "./db/db.setupNotifications.js";
|
||||
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";
|
||||
@@ -30,6 +33,7 @@ import { ppooMonitoring } from "./warehousing/warehousing.ppooMonitor.js";
|
||||
|
||||
const port = Number(process.env.PORT) || 3000;
|
||||
export let systemSettings: Setting[] = [];
|
||||
|
||||
const start = async () => {
|
||||
const { app, baseUrl } = await createApp();
|
||||
|
||||
@@ -43,12 +47,13 @@ const start = async () => {
|
||||
startTCPServer();
|
||||
connectProdSql();
|
||||
connectGPSql();
|
||||
startDbNotificationListener();
|
||||
|
||||
// trigger startup processes these must run before anything else can run
|
||||
await baseSettingValidationCheck();
|
||||
systemSettings = await db.select().from(settings);
|
||||
|
||||
//when starting up long lived features the name must match the setting name.
|
||||
// when starting up long lived features the name must match the setting name.
|
||||
// also we always want to have long lived processes inside a setting check.
|
||||
setTimeout(() => {
|
||||
if (systemSettings.filter((n) => n.name === "opendock_sync")[0]?.active) {
|
||||
@@ -89,6 +94,7 @@ const start = async () => {
|
||||
startNotifications();
|
||||
serversChecks();
|
||||
aggregateRouteHitsForBusinessDay();
|
||||
setupDbNotifications();
|
||||
|
||||
// can be removed at a later date
|
||||
sqlJobCleanUp();
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { RoomId } from "./roomDefinitions.socket.js";
|
||||
|
||||
export const MAX_HISTORY = 50;
|
||||
export const FLUSH_INTERVAL = 100; // 50ms change higher if needed
|
||||
|
||||
export const roomHistory = new Map<RoomId, unknown[]>();
|
||||
export const roomBuffers = new Map<RoomId, any[]>();
|
||||
export const roomFlushTimers = new Map<RoomId, NodeJS.Timeout>();
|
||||
@@ -1,122 +0,0 @@
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { dockDoorScans } from "../db/schema/dockdoor.scans.schema.js";
|
||||
import { logs } from "../db/schema/logs.schema.js";
|
||||
import { ppoRun } from "../warehousing/warehousing.ppooMonitor.js";
|
||||
|
||||
type RoomDefinition<T = unknown> = {
|
||||
seed: (limit: number) => Promise<T[]>;
|
||||
};
|
||||
|
||||
export type StaticRoomId =
|
||||
| "logs"
|
||||
| "labels"
|
||||
| "admin"
|
||||
| "admin:build"
|
||||
| "ppoo"
|
||||
| "dockDoorLoading:2";
|
||||
export type DynamicRoomId = `dockDoorLoading:${string}`;
|
||||
export type RoomId = StaticRoomId | DynamicRoomId;
|
||||
|
||||
export type RoomConfig = {
|
||||
requiresAuth?: boolean;
|
||||
role?: string[];
|
||||
seed?: (limit: number, roomId: RoomId) => Promise<unknown[]>;
|
||||
};
|
||||
|
||||
export const protectedRooms: Record<StaticRoomId, RoomConfig> = {
|
||||
logs: { requiresAuth: true, role: ["admin", "systemAdmin"] },
|
||||
//admin: { requiresAuth: false, role: ["admin", "systemAdmin"] },
|
||||
labels: {},
|
||||
admin: {},
|
||||
"admin:build": {},
|
||||
ppoo: {},
|
||||
"dockDoorLoading:2": {},
|
||||
};
|
||||
|
||||
export function getRoomConfig(roomId: string): RoomConfig | null {
|
||||
if (roomId in protectedRooms) {
|
||||
return protectedRooms[roomId as StaticRoomId];
|
||||
}
|
||||
|
||||
if (roomId.startsWith("dockDoorLoading:")) {
|
||||
const dockId = roomId.split(":")[1];
|
||||
|
||||
if (!dockId) return null;
|
||||
|
||||
return {
|
||||
requiresAuth: true,
|
||||
role: ["admin", "systemAdmin", "dockDoor"],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const roomDefinition: Record<RoomId, RoomDefinition> = {
|
||||
logs: {
|
||||
seed: async (limit) => {
|
||||
try {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(logs)
|
||||
.orderBy(desc(logs.createdAt))
|
||||
.limit(limit);
|
||||
|
||||
return rows; //.reverse();
|
||||
} catch (e) {
|
||||
console.error("Failed to seed logs:", e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
labels: {
|
||||
seed: async (limit) => {
|
||||
console.info(limit);
|
||||
return [];
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
seed: async (limit) => {
|
||||
console.info(limit);
|
||||
return [];
|
||||
},
|
||||
},
|
||||
"admin:build": {
|
||||
seed: async (limit) => {
|
||||
console.info(limit);
|
||||
return [];
|
||||
},
|
||||
},
|
||||
ppoo: {
|
||||
seed: async (limit) => {
|
||||
console.log(limit);
|
||||
return {
|
||||
type: "snapshot",
|
||||
items: await ppoRun(),
|
||||
createdAt: new Date().toISOString(),
|
||||
} as any;
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: add in dynamic room seeding
|
||||
"dockDoorLoading:2": {
|
||||
seed: async (limit) => {
|
||||
console.log(limit);
|
||||
try {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(dockDoorScans)
|
||||
.where(eq(dockDoorScans.status, "active"))
|
||||
.orderBy(desc(dockDoorScans.upd_date))
|
||||
.limit(limit);
|
||||
|
||||
return rows; //.reverse();
|
||||
} catch (e) {
|
||||
console.error("Failed to seed logs:", e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,27 +1,73 @@
|
||||
// the emitter setup
|
||||
// TODO: validate if we want to add event back in later..
|
||||
// let emitFn: ((roomId: string, event: string, payload: unknown) => void) | null =
|
||||
// null;
|
||||
|
||||
import type { RoomId } from "./roomDefinitions.socket.js";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
|
||||
let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null;
|
||||
type QueuedPayload = unknown;
|
||||
|
||||
let emitFn: ((roomId: string, payload: QueuedPayload[]) => void) | null = null;
|
||||
|
||||
const queues = new Map<string, QueuedPayload[]>();
|
||||
const timers = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
const FLUSH_MS = 500;
|
||||
const MAX_QUEUE_SIZE = 200;
|
||||
|
||||
export const registerEmitter = (
|
||||
fn: (roomId: RoomId, payload: unknown[]) => void,
|
||||
fn: (roomId: string, payload: QueuedPayload) => void,
|
||||
) => {
|
||||
addDataToRoom = fn;
|
||||
emitFn = fn;
|
||||
};
|
||||
|
||||
export const emitToRoom = (roomId: RoomId, payload: unknown[]) => {
|
||||
if (!addDataToRoom) {
|
||||
export const emitToRoom = (roomId: string, payload: QueuedPayload) => {
|
||||
const log = createLogger({ module: "socket.io", subModule: "emitter" });
|
||||
if (!emitFn) {
|
||||
console.error("Socket emitter not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
addDataToRoom(roomId, payload);
|
||||
const queue = queues.get(roomId) ?? [];
|
||||
|
||||
if (queue.length > MAX_QUEUE_SIZE) {
|
||||
log.error(
|
||||
{ stack: { roomId, size: queue.length }, notify: true },
|
||||
`Socket queue exceeded max size for ${roomId}`,
|
||||
);
|
||||
}
|
||||
queue.push(payload);
|
||||
queues.set(roomId, queue);
|
||||
|
||||
if (timers.has(roomId)) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
const payloads = queues.get(roomId) ?? [];
|
||||
|
||||
if (payloads.length === 0) return;
|
||||
emitFn?.(roomId, payloads);
|
||||
|
||||
queues.delete(roomId);
|
||||
} catch (e) {
|
||||
console.error("Socket emit failed", { roomId, e });
|
||||
} finally {
|
||||
timers.delete(roomId);
|
||||
}
|
||||
}, FLUSH_MS);
|
||||
|
||||
timers.set(roomId, timer);
|
||||
};
|
||||
|
||||
/*
|
||||
import { emitToRoom } from "../socket/socketEmitter.js";
|
||||
// room name
|
||||
// its payload
|
||||
emitToRoom("logs", newLogRow);
|
||||
example emitToRoom(room, payload)
|
||||
|
||||
payload can be anything json serilized example below.
|
||||
|
||||
emitToRoom("inventory:ppoo", {
|
||||
type: "snapshot",
|
||||
location: "ppoo",
|
||||
items,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
*/
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import type { Server } from "socket.io";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import {
|
||||
FLUSH_INTERVAL,
|
||||
MAX_HISTORY,
|
||||
roomBuffers,
|
||||
roomFlushTimers,
|
||||
roomHistory,
|
||||
} from "./roomCache.socket.js";
|
||||
import { type RoomId, roomDefinition } from "./roomDefinitions.socket.js";
|
||||
|
||||
// get the db data if not exiting already
|
||||
const log = createLogger({ module: "socket.io", subModule: "roomService" });
|
||||
let ioRef: Server | null = null;
|
||||
|
||||
export const registerRoomService = (io: Server) => {
|
||||
ioRef = io;
|
||||
};
|
||||
|
||||
export const hasRoomMembers = (roomId: string): boolean => {
|
||||
if (!ioRef) return false;
|
||||
|
||||
return (ioRef.sockets.adapter.rooms.get(roomId)?.size ?? 0) > 0;
|
||||
};
|
||||
|
||||
export const getRoomMemberCount = (roomId: string): number => {
|
||||
if (!ioRef) return 0;
|
||||
|
||||
return ioRef.sockets.adapter.rooms.get(roomId)?.size ?? 0;
|
||||
};
|
||||
export const preseedRoom = async (roomId: RoomId) => {
|
||||
if (roomHistory.has(roomId)) {
|
||||
if (!roomId.includes("dock")) {
|
||||
return roomHistory.get(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
const roomDef = roomDefinition[roomId] as any;
|
||||
|
||||
if (!roomDef) {
|
||||
log.error({}, `Room ${roomId} is not defined`);
|
||||
}
|
||||
|
||||
const latestData = await roomDef.seed(MAX_HISTORY);
|
||||
|
||||
roomHistory.set(roomId, latestData);
|
||||
|
||||
return latestData;
|
||||
};
|
||||
|
||||
export const createRoomEmitter = (io: Server) => {
|
||||
const addDataToRoom = <T>(roomId: RoomId, payload: T[]) => {
|
||||
if (!roomHistory.has(roomId)) {
|
||||
roomHistory.set(roomId, []);
|
||||
}
|
||||
|
||||
const history = roomHistory.get(roomId)!;
|
||||
history?.push(payload);
|
||||
|
||||
if (history?.length > MAX_HISTORY) {
|
||||
history?.shift();
|
||||
}
|
||||
|
||||
if (!roomBuffers.has(roomId)) {
|
||||
roomBuffers.set(roomId, []);
|
||||
}
|
||||
|
||||
roomBuffers.get(roomId)!.push(payload);
|
||||
|
||||
if (!roomFlushTimers.has(roomId)) {
|
||||
const timer = setTimeout(() => {
|
||||
const buffered = roomBuffers.get(roomId) || [];
|
||||
|
||||
if (buffered.length > 0) {
|
||||
io.to(roomId).emit("room-update", {
|
||||
roomId,
|
||||
payloads: buffered, // ✅ array now
|
||||
});
|
||||
}
|
||||
|
||||
roomBuffers.set(roomId, []);
|
||||
roomFlushTimers.delete(roomId);
|
||||
}, FLUSH_INTERVAL);
|
||||
|
||||
roomFlushTimers.set(roomId, timer);
|
||||
}
|
||||
};
|
||||
|
||||
return { addDataToRoom };
|
||||
};
|
||||
@@ -1,33 +1,16 @@
|
||||
import type { Server as HttpServer } from "node:http";
|
||||
//import { dirname, join } from "node:path";
|
||||
//import { fileURLToPath } from "node:url";
|
||||
import { instrument } from "@socket.io/admin-ui";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { auth } from "../utils/auth.utils.js";
|
||||
import { allowedOrigins } from "../utils/cors.utils.js";
|
||||
import { registerEmitter } from "./roomEmitter.socket.js";
|
||||
import {
|
||||
createRoomEmitter,
|
||||
preseedRoom,
|
||||
registerRoomService,
|
||||
} from "./roomService.socket.js";
|
||||
import { registerHasRoomMembers } from "./socket.manager.js";
|
||||
import { isRoomKey, roomConfigs } from "./socket.roomConfig.js";
|
||||
|
||||
//const __filename = fileURLToPath(import.meta.url);
|
||||
//const __dirname = dirname(__filename);
|
||||
const log = createLogger({ module: "socket.io", subModule: "setup" });
|
||||
|
||||
import { auth } from "../utils/auth.utils.js";
|
||||
//import type { Session, User } from "better-auth"; // adjust if needed
|
||||
import { getRoomConfig } from "./roomDefinitions.socket.js";
|
||||
|
||||
// declare module "socket.io" {
|
||||
// interface Socket {
|
||||
// user?: User | any;
|
||||
// session?: Session;
|
||||
// }
|
||||
// }
|
||||
|
||||
export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
|
||||
const io = new Server(server, {
|
||||
path: `${baseUrl}/api/socket.io`,
|
||||
@@ -37,12 +20,16 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
|
||||
},
|
||||
});
|
||||
|
||||
// manage members of the rooms.
|
||||
registerRoomService(io);
|
||||
registerHasRoomMembers((roomId) => {
|
||||
return (io.sockets.adapter.rooms.get(roomId)?.size ?? 0) > 0;
|
||||
});
|
||||
|
||||
// ✅ Create emitter instance
|
||||
const { addDataToRoom } = createRoomEmitter(io);
|
||||
registerEmitter(addDataToRoom);
|
||||
registerEmitter((roomId, payloads) => {
|
||||
io.to(roomId).emit("room-update", {
|
||||
roomId,
|
||||
payloads,
|
||||
});
|
||||
});
|
||||
|
||||
io.use(async (socket, next) => {
|
||||
try {
|
||||
@@ -85,79 +72,95 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// s.on("join-room", async (rn) => {
|
||||
// const config = protectedRooms[rn];
|
||||
s.on("join-room", async ({ room, params }) => {
|
||||
if (!isRoomKey(room)) return;
|
||||
|
||||
// if (config?.requiresAuth && !s.user) {
|
||||
// return s.emit("room-error", {
|
||||
// room: rn,
|
||||
// message: "Authentication required",
|
||||
// });
|
||||
// }
|
||||
|
||||
// const roles = Array.isArray(config?.role) ? config?.role : [config?.role];
|
||||
|
||||
// //if (config?.role && s.user?.role !== config.role) {
|
||||
// if (config?.role && !roles.includes(s.user?.role)) {
|
||||
// return s.emit("room-error", {
|
||||
// roomId: rn,
|
||||
// message: `Not authorized to be in room: ${rn}`,
|
||||
// });
|
||||
// }
|
||||
// s.join(rn);
|
||||
|
||||
// // get room seeded
|
||||
// const history = await preseedRoom(rn);
|
||||
// log.info({}, `User joined ${rn}: ${s.id}`);
|
||||
// // send the intial data
|
||||
// s.emit("room-update", {
|
||||
// roomId: rn,
|
||||
// payloads: history,
|
||||
// initial: true,
|
||||
// });
|
||||
// });
|
||||
|
||||
s.on("join-room", async (rn: string) => {
|
||||
const config = getRoomConfig(rn);
|
||||
const config = roomConfigs[room];
|
||||
|
||||
if (!config) {
|
||||
return s.emit("room-error", {
|
||||
roomId: rn,
|
||||
message: `Unknown room: ${rn}`,
|
||||
roomId: room,
|
||||
message: `Unknown room: ${room}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (config.requiresAuth && !s.user) {
|
||||
const actualRoom = config.buildRoom
|
||||
? config.buildRoom(params)
|
||||
: (room as any);
|
||||
|
||||
const allowed = config.canJoin
|
||||
? await config.canJoin({
|
||||
socket: s,
|
||||
user: s.user,
|
||||
room,
|
||||
actualRoom,
|
||||
params,
|
||||
})
|
||||
: true;
|
||||
|
||||
if (!allowed) {
|
||||
return s.emit("room-error", {
|
||||
roomId: rn,
|
||||
message: "Authentication required",
|
||||
roomId: room,
|
||||
message: `Not authorized to be in room: ${room}`,
|
||||
});
|
||||
}
|
||||
|
||||
const roles = Array.isArray(config.role) ? config.role : [];
|
||||
await s.join(actualRoom);
|
||||
|
||||
if (roles.length > 0 && !roles.includes(s.user?.role)) {
|
||||
return s.emit("room-error", {
|
||||
roomId: rn,
|
||||
message: `Not authorized to be in room: ${rn}`,
|
||||
});
|
||||
}
|
||||
|
||||
s.join(rn);
|
||||
|
||||
const history = await preseedRoom(rn as any);
|
||||
|
||||
log.info({}, `User joined ${rn}: ${s.id}`);
|
||||
|
||||
s.emit("room-update", {
|
||||
roomId: rn,
|
||||
payloads: history,
|
||||
initial: true,
|
||||
s.emit("room-joined", {
|
||||
room,
|
||||
roomId: actualRoom,
|
||||
params,
|
||||
});
|
||||
|
||||
if (config.seed) {
|
||||
const payloads = await config.seed({
|
||||
room,
|
||||
actualRoom,
|
||||
params,
|
||||
user: s.user,
|
||||
});
|
||||
|
||||
s.emit("room-update", {
|
||||
room,
|
||||
roomId: actualRoom,
|
||||
type: "snapshot",
|
||||
payloads,
|
||||
});
|
||||
}
|
||||
|
||||
log.info(
|
||||
{ room, actualRoom, params },
|
||||
`User joined ${actualRoom}: ${s.id}`,
|
||||
);
|
||||
});
|
||||
s.on("leave-room", (room) => {
|
||||
s.leave(room);
|
||||
log.info({}, `${s.id} left room: ${room}`);
|
||||
// s.on("leave-room", (room) => {
|
||||
// s.leave(room);
|
||||
// log.info({}, `${s.id} left room: ${JSON.stringify(room)}`);
|
||||
// });
|
||||
s.on("leave-room", async ({ room, params }) => {
|
||||
if (!isRoomKey(room)) return;
|
||||
|
||||
const config = roomConfigs[room];
|
||||
|
||||
if (!config) return;
|
||||
|
||||
const actualRoom = config.buildRoom
|
||||
? config.buildRoom(params)
|
||||
: (room as any);
|
||||
|
||||
await s.leave(actualRoom);
|
||||
|
||||
s.emit("room-left", {
|
||||
room,
|
||||
roomId: actualRoom,
|
||||
params,
|
||||
});
|
||||
|
||||
log.info(
|
||||
{ room, actualRoom, params },
|
||||
`${s.id} left room: ${actualRoom}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
10
backend/socket.io/socket.manager.ts
Normal file
10
backend/socket.io/socket.manager.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
let hasMembersFn: ((roomId: string) => boolean) | null = null;
|
||||
|
||||
export const registerHasRoomMembers = (fn: (roomId: string) => boolean) => {
|
||||
hasMembersFn = fn;
|
||||
};
|
||||
|
||||
export const hasRoomMembers = (roomId: string) => {
|
||||
if (!hasMembersFn) return false;
|
||||
return hasMembersFn(roomId);
|
||||
};
|
||||
117
backend/socket.io/socket.roomConfig.ts
Normal file
117
backend/socket.io/socket.roomConfig.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { getRecentLogs } from "../db/db.socketSeed.js";
|
||||
import { getRecentDockScans } from "../dockdoorScanning/dockdoor.socket.seed.js";
|
||||
|
||||
export type RoomKey =
|
||||
| "logs"
|
||||
| "labels"
|
||||
| "admin"
|
||||
| "inventory"
|
||||
| "dockDoorLoading";
|
||||
|
||||
export type SocketUser = {
|
||||
id: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
};
|
||||
|
||||
export type CanJoinArgs = {
|
||||
socket: any;
|
||||
user?: SocketUser;
|
||||
room: string;
|
||||
actualRoom: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type RoomConfig = {
|
||||
//requiresAuth?: boolean;
|
||||
//roles?: string[];
|
||||
canJoin?: (args: CanJoinArgs) => boolean | Promise<boolean>;
|
||||
buildRoom?: (params?: Record<string, unknown>) => string | null;
|
||||
seed?: (args: {
|
||||
room: string;
|
||||
actualRoom: string;
|
||||
params?: Record<string, unknown>;
|
||||
user?: SocketUser;
|
||||
}) => Promise<unknown[]>;
|
||||
};
|
||||
|
||||
export function isRoomKey(room: string): room is RoomKey {
|
||||
return room in roomConfigs;
|
||||
}
|
||||
|
||||
export const roomConfigs: Record<RoomKey, RoomConfig> = {
|
||||
logs: {
|
||||
canJoin: ({ user, params }) => {
|
||||
if (!params?.submodule && !params?.module) {
|
||||
return user?.role === "systemAdmin";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
buildRoom: (params) => {
|
||||
const module = String(params?.module ?? "").toLowerCase();
|
||||
const submodule = String(params?.submodule ?? "").toLowerCase();
|
||||
|
||||
if (module && submodule) return `logs:${module}:${submodule}`;
|
||||
if (submodule) return `logs:${submodule}`;
|
||||
if (module) return `logs:${module}`;
|
||||
|
||||
return "logs";
|
||||
},
|
||||
seed: async ({ params }) => {
|
||||
const module = params?.module ? String(params.module) : undefined;
|
||||
const submodule = params?.submodule
|
||||
? String(params.submodule)
|
||||
: undefined;
|
||||
|
||||
return await getRecentLogs({
|
||||
module,
|
||||
submodule,
|
||||
limit: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
labels: {
|
||||
canJoin: () => true,
|
||||
buildRoom: () => "labels",
|
||||
},
|
||||
|
||||
admin: {
|
||||
canJoin: ({ user, params }) => {
|
||||
if (params?.section === "system") {
|
||||
return user?.role === "systemAdmin";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
buildRoom: (params) =>
|
||||
params?.section ? `admin:${params.section}` : "admin",
|
||||
},
|
||||
inventory: {
|
||||
canJoin: () => true,
|
||||
buildRoom: (params) =>
|
||||
params?.location ? `inventory:${params.location}` : null,
|
||||
},
|
||||
|
||||
dockDoorLoading: {
|
||||
canJoin: () => true,
|
||||
buildRoom: (params) =>
|
||||
params?.dockId ? `dockDoorLoading:${params.dockId}` : null,
|
||||
seed: async ({ params }) => {
|
||||
return await getRecentDockScans({
|
||||
loadingOrder: params?.loadingOrder as string,
|
||||
limit: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, RoomConfig>;
|
||||
|
||||
/*
|
||||
|
||||
socket.emit("join-room", {
|
||||
room: "dockDoorLoading",
|
||||
params: { dockId: "2" },
|
||||
});
|
||||
|
||||
*/
|
||||
@@ -6,12 +6,15 @@ import {
|
||||
type SqlQuery,
|
||||
sqlQuerySelector,
|
||||
} from "../prodSql/prodSqlQuerySelector.utils.js";
|
||||
|
||||
import { isServerRunning } from "../tcpServer/tcp.server.js";
|
||||
import { getAppVersion } from "../utils/version.utils.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (_, res) => {
|
||||
const used = process.memoryUsage();
|
||||
const version = await getAppVersion();
|
||||
|
||||
const query = sqlQuerySelector("prodSqlStats") as SqlQuery;
|
||||
|
||||
@@ -20,6 +23,8 @@ router.get("/", async (_, res) => {
|
||||
status: "ok",
|
||||
uptime: process.uptime(),
|
||||
nodeVersion: process.version,
|
||||
appVersion: version.version ?? "",
|
||||
lastBuildDate: version.lastBuildTime ?? "",
|
||||
memoryUsage: `Heap: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB / RSS: ${(
|
||||
used.rss / 1024 / 1024
|
||||
).toFixed(2)} MB`,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
import { updateAppStats } from "./updateAppStats.utils.js";
|
||||
import { getAppVersion } from "./version.utils.js";
|
||||
import { zipBuild } from "./zipper.utils.js";
|
||||
|
||||
export const emitBuildLog = (message: string, level = "info") => {
|
||||
@@ -28,6 +29,8 @@ export let building = false;
|
||||
const log = createLogger({ module: "utils", subModule: "builds" });
|
||||
export const build = async () => {
|
||||
const appDir = process.env.DEV_DIR ?? "";
|
||||
const build = await getAppVersion();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
building = true;
|
||||
|
||||
@@ -72,6 +75,7 @@ export const build = async () => {
|
||||
updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: false,
|
||||
currentBuild: build.build,
|
||||
});
|
||||
emitBuildLog(`Build failed with code ${code}`, "error");
|
||||
//reject(new Error(`Build failed with code ${code}`));
|
||||
|
||||
49
backend/utils/excelTemplates.utils.ts
Normal file
49
backend/utils/excelTemplates.utils.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
export type Template = {
|
||||
name: string;
|
||||
headers: string[];
|
||||
};
|
||||
|
||||
export const excelTemplate = async (data: Template) => {
|
||||
/**
|
||||
* Creates the standard Template for bulk orders in
|
||||
*/
|
||||
|
||||
// const headers = [
|
||||
// [
|
||||
// "CustomerArticleNumber",
|
||||
// "CustomerOrderNumber",
|
||||
// "CustomerLineNumber",
|
||||
// "CustomerRealeaseNumber",
|
||||
// "Quantity",
|
||||
// "DeliveryDate",
|
||||
// "CustomerID",
|
||||
// "Remark",
|
||||
// // "InvoiceID",
|
||||
// ],
|
||||
// ];
|
||||
|
||||
// create a new workbook
|
||||
const wb = XLSX.utils.book_new();
|
||||
const ws = XLSX.utils.aoa_to_sheet([data.headers]);
|
||||
//const ws2 = XLSX.utils.aoa_to_sheet(headers2);
|
||||
|
||||
const columnWidths = data.headers.map((header) => ({
|
||||
width: header.length + 2,
|
||||
}));
|
||||
|
||||
ws["!cols"] = columnWidths;
|
||||
|
||||
// append the worksheet to the workbook
|
||||
XLSX.utils.book_append_sheet(wb, ws, `Sheet1`);
|
||||
//XLSX.utils.book_append_sheet(wb, ws2, `Sheet2`);
|
||||
|
||||
// Creates the file to disk'
|
||||
// XLSX.writeFile(wb, data.name);
|
||||
|
||||
// Write the workbook to a buffer and return it
|
||||
const excelBuffer = XLSX.write(wb, { bookType: "xlsx", type: "buffer" });
|
||||
|
||||
return excelBuffer;
|
||||
};
|
||||
28
backend/utils/excelToDate.utils.ts
Normal file
28
backend/utils/excelToDate.utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getJsDateFromExcel } from "excel-date-to-js";
|
||||
|
||||
export const excelDateStuff = (serial: number, time?: any) => {
|
||||
if (typeof serial !== "number" || serial <= 0) {
|
||||
return "invalid Date";
|
||||
}
|
||||
|
||||
// Default time to 8:00 AM if not provided
|
||||
if (!time) {
|
||||
time = 800;
|
||||
}
|
||||
|
||||
// Get base date from Excel serial (this gives you UTC midnight)
|
||||
const date = getJsDateFromExcel(serial);
|
||||
|
||||
const localOffset = new Date().getTimezoneOffset() / 60;
|
||||
const hours = Math.floor(time / 100);
|
||||
const minutes = time % 100;
|
||||
|
||||
// Set the time in UTC
|
||||
date.setUTCHours(hours + localOffset);
|
||||
date.setUTCMinutes(minutes);
|
||||
date.setUTCSeconds(0);
|
||||
date.setUTCMilliseconds(0);
|
||||
|
||||
//console.log(date.toISOString(), serial, time);
|
||||
return date.toISOString();
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import https from "node:https";
|
||||
import axios from "axios";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { returnFunc } from "./returnHelper.utils.js";
|
||||
import { tryCatch } from "./trycatch.utils.js";
|
||||
|
||||
@@ -59,9 +60,14 @@ export const prodEndpointCreation = async (endpoint: string) => {
|
||||
* @param timeoutDelay
|
||||
* @returns
|
||||
*/
|
||||
export const runProdApi = async (data: Data) => {
|
||||
export const runProdApi = async (data: Data, name?: string) => {
|
||||
const log = createLogger({ module: "utils", subModule: "prodEndpoints" });
|
||||
const url = await prodEndpointCreation(data.endpoint);
|
||||
|
||||
log.debug(
|
||||
{ stack: data },
|
||||
`Info passed over for ${name ? name : "Missing name"}`,
|
||||
);
|
||||
const { data: d, error } = await tryCatch(
|
||||
axios({
|
||||
method: data.method as string,
|
||||
@@ -94,7 +100,7 @@ export const runProdApi = async (data: Data) => {
|
||||
level: "error",
|
||||
module: "utils",
|
||||
subModule: "prodEndpoint",
|
||||
message: "Data from prod endpoint",
|
||||
message: "Error data from prod endpoint",
|
||||
data: d.data,
|
||||
notify: false,
|
||||
});
|
||||
@@ -104,10 +110,30 @@ export const runProdApi = async (data: Data) => {
|
||||
level: "error",
|
||||
module: "utils",
|
||||
subModule: "prodEndpoint",
|
||||
message: "Data from prod endpoint",
|
||||
message: "Error data from prod endpoint",
|
||||
data: d.data,
|
||||
notify: false,
|
||||
});
|
||||
case 500:
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "utils",
|
||||
subModule: "prodEndpoint",
|
||||
message: "Error data from prod endpoint",
|
||||
data: d.data,
|
||||
notify: false,
|
||||
});
|
||||
default:
|
||||
returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "utils",
|
||||
subModule: "prodEndpoint",
|
||||
message: "Unknown error encountered",
|
||||
data: d?.data as any,
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -18,7 +18,8 @@ export interface ReturnHelper<T = unknown[]> {
|
||||
| "admin"
|
||||
| "mobile"
|
||||
| "dockdoor"
|
||||
| "eom";
|
||||
| "eom"
|
||||
| "dm";
|
||||
subModule: string;
|
||||
|
||||
level: "info" | "error" | "debug" | "fatal" | "warn";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { appStats } from "../db/schema/stats.schema.js";
|
||||
export const updateAppStats = async (
|
||||
data: Partial<typeof appStats.$inferInsert>,
|
||||
) => {
|
||||
console.log(data);
|
||||
await db
|
||||
.insert(appStats)
|
||||
.values({
|
||||
|
||||
17
backend/utils/version.utils.ts
Normal file
17
backend/utils/version.utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import fsp from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const getAppVersion = async () => {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const version = path.join(__dirname, "../../package.json");
|
||||
const raw = await fsp.readFile(`${version}`, "utf8");
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
return {
|
||||
version: `${config.version}.${parseInt((config.build as string) ?? "1", 0) - 1}`,
|
||||
build: parseInt((config.build as string) ?? "1", 0),
|
||||
lastBuildTime: config.lastBuildDate,
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import fsp from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import archiver from "archiver";
|
||||
import { format } from "date-fns";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { emitBuildLog } from "./build.utils.js";
|
||||
import { updateAppStats } from "./updateAppStats.utils.js";
|
||||
@@ -17,33 +18,31 @@ const exists = async (target: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getNextBuildNumber = async (buildNumberFile: string) => {
|
||||
if (!(await exists(buildNumberFile))) {
|
||||
await fsp.writeFile(buildNumberFile, "1", "utf8");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const raw = await fsp.readFile(buildNumberFile, "utf8");
|
||||
const current = Number.parseInt(raw.trim(), 10);
|
||||
const getNextBuildNumber = async (versionPath: string) => {
|
||||
const raw = await fsp.readFile(versionPath, "utf8");
|
||||
const config = JSON.parse(raw);
|
||||
const current = Number.parseInt(config.build.trim(), 10);
|
||||
|
||||
let nextBuild: string;
|
||||
if (Number.isNaN(current) || current < 1) {
|
||||
await fsp.writeFile(buildNumberFile, "1", "utf8");
|
||||
return 1;
|
||||
nextBuild = "1";
|
||||
} else {
|
||||
nextBuild = String(current + 1); // Incrementing the build number
|
||||
}
|
||||
|
||||
const next = current + 1;
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
build: nextBuild,
|
||||
lastBuildDate: format(new Date(Date.now()), "M/d/yyyy HH:mm"),
|
||||
};
|
||||
|
||||
await fsp.writeFile(buildNumberFile, String(next), "utf8");
|
||||
await fsp.writeFile(
|
||||
versionPath,
|
||||
JSON.stringify(updatedConfig, null, 4) + "\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// update the server with the next build number
|
||||
|
||||
await updateAppStats({
|
||||
currentBuild: next,
|
||||
lastBuildAt: new Date(),
|
||||
building: true,
|
||||
});
|
||||
|
||||
return next;
|
||||
return { version: config.version, build: config.build };
|
||||
};
|
||||
|
||||
const cleanupOldBuilds = async (buildFolder: string, maxBuilds: number) => {
|
||||
@@ -53,7 +52,8 @@ const cleanupOldBuilds = async (buildFolder: string, maxBuilds: number) => {
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
if (!/^LSTV3-\d+\.zip$/i.test(entry.name)) continue;
|
||||
//if (!/^LSTV3-\d+\.zip$/i.test(entry.name)) continue;
|
||||
if (!entry.name.includes("LSTV3")) continue;
|
||||
|
||||
const fullPath = path.join(buildFolder, entry.name);
|
||||
const stat = await fsp.stat(fullPath);
|
||||
@@ -85,7 +85,7 @@ export const zipBuild = async () => {
|
||||
}
|
||||
|
||||
const includesFile = path.join(appDir, ".includes");
|
||||
const buildNumberFile = path.join(appDir, ".buildNumber");
|
||||
const version = path.join(appDir, "package.json");
|
||||
const buildFolder = path.join(appDir, "builds");
|
||||
const tempFolder = path.join(appDir, "temp", "zip-temp");
|
||||
if (!(await exists(includesFile))) {
|
||||
@@ -95,11 +95,13 @@ export const zipBuild = async () => {
|
||||
|
||||
await fsp.mkdir(buildFolder, { recursive: true });
|
||||
|
||||
const buildNumber = await getNextBuildNumber(buildNumberFile);
|
||||
const zipFileName = `LSTV3-${buildNumber}.zip`;
|
||||
const lstVersion = await getNextBuildNumber(version);
|
||||
const zipFileName = `LSTV3-${lstVersion.version}.${lstVersion.build}.zip`;
|
||||
const zipFile = path.join(buildFolder, zipFileName);
|
||||
// make the folders in case they are not created already
|
||||
emitBuildLog(`Using build number: ${buildNumber}`);
|
||||
emitBuildLog(
|
||||
`Using version, build number: ${lstVersion.version}.${lstVersion.build}`,
|
||||
);
|
||||
|
||||
if (await exists(tempFolder)) {
|
||||
await fsp.rm(tempFolder, { recursive: true, force: true });
|
||||
@@ -166,11 +168,12 @@ export const zipBuild = async () => {
|
||||
await updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: false,
|
||||
currentBuild: lstVersion.build,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
buildNumber,
|
||||
build: lstVersion.build,
|
||||
zipFile,
|
||||
zipFileName,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
import { hasRoomMembers } from "../socket.io/roomService.socket.js";
|
||||
import { hasRoomMembers } from "../socket.io/socket.manager.js";
|
||||
import { runProdApi } from "../utils/prodEndpoint.utils.js";
|
||||
|
||||
export const ppoRun = async () => {
|
||||
@@ -17,11 +17,13 @@ export const ppoRun = async () => {
|
||||
};
|
||||
|
||||
export const ppooMonitoring = async () => {
|
||||
if (!hasRoomMembers(`ppoo`)) {
|
||||
const roomId = "inventory:ppoo";
|
||||
|
||||
if (!hasRoomMembers(roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
emitToRoom("ppoo", {
|
||||
emitToRoom(roomId, {
|
||||
type: "snapshot",
|
||||
items: await ppoRun(),
|
||||
createdAt: new Date().toISOString(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
//import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ChevronRight, Link as link } from "lucide-react";
|
||||
import { permissionQuery } from "../../lib/queries/permsCheck";
|
||||
//import { permissionQuery } from "../../lib/queries/permsCheck";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
} from "../ui/sidebar";
|
||||
|
||||
export default function WarehouseBar() {
|
||||
const { data: canCreate = false } = useQuery(
|
||||
permissionQuery({
|
||||
warehouse: ["read"],
|
||||
}),
|
||||
);
|
||||
// const { data: canCreate = false } = useQuery(
|
||||
// permissionQuery({
|
||||
// warehouse: ["read"],
|
||||
// }),
|
||||
// );
|
||||
|
||||
const { setOpen } = useSidebar();
|
||||
const items = [
|
||||
@@ -33,7 +33,7 @@ export default function WarehouseBar() {
|
||||
title: "Dock Door Scanning",
|
||||
url: "/warehouse",
|
||||
//icon,
|
||||
isActive: canCreate,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "DockDoorScanning",
|
||||
|
||||
@@ -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,12 +27,13 @@ export function AppSidebar() {
|
||||
openDock: ["read"],
|
||||
}),
|
||||
);
|
||||
const { setOpen } = useSidebar();
|
||||
|
||||
const { data: canReadWarehouse = false } = useQuery(
|
||||
permissionQuery({
|
||||
warehouse: ["read"],
|
||||
}),
|
||||
);
|
||||
// const { data: canReadWarehouse = false } = useQuery(
|
||||
// permissionQuery({
|
||||
// warehouse: ["read"],
|
||||
// }),
|
||||
// );
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
@@ -36,7 +41,7 @@ export function AppSidebar() {
|
||||
collapsible="offcanvas"
|
||||
className="top-(--header-height) h-[calc(100svh-var(--header-height))]!"
|
||||
>
|
||||
<SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarContent>
|
||||
@@ -53,8 +58,7 @@ export function AppSidebar() {
|
||||
|
||||
{!isLoading &&
|
||||
settings.filter((n: any) => n.name === "dockDoorScanning")[0]
|
||||
?.active &&
|
||||
canReadWarehouse && <WarehouseBar />}
|
||||
?.active && <WarehouseBar />}
|
||||
|
||||
{session &&
|
||||
(session.user.role === "admin" ||
|
||||
@@ -65,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,110 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import socket from "@/lib/socket.io";
|
||||
|
||||
type RoomParams = Record<string, unknown>;
|
||||
|
||||
type JoinRoomPayload = {
|
||||
room: string;
|
||||
params?: RoomParams;
|
||||
};
|
||||
|
||||
type RoomUpdatePayload<T> = {
|
||||
roomId: string;
|
||||
payloads: T[];
|
||||
type: string;
|
||||
};
|
||||
|
||||
type RoomJoinedPayload = {
|
||||
room: string;
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
type RoomErrorPayload = {
|
||||
room?: string;
|
||||
roomId?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type UpdateMode = "append" | "replace";
|
||||
|
||||
export function useSocketRoom<T>(
|
||||
roomId: string,
|
||||
getKey?: (item: T) => string | number,
|
||||
updateMode: UpdateMode = "append",
|
||||
) {
|
||||
export function useSocketRoom<T>(room: string, params?: RoomParams) {
|
||||
const [actualRoomId, setActualRoomId] = useState<string | null>(null);
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [info, setInfo] = useState(
|
||||
"No data yet — join the room to start receiving",
|
||||
);
|
||||
|
||||
const clearRoom = useCallback(
|
||||
(id?: string | number) => {
|
||||
if (id !== undefined && getKey) {
|
||||
setData((prev) => prev.filter((item) => getKey(item) !== id));
|
||||
setInfo(`Removed item ${id}`);
|
||||
return;
|
||||
}
|
||||
console.log("cleared data from the room");
|
||||
setData([]);
|
||||
setInfo("Room data cleared");
|
||||
},
|
||||
[getKey],
|
||||
// This is the payload we send to the server.
|
||||
// Example:
|
||||
// { room: "inventory", params: { location: "ppoo" } }
|
||||
const joinPayload = useMemo<JoinRoomPayload>(
|
||||
() => ({
|
||||
room,
|
||||
params,
|
||||
}),
|
||||
[room, params],
|
||||
);
|
||||
|
||||
const clearRoom = useCallback((filterFn?: (item: T) => boolean) => {
|
||||
if (filterFn) {
|
||||
setData((prev) => prev.filter((item) => !filterFn(item)));
|
||||
return;
|
||||
}
|
||||
|
||||
setData([]);
|
||||
setInfo("Room data cleared");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function handleConnect() {
|
||||
socket.emit("join-room", roomId);
|
||||
setInfo(`Joined room: ${roomId}`);
|
||||
// Join the logical room.
|
||||
// The server decides the real Socket.IO roomId.
|
||||
// Example:
|
||||
// client sends: { room: "inventory", params: { location: "ppoo" } }
|
||||
// server joins: "inventory:ppoo"
|
||||
function joinRoom() {
|
||||
socket.emit("join-room", joinPayload);
|
||||
setInfo(`Joining room: ${room}`);
|
||||
}
|
||||
|
||||
// Server should emit this after socket.join(actualRoom).
|
||||
// This lets the client know the final roomId to filter updates by.
|
||||
function handleJoined(payload: RoomJoinedPayload) {
|
||||
//if (payload.room !== room) return;
|
||||
|
||||
setActualRoomId(payload.roomId);
|
||||
setInfo(`Joined room: ${payload.roomId}`);
|
||||
}
|
||||
|
||||
function handleUpdate(payload: RoomUpdatePayload<T>) {
|
||||
// protects against other room updates hitting this hook
|
||||
if (payload.roomId !== roomId) return;
|
||||
// If we know the actual roomId, only accept updates for that room.
|
||||
// This protects against other pages/rooms also listening to "room-update".
|
||||
|
||||
// resetting room data for rooms that just need updated data.
|
||||
if (updateMode === "replace") {
|
||||
if (!actualRoomId) return;
|
||||
|
||||
if (payload.roomId !== actualRoomId) return;
|
||||
|
||||
if (payload.type === "snapshot") {
|
||||
setData(payload.payloads);
|
||||
} else {
|
||||
setData((prev) => [...payload.payloads, ...prev]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Append mode is good for logs/scans/events.
|
||||
setData((prev) => [...payload.payloads, ...prev]);
|
||||
setInfo("");
|
||||
}
|
||||
|
||||
function handleError(err: RoomErrorPayload) {
|
||||
if (err.roomId && err.roomId !== roomId) return;
|
||||
// Ignore errors for other logical rooms.
|
||||
if (err.room && err.room !== room) return;
|
||||
|
||||
// Ignore errors for other actual rooms.
|
||||
if (err.roomId && room && err.roomId !== room) return;
|
||||
|
||||
toast.error(err.message);
|
||||
setInfo(err.message ?? "Room error");
|
||||
}
|
||||
|
||||
socket.on("connect", handleConnect);
|
||||
socket.on("connect", joinRoom);
|
||||
socket.on("room-joined", handleJoined);
|
||||
socket.on("room-update", handleUpdate);
|
||||
socket.on("room-error", handleError);
|
||||
|
||||
@@ -70,31 +112,26 @@ export function useSocketRoom<T>(
|
||||
socket.connect();
|
||||
}
|
||||
|
||||
// If already connected, join immediately
|
||||
// If socket is already connected, join immediately.
|
||||
if (socket.connected) {
|
||||
socket.emit("join-room", roomId);
|
||||
setInfo(`Joined room: ${roomId}`);
|
||||
joinRoom();
|
||||
}
|
||||
|
||||
return () => {
|
||||
socket.emit("leave-room", roomId);
|
||||
// Leave using the same logical room payload.
|
||||
// Server should rebuild the actual room and call socket.leave(actualRoom).
|
||||
socket.emit("leave-room", joinPayload);
|
||||
|
||||
socket.off("connect", handleConnect);
|
||||
socket.off("connect", joinRoom);
|
||||
socket.off("room-joined", handleJoined);
|
||||
socket.off("room-update", handleUpdate);
|
||||
socket.off("room-error", handleError);
|
||||
};
|
||||
}, [roomId, updateMode]);
|
||||
}, [room, joinPayload, actualRoomId]);
|
||||
|
||||
return { data, info, clearRoom };
|
||||
return {
|
||||
data,
|
||||
info,
|
||||
clearRoom,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
const isDockDoorPage = location.pathname.startsWith("/dockdoor");
|
||||
|
||||
useSocketRoom(
|
||||
dockId ? `dockdoor:${dockId}` : null,
|
||||
isDockDoorPage,
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
//import { useMemo } from "react";
|
||||
import { useSocketRoom } from "@/hooks/socket.io.hook";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
@@ -71,9 +72,10 @@ function LevelBadge({ level }: { level: string }) {
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
// const logParams = useMemo(() => ({ subModule: "query" }), []);
|
||||
// const { data: logs } = useSocketRoom<LogEntry>("logs", logParams);
|
||||
const { data: logs } = useSocketRoom<LogEntry>("logs");
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
|
||||
const column = [
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: ({ column }) => <SearchableHeader column={column} title="Time" />,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { format } from "date-fns-tz";
|
||||
import { CircleFadingArrowUp, Trash } from "lucide-react";
|
||||
import { Suspense, useState } from "react";
|
||||
import { Suspense, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Spinner } from "../../components/ui/spinner";
|
||||
@@ -171,7 +171,13 @@ const ServerTable = () => {
|
||||
};
|
||||
|
||||
function RouteComponent() {
|
||||
const { data: logs = [], clearRoom } = useSocketRoom<any>("admin:build");
|
||||
const params = useMemo(
|
||||
() => ({
|
||||
submodule: "builds",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const { data: logs = [], clearRoom } = useSocketRoom<any>("logs", params);
|
||||
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
|
||||
@@ -181,7 +187,7 @@ function RouteComponent() {
|
||||
<SearchableHeader column={column} title="Time" searchable={false} />
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"),
|
||||
cell: (i) => i.getValue(), //format(i.getValue(), "M/d/yyyy HH:mm") ,
|
||||
}),
|
||||
columnHelper.accessor("message", {
|
||||
header: ({ column }) => (
|
||||
@@ -210,7 +216,7 @@ function RouteComponent() {
|
||||
<Button
|
||||
size="icon"
|
||||
variant={"destructive"}
|
||||
onClick={() => clearRoom(x.timestamp)}
|
||||
onClick={() => clearRoom((item) => item.timestamp === x.timestamp)}
|
||||
>
|
||||
<Trash />
|
||||
</Button>
|
||||
@@ -242,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>
|
||||
|
||||
15
frontend/src/routes/apidocs.tsx
Normal file
15
frontend/src/routes/apidocs.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
17
frontend/src/routes/docs/datamart.tsx
Normal file
17
frontend/src/routes/docs/datamart.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/docs/datamart")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="w-full h-[calc(100vh-48px)] overflow-hidden mr-2 ml-2">
|
||||
<iframe
|
||||
className="w-full h-full border-0"
|
||||
title="datamart"
|
||||
src="https://docs.tuffraid.net/share/40lshswjqq/p/logistics-support-tool-docs-g3nuKAZ7Pw"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
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);
|
||||
const onTrigger = async () => {
|
||||
setActiveToggle(true);
|
||||
|
||||
try {
|
||||
const res = await api.delete(
|
||||
`/opendock/articleCheck/${i.row.original.id}`,
|
||||
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>
|
||||
{
|
||||
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
|
||||
|
||||
@@ -21,6 +21,7 @@ export const finishLoadingOrder = async (
|
||||
dockId: string,
|
||||
refetch: any,
|
||||
refetchActiveLoading: any,
|
||||
clear?: boolean,
|
||||
) => {
|
||||
try {
|
||||
const res = await api.post(
|
||||
@@ -28,6 +29,7 @@ export const finishLoadingOrder = async (
|
||||
{
|
||||
loadingOrder: loadingOrder,
|
||||
dockId: dockId,
|
||||
clear,
|
||||
},
|
||||
{ validateStatus: (status) => status < 500 },
|
||||
);
|
||||
@@ -80,8 +82,8 @@ function RouteComponent() {
|
||||
(x: any) => x.id === Number(i.currentLoadingOrder),
|
||||
)
|
||||
: [];
|
||||
console.log(loadingPlan);
|
||||
console.log(loadingPlanItems);
|
||||
// console.log(loadingPlan);
|
||||
// console.log(loadingPlanItems);
|
||||
return (
|
||||
<Card
|
||||
key={i.id}
|
||||
@@ -150,6 +152,7 @@ function RouteComponent() {
|
||||
i.dockId,
|
||||
refetch,
|
||||
refetchActiveLoading,
|
||||
true,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { useSocketRoom } from "../../../../hooks/socket.io.hook";
|
||||
@@ -21,11 +22,25 @@ export const Route = createFileRoute(
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { dockScans } = Route.useParams();
|
||||
const { data: logs, clearRoom } = useSocketRoom<any>(
|
||||
`dockDoorLoading:${dockScans}`,
|
||||
const { data: canSee = false } = useQuery(
|
||||
permissionQuery({
|
||||
warehouse: ["update"],
|
||||
}),
|
||||
);
|
||||
const { data, refetch } = useSuspenseQuery(getActiveDockScanners());
|
||||
const { dockScans } = Route.useParams();
|
||||
const params = useMemo(
|
||||
() => ({
|
||||
dockId: dockScans,
|
||||
loadingOrder: data[0].currentLoadingOrder ?? undefined,
|
||||
}),
|
||||
[dockScans, data],
|
||||
);
|
||||
const { data: logs, clearRoom } = useSocketRoom<any>(
|
||||
`dockDoorLoading`,
|
||||
params,
|
||||
);
|
||||
|
||||
const { data: loadingPlanItems, refetch: refetchActiveLoading } =
|
||||
useSuspenseQuery(getActiveLoadingOrders());
|
||||
|
||||
@@ -36,6 +51,24 @@ function RouteComponent() {
|
||||
);
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
|
||||
const logCount = logs.length;
|
||||
|
||||
// TODO: move this to an onMessage: handFunction
|
||||
/*
|
||||
const handleLogMessage = useCallback(() => {
|
||||
refetchActiveLoading();
|
||||
}, [refetchActiveLoading]);
|
||||
|
||||
const { data: logs } = useSocketRoom<LogEntry>("logs", {
|
||||
onMessage: handleLogMessage,
|
||||
});
|
||||
*/
|
||||
|
||||
// biome-ignore lint: false
|
||||
useEffect(() => {
|
||||
refetchActiveLoading();
|
||||
}, [logCount, refetchActiveLoading]);
|
||||
|
||||
const column = [
|
||||
columnHelper.accessor("loadingOrder", {
|
||||
header: ({ column }) => (
|
||||
@@ -147,33 +180,36 @@ function RouteComponent() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<div className="mb-2 mr-2 max-w-48">
|
||||
<form.AppField name="runningNo">
|
||||
{(field) => (
|
||||
<field.InputField
|
||||
label="Running Number"
|
||||
inputType="text"
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
</form.AppField>
|
||||
{canSee && (
|
||||
<div>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<div className="mb-2 mr-2 max-w-48">
|
||||
<form.AppField name="runningNo">
|
||||
{(field) => (
|
||||
<field.InputField
|
||||
label="Running Number"
|
||||
inputType="text"
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
</form.AppField>
|
||||
</div>
|
||||
<div className="flex justify-end mt-8 mr-3 ">
|
||||
<form.AppForm>
|
||||
<form.SubmitButton>Submit</form.SubmitButton>
|
||||
</form.AppForm>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-8 mr-3 ">
|
||||
<form.AppForm>
|
||||
<form.SubmitButton>Submit</form.SubmitButton>
|
||||
</form.AppForm>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingPlan && loadingPlan.length > 0 && (
|
||||
<div className="flex mb-2 gap-2">
|
||||
<Button
|
||||
|
||||
@@ -4,4 +4,10 @@
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
migrations/0064_magical_lady_mastermind.sql
Normal file
13
migrations/0064_magical_lady_mastermind.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE "forecast_import" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"receiving_plant_id" text NOT NULL,
|
||||
"documentName" text,
|
||||
"sender" text,
|
||||
"customer_id" text,
|
||||
"raw_data" jsonb DEFAULT '[]'::jsonb,
|
||||
"add_date" timestamp with time zone DEFAULT now(),
|
||||
"add_user" text DEFAULT 'lst-system',
|
||||
"upd_date" timestamp with time zone DEFAULT now(),
|
||||
"upd_user" text DEFAULT 'lst-system',
|
||||
CONSTRAINT "forecast_import_documentName_unique" UNIQUE("documentName")
|
||||
);
|
||||
1
migrations/0065_jittery_ares.sql
Normal file
1
migrations/0065_jittery_ares.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "forecast_import" DROP CONSTRAINT "forecast_import_documentName_unique";
|
||||
2783
migrations/meta/0064_snapshot.json
Normal file
2783
migrations/meta/0064_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2775
migrations/meta/0065_snapshot.json
Normal file
2775
migrations/meta/0065_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -449,6 +449,20 @@
|
||||
"when": 1781045714275,
|
||||
"tag": "0063_illegal_mauler",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 64,
|
||||
"version": "7",
|
||||
"when": 1781425987022,
|
||||
"tag": "0064_magical_lady_mastermind",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 65,
|
||||
"version": "7",
|
||||
"when": 1781426193735,
|
||||
"tag": "0065_jittery_ares",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
196
package-lock.json
generated
196
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "lst_v3",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"version": "0.1.0-alpha.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lst_v3",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"version": "0.1.0-alpha.3",
|
||||
"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",
|
||||
@@ -25,6 +25,7 @@
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"excel-date-to-js": "^1.1.5",
|
||||
"express": "^5.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"ldapts": "^8.1.7",
|
||||
@@ -42,7 +43,9 @@
|
||||
"powershell": "^2.3.3",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"zod": "^4.3.6"
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.3.6",
|
||||
"zod-openapi": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.8",
|
||||
@@ -2422,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"
|
||||
@@ -2470,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",
|
||||
@@ -3412,6 +3439,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
@@ -4301,6 +4337,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
@@ -4402,6 +4451,15 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -7634,6 +7692,15 @@
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/excel-date-to-js": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/excel-date-to-js/-/excel-date-to-js-1.1.5.tgz",
|
||||
"integrity": "sha512-grZW0MPye0VGCzLNljI7H22QWgrI8/hkTCvIUczYsQTTSaPQU/UTcz1fBPHNxWKpiv8Zu2I/98z+aAnlp6STNw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
@@ -8176,6 +8243,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
@@ -10515,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",
|
||||
@@ -12461,6 +12537,18 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
@@ -13189,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"
|
||||
@@ -13581,6 +13669,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -13674,6 +13780,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
@@ -13835,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
221
package.json
221
package.json
@@ -1,110 +1,115 @@
|
||||
{
|
||||
"name": "lst_v3",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"description": "The tool that supports us in our everyday alplaprod",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "dotenvx run -f .env -- vitest",
|
||||
"test:run": "dotenvx run -f .env -- vitest run",
|
||||
"dev": "concurrently -n \"server,frontend\" -c \"#007755, #1F73D1\" \"npm run dev:app\" \"npm run dev:frontend\"",
|
||||
"dev:app": "dotenvx run -f .env -- tsx watch backend/server.ts",
|
||||
"dev:frontend": "cd frontend && npm run dev",
|
||||
"dev:db:migrate": "npx drizzle-kit push",
|
||||
"dev:db:generate": "tsc && npx drizzle-kit generate --config=drizzle.config.ts",
|
||||
"build": "rimraf dist && npm run dev:db:generate && npm run dev:db:migrate && npm run build:app && npm run build:copySql && npm run build:copyGpSql && npm run build:emailTemplate && cd frontend && npm run build",
|
||||
"build:app": "tsc",
|
||||
"agent": "powershell -ExecutionPolicy Bypass -File scripts/agentController.ps1",
|
||||
"build:docker": "rimraf dist && npm run build:app && npm run build:copySql && npm run build:copyGpSql && npm run build:emailTemplate",
|
||||
"build:emailTemplate": "cpy \"backend/utils/mailViews/**/*\" dist/utils/mailViews --parents",
|
||||
"build:copyGpSql": "cpy \"backend/gpSql/queries/**/*\" dist/gpSql/queries --parents",
|
||||
"build:copySql": "cpy \"backend/prodSql/queries/**/*\" dist/prodSql/queries --parents",
|
||||
"lint": "tsc && biome lint",
|
||||
"start": "npm run start:server",
|
||||
"start:server": "dotenvx run -f .env -- node dist/server.js",
|
||||
"start:docker": "node dist/server.js",
|
||||
"version": "changeset version",
|
||||
"specCheck": "node scripts/check-route-specs.mjs",
|
||||
"commit": "cz",
|
||||
"release": "npm run build && commit-and-tag-version",
|
||||
"build:apk": "cd lstMobile && expo prebuild --clean && cd android && gradlew.bat assembleRelease "
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.tuffraid.net/cowch/lst_v3.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.8",
|
||||
"@commitlint/cli": "^20.5.0",
|
||||
"@commitlint/config-conventional": "^20.5.0",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/mssql": "^9.1.9",
|
||||
"@types/multer": "^2.1.0",
|
||||
"@types/net-snmp": "^3.23.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/nodemailer-express-handlebars": "^4.0.6",
|
||||
"@types/pg": "^8.18.0",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"commit-and-tag-version": "^12.7.1",
|
||||
"commitizen": "^4.3.1",
|
||||
"cpy-cli": "^7.0.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"npm-check-updates": "^19.6.5",
|
||||
"openapi-types": "^12.1.3",
|
||||
"supertest": "^7.2.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dotenvx/dotenvx": "^1.57.0",
|
||||
"@scalar/express-api-reference": "^0.9.4",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.13.6",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-auth": "^1.5.5",
|
||||
"chokidar": "^5.0.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"cors": "^2.8.6",
|
||||
"croner": "^10.0.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"express": "^5.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"ldapts": "^8.1.7",
|
||||
"modbus-serial": "^8.0.25",
|
||||
"morgan": "^1.10.1",
|
||||
"mssql": "^12.2.1",
|
||||
"multer": "^2.1.1",
|
||||
"net-snmp": "^3.26.1",
|
||||
"nodemailer": "^8.0.3",
|
||||
"nodemailer-express-handlebars": "^7.0.0",
|
||||
"pg": "^8.20.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"postgres": "^3.4.8",
|
||||
"powershell": "^2.3.3",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
"name": "lst_v3",
|
||||
"version": "0.1.0-alpha.3",
|
||||
"build": "179",
|
||||
"lastBuildDate": "6/17/2026 11:03",
|
||||
"description": "The tool that supports us in our everyday alplaprod",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "dotenvx run -f .env -- vitest",
|
||||
"test:run": "dotenvx run -f .env -- vitest run",
|
||||
"dev": "concurrently -n \"server,frontend\" -c \"#007755, #1F73D1\" \"npm run dev:app\" \"npm run dev:frontend\"",
|
||||
"dev:app": "dotenvx run -f .env -- tsx watch backend/server.ts",
|
||||
"dev:frontend": "cd frontend && npm run dev",
|
||||
"dev:db:migrate": "npx drizzle-kit push",
|
||||
"dev:db:generate": "tsc && npx drizzle-kit generate --config=drizzle.config.ts",
|
||||
"build": "rimraf dist && npm run dev:db:generate && npm run dev:db:migrate && npm run build:app && npm run build:copySql && npm run build:copyGpSql && npm run build:emailTemplate && cd frontend && npm run build",
|
||||
"build:app": "tsc",
|
||||
"agent": "powershell -ExecutionPolicy Bypass -File scripts/agentController.ps1",
|
||||
"build:docker": "rimraf dist && npm run build:app && npm run build:copySql && npm run build:copyGpSql && npm run build:emailTemplate",
|
||||
"build:emailTemplate": "cpy \"backend/utils/mailViews/**/*\" dist/utils/mailViews --parents",
|
||||
"build:copyGpSql": "cpy \"backend/gpSql/queries/**/*\" dist/gpSql/queries --parents",
|
||||
"build:copySql": "cpy \"backend/prodSql/queries/**/*\" dist/prodSql/queries --parents",
|
||||
"lint": "tsc && biome lint",
|
||||
"start": "npm run start:server",
|
||||
"start:server": "dotenvx run -f .env -- node dist/server.js",
|
||||
"start:docker": "node dist/server.js",
|
||||
"version": "changeset version",
|
||||
"specCheck": "node scripts/check-route-specs.mjs",
|
||||
"commit": "cz",
|
||||
"release": "npm run build && commit-and-tag-version",
|
||||
"build:apk": "cd lstMobile && expo prebuild --clean && cd android && gradlew.bat assembleRelease "
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.tuffraid.net/cowch/lst_v3.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.8",
|
||||
"@commitlint/cli": "^20.5.0",
|
||||
"@commitlint/config-conventional": "^20.5.0",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/mssql": "^9.1.9",
|
||||
"@types/multer": "^2.1.0",
|
||||
"@types/net-snmp": "^3.23.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/nodemailer-express-handlebars": "^4.0.6",
|
||||
"@types/pg": "^8.18.0",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"commit-and-tag-version": "^12.7.1",
|
||||
"commitizen": "^4.3.1",
|
||||
"cpy-cli": "^7.0.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"npm-check-updates": "^19.6.5",
|
||||
"openapi-types": "^12.1.3",
|
||||
"supertest": "^7.2.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dotenvx/dotenvx": "^1.57.0",
|
||||
"@scalar/express-api-reference": "^0.9.20",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.13.6",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-auth": "^1.5.5",
|
||||
"chokidar": "^5.0.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"cors": "^2.8.6",
|
||||
"croner": "^10.0.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"excel-date-to-js": "^1.1.5",
|
||||
"express": "^5.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"ldapts": "^8.1.7",
|
||||
"modbus-serial": "^8.0.25",
|
||||
"morgan": "^1.10.1",
|
||||
"mssql": "^12.2.1",
|
||||
"multer": "^2.1.1",
|
||||
"net-snmp": "^3.26.1",
|
||||
"nodemailer": "^8.0.3",
|
||||
"nodemailer-express-handlebars": "^7.0.0",
|
||||
"pg": "^8.20.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"postgres": "^3.4.8",
|
||||
"powershell": "^2.3.3",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.3.6",
|
||||
"zod-openapi": "^6.0.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,24 +31,32 @@ function Update-Server {
|
||||
[string]$Server,
|
||||
[string]$Token
|
||||
)
|
||||
$buildFile = Join-Path $AppDir ".buildNumber"
|
||||
|
||||
$buildFile = Join-Path $AppDir "package.json"
|
||||
$BuildNumber = 1
|
||||
$BuildFolder = Join-Path $AppDir "builds"
|
||||
|
||||
if (Test-Path $BuildFile) {
|
||||
$content = Get-Content $BuildFile | Select-Object -First 1
|
||||
$num = $content.Trim() -as [int] # safe cast
|
||||
$packageJson = Get-Content $buildFile -Raw | ConvertFrom-Json
|
||||
|
||||
if ($num) {
|
||||
$BuildNumber = $num + 1
|
||||
}
|
||||
else {
|
||||
$BuildNumber = 1
|
||||
}
|
||||
}
|
||||
# Extract the .build property and cast it safely to an integer
|
||||
$num = "$($packageJson.version).$(([int]$packageJson.build) -1)"
|
||||
# if (Test-Path $BuildFile) {
|
||||
# # Parse the entire JSON file into a PowerShell object
|
||||
# $packageJson = Get-Content $buildFile -Raw | ConvertFrom-Json
|
||||
|
||||
# # Extract the .build property and cast it safely to an integer
|
||||
# $num = "($packageJson.version).($packageJson.build)"
|
||||
|
||||
# if ($null -ne $num) {
|
||||
# $BuildNumber = $num + 1
|
||||
# }
|
||||
# else {
|
||||
# $BuildNumber = 1
|
||||
# }
|
||||
# }
|
||||
|
||||
# Get The current Build we have zipped up
|
||||
$BuildNumber = ([int]$BuildNumber - 1).ToString()
|
||||
$BuildNumber = $num
|
||||
|
||||
|
||||
# copy the latest build over
|
||||
|
||||
@@ -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"
|
||||
]}
|
||||
Reference in New Issue
Block a user