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,210 +1,105 @@
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 () => { return sortDir === "asc" ? aDate - bDate : bDate - aDate;
sub.remove(); });
zebraScanner.stopListening(); }, [items, sortDir]);
//setUnits(null);
}; //console.log(logsInfo);
}, [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`}> </View>
// <ScrollView className="w-full flex-1"> {sortedItems.length === 0 ? (
// <View className="flex items-center gap-2 w-full"> <View className="flex items-center">
// {units.data?.map((i: any, index: any) => ( <Text>Loading PPOO...</Text>
// <View key={`${i.runningNumber}-${index}`}> </View>
// <Text>example</Text> ) : (
// </View> <SafeAreaView className="flex-1">
// ))} <ScrollView className="w-full">
// </View> <View className="w-full flex-row flex-wrap gap-2 m-2">
// </ScrollView> {sortedItems.map((i: any) => {
// </SafeAreaView> return (
<SafeAreaView className={`w-full items-center`}> <View key={i.runningNumber}>
<View style={{ padding: 2 }}> <Card
<Text>There Are {units.data.length} units in PPOO</Text> //className={isTablet ? "w-[32%]" : "w-full"}
</View> style={{
<ScrollView className="w-full" style={{ marginBottom: 20 }}> borderColor:
<View> i.mainDefectId === 864
{units.data.map((i, index) => ( ? "blue"
<View : i.state === "QualityBlocked"
key={`${i.runningNumber}-${index}`} ? "red"
style={{ : undefined,
justifyContent: "center", borderWidth: 4,
margin: 2, width: cardWidth,
}} }}
> >
<Dialog> <CardContent>
<DialogTrigger> <Text>
<Card {i.articleId} - {i.articleName}
className="w-full" </Text>
style={{
borderColor:
i.state === "QualityBlocked" ? "red" : undefined,
borderWidth: 4,
}}
>
<CardContent>
<Text>
{i.articleId} - {i.articleName}
</Text>
<Text> <Text>
Running Number: {i.runningNumber ?? "Non barcoded"} Running Number: {i.runningNumber ?? "Non barcoded"}
</Text> </Text>
</CardContent> <Text>
</Card> Date: {format(i.lastMovingDate, "M/d/yyyy HH:mm")}
</DialogTrigger> </Text>
<DialogContent> </CardContent>
<DialogHeader> </Card>
<DialogTitle> </View>
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>
</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">
<Text style={{ fontSize: 14, fontWeight: "600" }}> <View className="flex flex-col gap-2 items-center">
User: {formatName(user?.name ?? "")} <Text style={{ fontSize: 14, fontWeight: "600" }}>
</Text> Lst user: {formatName(user?.name ?? "")}
<Text style={{ fontSize: 18, fontWeight: "600" }}> </Text>
LST Scanner id: {user?.scannerId} {/* <Text style={{ fontSize: 14, fontWeight: "600" }}>
</Text> LST Scanner id: {user?.scannerId}
</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) && (
{hasUpdate && ( <View className="bg-slate-500">
<View className="items-center h-[75px] bg-[#EB091A]"> {hasUpdate && (
<Link href={"/updateScreen"}> <View className="items-center h-[75px] bg-[#EB091A] justify-center">
<Text className="h-[75px] font-medium text-base text-wrap text-center"> <Link href="/updateScreen">
Critical updates pending, once you are completed with your task <Text className="font-medium text-base text-center">
please click me for instructions to update Critical updates pending, once you are completed with your
</Text> task please click me for instructions to update
</Link> </Text>
</View> </Link>
)} </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
} }
] ]
} }