43 Commits

Author SHA1 Message Date
d85f08cb19 chore(release): 0.1.0-alpha.3
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m17s
Release and Build Image / release (push) Successful in 9s
2026-06-10 16:31:04 -05:00
15c939ebe8 refactor(server builds): added in proper logging will still need fine tuned though
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m52s
2026-06-10 16:29:23 -05:00
9ff428f5ea refactor(logs): socket io setup to be properly logging 2026-06-10 16:28:52 -05:00
2e460c7f8a refactor(warehouse): fixed ppoo check to match the new changes 2026-06-10 16:27:52 -05:00
7c4c5f980a refactor(opendock): changed the subModule for better logging 2026-06-10 16:27:23 -05:00
86e1237509 refactor(dock door scanning): fixes and final writes for the intial trial went smooth 2026-06-10 16:26:58 -05:00
706ab8b448 refactor(db): added in notifications vs pulling from the db makes it easier on the system 2026-06-10 16:26:21 -05:00
9440b44f3b refactor(socket.io): complete rewrite to manage dynamic rooms and seeding better 2026-06-10 16:25:48 -05:00
a2d9a6c127 refactor(opendock): changed the article to look at the label to match all plants
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m26s
2026-06-09 21:23:49 -05:00
c0a7d4a125 refactor(opendock): added some new goodies to the app to help manage releases
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-06-09 21:09:03 -05:00
8fcb2c66ed refactor(socketio): changes that if we are in dock room to constant reseed the room
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m49s
this will be fixed later when we redo the socket io rooms with dynamic stuff
2026-06-09 12:07:53 -05:00
8fc3129f7d refactor(opend dock): added in default dock so it uses the default setup as a backup
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m40s
This will help the live/drop and dont know what dock
2026-06-09 12:00:50 -05:00
05c553e927 fix(eom): removed un needed imports 2026-06-09 11:59:48 -05:00
6eaae0f537 refactor(dock scanner): more work on dock scanner and semi finished 2026-06-09 11:59:27 -05:00
3ad84dab71 refactor(opendock): added in proper complete and ignore of picksheets 2026-06-09 11:58:25 -05:00
5a7046bacd refactor(opendock): if load type is drop we will not do anything unless its already scanned
this means that if we drag down a bunch of loads we wont instantly change to arrived we will need to
have a single scan before we do this
2026-06-09 11:46:22 -05:00
cfc497c1f2 refactor(opendock): added in check for really using the article link 2026-06-09 11:45:09 -05:00
7bbdd4e555 refactor(mobile): new error found and added in
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m24s
2026-06-09 07:01:02 -05:00
e909e8deec feat(eom): migrated eom endpoints from old version validated working 2026-06-09 07:00:27 -05:00
4249e90307 fix(dockscanner): removed console log 2026-06-08 15:29:09 -05:00
2f495739e6 fix(dock door scanning): correction to how the data is posted 2026-06-08 15:26:43 -05:00
7d722c4aac refactor(datamart): if added to query will include plant token now 2026-06-08 15:24:45 -05:00
5f2148f5f0 refactor(servers): all remaining servers added 2026-06-08 15:23:25 -05:00
7671172d97 refactor(dockscanning): more work on the dock door scanning
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m47s
ref #12
2026-06-08 07:49:41 -05:00
6dd1a5b332 fix(scanner): corrected teh endpoint to delete the user if needed 2026-06-04 07:17:18 -05:00
865f280cfa fix(opendock): changed the header of delete to be properly named
ref #23
2026-06-04 07:16:53 -05:00
a717260b8d fix(opendock): correction to article link success on delete
ref #23
2026-06-04 07:02:27 -05:00
4464cea022 feat(opendock): added delete button in the article tab
ref #23
2026-06-04 07:01:10 -05:00
55418e2f09 refactor(new role): added in warehouse role 2026-06-04 06:49:49 -05:00
5281118596 fix(notifications): missed api converstion in the front end for updating 2026-06-03 08:40:15 -05:00
f635415b75 fix(opendock): ref wrong field oops
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m0s
2026-06-02 06:25:34 -05:00
40edbc3eae refactor(times): added a better view for times as we save all db as there respective timezone
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m50s
2026-06-01 16:57:38 -05:00
2558b2e5bb refactor(db): added timezone check in so it comes over correct based on the backend timezone 2026-06-01 16:05:36 -05:00
45a0dee9ca refactor(logs): refactored to show the submodule and stack as well to make it more easy to watch 2026-06-01 15:21:17 -05:00
bb7931d6c8 feat(opendock): added in a new article link system
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 4m11s
2026-06-01 14:26:51 -05:00
4f848bb649 fix(logistics): historical checks for no data errors when feature is activeed 2026-06-01 14:24:12 -05:00
f8335f5217 refactor(backend): dock door scanning socket and perms 2026-06-01 14:23:26 -05:00
2a35381fe4 refactor(mobile): intial addin of dockdoor scanning on mobile 2026-06-01 14:22:40 -05:00
e6b92aeb10 test(test): added in vitest to start doing more testing before deploying 2026-06-01 14:21:19 -05:00
da87e2e1d3 refactor(logger): included error in the stack version so we dont have to remove it all 2026-06-01 14:20:48 -05:00
3ef0f230dd fix(mobile): temp removed the autodownload as its causing issues 2026-05-29 06:16:52 -05:00
78f7b8a179 docs(app): updated last updated to the readme to show current progress
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m58s
2026-05-27 21:19:57 -05:00
1a3d8a7ddc docs(app): updated readme 2026-05-27 21:18:30 -05:00
143 changed files with 19669 additions and 739 deletions

View File

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

View File

@@ -7,7 +7,7 @@
Quick summary of current rewrite/migration goal.
- **Phase:** Backend rewrite
- **Last updated:** 2026-04-06
- **Last updated:** 2026-05-27
---
@@ -17,20 +17,20 @@ Quick summary of current rewrite/migration goal.
|----------|--------------|--------|
| User Authentication | ~~Login~~, ~~Signup~~, API Key | 🟨 In Progress |
| User Profile | ~~Edit profile~~, upload avatar | 🟨 In Progress |
| User Admin | Edit user, create user, remove user, alplaprod user integration | ⏳ Not Started |
| User Admin | ~~Edit user~~, ~~create user~~, remove user, alplaprod user integration | ⏳ Not Started |
| Notifications | ~~Subscribe~~, ~~Create~~, ~~Update~~, ~~~~Remove~~, Manual Trigger | 🟨 In Progress |
| Datamart | ~~Create~~, ~~Update~~, ~~Run~~, Deactivate | 🟨 In Progress |
| Frontend | Analytics and charts | ⏳ Not Started |
| Docs | Instructions and trouble shooting | ⏳ Not Started |
| One Click Print | Get printers, monitor printers, label process, material process, Special processes | ⏳ Not Started |
| One Click Print | ~~Get printers~~, monitor printers, label process, material process, Special processes | 🟨 In Progress |
| Silo Adjustments | Create, History, Comments | ⏳ Not Started |
| Demand Management | Orders, Forecast, Special Mappings, Create trucks, Load Trucks (tablet scanning) | ⏳ Not Started |
| Open Docks | Integrations | ⏳ Not Started |
| Open Docks | Integrations | 🟨 In Progress |
| Transport Insight | Integrations | ⏳ Not Started |
| Quality Request Tool | Add Pallet, Monitor for moved, status changes, alerts | ⏳ Not Started |
| Logistics | Consume material, return and print, label info, relocate | ⏳ Not Started |
| EOM | Endpoints, Report Pull for finance | ⏳ Not Started |
| OCME | Custom integration | ⏳ Not Started |
| EOM | ~~Endpoints~~, Report Pull for finance, SSRS report | 🟨 In Progress |
| ~~OCME~~ | ~~Custom integration~~ | Canceled |
| API Migration | Moving to new REST endpoints | 🔧 In Progress |
| System | Tests,Builds, Updates, Remote Logging, DB Backups, Alerting | ⏳ Not Started |
@@ -47,4 +47,13 @@ How to run the current version of the app.
git clone https://git.tuffraid.net/cowch/lst_v3.git
cd lst_v3
npm install
npm run dev
```
Rename the .env-example to .env
Update all the fields
```bash
npm run dev:db:migrate
npm run dev
```

View File

@@ -46,7 +46,8 @@ const createApp = async () => {
server: ${JSON.stringify(umamiConfig.server ?? "unknown")},
appVersion: ${JSON.stringify(umamiConfig.appVersion ?? "dev")},
umamiHost: ${JSON.stringify(umamiConfig.umamiHost ?? "")},
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")}
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")},
timezone: ${JSON.stringify(process.env.TIMEZONE ?? "America/Chicago")}
};
`);
});

View File

@@ -326,8 +326,13 @@ export const runDatamartQuery = async (data: Data) => {
level: "info",
module: "datamart",
subModule: "query",
message: `Data for: ${data.name}`,
data: queryRun.data,
message: `Data for: ${data.name} ${data.options.includePlantToken ? "including plant token" : ""}`,
// if includePlantToken was passed we should map this into the data
data: data.options.includePlantToken
? queryRun.data.map((i) => {
return { plantToken: process.env.PROD_PLANT_TOKEN, ...i };
})
: queryRun.data,
notify: false,
});
};

View File

@@ -0,0 +1,68 @@
import express from "express";
import request from "supertest";
import { describe, expect, it, vi } from "vitest";
vi.mock("../db/db.controller.js", () => ({
db: {},
}));
vi.mock("../logger/logger.controller.js", () => ({
createLogger: () => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
}));
vi.mock("./datamart.controller.js", () => ({
runDatamartQuery: vi.fn(async ({ name, options }) => ({
success: true,
message: `Ran ${name}`,
data: {
name,
options,
},
})),
}));
import { runDatamartQuery } from "./datamart.controller.js";
import getDatamartRoute from "./getDatamart.route.js";
function createTestApp() {
const app = express();
app.use(express.json());
app.use("/datamart", getDatamartRoute);
return app;
}
describe("GET /datamart/:name", () => {
it("runs a datamart query by name and returns api response", async () => {
const app = createTestApp();
const res = await request(app).get("/datamart/orders").query({
value: "123",
});
expect(res.status).toBe(200);
expect(runDatamartQuery).toHaveBeenCalledWith({
name: "orders",
options: {
value: "123",
},
});
expect(res.body.success).toBe(true);
expect(res.body.module).toBe("datamart");
expect(res.body.subModule).toBe("query");
expect(res.body.data).toEqual({
name: "orders",
options: {
value: "123",
},
});
});
});

