From f8335f5217c339a7dc09883fa8ba9c60aa4c35b1 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Mon, 1 Jun 2026 14:23:26 -0500 Subject: [PATCH] refactor(backend): dock door scanning socket and perms --- .../dockdoor.closeLoadingOrder.route.ts | 70 ++++++++-- .../dockdoor.loadUnits.route.ts | 25 ++++ .../dockdoorScanning/dockdoor.loadUnits.ts | 70 ++++++++-- backend/dockdoorScanning/dockdoor.routes.ts | 9 +- .../dockdoor.startLoad.route.ts | 3 +- .../opendock/openDockRreleaseMonitor.utils.ts | 124 +++++++++++++++++- backend/prodSql/queries/featureCheck.sql | 75 ++++++++++- backend/socket.io/roomDefinitions.socket.ts | 17 ++- backend/tcpServer/tcp.server.ts | 3 +- backend/utils/auth.utils.ts | 10 +- frontend/src/hooks/socket.io.hook.ts | 23 ++-- 11 files changed, 391 insertions(+), 38 deletions(-) create mode 100644 backend/dockdoorScanning/dockdoor.loadUnits.route.ts diff --git a/backend/dockdoorScanning/dockdoor.closeLoadingOrder.route.ts b/backend/dockdoorScanning/dockdoor.closeLoadingOrder.route.ts index 8bcc123..f4b2cd8 100644 --- a/backend/dockdoorScanning/dockdoor.closeLoadingOrder.route.ts +++ b/backend/dockdoorScanning/dockdoor.closeLoadingOrder.route.ts @@ -1,18 +1,70 @@ +import { eq, sql } from "drizzle-orm"; import { Router } from "express"; +import z from "zod"; +import { db } from "../db/db.controller.js"; +import { dockDoorScanners } from "../db/schema/dockdoor.schema.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, - }); + // close the loading order + + // clear the loading order off the dock + + try { + const validated = endLoading.parse(req.body); + + const { data, error } = await tryCatch( + db + .update(dockDoorScanners) + .set({ + currentLoadingOrder: "", + upd_date: sql`NOW()`, + upd_user: req.user?.username, + }) + .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: true, + level: "info", + module: "dockdoor", + subModule: "loadingOrder", + message: `Loading order ${validated.loadingOrder} was just closed.`, + data: data ?? [], + status: 200, + }); + } catch (error) { + return apiReturn(res, { + success: false, + level: "error", + module: "dockdoor", + subModule: "loadingOrder", + message: `Failed to start loading order.`, + data: (error as any) ?? [], + status: 400, + }); + } }); export default r; diff --git a/backend/dockdoorScanning/dockdoor.loadUnits.route.ts b/backend/dockdoorScanning/dockdoor.loadUnits.route.ts new file mode 100644 index 0000000..f0f9396 --- /dev/null +++ b/backend/dockdoorScanning/dockdoor.loadUnits.route.ts @@ -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; diff --git a/backend/dockdoorScanning/dockdoor.loadUnits.ts b/backend/dockdoorScanning/dockdoor.loadUnits.ts index e21e780..5598028 100644 --- a/backend/dockdoorScanning/dockdoor.loadUnits.ts +++ b/backend/dockdoorScanning/dockdoor.loadUnits.ts @@ -3,6 +3,7 @@ import { eq } from "drizzle-orm"; import { db } from "../db/db.controller.js"; import { dockDoorScanners } from "../db/schema/dockdoor.schema.js"; +import { emitToRoom } from "../socket.io/roomEmitter.socket.js"; import { runProdApi } from "../utils/prodEndpoint.utils.js"; import { returnFunc } from "../utils/returnHelper.utils.js"; @@ -11,10 +12,10 @@ import { returnFunc } from "../utils/returnHelper.utils.js"; type Data = { dockId?: string; sscc?: string; - runningNr?: string; + runningNo?: string; }; -export const loadUnit = async (data: Data) => { +const loadUnit = async (data: Data) => { // are we even active at this time? const dockDoorActive = await db.query.settings.findFirst({ where: (u, { eq }) => eq(u.name, "dockDoorScanning"), @@ -44,7 +45,7 @@ export const loadUnit = async (data: Data) => { "Failed to load the unit to the truck, there was no pallet read.", data: [], notify: false, - room: `dockDoorLoading${data.dockId}`, + room: `dockDoorLoading:${data.dockId}`, }); } @@ -64,7 +65,7 @@ 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}`, }); } @@ -76,14 +77,67 @@ export const loadUnit = async (data: Data) => { // add the loading units try { - const prod = await runProdApi({ + const unitToScan = data.sscc + ? { sscc: data.sscc?.slice(2) } + : { runningNo: Number(data.runningNo) }; + + 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) { + emitToRoom(`dockDoorLoading:${data.dockId}`, prod?.data.errors[0]); + 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, + }; + emitToRoom(`dockDoorLoading:${data.dockId}`, emitData as any); + 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; diff --git a/backend/dockdoorScanning/dockdoor.routes.ts b/backend/dockdoorScanning/dockdoor.routes.ts index 843a80f..54fc9ae 100644 --- a/backend/dockdoorScanning/dockdoor.routes.ts +++ b/backend/dockdoorScanning/dockdoor.routes.ts @@ -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/* }; diff --git a/backend/dockdoorScanning/dockdoor.startLoad.route.ts b/backend/dockdoorScanning/dockdoor.startLoad.route.ts index d2fa5b0..774b399 100644 --- a/backend/dockdoorScanning/dockdoor.startLoad.route.ts +++ b/backend/dockdoorScanning/dockdoor.startLoad.route.ts @@ -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(), ); diff --git a/backend/opendock/openDockRreleaseMonitor.utils.ts b/backend/opendock/openDockRreleaseMonitor.utils.ts index 54d0163..a2af101 100644 --- a/backend/opendock/openDockRreleaseMonitor.utils.ts +++ b/backend/opendock/openDockRreleaseMonitor.utils.ts @@ -31,6 +31,14 @@ 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" }, +]; const timeZone = process.env.TIMEZONE as string; const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; const log = createLogger({ module: "opendock", subModule: "releaseMonitor" }); @@ -64,6 +72,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,13 +91,29 @@ 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( @@ -97,6 +122,24 @@ const postRelease = async (release: Releases) => { ), }); + // 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 +170,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, @@ -403,7 +446,12 @@ const postRelease = async (release: Releases) => { return; } } - } else { + } else if ( + (releaseLoadtypeCheck || + opendDockArticleCheck?.loadType === "drop" || + defaultDock?.value === "drop") && + releaseOkToSchedule + ) { try { const response = await axios.post( `${process.env.OPENDOCK_URL}/appointment`, @@ -458,6 +506,76 @@ const postRelease = async (release: Releases) => { 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 diff --git a/backend/prodSql/queries/featureCheck.sql b/backend/prodSql/queries/featureCheck.sql index 8ef54f4..9fc7b45 100644 --- a/backend/prodSql/queries/featureCheck.sql +++ b/backend/prodSql/queries/featureCheck.sql @@ -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 */ diff --git a/backend/socket.io/roomDefinitions.socket.ts b/backend/socket.io/roomDefinitions.socket.ts index 7f0adf7..f36f5d4 100644 --- a/backend/socket.io/roomDefinitions.socket.ts +++ b/backend/socket.io/roomDefinitions.socket.ts @@ -7,7 +7,13 @@ type RoomDefinition = { seed: (limit: number) => Promise; }; -export type StaticRoomId = "logs" | "labels" | "admin" | "admin:build" | "ppoo"; +export type StaticRoomId = + | "logs" + | "labels" + | "admin" + | "admin:build" + | "ppoo" + | "dockDoorLoading:2"; export type DynamicRoomId = `dockDoorLoading:${string}`; export type RoomId = StaticRoomId | DynamicRoomId; @@ -24,6 +30,7 @@ export const protectedRooms: Record = { admin: {}, "admin:build": {}, ppoo: {}, + "dockDoorLoading:2": {}, }; export function getRoomConfig(roomId: string): RoomConfig | null { @@ -91,4 +98,12 @@ export const roomDefinition: Record = { } as any; }, }, + + // TODO: add in dynamic room seeding + "dockDoorLoading:2": { + seed: async (limit) => { + console.log(limit); + return []; + }, + }, }; diff --git a/backend/tcpServer/tcp.server.ts b/backend/tcpServer/tcp.server.ts index 2703999..917424a 100644 --- a/backend/tcpServer/tcp.server.ts +++ b/backend/tcpServer/tcp.server.ts @@ -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:", ""))) { diff --git a/backend/utils/auth.utils.ts b/backend/utils/auth.utils.ts index 64cf515..a732717 100644 --- a/backend/utils/auth.utils.ts +++ b/backend/utils/auth.utils.ts @@ -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, }, }), diff --git a/frontend/src/hooks/socket.io.hook.ts b/frontend/src/hooks/socket.io.hook.ts index 0a09de9..c287092 100644 --- a/frontend/src/hooks/socket.io.hook.ts +++ b/frontend/src/hooks/socket.io.hook.ts @@ -11,10 +11,12 @@ type RoomErrorPayload = { message?: string; }; +type UpdateMode = "append" | "replace"; + export function useSocketRoom( roomId: string, - enabled = true, getKey?: (item: T) => string | number, + updateMode: UpdateMode = "append", ) { const [data, setData] = useState([]); const [info, setInfo] = useState( @@ -36,7 +38,6 @@ export function useSocketRoom( ); useEffect(() => { - if (!roomId || !enabled) return; function handleConnect() { socket.emit("join-room", roomId); setInfo(`Joined room: ${roomId}`); @@ -46,7 +47,13 @@ export function useSocketRoom( // protects against other room updates hitting this hook if (payload.roomId !== roomId) return; - setData((prev) => [...payload.payloads, ...prev]); + // resetting room data for rooms that just need updated data. + if (updateMode === "replace") { + setData(payload.payloads); + } else { + setData((prev) => [...payload.payloads, ...prev]); + } + setInfo(""); } @@ -55,14 +62,14 @@ export function useSocketRoom( setInfo(err.message ?? "Room error"); } - if (!socket.connected) { - socket.connect(); - } - socket.on("connect", handleConnect); socket.on("room-update", handleUpdate); socket.on("room-error", handleError); + if (!socket.connected && socket.disconnected) { + socket.connect(); + } + // If already connected, join immediately if (socket.connected) { socket.emit("join-room", roomId); @@ -76,7 +83,7 @@ export function useSocketRoom( socket.off("room-update", handleUpdate); socket.off("room-error", handleError); }; - }, [roomId, enabled]); + }, [roomId, updateMode]); return { data, info, clearRoom }; }