Compare commits
43 Commits
2a648f6306
...
v0.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| d85f08cb19 | |||
| 15c939ebe8 | |||
| 9ff428f5ea | |||
| 2e460c7f8a | |||
| 7c4c5f980a | |||
| 86e1237509 | |||
| 706ab8b448 | |||
| 9440b44f3b | |||
| a2d9a6c127 | |||
| c0a7d4a125 | |||
| 8fcb2c66ed | |||
| 8fc3129f7d | |||
| 05c553e927 | |||
| 6eaae0f537 | |||
| 3ad84dab71 | |||
| 5a7046bacd | |||
| cfc497c1f2 | |||
| 7bbdd4e555 | |||
| e909e8deec | |||
| 4249e90307 | |||
| 2f495739e6 | |||
| 7d722c4aac | |||
| 5f2148f5f0 | |||
| 7671172d97 | |||
| 6dd1a5b332 | |||
| 865f280cfa | |||
| a717260b8d | |||
| 4464cea022 | |||
| 55418e2f09 | |||
| 5281118596 | |||
| f635415b75 | |||
| 40edbc3eae | |||
| 2558b2e5bb | |||
| 45a0dee9ca | |||
| bb7931d6c8 | |||
| 4f848bb649 | |||
| f8335f5217 | |||
| 2a35381fe4 | |||
| e6b92aeb10 | |||
| da87e2e1d3 | |||
| 3ef0f230dd | |||
| 78f7b8a179 | |||
| 1a3d8a7ddc |
79
CHANGELOG.md
79
CHANGELOG.md
@@ -1,5 +1,84 @@
|
||||
# All Changes to LST can be found below.
|
||||
|
||||
## [0.1.0-alpha.3](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.2...v0.1.0-alpha.3) (2026-06-10)
|
||||
|
||||
|
||||
### 🌟 Enhancements
|
||||
|
||||
* **eom:** migrated eom endpoints from old version validated working ([e909e8d](https://git.tuffraid.net/cowch/lst_v3/commits/e909e8deecb54a3e4c39789609b0aa7435b9e08a))
|
||||
* **mobile:** added auto download of latest ([6d0fb8a](https://git.tuffraid.net/cowch/lst_v3/commits/6d0fb8aee45c8b5c56ccd7d8a010e1dc803408bf))
|
||||
* **mobile:** dock door scanning backend added ([fe0b157](https://git.tuffraid.net/cowch/lst_v3/commits/fe0b1573f3ba6fd220f181088b994588c52af139)), closes [#12](https://git.tuffraid.net/cowch/lst_v3/issues/12)
|
||||
* **opendock:** added delete button in the article tab ([4464cea](https://git.tuffraid.net/cowch/lst_v3/commits/4464cea022ba48744b884b83fef0fc3f3421dea5)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
|
||||
* **opendock:** added in a new article link system ([bb7931d](https://git.tuffraid.net/cowch/lst_v3/commits/bb7931d6c8d0f5ce4065955491e9ee1247b5e92d))
|
||||
* **warehousing:** ppoo monitoring added ([8b07694](https://git.tuffraid.net/cowch/lst_v3/commits/8b076949a7f8e723bc87619f729082d2c1991b2d)), closes [#13](https://git.tuffraid.net/cowch/lst_v3/issues/13)
|
||||
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
* **app:** type in the templates... they all looked the same ([2a648f6](https://git.tuffraid.net/cowch/lst_v3/commits/2a648f63064a300b0a2888bae3322b857af6a238))
|
||||
* **dock door scanning:** correction to how the data is posted ([2f49573](https://git.tuffraid.net/cowch/lst_v3/commits/2f495739e653c462eff7f10ff6343d9572f25563))
|
||||
* **dockscanner:** removed console log ([4249e90](https://git.tuffraid.net/cowch/lst_v3/commits/4249e90307ba1a1992753803e1dc3ab7dd7ac95e))
|
||||
* **eom:** removed un needed imports ([05c553e](https://git.tuffraid.net/cowch/lst_v3/commits/05c553e9279c6e8384d61073781bf915733b0ab5))
|
||||
* **logistics:** historical checks for no data errors when feature is activeed ([4f848bb](https://git.tuffraid.net/cowch/lst_v3/commits/4f848bb649f350c9d370daa09c6fc48f7b76e2e2))
|
||||
* **mobile users:** corrected and endpoint that prevented us from change the pin number ([347edb7](https://git.tuffraid.net/cowch/lst_v3/commits/347edb7078fb4ce959975cd968a6f026bacc98bf))
|
||||
* **mobile:** scan log incorrect user ref ([9c0ef1f](https://git.tuffraid.net/cowch/lst_v3/commits/9c0ef1f5dfa13e8f7e1f72d47d0d5842b3da3c87))
|
||||
* **mobile:** temp removed the autodownload as its causing issues ([3ef0f23](https://git.tuffraid.net/cowch/lst_v3/commits/3ef0f230ddbaef4c3d738cc531e7afee25b210dd))
|
||||
* **mobile:** ui over lapping ([db28635](https://git.tuffraid.net/cowch/lst_v3/commits/db28635c8c260d0f378e109755d32201acdb2328)), closes [#25](https://git.tuffraid.net/cowch/lst_v3/issues/25)
|
||||
* **notifications:** missed api converstion in the front end for updating ([5281118](https://git.tuffraid.net/cowch/lst_v3/commits/52811185965cb0fe4c9b42e73c447b67e917a5ca))
|
||||
* **opendock:** changed the header of delete to be properly named ([865f280](https://git.tuffraid.net/cowch/lst_v3/commits/865f280cfaf82a7126ca4b57d658aed50cf19a73)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
|
||||
* **opendock:** correction to article link success on delete ([a717260](https://git.tuffraid.net/cowch/lst_v3/commits/a717260b8d5cf72534b055721de2b79901506875)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
|
||||
* **opendock:** ref wrong field oops ([f635415](https://git.tuffraid.net/cowch/lst_v3/commits/f635415b751e11d0e7beb557d18b83915155428a))
|
||||
* **scanner:** corrected teh endpoint to delete the user if needed ([6dd1a5b](https://git.tuffraid.net/cowch/lst_v3/commits/6dd1a5b332a853f0e2c7226ac1f64e403ef752d7))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* **app:** updated last updated to the readme to show current progress ([78f7b8a](https://git.tuffraid.net/cowch/lst_v3/commits/78f7b8a179d078fc1b2740fd2bf4af71a3df8292))
|
||||
* **app:** updated readme ([1a3d8a7](https://git.tuffraid.net/cowch/lst_v3/commits/1a3d8a7ddcb7a2e51da4b77968ffdefb7644a0b1))
|
||||
|
||||
|
||||
### 🛠️ Code Refactor
|
||||
|
||||
* **backend:** dock door scanning socket and perms ([f8335f5](https://git.tuffraid.net/cowch/lst_v3/commits/f8335f5217c339a7dc09883fa8ba9c60aa4c35b1))
|
||||
* **datamart:** if added to query will include plant token now ([7d722c4](https://git.tuffraid.net/cowch/lst_v3/commits/7d722c4aac16bc1d2cfbfbc70539783f34003f77))
|
||||
* **db:** added in notifications vs pulling from the db makes it easier on the system ([706ab8b](https://git.tuffraid.net/cowch/lst_v3/commits/706ab8b448aafb81da94a76fbf8d3d400cba616b))
|
||||
* **db:** added timezone check in so it comes over correct based on the backend timezone ([2558b2e](https://git.tuffraid.net/cowch/lst_v3/commits/2558b2e5bb68be7a3f46de09bb509c99423adeb6))
|
||||
* **dock door scanning:** fixes and final writes for the intial trial went smooth ([86e1237](https://git.tuffraid.net/cowch/lst_v3/commits/86e1237509b81722dee7b42762d0bfced8d26fa3))
|
||||
* **dock scanner:** more work on dock scanner and semi finished ([6eaae0f](https://git.tuffraid.net/cowch/lst_v3/commits/6eaae0f5378e39c4002dadd8325833698dd960e7))
|
||||
* **dockscanning:** more work on the dock door scanning ([7671172](https://git.tuffraid.net/cowch/lst_v3/commits/7671172d975355b5d245a482b481726b49153578)), closes [#12](https://git.tuffraid.net/cowch/lst_v3/issues/12)
|
||||
* **logger:** included error in the stack version so we dont have to remove it all ([da87e2e](https://git.tuffraid.net/cowch/lst_v3/commits/da87e2e1d322a45ca9a7b500d77499fe4c7b999e))
|
||||
* **logs:** refactored to show the submodule and stack as well to make it more easy to watch ([45a0dee](https://git.tuffraid.net/cowch/lst_v3/commits/45a0dee9caae14df639439b905fa81b340791197))
|
||||
* **logs:** socket io setup to be properly logging ([9ff428f](https://git.tuffraid.net/cowch/lst_v3/commits/9ff428f5ea1051e62521632947092f74eb93944d))
|
||||
* **mobile:** intial addin of dockdoor scanning on mobile ([2a35381](https://git.tuffraid.net/cowch/lst_v3/commits/2a35381fe400fc46e6f10c4e72c3ab9ac435e0e5))
|
||||
* **mobile:** moved logout to the tab bar ([bcdf956](https://git.tuffraid.net/cowch/lst_v3/commits/bcdf9566bcf721a085f46ce3befe783eb7b91949))
|
||||
* **mobile:** new error found and added in ([7bbdd4e](https://git.tuffraid.net/cowch/lst_v3/commits/7bbdd4e5552889434aff81ed10ea7c64174f7d34))
|
||||
* **mobile:** setup - added button to go home as it caused confustion ([c15ee07](https://git.tuffraid.net/cowch/lst_v3/commits/c15ee070e7057ad8a5e3d42f51c230d680db9e21))
|
||||
* **new role:** added in warehouse role ([55418e2](https://git.tuffraid.net/cowch/lst_v3/commits/55418e2f098193b0891129a19532608dce2abd9c))
|
||||
* **opend dock:** added in default dock so it uses the default setup as a backup ([8fc3129](https://git.tuffraid.net/cowch/lst_v3/commits/8fc3129f7d687d45dc242aca9a6e71f43d0352ab))
|
||||
* **opendock:** added in check for really using the article link ([cfc497c](https://git.tuffraid.net/cowch/lst_v3/commits/cfc497c1f2254dc12a6e7bb15cfc0dce2a64c1e6))
|
||||
* **opendock:** added in proper complete and ignore of picksheets ([3ad84da](https://git.tuffraid.net/cowch/lst_v3/commits/3ad84dab71ceea081efc824e97d075c6f33807ad))
|
||||
* **opendock:** added some new goodies to the app to help manage releases ([c0a7d4a](https://git.tuffraid.net/cowch/lst_v3/commits/c0a7d4a1252e3f14240d14288a3ac4add44a6463))
|
||||
* **opendock:** changed the article to look at the label to match all plants ([a2d9a6c](https://git.tuffraid.net/cowch/lst_v3/commits/a2d9a6c127cae3b6dc5beb1a996558ab1383fa8c))
|
||||
* **opendock:** changed the subModule for better logging ([7c4c5f9](https://git.tuffraid.net/cowch/lst_v3/commits/7c4c5f980a15e13e2a43a8583a652622370ec1dd))
|
||||
* **opendock:** if load type is drop we will not do anything unless its already scanned ([5a7046b](https://git.tuffraid.net/cowch/lst_v3/commits/5a7046bacd9323627a37c89d7a9a329383e8b1be))
|
||||
* **server builds:** added in proper logging will still need fine tuned though ([15c939e](https://git.tuffraid.net/cowch/lst_v3/commits/15c939ebe8b808b3a9a46a4c1e62fb0488d3649b))
|
||||
* **servers:** all remaining servers added ([5f2148f](https://git.tuffraid.net/cowch/lst_v3/commits/5f2148f5f0f7688cf71e1dad99d4a5f6c34241dd))
|
||||
* **socket.io:** complete rewrite to manage dynamic rooms and seeding better ([9440b44](https://git.tuffraid.net/cowch/lst_v3/commits/9440b44f3bb1842d0e6ffc479d340ae9c5b84656))
|
||||
* **socketio:** changes that if we are in dock room to constant reseed the room ([8fcb2c6](https://git.tuffraid.net/cowch/lst_v3/commits/8fcb2c66ed25c746dc1d63c5290e11fe6a56328e))
|
||||
* **times:** added a better view for times as we save all db as there respective timezone ([40edbc3](https://git.tuffraid.net/cowch/lst_v3/commits/40edbc3eae9000afea447f81ec127884712b38da))
|
||||
* **warehouse:** fixed ppoo check to match the new changes ([2e460c7](https://git.tuffraid.net/cowch/lst_v3/commits/2e460c7f8aeb7de1d641c42357001704a4d8d8b2))
|
||||
|
||||
|
||||
### 📝 Testing Code
|
||||
|
||||
* **test:** added in vitest to start doing more testing before deploying ([e6b92ae](https://git.tuffraid.net/cowch/lst_v3/commits/e6b92aeb109394691dd2a7d548ddadd02769a159))
|
||||
|
||||
|
||||
### 📈 Project changes
|
||||
|
||||
* **app:** added more labels to the templates ([69a9a81](https://git.tuffraid.net/cowch/lst_v3/commits/69a9a81a889aca010ec8835ffab7084f7cd4a9b0))
|
||||
* **app:** changed the issues templates to be easier to add to the part of the app needed ([188fdd0](https://git.tuffraid.net/cowch/lst_v3/commits/188fdd0eb7c6bedf52ae1dcc9109a0fdbe969a21))
|
||||
|
||||
## [0.1.0-alpha.2](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.1...v0.1.0-alpha.2) (2026-05-23)
|
||||
|
||||
|
||||
|
||||
23
README.md
23
README.md
@@ -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
|
||||
```
|
||||
@@ -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")}
|
||||
};
|
||||
`);
|
||||
});
|
||||
|
||||
0
backend/datamart/datamart.controller.test.ts
Normal file
0
backend/datamart/datamart.controller.test.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
|
||||
68
backend/datamart/getDatamart.route.test.ts
Normal file
68
backend/datamart/getDatamart.route.test.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as dockScans from "./schema/dockdoor.scans.schema.js";
|
||||
import * as logs from "./schema/logs.schema.js";
|
||||
import * as opendockAVCheck from "./schema/opendock_articleSetup.js";
|
||||
import * as scanUserSchema from "./schema/scanUsers.js";
|
||||
import * as settingsSchema from "./schema/settings.schema.js";
|
||||
@@ -23,5 +25,7 @@ export const db = drizzle(queryClient, {
|
||||
...scanUserSchema,
|
||||
...settingsSchema,
|
||||
...opendockAVCheck,
|
||||
...logs,
|
||||
...dockScans,
|
||||
},
|
||||
});
|
||||
|
||||
59
backend/db/db.listener.ts
Normal file
59
backend/db/db.listener.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import postgres from "postgres";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { handleDbNotification } from "./db.router.js";
|
||||
|
||||
const log = createLogger({
|
||||
module: "db",
|
||||
subModule: "notifications",
|
||||
});
|
||||
|
||||
const CHANNELS = [
|
||||
"logs_inserted",
|
||||
// "labels_inserted",
|
||||
// "dock_scans_inserted",
|
||||
] as const;
|
||||
|
||||
type DbNotificationChannel = (typeof CHANNELS)[number];
|
||||
|
||||
type DbNotificationPayload = {
|
||||
table: string;
|
||||
action: "INSERT" | "UPDATE" | "DELETE";
|
||||
id: string;
|
||||
};
|
||||
|
||||
export async function startDbNotificationListener() {
|
||||
const sql = postgres({
|
||||
host: `${process.env.DATABASE_HOST}`,
|
||||
port: Number(process.env.DATABASE_PORT),
|
||||
database: `${process.env.DATABASE_DB}`,
|
||||
username: process.env.DATABASE_USER,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
});
|
||||
|
||||
for (const channel of CHANNELS) {
|
||||
await sql.listen(channel, async (rawPayload) => {
|
||||
await processNotification(channel, rawPayload);
|
||||
});
|
||||
|
||||
log.info({ stack: { channel } }, `Listening for ${channel}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function processNotification(
|
||||
channel: DbNotificationChannel,
|
||||
rawPayload: string,
|
||||
) {
|
||||
try {
|
||||
const payload = JSON.parse(rawPayload) as DbNotificationPayload;
|
||||
|
||||
await handleDbNotification({
|
||||
channel,
|
||||
payload,
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(
|
||||
{ stack: { channel, rawPayload, e }, notify: true },
|
||||
"Failed processing DB notification",
|
||||
);
|
||||
}
|
||||
}
|
||||
41
backend/db/db.router.ts
Normal file
41
backend/db/db.router.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { handleDockScanInsertedNotification } from "../dockdoorScanning/dockdoor.socket.notifications.js";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { handleLogInsertedNotification } from "../logger/logger.socket.notifications.js";
|
||||
|
||||
const log = createLogger({
|
||||
module: "db",
|
||||
subModule: "notifications-router",
|
||||
});
|
||||
|
||||
type DbNotification = {
|
||||
channel: string;
|
||||
payload: {
|
||||
table: string;
|
||||
action: "INSERT" | "UPDATE" | "DELETE";
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function handleDbNotification(notification: DbNotification) {
|
||||
const { channel, payload } = notification;
|
||||
|
||||
switch (channel) {
|
||||
case "logs_inserted":
|
||||
await handleLogInsertedNotification(payload.id);
|
||||
return;
|
||||
|
||||
// case "labels_inserted":
|
||||
// await handleLabelInsertedNotification(payload.id);
|
||||
// return;
|
||||
|
||||
case "dock_scan_inserted":
|
||||
await handleDockScanInsertedNotification(payload.id);
|
||||
return;
|
||||
|
||||
default:
|
||||
log.warn(
|
||||
{ stack: notification },
|
||||
`Unhandled DB notification channel: ${channel}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
99
backend/db/db.setupNotifications.ts
Normal file
99
backend/db/db.setupNotifications.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
|
||||
const log = createLogger({
|
||||
module: "db",
|
||||
subModule: "notifications",
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates/updates Postgres notification functions + triggers.
|
||||
*
|
||||
* Safe to run on every app startup.
|
||||
* CREATE OR REPLACE updates the function.
|
||||
* DROP TRIGGER IF EXISTS prevents duplicate triggers.
|
||||
*/
|
||||
export async function setupDbNotifications() {
|
||||
log.info({}, "Setting up DB notifications");
|
||||
|
||||
await setupLogsNotifications();
|
||||
await setupDockScansNotifications();
|
||||
|
||||
log.info({}, "DB notifications setup complete");
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs notification setup.
|
||||
*
|
||||
* Flow:
|
||||
* 1. app inserts into logs table
|
||||
* 2. trigger runs after insert
|
||||
* 3. Postgres sends NOTIFY logs_inserted with the new log id
|
||||
* 4. Node listener receives id and fetches/emits full row
|
||||
*/
|
||||
async function setupLogsNotifications() {
|
||||
await db.execute(sql`
|
||||
CREATE OR REPLACE FUNCTION notify_logs_inserted()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify(
|
||||
'logs_inserted',
|
||||
json_build_object(
|
||||
'table', TG_TABLE_NAME,
|
||||
'action', TG_OP,
|
||||
'id', NEW.id
|
||||
)::text
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
DROP TRIGGER IF EXISTS logs_inserted_notify_trigger ON logs;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TRIGGER logs_inserted_notify_trigger
|
||||
AFTER INSERT ON logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_logs_inserted();
|
||||
`);
|
||||
|
||||
log.info({}, "Logs DB notification trigger ready");
|
||||
}
|
||||
|
||||
async function setupDockScansNotifications() {
|
||||
await db.execute(sql`
|
||||
CREATE OR REPLACE FUNCTION notify_dock_scan_inserted()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify(
|
||||
'dock_scan_inserted',
|
||||
json_build_object(
|
||||
'table', TG_TABLE_NAME,
|
||||
'action', TG_OP,
|
||||
'id', NEW.id
|
||||
)::text
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
DROP TRIGGER IF EXISTS dock_scan_inserted_notify_trigger ON dock_door_scans;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TRIGGER dock_scan_inserted_notify_trigger
|
||||
AFTER INSERT ON dock_door_scans
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_dock_scan_inserted();
|
||||
`);
|
||||
|
||||
log.info({}, "Dock scan DB notification trigger ready");
|
||||
}
|
||||
21
backend/db/db.socketSeed.ts
Normal file
21
backend/db/db.socketSeed.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { db } from "./db.controller.js";
|
||||
|
||||
export const getRecentLogs = ({
|
||||
module,
|
||||
submodule,
|
||||
limit = 200,
|
||||
}: {
|
||||
module?: string | undefined;
|
||||
submodule?: string | undefined;
|
||||
limit?: number | undefined;
|
||||
}) => {
|
||||
return db.query.logs.findMany({
|
||||
where: (logs, { and, eq }) =>
|
||||
and(
|
||||
module ? eq(logs.module, module) : undefined,
|
||||
submodule ? eq(logs.subModule, submodule) : undefined,
|
||||
),
|
||||
orderBy: (logs, { desc }) => [desc(logs.createdAt)],
|
||||
limit,
|
||||
});
|
||||
};
|
||||
@@ -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 =
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
|
||||
23
backend/db/schema/dockdoor.scans.schema.ts
Normal file
23
backend/db/schema/dockdoor.scans.schema.ts
Normal 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>;
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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})`),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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})`),
|
||||
|
||||
@@ -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({}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
25
backend/dockdoorScanning/dockdoor.loadUnits.route.ts
Normal file
25
backend/dockdoorScanning/dockdoor.loadUnits.route.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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/*
|
||||
};
|
||||
|
||||
17
backend/dockdoorScanning/dockdoor.socket.notifications.ts
Normal file
17
backend/dockdoorScanning/dockdoor.socket.notifications.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { logs } from "../db/schema/logs.schema.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
|
||||
export async function handleDockScanInsertedNotification(id: string) {
|
||||
const row = await db.query.dockDoorScans.findFirst({
|
||||
where: eq(logs.id, id),
|
||||
});
|
||||
|
||||
if (!row) return;
|
||||
|
||||
// send only to the current dock door
|
||||
if (row.dockId) {
|
||||
emitToRoom(`dockDoorLoading:${row.dockId}`, row);
|
||||
}
|
||||
}
|
||||
20
backend/dockdoorScanning/dockdoor.socket.seed.ts
Normal file
20
backend/dockdoorScanning/dockdoor.socket.seed.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
|
||||
107
backend/eom/eom.gpdata.route.ts
Normal file
107
backend/eom/eom.gpdata.route.ts
Normal 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;
|
||||
9
backend/eom/eom.history.controller.ts
Normal file
9
backend/eom/eom.history.controller.ts
Normal 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 () => {};
|
||||
61
backend/eom/eom.historyInv.route.ts
Normal file
61
backend/eom/eom.historyInv.route.ts
Normal 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;
|
||||
77
backend/eom/eom.lastPurchasePrice.route.ts
Normal file
77
backend/eom/eom.lastPurchasePrice.route.ts
Normal 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;
|
||||
89
backend/eom/eom.lastSalesPrice.route.ts
Normal file
89
backend/eom/eom.lastSalesPrice.route.ts
Normal 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;
|
||||
97
backend/eom/eom.productionConsumption.route.ts
Normal file
97
backend/eom/eom.productionConsumption.route.ts
Normal 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;
|
||||
98
backend/eom/eom.purchased.route.ts
Normal file
98
backend/eom/eom.purchased.route.ts
Normal 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;
|
||||
98
backend/eom/eom.regrind.route.ts
Normal file
98
backend/eom/eom.regrind.route.ts
Normal 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
35
backend/eom/eom.routes.ts
Normal 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);
|
||||
};
|
||||
98
backend/eom/eom.soldItems.route.ts
Normal file
98
backend/eom/eom.soldItems.route.ts
Normal 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;
|
||||
16
backend/gpSql/queries/gp.eom.data.sql
Normal file
16
backend/gpSql/queries/gp.eom.data.sql
Normal 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]'
|
||||
@@ -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);
|
||||
|
||||
24
backend/logger/logger.socket.notifications.ts
Normal file
24
backend/logger/logger.socket.notifications.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { logs } from "../db/schema/logs.schema.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
|
||||
export async function handleLogInsertedNotification(id: string) {
|
||||
const row = await db.query.logs.findFirst({
|
||||
where: eq(logs.id, id),
|
||||
});
|
||||
|
||||
if (!row) return;
|
||||
|
||||
// More targeted rooms.
|
||||
if (row.module) {
|
||||
emitToRoom(`logs:${row.module}`, row);
|
||||
}
|
||||
|
||||
if (row.subModule) {
|
||||
emitToRoom(`logs:${row.subModule}`, row);
|
||||
}
|
||||
|
||||
// Everyone listening to all logs.
|
||||
emitToRoom("logs", row);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
73
backend/opendock/openDockUndoLastStatus.ts
Normal file
73
backend/opendock/openDockUndoLastStatus.ts
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ export let odToken: ODToken = {
|
||||
};
|
||||
|
||||
export const getToken = async () => {
|
||||
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
|
||||
const log = createLogger({ module: "opendock", subModule: "auth" });
|
||||
try {
|
||||
const { status, data } = await axios.post(
|
||||
`${process.env.OPENDOCK_URL}/auth/login`,
|
||||
@@ -29,7 +29,8 @@ export const getToken = async () => {
|
||||
|
||||
odToken = { odToken: data.access_token, tokenDate: new Date() };
|
||||
log.info({ odToken }, "Token added");
|
||||
return;
|
||||
} catch (e) {
|
||||
log.error({ error: e }, "Error getting/refreshing token");
|
||||
log.error({ stack: e }, "Error getting/refreshing token");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,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;
|
||||
124
backend/opendock/opendockRelease.route.ts
Normal file
124
backend/opendock/opendockRelease.route.ts
Normal 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;
|
||||
15
backend/prodSql/queries/eom.lastPurchasePrice.sql
Normal file
15
backend/prodSql/queries/eom.lastPurchasePrice.sql
Normal 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
|
||||
14
backend/prodSql/queries/eom.lastSalesPrice.sql
Normal file
14
backend/prodSql/queries/eom.lastSalesPrice.sql
Normal 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
|
||||
7
backend/prodSql/queries/eom.productionConsumption.sql
Normal file
7
backend/prodSql/queries/eom.productionConsumption.sql
Normal 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
|
||||
40
backend/prodSql/queries/eom.purchased.sql
Normal file
40
backend/prodSql/queries/eom.purchased.sql
Normal 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
|
||||
13
backend/prodSql/queries/eom.regrind.sql
Normal file
13
backend/prodSql/queries/eom.regrind.sql
Normal 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
|
||||
15
backend/prodSql/queries/eom.soldItems.sql
Normal file
15
backend/prodSql/queries/eom.soldItems.sql
Normal 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]'
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { RoomId } from "./roomDefinitions.socket.js";
|
||||
|
||||
export const MAX_HISTORY = 50;
|
||||
export const FLUSH_INTERVAL = 100; // 50ms change higher if needed
|
||||
|
||||
export const roomHistory = new Map<RoomId, unknown[]>();
|
||||
export const roomBuffers = new Map<RoomId, any[]>();
|
||||
export const roomFlushTimers = new Map<RoomId, NodeJS.Timeout>();
|
||||
@@ -1,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;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,27 +1,73 @@
|
||||
// the emitter setup
|
||||
// TODO: validate if we want to add event back in later..
|
||||
// let emitFn: ((roomId: string, event: string, payload: unknown) => void) | null =
|
||||
// null;
|
||||
|
||||
import type { RoomId } from "./roomDefinitions.socket.js";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
|
||||
let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null;
|
||||
type QueuedPayload = unknown;
|
||||
|
||||
let emitFn: ((roomId: string, payload: QueuedPayload[]) => void) | null = null;
|
||||
|
||||
const queues = new Map<string, QueuedPayload[]>();
|
||||
const timers = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
const FLUSH_MS = 500;
|
||||
const MAX_QUEUE_SIZE = 200;
|
||||
|
||||
export const registerEmitter = (
|
||||
fn: (roomId: RoomId, payload: unknown[]) => void,
|
||||
fn: (roomId: string, payload: QueuedPayload) => void,
|
||||
) => {
|
||||
addDataToRoom = fn;
|
||||
emitFn = fn;
|
||||
};
|
||||
|
||||
export const emitToRoom = (roomId: RoomId, payload: unknown[]) => {
|
||||
if (!addDataToRoom) {
|
||||
export const emitToRoom = (roomId: string, payload: QueuedPayload) => {
|
||||
const log = createLogger({ module: "socket.io", subModule: "emitter" });
|
||||
if (!emitFn) {
|
||||
console.error("Socket emitter not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
addDataToRoom(roomId, payload);
|
||||
const queue = queues.get(roomId) ?? [];
|
||||
|
||||
if (queue.length > MAX_QUEUE_SIZE) {
|
||||
log.error(
|
||||
{ stack: { roomId, size: queue.length }, notify: true },
|
||||
`Socket queue exceeded max size for ${roomId}`,
|
||||
);
|
||||
}
|
||||
queue.push(payload);
|
||||
queues.set(roomId, queue);
|
||||
|
||||
if (timers.has(roomId)) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
const payloads = queues.get(roomId) ?? [];
|
||||
|
||||
if (payloads.length === 0) return;
|
||||
emitFn?.(roomId, payloads);
|
||||
|
||||
queues.delete(roomId);
|
||||
} catch (e) {
|
||||
console.error("Socket emit failed", { roomId, e });
|
||||
} finally {
|
||||
timers.delete(roomId);
|
||||
}
|
||||
}, FLUSH_MS);
|
||||
|
||||
timers.set(roomId, timer);
|
||||
};
|
||||
|
||||
/*
|
||||
import { emitToRoom } from "../socket/socketEmitter.js";
|
||||
// room name
|
||||
// its payload
|
||||
emitToRoom("logs", newLogRow);
|
||||
example emitToRoom(room, payload)
|
||||
|
||||
payload can be anything json serilized example below.
|
||||
|
||||
emitToRoom("inventory:ppoo", {
|
||||
type: "snapshot",
|
||||
location: "ppoo",
|
||||
items,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
*/
|
||||
|
||||
@@ -1,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 };
|
||||
};
|
||||
@@ -1,33 +1,16 @@
|
||||
import type { Server as HttpServer } from "node:http";
|
||||
//import { dirname, join } from "node:path";
|
||||
//import { fileURLToPath } from "node:url";
|
||||
import { instrument } from "@socket.io/admin-ui";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { auth } from "../utils/auth.utils.js";
|
||||
import { allowedOrigins } from "../utils/cors.utils.js";
|
||||
import { registerEmitter } from "./roomEmitter.socket.js";
|
||||
import {
|
||||
createRoomEmitter,
|
||||
preseedRoom,
|
||||
registerRoomService,
|
||||
} from "./roomService.socket.js";
|
||||
import { registerHasRoomMembers } from "./socket.manager.js";
|
||||
import { isRoomKey, roomConfigs } from "./socket.roomConfig.js";
|
||||
|
||||
//const __filename = fileURLToPath(import.meta.url);
|
||||
//const __dirname = dirname(__filename);
|
||||
const log = createLogger({ module: "socket.io", subModule: "setup" });
|
||||
|
||||
import { auth } from "../utils/auth.utils.js";
|
||||
//import type { Session, User } from "better-auth"; // adjust if needed
|
||||
import { getRoomConfig } from "./roomDefinitions.socket.js";
|
||||
|
||||
// declare module "socket.io" {
|
||||
// interface Socket {
|
||||
// user?: User | any;
|
||||
// session?: Session;
|
||||
// }
|
||||
// }
|
||||
|
||||
export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
|
||||
const io = new Server(server, {
|
||||
path: `${baseUrl}/api/socket.io`,
|
||||
@@ -37,12 +20,16 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
|
||||
},
|
||||
});
|
||||
|
||||
// manage members of the rooms.
|
||||
registerRoomService(io);
|
||||
registerHasRoomMembers((roomId) => {
|
||||
return (io.sockets.adapter.rooms.get(roomId)?.size ?? 0) > 0;
|
||||
});
|
||||
|
||||
// ✅ Create emitter instance
|
||||
const { addDataToRoom } = createRoomEmitter(io);
|
||||
registerEmitter(addDataToRoom);
|
||||
registerEmitter((roomId, payloads) => {
|
||||
io.to(roomId).emit("room-update", {
|
||||
roomId,
|
||||
payloads,
|
||||
});
|
||||
});
|
||||
|
||||
io.use(async (socket, next) => {
|
||||
try {
|
||||
@@ -85,79 +72,95 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// s.on("join-room", async (rn) => {
|
||||
// const config = protectedRooms[rn];
|
||||
s.on("join-room", async ({ room, params }) => {
|
||||
if (!isRoomKey(room)) return;
|
||||
|
||||
// if (config?.requiresAuth && !s.user) {
|
||||
// return s.emit("room-error", {
|
||||
// room: rn,
|
||||
// message: "Authentication required",
|
||||
// });
|
||||
// }
|
||||
|
||||
// const roles = Array.isArray(config?.role) ? config?.role : [config?.role];
|
||||
|
||||
// //if (config?.role && s.user?.role !== config.role) {
|
||||
// if (config?.role && !roles.includes(s.user?.role)) {
|
||||
// return s.emit("room-error", {
|
||||
// roomId: rn,
|
||||
// message: `Not authorized to be in room: ${rn}`,
|
||||
// });
|
||||
// }
|
||||
// s.join(rn);
|
||||
|
||||
// // get room seeded
|
||||
// const history = await preseedRoom(rn);
|
||||
// log.info({}, `User joined ${rn}: ${s.id}`);
|
||||
// // send the intial data
|
||||
// s.emit("room-update", {
|
||||
// roomId: rn,
|
||||
// payloads: history,
|
||||
// initial: true,
|
||||
// });
|
||||
// });
|
||||
|
||||
s.on("join-room", async (rn: string) => {
|
||||
const config = getRoomConfig(rn);
|
||||
const config = roomConfigs[room];
|
||||
|
||||
if (!config) {
|
||||
return s.emit("room-error", {
|
||||
roomId: rn,
|
||||
message: `Unknown room: ${rn}`,
|
||||
roomId: room,
|
||||
message: `Unknown room: ${room}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (config.requiresAuth && !s.user) {
|
||||
const actualRoom = config.buildRoom
|
||||
? config.buildRoom(params)
|
||||
: (room as any);
|
||||
|
||||
const allowed = config.canJoin
|
||||
? await config.canJoin({
|
||||
socket: s,
|
||||
user: s.user,
|
||||
room,
|
||||
actualRoom,
|
||||
params,
|
||||
})
|
||||
: true;
|
||||
|
||||
if (!allowed) {
|
||||
return s.emit("room-error", {
|
||||
roomId: rn,
|
||||
message: "Authentication required",
|
||||
roomId: room,
|
||||
message: `Not authorized to be in room: ${room}`,
|
||||
});
|
||||
}
|
||||
|
||||
const roles = Array.isArray(config.role) ? config.role : [];
|
||||
await s.join(actualRoom);
|
||||
|
||||
if (roles.length > 0 && !roles.includes(s.user?.role)) {
|
||||
return s.emit("room-error", {
|
||||
roomId: rn,
|
||||
message: `Not authorized to be in room: ${rn}`,
|
||||
});
|
||||
}
|
||||
|
||||
s.join(rn);
|
||||
|
||||
const history = await preseedRoom(rn as any);
|
||||
|
||||
log.info({}, `User joined ${rn}: ${s.id}`);
|
||||
|
||||
s.emit("room-update", {
|
||||
roomId: rn,
|
||||
payloads: history,
|
||||
initial: true,
|
||||
s.emit("room-joined", {
|
||||
room,
|
||||
roomId: actualRoom,
|
||||
params,
|
||||
});
|
||||
|
||||
if (config.seed) {
|
||||
const payloads = await config.seed({
|
||||
room,
|
||||
actualRoom,
|
||||
params,
|
||||
user: s.user,
|
||||
});
|
||||
|
||||
s.emit("room-update", {
|
||||
room,
|
||||
roomId: actualRoom,
|
||||
type: "snapshot",
|
||||
payloads,
|
||||
});
|
||||
}
|
||||
|
||||
log.info(
|
||||
{ room, actualRoom, params },
|
||||
`User joined ${actualRoom}: ${s.id}`,
|
||||
);
|
||||
});
|
||||
s.on("leave-room", (room) => {
|
||||
s.leave(room);
|
||||
log.info({}, `${s.id} left room: ${room}`);
|
||||
// s.on("leave-room", (room) => {
|
||||
// s.leave(room);
|
||||
// log.info({}, `${s.id} left room: ${JSON.stringify(room)}`);
|
||||
// });
|
||||
s.on("leave-room", async ({ room, params }) => {
|
||||
if (!isRoomKey(room)) return;
|
||||
|
||||
const config = roomConfigs[room];
|
||||
|
||||
if (!config) return;
|
||||
|
||||
const actualRoom = config.buildRoom
|
||||
? config.buildRoom(params)
|
||||
: (room as any);
|
||||
|
||||
await s.leave(actualRoom);
|
||||
|
||||
s.emit("room-left", {
|
||||
room,
|
||||
roomId: actualRoom,
|
||||
params,
|
||||
});
|
||||
|
||||
log.info(
|
||||
{ room, actualRoom, params },
|
||||
`${s.id} left room: ${actualRoom}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
10
backend/socket.io/socket.manager.ts
Normal file
10
backend/socket.io/socket.manager.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
let hasMembersFn: ((roomId: string) => boolean) | null = null;
|
||||
|
||||
export const registerHasRoomMembers = (fn: (roomId: string) => boolean) => {
|
||||
hasMembersFn = fn;
|
||||
};
|
||||
|
||||
export const hasRoomMembers = (roomId: string) => {
|
||||
if (!hasMembersFn) return false;
|
||||
return hasMembersFn(roomId);
|
||||
};
|
||||
117
backend/socket.io/socket.roomConfig.ts
Normal file
117
backend/socket.io/socket.roomConfig.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { getRecentLogs } from "../db/db.socketSeed.js";
|
||||
import { getRecentDockScans } from "../dockdoorScanning/dockdoor.socket.seed.js";
|
||||
|
||||
export type RoomKey =
|
||||
| "logs"
|
||||
| "labels"
|
||||
| "admin"
|
||||
| "inventory"
|
||||
| "dockDoorLoading";
|
||||
|
||||
export type SocketUser = {
|
||||
id: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
};
|
||||
|
||||
export type CanJoinArgs = {
|
||||
socket: any;
|
||||
user?: SocketUser;
|
||||
room: string;
|
||||
actualRoom: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type RoomConfig = {
|
||||
//requiresAuth?: boolean;
|
||||
//roles?: string[];
|
||||
canJoin?: (args: CanJoinArgs) => boolean | Promise<boolean>;
|
||||
buildRoom?: (params?: Record<string, unknown>) => string | null;
|
||||
seed?: (args: {
|
||||
room: string;
|
||||
actualRoom: string;
|
||||
params?: Record<string, unknown>;
|
||||
user?: SocketUser;
|
||||
}) => Promise<unknown[]>;
|
||||
};
|
||||
|
||||
export function isRoomKey(room: string): room is RoomKey {
|
||||
return room in roomConfigs;
|
||||
}
|
||||
|
||||
export const roomConfigs: Record<RoomKey, RoomConfig> = {
|
||||
logs: {
|
||||
canJoin: ({ user, params }) => {
|
||||
if (!params?.submodule && !params?.module) {
|
||||
return user?.role === "systemAdmin";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
buildRoom: (params) => {
|
||||
const module = String(params?.module ?? "").toLowerCase();
|
||||
const submodule = String(params?.submodule ?? "").toLowerCase();
|
||||
|
||||
if (module && submodule) return `logs:${module}:${submodule}`;
|
||||
if (submodule) return `logs:${submodule}`;
|
||||
if (module) return `logs:${module}`;
|
||||
|
||||
return "logs";
|
||||
},
|
||||
seed: async ({ params }) => {
|
||||
const module = params?.module ? String(params.module) : undefined;
|
||||
const submodule = params?.submodule
|
||||
? String(params.submodule)
|
||||
: undefined;
|
||||
|
||||
return await getRecentLogs({
|
||||
module,
|
||||
submodule,
|
||||
limit: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
labels: {
|
||||
canJoin: () => true,
|
||||
buildRoom: () => "labels",
|
||||
},
|
||||
|
||||
admin: {
|
||||
canJoin: ({ user, params }) => {
|
||||
if (params?.section === "system") {
|
||||
return user?.role === "systemAdmin";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
buildRoom: (params) =>
|
||||
params?.section ? `admin:${params.section}` : "admin",
|
||||
},
|
||||
inventory: {
|
||||
canJoin: () => true,
|
||||
buildRoom: (params) =>
|
||||
params?.location ? `inventory:${params.location}` : null,
|
||||
},
|
||||
|
||||
dockDoorLoading: {
|
||||
canJoin: () => true,
|
||||
buildRoom: (params) =>
|
||||
params?.dockId ? `dockDoorLoading:${params.dockId}` : null,
|
||||
seed: async ({ params }) => {
|
||||
return await getRecentDockScans({
|
||||
loadingOrder: params?.loadingOrder as string,
|
||||
limit: 200,
|
||||
});
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, RoomConfig>;
|
||||
|
||||
/*
|
||||
|
||||
socket.emit("join-room", {
|
||||
room: "dockDoorLoading",
|
||||
params: { dockId: "2" },
|
||||
});
|
||||
|
||||
*/
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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:", ""))) {
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -17,7 +17,8 @@ export interface ReturnHelper<T = unknown[]> {
|
||||
| "logistics"
|
||||
| "admin"
|
||||
| "mobile"
|
||||
| "dockdoor";
|
||||
| "dockdoor"
|
||||
| "eom";
|
||||
subModule: string;
|
||||
|
||||
level: "info" | "error" | "debug" | "fatal" | "warn";
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -40,6 +40,11 @@ export default function TransportationBar() {
|
||||
icon: link,
|
||||
url: "/transportation/opendock",
|
||||
},
|
||||
{
|
||||
title: "Active releases",
|
||||
icon: link,
|
||||
url: "/transportation/opendock/releases",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
94
frontend/src/components/Sidebar/Warhouse.tsx
Normal file
94
frontend/src/components/Sidebar/Warhouse.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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" ||
|
||||
|
||||
49
frontend/src/components/ui/badge.tsx
Normal file
49
frontend/src/components/ui/badge.tsx
Normal 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 }
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
|
||||
|
||||
21
frontend/src/lib/queries/getActiveDockScanners.ts
Normal file
21
frontend/src/lib/queries/getActiveDockScanners.ts
Normal 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 ?? [];
|
||||
};
|
||||
25
frontend/src/lib/queries/getActiveLoadingOrders.ts
Normal file
25
frontend/src/lib/queries/getActiveLoadingOrders.ts
Normal 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 ?? [];
|
||||
};
|
||||
23
frontend/src/lib/queries/openDockApt.ts
Normal file
23
frontend/src/lib/queries/openDockApt.ts
Normal 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;
|
||||
};
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user