View File

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

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

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

View File

@@ -17,15 +17,15 @@ export const alplaPurchaseHistory = pgTable("alpla_purchase_history", {
status: integer("status"),
statusText: text("status_text"),
journalNum: integer("journal_num"),
add_date: timestamp("add_date").defaultNow(),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user"),
upd_user: text("upd_user"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
remark: text("remark"),
approvedStatus: text("approved_status").default("new"),
position: jsonb("position").default([]),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
export const alplaPurchaseHistorySchema =

View File

@@ -3,7 +3,9 @@ import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const analytics = pgTable("analytics", {
id: uuid("id").defaultRandom().primaryKey(),
createdAt: timestamp("created_at").defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),

View File

@@ -16,13 +16,13 @@ export const jobAuditLog = pgTable(
id: uuid("id").defaultRandom().primaryKey(),
jobName: text("job_name"),
startedAt: timestamp("start_at"),
finishedAt: timestamp("finished_at"),
finishedAt: timestamp("finished_at", { withTimezone: true }),
durationMs: integer("duration_ms"),
status: text("status"), //success | error
errorMessage: text("error_message"),
errorStack: text("error_stack"),
metadata: jsonb("meta_data"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
},
(table) => {
return {

View File

@@ -15,7 +15,7 @@ export const user = pgTable("user", {
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),

View File

@@ -6,5 +6,5 @@ export const deploymentHistory = pgTable("deployment_history", {
buildNumber: integer("build_number").notNull(),
status: text("status").notNull(), // started, success, failed
message: text("message"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});

View File

@@ -28,11 +28,15 @@ export const analyticsDaily = pgTable(
avgDurationMs: integer("avg_duration_ms").notNull(),
maxDurationMs: integer("max_duration_ms").notNull(),
firstHitAt: timestamp("first_hit_at").notNull(),
lastHitAt: timestamp("last_hit_at").notNull(),
firstHitAt: timestamp("first_hit_at", { withTimezone: true }).notNull(),
lastHitAt: timestamp("last_hit_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => [
unique("analytics_daily_business_route_unique").on(

View File

@@ -18,9 +18,9 @@ export const datamart = pgTable("datamart", {
active: boolean("active").default(true),
options: text("options").default(""),
public: boolean("public_access").default(false),
add_date: timestamp("add_date").defaultNow(),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});

View File

@@ -0,0 +1,23 @@
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const dockDoorScans = pgTable("dock_door_scans", {
id: uuid("id").defaultRandom().primaryKey(),
dockId: text("dock_id").notNull(),
loadingOrder: text("loading_order").notNull(),
loadingUnit: text("loading_Unit"), // can be running number or sscc depending on where it came from
loadingUnitStatus: text("loading_unit_status").default("loaded"), // TODO: add enums on the status of each load.
message: text("message"), // the response it gave when scanning
status: text("status").default("active"), // TODO: add in enums for this
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 dockDoorScansSchema = createSelectSchema(dockDoorScans);
export const newDockDoorScansSchema = createInsertSchema(dockDoorScans);
export type DockDoorScans = z.infer<typeof dockDoorScansSchema>;
export type NewDockDoorScans = z.infer<typeof newDockDoorScansSchema>;

View File

@@ -9,9 +9,9 @@ export const dockDoorScanners = pgTable("dock_door_scanners", {
dockId: text("dock_id"),
active: boolean("active").default(true),
currentLoadingOrder: text("current_loading_order").default(""),
add_date: timestamp("add_date").defaultNow(),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});

View File

@@ -20,7 +20,7 @@ export const invHistoricalData = pgTable("inv_historical_data", {
whseId: text("whse_id").default(""),
whseName: text("whse_name").default("missing whseName"),
upd_user: text("upd_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
});
export const invHistoricalDataSchema = createSelectSchema(invHistoricalData);

View File

@@ -18,7 +18,7 @@ export const logs = pgTable("logs", {
stack: jsonb("stack").default([]),
checked: boolean("checked").default(false),
hostname: text("hostname"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});
export const logSchema = createSelectSchema(logs);

View File

@@ -16,9 +16,14 @@ export const opendockApt = pgTable(
id: uuid("id").defaultRandom().primaryKey(),
release: integer("release").notNull().unique("opendock_apt_release_unique"),
openDockAptId: text("open_dock_apt_id").notNull(),
status: text("status").default("active"),
appointment: jsonb("appointment").notNull().default([]),
upd_date: timestamp("upd_date").notNull().defaultNow(),
createdAt: timestamp("created_at").notNull().defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on(

View File

@@ -22,9 +22,13 @@ export const opendockArticleSetup = pgTable(
customerDescription: text("customer_description").notNull(),
loadType: loadTypeEnum("load_type").notNull().default("drop"),
dock: text("dock").notNull(),
upd_date: timestamp("upd_date").notNull().defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
},
(table) => ({

View File

@@ -6,9 +6,13 @@ export const opendockDockSetup = pgTable("opendock_dock_setup", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
dockID: text("dock_id").notNull(),
upd_date: timestamp("upd_date").notNull().defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
});

View File

@@ -7,5 +7,5 @@ export const printerLog = pgTable("printer_log", {
printerSN: text("printer_sn"),
condition: text("condition").notNull(),
message: text("message"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});

View File

@@ -28,8 +28,8 @@ export const printerData = pgTable(
printDelay: integer("printDelay").default(90),
processes: jsonb("processes").default([]),
printDelayOverride: boolean("print_delay_override").default(false), // this will be more for if we have the lot time active but want to over ride this single line for some reason
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => [
//uniqueIndex("emailUniqueIndex").on(sql`lower(${table.email})`),

View File

@@ -30,8 +30,8 @@ export const scanUser = pgTable(
role: mobileRoleEnum("role").notNull().default("user"),
active: boolean("active").default(true),
lastScan: timestamp("last_scan").defaultNow(),
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => ({
userNotificationUnique: unique("scan_user_unique").on(

View File

@@ -13,7 +13,7 @@ export const scanLog = pgTable("scan_log", {
status: text("status"),
scannerVersion: text("scanner_version").default("0"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_date").defaultNow(),
add_Date: timestamp("add_date", { withTimezone: true }).defaultNow(),
});
export const scanLogSchema = createSelectSchema(scanLog);

View File

@@ -22,7 +22,7 @@ export const serverData = pgTable(
contactPhone: text("contact_phone"),
active: boolean("active").default(true),
serverLoc: text("server_loc"),
lastUpdated: timestamp("last_updated").defaultNow(),
lastUpdated: timestamp("last_updated", { withTimezone: true }).defaultNow(),
buildNumber: integer("build_number"),
isUpgrading: boolean("is_upgrading").default(false),
},

View File

@@ -32,9 +32,9 @@ export const settings = pgTable(
settingType: settingType(),
seedVersion: integer("seed_version").default(1), // this is intended for if we want to update the settings.
add_User: text("add_User").default("LST_System").notNull(),
add_Date: timestamp("add_Date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_User").default("LST_System").notNull(),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => [
// uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),

View File

@@ -12,11 +12,11 @@ import type z from "zod";
export const appStats = pgTable("app_stats", {
id: text("id").primaryKey().default("primary"),
currentBuild: integer("current_build").notNull().default(1),
lastBuildAt: timestamp("last_build_at"),
lastDeployAt: timestamp("last_deploy_at"),
lastBuildAt: timestamp("last_build_at", { withTimezone: true }),
lastDeployAt: timestamp("last_deploy_at", { withTimezone: true }),
building: boolean("building").notNull().default(false),
updating: boolean("updating").notNull().default(false),
lastUpdated: timestamp("last_updated").defaultNow(),
lastUpdated: timestamp("last_updated", { withTimezone: true }).defaultNow(),
meta: jsonb("meta").$type<Record<string, unknown>>().default({}),
});

View File

@@ -16,6 +16,7 @@ r.get("/", async (_, res) => {
loadingDateTo: format(addDays(new Date(Date.now()), 3), "yyyy-MM-dd"),
states: [
1, // planned
2, // loading
],
//isCommissioned: true,
},

View File

@@ -1,18 +1,181 @@
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { dockDoorScans } from "../db/schema/dockdoor.scans.schema.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const endLoading = z.object({
loadingOrder: z.string(),
dockId: z.string(),
});
r.post("/", async (req, res) => {
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `Release x is being closed now. the bol should come out at the default printer.`,
data: req.body ?? [],
status: 200,
});
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);
const orders = (await runProdApi({
method: "post",
endpoint: `/public/v1.0/OutboundDeliveries/LoadingOrders/${req.body.loadingOrder}/Finish`,
data: [
{
printDeliveryDocuments: true,
},
],
})) as any;
if (orders?.data.errors) {
//console.log(orders.data.errors);
// if (orders.data.errors[0].code === 20) {
// await db
// .update(dockDoorScanners)
// .set({
// currentLoadingOrder: "",
// upd_date: sql`NOW()`,
// upd_user: req.user?.username ?? "lst-dock-system",
// })
// .where(eq(dockDoorScanners.dockId, validated.dockId));
// return apiReturn(res, {
// success: false,
// level: "warn",
// module: "dockdoor",
// subModule: "loadingOrder",
// message: `Loading order: ${validated.loadingOrder} cleared, It was probable completed by a scanner.. or user...`,
// data: (orders.data.errors as any) ?? [],
// status: 200,
// });
// }
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to finish the order: ${orders.data.errors[0].message}`,
data: (orders.data.errors as any) ?? [],
status: 400,
});
}
await tryCatch(
db
.update(dockDoorScans)
.set({
status: "completed",
upd_date: sql`NOW()`,
upd_user: req.user?.username ?? "lst-dock-system",
})
.where(
eq(dockDoorScans.loadingOrder, validated.loadingOrder.toString()),
)
.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, validated.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: orders.data.errors ? false : true,
level: orders.data.errors ? "error" : "info",
module: "dockdoor",
subModule: "loadingOrder",
message: orders.data.errors
? `Loading order was cleared but encountered an error: \n${orders.data.errors[0].message} \nPossible reason for this is the loading order was completed via scanner or other means.`
: `Loading order ${validated.loadingOrder} was just closed.`,
data: data ?? [],
status: orders.data.errors ? 400 : 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to Close loading order.`,
data: (error as any) ?? [],
status: 400,
});
}
});
export default r;

View File

@@ -0,0 +1,25 @@
import { Router } from "express";
import { apiReturn } from "../utils/returnHelper.utils.js";
import loadUnit from "./dockdoor.loadUnits.js";
const r = Router();
r.post("/", async (req, res) => {
const unit = await loadUnit({
dockId: req.body.dockId,
runningNo: req.body.runningNo,
});
return apiReturn(res, {
success: unit.success,
level: "info",
module: "dockdoor",
subModule: "loadingUnit",
message: unit.message,
data: unit?.data ?? [],
status: unit.success ? 200 : 400,
});
});
export default r;

View File

@@ -2,7 +2,10 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { dockDoorScans } from "../db/schema/dockdoor.scans.schema.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
@@ -11,15 +14,45 @@ import { returnFunc } from "../utils/returnHelper.utils.js";
type Data = {
dockId?: string;
sscc?: string;
runningNr?: string;
runningNo?: string;
};
export const loadUnit = async (data: Data) => {
const postScan = async (data: any) => {
try {
const newScan = (await db
.insert(dockDoorScans)
.values({
dockId: data.dockId,
loadingOrder: data.loadingOrder,
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
})
.returning()) as any;
emitToRoom(`dockDoorLoading:${data.dockId}`, newScan[0]);
} catch (error) {
console.log("Error: ", error);
}
};
const loadUnit = async (data: Data) => {
const log = createLogger({ module: "dockdoor", subModule: "loadunit" });
log.info({ stack: data }, "Data Passed over from the scanner.");
// are we even active at this time?
const dockDoorActive = await db.query.settings.findFirst({
where: (u, { eq }) => eq(u.name, "dockDoorScanning"),
});
const unitToScan = data.sscc
? { sscc: data.sscc !== "noread" ? data.sscc?.slice(2) : data.sscc }
: { runningNo: Number(data.runningNo) };
const dock = await db
.select()
.from(dockDoorScanners)
.where(eq(dockDoorScanners.dockId, data.dockId as string));
if (!dockDoorActive?.active) {
return returnFunc({
success: false,
@@ -32,29 +65,18 @@ export const loadUnit = async (data: Data) => {
room: "",
});
}
// check if its a valids an sscc
if (data.sscc === "noread") {
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message:
"Failed to load the unit to the truck, there was no pallet read.",
data: [],
notify: false,
room: `dockDoorLoading${data.dockId}`,
});
}
// check if we currently have a loading order attached to the dock door.
const dock = await db
.select()
.from(dockDoorScanners)
.where(eq(dockDoorScanners.dockId, data.dockId as string));
if (dock[0]?.currentLoadingOrder === "") {
postScan({
dockId: data.dockId,
loadingOrder: dock[0]?.currentLoadingOrder,
loadingUnit: unitToScan,
loadingUnitStatus: "notLoaded",
message:
"There are know current active loading orders please start one and try again.",
});
return returnFunc({
success: true,
level: "error",
@@ -64,7 +86,31 @@ export 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}`,
room: `dockDoorLoading:${data.dockId}`,
});
}
// check if its a valids an sscc
if (data.sscc === "noread") {
postScan({
dockId: data.dockId,
loadingOrder: dock[0]?.currentLoadingOrder,
loadingUnit: unitToScan,
loadingUnitStatus: "noread",
message:
"Failed to load the unit to the truck, there was no pallet read.",
});
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message:
"Failed to load the unit to the truck, there was no pallet read.",
data: [],
notify: false,
room: `dockDoorLoading:${data.dockId}`,
});
}
@@ -76,14 +122,78 @@ export const loadUnit = async (data: Data) => {
// add the loading units
try {
const prod = await runProdApi({
const prod = (await runProdApi({
method: "post",
endpoint: `/public/v1.0/OutboundDeliveries/LoadingOrders/${dock[0]?.currentLoadingOrder}/LoadUnit`,
data: [{ sscc: data.sscc?.slice(2) }],
});
data: [unitToScan],
})) as any;
console.log(prod?.data);
//emitToRoom(`dockDoorLoading:${data.dockId}`, prod?.data ?? []);
if (!prod?.success) {
postScan({
dockId: data.dockId,
loadingOrder: dock[0]?.currentLoadingOrder,
loadingUnit: unitToScan,
loadingUnitStatus: "notLoaded",
message: prod?.data.errors[0].message,
});
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message: `Unit encountered an error while loading`,
data: prod?.data.errors[0] as any,
notify: false,
//room: `dockDoorLoading:${data.dockId}`,
});
} else {
const emitData = {
message: `The unit ${prod.data.message.messageParams.runningNo} was loaded.`,
data: prod.data,
code: 0,
};
postScan({
dockId: data.dockId,
loadingOrder: dock[0]?.currentLoadingOrder,
loadingUnit: unitToScan,
loadingUnitStatus: "loaded",
message: emitData.message,
});
return returnFunc({
success: true,
level: "info",
module: "dockdoor",
subModule: "loadUnit",
message: `Unit added to loading order`,
data: [
{
message: `The unit ${prod.data.message.messageParams.runningNo} was loaded.`,
data: prod.data,
code: 0,
},
] as any,
notify: false,
//room: `dockDoorLoading:${data.dockId}`,
});
}
} catch (error) {
console.log(error);
return returnFunc({
success: true,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message: `Failed to load unit`,
data: error as any,
notify: false,
room: `dockDoorLoading:${data.dockId}`,
});
}
};
export default loadUnit;

View File

@@ -2,6 +2,7 @@ import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import activeLoadingOrders from "./dockdoor.activeLoadingOrders.route.js";
import closeLoadingOrder from "./dockdoor.closeLoadingOrder.route.js";
import load from "./dockdoor.loadUnits.route.js";
import startLoad from "./dockdoor.startLoad.route.js";
import prodDocks from "./dockdoors.docks.route.js";
import docks from "./dockdoors.route.js";
@@ -17,7 +18,7 @@ export const setupDockDoorRoutes = (baseUrl: string, app: Express) => {
);
app.use(
`${baseUrl}/api/dockDoor/closeLoadingOrder`,
`${baseUrl}/api/dockDoor/finishOrder`,
featureCheck("dockDoorScanning"),
closeLoadingOrder,
);
@@ -37,7 +38,11 @@ export const setupDockDoorRoutes = (baseUrl: string, app: Express) => {
prodDocks,
);
// TODO : add manual way to add pallets
app.use(
`${baseUrl}/api/dockDoor/loadUnit`,
featureCheck("dockDoorScanning"),
load,
);
// all other system should be under /api/system/*
};

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

View File

@@ -0,0 +1,20 @@
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) : undefined,
),
orderBy: (scans, { desc }) => [desc(scans.upd_date)],
limit,
});
};

View File

@@ -1,4 +1,4 @@
import { sql } from "drizzle-orm";
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
@@ -25,6 +25,7 @@ r.post("/", async (req, res) => {
upd_date: sql`NOW()`,
upd_user: req.user?.username,
})
.where(eq(dockDoorScanners.dockId, validated.dockId))
.returning(),
);

View File

@@ -0,0 +1,107 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { gpQuery } from "../gpSql/gpSqlQuery.controller.js";
import {
type SqlGPQuery,
sqlGpQuerySelector,
} from "../gpSql/gpSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, glCode, includePlantToken } = req.query;
if (
!startDate ||
startDate === "" ||
!endDate ||
endDate === "" ||
!glCode ||
glCode === ""
) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "GpData",
message:
"The start date, end date, and gl code are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlGpQuerySelector(`gp.eom.data`) as SqlGPQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "GpData",
message:
"Failed to get GpData sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
gpQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string)
.replace("[gpCode]", glCode as string),
"Eom GpData data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "GpData",
message: `Error getting GpData data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "GpData",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
Date_Received: formatInTimeZone(
i.Date_Received,
"etc/utc",
"M/d/yyyy",
),
};
})
: data.data.map((i) => {
return {
...i,
Date_Received: formatInTimeZone(
i.Date_Received,
"etc/utc",
"M/d/yyyy",
),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,9 @@
/**
* The flow that will trigger all the history functions to run and store the data each day and clean up as needed
*
* if we are on usmcd1vms036 we will run a get request to all servers in the db so we can store that data as well.
*
* this will be stored in both. the vms036 functions will store in a bigger server so it can be pulled faster and in ssrs
*/
export const eomHistory = async () => {};

View File

@@ -0,0 +1,61 @@
import { format } from "date-fns";
import { eq } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { invHistoricalData } from "../db/schema/historicalInv.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
// the params we are wanting to add in. min required will be the month so we dont pass everything over
const { date } = req.query;
if (!date || date === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "historical inv",
message:
"The day of the month is required to be included in order to pass.",
data: [],
status: 400,
});
}
// get the date passed over.
const { data, error } = await tryCatch(
db
.select()
.from(invHistoricalData)
.where(
eq(invHistoricalData.histDate, format(date as string, "yyyy-MM-dd")),
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "historical inv",
message:
"There was an error getting the historical data from the server.",
data: error as any,
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "eom",
subModule: "historical inv",
message: "Eom Historical Inv Data",
data: data,
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,77 @@
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { includePlantToken } = req.query;
const sqlQuery = sqlQuerySelector(`eom.lastPurchasePrice`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastPurchasePrice",
message:
"Failed to get last sales price sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query,
"Eom last purchase price data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastPurchasePrice",
message: `Error getting last purchase Price data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "lastPurchasePrice",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
//validDate: formatInTimeZone(i.validDate, "etc/utc", "M/d/yyyy"),
};
})
: data.data.map((i) => {
return {
...i,
//validDate: formatInTimeZone(i.validDate, "etc/utc", "M/d/yyyy"),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,89 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { date, includePlantToken } = req.query;
if (!date || date === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastSalesPrice",
message: "A date is required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.lastSalesPrice`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastSalesPrice",
message:
"Failed to get last sales price sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query.replace("[date]", date as string),
"Eom last sales price data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastSalesPrice",
message: `Error getting last Sales Price data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "lastSalesPrice",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
validDate: formatInTimeZone(i.validDate, "etc/utc", "M/d/yyyy"),
};
})
: data.data.map((i) => {
return {
...i,
validDate: formatInTimeZone(i.validDate, "etc/utc", "M/d/yyyy"),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,97 @@
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, includePlantToken } = req.query;
if (!startDate || startDate === "" || !endDate || endDate === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "productionConsumption",
message: "The start date and end date are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.productionConsumption`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "productionConsumption",
message:
"Failed to get production consumption sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string),
"Eom production consumption data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "productionConsumption",
message: `Error getting production consumption data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "productionConsumption",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
// Prod_Date: formatInTimeZone(
// i.Prod_Date,
// "etc/utc",
// "M/d/yyyy",
// ),
};
})
: data.data.map((i) => {
return {
...i,
// Prod_Date: formatInTimeZone(
// i.Prod_Date,
// "etc/utc",
// "M/d/yyyy",
// ),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,98 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, includePlantToken } = req.query;
if (!startDate || startDate === "" || !endDate || endDate === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "purchased",
message: "The start date and end date are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.purchased`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "purchased",
message:
"Failed to get purchased sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string),
"Eom purchased data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "purchased",
message: `Error getting purchased data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "purchased",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
Received_Date: formatInTimeZone(
i.Received_Date,
"etc/utc",
"M/d/yyyy",
),
};
})
: data.data.map((i) => {
return {
...i,
Received_Date: formatInTimeZone(
i.Received_Date,
"etc/utc",
"M/d/yyyy",
),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,98 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, includePlantToken } = req.query;
if (!startDate || startDate === "" || !endDate || endDate === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "regrind",
message: "The start date and end date are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.regrind`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "regrind",
message:
"Failed to get regrind sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string),
"Eom regrind data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "regrind",
message: `Error getting regrind data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "regrind",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
Buchungsdatum: formatInTimeZone(
i.Buchungsdatum,
"etc/utc",
"M/d/yyyy",
),
};
})
: data.data.map((i) => {
return {
...i,
Buchungsdatum: formatInTimeZone(
i.Buchungsdatum,
"etc/utc",
"M/d/yyyy",
),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

35
backend/eom/eom.routes.ts Normal file
View File

@@ -0,0 +1,35 @@
import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import gpData from "./eom.gpdata.route.js";
import historyInv from "./eom.historyInv.route.js";
import lastPurchasePrice from "./eom.lastPurchasePrice.route.js";
import lastSalesPrice from "./eom.lastSalesPrice.route.js";
import productionConsumption from "./eom.productionConsumption.route.js";
import purchased from "./eom.purchased.route.js";
import regrind from "./eom.regrind.route.js";
import soldItems from "./eom.soldItems.route.js";
export const setupEomRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/eom/historyInv`, featureCheck("eom"), historyInv);
app.use(`${baseUrl}/api/eom/purchased`, featureCheck("eom"), purchased);
app.use(
`${baseUrl}/api/eom/lastSalesPrice`,
featureCheck("eom"),
lastSalesPrice,
);
app.use(
`${baseUrl}/api/eom/lastPurchasePrice`,
featureCheck("eom"),
lastPurchasePrice,
);
app.use(
`${baseUrl}/api/eom/productionConsumption`,
featureCheck("eom"),
productionConsumption,
);
app.use(`${baseUrl}/api/eom/regrind`, featureCheck("eom"), regrind);
app.use(`${baseUrl}/api/eom/soldItems`, featureCheck("eom"), soldItems);
app.use(`${baseUrl}/api/eom/gpData`, featureCheck("eom"), gpData);
};

View File

@@ -0,0 +1,98 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, includePlantToken } = req.query;
if (!startDate || startDate === "" || !endDate || endDate === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "soldItems",
message: "The start date and end date are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.soldItems`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "soldItems",
message:
"Failed to get soldItems sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string),
"Eom soldItems data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "soldItems",
message: `Error getting soldItems data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "soldItems",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
DeliveryDate: formatInTimeZone(
i.DeliveryDate,
"etc/utc",
"M/d/yyyy",
),
};
})
: data.data.map((i) => {
return {
...i,
DeliveryDate: formatInTimeZone(
i.DeliveryDate,
"etc/utc",
"M/d/yyyy",
),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,16 @@
select * from (
select
case when x.POPRCTNM is null then p.POPRCTNM else p.POPRCTNM end as RCT_Num,
PONUMBER PO,
p.VENDORID Supplier,
ITEMNMBR Item,
QTYSHPPD shipped,
UOFM Type,
TRXLOCTN Location,
case when CONVERT(DATE, x.receiptdate) is null then convert(date, p.DATERECD) else CONVERT(DATE, x.receiptdate) end as Date_Received
from ALPLA.dbo.pop10500 (nolock) as p
left join
ALPLA.dbo.POP10300 as x on p.POPRCTNM = x.POPRCTNM
WHERE TRXLOCTN LIKE '[gpCode]%' and p.POPTYPE = 1) a
where Date_Received BETWEEN '[startDate]' AND '[endDate]'

View File

@@ -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";
@@ -37,7 +36,7 @@ const dbStream = new Writable({
subModule: obj?.subModule?.toLowerCase(),
hostname: obj?.hostname?.toLowerCase(),
message: obj.msg,
stack: obj?.stack,
stack: obj?.stack || obj?.error, // this will add in the error or stack depending on how we pass it.
})
.returning(),
);
@@ -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);

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

View File

@@ -49,7 +49,7 @@ const historicalInvImport = async () => {
});
}
if (data?.length === 0) {
if (data.length === 0) {
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
if (!avSQLQuery.success) {
@@ -139,7 +139,7 @@ const historicalInvImport = async () => {
subModule: "inv",
message: `Error adding historical data to lst db`,
data: errorImport as any,
notify: true,
notify: false,
});
}

View File

@@ -22,7 +22,6 @@ r.patch(
requirePermission({ notifications: ["update"] }),
async (req, res: Response) => {
const { id } = req.params;
try {
const validated = updateNote.parse(req.body);
@@ -37,6 +36,7 @@ r.patch(
await modifiedNotification(id as string);
if (nError) {
return apiReturn(res, {
success: false,
level: "error",
@@ -58,6 +58,7 @@ r.patch(
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({

View File

@@ -21,6 +21,7 @@ type Releases = {
ReleaseNumber: number;
DeliveryState: number;
DeliveryDate: Date;
ReleaseState: number;
LineItemHumanReadableId: number;
ArticleAlias: string;
LoadingUnits: string;
@@ -31,6 +32,16 @@ type Releases = {
DeliveryAddressHumanReadableId: string;
AdditionalInformation1: string;
};
// TODO: add these docs into the db
const actaulDocks = [
{ name: "cermac", dockId: "bcb17fae-0b1a-47a7-9fbf-594c5ebccce9" },
{ name: "matrix", dockId: "3e32cbfc-49f4-4138-b491-9d5df9c94754" },
{ name: "gerber", dockId: "9109e789-6c15-4cd9-87cb-de1b18627b6d" },
{ name: "rb", dockId: "6be02526-6183-4789-a73f-e0aa155e6d1e" },
//test server dock
{ name: "second", dockId: "e87c92bd-13b4-4f7e-bf5e-b0182884c47a" },
];
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
@@ -64,6 +75,7 @@ let lastCheck = formatInTimeZone(
// };
const postRelease = async (release: Releases) => {
log.debug({}, `Release: ${release.ReleaseNumber} is about to be validated`);
if (!odToken.odToken) {
log.info({}, "Getting Auth Token");
await getToken();
@@ -82,21 +94,55 @@ const postRelease = async (release: Releases) => {
where: (u, { eq }) => eq(u.name, "defaultLoadType"),
});
// check if the release has the data in it
// check if the release has the loadtype in it
const releaseLoadtypeCheck = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("drop");
// allowed to schedule now, as long as we see od in here somewhere
const releaseOkToSchedule = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("od");
// dock was sent over
const releaseDockInfo = actaulDocks.some((dock) =>
(release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes(dock.name.toLowerCase()),
);
const opendDockArticleCheck = await db.query.opendockArticleSetup.findFirst({
where: (table, { and, eq }) =>
and(
eq(table.av, release.LineItemArticleWeight),
eq(table.av, release.LineItemHumanReadableId),
eq(table.customer, release.DeliveryAddressHumanReadableId),
),
});
// selected dock
const releaseDocks = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim());
const matchedDock = actaulDocks.find((dock) =>
releaseDocks.includes(dock.name.toLowerCase()),
);
const setDock =
// validate we dont have the dock in the release
releaseDockInfo
? matchedDock?.dockId
: // validate we dont have the dock in the aritcle check
(actaulDocks.find((d) => d.name === opendDockArticleCheck?.dock)
?.dockId ?? process.env.DEFAULT_DOCK);
// TODO: add in docks from lst db here to make it more universal for the team
/**
* ReleaseState
@@ -127,7 +173,7 @@ const postRelease = async (release: Releases) => {
userId: process.env.DEFAULT_CARRIER, // this should be the carrierid
loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one
// TODO: look in the remarks in the release and if its says
dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out
dockId: setDock, // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber],
//refNumber: release.ReleaseNumber,
start: release.DeliveryDate,
@@ -230,11 +276,65 @@ const postRelease = async (release: Releases) => {
if (existing) {
const id = existing.openDockAptId;
// deal with canceled stuff as we want this gone off od
if (release.ReleaseState === 2 || release.ReleaseState === 4) {
// delete the order in od and change the state to canceled in lst
try {
const response = await axios.delete(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
// {
// hardDelete: true,
// },
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.update(opendockApt)
.set({
status: "canceled",
upd_date: sql`Now()`,
})
.where(eq(opendockApt.release, release.ReleaseNumber))
.returning();
log.info({}, `${release.ReleaseNumber} was canceled`);
} catch (e) {
log.error(
{ stack: e },
`Error canceling the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ stack: e.response.data },
`An error has occurred during canceling of the release: ${release.ReleaseNumber}`,
);
return;
}
return;
}
if (
(releaseLoadtypeCheck ||
opendDockArticleCheck?.loadType === "drop" ||
defaultDock?.value === "drop") &&
(release.DeliveryState === 0 || release.DeliveryState === 1)
release.DeliveryState === 2
) {
const setArrival = { ...newDockApt, status: "Arrived" };
@@ -348,7 +448,12 @@ const postRelease = async (release: Releases) => {
return;
}
} else {
// changing to only trigger the change if the state is 2 meaning it has a scan to it and already in progress of being loaded.
} else if (
release.DeliveryState === 0 ||
release.DeliveryState === 1 ||
release.DeliveryState === 2
) {
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
@@ -400,10 +505,66 @@ const postRelease = async (release: Releases) => {
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
// if we are finished we need to set to completed
} else if (release.DeliveryState === 3 || release.DeliveryState === 4) {
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
status: "completed",
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(
{ stack: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
}
} else {
} else if (opendDockArticleCheck?.loadType === "live") {
try {
const response = await axios.post(
`${process.env.OPENDOCK_URL}/appointment`,
@@ -458,6 +619,136 @@ const postRelease = async (release: Releases) => {
return;
}
} else if (
(releaseLoadtypeCheck ||
opendDockArticleCheck?.loadType === "drop" ||
defaultDock?.value === "drop") &&
releaseOkToSchedule
) {
try {
const response = await axios.post(
`${process.env.OPENDOCK_URL}/appointment`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
// we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
// this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
// TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
//console.info(response.data.data, "Was Created");
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was created`);
} catch (e) {
log.error({ stack: e }, "Error creating new release");
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
log.error(
{ stack: e?.response?.data },
`Error posting new release to opendock, ${release.ReleaseNumber}`,
);
return;
}
} else {
// try {
// const response = await axios.post(
// `${process.env.OPENDOCK_URL}/appointment`,
// newDockApt,
// {
// headers: {
// "content-type": "application/json; charset=utf-8",
// Authorization: `Bearer ${odToken.odToken}`,
// },
// },
// );
// // we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
// // this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.
// if (response.status === 400) {
// log.error({}, response.data.data.message);
// return;
// }
// // the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
// // TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
// //console.info(response.data.data, "Was Created");
// try {
// await db
// .insert(opendockApt)
// .values({
// release: release.ReleaseNumber,
// openDockAptId: response.data.data.id,
// appointment: response.data.data,
// })
// .onConflictDoUpdate({
// target: opendockApt.release,
// set: {
// openDockAptId: response.data.data.id,
// appointment: response.data.data,
// upd_date: sql`NOW()`,
// },
// })
// .returning();
// log.info({}, `${release.ReleaseNumber} was created`);
// } catch (e) {
// log.error({ stack: e }, "Error creating new release");
// }
// // biome-ignore lint/suspicious/noExplicitAny: to many possibilities
// } catch (e: any) {
// log.error(
// { stack: e?.response?.data },
// `Error posting new release to opendock, ${release.ReleaseNumber}`,
// );
// return;
// }
log.info(
{
stack: {
release: release.ReleaseNumber,
releaseLoadtypeCheck,
articleLoadType: opendDockArticleCheck?.loadType,
defaultLoadType: defaultDock?.value,
releaseOkToSchedule,
},
},
`Skipping OpenDock post - release: ${release.ReleaseNumber} is not allowed to schedule`,
);
return;
}
await delay(750); // rate limit protection

View File

@@ -0,0 +1,73 @@
import axios from "axios";
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { odToken } from "./opendock.utils.js";
const r = Router();
r.patch("/:id", async (req, res) => {
//const limit
const { id } = req.params;
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}/undo-latest-status`,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: response.data.data.message,
data: [],
status: 400,
});
}
// update the release in the db
const { data } = await tryCatch(
db
.update(opendockApt)
.set({
appointment: response.data.data,
upd_date: sql`NOW()`,
})
.where(eq(opendockApt.openDockAptId, id)),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The release was reverted to the last state.`,
data: data as any,
status: 200,
});
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: `An error updating the release in opendock`,
data: e.response.data,
status: 400,
});
}
});
export default r;

View File

@@ -137,7 +137,7 @@ r.delete("/:id", async (req, res) => {
.where(eq(opendockArticleSetup.id, id))
.returning();
return apiReturn(res, {
success: false,
success: true,
level: "info", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",

View File

@@ -1,9 +1,9 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import undo from "./openDockUndoLastStatus.js";
import articleCheck from "./opendock.articleCheck.route.js";
import getApt from "./opendockGetRelease.route.js";
import getApt from "./opendockRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
@@ -21,4 +21,11 @@ export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
requireAuth,
articleCheck,
);
app.use(
`${baseUrl}/api/opendock/undo-latest-status`,
featureCheck("opendock_sync"),
requireAuth,
undo,
);
};

View File

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

View File

@@ -1,40 +0,0 @@
import { desc, gte, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (_, res) => {
//const limit
const daysCreated = 30;
const { data } = await tryCatch(
db
.select()
.from(opendockApt)
.where(
gte(
opendockApt.createdAt,
sql.raw(`NOW() - INTERVAL '${daysCreated} days'`),
),
)
.orderBy(desc(opendockApt.createdAt))
.limit(500),
);
apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The first ${data?.length} Apt(s) that were created in the last ${daysCreated} `,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,124 @@
import axios from "axios";
import { and, desc, eq, gte, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { odToken } from "./opendock.utils.js";
const r = Router();
r.get("/", async (req, res) => {
//const limit
const daysCreated = req.query.daysCreated ?? 30;
const { data } = await tryCatch(
db
.select()
.from(opendockApt)
.where(
and(
gte(
opendockApt.upd_date,
sql.raw(`NOW() - INTERVAL '${daysCreated} days'`),
),
eq(opendockApt.status, "active"),
),
)
.orderBy(desc(opendockApt.upd_date))
.limit(500),
);
apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The first ${data?.length} Apt(s) that were created in the last ${daysCreated} `,
data: data ?? [],
status: 200,
});
});
r.delete("/:id", async (req, res) => {
const { id } = req.params;
const { data: releaseInfo } = (await tryCatch(
db
.select()
.from(opendockApt)
.where(eq(opendockApt.id, id as string)),
)) as any;
if (releaseInfo.length === 0) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: "Invalid release id passed over please try again",
data: [],
status: 400,
});
}
try {
const response = await axios.delete(
`${process.env.OPENDOCK_URL}/appointment/${releaseInfo[0].appointment.id}`,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
// {
// hardDelete: true,
// },
);
if (response.status === 400) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: response.data.data.message,
data: [],
status: 400,
});
}
// update the release in the db leaving as insert just in-case something weird happened
const { data } = await tryCatch(
db
.delete(opendockApt)
.where(eq(opendockApt.id, id as string))
.returning(),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The release was deleted, this is un unrecoverable`,
data: data as any,
status: 200,
});
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: `An error deleting the release in opendock`,
data: e.response.data,
status: 400,
});
}
});
export default r;

View File

@@ -0,0 +1,15 @@
use AlplaPROD_test1
SELECT plant=(SELECT Wert FROM dbo.T_SystemParameter (nolock) WHERE (Bezeichnung = 'Werkskuerzel')),
plantName=(SELECT Wert FROM dbo.T_SystemParameter AS T_SystemParameter (nolock) WHERE (Bezeichnung = 'Mandant-intern')),*
from
(Select IdBestellung as 'Purchase order number',
IdArtikelVarianten AS AV,
BestellMenge,
BestellMengeVPK,
PreisProEinheit,
convert(varchar,Lieferdatum,23) as deliveryDate,
ROW_NUMBER() over(partition by IdArtikelVarianten order by Lieferdatum desc) rn
from dbo.V_Bestellpositionen_PURCHASE (nolock)
where PositionsStatus = '7' or PositionsStatus = '6' or PositionsStatus = '5' and convert(varchar,Lieferdatum,23) > DATEADD(year, -5, GetDate()) )a
where rn = 1

View File

@@ -0,0 +1,14 @@
use AlplaPROD_test1
select * from
(select IdArtikelvarianten as av,
VKPreis as salesPrice,
MPB, FWMPAlpla,
FWMPB,
ROW_NUMBER() over(partition by IdArtikelVarianten order by gueltigabdatum desc) rn,
convert(date, gueltigabdatum, 120) as validDate
from dbo.T_HistoryVK (nolock)
where convert(date, gueltigabdatum, 120) <= '[date]' and StandardKunde = 1) a
where rn =1
order by av asc,
validDate desc

View File

@@ -0,0 +1,7 @@
use alplaprod_test1
SELECT IdArtikelvarianten AS AV,
Menge AS Quantity,
CONVERT(DATE, BuchDatum) AS Prod_Date
FROM dbo.T_LBW (nolock)
WHERE BuchDatum BETWEEN '[startDate]' AND '[endDate]' ORDER BY BuchDatum DESC

View File

@@ -0,0 +1,40 @@
use AlplaPROD_test1
declare @start_date nvarchar(30) = '[startDate] '
declare @end_date nvarchar(30) = '[endDate] '
select T_Wareneingaenge.IdBestellung AS Purchase_order,
T_Adressen.IdAdressen,
T_Adressen.Bezeichnung,
T_Wareneingaenge.IdArtikelVarianten AS AV,
V_Artikel.Alias,
x.Bemerkung AS Remark,
T_Wareneingaenge.Bemerkung AS Purchase_Remark,
x.Add_User,
CONVERT(DATE, x.Add_Date) AS Received_Date,
x.IdWareneingangPlanung,
T_Wareneingaenge.SollMenge As Ordered_QTY,
x.EntladeMenge As Received_QTY,
case when T_Adressen.Bezeichnung LIKE '%Alpla%' Then 'AlplaPlant' Else 'Supplier' End AS
Supplier,
x.Typ as incoming_goods_type
from dbo.T_WareneingangPlanungen (nolock) as x
join
dbo.T_Wareneingaenge (nolock) on
x.IdWareneingang=
dbo.T_Wareneingaenge.IdWareneingang
join
dbo.V_Artikel (nolock) on
dbo.T_Wareneingaenge.IdArtikelVarianten=
dbo.V_Artikel.IdArtikelvarianten
join
dbo.T_Adressen (nolock) on dbo.T_Wareneingaenge.IdLieferantAdresse =
dbo.T_Adressen.IdAdressen
where x.add_date between @start_date + (select top(1) CONVERT(char(8), StartDate, 108) as startTime from [test1_AlplaPROD2.0_Read].masterData.ShiftDefinition (nolock) where TeamNumber = 1)
AND @end_date + (select top(1) CONVERT(char(8), StartDate, 108) as startTime from [test1_AlplaPROD2.0_Read].masterData.ShiftDefinition (nolock) where TeamNumber = 1)
order by x.add_date desc

View File

@@ -0,0 +1,13 @@
select IdArtikelVarianten,
ArtikelVariantenAlias,
IdRezeptur,
Menge,
IdBuchungsGrund,
Buchungsdatum,
ProduktionsLos,
IdReinheit,
ReinheitBez, HerkunftBez
from alplaprod_test1.[dbo].[V_AbfallLagerBuchungen] (nolock)
where Buchungsdatum between '[startDate] ' + (select top(1) CONVERT(char(8), StartDate, 108) as startTime from [test1_AlplaPROD2.0_Read].masterData.ShiftDefinition (nolock) where TeamNumber = 1)
and '[endDate] ' + (select top(1) CONVERT(char(8), StartDate, 108) as startTime from [test1_AlplaPROD2.0_Read].masterData.ShiftDefinition (nolock) where TeamNumber = 1)
and IdBuchungsGrund in (140, 240) and BuchungsTyp = 1

View File

@@ -0,0 +1,15 @@
select IdArtikelVarianten AS AV,
ArtikelVariantenAlias AS AVDescription,
convert(date,AbrufLadeDatum,23) As DeliveryDate,
idlieferadresse AS DeliveryAddress,
LieferAdressBez,
AuftragsNummer AS PO_Number,
IdAuftragsPosition AS LineITEM,
IdAuftragsAbruf AS ReleaseNumber,
AbrufMengeVPK AS PalletsRequested,
AbrufMenge AS PiecesRequested,
GelieferteMengeVPK AS DeliveredPallets,
GelieferteMenge AS DeliveredQTY,
case when LieferAdressBez Like '%alpla%' Then 'AlplaPlant' ELSE 'Customer' End as CustomerType
from alplaprod_test1.dbo.V_TrackerAuftragsAbrufe (nolock)
where AbrufLadeDatum between '[startDate]' and '[endDate]'

View File

@@ -1,11 +1,80 @@
SELECT count(*) as activated
FROM [test1_AlplaPROD2.0_Read].[support].[FeatureActivation]
where feature in (108,7)
where feature in (7)
/*
as more features get activated and need to have this checked to include the new endpoints add here so we can check this.
108 = waste
7 = warehousing
[DefaultTranslation("Blocking")]
Blocking = 1,
[DefaultTranslation("Users")]
UserManagement = 2,
[DefaultTranslation("Complaint Handling")]
ComplaintHandling = 3,
[DefaultTranslation("Demand Management")]
DemandManagement = 4,
[DefaultTranslation("Issue Material")]
IssueMaterial = 5,
[DefaultTranslation("Production Controlling")]
ProductionControlling = 6,
[DefaultTranslation("Warehousing")]
Warehousing = 7,
[DefaultTranslation("Outbound Deliveries")]
OutboundDeliveries = 8,
[DefaultTranslation("Production Scheduling")]
ProductionScheduling = 9,
[DefaultTranslation("Advanced Scheduling")]
AdvancedScheduling = 10,
[DefaultTranslation("Material Requirements Planning")]
MaterialRequirementsPlanning = 11,
[DefaultTranslation("Production Labelling")]
ProductionLabelling = 12,
[SpecialProcess]
[DefaultTranslation("Accounting")]
Accounting = 100,
[SpecialProcess]
[DefaultTranslation("Irradiation")]
Irradiation = 101,
[SpecialProcess]
[DefaultTranslation("Central Moulds")]
CentralMoulds = 102,
[SpecialProcess]
[DefaultTranslation("Maintenance")]
Maintenance = 103,
[SpecialProcess]
[DefaultTranslation("Disable Manual Bookings")]
DisableManualBookings = 104,
[SpecialProcess]
[DefaultTranslation("Purchasing")]
Purchasing = 105,
[SpecialProcess]
[DefaultTranslation("Tracing")]
Tracing = 106,
[SpecialProcess]
[DefaultTranslation("AlplaERP (D365)")]
AlplaErp = 107,
[SpecialProcess]
[DefaultTranslation("AI chatbot")]
AiChatBot = 108
*/

View File

@@ -5,6 +5,7 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
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 { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js";
@@ -31,4 +32,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app);
setupDockDoorRoutes(baseUrl, app);
setupEomRoutes(baseUrl, app);
};

View File

@@ -2,6 +2,8 @@ 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";
@@ -43,12 +45,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 +92,7 @@ const start = async () => {
startNotifications();
serversChecks();
aggregateRouteHitsForBusinessDay();
setupDbNotifications();
// can be removed at a later date
sqlJobCleanUp();

View File

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

View File

@@ -1,94 +0,0 @@
import { desc } from "drizzle-orm";
import { db } from "../db/db.controller.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";
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: {},
};
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;
},
},
};

View File

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

View File

@@ -1,88 +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)) {
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 };
};

View File

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

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

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

View File

@@ -140,6 +140,17 @@ const servers: NewServerData[] = [
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Iowa City PET",
server: "USIOW1VMS006",
plantToken: "usiow2",
idAddress: "10.75.0.26",
greatPlainsPlantCode: "31",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3_2",
buildNumber: 1,
},
{
name: "Bowling Green 1",
server: "USBOW1VMS006",
@@ -173,6 +184,39 @@ const servers: NewServerData[] = [
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Bowling Green 2",
server: "USBOW2VMS006",
plantToken: "usbow2",
idAddress: "10.106.0.26",
greatPlainsPlantCode: "56",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Kansas City",
server: "USKSC1VMS006",
plantToken: "usksc1",
idAddress: "10.42.9.26",
greatPlainsPlantCode: "85",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Florence",
server: "USFLO1VMS006",
plantToken: "usflo1",
idAddress: "10.203.0.26",
greatPlainsPlantCode: "22",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
];
// notes here for when it comes time to pull in all the server address info [test1_AlplaPROD2.0_Read].[masterData].[Plant] has everything from every server :D

View File

@@ -96,6 +96,16 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
seedVersion: 1,
},
{
name: "eom",
value: "0",
active: true,
description: "Eom processes to capture data",
moduleName: "eom",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings
{

View File

@@ -3,7 +3,7 @@ import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { printerData } from "../db/schema/printers.schema.js";
import { loadUnit } from "../dockdoorScanning/dockdoor.loadUnits.js";
import loadUnit from "../dockdoorScanning/dockdoor.loadUnits.js";
import { createLogger } from "../logger/logger.controller.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
@@ -79,7 +79,6 @@ export const startTCPServer = async () => {
}
// check if its a dock door scanner
// TODO: move to the db and get real info lol
const dockdoorScanners = await db.select().from(dockDoorScanners);
if (dockdoorScanners.some((s) => s.ip === ip.replace("::ffff:", ""))) {

View File

@@ -8,6 +8,7 @@ export const statement = {
logistics: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "update", "delete", "readAll"],
openDock: ["read", "create", "update", "delete"],
warehouse: ["read", "create", "update", "delete"],
notifications: ["read", "create", "update", "delete", "readAll"],
} as const;
@@ -17,17 +18,20 @@ export const user = ac.newRole({
app: ["read", "create"],
notifications: ["read", "create"],
openDock: ["read"],
warehouse: ["read"],
});
export const manager = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
openDock: ["read", "create", "update"],
warehouse: ["read", "create"],
});
export const transport = ac.newRole({
app: ["read", "create", "update"],
openDock: ["read", "create", "update"],
warehouse: ["read", "create"],
});
export const admin = ac.newRole({
@@ -35,6 +39,7 @@ export const admin = ac.newRole({
mobile: ["read", "create", "update"],
user: ["create", "update", "ban"],
openDock: ["read", "create", "update"],
warehouse: ["read", "create", "update"],
});
export const systemAdmin = ac.newRole({
@@ -44,6 +49,7 @@ export const systemAdmin = ac.newRole({
mobile: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "update", "delete", "readAll"],
notifications: ["read", "create", "update", "delete", "readAll"],
warehouse: ["read", "create", "update", "delete"],
openDock: ["read", "create", "update", "delete"],
});

View File

@@ -13,7 +13,14 @@ import {
//import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import * as rawSchema from "../db/schema/auth.schema.js";
import { ac, admin, manager, systemAdmin, user } from "./auth.permissions.js";
import {
ac,
admin,
manager,
systemAdmin,
transport,
user,
} from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js";
@@ -164,6 +171,7 @@ export const auth = betterAuth({
admin,
user,
manager,
transport,
systemAdmin,
},
}),

View File

@@ -17,7 +17,8 @@ export interface ReturnHelper<T = unknown[]> {
| "logistics"
| "admin"
| "mobile"
| "dockdoor";
| "dockdoor"
| "eom";
subModule: string;
level: "info" | "error" | "debug" | "fatal" | "warn";

View File

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

View File

@@ -40,6 +40,11 @@ export default function TransportationBar() {
icon: link,
url: "/transportation/opendock",
},
{
title: "Active releases",
icon: link,
url: "/transportation/opendock/releases",
},
],
},
];

View File

@@ -0,0 +1,94 @@
//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 {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from "../ui/sidebar";
export default function WarehouseBar() {
// const { data: canCreate = false } = useQuery(
// permissionQuery({
// warehouse: ["read"],
// }),
// );
const { setOpen } = useSidebar();
const items = [
{
title: "Dock Door Scanning",
url: "/warehouse",
//icon,
isActive: true,
items: [
{
title: "DockDoorScanning",
icon: link,
url: "/warehouse/dockdoorscanning",
},
],
},
];
return (
<SidebarGroup>
<SidebarGroupLabel>Warehouse</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<div key={item.title}>
{item.isActive && (
<Collapsible
asChild
//defaultOpen={isNotifications}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.title}
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<Link
to={subItem.url}
onClick={() => setOpen(false)}
>
<subItem.icon />
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)}
</div>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@@ -13,16 +13,23 @@ import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar";
import MobileBar from "./MobileBar";
import TransportationBar from "./TransportationBar";
import WarehouseBar from "./Warhouse";
export function AppSidebar() {
const { data: session } = useSession();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
const { data: canRead = false } = useQuery(
const { data: canReadOpenDock = false } = useQuery(
permissionQuery({
openDock: ["read"],
}),
);
// const { data: canReadWarehouse = false } = useQuery(
// permissionQuery({
// warehouse: ["read"],
// }),
// );
return (
<Sidebar
variant="sidebar"
@@ -42,7 +49,11 @@ export function AppSidebar() {
{!isLoading &&
settings.filter((n: any) => n.name === "opendock_sync")[0]
?.active &&
canRead && <TransportationBar />}
canReadOpenDock && <TransportationBar />}
{!isLoading &&
settings.filter((n: any) => n.name === "dockDoorScanning")[0]
?.active && <WarehouseBar />}
{session &&
(session.user.role === "admin" ||

View File

@@ -0,0 +1,49 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -1,93 +1,137 @@
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;
};
export function useSocketRoom<T>(
roomId: string,
enabled = true,
getKey?: (item: T) => string | number,
) {
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;
}
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(() => {
if (!roomId || !enabled) return;
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".
if (!actualRoomId) return;
if (payload.roomId !== actualRoomId) return;
if (payload.type === "snapshot") {
setData(payload.payloads);
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");
}
if (!socket.connected) {
socket.connect();
}
socket.on("connect", handleConnect);
socket.on("connect", joinRoom);
socket.on("room-joined", handleJoined);
socket.on("room-update", handleUpdate);
socket.on("room-error", handleError);
// If already connected, join immediately
if (!socket.connected && socket.disconnected) {
socket.connect();
}
// 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, enabled]);
}, [room, joinPayload, actualRoomId]);
return { data, info, clearRoom };
return {
data,
info,
clearRoom,
};
}
/*
const isDockDoorPage = location.pathname.startsWith("/dockdoor");
useSocketRoom(
dockId ? `dockdoor:${dockId}` : null,
isDockDoorPage,
);
*/

View File

@@ -6,7 +6,14 @@ import {
} from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import { ac, admin, manager, systemAdmin, user } from "./auth-permissions";
import {
ac,
admin,
manager,
systemAdmin,
transport,
user,
} from "./auth-permissions";
export const authClient = createAuthClient({
baseURL: `${window.location.origin}/lst/api/auth`,
@@ -17,6 +24,7 @@ export const authClient = createAuthClient({
admin,
user,
manager,
transport,
systemAdmin,
},
}),

View File

@@ -14,6 +14,7 @@ export const selectableRoles: SelectableRole[] = [
{ label: "User", value: "user" },
{ label: "Manager", value: "manager" },
{ label: "Transport", value: "transport" },
{ label: "Warehouse", value: "warehouse" },
{ label: "Admin", value: "admin" },
{ label: "System Admin", value: "systemAdmin" },
];
@@ -25,6 +26,7 @@ export const statement = {
logistics: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "update", "delete", "readAll"],
openDock: ["read", "create", "update", "delete"],
warehouse: ["read", "create", "update", "delete"],
notifications: ["read", "create", "update", "delete", "readAll"],
} as const;
@@ -34,17 +36,20 @@ export const user = ac.newRole({
app: ["read", "create"],
notifications: ["read", "create"],
openDock: ["read"],
warehouse: ["read"],
});
export const manager = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
openDock: ["read", "create", "update"],
warehouse: ["read", "create"],
});
export const transport = ac.newRole({
app: ["read", "create", "update"],
openDock: ["read", "create", "update"],
warehouse: ["read", "create"],
});
export const admin = ac.newRole({
@@ -52,6 +57,7 @@ export const admin = ac.newRole({
mobile: ["read", "create", "update"],
user: ["create", "update", "ban"],
openDock: ["read", "create", "update"],
warehouse: ["read", "create", "update"],
});
export const systemAdmin = ac.newRole({
@@ -61,6 +67,7 @@ export const systemAdmin = ac.newRole({
mobile: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "update", "delete", "readAll"],
notifications: ["read", "create", "update", "delete", "readAll"],
warehouse: ["read", "create", "update", "delete"],
openDock: ["read", "create", "update", "delete"],
});

View File

@@ -0,0 +1,21 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getActiveLoadingOrders() {
return queryOptions({
queryKey: ["getActiveLoadingOrders"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
const { data } = await api.get("/dockDoor/activeLoadingOrders");
if (!data.success) {
throw new Error(data.message ?? "Failed to load articles");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,25 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getActiveDockScanners() {
return queryOptions({
queryKey: ["getActiveDockScanners"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await api.get("/dockDoor/scanners");
if (!data.success) {
throw new Error(data.message ?? "Failed to load articles");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,23 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function opendockApt() {
return queryOptions({
queryKey: ["opendockApt"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await api.get("/opendock?daysCreated=90");
return data.data;
};

View File

@@ -5,6 +5,7 @@ type RuntimeConfig = {
appVersion: string;
umamiHost: string;
umamiWebsiteId: string;
timezone: string;
};
declare global {
@@ -23,6 +24,7 @@ export const runtimeConfig: RuntimeConfig = {
appVersion: window.LST_CONFIG?.appVersion ?? "dev",
umamiHost: window.LST_CONFIG?.umamiHost ?? "",
umamiWebsiteId: window.LST_CONFIG?.umamiWebsiteId ?? "",
timezone: window.LST_CONFIG?.timezone ?? "America/Chicago",
};
export function loadUmami() {

View File

@@ -22,10 +22,14 @@ import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications'
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as WarehouseDockdoorscanningIndexRouteImport } from './routes/warehouse/dockdoorscanning/index'
import { Route as TransportationOpendockIndexRouteImport } from './routes/transportation/opendock/index'
import { Route as WarehouseDockdoorscanningDockRouteImport } from './routes/warehouse/dockdoorscanning/$dock'
import { Route as TransportationOpendockReleasesRouteImport } from './routes/transportation/opendock/releases'
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile'
import { Route as WarehouseDockdoorscanningScansDockScansRouteImport } from './routes/warehouse/dockdoorscanning/scans/$dockScans'
const ForbiddenRoute = ForbiddenRouteImport.update({
id: '/forbidden',
@@ -92,12 +96,30 @@ const authLoginRoute = authLoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const WarehouseDockdoorscanningIndexRoute =
WarehouseDockdoorscanningIndexRouteImport.update({
id: '/warehouse/dockdoorscanning/',
path: '/warehouse/dockdoorscanning/',
getParentRoute: () => rootRouteImport,
} as any)
const TransportationOpendockIndexRoute =
TransportationOpendockIndexRouteImport.update({
id: '/transportation/opendock/',
path: '/transportation/opendock/',
getParentRoute: () => rootRouteImport,
} as any)
const WarehouseDockdoorscanningDockRoute =
WarehouseDockdoorscanningDockRouteImport.update({
id: '/warehouse/dockdoorscanning/$dock',
path: '/warehouse/dockdoorscanning/$dock',
getParentRoute: () => rootRouteImport,
} as any)
const TransportationOpendockReleasesRoute =
TransportationOpendockReleasesRouteImport.update({
id: '/transportation/opendock/releases',
path: '/transportation/opendock/releases',
getParentRoute: () => rootRouteImport,
} as any)
const authUserSignupRoute = authUserSignupRouteImport.update({
id: '/(auth)/user/signup',
path: '/user/signup',
@@ -113,6 +135,12 @@ const authUserProfileRoute = authUserProfileRouteImport.update({
path: '/user/profile',
getParentRoute: () => rootRouteImport,
} as any)
const WarehouseDockdoorscanningScansDockScansRoute =
WarehouseDockdoorscanningScansDockScansRouteImport.update({
id: '/warehouse/dockdoorscanning/scans/$dockScans',
path: '/warehouse/dockdoorscanning/scans/$dockScans',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -131,7 +159,11 @@ export interface FileRoutesByFullPath {
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute
'/transportation/opendock/releases': typeof TransportationOpendockReleasesRoute
'/warehouse/dockdoorscanning/$dock': typeof WarehouseDockdoorscanningDockRoute
'/transportation/opendock/': typeof TransportationOpendockIndexRoute
'/warehouse/dockdoorscanning/': typeof WarehouseDockdoorscanningIndexRoute
'/warehouse/dockdoorscanning/scans/$dockScans': typeof WarehouseDockdoorscanningScansDockScansRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
@@ -150,7 +182,11 @@ export interface FileRoutesByTo {
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute
'/transportation/opendock/releases': typeof TransportationOpendockReleasesRoute
'/warehouse/dockdoorscanning/$dock': typeof WarehouseDockdoorscanningDockRoute
'/transportation/opendock': typeof TransportationOpendockIndexRoute
'/warehouse/dockdoorscanning': typeof WarehouseDockdoorscanningIndexRoute
'/warehouse/dockdoorscanning/scans/$dockScans': typeof WarehouseDockdoorscanningScansDockScansRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -170,7 +206,11 @@ export interface FileRoutesById {
'/(auth)/user/profile': typeof authUserProfileRoute
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
'/(auth)/user/signup': typeof authUserSignupRoute
'/transportation/opendock/releases': typeof TransportationOpendockReleasesRoute
'/warehouse/dockdoorscanning/$dock': typeof WarehouseDockdoorscanningDockRoute
'/transportation/opendock/': typeof TransportationOpendockIndexRoute
'/warehouse/dockdoorscanning/': typeof WarehouseDockdoorscanningIndexRoute
'/warehouse/dockdoorscanning/scans/$dockScans': typeof WarehouseDockdoorscanningScansDockScansRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -191,7 +231,11 @@ export interface FileRouteTypes {
| '/user/profile'
| '/user/resetpassword'
| '/user/signup'
| '/transportation/opendock/releases'
| '/warehouse/dockdoorscanning/$dock'
| '/transportation/opendock/'
| '/warehouse/dockdoorscanning/'
| '/warehouse/dockdoorscanning/scans/$dockScans'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -210,7 +254,11 @@ export interface FileRouteTypes {
| '/user/profile'
| '/user/resetpassword'
| '/user/signup'
| '/transportation/opendock/releases'
| '/warehouse/dockdoorscanning/$dock'
| '/transportation/opendock'
| '/warehouse/dockdoorscanning'
| '/warehouse/dockdoorscanning/scans/$dockScans'
id:
| '__root__'
| '/'
@@ -229,7 +277,11 @@ export interface FileRouteTypes {
| '/(auth)/user/profile'
| '/(auth)/user/resetpassword'
| '/(auth)/user/signup'
| '/transportation/opendock/releases'
| '/warehouse/dockdoorscanning/$dock'
| '/transportation/opendock/'
| '/warehouse/dockdoorscanning/'
| '/warehouse/dockdoorscanning/scans/$dockScans'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -249,7 +301,11 @@ export interface RootRouteChildren {
authUserProfileRoute: typeof authUserProfileRoute
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
authUserSignupRoute: typeof authUserSignupRoute
TransportationOpendockReleasesRoute: typeof TransportationOpendockReleasesRoute
WarehouseDockdoorscanningDockRoute: typeof WarehouseDockdoorscanningDockRoute
TransportationOpendockIndexRoute: typeof TransportationOpendockIndexRoute
WarehouseDockdoorscanningIndexRoute: typeof WarehouseDockdoorscanningIndexRoute
WarehouseDockdoorscanningScansDockScansRoute: typeof WarehouseDockdoorscanningScansDockScansRoute
}
declare module '@tanstack/react-router' {
@@ -345,6 +401,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authLoginRouteImport
parentRoute: typeof rootRouteImport
}
'/warehouse/dockdoorscanning/': {
id: '/warehouse/dockdoorscanning/'
path: '/warehouse/dockdoorscanning'
fullPath: '/warehouse/dockdoorscanning/'
preLoaderRoute: typeof WarehouseDockdoorscanningIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/transportation/opendock/': {
id: '/transportation/opendock/'
path: '/transportation/opendock'
@@ -352,6 +415,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TransportationOpendockIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/warehouse/dockdoorscanning/$dock': {
id: '/warehouse/dockdoorscanning/$dock'
path: '/warehouse/dockdoorscanning/$dock'
fullPath: '/warehouse/dockdoorscanning/$dock'
preLoaderRoute: typeof WarehouseDockdoorscanningDockRouteImport
parentRoute: typeof rootRouteImport
}
'/transportation/opendock/releases': {
id: '/transportation/opendock/releases'
path: '/transportation/opendock/releases'
fullPath: '/transportation/opendock/releases'
preLoaderRoute: typeof TransportationOpendockReleasesRouteImport
parentRoute: typeof rootRouteImport
}
'/(auth)/user/signup': {
id: '/(auth)/user/signup'
path: '/user/signup'
@@ -373,6 +450,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authUserProfileRouteImport
parentRoute: typeof rootRouteImport
}
'/warehouse/dockdoorscanning/scans/$dockScans': {
id: '/warehouse/dockdoorscanning/scans/$dockScans'
path: '/warehouse/dockdoorscanning/scans/$dockScans'
fullPath: '/warehouse/dockdoorscanning/scans/$dockScans'
preLoaderRoute: typeof WarehouseDockdoorscanningScansDockScansRouteImport
parentRoute: typeof rootRouteImport
}
}
}
@@ -393,7 +477,12 @@ const rootRouteChildren: RootRouteChildren = {
authUserProfileRoute: authUserProfileRoute,
authUserResetpasswordRoute: authUserResetpasswordRoute,
authUserSignupRoute: authUserSignupRoute,
TransportationOpendockReleasesRoute: TransportationOpendockReleasesRoute,
WarehouseDockdoorscanningDockRoute: WarehouseDockdoorscanningDockRoute,
TransportationOpendockIndexRoute: TransportationOpendockIndexRoute,
WarehouseDockdoorscanningIndexRoute: WarehouseDockdoorscanningIndexRoute,
WarehouseDockdoorscanningScansDockScansRoute:
WarehouseDockdoorscanningScansDockScansRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -1,6 +1,17 @@
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";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
export const Route = createFileRoute("/admin/logs")({
beforeLoad: async ({ location }) => {
@@ -37,117 +48,169 @@ interface LogEntry {
[key: string]: any; // catch any extra fields
}
function LevelBadge({ level }: { level: number }) {
const config: Record<number, { label: string; className: string }> = {
10: { label: "TRACE", className: "bg-gray-100 text-gray-600" },
20: { label: "DEBUG", className: "bg-blue-100 text-blue-700" },
30: { label: "INFO", className: "bg-green-100 text-green-700" },
40: { label: "WARN", className: "bg-yellow-100 text-yellow-700" },
50: { label: "ERROR", className: "bg-red-100 text-red-700" },
60: { label: "FATAL", className: "bg-purple-100 text-purple-700" },
function LevelBadge({ level }: { level: string }) {
const config: Record<string, { label: string; className: string }> = {
trace: { label: "TRACE", className: "bg-gray-100 text-gray-600" },
debug: { label: "DEBUG", className: "bg-blue-50 text-blue-700" },
info: { label: "INFO", className: "bg-green-50 text-green-700" },
warn: { label: "WARN", className: "bg-yellow-100 text-yellow-700" },
error: { label: "ERROR", className: "bg-red-50 text-red-700" },
fatal: { label: "FATAL", className: "bg-purple-100 text-purple-700" },
};
const { label, className } = config[level] ?? {
label: String(level),
className: "bg-gray-100",
};
const badge = config[level];
return (
<span className={`px-2 py-0.5 rounded text-xs font-medium ${className}`}>
{label}
</span>
<Badge
className={`rounded px-2 py-0.5 text-xs font-medium ${
badge?.className ?? "bg-gray-100 text-gray-600"
}`}
>
{badge?.label ?? level}
</Badge>
);
}
function RouteComponent() {
const { data: logs, info: logsInfo } = useSocketRoom<LogEntry>("logs");
//const { user } = Route.useRouteContext();
//const router = useRouter();
// const [logs, setLogs] = useState<LogEntry[]>([]);
// const [logsInfo, setLogInfo] = useState(
// "No logs yet — join the room to start receiving",
// );
// 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" />,
filterFn: "includesString",
cell: (i) =>
formatInTimeZone(
i.getValue(),
`${window.LST_CONFIG?.timezone}`,
"MM/dd/yyyy HH:mm:ss",
),
}),
columnHelper.accessor("level", {
header: ({ column }) => (
<SearchableHeader column={column} title="Level" searchable={true} />
),
filterFn: "includesString",
cell: (i) => <LevelBadge level={i.getValue()} />,
}),
columnHelper.accessor("module", {
header: ({ column }) => (
<SearchableHeader column={column} title="Module" />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("subModule", {
header: ({ column }) => (
<SearchableHeader column={column} title="Submodule" />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("hostname", {
header: ({ column }) => (
<SearchableHeader column={column} title="Client" />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("message", {
header: ({ column }) => (
<SearchableHeader column={column} title="Message" />
),
filterFn: "includesString",
cell: (i) => {
return i.getValue().length > 50 ? (
<Tooltip>
<TooltipTrigger>
<p>{i.getValue().slice(0, 50)}...</p>
</TooltipTrigger>
<TooltipContent>
<p className="text-wrap max-w-48">{i.getValue()}</p>
</TooltipContent>
</Tooltip>
) : (
i.getValue()
);
},
}),
columnHelper.accessor("stack", {
header: ({ column }) => (
<SearchableHeader column={column} title="Stack" />
),
filterFn: "includesString",
cell: (i) => {
const stack = i.row.original.stack;
if (!stack) return <span className="text-muted-foreground"></span>;
// useEffect(() => {
// // Connect if not already connected
// if (!socket.connected) {
// socket.connect();
// }
// socket.on("connect", () => {
// socket.emit("join-room", "logs");
// });
// socket.emit("join-room", "logs");
// socket.on(
// "room-update",
// (data: { payloads: LogEntry[]; roomId: string }) => {
// setLogs((prev) => [...data.payloads, ...prev]);
// },
// );
// socket.on("room-error", (data) => {
// setLogInfo(data.message);
// });
// // socket.on("logs", (data) => {
// // console.log(data);
// // setLogs((prev) => [...data.payloads, ...prev]);
// // });
// // Cleanup listeners on unmount
// return () => {
// socket.emit("leave-room", "logs");
// socket.off("room-update");
// socket.off("room-error");
// socket.off("logs");
// };
// }, []);
return (
<Tooltip>
<TooltipTrigger>
<p className="max-w-[350px] truncate text-left font-mono text-xs text-muted-foreground hover:text-foreground">
{stack.length !== 0 ? "Hover to see stack error" : "-"}
</p>
</TooltipTrigger>
<TooltipContent
side="left"
align="start"
className="max-w-[900px] bg-zinc-950 text-zinc-50"
>
<pre className="max-h-[500px] overflow-auto whitespace-pre-wrap rounded-md p-3 text-xs leading-relaxed">
<code>{JSON.stringify(stack, null, 2)}</code>
</pre>
</TooltipContent>
</Tooltip>
);
},
}),
];
return (
<div>
{/* Log Table */}
<div className="rounded border overflow-auto max-h-[600px]">
<table className="w-full text-sm">
<thead className="bg-muted sticky top-0">
<tr>
<th className="text-left px-3 py-2">Time</th>
<th className="text-left px-3 py-2">Level</th>
<th className="text-left px-3 py-2">Module</th>
<th className="text-left px-3 py-2">Host</th>
<th className="text-left px-3 py-2">Message</th>
</tr>
</thead>
<tbody>
{logs.length === 0 ? (
<tr>
<td
colSpan={6}
className="text-center py-6 text-muted-foreground"
>
{logsInfo}
</td>
</tr>
) : (
logs.map((log, i) => (
<tr
key={`${log.id}-${i}`}
className="border-t hover:bg-muted/50"
>
<td className="px-3 py-2 whitespace-nowrap">
{new Date(log.createdAt).toLocaleTimeString()}
</td>
<td className="px-3 py-2">
<LevelBadge level={log.level} />
</td>
<td className="px-3 py-2">{log.module}</td>
<td className="px-3 py-2">{log.hostname}</td>
<td className="px-3 py-2 max-w-sm truncate">{log.message}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
// <div>
// {/* Log Table */}
// <div className="rounded border overflow-auto max-h-[600px]">
// <table className="w-full text-sm">
// <thead className="bg-muted sticky top-0">
// <tr>
// <th className="text-left px-3 py-2">Time</th>
// <th className="text-left px-3 py-2">Level</th>
// <th className="text-left px-3 py-2">Module</th>
// <th className="text-left px-3 py-2">Host</th>
// <th className="text-left px-3 py-2">Message</th>
// </tr>
// </thead>
// <tbody>
// {logs.length === 0 ? (
// <tr>
// <td
// colSpan={6}
// className="text-center py-6 text-muted-foreground"
// >
// {logsInfo}
// </td>
// </tr>
// ) : (
// logs.map((log, i) => (
// <tr
// key={`${log.id}-${i}`}
// className="border-t hover:bg-muted/50"
// >
// <td className="px-3 py-2 whitespace-nowrap">
// {new Date(log.createdAt).toLocaleTimeString()}
// </td>
// <td className="px-3 py-2">
// <LevelBadge level={log.level} />
// </td>
// <td className="px-3 py-2">{log.module}</td>
// <td className="px-3 py-2">{log.hostname}</td>
// <td className="px-3 py-2 max-w-sm truncate">{log.message}</td>
// </tr>
// ))
// )}
// </tbody>
// </table>
// </div>
// </div>
<LstTable data={logs} columns={column} pageSize={50} />
);
}

View File

@@ -170,11 +170,11 @@ const NotificationTable = () => {
try {
const res = await api.patch(
`/lst/api/notification/${i.row.original.id}`,
`/notification/${i.row.original.id}`,
{
active: !activeToggle,
},
{ withCredentials: true },
);
if (res.data.success) {

View File

@@ -180,7 +180,7 @@ const ScanUserTable = () => {
try {
const res = await api.delete(
`/lst/api/mobile/auth/user/${i.row.original.id}`,
`/mobile/auth/user/${i.row.original.id}`,
{
withCredentials: true,

Some files were not shown because too many files have changed in this diff Show More