26 Commits

Author SHA1 Message Date
2ad78e22f1 chore(release): 0.0.2-alpha.7
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4m3s
Release and Build Image / release (push) Failing after 2m30s
2026-05-05 19:50:58 -05:00
518c0a8c19 refactor(scanner): format changes
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-05 19:50:02 -05:00
cd13360cfb feat(intial auth): intial auth setup for the scanner
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-05 19:48:36 -05:00
4e0cf8c54c refactor(docker compose): changed to have the correct url that will be used as this is for auth 2026-05-05 14:52:36 -05:00
36995e9fb4 refactor(gp connection): added in gp ip into env if not there use static name for dns
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 8m37s
2026-05-05 13:15:52 -05:00
30ffd843c7 feat(mobile): update notifications and more error handling added
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-04-30 17:02:21 -05:00
bb6155c969 refactor(mobile): more look and feel work
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m17s
2026-04-28 19:49:07 -05:00
7d2f048932 feat(mobile): shadcn like and tailwind added to make things look yummy
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m21s
2026-04-27 21:23:40 -05:00
649ae1ee9f feat(mobile): new route for the ehs launcher 2026-04-27 21:22:59 -05:00
8446dbc955 feat(servers): added iowa ebm
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m45s
2026-04-26 19:51:49 -05:00
0b7318f856 fix(mobile): typo for version checking 2026-04-26 19:51:12 -05:00
bddc9aca0d refactor(mobile): moved the versioning lookup at at the mobile folder plus renamed 2026-04-26 18:33:28 -05:00
77b4533dea feat(scanner): more work on the scanner and can now scan to prod no lst right now
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m41s
2026-04-25 18:13:07 -05:00
83a542d1b7 build(scripts): changing how the relase works so it purposly builds before it trys to release
this is to help prevent errors in the build and release stuff in git
2026-04-23 07:48:13 -05:00
4855412733 chore(release): 0.0.2-alpha.6
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m20s
Release and Build Image / release (push) Successful in 14s
2026-04-23 07:24:45 -05:00
251970ec8f chore(release): 0.0.2-alpha.1
All checks were successful
Release and Build Image / release (push) Successful in 2m36s
2026-04-23 07:24:12 -05:00
f7ea5f709e chore(release): 0.0.2-alpha.0
All checks were successful
Release and Build Image / release (push) Successful in 2m51s
2026-04-23 07:24:02 -05:00
3d3c2aa964 chore(release): 0.0.1
All checks were successful
Release and Build Image / release (push) Successful in 2m39s
2026-04-23 07:23:49 -05:00
781025dca0 fix(frontend): lingering import crashed us 2026-04-23 07:23:25 -05:00
a593bb2baa chore(doc remove): removed a doc and put it in the real area for docs 2026-04-23 07:23:01 -05:00
759f96b0b6 chore(release): 0.0.1-alpha.5
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m36s
Release and Build Image / release (push) Failing after 38s
2026-04-23 07:11:50 -05:00
de5df2b00b chore(scripts): added in a helper to remove old stuff
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-23 07:10:53 -05:00
4d53af0338 feat(servers): added marked tree in to the mix 2026-04-23 07:10:27 -05:00
f7276ca2d7 feat(oidc): added in so we could use an oidc to login as well :D 2026-04-23 07:09:49 -05:00
d6328ab764 fix(gp): weird issue with db username and password
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m41s
2026-04-22 06:39:22 -05:00
a6d53f0266 refactor(sql): changed sql connection to ip:port
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m42s
this change was needed for when we run in docker so we can connect to the servers
2026-04-22 05:40:38 -05:00
105 changed files with 13607 additions and 572 deletions

View File

@@ -50,3 +50,11 @@ GP_PASSWORD=
# how often to check for new/updated queries in min
QUERY_TIME_TYPE=m #valid options are m, h
QUERY_CHECK=1
# Oauth setup
PROVIDER=""
CLIENT_ID=""
CLIENT_SECRET=""
CLIENT_SCOPES="openid profile email groups"
DISCOVERY_URL=""

View File

