diff --git a/backend/db/db.controller.ts b/backend/db/db.controller.ts index 4b27765..8267ae4 100644 --- a/backend/db/db.controller.ts +++ b/backend/db/db.controller.ts @@ -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, + }, +}); diff --git a/backend/db/schema/scanUsers.ts b/backend/db/schema/scanUsers.ts new file mode 100644 index 0000000..c4ac877 --- /dev/null +++ b/backend/db/schema/scanUsers.ts @@ -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; +export type NewScanUser = z.infer; diff --git a/backend/mobile/donwloadApps.route.ts b/backend/mobile/donwloadApps.route.ts new file mode 100644 index 0000000..9df15c4 --- /dev/null +++ b/backend/mobile/donwloadApps.route.ts @@ -0,0 +1,46 @@ +import fs from "node:fs"; +import { Router } from "express"; +import path from "path"; +import { fileURLToPath } from "url"; + +const router = Router(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const downloadDir = path.resolve(__dirname, "../../downloads/mobile"); + +const currentApk = { + fileName: "lst-mobile.apk", +}; + +router.get("/latest", (_, res) => { + const apkPath = path.join(downloadDir, currentApk.fileName); + + 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="${currentApk.fileName}"`, + ); + + 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; diff --git a/backend/mobile/mobile.routes.ts b/backend/mobile/mobile.routes.ts new file mode 100644 index 0000000..cec1c2b --- /dev/null +++ b/backend/mobile/mobile.routes.ts @@ -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/* +}; diff --git a/backend/mobile/mobileAuth.route.ts b/backend/mobile/mobileAuth.route.ts new file mode 100644 index 0000000..017443b --- /dev/null +++ b/backend/mobile/mobileAuth.route.ts @@ -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 = {}; + 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; diff --git a/backend/mobile/mobilePin.route.ts b/backend/mobile/mobilePin.route.ts new file mode 100644 index 0000000..31c8056 --- /dev/null +++ b/backend/mobile/mobilePin.route.ts @@ -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; diff --git a/backend/mobile/scanLogs.route.ts b/backend/mobile/scanLogs.route.ts new file mode 100644 index 0000000..feda531 --- /dev/null +++ b/backend/mobile/scanLogs.route.ts @@ -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; diff --git a/backend/mobile/version.route.ts b/backend/mobile/version.route.ts new file mode 100644 index 0000000..3d36613 --- /dev/null +++ b/backend/mobile/version.route.ts @@ -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; diff --git a/backend/routeHandler.routes.ts b/backend/routeHandler.routes.ts index 6eb854c..4da1be9 100644 --- a/backend/routeHandler.routes.ts +++ b/backend/routeHandler.routes.ts @@ -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); }; diff --git a/backend/system/serverData.controller.ts b/backend/system/serverData.controller.ts index 21b4120..3f81295 100644 --- a/backend/system/serverData.controller.ts +++ b/backend/system/serverData.controller.ts @@ -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", // 3000 opened and working + idAddress: "10.44.0.56", // ports opened 3000 and 2222 greatPlainsPlantCode: "80", contactEmail: "", contactPhone: "", @@ -122,7 +122,7 @@ const servers: NewServerData[] = [ name: "Marked Tree", server: "USMAR1VMS006", plantToken: "usmar1", - idAddress: "10.206.9.26", + idAddress: "10.206.9.26", // 3000,2222 requested REQ0236838 greatPlainsPlantCode: "90", contactEmail: "", contactPhone: "", @@ -144,7 +144,7 @@ const servers: NewServerData[] = [ name: "Bowling Green 1", server: "USBOW1VMS006", plantToken: "usbow1", - idAddress: "10.25.0.26", // 3000 is open REQ0236527 + idAddress: "10.25.0.26", // 3000 is open REQ0236527 2222 already open greatPlainsPlantCode: "55", contactEmail: "", contactPhone: "", diff --git a/backend/system/system.mobileApp.ts b/backend/system/system.mobileApp.ts deleted file mode 100644 index 0385b9d..0000000 --- a/backend/system/system.mobileApp.ts +++ /dev/null @@ -1,93 +0,0 @@ -import fs from "node:fs"; -import { Router } from "express"; -import path from "path"; -import { fileURLToPath } from "url"; -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(); - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const downloadDir = path.resolve(__dirname, "../../downloads/mobile"); -const projectRoot = path.resolve("./lstMobile"); // adjust as needed -const appJsonPath = path.join(projectRoot, "app.json"); - -const currentApk = { - fileName: "lst-mobile.apk", -}; - -router.get("/version", 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`, - }); -}); - -router.get("/apk/latest", (_, res) => { - const apkPath = path.join(downloadDir, currentApk.fileName); - - 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="${currentApk.fileName}"`, - ); - - return res.sendFile(apkPath); -}); - -router.get("/apk/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); -}); - -router.post("/logs", async (req, res) => { - const body = req.body; - const newLog = await db - .insert(scanLog) - .values({ - scannerId: body.data.scannerId, - message: body.data.message, - prompt: body.data.prompt, - commandDescription: body.data.commandDescription, - status: body.data.status, - lines: body.data.lines, - }) - .returning(); - - return apiReturn(res, { - success: true, - level: "info", - module: "mobile", - subModule: "scan logs", - message: `New log from ${body.data.scannerId}`, - data: newLog, - status: 200, - }); -}); - -export default router; diff --git a/backend/system/system.routes.ts b/backend/system/system.routes.ts index 019265a..bce1576 100644 --- a/backend/system/system.routes.ts +++ b/backend/system/system.routes.ts @@ -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); diff --git a/backend/utils/generateScannerPin.utils.ts b/backend/utils/generateScannerPin.utils.ts new file mode 100644 index 0000000..fe8a19b --- /dev/null +++ b/backend/utils/generateScannerPin.utils.ts @@ -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: "", + }); +} diff --git a/frontend/src/components/Sidebar/AdminBar.tsx b/frontend/src/components/Sidebar/AdminBar.tsx index 708b4f7..45a9eac 100644 --- a/frontend/src/components/Sidebar/AdminBar.tsx +++ b/frontend/src/components/Sidebar/AdminBar.tsx @@ -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 ( diff --git a/frontend/src/components/Sidebar/DocBar.tsx b/frontend/src/components/Sidebar/DocBar.tsx index 76cb473..ab0ab6b 100644 --- a/frontend/src/components/Sidebar/DocBar.tsx +++ b/frontend/src/components/Sidebar/DocBar.tsx @@ -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(); diff --git a/frontend/src/components/Sidebar/MobileBar.tsx b/frontend/src/components/Sidebar/MobileBar.tsx new file mode 100644 index 0000000..74064d9 --- /dev/null +++ b/frontend/src/components/Sidebar/MobileBar.tsx @@ -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 ( + + Mobile + + + {items.map((item) => ( + + + setOpen(false)}> + + {item.title} + + + + ))} + + + + ); +} diff --git a/frontend/src/components/Sidebar/sidebar.tsx b/frontend/src/components/Sidebar/sidebar.tsx index 7c07772..f9a07b3 100644 --- a/frontend/src/components/Sidebar/sidebar.tsx +++ b/frontend/src/components/Sidebar/sidebar.tsx @@ -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() { - + + {session && (session.user.role === "admin" || session.user.role === "systemAdmin") && ( diff --git a/frontend/src/docs/notifications/updateInstructions.tsx b/frontend/src/docs/notifications/updateInstructions.tsx new file mode 100644 index 0000000..e972a5a --- /dev/null +++ b/frontend/src/docs/notifications/updateInstructions.tsx @@ -0,0 +1,3 @@ +export default function updateInstructions() { + return
updateInstructions
; +} diff --git a/frontend/src/lib/tableStuff/LstTable.tsx b/frontend/src/lib/tableStuff/LstTable.tsx index d98b050..e7d27b6 100644 --- a/frontend/src/lib/tableStuff/LstTable.tsx +++ b/frontend/src/lib/tableStuff/LstTable.tsx @@ -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([]); const [columnFilters, setColumnFilters] = useState([]); + 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 }) => , //getRowCanExpand: () => true, + // columnResizeMode: "onChange", filterFns: {}, state: { sorting, + pagination, columnFilters, }, }); return (
- +
{/* TODO: Add table header in here like title */}
+ {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( - + {header.isPlaceholder ? null : flexRender( @@ -76,6 +102,7 @@ export default function LstTable({ ))} + {table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( @@ -107,14 +134,23 @@ export default function LstTable({ +
+ + +
); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index c6a6131..acf76fe 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -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, diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 62d8367..db04b8e 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -3,30 +3,36 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { Toaster } from "sonner"; import Header from "@/components/Header"; import { AppSidebar } from "@/components/Sidebar/sidebar"; -import { SidebarProvider } from "@/components/ui/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 ( +
+ + +
-
- +
+ -
-
- -
-
-
+
+
+ +
+
+
- - - - -
-); + + + + {session && session.user.role === "systemAdmin" && ( + + )} +
+ ); +}; export const Route = createRootRoute({ component: RootLayout }); diff --git a/frontend/src/routes/admin/scanUsers.tsx b/frontend/src/routes/admin/scanUsers.tsx new file mode 100644 index 0000000..e74fba9 --- /dev/null +++ b/frontend/src/routes/admin/scanUsers.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/admin/scanUsers')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/admin/scanUsers"!
+} diff --git a/frontend/src/routes/admin/servers.tsx b/frontend/src/routes/admin/servers.tsx index f017a53..413922b 100644 --- a/frontend/src/routes/admin/servers.tsx +++ b/frontend/src/routes/admin/servers.tsx @@ -155,7 +155,7 @@ const ServerTable = () => { ); } - return ; + return ; }; function RouteComponent() { diff --git a/frontend/src/routes/docs/index.tsx b/frontend/src/routes/docs/index.tsx index 9fda92d..2fb4ea7 100644 --- a/frontend/src/routes/docs/index.tsx +++ b/frontend/src/routes/docs/index.tsx @@ -59,6 +59,33 @@ function RouteComponent() { Only shows machines that are attached to the silo. + {/* Mobile stuff */} +
  • Mobile App
  • +
      +
    • Rewrite of Alpla scan
    • +
        +
      • All old settings same as before id, ip, port
      • +
      • Currently scanned pallets will show now as well
      • +
      +
    • + Custom addition - login and more features NOTE: This is activated + based on how you enter the settings +
    • +
        +
      • Pin numbers login
      • +
      • + Scan a lane barcode and it returns whats in the lane and its + current status +
      • +
      • Command restrictions per pin login
      • +
      • Dock Door scanning
      • +
      • + More details on the pallet that is scanned by touching the running + number on the scanner. +
      • +
      +
    + {/* TMS integration */}
  • TMS integration
    • integration with TI to auto add in orders
    • diff --git a/lstMobile/app.json b/lstMobile/app.json index d8cadb8..ef960fa 100644 --- a/lstMobile/app.json +++ b/lstMobile/app.json @@ -15,7 +15,7 @@ "foregroundImage": "./assets/adaptive-icon-white.png", "backgroundColor": "#ffffff" }, - "versionCode": 21, + "versionCode": 23, "minSupportedVersionCode": 21, "predictiveBackGestureEnabled": false, "package": "net.alpla.lst.mobile" @@ -26,7 +26,7 @@ "bundler": "metro" }, "plugins": [ - "./plugins/withZebraScanner", + "./plugins/withZebraDataWedge", "expo-router", [ "expo-splash-screen", diff --git a/lstMobile/package.json b/lstMobile/package.json index aa7bf98..0b2bc13 100644 --- a/lstMobile/package.json +++ b/lstMobile/package.json @@ -9,7 +9,7 @@ "ios": "expo run:ios", "web": "expo start --web", "lint": "expo lint", - "build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease && npm run copy: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", diff --git a/lstMobile/plugins/withZebraScanner.js b/lstMobile/plugins/withZebraDataWedge.js similarity index 92% rename from lstMobile/plugins/withZebraScanner.js rename to lstMobile/plugins/withZebraDataWedge.js index a0ed266..ddfe141 100644 --- a/lstMobile/plugins/withZebraScanner.js +++ b/lstMobile/plugins/withZebraDataWedge.js @@ -145,34 +145,32 @@ class ZebraScannerModule( Thread.sleep(500) - val barcodeConfig = Bundle().apply { - putString("PLUGIN_NAME", "BARCODE") - putString("RESET_CONFIG", "true") + val barcodeConfig = Bundle().apply { + putString("PLUGIN_NAME", "BARCODE") + putString("RESET_CONFIG", "true") - val isLegacyTc8000 = - android.os.Build.MODEL.contains("TC8000", ignoreCase = true) + val props = Bundle().apply { + putString("scanner_input_enabled", "true") - val props = Bundle().apply { - putString("scanner_input_enabled", "true") - - // Baseline that should be safe on old and new Zebra devices - putString("scanner_selection", "auto") - - if (!isLegacyTc8000) { - // Newer Zebra devices + // 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") // HARD trigger + 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") - } - } - putBundle("PARAM_LIST", props) - } + // add in wake on trigger + putString("trigger_wakeup_scan", "true"); + } + + putBundle("PARAM_LIST", props) + } val intentConfig = Bundle().apply { putString("PLUGIN_NAME", "INTENT") diff --git a/lstMobile/src/app/(tabs)/logs.tsx b/lstMobile/src/app/(tabs)/logs.tsx index b41d477..147a2fe 100644 --- a/lstMobile/src/app/(tabs)/logs.tsx +++ b/lstMobile/src/app/(tabs)/logs.tsx @@ -1,13 +1,26 @@ -import React from 'react' -import { Text, View } from 'react-native' +import React from "react"; +import { Text, View } from "react-native"; +import { Button } from "../../components/ui/button"; export default function Logs() { - return ( - { + const info = "ho"; + + console.log(info); + }; + return ( + Logs - ) + }} + > + Logs + + + ); } diff --git a/lstMobile/src/app/(tabs)/scanner.tsx b/lstMobile/src/app/(tabs)/scanner.tsx index 8eb0db4..8d6989b 100644 --- a/lstMobile/src/app/(tabs)/scanner.tsx +++ b/lstMobile/src/app/(tabs)/scanner.tsx @@ -1,11 +1,9 @@ -import React from "react"; -import { View } from "react-native"; - -import { useAppStore } from "../../hooks/useAppStore"; -import ProdScanner from "../../components/ProdScanner"; +import { View } from "react-native"; import LSTScanner from "../../components/LSTScanner"; +import ProdScanner from "../../components/ProdScanner"; +import { useAppStore } from "../../hooks/useAppStore"; -export default function scanner() { +export default function Scanner() { const serverPort = useAppStore((s) => s.serverPort); return ( - {parseInt(serverPort || "0", 10) >= 50000 ? : } + {parseInt(serverPort || "0", 10) >= 50000 ? ( + + ) : ( + + )} ); } diff --git a/lstMobile/src/app/index.tsx b/lstMobile/src/app/index.tsx index 2ecead3..8bd0236 100644 --- a/lstMobile/src/app/index.tsx +++ b/lstMobile/src/app/index.tsx @@ -4,6 +4,7 @@ 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"; @@ -12,6 +13,7 @@ export default function Index() { const [message, setMessage] = useState(Starting app...); 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); @@ -86,7 +88,7 @@ export default function Index() { // TODO if theres an update go to update screen message :D setMessage(Opening LST scan app); await devDelay(3250); - //router.replace("/scanner"); + setReady(true); } catch (error) { console.log("Startup error", error); @@ -104,6 +106,9 @@ export default function Index() { setServerVersion, ]); + // if (ready && !isUnlocked) { + // return ; + // } if (ready) { return ; } diff --git a/lstMobile/src/app/login.tsx b/lstMobile/src/app/login.tsx new file mode 100644 index 0000000..18f3acc --- /dev/null +++ b/lstMobile/src/app/login.tsx @@ -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 ( + + + + LST Scanner Login + + + + + +