7 Commits

40 changed files with 2424 additions and 175 deletions

View File

@@ -0,0 +1,68 @@
import express from "express";
import request from "supertest";
import { describe, expect, it, vi } from "vitest";
vi.mock("../db/db.controller.js", () => ({
db: {},
}));
vi.mock("../logger/logger.controller.js", () => ({
createLogger: () => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
}));
vi.mock("./datamart.controller.js", () => ({
runDatamartQuery: vi.fn(async ({ name, options }) => ({
success: true,
message: `Ran ${name}`,
data: {
name,
options,
},
})),
}));
import { runDatamartQuery } from "./datamart.controller.js";
import getDatamartRoute from "./getDatamart.route.js";
function createTestApp() {
const app = express();
app.use(express.json());
app.use("/datamart", getDatamartRoute);
return app;
}
describe("GET /datamart/:name", () => {
it("runs a datamart query by name and returns api response", async () => {
const app = createTestApp();
const res = await request(app).get("/datamart/orders").query({
value: "123",
});
expect(res.status).toBe(200);
expect(runDatamartQuery).toHaveBeenCalledWith({
name: "orders",
options: {
value: "123",
},
});
expect(res.body.success).toBe(true);
expect(res.body.module).toBe("datamart");
expect(res.body.subModule).toBe("query");
expect(res.body.data).toEqual({
name: "orders",
options: {
value: "123",
},
});
});
});

View File

@@ -1,18 +1,70 @@
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const endLoading = z.object({
loadingOrder: z.string(),
dockId: z.string(),
});
r.post("/", async (req, res) => {
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `Release x is being closed now. the bol should come out at the default printer.`,
data: req.body ?? [],
status: 200,
});
// close the loading order
// clear the loading order off the dock
try {
const validated = endLoading.parse(req.body);
const { data, error } = await tryCatch(
db
.update(dockDoorScanners)
.set({
currentLoadingOrder: "",
upd_date: sql`NOW()`,
upd_user: req.user?.username,
})
.where(eq(dockDoorScanners.dockId, validated.dockId))
.returning(),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to updating the dock.`,
data: (error as any) ?? [],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "loadingOrder",
message: `Loading order ${validated.loadingOrder} was just closed.`,
data: data ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to start loading order.`,
data: (error as any) ?? [],
status: 400,
});
}
});
export default r;

View File

@@ -0,0 +1,25 @@
import { Router } from "express";
import { apiReturn } from "../utils/returnHelper.utils.js";
import loadUnit from "./dockdoor.loadUnits.js";
const r = Router();
r.post("/", async (req, res) => {
const unit = await loadUnit({
dockId: req.body.dockId,
runningNo: req.body.runningNo,
});
return apiReturn(res, {
success: unit.success,
level: "info",
module: "dockdoor",
subModule: "loadingUnit",
message: unit.message,
data: unit?.data ?? [],
status: unit.success ? 200 : 400,
});
});
export default r;

View File

@@ -3,6 +3,7 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
@@ -11,10 +12,10 @@ import { returnFunc } from "../utils/returnHelper.utils.js";
type Data = {
dockId?: string;
sscc?: string;
runningNr?: string;
runningNo?: string;
};
export const loadUnit = async (data: Data) => {
const loadUnit = async (data: Data) => {
// are we even active at this time?
const dockDoorActive = await db.query.settings.findFirst({
where: (u, { eq }) => eq(u.name, "dockDoorScanning"),
@@ -44,7 +45,7 @@ export const loadUnit = async (data: Data) => {
"Failed to load the unit to the truck, there was no pallet read.",
data: [],
notify: false,
room: `dockDoorLoading${data.dockId}`,
room: `dockDoorLoading:${data.dockId}`,
});
}
@@ -64,7 +65,7 @@ export const loadUnit = async (data: Data) => {
"There are know current active loading orders please start one and try again.",
data: [],
notify: false,
room: `dockDoorLoading${data.dockId}`,
room: `dockDoorLoading:${data.dockId}`,
});
}
@@ -76,14 +77,67 @@ export const loadUnit = async (data: Data) => {
// add the loading units
try {
const prod = await runProdApi({
const unitToScan = data.sscc
? { sscc: data.sscc?.slice(2) }
: { runningNo: Number(data.runningNo) };
const prod = (await runProdApi({
method: "post",
endpoint: `/public/v1.0/OutboundDeliveries/LoadingOrders/${dock[0]?.currentLoadingOrder}/LoadUnit`,
data: [{ sscc: data.sscc?.slice(2) }],
});
data: [unitToScan],
})) as any;
console.log(prod?.data);
//emitToRoom(`dockDoorLoading:${data.dockId}`, prod?.data ?? []);
if (!prod?.success) {
emitToRoom(`dockDoorLoading:${data.dockId}`, prod?.data.errors[0]);
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message: `Unit encountered an error while loading`,
data: prod?.data.errors[0] as any,
notify: false,
//room: `dockDoorLoading:${data.dockId}`,
});
} else {
const emitData = {
message: `The unit ${prod.data.message.messageParams.runningNo} was loaded.`,
data: prod.data,
code: 0,
};
emitToRoom(`dockDoorLoading:${data.dockId}`, emitData as any);
return returnFunc({
success: true,
level: "info",
module: "dockdoor",
subModule: "loadUnit",
message: `Unit added to loading order`,
data: [
{
message: `The unit ${prod.data.message.messageParams.runningNo} was loaded.`,
data: prod.data,
code: 0,
},
] as any,
notify: false,
//room: `dockDoorLoading:${data.dockId}`,
});
}
} catch (error) {
console.log(error);
return returnFunc({
success: true,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message: `Failed to load unit`,
data: error as any,
notify: false,
room: `dockDoorLoading:${data.dockId}`,
});
}
};
export default loadUnit;