@@ -1,5 +1,90 @@
# All Changes to LST can be found below.
## [0.0.2-alpha.7](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.6...v0.0.2-alpha.7) (2026-05-06)
### 🌟 Enhancements
* **intial auth:** intial auth setup for the scanner ([cd13360](https://git.tuffraid.net/cowch/lst_v3/commits/cd13360cfb931daca50fd7b111e1c8f8ab09a909))
* **mobile:** new route for the ehs launcher ([649ae1e](https://git.tuffraid.net/cowch/lst_v3/commits/649ae1ee9f245a9b5d308ea8a636357bf72b1e34))
* **mobile:** shadcn like and tailwind added to make things look yummy ([7d2f048](https://git.tuffraid.net/cowch/lst_v3/commits/7d2f048932b77269568149de34351840b75486e2))
* **mobile:** update notifications and more error handling added ([30ffd84](https://git.tuffraid.net/cowch/lst_v3/commits/30ffd843c725da79ed035e2d9564f60a6babcda8))
* **scanner:** more work on the scanner and can now scan to prod no lst right now ([77b4533](https://git.tuffraid.net/cowch/lst_v3/commits/77b4533dea8314fd4fb81a597995cabd041fe188))
* **servers:** added iowa ebm ([8446dbc](https://git.tuffraid.net/cowch/lst_v3/commits/8446dbc955462235b9df35c501354761661e4f6a))
### 🐛 Bug fixes
* **mobile:** typo for version checking ([0b7318f](https://git.tuffraid.net/cowch/lst_v3/commits/0b7318f8566d15414edd3cd67c89fa5346058ab0))
### 🛠️ Code Refactor
* **docker compose:** changed to have the correct url that will be used as this is for auth ([4e0cf8c](https://git.tuffraid.net/cowch/lst_v3/commits/4e0cf8c54c4dfd68edba7e733518846a47c55064))
* **gp connection:** added in gp ip into env if not there use static name for dns ([36995e9](https://git.tuffraid.net/cowch/lst_v3/commits/36995e9fb42cfa1b72c096b8860866d70b86e70c))
* **mobile:** more look and feel work ([bb6155c](https://git.tuffraid.net/cowch/lst_v3/commits/bb6155c9692220542a52664848abf0b9eee91a43))
* **mobile:** moved the versioning lookup at at the mobile folder plus renamed ([bddc9ac](https://git.tuffraid.net/cowch/lst_v3/commits/bddc9aca0d2da2b2f53dec1250276d7a076a8601))
* **scanner:** format changes ([518c0a8](https://git.tuffraid.net/cowch/lst_v3/commits/518c0a8c19a4bff0b757bbd06ca5460d3565d8bd))
### 📈 Project Builds
* **scripts:** changing how the relase works so it purposly builds before it trys to release ([83a542d](https://git.tuffraid.net/cowch/lst_v3/commits/83a542d1b7beafe394949c001917f2b25056fac2))
## [0.0.2-alpha.6](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.1...v0.0.2-alpha.6) (2026-04-23)
## [0.0.2-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.0...v0.0.2-alpha.1) (2026-04-23)
## [0.0.2-alpha.0](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1...v0.0.2-alpha.0) (2026-04-23)
## [0.0.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.5...v0.0.1) (2026-04-23)
### 🐛 Bug fixes
* **frontend:** lingering import crashed us ([781025d](https://git.tuffraid.net/cowch/lst_v3/commits/781025dca00e9dd4b2ad9b283be944ed91bbc1e5))
### 📝 Chore
* **doc remove:** removed a doc and put it in the real area for docs ([a593bb2](https://git.tuffraid.net/cowch/lst_v3/commits/a593bb2baafd0166a178b80cd76dd8862f240e11))
## [0.0.1-alpha.5](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.4...v0.0.1-alpha.5) (2026-04-23)
### 🌟 Enhancements
* **admin:** moved server build/update to full app ([cb00add](https://git.tuffraid.net/cowch/lst_v3/commits/cb00addee96b3ecccf2694f85cb7882cac9c7e3d))
* **lstmobile:** intial scanner setup kinda working ([3734d9d](https://git.tuffraid.net/cowch/lst_v3/commits/3734d9daac143ad8fb4404c59990bc4f546f365b))
* **oidc:** added in so we could use an oidc to login as well :D ([f7276ca](https://git.tuffraid.net/cowch/lst_v3/commits/f7276ca2d722e30da65bbead23dc9bd57df25aa7))
* **servers:** added marked tree in to the mix ([4d53af0](https://git.tuffraid.net/cowch/lst_v3/commits/4d53af033876d81e0d38c148c15cb0af6f3d5bf0))
### 🐛 Bug fixes
* **datamart:** fixes to correct how we handle activations of new features and legacy queries ([b832d7a](https://git.tuffraid.net/cowch/lst_v3/commits/b832d7aa1ecd063be1bbb7e969617fc7a6376ffa))
* **datamart:** if we do not have 2.0 warehousing activate we need to use legacy ([5b1c885](https://git.tuffraid.net/cowch/lst_v3/commits/5b1c88546ff9a42dc572450fe05ad68015edb627))
* **gp:** weird issue with db username and password ([d6328ab](https://git.tuffraid.net/cowch/lst_v3/commits/d6328ab764c3626aef99727b873003384951d299))
* **inventory:** changes to accruatly adjust the query and check the feature set ([32517d0](https://git.tuffraid.net/cowch/lst_v3/commits/32517d0c98c42a0f0f60135b4a9951c4090ccd58))
* **logistics:** historical issue where it was being really weird ([cfbc156](https://git.tuffraid.net/cowch/lst_v3/commits/cfbc1565172f7c2e27f0a1593fe8e99b00d91bb7))
* **logistics:** purchasing monitoring was going off every 5th min instead of every 5 min ([3639c1b](https://git.tuffraid.net/cowch/lst_v3/commits/3639c1b77c597a94816bfedd0892f0c8980c6403))
* **ocp:** fixes to make sure we always hav printer.data as an array or dont do anything ([fb3cd85](https://git.tuffraid.net/cowch/lst_v3/commits/fb3cd85b411315cac0abd22d050ee88929754833))
* **psi:** refactor psi queries ([a1eeade](https://git.tuffraid.net/cowch/lst_v3/commits/a1eeadeec438f7c5c6d31f190fee5c22f83dc6b0))
### 📝 Chore
* **clean:** removed bruno api a proper api doc will be added to lst later ([f716de1](https://git.tuffraid.net/cowch/lst_v3/commits/f716de1a58a4a4c02d9a0a375444ceecea4a018b))
* **scripts:** added in a helper to remove old stuff ([de5df2b](https://git.tuffraid.net/cowch/lst_v3/commits/de5df2b00b1c6fe7c53d6ea075b4cf7e0fb845f9))
### 🛠️ Code Refactor
* **scanner:** more basic work to get the scanner just running ([82f8369](https://git.tuffraid.net/cowch/lst_v3/commits/82f8369640b2b0ff63dd640dc0aa0609a42c7dda))
* **servers:** added mcd and stp1 ([88cef2a](https://git.tuffraid.net/cowch/lst_v3/commits/88cef2a56c390b692866658ce519e59ffeaf4c17))
* **server:** server updates can now only be done from a dev pc ([7962463](https://git.tuffraid.net/cowch/lst_v3/commits/7962463927c4c5d2e12db9a0dd536b0f29fc65b2))
* **sql:** changed sql connection to ip:port ([a6d53f0](https://git.tuffraid.net/cowch/lst_v3/commits/a6d53f0266f1edc3f3946cd1f07d893c8a98d9c7))
## [0.0.1-alpha.4](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.3...v0.0.1-alpha.4) (2026-04-15)

View File

@@ -1,10 +1,16 @@
import type sql from "mssql";
// TODO : Remove this later and get it onto the env
const username = "gpviewer";
const password = "gp$$ViewOnly!";
const port = process.env.SQL_PORT
? Number.parseInt(process.env.SQL_PORT, 10)
: undefined;
export const gpSqlConfig: sql.config = {
server: `USMCD1VMS011`,
server: `${process.env.GP_SERVER ?? "USMCD1VMS011"}`,
port: port,
database: `ALPLA`,
user: username,
password: password,

View File

@@ -1,7 +1,13 @@
import type sql from "mssql";
const port = process.env.SQL_PORT
? Number.parseInt(process.env.SQL_PORT, 10)
: undefined;
export const prodSqlConfig: sql.config = {
server: `${process.env.PROD_SERVER}`,
database: `AlplaPROD_${process.env.PROD_PLANT_TOKEN}_cus`,
port: port,
user: process.env.PROD_USER,
password: process.env.PROD_PASSWORD,
options: {

View File

@@ -1,6 +1,8 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as scanUserSchema from "./schema/scanUsers.js";
const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`;
const queryClient = postgres(dbURL, {
@@ -13,4 +15,10 @@ const queryClient = postgres(dbURL, {
},
});
export const db = drizzle({ client: queryClient });
//export const db = drizzle({ client: queryClient });
export const db = drizzle(queryClient, {
schema: {
...scanUserSchema,
},
});

View File

@@ -0,0 +1,47 @@
import {
boolean,
pgEnum,
pgTable,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const mobileRoleEnum = pgEnum("mobile_role", [
"user",
"lead",
"manager",
"admin",
]);
export const scanUser = pgTable(
"scan_users",
{
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(), // the user that will be using the scanner
scannerId: text("scanner_id").unique().notNull(),
pinNumber: text("pin_number").unique().notNull(),
pinHash: text("pin_hash").notNull(),
excludedCommand: text("excluded_commands").default(""),
role: mobileRoleEnum("role").notNull().default("user"),
active: boolean("active").default(true),
lastScan: timestamp("last_scan").defaultNow(),
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
},
(table) => ({
userNotificationUnique: unique("scan_user_unique").on(
table.scannerId,
table.pinNumber,
),
}),
);
export const scanUserSchema = createSelectSchema(scanUser);
export const newsSanUserSchema = createInsertSchema(scanUser);
export type ScanUser = z.infer<typeof scanUserSchema>;
export type NewScanUser = z.infer<typeof newsSanUserSchema>;

View File

@@ -0,0 +1,20 @@
import { jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const scanLog = pgTable("scan_log", {
id: uuid("id").defaultRandom().primaryKey(),
scannerId: text("scanner_id"),
message: text("message").notNull(),
prompt: text("prompt"),
commandDescription: text("command_description"),
status: text("status"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_Date").defaultNow(),
});
export const scanLogSchema = createSelectSchema(scanLog);
export const newScanLogSchema = createInsertSchema(scanLog);
export type Printer = z.infer<typeof scanLogSchema>;
export type NewPrinter = z.infer<typeof newScanLogSchema>;

View File

@@ -13,7 +13,9 @@ let attempt = 0;
const maxAttempts = 10;
export const connectGPSql = async () => {
const serverUp = await checkHostnamePort(`USMCD1VMS011:1433`);
const serverUp = await checkHostnamePort(
`${process.env.GP_SERVER ?? "usmcd1vms011"}:1433`,
);
if (!serverUp) {
// we will try to reconnect
connected = false;
@@ -53,13 +55,14 @@ export const connectGPSql = async () => {
notify: false,
});
} catch (error) {
console.log(error);
reconnectToSql;
return returnFunc({
success: false,
level: "error",
module: "system",
subModule: "db",
message: "Failed to connect to the prod sql server.",
message: "Failed to connect to the gp sql server.",
data: [error],
notify: false,
});
@@ -118,7 +121,9 @@ export const reconnectToSql = async () => {
await new Promise((res) => setTimeout(res, delayStart));
const serverUp = await checkHostnamePort(`${process.env.PROD_SERVER}:1433`);
const serverUp = await checkHostnamePort(
`${process.env.GP_SERVER ?? "usmcd1vms011"}:1433`,
);
if (!serverUp) {
delayStart = Math.min(delayStart * 2, 30000); // exponential backoff until up to 30000

View File

@@ -11,26 +11,10 @@ const __dirname = path.dirname(__filename);
const downloadDir = path.resolve(__dirname, "../../downloads/mobile");
const currentApk = {
packageName: "net.alpla.lst.mobile",
versionName: "0.0.1-alpha",
versionCode: 1,
minSupportedVersionCode: 1,
fileName: "lst-mobile.apk",
};
router.get("/version", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
res.json({
packageName: currentApk.packageName,
versionName: currentApk.versionName,
versionCode: currentApk.versionCode,
minSupportedVersionCode: currentApk.minSupportedVersionCode,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
});
});
router.get("/apk/latest", (_, res) => {
router.get("/latest", (_, res) => {
const apkPath = path.join(downloadDir, currentApk.fileName);
if (!fs.existsSync(apkPath)) {
@@ -46,4 +30,17 @@ router.get("/apk/latest", (_, res) => {
return res.sendFile(apkPath);
});
router.get("/ehs", (_, res) => {
const apkPath = path.join(downloadDir, "EHS.apk");
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader("Content-Disposition", `attachment; filename="EHS.apk}"`);
return res.sendFile(apkPath);
});
export default router;

View File

@@ -0,0 +1,17 @@
import type { Express } from "express";
import downloads from "./donwloadApps.route.js";
import authPin from "./mobileAuth.route.js";
import newPin from "./mobilePin.route.js";
import logs from "./scanLogs.route.js";
import version from "./version.route.js";
export const setupMobileRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/mobile/version`, version);
app.use(`${baseUrl}/api/mobile/apk`, downloads);
app.use(`${baseUrl}/api/mobile/logs`, logs);
app.use(`${baseUrl}/api/mobile/auth`, authPin);
app.use(`${baseUrl}/api/mobile/pin`, newPin);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,331 @@
import bcrypt from "bcryptjs";
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import {
type NewScanUser,
type ScanUser,
scanUser,
} from "../db/schema/scanUsers.js";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn, returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
export async function hashPin(pin: string) {
// if (!/^\d{6}$/.test(pin)) {
// throw new Error("PIN must be exactly 6 digits");
// }
return bcrypt.hashSync(pin, 12);
}
const registerSchema = z.object({
name: z.string().min(2).max(100),
pinNumber: z.string(),
scannerId: z
.string()
.min(1)
.max(500)
.optional()
.describe("if you leave blank it will be the same as your username"),
role: z
.enum(["user", "lead", "manager", "admin"])
.optional()
.describe("What roles are available to use."),
pinHash: z.string().optional(),
});
r.post("/pin", async (req, res) => {
const { pin } = req.body;
if (!pin || pin.length !== 6) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Pin number must be a min of 6 digits`,
data: [],
status: 401,
});
}
// const user = await db
// .select()
// .from(scanUser)
// .where(eq(scanUser.pinNumber, parseInt(pin, 10)));
const user = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinNumber, pin),
});
if (!user) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid login please try again.`,
data: [],
status: 401,
});
}
const validPin = bcrypt.compareSync(pin, user.pinHash);
if (!validPin) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid pin please try again.`,
data: [],
status: 401,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `Welcome back ${user.name}`,
data: user as ScanUser | any,
status: 200,
});
});
r.post("/user", async (req, res) => {
try {
// validate the body is correct before accepting it
let validated = registerSchema.parse(req.body);
validated = {
...validated,
pinHash: await hashPin(validated.pinNumber.toString()),
};
const values: NewScanUser = {
name: validated.name,
pinNumber: validated.pinNumber,
pinHash: validated.pinHash ?? "",
scannerId: validated.scannerId ?? "",
};
const newUser = await db.insert(scanUser).values(values).returning();
apiReturn(res, {
success: true,
level: "info", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message: `${validated.name} was just created`,
data: newUser as any,
status: 200, //connect.success ? 200 : 400,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message:
"This User already exist with this pin or scanner id please try again",
data: [err],
status: 400, //connect.success ? 200 : 400,
});
}
});
r.get("/user", requireAuth, async (_, res) => {
const { data, error } = await tryCatch(db.select().from(scanUser));
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error getting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `There are no users you should add one . `,
data: [],
status: 200,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `All users. `,
data,
status: 200,
});
});
r.patch("/user/:id", requireAuth, async (req, res) => {
const updates: Record<string, unknown | null> = {};
const { id } = req.params;
const { data, error } = await tryCatch(
db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.id, `${id}`),
}),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error getting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid user id was passed over. `,
data: [],
status: 400,
});
}
if (req.body?.name !== undefined) {
updates.name = req.body.name;
}
if (req.body?.pinNumber !== undefined) {
const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, req.body.pinNumber),
});
if (existing)
return returnFunc({
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `${req.body.pinNumber} already exists please try again`,
data: [],
notify: false,
room: "",
});
updates.pinNumber = req.body.pinNumber;
updates.pinHash = await hashPin(req.body.pinNumber);
}
if (req.body?.scannerId !== undefined) {
updates.scannerId = req.body.scannerId;
}
if (req.body?.active !== undefined) {
updates.active = req.body.active;
}
if (req.body?.role !== undefined) {
updates.role = req.body.role;
}
updates.upd_date = sql`NOW()`;
const updatedSetting = await db
.update(scanUser)
.set(updates)
.where(eq(scanUser.id, `${id}`))
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "user",
message: `User ${data.name} was updated. `,
data: updatedSetting,
status: 200,
});
});
r.delete("/user/:id", requireAuth, async (req, res) => {
const { id } = req.params;
const { data, error } = await tryCatch(
db.delete(scanUser).where(eq(scanUser.id, `${id}`)),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error deleting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was no user to delete. `,
data: [],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "user",
message: `User was deleted. `,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,21 @@
import { Router } from "express";
import { generateUniquePin } from "../utils/generateScannerPin.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.get("/new", async (_, res) => {
const getPin = await generateUniquePin();
return apiReturn(res, {
success: getPin.success,
level: getPin.level,
module: "mobile",
subModule: "auth",
message: getPin.message,
data: getPin.data,
status: getPin.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,35 @@
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/", async (req, res) => {
const body = req.body;
const newLog = await db
.insert(scanLog)
.values({
scannerId: body.scannerId,
message: body.message,
prompt: body.prompt,
commandDescription: body.commandDescription,
status: body.status,
lines: body.lines,
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scan logs",
message: `New log from ${body.scannerId}`,
data: newLog,
status: 200,
});
});
export default router;

View File

@@ -0,0 +1,28 @@
import fs from "node:fs";
import { Router } from "express";
import path from "path";
const router = Router();
const projectRoot = path.resolve("./lstMobile"); // adjust as needed
const appJsonPath = path.join(projectRoot, "app.json");
router.get("/", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw);
const exp = config.expo;
res.json({
packageName: exp.android?.package,
versionName: exp.version,
versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
});
});
export default router;

View File

@@ -0,0 +1,113 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { sendEmail } from "../utils/sendEmail.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
/**
*
*/
const func = async (data: any, emails: string) => {
// get the actual notification as items will be updated between intervals if no one touches
const { data: l, error: le } = (await tryCatch(
db.select().from(notifications).where(eq(notifications.id, data.id)),
)) as any;
if (le) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `${data.name} encountered an error while trying to get initial info`,
data: [le],
notify: true,
});
}
// search the query db for the query by name
const sqlQuery = sqlQuerySelector(`${data.name}`) as SqlQuery;
// create the ignore audit logs ids
const ignoreIds = l[0].options[0]?.auditId
? `${l[0].options[0]?.auditId}`
: "0";
// run the check
const { data: queryRun, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[intervalCheck]", l[0].interval)
.replace("[ignoreList]", ignoreIds),
`Running notification query: ${l[0].name}`,
),
);
if (error) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [error],
notify: true,
});
}
if (queryRun.data.length > 0) {
// update the latest audit id
const { error: dbe } = await tryCatch(
db
.update(notifications)
.set({ options: [{ auditId: `${queryRun.data[0].id}` }] })
.where(eq(notifications.id, data.id)),
);
if (dbe) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [dbe],
notify: true,
});
}
// send the email
const sentEmail = await sendEmail({
email: emails,
subject: "Alert! Label Reprinted",
template: "reprintLabels",
context: {
items: queryRun.data,
},
});
if (!sentEmail?.success) {
return returnFunc({
success: false,
level: "error",
module: "email",
subModule: "notification",
message: `${l[0].name} failed to send the email`,
data: [sentEmail],
notify: true,
});
}
} else {
console.log("doing nothing as there is nothing to do.");
}
// TODO send the error to systemAdmin users so they do not always need to be on the notifications.
// these errors are defined per notification.
};
export default func;

View File

@@ -5,6 +5,7 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js";
import { setupOpendockRoutes } from "./opendock/opendock.routes.js";
@@ -27,4 +28,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app);
setupMobileRoutes(baseUrl, app);
};

View File

@@ -34,7 +34,7 @@ const servers: NewServerData[] = [
name: "Lima",
server: "USLIM1VMS006",
plantToken: "uslim1",
idAddress: "10.53.0.26",
idAddress: "10.53.0.26", // port opened 3000 2222
greatPlainsPlantCode: "50",
contactEmail: "",
contactPhone: "",
@@ -56,7 +56,7 @@ const servers: NewServerData[] = [
name: "Dayton",
server: "usday1VMS006",
plantToken: "usday1",
idAddress: "10.44.0.56",
idAddress: "10.44.0.56", // ports opened 3000 and 2222
greatPlainsPlantCode: "80",
contactEmail: "",
contactPhone: "",
@@ -105,11 +105,11 @@ const servers: NewServerData[] = [
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 82,
buildNumber: 1,
},
{
name: "St. Peters",
server: "USTP1VMS006",
server: "USSTP1VMS006",
plantToken: "usstp1",
idAddress: "10.37.0.26",
greatPlainsPlantCode: "45",
@@ -118,8 +118,53 @@ const servers: NewServerData[] = [
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Marked Tree",
server: "USMAR1VMS006",
plantToken: "usmar1",
idAddress: "10.206.9.26", // 3000,2222 requested REQ0236838
greatPlainsPlantCode: "90",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Iowa City EBM",
server: "USIOW1VMS006",
plantToken: "usiow1",
idAddress: "10.75.0.26",
greatPlainsPlantCode: "30",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Bowling Green 1",
server: "USBOW1VMS006",
plantToken: "usbow1",
idAddress: "10.25.0.26", // 3000 is open REQ0236527 2222 already open
greatPlainsPlantCode: "55",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Bethlehem",
server: "USBET1VMS006",
plantToken: "usbet1",
idAddress: "10.25.0.26",
greatPlainsPlantCode: "75",
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
export const serversChecks = async () => {
const log = createLogger({ module: "system", subModule: "serverData" });
const { data, error } = await tryCatch(
@@ -152,3 +197,9 @@ export const serversChecks = async () => {
log.info({}, "All Servers were added/updated");
}
};
// Communication from logistic network to logisticsSupportTool (for printers and scanners)
// network justification
// scanners and printers are on dhcp

View File

@@ -4,12 +4,10 @@ import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js";
import stats from "./stats.route.js";
import mobile from "./system.mobileApp.js";
export const setupSystemRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/stats`, stats);
app.use(`${baseUrl}/api/mobile`, mobile);
app.use(`${baseUrl}/api/settings`, getSettings);
app.use(`${baseUrl}/api/servers`, getServers);
app.use(`${baseUrl}/api/settings`, requireAuth, updSetting);

View File

@@ -2,6 +2,7 @@ import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import {
admin as adminPlugin,
genericOAuth,
// apiKey,
// createAuthMiddleware,
//customSession,
@@ -16,6 +17,46 @@ import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js";
function decodeJwtPayload<T = Record<string, unknown>>(jwt: string): T {
const parts = jwt.split(".");
if (parts.length < 2) {
throw new Error("Invalid JWT");
}
const payload = parts[1]?.replace(/-/g, "+").replace(/_/g, "/");
const padded = payload?.padEnd(
payload.length + ((4 - (payload.length % 4)) % 4),
"=",
);
const json = Buffer.from(padded ?? "", "base64").toString("utf8");
return JSON.parse(json) as T;
}
function normalizeGroups(groups?: unknown): string[] {
if (!Array.isArray(groups)) return [];
return groups
.filter((g): g is string => typeof g === "string")
.map((g) => g.trim().toLowerCase())
.filter((g) => g.length > 0);
}
type VoidAuthClaims = {
sub: string;
name?: string;
preferred_username?: string;
email?: string;
email_verified?: boolean;
groups?: string[];
picture?: string;
iss?: string;
aud?: string;
exp?: number;
iat?: number;
};
export const schema = {
user: rawSchema.user,
session: rawSchema.session,
@@ -25,9 +66,73 @@ export const schema = {
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
};
const hasOAuth =
Boolean(process.env.PROVIDER) &&
Boolean(process.env.CLIENT_ID) &&
Boolean(process.env.CLIENT_SECRET) &&
Boolean(process.env.DISCOVERY_URL);
if (!hasOAuth) {
console.warn("Missing oauth data.");
}
const oauthPlugins = hasOAuth
? [
genericOAuth({
config: [
{
providerId: process.env.PROVIDER!,
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
discoveryUrl: process.env.DISCOVERY_URL!,
scopes: (process.env.CLIENT_SCOPES ?? "")
.split(/[,\s]+/)
.filter(Boolean),
pkce: true,
requireIssuerValidation: true,
redirectURI: `${process.env.URL}/lst/api/auth/oauth2/callback/${process.env.PROVIDER!}`,
getUserInfo: async (tokens) => {
if (!tokens.idToken) {
throw new Error("VoidAuth did not return an idToken");
}
const claims = decodeJwtPayload<VoidAuthClaims>(tokens.idToken);
const groups = normalizeGroups(claims.groups);
return {
id: claims.sub,
email: claims.email ?? "",
name:
claims.name ??
claims.preferred_username ??
claims.email ??
"Unknown User",
image: claims.picture ?? null,
emailVerified: Boolean(claims.email_verified),
groups,
username: claims.preferred_username ?? null,
} as any;
},
mapProfileToUser: async (profile) => {
return {
name: profile.name,
role: profile.groups?.includes("lst_admins")
? "systemAdmin"
: profile.groups?.includes("admins")
? "admin"
: "user",
};
},
},
],
}),
]
: [];
export const auth = betterAuth({
appName: "lst",
baseURL: process.env.URL,
baseURL: `${process.env.URL}/lst/api/auth`,
database: drizzleAdapter(db, {
provider: "pg",
schema,
@@ -42,6 +147,14 @@ export const auth = betterAuth({
},
},
},
account: {
encryptOAuthTokens: true,
updateAccountOnSignIn: true,
accountLinking: {
enabled: true,
trustedProviders: ["voidauth"],
},
},
plugins: [
jwt({ jwt: { expirationTime: "1h" } }),
//apiKey(),
@@ -63,6 +176,7 @@ export const auth = betterAuth({
return true;
},
}),
...oauthPlugins,
// customSession(async ({ user, session }) => {
// const roles = await db
@@ -121,7 +235,7 @@ export const auth = betterAuth({
},
},
cookie: {
path: "/lst/app",
path: "/lst",
sameSite: "lax",
secure: false,
httpOnly: true,

View File

@@ -0,0 +1,39 @@
import { db } from "../db/db.controller.js";
import { returnFunc } from "./returnHelper.utils.js";
export function generateSixDigitPin() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function generateUniquePin() {
for (let i = 0; i < 10; i++) {
const pin = generateSixDigitPin();
const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, pin), // ⚠️ we'll fix this below
});
if (!existing)
return returnFunc({
success: true,
level: "info",
module: "utils",
subModule: "genPin",
message: "New pin generated",
data: [{ pin: pin }],
notify: false,
room: "",
});
}
return returnFunc({
success: false,
level: "error",
module: "utils",
subModule: "genPin",
message: "Failed to generate unique PIN after 10 attempts",
data: [],
notify: true,
room: "",
});
}

View File

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

View File

@@ -12,48 +12,39 @@ services:
#- "${VITE_PORT:-4200}:4200"
- "3600:3000"
dns:
- 10.193.9.250
- 10.193.9.251 # your internal DNS server
dns_search:
- alpla.net # or your internal search suffix
- 10.44.9.250
- 10.44.9.251 # your internal DNS server
- 1.1.1.1
environment:
- NODE_ENV=production
- LOG_LEVEL=info
- EXTERNAL_URL=http://192.168.8.222:3600
- DATABASE_HOST=host.docker.internal # if running on the same docker then do this
- DATABASE_PORT=5433
- URL=http://localhost:3600
- DATABASE_HOST=postgres # if running on the same docker then do this
- DATABASE_PORT=5432
- DATABASE_USER=${DATABASE_USER}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
- DATABASE_DB=${DATABASE_DB}
- PROD_SERVER=${PROD_SERVER}
- PROD_SERVER=10.75.9.56 #${PROD_SERVER}
- PROD_PLANT_TOKEN=${PROD_PLANT_TOKEN}
- PROD_USER=${PROD_USER}
- PROD_PASSWORD=${PROD_PASSWORD}
- GP_SERVER=10.193.9.31
- SQL_PORT=1433
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- BETTER_AUTH_URL=${URL}
# for all host including prod servers, plc's, printers, or other de
# extra_hosts:
# - "${PROD_SERVER}:${PROD_IP}"
- OPENDOCK_URL=${OPENDOCK_URL}
- OPENDOCK_PASSWORD=${OPENDOCK_PASSWORD}
- DEFAULT_DOCK=${DEFAULT_DOCK}
- DEFAULT_LOAD_TYPE=${DEFAULT_LOAD_TYPE}
- DEFAULT_CARRIER=${DEFAULT_CARRIER}
# networks:
# - default
# - logisticsNetwork
# #- mlan1
# networks:
# logisticsNetwork:
# driver: macvlan
# driver_opts:
# parent: eth0
# ipam:
# config:
# - subnet: ${LOGISTICS_NETWORK}
# gateway: ${LOGISTICS_GATEWAY}
#for all host including prod servers, plc's, printers, or other de
networks:
- docker-network
- pgNetwork
# mlan1:
# driver: macvlan
# driver_opts:
# parent: eth0
# ipam:
# config:
# - subnet: ${MLAN1_NETWORK}
# gateway: ${MLAN1_GATEWAY}
networks:
docker-network:
external: true
pgNetwork:
external: true

View File

@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router";
import { Bell, Logs, Server, Settings } from "lucide-react";
import { Bell, Logs, Server, Settings, UsersRound } from "lucide-react";
import {
SidebarGroup,
@@ -56,22 +56,22 @@ export default function AdminSidebar({ session }: any) {
module: "admin",
active: true,
},
// {
// title: "Modules",
// url: "/admin/modules",
// icon: Settings,
// role: ["systemAdmin", "admin"],
// module: "admin",
// active: true,
// },
// {
// title: "Servers",
// url: "/admin/servers",
// icon: Server,
// role: ["systemAdmin", "admin"],
// module: "admin",
// active: true,
// },
{
title: "Users",
url: "/admin/users",
icon: UsersRound,
role: ["systemAdmin", "admin"],
module: "admin",
active: true,
},
{
title: "Scan users",
url: "/admin/scanUsers",
icon: UsersRound,
role: ["systemAdmin", "admin"],
module: "admin",
active: true,
},
];
return (
<SidebarGroup>

View File

@@ -36,6 +36,17 @@ const docs = [
},
],
},
{
title: "Mobile",
url: "/updateInstructions",
isActive: false,
items: [
{
title: "Settings",
url: "/mobile-settings",
},
],
},
];
export default function DocBar() {
const { setOpen } = useSidebar();

View File

@@ -0,0 +1,49 @@
import { Link } from "@tanstack/react-router";
import { ScanText, ScrollText } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "../ui/sidebar";
export default function MobileBar({ session }: any) {
const { setOpen } = useSidebar();
const items = [
{
title: "Update Instructions",
url: "/",
icon: ScrollText,
},
{
title: "Scan Log",
url: "/",
icon: ScanText,
},
];
console.log(session);
return (
<SidebarGroup>
<SidebarGroupLabel>Mobile</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@@ -8,6 +8,7 @@ import {
import { useSession } from "@/lib/auth-client";
import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar";
import MobileBar from "./MobileBar";
export function AppSidebar() {
const { data: session } = useSession();
@@ -22,7 +23,8 @@ export function AppSidebar() {
<SidebarMenu>
<SidebarMenuItem>
<SidebarContent>
<DocBar/>
<DocBar />
<MobileBar session={session} />
{session &&
(session.user.role === "admin" ||
session.user.role === "systemAdmin") && (

View File

@@ -0,0 +1,3 @@
export default function updateInstructions() {
return <div>updateInstructions</div>;
}

View File

@@ -1,4 +1,4 @@
import { adminClient } from "better-auth/client/plugins";
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import { ac, admin, systemAdmin, user } from "./auth-permissions";
@@ -13,6 +13,7 @@ export const authClient = createAuthClient({
systemAdmin,
},
}),
genericOAuthClient(),
],
});

View File

@@ -11,6 +11,15 @@ import {
import React, { useState } from "react";
import { Button } from "../../components/ui/button";
import { ScrollArea, ScrollBar } from "../../components/ui/scroll-area";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import {
Table,
TableBody,
@@ -26,15 +35,23 @@ type LstTableType = {
tableClassName?: string;
data: any;
columns: any;
height?: string;
pageSize?: number;
};
export default function LstTable({
className = "",
tableClassName = "",
data,
columns,
height = "h-full",
pageSize = 5,
}: LstTableType) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [pagination, setPagination] = useState({
pageIndex: 0, //initial page index
pageSize: pageSize, //default page size
});
//console.log(data);
const table = useReactTable({
@@ -46,24 +63,33 @@ export default function LstTable({
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: setPagination,
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true,
// columnResizeMode: "onChange",
filterFns: {},
state: {
sorting,
pagination,
columnFilters,
},
});
return (
<div className={className}>
<ScrollArea className="w-full rounded-md border whitespace-nowrap">
<div>{/* TODO: Add table header in here like title */}</div>
<ScrollArea
className={`w-full rounded-md border whitespace-nowrap ${height}`}
>
<Table className={cn("w-full", tableClassName)}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
<TableHead
key={header.id}
className="sticky top-0 z-20 bg-background"
>
{header.isPlaceholder
? null
: flexRender(
@@ -76,6 +102,7 @@ export default function LstTable({
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
@@ -107,14 +134,23 @@ export default function LstTable({
<ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
</ScrollArea>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
{"<<"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
{"<"}
</Button>
<Button
variant="outline"
@@ -122,8 +158,42 @@ export default function LstTable({
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
{">"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
{">>"}
</Button>
<Select
value={pagination.pageSize.toString()}
onValueChange={(e) =>
setPagination({
...pagination,
pageSize: e === "all" ? data.length : parseInt(e, 10),
})
}
>
<SelectTrigger className="w-16">
<SelectValue
//id={field.name}
placeholder="Select Page"
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Page Size</SelectLabel>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="all">All</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
);

View File

@@ -15,6 +15,7 @@ import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminServersRouteImport } from './routes/admin/servers'
import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications'
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
import { Route as authLoginRouteImport } from './routes/(auth)/login'
@@ -52,6 +53,11 @@ const AdminServersRoute = AdminServersRouteImport.update({
path: '/admin/servers',
getParentRoute: () => rootRouteImport,
} as any)
const AdminScanUsersRoute = AdminScanUsersRouteImport.update({
id: '/admin/scanUsers',
path: '/admin/scanUsers',
getParentRoute: () => rootRouteImport,
} as any)
const AdminNotificationsRoute = AdminNotificationsRouteImport.update({
id: '/admin/notifications',
path: '/admin/notifications',
@@ -89,6 +95,7 @@ export interface FileRoutesByFullPath {
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
@@ -103,6 +110,7 @@ export interface FileRoutesByTo {
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
@@ -118,6 +126,7 @@ export interface FileRoutesById {
'/(auth)/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
@@ -134,6 +143,7 @@ export interface FileRouteTypes {
| '/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/docs/$'
@@ -148,6 +158,7 @@ export interface FileRouteTypes {
| '/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/docs/$'
@@ -162,6 +173,7 @@ export interface FileRouteTypes {
| '/(auth)/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/docs/$'
@@ -177,6 +189,7 @@ export interface RootRouteChildren {
authLoginRoute: typeof authLoginRoute
AdminLogsRoute: typeof AdminLogsRoute
AdminNotificationsRoute: typeof AdminNotificationsRoute
AdminScanUsersRoute: typeof AdminScanUsersRoute
AdminServersRoute: typeof AdminServersRoute
AdminSettingsRoute: typeof AdminSettingsRoute
DocsSplatRoute: typeof DocsSplatRoute
@@ -230,6 +243,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AdminServersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/scanUsers': {
id: '/admin/scanUsers'
path: '/admin/scanUsers'
fullPath: '/admin/scanUsers'
preLoaderRoute: typeof AdminScanUsersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/notifications': {
id: '/admin/notifications'
path: '/admin/notifications'
@@ -281,6 +301,7 @@ const rootRouteChildren: RootRouteChildren = {
authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute,
AdminScanUsersRoute: AdminScanUsersRoute,
AdminServersRoute: AdminServersRoute,
AdminSettingsRoute: AdminSettingsRoute,
DocsSplatRoute: DocsSplatRoute,

View File

@@ -1,4 +1,5 @@
import { Link, useNavigate } from "@tanstack/react-router";
import { Cat } from "lucide-react";
import { toast } from "sonner";
import {
Card,
@@ -9,13 +10,23 @@ import {
} from "@/components/ui/card";
import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff";
import { Button } from "../../../components/ui/button";
import socket from "../../../lib/socket.io";
export default function LoginForm({ redirectPath }: { redirectPath: string }) {
const loginEmail = localStorage.getItem("loginEmail") || "";
const rememberMe = localStorage.getItem("rememberMe") === "true";
const navigate = useNavigate();
const oauthLogin = async () => {
await authClient.signIn.oauth2({
providerId: "voidauth",
callbackURL: "/lst/app",
errorCallbackURL: "/lst/app/login",
});
};
const form = useAppForm({
defaultValues: {
email: loginEmail,
@@ -26,7 +37,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
// set remember me incase we want it later
if (value.rememberMe) {
localStorage.setItem("rememberMe", value.rememberMe.toString());
localStorage.setItem("loginEmail", value.email);
localStorage.setItem("loginEmail", value.email.toLocaleLowerCase());
} else {
localStorage.removeItem("rememberMe");
localStorage.removeItem("loginEmail");
@@ -62,7 +73,17 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
<div>
<Card className="p-3 w-96">
<CardHeader>
<CardTitle>Login to your account</CardTitle>
<CardTitle>
<div className="flex flex-row justify-center">
<Button onClick={oauthLogin} size="lg" variant="ghost">
<Cat />
</Button>
<span className="mt-2">Login to your account</span>{" "}
<Button size="lg" variant="ghost">
<Cat />
</Button>
</div>
</CardTitle>
<CardDescription>
Enter your username and password below
</CardDescription>
@@ -76,12 +97,19 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
>
<form.AppField name="email">
{(field) => (
<field.InputField label="Email" inputType="email" required />
<field.InputField
label="Email"
inputType="email"
required={rememberMe}
/>
)}
</form.AppField>
<form.AppField name="password">
{(field) => (
<field.InputPasswordField label="Password" required={true} />
<field.InputPasswordField
label="Password"
required={rememberMe}
/>
)}
</form.AppField>
@@ -98,7 +126,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
</Link>
</div>
<div className="flex justify-end mt-2">
<div className="flex justify-between mt-2 ">
<form.AppForm>
<form.SubmitButton>Login</form.SubmitButton>
</form.AppForm>

View File

@@ -5,8 +5,11 @@ import Header from "@/components/Header";
import { AppSidebar } from "@/components/Sidebar/sidebar";
import { SidebarProvider } from "@/components/ui/sidebar";
import { ThemeProvider } from "@/lib/theme-provider";
import { useSession } from "../lib/auth-client";
const RootLayout = () => (
const RootLayout = () => {
const { data: session } = useSession();
return (
<div className="[--header-height:calc(--spacing(14))]">
<ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}>
@@ -25,8 +28,11 @@ const RootLayout = () => (
<Toaster expand richColors closeButton />
</SidebarProvider>
</ThemeProvider>
{session && session.user.role === "systemAdmin" && (
<TanStackRouterDevtools />
)}
</div>
);
);
};
export const Route = createRootRoute({ component: RootLayout });

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/admin/scanUsers')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/admin/scanUsers"!</div>
}

View File

@@ -155,7 +155,7 @@ const ServerTable = () => {
);
}
return <LstTable data={data} columns={columns} />;
return <LstTable data={data} columns={columns} pageSize={50} />;
};
function RouteComponent() {

View File

@@ -59,6 +59,33 @@ function RouteComponent() {
Only shows machines that are attached to the silo.
</ul>
</ul>
{/* Mobile stuff */}
<li>Mobile App</li>
<ul className="list-disc list-inside indent-8">
<li>Rewrite of Alpla scan</li>
<ul className="list-disc list-inside indent-16">
<li>All old settings same as before id, ip, port</li>
<li>Currently scanned pallets will show now as well</li>
</ul>
<li>
Custom addition - login and more features NOTE: This is activated
based on how you enter the settings
</li>
<ul className="list-disc list-inside indent-16">
<li>Pin numbers login</li>
<li>
Scan a lane barcode and it returns whats in the lane and its
current status
</li>
<li>Command restrictions per pin login</li>
<li>Dock Door scanning</li>
<li>
More details on the pallet that is scanned by touching the running
number on the scanner.
</li>
</ul>
</ul>
{/* TMS integration */}
<li>TMS integration</li>
<ul className="list-disc list-inside indent-8">
<li>integration with TI to auto add in orders</li>

View File

@@ -2,9 +2,9 @@
"expo": {
"name": "LST mobile",
"slug": "lst-mobile",
"version": "0.0.1-alpha",
"version": "0.11.1-alpha",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"icon": "./assets/icon_white.png",
"scheme": "lstmobile",
"userInterfaceStyle": "automatic",
"ios": {
@@ -12,29 +12,44 @@
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png",
"package": "net.alpla.lst.mobile",
"versionCode": 1
"foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff"
},
"versionCode": 23,
"minSupportedVersionCode": 21,
"predictiveBackGestureEnabled": false,
"package": "com.anonymous.lstMobile"
"package": "net.alpla.lst.mobile"
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
"favicon": "./assets/images/favicon.png",
"bundler": "metro"
},
"plugins": [
"./plugins/withZebraDataWedge",
"expo-router",
[
"expo-splash-screen",
{
"backgroundColor": "#208AEF",
"android": {
"image": "./assets/images/splash-icon.png",
"imageWidth": 76
"resizeMode": "cover",
"image": "./assets/splash_white.png",
"backgroundColor": "#ffffff",
"dark": {
"image": "./assets/splash.png",
"backgroundColor": "#000000"
},
"imageWidth": 200
}
}
],
"expo-audio",
[
"expo-build-properties",
{
"android": {
"usesCleartextTraffic": true
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
lstMobile/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Binary file not shown.

BIN
lstMobile/assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,9 @@
module.exports = (api) => {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};

19
lstMobile/components.json Normal file
View File

@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "global.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

58
lstMobile/global.css Normal file
View File

@@ -0,0 +1,58 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 63%;
--radius: 0.625rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark:root {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 70.9% 59.4%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 300 0% 45%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}

View File

@@ -0,0 +1,9 @@
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, {
input: "./global.css",
inlineRem: 16,
});

3
lstMobile/nativewind-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "lstmobile",
"main": "expo-router/entry",
"version": "0.0.1-alpha",
"version": "0.0.2-alpha",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
@@ -9,22 +9,38 @@
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint",
"build:apk": "expo prebuild --clean && cd android && gradlew.bat assembleRelease ",
"update": "adb install android/app/build/outputs/apk/release/app-release.apk"
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat clean && gradlew.bat assembleRelease && npm run copy:apk",
"build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:mobile": "cd scripts && node runBuild.ts",
"build:mobile:bump": "cd scripts && node runBuild.ts --bump",
"copy:apk": "cd android && copy /Y app\\build\\outputs\\apk\\release\\app-release.apk ..\\..\\downloads\\mobile\\lst-mobile.apk",
"update": "adb install android/app/build/outputs/apk/release/app-release.apk",
"checklogs": "adb logcat -v time -s ReactNativeJS"
},
"dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.33",
"@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0",
"@tanstack/react-query": "^5.99.0",
"axios": "^1.15.0",
"babel-preset-expo": "^55.0.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns-tz": "^3.2.0",
"expo": "~55.0.15",
"expo-application": "~55.0.14",
"expo-audio": "~55.0.14",
"expo-av": "^16.0.8",
"expo-build-properties": "~55.0.13",
"expo-constants": "~55.0.14",
"expo-device": "~55.0.15",
"expo-font": "~55.0.6",
"expo-glass-effect": "~55.0.10",
"expo-haptics": "~55.0.14",
"expo-image": "~55.0.8",
"expo-linking": "~55.0.13",
"expo-router": "~55.0.12",
@@ -34,16 +50,22 @@
"expo-system-ui": "~55.0.15",
"expo-web-browser": "~55.0.14",
"lucide-react-native": "^1.8.0",
"nativewind": "^4.2.3",
"prettier-plugin-tailwindcss": "^0.5.14",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.4",
"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-screens": "~4.23.0",
"react-native-tcp-socket": "^6.4.1",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.7.2",
"socket.io-client": "^4.8.3",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.3.6",
"zustand": "^5.0.12"
},

View File

@@ -0,0 +1,317 @@
const { withDangerousMod } = require("@expo/config-plugins");
const fs = require("fs");
const path = require("path");
// const packageName = "net.alpla.lst.mobile";
// const packagePath = "com/alpla/lst/mobile";
const packageName = "net.alpla.lst.mobile";
const packagePath = "net/alpla/lst/mobile";
// const packageName = config.android?.package;
// const packagePath = packageName.replace(/\./g, "/");
const moduleCode = `package ${packageName}.scanner
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
class ZebraScannerModule(
private val reactContext: ReactApplicationContext
) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "ZebraScanner"
private val scanAction = "com.lst.mobile.SCAN"
private var receiverRegistered = false
private val scanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
println("LST SCANNER: received intent -> \${intent?.action}")
if (intent?.action != scanAction) {
println("LST SCANNER: wrong action")
return
}
val barcodeData: String? =
intent.getStringExtra("com.symbol.datawedge.data_string")
val labelType: String? =
intent.getStringExtra("com.symbol.datawedge.label_type")
val source: String? =
intent.getStringExtra("com.symbol.datawedge.source")
println("LST SCANNER: data=$barcodeData label=$labelType source=$source")
if (barcodeData.isNullOrBlank()) {
println("LST SCANNER: empty barcode")
return
}
val payload = Arguments.createMap().apply {
putString("data", barcodeData)
putString("labelType", labelType)
putString("source", source)
putDouble("timestamp", System.currentTimeMillis().toDouble())
}
sendEvent("barcodeScanned", payload)
}
}
@ReactMethod
fun startListening() {
if (receiverRegistered) return
reactContext.registerReceiver(
scanReceiver,
IntentFilter(scanAction),
Context.RECEIVER_EXPORTED
)
receiverRegistered = true
}
@ReactMethod
fun stopListening() {
if (!receiverRegistered) return
try {
reactContext.unregisterReceiver(scanReceiver)
} catch (_: Exception) {}
receiverRegistered = false
}
/**
* Required for React Native NativeEventEmitter
*/
@ReactMethod
fun addListener(eventName: String) {
// No-op
}
/**
* Required for React Native NativeEventEmitter
*/
@ReactMethod
fun removeListeners(count: Int) {
// No-op
}
@ReactMethod
fun triggerScan() {
val intent = Intent().apply {
action = "com.symbol.datawedge.api.ACTION"
putExtra("com.symbol.datawedge.api.SOFT_SCAN_TRIGGER", "TOGGLE_SCANNING")
}
reactContext.sendBroadcast(intent)
}
private fun sendCommand(command: String, value: Any) {
val intent = Intent().apply {
action = "com.symbol.datawedge.api.ACTION"
when (value) {
is String -> putExtra(command, value)
is Bundle -> putExtra(command, value)
}
}
reactContext.sendBroadcast(intent)
}
private fun sendEvent(eventName: String, payload: WritableMap) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, payload)
}
//
@ReactMethod
fun ensureProfile() {
val profileName = "LST_MOBILE"
sendCommand(
"com.symbol.datawedge.api.CREATE_PROFILE",
profileName
)
Thread.sleep(500)
val barcodeConfig = Bundle().apply {
putString("PLUGIN_NAME", "BARCODE")
putString("RESET_CONFIG", "true")
val props = Bundle().apply {
putString("scanner_input_enabled", "true")
// Auto-select internal scanner
putString("scanner_selection", "auto")
putString("scanner_selection_by_identifier", "AUTO")
// Hardware trigger behavior
putString("hardware_trigger_enabled", "true")
putString("trigger_mode", "2") // 2 = HARD trigger
// Disable Zebra's loud initial decode feedback
putString("decode_audio_feedback_uri", "")
putString("decode_haptic_feedback", "false")
putString("decode_led_feedback", "false")
// add in wake on trigger
putString("trigger_wakeup_scan", "true");
}
putBundle("PARAM_LIST", props)
}
val intentConfig = Bundle().apply {
putString("PLUGIN_NAME", "INTENT")
putString("RESET_CONFIG", "true")
val props = Bundle().apply {
putString("intent_output_enabled", "true")
putString("intent_action", scanAction)
putString("intent_delivery", "2") // broadcast
putString("intent_use_content_provider", "false")
}
putBundle("PARAM_LIST", props)
}
val keystrokeConfig = Bundle().apply {
putString("PLUGIN_NAME", "KEYSTROKE")
putString("RESET_CONFIG", "true")
val props = Bundle().apply {
putString("keystroke_output_enabled", "false")
}
putBundle("PARAM_LIST", props)
}
val profileConfig = Bundle().apply {
putString("PROFILE_NAME", profileName)
putString("PROFILE_ENABLED", "true")
putString("CONFIG_MODE", "CREATE_IF_NOT_EXIST")
putParcelableArrayList(
"PLUGIN_CONFIG",
arrayListOf(barcodeConfig, intentConfig, keystrokeConfig)
)
}
sendCommand("com.symbol.datawedge.api.SET_CONFIG", profileConfig)
val appConfig = Bundle().apply {
putString("PACKAGE_NAME", reactContext.packageName)
putStringArray("ACTIVITY_LIST", arrayOf("*"))
}
val associateConfig = Bundle().apply {
putString("PROFILE_NAME", profileName)
putString("CONFIG_MODE", "UPDATE")
putParcelableArray("APP_LIST", arrayOf(appConfig))
}
sendCommand("com.symbol.datawedge.api.SET_CONFIG", associateConfig)
// Runtime nudge: make sure scanner input is enabled for the active profile
sendCommand(
"com.symbol.datawedge.api.SCANNER_INPUT_PLUGIN",
"ENABLE_PLUGIN"
)
}
}
`;
const packageCode = `package ${packageName}.scanner
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class ZebraScannerPackage : ReactPackage {
override fun createNativeModules(
reactContext: ReactApplicationContext
): List<NativeModule> {
return listOf(ZebraScannerModule(reactContext))
}
override fun createViewManagers(
reactContext: ReactApplicationContext
): List<ViewManager<*, *>> {
return emptyList()
}
}
`;
function patchMainApplication(mainApplicationPath) {
let contents = fs.readFileSync(mainApplicationPath, "utf8");
const importLine = `import ${packageName}.scanner.ZebraScannerPackage`;
if (!contents.includes(importLine)) {
contents = contents.replace(
/import com\.facebook\.react\.PackageList/,
`import com.facebook.react.PackageList\n${importLine}`,
);
}
if (!contents.includes("add(ZebraScannerPackage())")) {
contents = contents.replace(
/PackageList\(this\)\.packages\.apply\s*\{/,
`PackageList(this).packages.apply {\n add(ZebraScannerPackage())`,
);
}
fs.writeFileSync(mainApplicationPath, contents);
}
module.exports = function withZebraScanner(config) {
return withDangerousMod(config, [
"android",
async (config) => {
const androidRoot = config.modRequest.platformProjectRoot;
const scannerDir = path.join(
androidRoot,
"app/src/main/java",
packagePath,
"scanner",
);
fs.mkdirSync(scannerDir, { recursive: true });
fs.writeFileSync(
path.join(scannerDir, "ZebraScannerModule.kt"),
moduleCode,
);
fs.writeFileSync(
path.join(scannerDir, "ZebraScannerPackage.kt"),
packageCode,
);
const mainApplicationPath = path.join(
androidRoot,
"app/src/main/java",
packagePath,
"MainApplication.kt",
);
patchMainApplication(mainApplicationPath);
return config;
},
]);
};

View File

@@ -0,0 +1,57 @@
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
const appJsonPath = path.resolve("../app.json");
// detect flags
const args = process.argv.slice(2);
const shouldBumpMin = args.includes("--bump");
try {
// 📖 read file
const raw = fs.readFileSync(appJsonPath, "utf-8");
const json = JSON.parse(raw);
const expo = json.expo ?? json; // supports both formats
if (!expo.android) {
throw new Error("No android config found in app.json");
}
// 🔢 current values
const currentVersionCode = expo.android.versionCode ?? 1;
const currentMin = expo.android.minSupportedVersionCode ?? 1;
// 🚀 increment version
const newVersionCode = currentVersionCode + 1;
expo.android.versionCode = newVersionCode;
if (shouldBumpMin) {
expo.android.minSupportedVersionCode = newVersionCode;
} else {
// keep existing min if not bumping
expo.android.minSupportedVersionCode = currentMin;
}
// 💾 write back
fs.writeFileSync(appJsonPath, JSON.stringify(json, null, 2));
console.log("✅ app.json updated:");
console.log(" versionCode:", newVersionCode);
console.log(
" minSupportedVersionCode:",
expo.android.minSupportedVersionCode,
);
// 🏗 run build
console.log("\n🚧 Running build:apk...\n");
execSync("npm run build:apk", { stdio: "inherit" });
console.log("\n🎉 Build complete!");
} catch (err) {
console.error("❌ Build script failed:");
console.error(err);
process.exit(1);
}

View File

@@ -0,0 +1,56 @@
import { Tabs } from "expo-router";
import { Home, Settings } from "lucide-react-native";
import { useAppStore } from "../../hooks/useAppStore";
export default function TabsLayout() {
const serverPort = useAppStore((s) => s.serverPort);
return (
<Tabs
screenOptions={{
headerShown: false, // Hides the header for all screens in this navigator
}}
>
<Tabs.Screen
name="scanner"
options={{
title: "Scan",
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
// header: ({ route }) => {
// const version = serverVersion?.versionCode;
// const hasUpdate = version && version > build;
// if (!hasUpdate) return null; // 👈 hides header completely
// return <GlobalHeader title={route.name} />;
// },
}}
/>
<Tabs.Screen
name="config"
options={{
title: "settings",
tabBarIcon: ({ color, size }) => (
<Settings size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="logs"
options={{
title: "Logs",
href:
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
}}
/>
{/* <Tabs.Screen
name="lanes"
options={{
title: "Lanes",
href:
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
}}
/> */}
</Tabs>
);
}

View File

@@ -0,0 +1,7 @@
import { Link } from "expo-router";
import { Text, View } from "react-native";
import Setup from "../setup";
export default function SettingsTab() {
return <Setup />
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
export default function Logs() {
const getInfo = async () => {
const info = "ho";
console.log(info);
};
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>Logs</Text>
<Button onPress={getInfo}>
<Text>Check info</Text>
</Button>
</View>
);
}

View File

@@ -0,0 +1,24 @@
import { View } from "react-native";
import LSTScanner from "../../components/LSTScanner";
import ProdScanner from "../../components/ProdScanner";
import { useAppStore } from "../../hooks/useAppStore";
export default function Scanner() {
const serverPort = useAppStore((s) => s.serverPort);
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
{parseInt(serverPort || "0", 10) >= 50000 ? (
<ProdScanner />
) : (
<LSTScanner />
)}
</View>
);
}

View File

@@ -1,5 +1,8 @@
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import "../../global.css";
import { PortalHost } from "@rn-primitives/portal";
import { View } from "react-native";
export default function RootLayout() {
return (
@@ -7,8 +10,19 @@ export default function RootLayout() {
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
{/* <Stack.Screen name="(tabs)" /> */}
<View className="items-center">
<Stack.Screen
name="(tabs)"
options={{
title: "Pending update",
headerStyle: {
backgroundColor: "lightblue",
},
}}
/>
</View>
</Stack>
<PortalHost />
</>
);
}

View File

@@ -1,9 +0,0 @@
import { Text, View } from "react-native";
export default function blocked() {
return (
<View>
<Text>Blocked</Text>
</View>
);
}

View File

@@ -1,15 +1,24 @@
import { useRouter } from "expo-router";
import axios from "axios";
import Constants from "expo-constants";
import { Redirect, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useServerStore } from "../hooks/useServerCheck";
import { devDelay } from "../lib/devMode";
export default function Index() {
const router = useRouter();
const [message, setMessage] = useState(<Text>Starting app...</Text>);
const [ready, setReady] = useState(false);
const setServerVersion = useServerStore((s) => s.setServerVersion);
//const { isUnlocked } = useMobileAuthStore();
const hasHydrated = useAppStore((s) => s.hasHydrated);
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
const build = Constants.expoConfig?.android?.versionCode ?? 1;
const hasValidSetup = useAppStore((s) => s.hasValidSetup);
useEffect(() => {
@@ -30,6 +39,35 @@ export default function Index() {
return;
}
// checking for lst.
console.log(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
);
try {
const res = await axios.get(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
{
timeout: 5000,
},
);
console.log(res.data);
// if the build version dose not match the latest server version force update
if (res.status === 200) {
setServerVersion(res.data);
}
// TODO: change the header to show orange and theres a new version
// console.log(build < res.data.minSupportedVersionCode);
// if (build < res.data.minSupportedVersionCode) {
// router.replace("/updateScreen");
// return;
// }
} catch (error) {
console.log("Error: ", error);
}
setMessage(<Text>Checking scanner mode...</Text>);
await devDelay(1500);
@@ -40,13 +78,18 @@ export default function Index() {
</Text>,
);
await devDelay(1500);
router.replace("/scanner");
//router.replace("/scanner");
setReady(true);
return;
}
setMessage(<Text>Checking for updates</Text>);
await devDelay(1500);
// TODO if theres an update go to update screen message :D
setMessage(<Text>Opening LST scan app</Text>);
await devDelay(3250);
router.replace("/scanner");
setReady(true);
} catch (error) {
console.log("Startup error", error);
setMessage(<Text>Something went wrong during startup.</Text>);
@@ -54,8 +97,21 @@ export default function Index() {
};
startup();
}, [hasHydrated, hasValidSetup, serverPort, router]);
}, [
hasHydrated,
hasValidSetup,
serverPort,
serverIp,
router,
setServerVersion,
]);
// if (ready && !isUnlocked) {
// return <Redirect href={"/login"} />;
// }
if (ready) {
return <Redirect href="/(tabs)/scanner" />;
}
return (
<View
style={{

View File

@@ -0,0 +1,52 @@
import axios from "axios";
import Constants from "expo-constants";
import { useRouter } from "expo-router";
import { useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
export default function Login() {
const { setUser } = useMobileAuthStore();
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
const onLogin = async () => {
try {
const res = await axios.get(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
{
timeout: 5000,
},
);
console.log(res.data);
} catch (error) {}
};
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<View className="flex items-center m-5">
<Text style={{ fontSize: 20, fontWeight: "600" }}>
LST Scanner Login
</Text>
<View className="w-64 p-4">
<Input
className="w-fit"
keyboardType="number-pad"
textContentType="oneTimeCode"
placeholder="Pin number"
/>
</View>
</View>
<Button title="Login" onPress={onLogin} />
</View>
);
}

View File

@@ -1,32 +0,0 @@
import React from "react";
import { Text, View } from "react-native";
export default function scanner() {
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<View style={{ alignItems: "center", margin: 10 }}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>LST Scanner</Text>
</View>
<View
style={{
marginTop: 50,
alignItems: "center",
}}
>
<Text>Relocate</Text>
<Text>0 / 4</Text>
</View>
{/* <View>
<Text>List of recent scanned pallets TBA</Text>
</View> */}
</View>
);
}

View File

@@ -3,8 +3,9 @@ import { useRouter } from "expo-router";
import { useState } from "react";
import { Alert, Button, Text, TextInput, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck";
export default function setup() {
export default function Setup() {
const router = useRouter();
const [auth, setAuth] = useState(false);
const [pin, setPin] = useState("");
@@ -22,6 +23,8 @@ export default function setup() {
const [serverPort, setLocalServerPort] = useState(serverPortFromStore);
const [scannerId, setScannerId] = useState(scannerIdFromStore);
const server = useServerStore((s) => s.serverVersion);
const authCheck = () => {
if (pin === "6971") {
setAuth(true);
@@ -40,6 +43,7 @@ export default function setup() {
updateAppState({
serverIp: serverIp.trim(),
serverPort: serverPort.trim(),
scannerId: scannerId?.trim(),
setupCompleted: true,
isRegistered: true,
});
@@ -80,7 +84,7 @@ export default function setup() {
borderRadius: 8,
}}
>
<Button title="Save Config" onPress={authCheck} />
<Button title="Submit" onPress={authCheck} />
</View>
</View>
) : (
@@ -136,7 +140,7 @@ export default function setup() {
<Button
title="Home"
onPress={() => {
router.push("/");
router.push("/(tabs)/scanner");
}}
/>
</View>
@@ -150,8 +154,11 @@ export default function setup() {
marginBottom: 12,
}}
>
<Text style={{ fontSize: 12, color: "#666" }}>
LST Scanner v{version}-{build}
<Text className="text-sm color-[#312f2f]">
App v{version}-{build}
</Text>
<Text className="text-sm color-[#312f2f]">
Server version - v{server?.versionName}-{server?.versionCode}
</Text>
</View>
</View>

View File

@@ -0,0 +1,47 @@
import Constants from "expo-constants";
import { Link } from "expo-router";
import { Text, View } from "react-native";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "../components/ui/card";
import { Separator } from "../components/ui/separator";
import { useServerStore } from "../hooks/useServerCheck";
export default function Update() {
const version = Constants.expoConfig?.version;
const build = Constants.expoConfig?.android?.versionCode ?? 1;
const server = useServerStore((s) => s.serverVersion);
return (
<View className="flex-1 mt-5 p-5">
<Card>
<CardHeader>
<Text className="text-center underline">Update Required</Text>
</CardHeader>
<CardContent>
<Text>Your app is out of date and needs to be updated</Text>
<Separator className="mt-5 mb-5" />
<Text>
App version - v{version}-{build}
</Text>
<Text>
Server version - v{server?.versionName}-{server?.versionCode}
</Text>
<Separator className="mt-5 mb-5" />
<Text>
To update the app please head go to a computer and open LST.
</Text>
<Text>Then head to Scan.</Text>
<Text>Click update Then follow the instructions on screen</Text>
</CardContent>
</Card>
{server && server?.versionCode >= build && (
<Link href={"/"}>
<Text className="text-center underline">Home</Text>
</Link>
)}
</View>
);
}

View File

@@ -0,0 +1,51 @@
import { useCallback, useEffect } from "react";
import { Text, View } from "react-native";
import { zebraScanner } from "../lib/ZebraScanner";
export default function LSTScanner() {
const handleScan = useCallback(async (scan: any) => {
console.log(scan);
}, []);
const clearScans = () => {
// add in
};
//console.log(lastScan);
useEffect(() => {
zebraScanner.ensureProfile();
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
return (
<View>
<View style={{ alignItems: "center", margin: 10 }}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>LST Scanner</Text>
</View>
<View
style={{
marginTop: 50,
alignItems: "center",
}}
>
<Text>Relocate</Text>
<Text>0 / 4</Text>
</View>
{/* <View>
<Text>List of recent scanned pallets TBA</Text>
</View> */}
</View>
);
}

View File

@@ -0,0 +1,160 @@
import axios from "axios";
import { format } from "date-fns-tz";
import { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter";
import { Separator } from "./ui/separator";
const STX = "\x02";
const ETX = "\x03";
export default function ProdScanner() {
const lastScan = useScannerStore((s) => s.lastScan);
const setLastScan = useScannerStore((s) => s.setLastScan);
const [tagScans, setTagScans] = useState<any>([]);
const scannerIdFromStore = useAppStore((s) => s.scannerId);
const serverIp = useAppStore((s) => s.serverIp);
const serverPort = useAppStore((s) => s.serverPort);
const [bgColor, setBGColor] = useState<string | null>(null);
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
let commandToSend = `${STX}${scannerIdFromStore}@${scan.data}${ETX}`;
// if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
if (scan.data.startsWith("000")) {
commandToSend = `${STX}${scannerIdFromStore}@]C1${scan.data}${ETX}`;
setTagScans((prev: any) => [
{
label: parseInt(scan.data.slice(10, -1) || "000", 10).toString(),
date: format(new Date(Date.now()), "HH:mm"),
},
...prev,
]);
}
const scanned = (await sendTcpMessage(
commandToSend,
serverIp,
parseInt(serverPort || "0", 10),
)) as any;
// send the logs to lst but allow it to time out if it dose not exist just bc.
try {
await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/logs`,
scanned,
);
} catch (error) {
console.log(error);
}
// const response = await sendTcpMessage(tcpMessage);
console.log(scanned.data);
if (scanned.data.status !== "error") {
await scannerFeedback({
type: "good",
sound: true,
vibrate: true,
led: true,
});
setBGColor("bg-green-500");
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
}
if (scanned.data.status === "error") {
await scannerFeedback({
type: scanned.data.status === "error" ? "bad" : "good",
sound: true,
vibrate: true,
led: true,
});
setBGColor("bg-red-500");
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
}
setLastScan(scanned.data);
// if we change commands we want to zero out the last scanned labels
if (/^[a-zA-Z]/.test(scan.data)) {
setTagScans([]);
}
},
[scannerIdFromStore, serverIp, serverPort, setLastScan],
);
const clearScans = () => {
setTagScans([]);
};
//console.log(lastScan);
useEffect(() => {
zebraScanner.ensureProfile();
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View>
<View style={{ alignItems: "center", margin: 10 }}>
<Text style={{ fontSize: 15, fontWeight: "600" }}>
Scanner ID: {parseInt(scannerIdFromStore || "0", 10)}
</Text>
</View>
<Separator />
{!lastScan ? (
<View style={{ marginTop: 10, alignItems: "center" }}>
<Text className="text-xl font-bold">Ready to scan</Text>
<Text>Waiting for first scan...</Text>
</View>
) : (
<View
style={{
marginTop: 10,
alignItems: "center",
}}
>
{lastScan.lines
?.filter((line) => !/^\d+@$/.test(line))
.map((i) => {
return (
<View style={{ marginTop: 10, alignItems: "center" }} key={i}>
<Text style={{ fontSize: 18, fontWeight: "600" }}>{i}</Text>
</View>
);
})}
</View>
)}
</View>
<Separator className="m-2" />
<View className="flex-1 w-full px-4">
<ScannedLabelBox
labels={tagScans}
color={bgColor}
clearScan={clearScans}
/>
</View>
<View>
<GlobalFooter />
</View>
</View>
);
}

View File

@@ -0,0 +1,58 @@
import { useEffect, useState } from "react";
import { Button, Text, View } from "react-native";
import { sendTcpMessage } from "../lib/tcpScan";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
const STX = "\x02";
const ETX = "\x03";
export function ScannerTestScreen() {
const [lastResponse, setLastResponse] = useState("");
const handleScan = async (scan: ZebraScanResult) => {
console.log("Raw Zebra scan:", scan);
const scanned = scan.data;
let commandToSend = `${STX}98@${scanned}${ETX}`;
// if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
if (scan.data.startsWith("000")) {
commandToSend = `${STX}98@]C1${scanned}${ETX}`;
}
const something = await sendTcpMessage(commandToSend, "10.44.0.26", 50001);
// Later this is where your TCP send goes.
// const response = await sendTcpMessage(tcpMessage);
console.log("TCP response:", something);
setLastResponse(JSON.stringify(something));
};
useEffect(() => {
zebraScanner.ensureProfile();
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, []);
return (
<View style={{ padding: 20, gap: 12 }}>
<Button
title="Soft Trigger Scan"
onPress={() => zebraScanner.triggerScan()}
/>
<Text>Waiting for scan...</Text>
<Text>{lastResponse}</Text>
</View>
);
}

View File

@@ -0,0 +1,58 @@
import { ScrollView, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Button } from "@/components/ui/button";
import { Text } from "@/components/ui/text";
import { Card } from "./ui/card";
type ScannedLabel = {
label: string;
date: Date;
};
type ScannedLabelBoxProps = {
labels: ScannedLabel[];
color: string | null;
clearScan: () => void;
};
export function ScannedLabelBox({
labels,
color,
clearScan,
}: ScannedLabelBoxProps) {
return (
<SafeAreaView className={`flex-1 w-full items-center ${color ?? ""}`}>
<View className="flex flex-col gap-2">
<Text style={{ fontSize: 18, fontWeight: "700", marginBottom: 8 }}>
Current scanned labels
</Text>
</View>
<ScrollView className="w-full flex-1">
{labels.length === 0 ? (
<Text className="text-center">
pending new labels to be scanned...
</Text>
) : (
<View className="flex items-center gap-2 w-full">
{labels.map((i, index) => (
<Card
key={`${i.label}-${index}`}
className={`p-2 border rounded items-center ${color ?? ""} w-full`}
>
<Text style={{ fontSize: 18, fontWeight: "700" }}>
{i.label} - {i.date.toString()}
</Text>
</Card>
))}
</View>
)}
</ScrollView>
{/* {labels.length !== 0 && (
<Button onPress={clearScan} variant="secondary">
<Text>Clear Scans</Text>
</Button>
)} */}
</SafeAreaView>
);
}

View File

@@ -0,0 +1,40 @@
import Constants from "expo-constants";
import { Link } from "expo-router";
import { Text, View } from "react-native";
import { useServerStore } from "../hooks/useServerCheck";
export function GlobalFooter() {
const build = Constants.expoConfig?.android?.versionCode ?? 1;
const serverVersion = useServerStore((s) => s.serverVersion);
const hasUpdate =
serverVersion && serverVersion?.minSupportedVersionCode > build;
const shouldUpdate = serverVersion && serverVersion?.versionCode > build;
if (serverVersion && serverVersion?.versionCode <= build) return;
return (
<View>
<View>
{hasUpdate && (
<View className="items-center h-[75px] bg-[#EB091A]">
<Link href={"/updateScreen"}>
<Text className="h-[75px] font-medium text-base text-wrap text-center">
Critical updates pending, once you are completed with your task
please click me for instructions to update
</Text>
</Link>
</View>
)}
{!hasUpdate && shouldUpdate && (
<View className="bg-[#FDBA74]">
<Link href={"/updateScreen"}>
<Text className="h-[32] font-medium text-lg text-wrap text-center">
There is an update click me for instructions
</Text>
</Link>
</View>
)}
</View>
</View>
);
}

View File

@@ -0,0 +1,106 @@
import { TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';
import { Platform, Pressable } from 'react-native';
const buttonVariants = cva(
cn(
'group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none',
Platform.select({
web: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
})
),
{
variants: {
variant: {
default: cn(
'bg-primary active:bg-primary/90 shadow-sm shadow-black/5',
Platform.select({ web: 'hover:bg-primary/90' })
),
destructive: cn(
'bg-destructive active:bg-destructive/90 dark:bg-destructive/60 shadow-sm shadow-black/5',
Platform.select({
web: 'hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
})
),
outline: cn(
'border-border bg-background active:bg-accent dark:bg-input/30 dark:border-input dark:active:bg-input/50 border shadow-sm shadow-black/5',
Platform.select({
web: 'hover:bg-accent dark:hover:bg-input/50',
})
),
secondary: cn(
'bg-secondary active:bg-secondary/80 shadow-sm shadow-black/5',
Platform.select({ web: 'hover:bg-secondary/80' })
),
ghost: cn(
'active:bg-accent dark:active:bg-accent/50',
Platform.select({ web: 'hover:bg-accent dark:hover:bg-accent/50' })
),
link: '',
},
size: {
default: cn('h-10 px-4 py-2 sm:h-9', Platform.select({ web: 'has-[>svg]:px-3' })),
sm: cn('h-9 gap-1.5 rounded-md px-3 sm:h-8', Platform.select({ web: 'has-[>svg]:px-2.5' })),
lg: cn('h-11 rounded-md px-6 sm:h-10', Platform.select({ web: 'has-[>svg]:px-4' })),
icon: 'h-10 w-10 sm:h-9 sm:w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const buttonTextVariants = cva(
cn(
'text-foreground text-sm font-medium',
Platform.select({ web: 'pointer-events-none transition-colors' })
),
{
variants: {
variant: {
default: 'text-primary-foreground',
destructive: 'text-white',
outline: cn(
'group-active:text-accent-foreground',
Platform.select({ web: 'group-hover:text-accent-foreground' })
),
secondary: 'text-secondary-foreground',
ghost: 'group-active:text-accent-foreground',
link: cn(
'text-primary group-active:underline',
Platform.select({ web: 'underline-offset-4 hover:underline group-hover:underline' })
),
},
size: {
default: '',
sm: '',
lg: '',
icon: '',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
type ButtonProps = React.ComponentProps<typeof Pressable> & VariantProps<typeof buttonVariants>;
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
<Pressable
className={cn(props.disabled && 'opacity-50', buttonVariants({ variant, size }), className)}
role="button"
{...props}
/>
</TextClassContext.Provider>
);
}
export { Button, buttonTextVariants, buttonVariants };
export type { ButtonProps };

View File

@@ -0,0 +1,52 @@
import { Text, TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import { View } from 'react-native';
function Card({ className, ...props }: React.ComponentProps<typeof View>) {
return (
<TextClassContext.Provider value="text-card-foreground">
<View
className={cn(
'bg-card border-border flex flex-col gap-6 rounded-xl border py-6 shadow-sm shadow-black/5',
className
)}
{...props}
/>
</TextClassContext.Provider>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<typeof View>) {
return <View className={cn('flex flex-col gap-1.5 px-6', className)} {...props} />;
}
function CardTitle({
className,
...props
}: React.ComponentProps<typeof Text>) {
return (
<Text
role="heading"
aria-level={3}
className={cn('font-semibold leading-none', className)}
{...props}
/>
);
}
function CardDescription({
className,
...props
}: React.ComponentProps<typeof Text>) {
return <Text className={cn('text-muted-foreground text-sm', className)} {...props} />;
}
function CardContent({ className, ...props }: React.ComponentProps<typeof View>) {
return <View className={cn('px-6', className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<typeof View>) {
return <View className={cn('flex flex-row items-center px-6', className)} {...props} />;
}
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

View File

@@ -0,0 +1,29 @@
import { cn } from '@/lib/utils';
import { Platform, TextInput } from 'react-native';
function Input({ className, ...props }: React.ComponentProps<typeof TextInput> & React.RefAttributes<TextInput>) {
return (
<TextInput
className={cn(
'dark:bg-input/30 border-input bg-background text-foreground flex h-10 w-full min-w-0 flex-row items-center rounded-md border px-3 py-1 text-base leading-5 shadow-sm shadow-black/5 sm:h-9',
props.editable === false &&
cn(
'opacity-50',
Platform.select({ web: 'disabled:pointer-events-none disabled:cursor-not-allowed' })
),
Platform.select({
web: cn(
'placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground outline-none transition-[color,box-shadow] md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive'
),
native: 'placeholder:text-muted-foreground/50',
}),
className
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,24 @@
import { cn } from '@/lib/utils';
import * as SeparatorPrimitive from '@rn-primitives/separator';
function Separator({
className,
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
);
}
export { Separator };

View File

@@ -0,0 +1,88 @@
import { cn } from '@/lib/utils';
import * as Slot from '@rn-primitives/slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { Platform, Text as RNText, type Role } from 'react-native';
const textVariants = cva(
cn(
'text-foreground text-base',
Platform.select({
web: 'select-text',
})
),
{
variants: {
variant: {
default: '',
h1: cn(
'text-center text-4xl font-extrabold tracking-tight',
Platform.select({ web: 'scroll-m-20 text-balance' })
),
h2: cn(
'border-border border-b pb-2 text-3xl font-semibold tracking-tight',
Platform.select({ web: 'scroll-m-20 first:mt-0' })
),
h3: cn('text-2xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
h4: cn('text-xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
p: 'mt-3 leading-7 sm:mt-6',
blockquote: 'mt-4 border-l-2 pl-3 italic sm:mt-6 sm:pl-6',
code: cn(
'bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold'
),
lead: 'text-muted-foreground text-xl',
large: 'text-lg font-semibold',
small: 'text-sm font-medium leading-none',
muted: 'text-muted-foreground text-sm',
},
},
defaultVariants: {
variant: 'default',
},
}
);
type TextVariantProps = VariantProps<typeof textVariants>;
type TextVariant = NonNullable<TextVariantProps['variant']>;
const ROLE: Partial<Record<TextVariant, Role>> = {
h1: 'heading',
h2: 'heading',
h3: 'heading',
h4: 'heading',
blockquote: Platform.select({ web: 'blockquote' as Role }),
code: Platform.select({ web: 'code' as Role }),
};
const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {
h1: '1',
h2: '2',
h3: '3',
h4: '4',
};
const TextClassContext = React.createContext<string | undefined>(undefined);
function Text({
className,
asChild = false,
variant = 'default',
...props
}: React.ComponentProps<typeof RNText> &
TextVariantProps & {
asChild?: boolean;
}) {
const textClass = React.useContext(TextClassContext);
const Component = asChild ? Slot.Text : RNText;
return (
<Component
className={cn(textVariants({ variant }), textClass, className)}
role={variant ? ROLE[variant] : undefined}
aria-level={variant ? ARIA_LEVEL[variant] : undefined}
{...props}
/>
);
}
export { Text, TextClassContext };

View File

@@ -33,10 +33,8 @@ type AppActions = {
setValidationStatus: (status: ValidationStatus, validatedAt?: string) => void;
setAppVersion: (value?: string) => void;
setHasHydrated: (value: boolean) => void;
updateAppState: (updates: Partial<AppState>) => void;
resetApp: () => void;
hasValidSetup: () => boolean;
canEnterApp: () => boolean;
getServerUrl: () => string;
@@ -50,15 +48,11 @@ const defaultAppState: AppState = {
scannerId: "0001",
stageId: undefined,
deviceName: undefined,
setupCompleted: false,
isRegistered: false,
lastValidationStatus: "idle",
lastValidationAt: undefined,
appVersion: undefined,
hasHydrated: false,
};
@@ -74,28 +68,23 @@ export const useAppStore = create<AppStore>()(
setDeviceName: (value) => set({ deviceName: value }),
setSetupCompleted: (value) => set({ setupCompleted: value }),
setIsRegistered: (value) => set({ isRegistered: value }),
setValidationStatus: (status, validatedAt) =>
set({
lastValidationStatus: status,
lastValidationAt: validatedAt,
}),
setAppVersion: (value) => set({ appVersion: value }),
setHasHydrated: (value) => set({ hasHydrated: value }),
updateAppState: (updates) =>
set((state) => ({
...state,
...updates,
})),
resetApp: () =>
set({
...defaultAppState,
hasHydrated: true,
}),
hasValidSetup: () => {
const state = get();
return Boolean(
@@ -104,7 +93,6 @@ export const useAppStore = create<AppStore>()(
state.setupCompleted,
);
},
canEnterApp: () => {
const state = get();
return Boolean(

View File

@@ -0,0 +1,28 @@
import { create } from "zustand";
type MobileUser = {
id: string;
name: string;
role: "user" | "lead" | "manager" | "admin";
excludedCommand: string[];
};
type AuthState = {
user: MobileUser | null;
isUnlocked: boolean;
setUser: (user: MobileUser) => void;
lock: () => void;
logout: () => void;
};
export const useMobileAuthStore = create<AuthState>((set) => ({
user: null,
isUnlocked: false,
setUser: (user) => set({ user, isUnlocked: true }),
lock: () => set({ isUnlocked: false }),
logout: () => set({ user: null, isUnlocked: false }),
}));

View File

@@ -0,0 +1,33 @@
import { create } from "zustand";
type LastScan = {
terminalId?: string;
screen?: string;
prompt?: string;
message?: string;
status: "success" | "error" | "location" | "unknown";
lines?: string[];
timestamp?: number;
};
type ScannerStore = {
lastScan: LastScan | null;
setLastScan: (scan: LastScan | null) => void;
clearLastScan: () => void;
};
export const useScannerStore = create<ScannerStore>((set) => ({
lastScan: null,
setLastScan: (scan) =>
set({
lastScan: scan
? {
...scan,
timestamp: Date.now(),
}
: null,
}),
clearLastScan: () => set({ lastScan: null }),
}));

View File

@@ -0,0 +1,29 @@
import { create } from "zustand";
type ServerVersion = {
packageName: string;
versionName: string;
versionCode: number;
minSupportedVersionCode: number;
downloadUrl: string;
};
type AppState = {
serverVersion: ServerVersion | null;
setServerVersion: (data: ServerVersion) => void;
};
export const useServerStore = create<AppState>((set, get) => ({
serverVersion: null,
hasUpdate: () => {
const v = get().serverVersion;
if (!v) return false;
return v.versionCode < v.minSupportedVersionCode;
},
setServerVersion: (data) =>
set(() => ({
serverVersion: data,
})),
}));

View File

@@ -0,0 +1,40 @@
import {
type EmitterSubscription,
NativeEventEmitter,
NativeModules,
} from "react-native";
const { ZebraScanner } = NativeModules;
const scannerEmitter = new NativeEventEmitter(ZebraScanner);
export type ZebraScanResult = {
data: string;
labelType?: string;
source?: string;
timestamp: number;
};
export const zebraScanner = {
startListening() {
ZebraScanner.startListening();
},
stopListening() {
ZebraScanner.stopListening();
},
triggerScan() {
ZebraScanner.triggerScan();
},
ensureProfile() {
ZebraScanner.ensureProfile();
},
addScanListener(
callback: (scan: ZebraScanResult) => void,
): EmitterSubscription {
return scannerEmitter.addListener("barcodeScanned", callback);
},
};

View File

@@ -0,0 +1,13 @@
const roleRank = {
user: 1,
lead: 2,
manager: 3,
admin: 4,
} as const;
export function hasMobileRole(
userRole: keyof typeof roleRank,
requiredRole: keyof typeof roleRank,
) {
return roleRank[userRole] >= roleRank[requiredRole];
}

View File

@@ -0,0 +1,38 @@
import { createAudioPlayer } from "expo-audio";
import * as Haptics from "expo-haptics";
export type ScanFeedback = {
type: "good" | "bad";
sound?: boolean;
vibrate?: boolean;
led?: boolean;
};
const goodSound = createAudioPlayer(require("../../assets/sounds/good.wav"));
const badSound = createAudioPlayer(require("../../assets/sounds/bad.wav"));
export async function scannerFeedback({
type,
sound = true,
vibrate = true,
led = true,
}: ScanFeedback) {
if (sound) {
const player = type === "good" ? goodSound : badSound;
player.seekTo(0);
player.play();
}
if (vibrate) {
await Haptics.notificationAsync(
type === "good"
? Haptics.NotificationFeedbackType.Success
: Haptics.NotificationFeedbackType.Error,
);
}
if (led) {
// Zebra LED hook goes here
// More below 👇
}
}

View File

@@ -0,0 +1,292 @@
import TcpSocket from "react-native-tcp-socket";
// const STX = "\x02";
// const ETX = "\x03";
type TcpResponse = {
success: boolean;
message: string;
data: string[];
};
type ScannerEvent = {
scannerId?: string;
commandDescription?: string;
prompt?: string;
message?: string;
status: "success" | "error" | "location" | "unknown" | "scan";
lines?: string[];
};
// const ERROR_MESSAGES = [
// "Invalid barcode",
// "Already scanned",
// "Not on stock",
// "Article tolerance for consolidation not satisfied.",
// ];
const ERROR_KEYWORDS = [
"invalid barcode",
"already",
"not on stock",
"article tolerance",
"unloaded",
"delivered",
"blocked",
];
// function parseErpResponse(buffer: Buffer) {
// const text = buffer
// .toString("utf8")
// .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|#[0-9A-Za-z])/g, "")
// .replace(/\x02/g, "")
// .replace(/\x03/g, "")
// .trim();
// const noHeader = text.replace(/^\d+@/, "");
// console.log(text);
// if (!noHeader.includes("Scan:")) {
// return {
// raw: text,
// type: "error",
// message: noHeader.trim(),
// lines: [noHeader.trim()],
// };
// }
// const [actionPart, scanPart = ""] = noHeader.split("Scan:");
// const action = actionPart.trim();
// const scanClean = scanPart.trim();
// const successMatch = scanClean.match(/^(.*?)\s+V$/);
// if (successMatch) {
// const prompt = successMatch[1].trim();
// return {
// raw: text,
// type: "success",
// action,
// prompt,
// status: "V",
// lines: [action, prompt, "V"],
// };
// }
// // // Handles: "Production lotInvalid barcode"
// // const knownErrors = [
// // "Invalid barcode",
// // "Invalid machine",
// // "Not on stock",
// // "Article tolerance for consolidation not satisfied",
// // ].sort((a, b) => b.length - a.length);
// // const foundError = knownErrors.find((err) => scanClean.includes(err));
// // if (foundError) {
// // const prompt = scanClean.replace(foundError, "").trim();
// // return {
// // raw: text,
// // type: "error",
// // action,
// // prompt,
// // message: foundError,
// // lines: [action, prompt, foundError].filter(Boolean),
// // };
// // }
// // return {
// // raw: text,
// // type: "pending",
// // action,
// // prompt: scanClean,
// // lines: [action, scanClean].filter(Boolean),
// // };
// const unitMatch = scanClean.match(/^(Unit\s+\d+\/\d+)(.*)$/);
// if (unitMatch) {
// const prompt = unitMatch[1].trim(); // "Unit 1/4"
// const remainder = unitMatch[2].trim(); // everything after
// // SUCCESS
// if (remainder === "V") {
// return {
// raw: text,
// type: "success",
// action,
// prompt,
// status: "V",
// lines: [action, prompt, "V"],
// };
// }
// // Known ERP errors
// const knownErrors = [
// "Invalid barcode",
// "Invalid machine",
// "Not on stock",
// "Article tolerance for consolidation not satisfied",
// ];
// const foundError = knownErrors.find((err) =>
// remainder.toLowerCase().includes(err.toLowerCase()),
// );
// if (foundError) {
// return {
// raw: text,
// type: "error",
// action,
// prompt,
// message: foundError,
// lines: [action, prompt, foundError],
// };
// }
// if (remainder) {
// return {
// raw: text,
// type: "prompt",
// action,
// prompt,
// message: remainder,
// lines: [action, prompt, remainder],
// };
// }
// return {
// raw: text,
// type: "pending",
// action,
// prompt,
// lines: [action, prompt],
// };
// }
// }
const parseScannerText = (buffer: Buffer) => {
const text = buffer.toString("utf8");
return (
text
// remove cursor movement like ESC[122C, ESC[2;1H, ESC[8q
.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "\n")
// remove other ANSI sequences like ESC#5
.replace(/\x1B#[0-9]/g, "\n")
// normalize carriage returns
.replace(/\r/g, "\n")
// split into clean lines
.split(/\n+/)
// clean each line
.map((line) => line.trim())
// remove blanks
.filter(Boolean)
);
};
const parseScannerEvent = (lines: string[]): ScannerEvent => {
const scannerId = lines[0];
const messageLines = lines.slice(1);
const message = messageLines.at(-1);
const commandDescription = messageLines.find((x) => /^\d+\s+/.test(x));
const prompt = messageLines.find((x) => /^Scan:/i.test(x));
let status: ScannerEvent["status"] = "unknown";
const msg = message?.toLowerCase() ?? "";
if (msg === "v") status = "success";
else if (msg && ERROR_KEYWORDS.some((keyword) => msg.includes(keyword)))
status = "error";
else if (msg?.includes("scan")) status = "success";
// everything else will just be a location
else if (commandDescription?.includes("Relocate")) status = "location";
// TODO: split command description and use the command id next to description for sorting.
return {
scannerId,
commandDescription,
prompt,
message,
status,
lines,
};
};
/**
* Sends a Zebra-style TCP message:
* <STX>98@{scanned}<ETX>
*/
export async function sendTcpMessage(
command: string,
host: string,
port: number,
timeoutMs = 5000,
): Promise<TcpResponse> {
return new Promise((resolve) => {
const responses: any = [];
const client = TcpSocket.createConnection({ host, port }, () => {
//console.log("Sending TCP (visible):", `${command}`);
client.write(command);
});
const timeout = setTimeout(() => {
client.destroy();
resolve({
success: false,
message: "TCP timeout",
data: responses,
});
}, timeoutMs);
client.on("data", (data) => {
//console.log("TCP received:", text);
const parsed = parseScannerText(data);
//console.log("scanned:", parsed);
//responses.push(parsed);
const cleaned = parseScannerEvent(parsed);
//console.log(responses);
clearTimeout(timeout);
resolve({
success: true,
message: "TCP Response",
data: cleaned as any,
});
});
client.on("error", (err) => {
clearTimeout(timeout);
client.destroy();
resolve({
success: false,
message: err.message,
data: ["Error", "Please try again"],
});
});
client.on("close", () => {
clearTimeout(timeout);
resolve({
success: true,
message: "TCP complete",
data: ["Error", "Please try again"],
});
});
});
}

View File

@@ -0,0 +1,81 @@
import { DarkTheme, DefaultTheme, type Theme } from "@react-navigation/native";
export const THEME = {
light: {
background: "hsl(0 0% 100%)",
foreground: "hsl(0 0% 3.9%)",
card: "hsl(0 0% 100%)",
cardForeground: "hsl(0 0% 3.9%)",
popover: "hsl(0 0% 100%)",
popoverForeground: "hsl(0 0% 3.9%)",
primary: "hsl(0 0% 9%)",
primaryForeground: "hsl(0 0% 98%)",
secondary: "hsl(0 0% 96.1%)",
secondaryForeground: "hsl(0 0% 9%)",
muted: "hsl(0 0% 96.1%)",
mutedForeground: "hsl(0 0% 45.1%)",
accent: "hsl(0 0% 96.1%)",
accentForeground: "hsl(0 0% 9%)",
destructive: "hsl(0 84.2% 60.2%)",
border: "hsl(0 0% 89.8%)",
input: "hsl(0 0% 89.8%)",
ring: "hsl(0 0% 63%)",
radius: "0.625rem",
chart1: "hsl(12 76% 61%)",
chart2: "hsl(173 58% 39%)",
chart3: "hsl(197 37% 24%)",
chart4: "hsl(43 74% 66%)",
chart5: "hsl(27 87% 67%)",
},
dark: {
background: "hsl(0 0% 3.9%)",
foreground: "hsl(0 0% 98%)",
card: "hsl(0 0% 3.9%)",
cardForeground: "hsl(0 0% 98%)",
popover: "hsl(0 0% 3.9%)",
popoverForeground: "hsl(0 0% 98%)",
primary: "hsl(0 0% 98%)",
primaryForeground: "hsl(0 0% 9%)",
secondary: "hsl(0 0% 14.9%)",
secondaryForeground: "hsl(0 0% 98%)",
muted: "hsl(0 0% 14.9%)",
mutedForeground: "hsl(0 0% 63.9%)",
accent: "hsl(0 0% 14.9%)",
accentForeground: "hsl(0 0% 98%)",
destructive: "hsl(0 70.9% 59.4%)",
border: "hsl(0 0% 14.9%)",
input: "hsl(0 0% 14.9%)",
ring: "hsl(300 0% 45%)",
radius: "0.625rem",
chart1: "hsl(220 70% 50%)",
chart2: "hsl(160 60% 45%)",
chart3: "hsl(30 80% 55%)",
chart4: "hsl(280 65% 60%)",
chart5: "hsl(340 75% 55%)",
},
};
export const NAV_THEME: Record<"light" | "dark", Theme> = {
light: {
...DefaultTheme,
colors: {
background: THEME.light.background,
border: THEME.light.border,
card: THEME.light.card,
notification: THEME.light.destructive,
primary: THEME.light.primary,
text: THEME.light.foreground,
},
},
dark: {
...DarkTheme,
colors: {
background: THEME.dark.background,
border: THEME.dark.border,
card: THEME.dark.card,
notification: THEME.dark.destructive,
primary: THEME.dark.primary,
text: THEME.dark.foreground,
},
},
};

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,76 @@
const { hairlineWidth } = require("nativewind/theme");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
borderWidth: {
hairline: hairlineWidth(),
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
future: {
hoverOnlyWhenSupported: true,
},
plugins: [require("tailwindcss-animate")],
};

View File

@@ -4,7 +4,8 @@
"strict": true,
"paths": {
"@/*": [
"./src/*"
"./src/*",
"./*"
],
"@/assets/*": [
"./assets/*"
@@ -15,6 +16,7 @@
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
"expo-env.d.ts",
"nativewind-env.d.ts"
]
}

View File

@@ -0,0 +1,10 @@
CREATE TABLE "scan_log" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"scanner_id" text,
"message" text NOT NULL,
"prompt" text,
"command_description" text,
"status" text,
"lines" jsonb DEFAULT '[]'::jsonb,
"add_Date" timestamp DEFAULT now()
);

View File

@@ -0,0 +1,14 @@
CREATE TYPE "public"."mobile_role" AS ENUM('user', 'lead', 'manager', 'admin');--> statement-breakpoint
CREATE TABLE "scan_users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"scanner_id" integer NOT NULL,
"pin_number" integer NOT NULL,
"pin_hash" text NOT NULL,
"excluded_commands" text DEFAULT '',
"role" "mobile_role" DEFAULT 'user' NOT NULL,
"active" boolean DEFAULT true,
"last_scan" timestamp DEFAULT now(),
"add_Date" timestamp DEFAULT now(),
"upd_date" timestamp DEFAULT now()
);

View File

@@ -0,0 +1,3 @@
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_users_scanner_id_unique" UNIQUE("scanner_id");--> statement-breakpoint
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_users_pin_number_unique" UNIQUE("pin_number");--> statement-breakpoint
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_user_unique" UNIQUE("scanner_id","pin_number");

View File

@@ -0,0 +1,2 @@
ALTER TABLE "scan_users" ALTER COLUMN "scanner_id" SET DATA TYPE text;--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "pin_number" SET DATA TYPE text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -288,6 +288,34 @@
"when": 1776770845947,
"tag": "0040_rainy_white_tiger",
"breakpoints": true
},
{
"idx": 41,
"version": "7",
"when": 1777509638464,
"tag": "0041_bright_tempest",
"breakpoints": true
},
{
"idx": 42,
"version": "7",
"when": 1777659968051,
"tag": "0042_melted_talon",
"breakpoints": true
},
{
"idx": 43,
"version": "7",
"when": 1777664911423,
"tag": "0043_melted_lyja",
"breakpoints": true
},
{
"idx": 44,
"version": "7",
"when": 1777666145468,
"tag": "0044_steady_magneto",
"breakpoints": true
}
]
}

361
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "lst_v3",
"version": "0.0.1-alpha.4",
"version": "0.0.2-alpha.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lst_v3",
"version": "0.0.1-alpha.4",
"version": "0.0.2-alpha.7",
"license": "ISC",
"dependencies": {
"@dotenvx/dotenvx": "^1.57.0",
@@ -14,6 +14,7 @@
"@socket.io/admin-ui": "^0.5.1",
"archiver": "^7.0.1",
"axios": "^1.13.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5",
"concurrently": "^9.2.1",
"cors": "^2.8.6",
@@ -26,6 +27,7 @@
"express": "^5.2.1",
"husky": "^9.1.7",
"ldapts": "^8.1.7",
"modbus-serial": "^8.0.25",
"morgan": "^1.10.1",
"mssql": "^12.2.1",
"multer": "^2.1.1",
@@ -2116,6 +2118,263 @@
"node": ">=22"
}
},
"node_modules/@serialport/binding-mock": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "^1.2.1",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@serialport/bindings-cpp": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz",
"integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"@serialport/parser-readline": "12.0.0",
"debug": "4.4.0",
"node-addon-api": "8.3.0",
"node-gyp-build": "4.8.4"
},
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz",
"integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz",
"integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/parser-delimiter": "12.0.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@serialport/bindings-interface": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.22 || ^14.13 || >=16"
}
},
"node_modules/@serialport/parser-byte-length": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz",
"integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-cctalk": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz",
"integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-delimiter": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz",
"integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-inter-byte-timeout": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz",
"integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-packet-length": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz",
"integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/@serialport/parser-readline": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz",
"integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/parser-delimiter": "13.0.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-ready": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz",
"integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-regex": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz",
"integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-slip-encoder": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz",
"integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-spacepacket": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz",
"integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz",
"integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"debug": "4.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@simple-libs/child-process-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz",
@@ -2172,6 +2431,12 @@
"socket.io": ">=3.1.0"
}
},
"node_modules/@socket.io/admin-ui/node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/@socket.io/admin-ui/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@@ -3145,10 +3410,13 @@
"license": "MIT"
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/better-auth": {
"version": "1.5.5",
@@ -9124,6 +9392,18 @@
"node": ">=10"
}
},
"node_modules/modbus-serial": {
"version": "8.0.25",
"resolved": "https://registry.npmjs.org/modbus-serial/-/modbus-serial-8.0.25.tgz",
"integrity": "sha512-T6OHW80k7DtYZF96onavw84IXNu44EW+fybgVftWAGOraL8vTmMZod8w6thOrWj2I2qHC9Gsn2nitVTUDih+6A==",
"license": "ISC",
"dependencies": {
"debug": "^4.4.3"
},
"optionalDependencies": {
"serialport": "^13.0.0"
}
},
"node_modules/modify-values": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz",
@@ -9396,6 +9676,28 @@
"smart-buffer": "^4.1.0"
}
},
"node_modules/node-addon-api": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz",
"integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/nodemailer": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz",
@@ -10741,6 +11043,53 @@
"node": ">= 18"
}
},
"node_modules/serialport": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz",
"integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/binding-mock": "10.2.2",
"@serialport/bindings-cpp": "13.0.0",
"@serialport/parser-byte-length": "13.0.0",
"@serialport/parser-cctalk": "13.0.0",
"@serialport/parser-delimiter": "13.0.0",
"@serialport/parser-inter-byte-timeout": "13.0.0",
"@serialport/parser-packet-length": "13.0.0",
"@serialport/parser-readline": "13.0.0",
"@serialport/parser-ready": "13.0.0",
"@serialport/parser-regex": "13.0.0",
"@serialport/parser-slip-encoder": "13.0.0",
"@serialport/parser-spacepacket": "13.0.0",
"@serialport/stream": "13.0.0",
"debug": "4.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/serialport/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",

Some files were not shown because too many files have changed in this diff Show More