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() {