8 Commits

Author SHA1 Message Date
db28635c8c fix(mobile): ui over lapping
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m1s
the ui elements would over lap and cause visual issues with the scanning and seeing the old labels

closes #25
2026-05-27 20:57:49 -05:00
bcdf9566bc refactor(mobile): moved logout to the tab bar 2026-05-27 20:56:35 -05:00
c15ee070e7 refactor(mobile): setup - added button to go home as it caused confustion 2026-05-27 20:56:08 -05:00
347edb7078 fix(mobile users): corrected and endpoint that prevented us from change the pin number 2026-05-27 20:55:25 -05:00
fe0b1573f3 feat(mobile): dock door scanning backend added
ref #12
2026-05-27 20:54:47 -05:00
9c0ef1f5df fix(mobile): scan log incorrect user ref 2026-05-27 20:53:07 -05:00
8b076949a7 feat(warehousing): ppoo monitoring added
this will monitor ppoo every 45 seconds as long as someone is on the page.

closes #13
2026-05-27 20:52:34 -05:00
6d0fb8aee4 feat(mobile): added auto download of latest
this will predownload the latest if its there, this will speed up the update as the user will only
need to scan a single command and it will install restart app
2026-05-27 20:50:07 -05:00
44 changed files with 6411 additions and 281 deletions

View File

@@ -0,0 +1,22 @@
import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const dockDoorScanners = pgTable("dock_door_scanners", {
id: uuid("id").defaultRandom().primaryKey(),
ip: text("ip").notNull(),
name: text("name").unique(),
dockId: text("dock_id"),
active: boolean("active").default(true),
currentLoadingOrder: text("current_loading_order").default(""),
add_date: timestamp("add_date").defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});
export const dockDoorScannersSchema = createSelectSchema(dockDoorScanners);
export const newDockDoorScannersSchema = createInsertSchema(dockDoorScanners);
export type DockDoorScanners = z.infer<typeof dockDoorScannersSchema>;
export type NewDockDoorScanners = z.infer<typeof newDockDoorScannersSchema>;

View File

@@ -0,0 +1,35 @@
import { addDays, subDays } from "date-fns";
import { format } from "date-fns-tz";
import { Router } from "express";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.get("/", async (_, res) => {
const orders = await runProdApi({
method: "post",
endpoint: "/public/v1.0/OutboundDeliveries/LoadingOrders/Search",
data: [
{
loadingDateFrom: format(subDays(new Date(Date.now()), 3), "yyyy-MM-dd"),
loadingDateTo: format(addDays(new Date(Date.now()), 3), "yyyy-MM-dd"),
states: [
1, // planned
],
//isCommissioned: true,
},
],
});
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "current Active loading orders",
message: `Current active loading loaders.`,
data: orders?.data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,18 @@
import { Router } from "express";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
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,
});
});
export default r;

View File

@@ -0,0 +1,89 @@
// sends the units from the dock door scanner here.
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
// validate we are active
type Data = {
dockId?: string;
sscc?: string;
runningNr?: string;
};
export 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"),
});
if (!dockDoorActive?.active) {
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadunit",
message: "Dock door scanning feature is not active.",
data: [],
notify: false,
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 === "") {
return returnFunc({
success: true,
level: "error",
module: "dockdoor",
subModule: "loadingOrders",
message:
"There are know current active loading orders please start one and try again.",
data: [],
notify: false,
room: `dockDoorLoading${data.dockId}`,
});
}
// TODO: pallet validation, check if we are on hold, then check if we have been in the staging warehouse for more than x time.
// if on hold stop the scan and send a bad read with the reason its on hold and what its on hold for, including coa.
// if precheck is active then check if we have a warehouse, then check if the pallet was in the warehouse for greater than the define min, all fails send a warning and still do the scan
// add the loading units
try {
const prod = await runProdApi({
method: "post",
endpoint: `/public/v1.0/OutboundDeliveries/LoadingOrders/${dock[0]?.currentLoadingOrder}/LoadUnit`,
data: [{ sscc: data.sscc?.slice(2) }],
});
console.log(prod?.data);
} catch (error) {
console.log(error);
}
};

View File

