From 86e1237509b81722dee7b42762d0bfced8d26fa3 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Wed, 10 Jun 2026 16:26:58 -0500 Subject: [PATCH] refactor(dock door scanning): fixes and final writes for the intial trial went smooth --- .../dockdoor.closeLoadingOrder.route.ts | 57 +++++++- .../dockdoor.socket.notifications.ts | 17 +++ .../dockdoorScanning/dockdoor.socket.seed.ts | 20 +++ backend/logger/logger.controller.ts | 9 +- backend/logger/logger.socket.notifications.ts | 24 ++++ frontend/src/components/Sidebar/Warhouse.tsx | 16 +-- frontend/src/components/Sidebar/sidebar.tsx | 13 +- frontend/src/hooks/socket.io.hook.ts | 135 +++++++++++------- .../warehouse/dockdoorscanning/index.tsx | 7 +- .../dockdoorscanning/scans/$dockScans.tsx | 94 ++++++++---- 10 files changed, 290 insertions(+), 102 deletions(-) create mode 100644 backend/dockdoorScanning/dockdoor.socket.notifications.ts create mode 100644 backend/dockdoorScanning/dockdoor.socket.seed.ts create mode 100644 backend/logger/logger.socket.notifications.ts diff --git a/backend/dockdoorScanning/dockdoor.closeLoadingOrder.route.ts b/backend/dockdoorScanning/dockdoor.closeLoadingOrder.route.ts index 9d37751..cbb4033 100644 --- a/backend/dockdoorScanning/dockdoor.closeLoadingOrder.route.ts +++ b/backend/dockdoorScanning/dockdoor.closeLoadingOrder.route.ts @@ -16,7 +16,58 @@ const endLoading = z.object({ }); r.post("/", async (req, res) => { - // TODO: setup the emitter to just emit the data when we post to the db + if (req.body.clear) { + // just clear the loading order and clear out all the pallets to keep it clean. + await tryCatch( + db + .update(dockDoorScans) + .set({ + status: "completed", + upd_date: sql`NOW()`, + upd_user: req.user?.username ?? "lst-dock-system", + }) + .where( + req.body.loadingOrder + ? eq(dockDoorScanners.currentLoadingOrder, req.body.loadingOrder) + : undefined, + ) + .returning(), + ); + + const { data, error } = await tryCatch( + db + .update(dockDoorScanners) + .set({ + currentLoadingOrder: "", + upd_date: sql`NOW()`, + upd_user: req.user?.username ?? "lst-dock-system", + }) + .where(eq(dockDoorScanners.dockId, req.body.dockId)) + .returning(), + ); + + if (error) { + return apiReturn(res, { + success: false, + level: "error", + module: "dockdoor", + subModule: "loadingOrder", + message: `Failed to updating the dock.`, + data: (error as any) ?? [], + status: 400, + }); + } + + return apiReturn(res, { + success: true, + level: "info", + module: "dockdoor", + subModule: "loadingOrder", + message: `Loading order: ${req.body.loadingOrder} was just cleared out do to the process being completed in some other means. \nThis includes any scanned pallets as well.`, + data: data ?? [], + status: 200, + }); + } try { const validated = endLoading.parse(req.body); @@ -73,7 +124,9 @@ r.post("/", async (req, res) => { upd_date: sql`NOW()`, upd_user: req.user?.username ?? "lst-dock-system", }) - .where(eq(dockDoorScanners.currentLoadingOrder, validated.loadingOrder)) + .where( + eq(dockDoorScans.loadingOrder, validated.loadingOrder.toString()), + ) .returning(), ); diff --git a/backend/dockdoorScanning/dockdoor.socket.notifications.ts b/backend/dockdoorScanning/dockdoor.socket.notifications.ts new file mode 100644 index 0000000..035f4d0 --- /dev/null +++ b/backend/dockdoorScanning/dockdoor.socket.notifications.ts @@ -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); + } +} diff --git a/backend/dockdoorScanning/dockdoor.socket.seed.ts b/backend/dockdoorScanning/dockdoor.socket.seed.ts new file mode 100644 index 0000000..ba43dbf --- /dev/null +++ b/backend/dockdoorScanning/dockdoor.socket.seed.ts @@ -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, + }); +}; diff --git a/backend/logger/logger.controller.ts b/backend/logger/logger.controller.ts index 9d7a122..936a6b9 100644 --- a/backend/logger/logger.controller.ts +++ b/backend/logger/logger.controller.ts @@ -3,7 +3,6 @@ import { Writable } from "node:stream"; import pino, { type Logger } from "pino"; import { db } from "../db/db.controller.js"; import { logs } from "../db/schema/logs.schema.js"; -import { emitToRoom } from "../socket.io/roomEmitter.socket.js"; import { tryCatch } from "../utils/trycatch.utils.js"; import { notifySystemIssue } from "./logger.notify.js"; //import build from "pino-abstract-transport"; @@ -50,10 +49,10 @@ const dbStream = new Writable({ notifySystemIssue(obj); } - if (obj.room) { - emitToRoom(obj.room, res.data ? res.data[0] : obj); - } - emitToRoom("logs", res.data ? res.data[0] : obj); + // if (obj.room) { + // emitToRoom(obj.room, res.data ? res.data[0] : obj); + // } + // emitToRoom("logs", res.data ? res.data[0] : obj); callback(); } catch (err) { console.error("DB log insert error:", err); diff --git a/backend/logger/logger.socket.notifications.ts b/backend/logger/logger.socket.notifications.ts new file mode 100644 index 0000000..5318d25 --- /dev/null +++ b/backend/logger/logger.socket.notifications.ts @@ -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); +} diff --git a/frontend/src/components/Sidebar/Warhouse.tsx b/frontend/src/components/Sidebar/Warhouse.tsx index d12f74e..203b7f1 100644 --- a/frontend/src/components/Sidebar/Warhouse.tsx +++ b/frontend/src/components/Sidebar/Warhouse.tsx @@ -1,7 +1,7 @@ -import { useQuery } from "@tanstack/react-query"; +//import { useQuery } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { ChevronRight, Link as link } from "lucide-react"; -import { permissionQuery } from "../../lib/queries/permsCheck"; +//import { permissionQuery } from "../../lib/queries/permsCheck"; import { Collapsible, CollapsibleContent, @@ -21,11 +21,11 @@ import { } from "../ui/sidebar"; export default function WarehouseBar() { - const { data: canCreate = false } = useQuery( - permissionQuery({ - warehouse: ["read"], - }), - ); + // const { data: canCreate = false } = useQuery( + // permissionQuery({ + // warehouse: ["read"], + // }), + // ); const { setOpen } = useSidebar(); const items = [ @@ -33,7 +33,7 @@ export default function WarehouseBar() { title: "Dock Door Scanning", url: "/warehouse", //icon, - isActive: canCreate, + isActive: true, items: [ { title: "DockDoorScanning", diff --git a/frontend/src/components/Sidebar/sidebar.tsx b/frontend/src/components/Sidebar/sidebar.tsx index b0f5483..f309187 100644 --- a/frontend/src/components/Sidebar/sidebar.tsx +++ b/frontend/src/components/Sidebar/sidebar.tsx @@ -24,11 +24,11 @@ export function AppSidebar() { }), ); - const { data: canReadWarehouse = false } = useQuery( - permissionQuery({ - warehouse: ["read"], - }), - ); + // const { data: canReadWarehouse = false } = useQuery( + // permissionQuery({ + // warehouse: ["read"], + // }), + // ); return ( n.name === "dockDoorScanning")[0] - ?.active && - canReadWarehouse && } + ?.active && } {session && (session.user.role === "admin" || diff --git a/frontend/src/hooks/socket.io.hook.ts b/frontend/src/hooks/socket.io.hook.ts index 4227953..7f21c13 100644 --- a/frontend/src/hooks/socket.io.hook.ts +++ b/frontend/src/hooks/socket.io.hook.ts @@ -1,68 +1,110 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; import socket from "@/lib/socket.io"; +type RoomParams = Record; + +type JoinRoomPayload = { + room: string; + params?: RoomParams; +}; + type RoomUpdatePayload = { roomId: string; payloads: T[]; + type: string; +}; + +type RoomJoinedPayload = { + room: string; + roomId: string; }; type RoomErrorPayload = { + room?: string; roomId?: string; message?: string; }; -type UpdateMode = "append" | "replace"; - -export function useSocketRoom( - roomId: string, - getKey?: (item: T) => string | number, - updateMode: UpdateMode = "append", -) { +export function useSocketRoom(room: string, params?: RoomParams) { + const [actualRoomId, setActualRoomId] = useState(null); const [data, setData] = useState([]); const [info, setInfo] = useState( "No data yet — join the room to start receiving", ); - const clearRoom = useCallback( - (id?: string | number) => { - if (id !== undefined && getKey) { - setData((prev) => prev.filter((item) => getKey(item) !== id)); - setInfo(`Removed item ${id}`); - return; - } - console.log("cleared data from the room"); - setData([]); - setInfo("Room data cleared"); - }, - [getKey], + // This is the payload we send to the server. + // Example: + // { room: "inventory", params: { location: "ppoo" } } + const joinPayload = useMemo( + () => ({ + room, + params, + }), + [room, params], ); + const clearRoom = useCallback((filterFn?: (item: T) => boolean) => { + if (filterFn) { + setData((prev) => prev.filter((item) => !filterFn(item))); + return; + } + + setData([]); + setInfo("Room data cleared"); + }, []); + useEffect(() => { - function handleConnect() { - socket.emit("join-room", roomId); - setInfo(`Joined room: ${roomId}`); + // Join the logical room. + // The server decides the real Socket.IO roomId. + // Example: + // client sends: { room: "inventory", params: { location: "ppoo" } } + // server joins: "inventory:ppoo" + function joinRoom() { + socket.emit("join-room", joinPayload); + setInfo(`Joining room: ${room}`); + } + + // Server should emit this after socket.join(actualRoom). + // This lets the client know the final roomId to filter updates by. + function handleJoined(payload: RoomJoinedPayload) { + //if (payload.room !== room) return; + + setActualRoomId(payload.roomId); + setInfo(`Joined room: ${payload.roomId}`); } function handleUpdate(payload: RoomUpdatePayload) { - // protects against other room updates hitting this hook - if (payload.roomId !== roomId) return; + // If we know the actual roomId, only accept updates for that room. + // This protects against other pages/rooms also listening to "room-update". - // resetting room data for rooms that just need updated data. - if (updateMode === "replace") { + if (!actualRoomId) return; + + if (payload.roomId !== actualRoomId) return; + + if (payload.type === "snapshot") { setData(payload.payloads); - } else { - setData((prev) => [...payload.payloads, ...prev]); + return; } + // Append mode is good for logs/scans/events. + setData((prev) => [...payload.payloads, ...prev]); setInfo(""); } function handleError(err: RoomErrorPayload) { - if (err.roomId && err.roomId !== roomId) return; + // Ignore errors for other logical rooms. + if (err.room && err.room !== room) return; + + // Ignore errors for other actual rooms. + if (err.roomId && room && err.roomId !== room) return; + + toast.error(err.message); setInfo(err.message ?? "Room error"); } - socket.on("connect", handleConnect); + socket.on("connect", joinRoom); + socket.on("room-joined", handleJoined); socket.on("room-update", handleUpdate); socket.on("room-error", handleError); @@ -70,31 +112,26 @@ export function useSocketRoom( socket.connect(); } - // If already connected, join immediately + // If socket is already connected, join immediately. if (socket.connected) { - socket.emit("join-room", roomId); - setInfo(`Joined room: ${roomId}`); + joinRoom(); } return () => { - socket.emit("leave-room", roomId); + // Leave using the same logical room payload. + // Server should rebuild the actual room and call socket.leave(actualRoom). + socket.emit("leave-room", joinPayload); - socket.off("connect", handleConnect); + socket.off("connect", joinRoom); + socket.off("room-joined", handleJoined); socket.off("room-update", handleUpdate); socket.off("room-error", handleError); }; - }, [roomId, updateMode]); + }, [room, joinPayload, actualRoomId]); - return { data, info, clearRoom }; + return { + data, + info, + clearRoom, + }; } - -/* - -const isDockDoorPage = location.pathname.startsWith("/dockdoor"); - -useSocketRoom( - dockId ? `dockdoor:${dockId}` : null, - isDockDoorPage, -); - -*/ diff --git a/frontend/src/routes/warehouse/dockdoorscanning/index.tsx b/frontend/src/routes/warehouse/dockdoorscanning/index.tsx index 02708dd..c7b6fa8 100644 --- a/frontend/src/routes/warehouse/dockdoorscanning/index.tsx +++ b/frontend/src/routes/warehouse/dockdoorscanning/index.tsx @@ -21,6 +21,7 @@ export const finishLoadingOrder = async ( dockId: string, refetch: any, refetchActiveLoading: any, + clear?: boolean, ) => { try { const res = await api.post( @@ -28,6 +29,7 @@ export const finishLoadingOrder = async ( { loadingOrder: loadingOrder, dockId: dockId, + clear, }, { validateStatus: (status) => status < 500 }, ); @@ -80,8 +82,8 @@ function RouteComponent() { (x: any) => x.id === Number(i.currentLoadingOrder), ) : []; - console.log(loadingPlan); - console.log(loadingPlanItems); + // console.log(loadingPlan); + // console.log(loadingPlanItems); return ( diff --git a/frontend/src/routes/warehouse/dockdoorscanning/scans/$dockScans.tsx b/frontend/src/routes/warehouse/dockdoorscanning/scans/$dockScans.tsx index 38d671a..8f4f071 100644 --- a/frontend/src/routes/warehouse/dockdoorscanning/scans/$dockScans.tsx +++ b/frontend/src/routes/warehouse/dockdoorscanning/scans/$dockScans.tsx @@ -2,6 +2,7 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { createColumnHelper } from "@tanstack/react-table"; import { formatInTimeZone } from "date-fns-tz"; +import { useEffect, useMemo } from "react"; import { toast } from "sonner"; import { Button } from "../../../../components/ui/button"; import { useSocketRoom } from "../../../../hooks/socket.io.hook"; @@ -21,11 +22,25 @@ export const Route = createFileRoute( }); function RouteComponent() { - const { dockScans } = Route.useParams(); - const { data: logs, clearRoom } = useSocketRoom( - `dockDoorLoading:${dockScans}`, + const { data: canSee = false } = useQuery( + permissionQuery({ + warehouse: ["update"], + }), ); const { data, refetch } = useSuspenseQuery(getActiveDockScanners()); + const { dockScans } = Route.useParams(); + const params = useMemo( + () => ({ + dockId: dockScans, + loadingOrder: data[0].currentLoadingOrder ?? undefined, + }), + [dockScans, data], + ); + const { data: logs, clearRoom } = useSocketRoom( + `dockDoorLoading`, + params, + ); + const { data: loadingPlanItems, refetch: refetchActiveLoading } = useSuspenseQuery(getActiveLoadingOrders()); @@ -36,6 +51,24 @@ function RouteComponent() { ); const columnHelper = createColumnHelper(); + const logCount = logs.length; + + // TODO: move this to an onMessage: handFunction + /* + const handleLogMessage = useCallback(() => { + refetchActiveLoading(); + }, [refetchActiveLoading]); + + const { data: logs } = useSocketRoom("logs", { + onMessage: handleLogMessage, + }); + */ + + // biome-ignore lint: false + useEffect(() => { + refetchActiveLoading(); + }, [logCount, refetchActiveLoading]); + const column = [ columnHelper.accessor("loadingOrder", { header: ({ column }) => ( @@ -147,33 +180,36 @@ function RouteComponent() {

-
-
{ - e.preventDefault(); - form.handleSubmit(); - }} - > -
-
- - {(field) => ( - - )} - + {canSee && ( +
+ { + e.preventDefault(); + form.handleSubmit(); + }} + > +
+
+ + {(field) => ( + + )} + +
+
+ + Submit + +
-
- - Submit - -
-
- -
+ +
+ )} + {loadingPlan && loadingPlan.length > 0 && (