View File

@@ -2,6 +2,7 @@ import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import activeLoadingOrders from "./dockdoor.activeLoadingOrders.route.js";
import closeLoadingOrder from "./dockdoor.closeLoadingOrder.route.js";
import load from "./dockdoor.loadUnits.route.js";
import startLoad from "./dockdoor.startLoad.route.js";
import prodDocks from "./dockdoors.docks.route.js";
import docks from "./dockdoors.route.js";
@@ -17,7 +18,7 @@ export const setupDockDoorRoutes = (baseUrl: string, app: Express) => {
);
app.use(
`${baseUrl}/api/dockDoor/closeLoadingOrder`,
`${baseUrl}/api/dockDoor/finishOrder`,
featureCheck("dockDoorScanning"),
closeLoadingOrder,
);
@@ -37,7 +38,11 @@ export const setupDockDoorRoutes = (baseUrl: string, app: Express) => {
prodDocks,
);
// TODO : add manual way to add pallets
app.use(
`${baseUrl}/api/dockDoor/loadUnit`,
featureCheck("dockDoorScanning"),
load,
);
// all other system should be under /api/system/*
};

View File

@@ -1,4 +1,4 @@
import { sql } from "drizzle-orm";
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
@@ -25,6 +25,7 @@ r.post("/", async (req, res) => {
upd_date: sql`NOW()`,
upd_user: req.user?.username,
})
.where(eq(dockDoorScanners.dockId, validated.dockId))
.returning(),
);

View File

@@ -37,7 +37,7 @@ const dbStream = new Writable({
subModule: obj?.subModule?.toLowerCase(),
hostname: obj?.hostname?.toLowerCase(),
message: obj.msg,
stack: obj?.stack,
stack: obj?.stack || obj?.error, // this will add in the error or stack depending on how we pass it.
})
.returning(),
);

View File