@@ -0,0 +1,43 @@
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 startLoad from "./dockdoor.startLoad.route.js";
import prodDocks from "./dockdoors.docks.route.js";
import docks from "./dockdoors.route.js";
export const setupDockDoorRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(
`${baseUrl}/api/dockDoor/scanners`,
featureCheck("dockDoorScanning"),
docks,
);
app.use(
`${baseUrl}/api/dockDoor/closeLoadingOrder`,
featureCheck("dockDoorScanning"),
closeLoadingOrder,
);
app.use(
`${baseUrl}/api/dockDoor/activeLoadingOrders`,
featureCheck("dockDoorScanning"),
activeLoadingOrders,
);
app.use(
`${baseUrl}/api/dockDoor/startLoad`,
featureCheck("dockDoorScanning"),
startLoad,
);
app.use(
`${baseUrl}/api/dockDoor/docks`,
featureCheck("dockDoorScanning"),
prodDocks,
);
// TODO : add manual way to add pallets
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,65 @@
import { 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 startLoading = z.object({
loadingOrder: z.string(),
dockId: z.string(),
});
r.post("/", async (req, res) => {
try {
const validated = startLoading.parse(req.body);
const { data, error } = await tryCatch(
db
.update(dockDoorScanners)
.set({
currentLoadingOrder: validated.loadingOrder,
upd_date: sql`NOW()`,
upd_user: req.user?.username,
})
.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 added to dockId ${validated.dockId}.`,
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;

View File

@@ -0,0 +1,54 @@
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 (_, res) => {
const activeDocks = sqlQuerySelector(`outbound.docks`) as SqlQuery;
if (!activeDocks.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "docks",
message: `There was an error getting the docks query.`,
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(activeDocks.query, "Current Active Docks"),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error getting the docks.`,
data: (error as any) ?? ([] as any),
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "docks",
message: `Current active docks.`,
data: (data.data as any) ?? ([] as any),
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,76 @@
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
const newDockScanner = z.object({
ip: z.string(),
name: z.string(),
dockId: z.string(),
});
r.get("/", async (_, res) => {
try {
const docks = await db
.select()
.from(dockDoorScanners)
.orderBy(dockDoorScanners.name);
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `All dock Doors.`,
data: docks ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error adding in the new dock.`,
data: error ?? ([] as any),
status: 200,
});
}
});
r.post("/", requireAuth, async (req, res) => {
try {
const validated = newDockScanner.parse(req.body);
const newDock = await db
.insert(dockDoorScanners)
.values(validated)
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `${validated.name} was just added.`,
data: newDock ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error adding in the new dock.`,
data: error ?? ([] as any),
status: 200,
});
}
});
export default r;

View File

@@ -13,7 +13,7 @@ router.post("/", async (req, res) => {
await db await db
.update(scanUser) .update(scanUser)
.set({ lastScan: sql`NOW()` }) .set({ lastScan: sql`NOW()` })
.where(eq(scanUser.name, body.name)); .where(eq(scanUser.name, body.user));
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -0,0 +1,6 @@
USE [test1_AlplaPROD2.0_Read]
SELECT *
FROM [masterData].[Dock] (nolock)
where active = 1
order by Description desc

View File

@@ -4,6 +4,7 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups // import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js"; import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js"; import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupDockDoorRoutes } from "./dockdoorScanning/dockdoor.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js"; import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js"; import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js"; import { setupNotificationRoutes } from "./notification/notification.routes.js";
@@ -29,4 +30,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupNotificationRoutes(baseUrl, app); setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app); setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app); setupTCPRoutes(baseUrl, app);
setupDockDoorRoutes(baseUrl, app);
}; };

View File

@@ -26,6 +26,7 @@ import {
} from "./utils/analyticRouteHits.utils.js"; } from "./utils/analyticRouteHits.utils.js";
import { createCronJob } from "./utils/croner.utils.js"; import { createCronJob } from "./utils/croner.utils.js";
import { sendEmail } from "./utils/sendEmail.utils.js"; import { sendEmail } from "./utils/sendEmail.utils.js";
import { ppooMonitoring } from "./warehousing/warehousing.ppooMonitor.js";
const port = Number(process.env.PORT) || 3000; const port = Number(process.env.PORT) || 3000;
export let systemSettings: Setting[] = []; export let systemSettings: Setting[] = [];
@@ -78,6 +79,10 @@ const start = async () => {
runRouteHitAnalyticsCron(), runRouteHitAnalyticsCron(),
); );
createCronJob("ppooMonitor", "*/45 * * * * *", async () =>
ppooMonitoring(),
);
createCronJob("cleanHitsUp", "0 0 7 * * *", () => cleanupOldRouteHits()); createCronJob("cleanHitsUp", "0 0 7 * * *", () => cleanupOldRouteHits());
// one shots only needed to run on server startups // one shots only needed to run on server startups
createNotifications(); createNotifications();

View File

@@ -1,4 +1,4 @@
import type { RoomId } from "./types.socket.js"; import type { RoomId } from "./roomDefinitions.socket.js";
export const MAX_HISTORY = 50; export const MAX_HISTORY = 50;
export const FLUSH_INTERVAL = 100; // 50ms change higher if needed export const FLUSH_INTERVAL = 100; // 50ms change higher if needed

View File

@@ -1,17 +1,50 @@
import { desc } from "drizzle-orm"; import { desc } from "drizzle-orm";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js"; import { logs } from "../db/schema/logs.schema.js";
import type { RoomId } from "./types.socket.js"; import { ppoRun } from "../warehousing/warehousing.ppooMonitor.js";
type RoomDefinition<T = unknown> = { type RoomDefinition<T = unknown> = {
seed: (limit: number) => Promise<T[]>; seed: (limit: number) => Promise<T[]>;
}; };
export const protectedRooms: any = { 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"] }, logs: { requiresAuth: true, role: ["admin", "systemAdmin"] },
//admin: { requiresAuth: false, 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> = { export const roomDefinition: Record<RoomId, RoomDefinition> = {
logs: { logs: {
seed: async (limit) => { seed: async (limit) => {
@@ -48,4 +81,14 @@ export const roomDefinition: Record<RoomId, RoomDefinition> = {
return []; return [];
}, },
}, },
ppoo: {
seed: async (limit) => {
console.log(limit);
return {
type: "snapshot",
items: await ppoRun(),
createdAt: new Date().toISOString(),
} as any;
},
},
}; };

View File

@@ -1,6 +1,6 @@
// the emitter setup // the emitter setup
import type { RoomId } from "./types.socket.js"; import type { RoomId } from "./roomDefinitions.socket.js";
let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null; let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null;

View File

@@ -7,18 +7,33 @@ import {
roomFlushTimers, roomFlushTimers,
roomHistory, roomHistory,
} from "./roomCache.socket.js"; } from "./roomCache.socket.js";
import { roomDefinition } from "./roomDefinitions.socket.js"; import { type RoomId, roomDefinition } from "./roomDefinitions.socket.js";
import type { RoomId } from "./types.socket.js";
// get the db data if not exiting already // get the db data if not exiting already
const log = createLogger({ module: "socket.io", subModule: "roomService" }); 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) => { export const preseedRoom = async (roomId: RoomId) => {
if (roomHistory.has(roomId)) { if (roomHistory.has(roomId)) {
return roomHistory.get(roomId); return roomHistory.get(roomId);
} }
const roomDef = roomDefinition[roomId]; const roomDef = roomDefinition[roomId] as any;
if (!roomDef) { if (!roomDef) {
log.error({}, `Room ${roomId} is not defined`); log.error({}, `Room ${roomId} is not defined`);
@@ -32,7 +47,7 @@ export const preseedRoom = async (roomId: RoomId) => {
}; };
export const createRoomEmitter = (io: Server) => { export const createRoomEmitter = (io: Server) => {
const addDataToRoom = <T>(roomId: RoomId, payload: T) => { const addDataToRoom = <T>(roomId: RoomId, payload: T[]) => {
if (!roomHistory.has(roomId)) { if (!roomHistory.has(roomId)) {
roomHistory.set(roomId, []); roomHistory.set(roomId, []);
} }

View File

@@ -7,7 +7,11 @@ import { Server } from "socket.io";
import { createLogger } from "../logger/logger.controller.js"; import { createLogger } from "../logger/logger.controller.js";
import { allowedOrigins } from "../utils/cors.utils.js"; import { allowedOrigins } from "../utils/cors.utils.js";
import { registerEmitter } from "./roomEmitter.socket.js"; import { registerEmitter } from "./roomEmitter.socket.js";
import { createRoomEmitter, preseedRoom } from "./roomService.socket.js"; import {
createRoomEmitter,
preseedRoom,
registerRoomService,
} from "./roomService.socket.js";
//const __filename = fileURLToPath(import.meta.url); //const __filename = fileURLToPath(import.meta.url);
//const __dirname = dirname(__filename); //const __dirname = dirname(__filename);
@@ -15,7 +19,7 @@ const log = createLogger({ module: "socket.io", subModule: "setup" });
import { auth } from "../utils/auth.utils.js"; import { auth } from "../utils/auth.utils.js";
//import type { Session, User } from "better-auth"; // adjust if needed //import type { Session, User } from "better-auth"; // adjust if needed
import { protectedRooms } from "./roomDefinitions.socket.js"; import { getRoomConfig } from "./roomDefinitions.socket.js";
// declare module "socket.io" { // declare module "socket.io" {
// interface Socket { // interface Socket {
@@ -33,6 +37,9 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
}, },
}); });
// manage members of the rooms.
registerRoomService(io);
// ✅ Create emitter instance // ✅ Create emitter instance
const { addDataToRoom } = createRoomEmitter(io); const { addDataToRoom } = createRoomEmitter(io);
registerEmitter(addDataToRoom); registerEmitter(addDataToRoom);
@@ -78,38 +85,76 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
version: "1.0.0", version: "1.0.0",
}); });
s.on("join-room", async (rn) => { // s.on("join-room", async (rn) => {
const config = protectedRooms[rn]; // const config = protectedRooms[rn];
if (config?.requiresAuth && !s.user) { // 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);
if (!config) {
return s.emit("room-error", { return s.emit("room-error", {
room: rn, roomId: rn,
message: `Unknown room: ${rn}`,
});
}
if (config.requiresAuth && !s.user) {
return s.emit("room-error", {
roomId: rn,
message: "Authentication required", message: "Authentication required",
}); });
} }
const roles = Array.isArray(config?.role) ? config?.role : [config?.role]; const roles = Array.isArray(config.role) ? config.role : [];
//if (config?.role && s.user?.role !== config.role) { if (roles.length > 0 && !roles.includes(s.user?.role)) {
if (config?.role && !roles.includes(s.user?.role)) {
return s.emit("room-error", { return s.emit("room-error", {
roomId: rn, roomId: rn,
message: `Not authorized to be in room: ${rn}`, message: `Not authorized to be in room: ${rn}`,
}); });
} }
s.join(rn); s.join(rn);
// get room seeded const history = await preseedRoom(rn as any);
const history = await preseedRoom(rn);
log.info({}, `User joined ${rn}: ${s.id}`); log.info({}, `User joined ${rn}: ${s.id}`);
// send the intial data
s.emit("room-update", { s.emit("room-update", {
roomId: rn, roomId: rn,
payloads: history, payloads: history,
initial: true, initial: true,
}); });
}); });
s.on("leave-room", (room) => { s.on("leave-room", (room) => {
s.leave(room); s.leave(room);
log.info({}, `${s.id} left room: ${room}`); log.info({}, `${s.id} left room: ${room}`);

View File

@@ -1 +0,0 @@
export type RoomId = "logs" | "labels" | "admin" | "admin:build"; //| "alerts" | "metrics";

View File

@@ -162,6 +162,17 @@ const servers: NewServerData[] = [
serverLoc: "D$\\LST_V3", serverLoc: "D$\\LST_V3",
buildNumber: 1, buildNumber: 1,
}, },
{
name: "Salt Lake City",
server: "USSLC1VMS006",
plantToken: "usslc1",
idAddress: "10.202.0.26",
greatPlainsPlantCode: "70",
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 // notes here for when it comes time to pull in all the server address info [test1_AlplaPROD2.0_Read].[masterData].[Plant] has everything from every server :D

View File

@@ -86,8 +86,40 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, seedVersion: 1,
}, },
{
name: "dockDoorScanning",
value: "0",
active: false,
description: "dock door scanning",
moduleName: "dockDoorScanning",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings // standard settings
{
name: "stagingWarehouse",
value: "30218",
active: true,
description:
"The warehouse we will use for staging, validation that we did our prechecks if required",
moduleName: "dockDoorScanning",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "precheck",
value: "5",
active: false,
description:
"Precheck is required, the value is in minute, 5 min should be 5",
moduleName: "dockDoorScanning",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{ {
name: "prolinkCheck", name: "prolinkCheck",
value: "1", value: "1",

View File

@@ -1,7 +1,9 @@
import net from "node:net"; import net from "node:net";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { printerData } from "../db/schema/printers.schema.js"; import { printerData } from "../db/schema/printers.schema.js";
import { loadUnit } from "../dockdoorScanning/dockdoor.loadUnits.js";
import { createLogger } from "../logger/logger.controller.js"; import { createLogger } from "../logger/logger.controller.js";
import { delay } from "../utils/delay.utils.js"; import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js"; import { returnFunc } from "../utils/returnHelper.utils.js";
@@ -14,6 +16,7 @@ export let isServerRunning = false;
const port = parseInt(process.env.TCP_PORT ?? "2222", 10); const port = parseInt(process.env.TCP_PORT ?? "2222", 10);
// This is the parser for zebra scanners
const parseTcpAlert = (input: string) => { const parseTcpAlert = (input: string) => {
// guard // guard
const colonIndex = input.indexOf(":"); const colonIndex = input.indexOf(":");
@@ -74,6 +77,24 @@ export const startTCPServer = async () => {
printerListen(printerData as PrinterData); printerListen(printerData as PrinterData);
} }
// 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:", ""))) {
console.log("dock door logic");
const currentDock = dockdoorScanners.filter(
(s) => s.ip === ip.replace("::ffff:", ""),
);
// send the data + dock scan over
loadUnit({
dockId: currentDock[0]?.dockId ?? "0",
sscc: data.toString(),
});
}
}); });
socket.on("end", () => { socket.on("end", () => {

View File

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

View File

@@ -0,0 +1,29 @@
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { hasRoomMembers } from "../socket.io/roomService.socket.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
export const ppoRun = async () => {
const laneData = await runProdApi({
method: "post",
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",
data: [
{
laneIds: ["0"],
},
],
});
return laneData?.data ?? [];
};
export const ppooMonitoring = async () => {
if (!hasRoomMembers(`ppoo`)) {
return;
}
emitToRoom("ppoo", {
type: "snapshot",
items: await ppoRun(),
createdAt: new Date().toISOString(),
} as any);
};

View File

@@ -13,6 +13,7 @@ type RoomErrorPayload = {
export function useSocketRoom<T>( export function useSocketRoom<T>(
roomId: string, roomId: string,
enabled = true,
getKey?: (item: T) => string | number, getKey?: (item: T) => string | number,
) { ) {
const [data, setData] = useState<T[]>([]); const [data, setData] = useState<T[]>([]);
@@ -35,6 +36,7 @@ export function useSocketRoom<T>(
); );
useEffect(() => { useEffect(() => {
if (!roomId || !enabled) return;
function handleConnect() { function handleConnect() {
socket.emit("join-room", roomId); socket.emit("join-room", roomId);
setInfo(`Joined room: ${roomId}`); setInfo(`Joined room: ${roomId}`);
@@ -74,7 +76,18 @@ export function useSocketRoom<T>(
socket.off("room-update", handleUpdate); socket.off("room-update", handleUpdate);
socket.off("room-error", handleError); socket.off("room-error", handleError);
}; };
}, [roomId]); }, [roomId, enabled]);
return { data, info, clearRoom }; return { data, info, clearRoom };
} }
/*
const isDockDoorPage = location.pathname.startsWith("/dockdoor");
useSocketRoom(
dockId ? `dockdoor:${dockId}` : null,
isDockDoorPage,
);
*/

View File

@@ -1,7 +1,6 @@
import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table"; import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import { Trash } from "lucide-react"; import { Trash } from "lucide-react";
import { Suspense, useState } from "react"; import { Suspense, useState } from "react";
@@ -56,7 +55,7 @@ const updateSettings = async (
) => { ) => {
//console.log(id, data); //console.log(id, data);
try { try {
const res = await axios.patch(`/mobile/auth/user/${id}`, data, { const res = await api.patch(`/mobile/auth/user/${id}`, data, {
withCredentials: true, withCredentials: true,
timeout: 15000, timeout: 15000,
validateStatus: () => true, validateStatus: () => true,
@@ -121,7 +120,7 @@ const ScanUserTable = () => {
<EditableCellInput <EditableCellInput
value={getValue()} value={getValue()}
id={row.original.id} id={row.original.id}
field="value" field="pinNumber"
onSubmit={({ id, field, value }) => { onSubmit={({ id, field, value }) => {
updateSetting.mutate({ id, field, value }); updateSetting.mutate({ id, field, value });
}} }}

View File

@@ -3,7 +3,7 @@
"name": "LST mobile", "name": "LST mobile",
"slug": "lst-mobile", "slug": "lst-mobile",
"version": "0.11.1-alpha", "version": "0.11.1-alpha",
"orientation": "portrait", "orientation": "default",
"icon": "./assets/icon_white.png", "icon": "./assets/icon_white.png",
"scheme": "lstmobile", "scheme": "lstmobile",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
@@ -15,10 +15,14 @@
"foregroundImage": "./assets/adaptive-icon-white.png", "foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"versionCode": 37, "versionCode": 39,
"minSupportedVersionCode": 33, "minSupportedVersionCode": 33,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile" "package": "net.alpla.lst.mobile",
"permissions": [
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE"
]
}, },
"web": { "web": {
"output": "static", "output": "static",

View File

@@ -35,6 +35,7 @@
"expo-image": "~55.0.8", "expo-image": "~55.0.8",
"expo-linking": "~55.0.13", "expo-linking": "~55.0.13",
"expo-router": "~55.0.12", "expo-router": "~55.0.12",
"expo-screen-orientation": "~55.0.16",
"expo-splash-screen": "~55.0.18", "expo-splash-screen": "~55.0.18",
"expo-status-bar": "~55.0.5", "expo-status-bar": "~55.0.5",
"expo-symbols": "~55.0.7", "expo-symbols": "~55.0.7",
@@ -46,6 +47,7 @@
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-blob-util": "^0.24.9",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "4.2.1", "react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2", "react-native-safe-area-context": "~5.6.2",
@@ -6132,6 +6134,11 @@
} }
} }
}, },
"node_modules/base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -8171,6 +8178,16 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/expo-screen-orientation": {
"version": "55.0.16",
"resolved": "https://registry.npmjs.org/expo-screen-orientation/-/expo-screen-orientation-55.0.16.tgz",
"integrity": "sha512-I9NIqb2zAkHsK/CxdmMdmgSFP7E1v++8z/Mj2X9j1AuK6l55yOma/JHo905KU3x2zPm9/l1BTzmMA320tiBebg==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-server": { "node_modules/expo-server": {
"version": "55.0.9", "version": "55.0.9",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.9.tgz", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.9.tgz",
@@ -12821,6 +12838,77 @@
} }
} }
}, },
"node_modules/react-native-blob-util": {
"version": "0.24.9",
"resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.24.9.tgz",
"integrity": "sha512-tG3+m0WhVdBGifvxSFxZDVqtr85D0fGBJU6E4UxmK3tU+RabJZTumXEn8k7jn5/NFe8OhQhPjtBEZ11ZJ6L7Vw==",
"license": "MIT",
"dependencies": {
"base-64": "0.1.0",
"glob": "13.0.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ronradtke"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-blob-util/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/react-native-blob-util/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/react-native-blob-util/node_modules/glob": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz",
"integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==",
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.1.2",
"minipass": "^7.1.2",
"path-scurry": "^2.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/react-native-blob-util/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/react-native-css-interop": { "node_modules/react-native-css-interop": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.2.3.tgz", "resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.2.3.tgz",

View File

@@ -45,6 +45,7 @@
"expo-image": "~55.0.8", "expo-image": "~55.0.8",
"expo-linking": "~55.0.13", "expo-linking": "~55.0.13",
"expo-router": "~55.0.12", "expo-router": "~55.0.12",
"expo-screen-orientation": "~55.0.16",
"expo-splash-screen": "~55.0.18", "expo-splash-screen": "~55.0.18",
"expo-status-bar": "~55.0.5", "expo-status-bar": "~55.0.5",
"expo-symbols": "~55.0.7", "expo-symbols": "~55.0.7",
@@ -56,6 +57,7 @@
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-blob-util": "^0.24.9",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "4.2.1", "react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2", "react-native-safe-area-context": "~5.6.2",

View File

@@ -1,12 +1,14 @@
import { Redirect, Tabs } from "expo-router"; import { Redirect, Tabs, useRouter } from "expo-router";
import { import {
Boxes, Boxes,
Container, Container,
Home, Home,
LogOut,
Logs, Logs,
Rows4, Rows4,
Settings, Settings,
} from "lucide-react-native"; } from "lucide-react-native";
import { Alert } from "react-native";
import { useAppStore } from "../../hooks/useAppStore"; import { useAppStore } from "../../hooks/useAppStore";
import { useMobileAuthStore } from "../../hooks/useMobileAuth"; import { useMobileAuthStore } from "../../hooks/useMobileAuth";
@@ -20,6 +22,8 @@ export default function TabsLayout() {
const serverPort = useAppStore((s) => s.serverPort); const serverPort = useAppStore((s) => s.serverPort);
const user = useMobileAuthStore((s) => s.user); const user = useMobileAuthStore((s) => s.user);
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked); const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
const logoutScanner = useMobileAuthStore((s) => s.logout);
const router = useRouter();
const port = parseInt(serverPort || "0", 10) >= 50000; const port = parseInt(serverPort || "0", 10) >= 50000;
@@ -36,6 +40,32 @@ export default function TabsLayout() {
return role ? allowed.includes(role) : false; return role ? allowed.includes(role) : false;
}; };
const logout = async () => {
try {
// optional confirm
Alert.alert("Logout", "Are you sure?", [
{ text: "Cancel", style: "cancel" },
{
text: "Logout",
style: "destructive",
onPress: async () => {
// clear auth/session
logoutScanner();
router.replace("/(tabs)/scanner");
// clear zustand/session stuff
//useAuthStore.getState().reset();
// maybe clear async storage too
// await AsyncStorage.clear();
},
},
]);
} catch (err) {
console.error(err);
}
};
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
@@ -62,10 +92,10 @@ export default function TabsLayout() {
name="ppoo" name="ppoo"
options={{ options={{
title: "PPOO", title: "PPOO",
href: // href:
isNormalScanner || !hasRole(["admin", "manager"]) // isNormalScanner || !hasRole(["admin", "manager"])
? null // ? null
: "/(tabs)/ppoo", // : "/(tabs)/ppoo",
tabBarIcon: ({ color, size }) => <Boxes size={size} color={color} />, tabBarIcon: ({ color, size }) => <Boxes size={size} color={color} />,
}} }}
/> />
@@ -73,10 +103,10 @@ export default function TabsLayout() {
name="laneCheck" name="laneCheck"
options={{ options={{
title: "Lane Check", title: "Lane Check",
href: // href:
isNormalScanner || !hasRole(["admin", "manager"]) // isNormalScanner || !hasRole(["admin", "manager"])
? null // ? null
: "/(tabs)/laneCheck", // : "/(tabs)/laneCheck",
tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />, tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />,
}} }}
/> />
@@ -112,6 +142,7 @@ export default function TabsLayout() {
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs", parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
}} }}
/> */} /> */}
<Tabs.Screen <Tabs.Screen
name="config" name="config"
options={{ options={{
@@ -121,6 +152,22 @@ export default function TabsLayout() {
), ),
}} }}
/> />
<Tabs.Screen
name="logout"
options={{
title: "Logout",
tabBarIcon: ({ color, size }) => <LogOut color={color} size={size} />,
}}
listeners={{
tabPress: (e) => {
// stop navigation
e.preventDefault();
// run logout logic
logout();
},
}}
/>
</Tabs> </Tabs>
); );
} }