@@ -49,7 +49,7 @@ const historicalInvImport = async () => {
});
}
if (data?.length === 0) {
if (data.length === 0) {
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
if (!avSQLQuery.success) {
@@ -139,7 +139,7 @@ const historicalInvImport = async () => {
subModule: "inv",
message: `Error adding historical data to lst db`,
data: errorImport as any,
notify: true,
notify: false,
});
}

View File

@@ -31,6 +31,14 @@ type Releases = {
DeliveryAddressHumanReadableId: string;
AdditionalInformation1: string;
};
// TODO: add these docs into the db
const actaulDocks = [
{ name: "cermac", dockId: "bcb17fae-0b1a-47a7-9fbf-594c5ebccce9" },
{ name: "matrix", dockId: "3e32cbfc-49f4-4138-b491-9d5df9c94754" },
{ name: "gerber", dockId: "9109e789-6c15-4cd9-87cb-de1b18627b6d" },
{ name: "rb", dockId: "6be02526-6183-4789-a73f-e0aa155e6d1e" },
];
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
@@ -64,6 +72,7 @@ let lastCheck = formatInTimeZone(
// };
const postRelease = async (release: Releases) => {
log.debug({}, `Release: ${release.ReleaseNumber} is about to be validated`);
if (!odToken.odToken) {
log.info({}, "Getting Auth Token");
await getToken();
@@ -82,13 +91,29 @@ const postRelease = async (release: Releases) => {
where: (u, { eq }) => eq(u.name, "defaultLoadType"),
});
// check if the release has the data in it
// check if the release has the loadtype in it
const releaseLoadtypeCheck = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("drop");
// allowed to schedule now, as long as we see od in here somewhere
const releaseOkToSchedule = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("od");
// dock was sent over
const releaseDockInfo = actaulDocks.some((dock) =>
(release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes(dock.name.toLowerCase()),
);
const opendDockArticleCheck = await db.query.opendockArticleSetup.findFirst({
where: (table, { and, eq }) =>
and(
@@ -97,6 +122,24 @@ const postRelease = async (release: Releases) => {
),
});
// selected dock
const releaseDocks = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim());
const matchedDock = actaulDocks.find((dock) =>
releaseDocks.includes(dock.name.toLowerCase()),
);
const setDock =
// validate we dont have the dock in the release
releaseDockInfo
? matchedDock?.dockId
: // validate we dont have the dock in the aritcle check
(actaulDocks.find((d) => d.name === opendDockArticleCheck?.dock)
?.dockId ?? process.env.DEFAULT_DOCK);
// TODO: add in docks from lst db here to make it more universal for the team
/**
* ReleaseState
@@ -127,7 +170,7 @@ const postRelease = async (release: Releases) => {
userId: process.env.DEFAULT_CARRIER, // this should be the carrierid
loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one
// TODO: look in the remarks in the release and if its says
dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out
dockId: setDock, // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber],
//refNumber: release.ReleaseNumber,
start: release.DeliveryDate,
@@ -403,7 +446,12 @@ const postRelease = async (release: Releases) => {
return;
}
}
} else {
} else if (
(releaseLoadtypeCheck ||
opendDockArticleCheck?.loadType === "drop" ||
defaultDock?.value === "drop") &&
releaseOkToSchedule
) {
try {
const response = await axios.post(
`${process.env.OPENDOCK_URL}/appointment`,
@@ -458,6 +506,76 @@ const postRelease = async (release: Releases) => {
return;
}
} else {
// try {
// const response = await axios.post(
// `${process.env.OPENDOCK_URL}/appointment`,
// newDockApt,
// {
// headers: {
// "content-type": "application/json; charset=utf-8",
// Authorization: `Bearer ${odToken.odToken}`,
// },
// },
// );
// // we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
// // this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.
// if (response.status === 400) {
// log.error({}, response.data.data.message);
// return;
// }
// // the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
// // TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
// //console.info(response.data.data, "Was Created");
// try {
// await db
// .insert(opendockApt)
// .values({
// release: release.ReleaseNumber,
// openDockAptId: response.data.data.id,
// appointment: response.data.data,
// })
// .onConflictDoUpdate({
// target: opendockApt.release,
// set: {
// openDockAptId: response.data.data.id,
// appointment: response.data.data,
// upd_date: sql`NOW()`,
// },
// })
// .returning();
// log.info({}, `${release.ReleaseNumber} was created`);
// } catch (e) {
// log.error({ stack: e }, "Error creating new release");
// }
// // biome-ignore lint/suspicious/noExplicitAny: to many possibilities
// } catch (e: any) {
// log.error(
// { stack: e?.response?.data },
// `Error posting new release to opendock, ${release.ReleaseNumber}`,
// );
// return;
// }
log.info(
{
stack: {
release: release.ReleaseNumber,
releaseLoadtypeCheck,
articleLoadType: opendDockArticleCheck?.loadType,
defaultLoadType: defaultDock?.value,
releaseOkToSchedule,
},
},
`Skipping OpenDock post - release: ${release.ReleaseNumber} is not allowed to schedule`,
);
return;
}
await delay(750); // rate limit protection

View File

@@ -1,11 +1,80 @@
SELECT count(*) as activated
FROM [test1_AlplaPROD2.0_Read].[support].[FeatureActivation]
where feature in (108,7)
where feature in (7)
/*
as more features get activated and need to have this checked to include the new endpoints add here so we can check this.
108 = waste
7 = warehousing
[DefaultTranslation("Blocking")]
Blocking = 1,
[DefaultTranslation("Users")]
UserManagement = 2,
[DefaultTranslation("Complaint Handling")]
ComplaintHandling = 3,
[DefaultTranslation("Demand Management")]
DemandManagement = 4,
[DefaultTranslation("Issue Material")]
IssueMaterial = 5,
[DefaultTranslation("Production Controlling")]
ProductionControlling = 6,
[DefaultTranslation("Warehousing")]
Warehousing = 7,
[DefaultTranslation("Outbound Deliveries")]
OutboundDeliveries = 8,
[DefaultTranslation("Production Scheduling")]
ProductionScheduling = 9,
[DefaultTranslation("Advanced Scheduling")]
AdvancedScheduling = 10,
[DefaultTranslation("Material Requirements Planning")]
MaterialRequirementsPlanning = 11,
[DefaultTranslation("Production Labelling")]
ProductionLabelling = 12,
[SpecialProcess]
[DefaultTranslation("Accounting")]
Accounting = 100,
[SpecialProcess]
[DefaultTranslation("Irradiation")]
Irradiation = 101,
[SpecialProcess]
[DefaultTranslation("Central Moulds")]
CentralMoulds = 102,
[SpecialProcess]
[DefaultTranslation("Maintenance")]
Maintenance = 103,
[SpecialProcess]
[DefaultTranslation("Disable Manual Bookings")]
DisableManualBookings = 104,
[SpecialProcess]
[DefaultTranslation("Purchasing")]
Purchasing = 105,
[SpecialProcess]
[DefaultTranslation("Tracing")]
Tracing = 106,
[SpecialProcess]
[DefaultTranslation("AlplaERP (D365)")]
AlplaErp = 107,
[SpecialProcess]
[DefaultTranslation("AI chatbot")]
AiChatBot = 108
*/

View File

@@ -7,7 +7,13 @@ type RoomDefinition<T = unknown> = {
seed: (limit: number) => Promise<T[]>;
};
export type StaticRoomId = "logs" | "labels" | "admin" | "admin:build" | "ppoo";
export type StaticRoomId =
| "logs"
| "labels"
| "admin"
| "admin:build"
| "ppoo"
| "dockDoorLoading:2";
export type DynamicRoomId = `dockDoorLoading:${string}`;
export type RoomId = StaticRoomId | DynamicRoomId;
@@ -24,6 +30,7 @@ export const protectedRooms: Record<StaticRoomId, RoomConfig> = {
admin: {},
"admin:build": {},
ppoo: {},
"dockDoorLoading:2": {},
};
export function getRoomConfig(roomId: string): RoomConfig | null {
@@ -91,4 +98,12 @@ export const roomDefinition: Record<RoomId, RoomDefinition> = {
} as any;
},
},
// TODO: add in dynamic room seeding
"dockDoorLoading:2": {
seed: async (limit) => {
console.log(limit);
return [];
},
},
};

View File

@@ -3,7 +3,7 @@ import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { printerData } from "../db/schema/printers.schema.js";
import { loadUnit } from "../dockdoorScanning/dockdoor.loadUnits.js";
import loadUnit from "../dockdoorScanning/dockdoor.loadUnits.js";
import { createLogger } from "../logger/logger.controller.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
@@ -79,7 +79,6 @@ export const startTCPServer = async () => {
}
// check if its a dock door scanner
// TODO: move to the db and get real info lol
const dockdoorScanners = await db.select().from(dockDoorScanners);
if (dockdoorScanners.some((s) => s.ip === ip.replace("::ffff:", ""))) {

View File

@@ -13,7 +13,14 @@ import {
//import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import * as rawSchema from "../db/schema/auth.schema.js";
import { ac, admin, manager, systemAdmin, user } from "./auth.permissions.js";
import {
ac,
admin,
manager,
systemAdmin,
transport,
user,
} from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js";
@@ -164,6 +171,7 @@ export const auth = betterAuth({
admin,
user,
manager,
transport,
systemAdmin,
},
}),

View File

@@ -11,10 +11,12 @@ type RoomErrorPayload = {
message?: string;
};
type UpdateMode = "append" | "replace";
export function useSocketRoom<T>(
roomId: string,
enabled = true,
getKey?: (item: T) => string | number,
updateMode: UpdateMode = "append",
) {
const [data, setData] = useState<T[]>([]);
const [info, setInfo] = useState(
@@ -36,7 +38,6 @@ export function useSocketRoom<T>(
);
useEffect(() => {
if (!roomId || !enabled) return;
function handleConnect() {
socket.emit("join-room", roomId);
setInfo(`Joined room: ${roomId}`);
@@ -46,7 +47,13 @@ export function useSocketRoom<T>(
// protects against other room updates hitting this hook
if (payload.roomId !== roomId) return;
setData((prev) => [...payload.payloads, ...prev]);
// resetting room data for rooms that just need updated data.
if (updateMode === "replace") {
setData(payload.payloads);
} else {
setData((prev) => [...payload.payloads, ...prev]);
}
setInfo("");
}
@@ -55,14 +62,14 @@ export function useSocketRoom<T>(
setInfo(err.message ?? "Room error");
}
if (!socket.connected) {
socket.connect();
}
socket.on("connect", handleConnect);
socket.on("room-update", handleUpdate);
socket.on("room-error", handleError);
if (!socket.connected && socket.disconnected) {
socket.connect();
}
// If already connected, join immediately
if (socket.connected) {
socket.emit("join-room", roomId);
@@ -76,7 +83,7 @@ export function useSocketRoom<T>(
socket.off("room-update", handleUpdate);
socket.off("room-error", handleError);
};
}, [roomId, enabled]);
}, [roomId, updateMode]);
return { data, info, clearRoom };
}

View File

@@ -1,6 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import z from "zod";
import { useSession } from "../lib/auth-client";
import { trackLstEvent } from "../lib/umami.utils";

View File

@@ -132,6 +132,10 @@ export default function NewArticleLink({ refetch }: { refetch: any }) {
label: "Matrix",
value: "matrix",
},
{
label: "RB",
value: "rb",
},
];
return (

View File

@@ -15,14 +15,14 @@
"foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff"
},
"versionCode": 39,
"versionCode": 42,
"minSupportedVersionCode": 33,
"predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile",
"permissions": [
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE"
]
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE"
]
},
"web": {
"output": "static",

View File

@@ -16,7 +16,7 @@
"@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0",
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-query": "^5.100.14",
"axios": "^1.15.0",
"babel-preset-expo": "^55.0.18",
"class-variance-authority": "^0.7.1",
@@ -5318,9 +5318,9 @@
"license": "MIT"
},
"node_modules/@tanstack/query-core": {
"version": "5.100.9",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz",
"integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==",
"version": "5.100.14",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz",
"integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==",
"license": "MIT",
"funding": {
"type": "github",
@@ -5328,12 +5328,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.100.9",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz",
"integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==",
"version": "5.100.14",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz",
"integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.100.9"
"@tanstack/query-core": "5.100.14"
},
"funding": {
"type": "github",

View File

@@ -26,7 +26,7 @@
"@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0",
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-query": "^5.100.14",
"axios": "^1.15.0",
"babel-preset-expo": "^55.0.18",
"class-variance-authority": "^0.7.1",

View File

@@ -51,7 +51,7 @@ export default function TabsLayout() {
onPress: async () => {
// clear auth/session
logoutScanner();
router.replace("/(tabs)/scanner");
router.replace("/");
// clear zustand/session stuff
//useAuthStore.getState().reset();

View File

@@ -1,26 +1,134 @@
import React from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
import { useSuspenseQuery } from "@tanstack/react-query";
import * as Device from "expo-device";
import { Link } from "expo-router";
import {
Button,
ScrollView,
Text,
useWindowDimensions,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
export default function LaneCheck() {
const getInfo = async () => {
const info = "ho";
import { Card, CardContent } from "../../components/ui/card";
import { getActiveLoadingOrders } from "../../lib/queryStuff/getActiveLoadingOrders";
import { getDocks } from "../../lib/queryStuff/getDocks";
console.log(info);
export default function DockScan() {
const { data } = useSuspenseQuery(getDocks());
const {
data: loadingOrders,
refetch,
isLoading,
} = useSuspenseQuery(getActiveLoadingOrders());
const { width } = useWindowDimensions();
const isTablet =
Device.modelName?.toLowerCase().includes("et40") ||
Device.modelName?.toLowerCase().includes("et45");
const columns = isTablet ? 3 : 1;
const gap = 8;
const cardWidth =
columns === 1 ? width - 16 : (width - gap * (columns + 1)) / columns;
const updateLoadingOrders = () => {
refetch();
Toast.show({
type: "success",
text1: `Refreshing Loading Orders`,
});
};
if (isLoading)
return (
<SafeAreaView>
<Text>Loading...</Text>
</SafeAreaView>
);
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>Dock Scanning</Text>
<Button onPress={getInfo}>
<Text>Check info</Text>
</Button>
<View className="flex">
<View
style={{
// flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text className="text-2xl text-bold">Dock Scanning</Text>
<Button title="Update Loading Orders" onPress={updateLoadingOrders} />
</View>
<View>
<SafeAreaView className="flex">
<ScrollView className="w-full">
<View className="w-full flex-row flex-wrap gap-2 m-2">
{data.map((i: any) => {
const loadingPlan =
i.currentLoadingOrder !== ""
? loadingOrders.filter(
(x: any) => x.id === Number(i.currentLoadingOrder),
)
: [];
return (
<View key={i.id}>
<Link
href={{
pathname: "/dock/[id]",
params: {
id: i.dockId.toString(),
currentLoading: i.currentLoadingOrder,
},
}}
>
<Card
style={{
borderWidth: 4,
width: cardWidth,
}}
>
<CardContent>
<Text>{i.name}</Text>
{i.currentLoadingOrder === "" ? (
<Text>Tap to active new loading order</Text>
) : (
<View>
<Text>
Current Loading order : {i.currentLoadingOrder}
</Text>
{loadingPlan && loadingPlan.length > 0 && (
<View>
<Text>
{`${loadingPlan[0].loadingPlanItems[0].articleId} - ${loadingPlan[0].loadingPlanItems[0].articleDescription}`}
</Text>
<Text>
Current Loaded :{" "}
{
loadingPlan[0].loadingPlanItems[0]
.loadedQuantityLUs
}{" "}
/{" "}
{
loadingPlan[0].loadingPlanItems[0]
.plannedQuantityLUs
}
</Text>
</View>
)}
</View>
)}
</CardContent>
</Card>
</Link>
</View>
);
})}
</View>
</ScrollView>
</SafeAreaView>
</View>
</View>
);
}

View File

@@ -44,8 +44,6 @@ export default function PPOO() {
});
}, [items, sortDir]);
//console.log(logsInfo);
return (
<View className="flex items-center mt-2">
<View className="flex m-2">
@@ -61,7 +59,7 @@ export default function PPOO() {
<Text>Loading PPOO...</Text>
</View>
) : (
<SafeAreaView className="flex-1">
<SafeAreaView className="flex">
<ScrollView className="w-full">
<View className="w-full flex-row flex-wrap gap-2 m-2">
{sortedItems.map((i: any) => {

View File

@@ -2,14 +2,18 @@ import { PortalHost } from "@rn-primitives/portal";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import "../../global.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { useEffect } from "react";
import Toast from "react-native-toast-message";
import useDeviceLock from "../hooks/useDeviceCheck";
import { useDeviceOrientationLock } from "../hooks/useDeviceOrientationLock";
import { queryClient } from "../lib/queryStuff/queryClient";
import { connectSocket } from "../lib/socket.io";
import { zebraScanner } from "../lib/ZebraScanner";
export default function RootLayout() {
useDeviceLock();
useDeviceOrientationLock();
useEffect(() => {
zebraScanner.ensureProfile();
@@ -18,15 +22,18 @@ export default function RootLayout() {
return (
<>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="login" />
<Stack.Screen name="setup" />
<Stack.Screen name="updateScreen" />
<Stack.Screen name="(tabs)" />
</Stack>
<PortalHost />
<QueryClientProvider client={queryClient}>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="login" />
<Stack.Screen name="setup" />
<Stack.Screen name="updateScreen" />
<Stack.Screen name="(tabs)" />
</Stack>
<PortalHost />
</QueryClientProvider>
<Toast />
</>
);

View File

@@ -0,0 +1,169 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import * as Device from "expo-device";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useState } from "react";
import {
Button,
Pressable,
ScrollView,
Text,
useWindowDimensions,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import { Card, CardContent } from "../../components/ui/card";
import { api } from "../../lib/apiHelper";
import { getActiveLoadingOrders } from "../../lib/queryStuff/getActiveLoadingOrders";
export default function DockPage() {
const { id, currentLoading } = useLocalSearchParams<{
id: string;
currentLoading: string;
}>();
const router = useRouter();
const [active] = useState(currentLoading !== "");
const { width } = useWindowDimensions();
const isTablet =
Device.modelName?.toLowerCase().includes("et40") ||
Device.modelName?.toLowerCase().includes("et45");
const columns = isTablet ? 3 : 1;
const gap = 8;
const cardWidth =
columns === 1 ? width - 16 : (width - gap * (columns + 1)) / columns;
const {
data: loadingOrders,
refetch,
isLoading,
} = useSuspenseQuery(getActiveLoadingOrders());
const dockFilter = loadingOrders.filter((i: any) => i.dockId === Number(id));
// add in start loading order, if this is already on the dock we will disabled and change to view current pallets
const startLoad = async (loadingOrder: string, dockId: string) => {
try {
const res = await api.post("/dockDoor/startLoad", {
loadingOrder,
dockId,
});
if (res.status === 200) {
Toast.show({ type: "success", text1: res.data.message });
refetch();
return;
}
} catch (error) {
Toast.show({
type: "error",
text1: JSON.stringify(error),
});
}
};
const endLoad = async (loadingOrder: string, dockId: string) => {
try {
const res = await api.post("/dockDoor/endLoad", {
loadingOrder,
dockId,
});
if (res.status === 200) {
Toast.show({ type: "success", text1: res.data.message });
refetch();
return;
}
} catch (error) {
Toast.show({
type: "error",
text1: JSON.stringify(error),
});
}
};
// add in ending loading order disabeled until all pallets are loaded.
if (isLoading)
return (
<SafeAreaView>
<Text>Loading</Text>
</SafeAreaView>
);
return (
<SafeAreaView>
<View className="flex flex-row justify-between gap-1 ml-1 mr-1">
<View>
<Pressable
onPress={() => router.back()}
className="self-start rounded-xl bg-gray-200 px-4 py-2"
>
<Text className="font-semibold"> Back</Text>
</Pressable>
</View>
<Text className="text-xl mt-1">{dockFilter[0].dockDescription}</Text>
<View>
<Pressable
onPress={() =>
router.push({
pathname: "/dock/scans/[scanner]",
params: {
scanner: id,
},
})
}
className="self-start rounded-xl bg-gray-200 px-4 py-2"
>
<Text className="font-semibold">Scans</Text>
</Pressable>
</View>
</View>
<ScrollView>
<View className="w-full flex-row flex-wrap gap-2 m-2">
{dockFilter.map((i: any) => {
return (
<View key={i.id}>
<Card
style={{
borderWidth: 4,
width: cardWidth,
}}
>
<CardContent>
<View>
<Text>Loading Order: {dockFilter[0].id}</Text>
<Text>
{`${dockFilter[0].loadingPlanItems[0].articleId} - ${dockFilter[0].loadingPlanItems[0].articleDescription}`}
</Text>
<Text>
Current Loaded :{" "}
{dockFilter[0].loadingPlanItems[0].loadedQuantityLUs} /{" "}
{dockFilter[0].loadingPlanItems[0].plannedQuantityLUs}
</Text>
</View>
<View className="mt-2 flex flex-row gap-2 justify-between">
<Button
title="Start Load"
onPress={() =>
startLoad(dockFilter[0].id.toString(), id)
}
disabled={active}
/>
<Button
title="End Load"
onPress={() => endLoad(dockFilter[0].id.toString(), id)}
disabled={active}
/>
</View>
</CardContent>
</Card>
</View>
);
})}
</View>
</ScrollView>
</SafeAreaView>
);
}

View File

@@ -0,0 +1,71 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useRouter } from "expo-router";
import { Pressable, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Card } from "../../../components/ui/card";
import { useSocketRoom } from "../../../hooks/socket.io.hook";
import { getActiveLoadingOrders } from "../../../lib/queryStuff/getActiveLoadingOrders";
export default function DockPage() {
const { scanner } = useLocalSearchParams<{
scanner: string;
}>();
const { data: loadingOrders, isLoading } = useSuspenseQuery(
getActiveLoadingOrders(),
);
const { data } = useSocketRoom<any>(
`dockDoorLoading:${scanner}`,
undefined,
"append",
) as any;
const dockFilter = loadingOrders.filter(
(i: any) => i.dockId === Number(scanner),
);
const router = useRouter();
if (isLoading)
return (
<SafeAreaView>
<Text>Loading...</Text>
</SafeAreaView>
);
return (
<SafeAreaView className="w-full">
<View className="flex flex-row justify-between gap-1 ml-1 mr-1">
<View>
<Pressable
onPress={() => router.back()}
className="self-start rounded-xl bg-gray-200 px-4 py-2"
>
<Text className="font-semibold"> Back</Text>
</Pressable>
</View>
<Text className="text-xl mt-1">{dockFilter[0].dockDescription}</Text>
<View>
<Pressable
onPress={() =>
router.replace({
pathname: "/(tabs)/dockScan",
})
}
className="self-start rounded-xl bg-gray-200 px-4 py-2"
>
<Text className="font-semibold">Docks</Text>
</Pressable>
</View>
<View>
{data.map((i: any, index: any) => {
return (
<View key={index} className="m-2">
<Card>
<Text>{JSON.stringify(i)}</Text>
</Card>
</View>
);
})}
</View>
</View>
</SafeAreaView>
);
}

View File

@@ -1,9 +1,11 @@
import axios from "axios";
import { useRouter } from "expo-router";
import { Settings } from "lucide-react-native";
import { useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import Toast from "react-native-toast-message";
import { ConfigButton } from "../components/ui/configButton";
import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
@@ -52,11 +54,6 @@ export default function Login() {
}
};
const config = () => {
console.log("config");
return router.replace("/setup");
};
return (
<View
style={{
@@ -67,9 +64,17 @@ export default function Login() {
}}
>
<View className="flex items-center m-5">
<Text style={{ fontSize: 20, fontWeight: "600" }}>
LST Scanner Login
</Text>
<View className="flex flex-row">
<View>
<Text style={{ fontSize: 20, fontWeight: "600" }} className="mt-2">
LST Scanner Login
</Text>
</View>
<View>
<ConfigButton />
</View>
</View>
<View className="w-64 p-4">
<Input
className="w-fit"
@@ -89,7 +94,6 @@ export default function Login() {
</View>
<View className="flex gap-2 flex-row">
<Button title="Login" onPress={onLogin} />
<Button title="Config" onPress={config} />
</View>
</View>
);

View File

@@ -0,0 +1,20 @@
import { useRouter } from "expo-router";
import { Settings } from "lucide-react-native";
import { Pressable } from "react-native";
export function ConfigButton() {
const router = useRouter();
const config = () => {
console.log("config");
return router.replace("/setup");
};
return (
<Pressable
onPress={config}
className="h-12 w-12 items-center justify-center rounded-x"
>
<Settings color="black" size={24} />
</Pressable>
);
}

View File

@@ -1,6 +1,7 @@
import axios from "axios";
import Constants from "expo-constants";
import { useEffect, useRef, useState } from "react";
import { setApiConfig } from "../lib/apiHelper";
import { devDelay } from "../lib/devMode";
import { versionCheck } from "../lib/versionValidation";
import { useAppStore } from "./useAppStore";
@@ -26,6 +27,11 @@ export function useAppStartup() {
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
setApiConfig({
serverIp,
serverPort,
});
useEffect(() => {
if (!hasHydrated) {
setStatus("loading");

View File

@@ -0,0 +1,32 @@
import * as Device from "expo-device";
import * as ScreenOrientation from "expo-screen-orientation";
import { useEffect } from "react";
const LANDSCAPE_MODELS = ["ET45", "ET40"]; // tablets
const PORTRAIT_MODELS = ["TC21", "TC26", "TC8300"]; // scanners
const isTabletModel = (modelName?: string | null) => {
const model = modelName?.toUpperCase() ?? "";
return LANDSCAPE_MODELS.some((m) => model.includes(m));
};
export function useDeviceOrientationLock() {
useEffect(() => {
async function lockOrientation() {
try {
const model = Device.modelName;
await ScreenOrientation.lockAsync(
isTabletModel(model)
? ScreenOrientation.OrientationLock.LANDSCAPE
: ScreenOrientation.OrientationLock.PORTRAIT_UP,
);
} catch (err) {
console.warn("Failed to lock orientation", err);
}
}
void lockOrientation();
}, []);
}

View File

@@ -0,0 +1,63 @@
import axios from "axios";
import { router } from "expo-router";
type ApiConfig = {
serverIp: string;
serverPort: string | number;
};
let currentConfig: ApiConfig | null = null;
export function setApiConfig(config: ApiConfig) {
currentConfig = config;
}
function getBaseUrl() {
if (!currentConfig) {
throw new Error("API config not initialized");
}
console.log(
`http://${currentConfig.serverIp}:${currentConfig.serverPort}/lst/api`,
);
return `http://${currentConfig.serverIp}:${currentConfig.serverPort}/lst/api`;
}
export const api = axios.create({
timeout: 15000,
});
api.interceptors.request.use((config) => {
config.baseURL = getBaseUrl();
return config;
});
api.interceptors.response.use(
(response) => response,
(error) => {
const isNetworkError =
error.code === "ERR_NETWORK" ||
error.code === "ECONNABORTED" ||
error.message === "Network Error" ||
error.message === "Failed to fetch" ||
!error.response;
// unauthorized
if (error.response?.status === 401) {
router.replace("/login");
}
// forbidden
if (error.response?.status === 403) {
router.replace("/");
}
// app/server offline
if (isNetworkError) {
router.replace("/");
}
console.log(error);
return Promise.reject(error);
},
);

View File

@@ -0,0 +1,21 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getActiveLoadingOrders() {
return queryOptions({
queryKey: ["getActiveLoadingOrders"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
const { data } = await api.get("/dockDoor/activeLoadingOrders");
if (!data.success) {
throw new Error(data.message ?? "Failed to load articles");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,21 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getDocks() {
return queryOptions({
queryKey: ["getDocks"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
const { data } = await api.get("/dockDoor/scanners");
if (!data.success) {
throw new Error(data.message ?? "Failed to load articles");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,17 @@
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
gcTime: 5 * 60_000,
retry: 2,
refetchOnWindowFocus: false,
refetchOnReconnect: true,
refetchOnMount: false,
},
mutations: {
retry: 0,
},
},
});

View File

@@ -65,7 +65,7 @@ export const versionCheck = async () => {
const build = Constants.expoConfig?.android?.versionCode ?? 1;
if (build < res.data.versionCode) {
await downloadLatestApk(serverIp, port);
//await downloadLatestApk(serverIp, port);
}
} catch (error) {
console.log("Version check error:", error);

1366
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
"main": "index.js",
"scripts": {
"test": "dotenvx run -f .env -- vitest",
"test:run": "dotenvx run -f .env -- vitest run",
"dev": "concurrently -n \"server,frontend\" -c \"#007755, #1F73D1\" \"npm run dev:app\" \"npm run dev:frontend\"",
"dev:app": "dotenvx run -f .env -- tsx watch backend/server.ts",
"dev:frontend": "cd frontend && npm run dev",
@@ -59,9 +60,11 @@
"cz-conventional-changelog": "^3.3.0",
"npm-check-updates": "^19.6.5",
"openapi-types": "^12.1.3",
"supertest": "^7.2.2",
"ts-node-dev": "^2.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vitest": "^4.1.8"
},
"dependencies": {
"@dotenvx/dotenvx": "^1.57.0",

View File

@@ -1,29 +0,0 @@
// import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
// import { connectProdSql } from "../backend/src/prodSql/prodSqlConnection.controller.js";
// let pool: unknown;
// describe("Prod SQL connection", () => {
// // This may take seconds, so give plenty of time
// vi.setTimeout(30000);
// beforeAll(async () => {
// pool = await connectProdSql();
// });
// afterAll(async () => {
// if (pool?.close) await pool.close();
// });
// it("should connect and return expected data", async () => {
// // Example query — use something safe and consistent
// const result = await pool
// .request()
// .query("SELECT TOP 1 id, name FROM Users ORDER BY id ASC");
// expect(result.recordset).toBeDefined();
// expect(Array.isArray(result.recordset)).toBe(true);
// expect(result.recordset.length).toBeGreaterThan(0);
// expect(result.recordset[0]).toHaveProperty("id");
// });
// });

View File

@@ -1,42 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { sendEmail } from "../backend/src/utils/sendEmail.utils";
// Mock the logger before imports
vi.mock("../backend/src/logger/logger.controller", () => ({
createLogger: () => ({
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
fatal: vi.fn(),
}),
}));
describe("Mail sending", () => {
test("should send an email successfully", async () => {
const result = await sendEmail({
email: "blake.matthes@alpla.com",
subject: "LST - Testing system",
template: "testEmail",
context: { name: "blake" },
});
expect(result).toMatchObject({
success: true,
});
expect(result?.message).toContain("blake.matthes@alpla.com");
});
test("should handle email send failure gracefully", async () => {
const badResult = await sendEmail({
email: "invalid-address",
subject: "LST - Testing system",
template: "",
context: {},
});
expect(badResult).toMatchObject({
success: false,
});
});
});

View File

@@ -3,6 +3,6 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
include: ["tests/**/*.test.ts"], // ← point to your tests folder
include: ["backend/**/*.test.ts"], // ← point to your tests folder
},
});