View File

@@ -0,0 +1,3 @@
export default function LogoutScreen() {
return null;
}

View File

@@ -1,154 +1,83 @@
import axios from "axios";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import * as Device from "expo-device";
import { useFocusEffect } from "expo-router"; import { useFocusEffect } from "expo-router";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { ScrollView, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import { GlobalFooter } from "../../components/UpdateFooter";
import { Button } from "../../components/ui/button";
import { Card, CardContent, CardHeader } from "../../components/ui/card";
import { import {
Dialog, Button,
DialogContent, ScrollView,
DialogDescription, Text,
DialogHeader, useWindowDimensions,
DialogTitle, View,
DialogTrigger, } from "react-native";
} from "../../components/ui/dialog"; import { SafeAreaView } from "react-native-safe-area-context";
import { useAppStore } from "../../hooks/useAppStore"; import { Card, CardContent } from "../../components/ui/card";
import { scannerFeedback } from "../../lib/feedbackScan"; import { useSocketRoom } from "../../hooks/socket.io.hook";
import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner";
const InfoRow = ({ type PPOO = {
label, type: string;
value, items: any;
}: { createdAt: Date;
label: string;
value: React.ReactNode;
}) => {
return (
<View className="flex-row justify-between gap-4 py-2 border-b border-gray-200">
<Text className="text-sm text-gray-500">{label}</Text>
<Text className="text-sm font-medium text-gray-900 text-right flex-1">
{value}
</Text>
</View>
);
}; };
export default function PPOO() { export default function PPOO() {
const [units, setUnits] = useState<any>(null); const { data } = useSocketRoom<any>("ppoo", undefined, "replace") as any;
const serverIp = useAppStore((s) => s.serverIp); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
const handleScan = useCallback( const { width } = useWindowDimensions();
async (scan: ZebraScanResult) => { const isTablet =
setUnits(null); Device.modelName?.toLowerCase().includes("et40") ||
await scannerFeedback({ Device.modelName?.toLowerCase().includes("et45");
type: "scan",
sound: true,
vibrate: true,
led: true,
});
if (!scan.data.startsWith("loc")) {
Toast.show({
type: "error",
text1: "Scan error",
text2: "The last scan was not a lane please try again",
});
return; const columns = isTablet ? 3 : 1;
}
try {
const res = await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/lanecheck`,
{
lane: "loc#1#0<",
},
{
timeout: 5000,
},
);
if (res.status === 200) {
setUnits(res.data);
Toast.show({
type: "info",
text1: "Lane Data",
text2: "All Loading Units from this lane will be listed below",
});
}
} catch (error) {
console.log(error);
Toast.show({
type: "error",
text1: "Lane Data",
text2: "Error getting lane data please try again",
});
}
},
[serverIp.trim],
);
useFocusEffect( const gap = 8;
useCallback(() => { const cardWidth =
zebraScanner.startListening(); columns === 1 ? width - 16 : (width - gap * (columns + 1)) / columns;
const sub = zebraScanner.addScanListener((scan) => { const items = data?.items ?? [];
//console.log("SCAN:", scan); const sortedItems = useMemo(() => {
handleScan(scan); return [...items].sort((a, b) => {
const aDate = new Date(a.lastMovingDate).getTime();
const bDate = new Date(b.lastMovingDate).getTime();
return sortDir === "asc" ? aDate - bDate : bDate - aDate;
}); });
}, [items, sortDir]);
return () => { //console.log(logsInfo);
sub.remove();
zebraScanner.stopListening();
//setUnits(null);
};
}, [handleScan]),
);
return ( return (
<View <View className="flex items-center mt-2">
style={{ <View className="flex m-2">
//justifyContent: "center", <Button
alignItems: "center", onPress={() =>
marginTop: 50, setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))
}} }
> title={`Sort: ${sortDir}`}
{units ? ( />
// <SafeAreaView className={`flex-1 w-full items-center`}>
// <ScrollView className="w-full flex-1">
// <View className="flex items-center gap-2 w-full">
// {units.data?.map((i: any, index: any) => (
// <View key={`${i.runningNumber}-${index}`}>
// <Text>example</Text>
// </View>
// ))}
// </View>
// </ScrollView>
// </SafeAreaView>
<SafeAreaView className={`w-full items-center`}>
<View style={{ padding: 2 }}>
<Text>There Are {units.data.length} units in PPOO</Text>
</View> </View>
<ScrollView className="w-full" style={{ marginBottom: 20 }}> {sortedItems.length === 0 ? (
<View> <View className="flex items-center">
{units.data.map((i, index) => ( <Text>Loading PPOO...</Text>
<View </View>
key={`${i.runningNumber}-${index}`} ) : (
style={{ <SafeAreaView className="flex-1">
justifyContent: "center", <ScrollView className="w-full">
margin: 2, <View className="w-full flex-row flex-wrap gap-2 m-2">
}} {sortedItems.map((i: any) => {
> return (
<Dialog> <View key={i.runningNumber}>
<DialogTrigger>
<Card <Card
className="w-full" //className={isTablet ? "w-[32%]" : "w-full"}
style={{ style={{
borderColor: borderColor:
i.state === "QualityBlocked" ? "red" : undefined, i.mainDefectId === 864
? "blue"
: i.state === "QualityBlocked"
? "red"
: undefined,
borderWidth: 4, borderWidth: 4,
width: cardWidth,
}} }}
> >
<CardContent> <CardContent>
@@ -159,52 +88,18 @@ export default function PPOO() {
<Text> <Text>
Running Number: {i.runningNumber ?? "Non barcoded"} Running Number: {i.runningNumber ?? "Non barcoded"}
</Text> </Text>
<Text>
Date: {format(i.lastMovingDate, "M/d/yyyy HH:mm")}
</Text>
</CardContent> </CardContent>
</Card> </Card>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Details for Article {i.articleId}, Rn:
{i.runningNumber ?? "Non barcoded"}{" "}
</DialogTitle>
<DialogDescription>
<InfoRow
label="Production Date"
value={format(i.productionDate, "MM/dd/yyyy HH:mm")}
/>
<InfoRow label="Quantity" value={i.quantity} />
{i.state === "QualityBlocked" && (
<InfoRow
label="Defect"
value={i.mainDefectGroupDescription}
/>
)}
{i.state === "QualityBlocked" && (
<InfoRow
label="Description"
value={i.mainDefectDescription}
/>
)}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</View> </View>
))} );
})}
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
) : (
<View className="mt-50">
<Text className="text-2xl text-center">
Please scan a lane to see all Units that are in the lane.
</Text>
</View>
)} )}
<View>
<GlobalFooter />
</View>
</View> </View>
); );
} }

View File

@@ -5,12 +5,15 @@ import "../../global.css";
import { useEffect } from "react"; import { useEffect } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import useDeviceLock from "../hooks/useDeviceCheck"; import useDeviceLock from "../hooks/useDeviceCheck";
import { connectSocket } from "../lib/socket.io";
import { zebraScanner } from "../lib/ZebraScanner"; import { zebraScanner } from "../lib/ZebraScanner";
export default function RootLayout() { export default function RootLayout() {
useDeviceLock(); useDeviceLock();
useEffect(() => { useEffect(() => {
zebraScanner.ensureProfile(); zebraScanner.ensureProfile();
connectSocket();
}, []); }, []);
return ( return (

View File

@@ -99,9 +99,16 @@ export default function Setup() {
justifyContent: "center", justifyContent: "center",
padding: 3, padding: 3,
borderRadius: 8, borderRadius: 8,
gap: 3,
}} }}
> >
<Button title="Submit" onPress={authCheck} /> <Button title="Submit" onPress={authCheck} />
<Button
title="Home"
onPress={() => {
router.push("/(tabs)/scanner");
}}
/>
</View> </View>
</View> </View>
) : ( ) : (
@@ -145,14 +152,7 @@ export default function Setup() {
</View> </View>
)} )}
<View <View className="flex gap-2 flex-row">
style={{
flexDirection: "row",
justifyContent: "center",
padding: 3,
gap: 3,
}}
>
<Button title="Save Config" onPress={handleSave} /> <Button title="Save Config" onPress={handleSave} />
<Button <Button
title="Home" title="Home"

View File

@@ -182,14 +182,17 @@ export default function LSTScanner() {
}, [handleScan]), }, [handleScan]),
); );
return ( return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}> <View className={`${bgColor ?? ""} flex-1 w-full`}>
<View style={{ alignItems: "center", margin: 5 }}> <View className="flex gap-2 items-center">
<View className="flex flex-col gap-2 items-center">
<Text style={{ fontSize: 14, fontWeight: "600" }}> <Text style={{ fontSize: 14, fontWeight: "600" }}>
User: {formatName(user?.name ?? "")} Lst user: {formatName(user?.name ?? "")}
</Text> </Text>
<Text style={{ fontSize: 18, fontWeight: "600" }}> {/* <Text style={{ fontSize: 14, fontWeight: "600" }}>
LST Scanner id: {user?.scannerId} LST Scanner id: {user?.scannerId}
</Text> </Text> */}
</View>
<View <View
style={{ style={{
marginTop: 5, marginTop: 5,
@@ -197,8 +200,8 @@ export default function LSTScanner() {
}} }}
> >
{!lastScan ? ( {!lastScan ? (
<View style={{ marginTop: 10, alignItems: "center" }}> <View style={{ marginTop: 2, alignItems: "center" }}>
<Text className="text-xl font-bold">Ready to scan</Text> <Text className="text-lg font-bold">Ready to scan</Text>
<Text>Please Scan a command to start scanning...</Text> <Text>Please Scan a command to start scanning...</Text>
<Text className="text-sm"> <Text className="text-sm">
Scanning a label could cause errors due to incorrect previous Scanning a label could cause errors due to incorrect previous
@@ -208,7 +211,7 @@ export default function LSTScanner() {
) : ( ) : (
<View <View
style={{ style={{
marginTop: 10, marginTop: 2,
alignItems: "center", alignItems: "center",
}} }}
> >
@@ -217,10 +220,10 @@ export default function LSTScanner() {
.map((i) => { .map((i) => {
return ( return (
<View <View
style={{ marginTop: 10, alignItems: "center" }} style={{ marginTop: 2, alignItems: "center" }}
key={i} key={i}
> >
<Text style={{ fontSize: 18, fontWeight: "600" }}> <Text style={{ fontSize: 12, fontWeight: "600" }}>
{i} {i}
</Text> </Text>
</View> </View>
@@ -237,18 +240,17 @@ export default function LSTScanner() {
color={bgColor} color={bgColor}
clearScan={clearScans} clearScan={clearScans}
/> />
<GlobalFooter />
</View> </View>
<View className="m-2"> {/* <View className="m-2">
{user && ( {user && (
<View className="items-center"> <View className="items-center">
<Button title="Logout" onPress={logoutScanner} /> <Button title="Logout" onPress={logoutScanner} />
</View> </View>
)} )}
</View> </View> */}
<View> {/* <View style={{ maxHeight: 75 }} className="flex-1 bg-slate-500"></View> */}
<GlobalFooter />
</View>
</View> </View>
); );
} }

View File

@@ -13,28 +13,30 @@ export function GlobalFooter() {
if (serverVersion && serverVersion?.versionCode <= build) return; if (serverVersion && serverVersion?.versionCode <= build) return;
return ( return (
<View> <View>
<View> {(hasUpdate || shouldUpdate) && (
<View className="bg-slate-500">
{hasUpdate && ( {hasUpdate && (
<View className="items-center h-[75px] bg-[#EB091A]"> <View className="items-center h-[75px] bg-[#EB091A] justify-center">
<Link href={"/updateScreen"}> <Link href="/updateScreen">
<Text className="h-[75px] font-medium text-base text-wrap text-center"> <Text className="font-medium text-base text-center">
Critical updates pending, once you are completed with your task Critical updates pending, once you are completed with your
please click me for instructions to update task please click me for instructions to update
</Text> </Text>
</Link> </Link>
</View> </View>
)} )}
{!hasUpdate && shouldUpdate && ( {!hasUpdate && shouldUpdate && (
<View className="bg-[#FDBA74]"> <View className="bg-[#FDBA74] py-2 items-center">
<Link href={"/updateScreen"}> <Link href="/updateScreen">
<Text className="h-[16] font-medium text-base text-wrap text-center"> <Text className="font-medium text-base text-center">
There is an update click me for instructions There is an update click me for instructions
</Text> </Text>
</Link> </Link>
</View> </View>
)} )}
</View> </View>
)}
</View> </View>
); );
} }

View File

@@ -0,0 +1,136 @@
import { useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { getSocket } from "../lib/socket.io";
type RoomUpdatePayload<T> = {
roomId: string;
payloads: T[];
};
type RoomErrorPayload = {
roomId?: string;
message?: string;
};
type UpdateMode = "append" | "replace";
export function useSocketRoom<T>(
roomId: string,
getKey?: (item: T) => string | number,
updateMode: UpdateMode = "append",
) {
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],
);
useFocusEffect(
useCallback(() => {
const socket = getSocket();
function handleConnect() {
socket.emit("join-room", roomId);
setInfo(`Joined room: ${roomId}`);
}
function handleUpdate(payload: RoomUpdatePayload<T>) {
// protects against other room updates hitting this hook
if (payload.roomId !== roomId) return;
// resetting room data for rooms that just need updated data.
if (updateMode === "replace") {
setData(payload.payloads);
} else {
setData((prev) => [...payload.payloads, ...prev]);
}
setInfo("");
}
function handleError(err: RoomErrorPayload) {
if (err.roomId && err.roomId !== roomId) return;
setInfo(err.message ?? "Room error");
}
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);
setInfo(`Joined room: ${roomId}`);
}
return () => {
socket.emit("leave-room", roomId);
socket.off("connect", handleConnect);
socket.off("room-update", handleUpdate);
socket.off("room-error", handleError);
};
}, [roomId, updateMode]),
);
// useEffect(() => {
// const socket = getSocket();
// function handleConnect() {
// socket.emit("join-room", roomId);
// setInfo(`Joined room: ${roomId}`);
// }
// function handleUpdate(payload: RoomUpdatePayload<T>) {
// // protects against other room updates hitting this hook
// if (payload.roomId !== roomId) return;
// setData((prev) => [...payload.payloads, ...prev]);
// setInfo("");
// }
// function handleError(err: RoomErrorPayload) {
// if (err.roomId && err.roomId !== roomId) return;
// setInfo(err.message ?? "Room error");
// }
// if (!socket.connected && socket.disconnected) {
// socket.connect();
// }
// // If already connected, join immediately
// if (socket.connected) {
// socket.emit("join-room", roomId);
// setInfo(`Joined room: ${roomId}`);
// }
// socket.on("connect", handleConnect);
// socket.on("room-update", handleUpdate);
// socket.on("room-error", handleError);
// return () => {
// socket.emit("leave-room", roomId);
// console.log("leaving Room");
// socket.off("connect", handleConnect);
// socket.off("room-update", handleUpdate);
// socket.off("room-error", handleError);
// };
// }, [roomId]);
return { data, info, clearRoom };
}

View File

@@ -0,0 +1,50 @@
import ReactNativeBlobUtil from "react-native-blob-util";
export async function downloadLatestApk(serverIp: string, port: string) {
const url = `http://${serverIp}:${port}/lst/api/mobile/apk/latest`;
const apkPath = `${ReactNativeBlobUtil.fs.dirs.DownloadDir}/lst-mobile.apk`;
// delete old apk if it exists
const exists = await ReactNativeBlobUtil.fs.exists(apkPath);
if (exists) {
const stat = await ReactNativeBlobUtil.fs.stat(apkPath);
// last modified time
const lastModified = Number(stat.lastModified);
// current time
const now = Date.now();
// 5 minutes in ms
const fiveMinutes = 5 * 60 * 1000;
// skip download if file is fresh
if (now - lastModified < fiveMinutes) {
console.log("APK already downloaded recently, skipping.");
return {
skipped: true,
path: apkPath,
};
}
// delete old apk before redownload
await ReactNativeBlobUtil.fs.unlink(apkPath);
}
const res = await ReactNativeBlobUtil.config({
addAndroidDownloads: {
useDownloadManager: true,
notification: true,
title: "LST Mobile Update",
description: "Downloading update for StageNow install",
mime: "application/vnd.android.package-archive",
path: apkPath,
mediaScannable: true,
},
}).fetch("GET", url);
return res.path();
}

View File

@@ -0,0 +1,34 @@
import { io, type Socket } from "socket.io-client";
import { useAppStore } from "../hooks/useAppStore";
let socket: Socket | null = null;
export function getSocket() {
const { serverIp, serverPort } = useAppStore.getState();
//const port = Number(serverPort) >= 50000 ? "3000" : serverPort;
const url = `http://${serverIp}:${serverPort}`;
if (!socket) {
socket = io(url, {
path: "/lst/api/socket.io",
withCredentials: true,
autoConnect: true,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
}
return socket;
}
export function connectSocket() {
const socket = getSocket();
if (!socket.connected && socket.disconnected) {
socket.connect();
}
return socket;
}

View File

@@ -1,6 +1,8 @@
import axios from "axios"; import axios from "axios";
import Constants from "expo-constants";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck"; import { useServerStore } from "../hooks/useServerCheck";
import { downloadLatestApk } from "./lastestVersion";
export type ServerVersionInfo = { export type ServerVersionInfo = {
packageName: string; packageName: string;
@@ -60,13 +62,11 @@ export const versionCheck = async () => {
setServerVersion(res.data); setServerVersion(res.data);
} }
// const build = Constants.expoConfig?.android?.versionCode ?? 1; const build = Constants.expoConfig?.android?.versionCode ?? 1;
// if (build < res.data.minSupportedVersionCode) { if (build < res.data.versionCode) {
// setStartupRoute("/updateScreen"); await downloadLatestApk(serverIp, port);
// setReady(true); }
// return;
// }
} catch (error) { } catch (error) {
console.log("Version check error:", error); console.log("Version check error:", error);
} }

View File

@@ -0,0 +1,14 @@
CREATE TABLE "dock_door_scanners" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"ip" text NOT NULL,
"name" text,
"dock_id" text,
"dock_name" text NOT NULL,
"active" boolean DEFAULT true,
"current_loading_order" text DEFAULT '',
"add_date" timestamp DEFAULT now(),
"add_user" text DEFAULT 'lst-system',
"upd_date" timestamp DEFAULT now(),
"upd_user" text DEFAULT 'lst-system',
CONSTRAINT "dock_door_scanners_name_unique" UNIQUE("name")
);

View File

@@ -0,0 +1 @@
ALTER TABLE "dock_door_scanners" DROP COLUMN "dock_name";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -400,6 +400,20 @@
"when": 1779454561527, "when": 1779454561527,
"tag": "0056_shallow_chimera", "tag": "0056_shallow_chimera",
"breakpoints": true "breakpoints": true
},
{
"idx": 57,
"version": "7",
"when": 1779843750556,
"tag": "0057_worthless_trish_tilby",
"breakpoints": true
},
{
"idx": 58,
"version": "7",
"when": 1779846894283,
"tag": "0058_damp_donald_blake",
"breakpoints": true
} }
] ]
} }