27 Commits

Author SHA1 Message Date
67f36c5499 chore(release): 0.1.0-alpha.0
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m55s
Release and Build Image / release (push) Successful in 9s
2026-05-13 20:56:14 -05:00
ebf1060475 refactor(servers): server name now links to the actual server:port
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-05-13 20:53:27 -05:00
c64392f457 refactor(api): changes to call a helper api to quit and redirect if needed 2026-05-13 20:52:55 -05:00
e9e73c829c ci(servives): helpers moved around 2026-05-13 20:52:16 -05:00
bcb7773007 ci(updateserver): changes to actually add the new env stuff 2026-05-13 20:51:54 -05:00
eb950d2c29 refactor(scanner): more scanner admin stuff 2026-05-13 20:51:22 -05:00
2616acf106 fix(notification subs): made it so only acitve show
closes #14
2026-05-13 20:50:51 -05:00
30ff7b71d9 refactor(app): changed ways we get data so we can have better reasons why app no worky 2026-05-13 20:49:43 -05:00
e7af3d1182 refactor(scanner): removed 69 as an option lol 2026-05-13 20:48:43 -05:00
3e66c3920d feat(notification): migrated sql cleanup 2026-05-13 20:48:22 -05:00
eb9d77c3d4 docs(scanner): added in westbend and dayton commands to scan for updates 2026-05-13 20:47:38 -05:00
342a97f6b1 refactor(users): some user refactoring and configuring 2026-05-13 16:42:36 -05:00
b0c7277a6c docs(scanner): added in instructions on how to update the scanner
only test2 stage now commands for now.
2026-05-12 20:26:19 -05:00
dc95e50a84 ci(mobile): added in ehs config to make it more easy for users to update the scanner app on the fly 2026-05-12 12:04:19 -05:00
d2a9e1d110 fix(app): required auth was in wrong spot caused entire app to want you logged in 2026-05-12 12:02:59 -05:00
a9c69250bd refactor(mobile): scanner response
ref #16
2026-05-12 08:53:12 -05:00
d61be61f44 refactor(scanner): logging - version of app 2026-05-11 19:06:25 -05:00
f5bae2c0c2 fix(anaylistics): changes to the daily section so it populates correctly now
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m9s
2026-05-11 15:41:20 -05:00
05758791be fix(scanner): fixes to be more clear that you need to scan a command to start
closes #16
2026-05-11 15:40:49 -05:00
51026e3e2c ci(notification): removal of more console logs that shouldnt be here 2026-05-11 15:38:44 -05:00
9631736e26 chore(mobile): removed console log that shouldnt be there 2026-05-11 15:38:04 -05:00
ce9d8eaaf5 feat(scan users): added in the place to add the new scanner users in 2026-05-11 15:37:38 -05:00
1bbf5c2a49 fix(table): skelly table causing hydration error 2026-05-11 15:35:46 -05:00
13718fe702 fix(anaylitics): unique values were missing causing a weird crash 2026-05-11 14:00:54 -05:00
0de2579942 fix(scanner): changed to not crash on logging
cloases #19
2026-05-11 13:35:07 -05:00
7c31b43a4a fix(app): emit.maxlistener issue
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m27s
BREAKING CHANGE: moved teh middleware to call the api hits to the main app and removed from
everywhere else

closes #18
2026-05-11 13:25:43 -05:00
85e96f5ed1 fix(scanner): logut out corrections
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
refs #17
2026-05-11 07:59:17 -05:00
102 changed files with 7052 additions and 361 deletions

3
.gitignore vendored
View File

@@ -9,8 +9,9 @@ downloads
.scriptCreds
node-v24.14.0-x64.msi
postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.exe
VSCodeSetup-x64-1.120.0.exe
nssm.exe
frontend/.tanstack
# Logs
logs

View File

@@ -1,5 +1,62 @@
# All Changes to LST can be found below.
## [0.1.0-alpha.0](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.10...v0.1.0-alpha.0) (2026-05-14)
### ⚠ BREAKING CHANGES
* **app:** moved teh middleware to call the api hits to the main app and removed from
everywhere else
### 🌟 Enhancements
* **notification:** migrated sql cleanup ([3e66c39](https://git.tuffraid.net/cowch/lst_v3/commits/3e66c3920d65cee7a0a788f3910c1ddf09a07805))
* **scan users:** added in the place to add the new scanner users in ([ce9d8ea](https://git.tuffraid.net/cowch/lst_v3/commits/ce9d8eaaf5bcb8f53ea4bdc191347df8d589fdfa))
### 🐛 Bug fixes
* **anaylistics:** changes to the daily section so it populates correctly now ([f5bae2c](https://git.tuffraid.net/cowch/lst_v3/commits/f5bae2c0c24b85423c5c421164d94d58159ff70a))
* **anaylitics:** unique values were missing causing a weird crash ([13718fe](https://git.tuffraid.net/cowch/lst_v3/commits/13718fe70293c039bd1d9bf8cf395852e6ea6c21))
* **app:** emit.maxlistener issue ([7c31b43](https://git.tuffraid.net/cowch/lst_v3/commits/7c31b43a4a313237fa63c0c9bbc3690b74f63a6f)), closes [#18](https://git.tuffraid.net/cowch/lst_v3/issues/18)
* **app:** required auth was in wrong spot caused entire app to want you logged in ([d2a9e1d](https://git.tuffraid.net/cowch/lst_v3/commits/d2a9e1d1107ea05f13725e9528bc6ab1566c8efb))
* **notification subs:** made it so only acitve show ([2616acf](https://git.tuffraid.net/cowch/lst_v3/commits/2616acf106530f5c5ee04d1b79033795cf06b42d)), closes [#14](https://git.tuffraid.net/cowch/lst_v3/issues/14)
* **scanner:** changed to not crash on logging ([0de2579](https://git.tuffraid.net/cowch/lst_v3/commits/0de25799420f38a293ee9acc70eb36e3287145c4)), closes [#19](https://git.tuffraid.net/cowch/lst_v3/issues/19)
* **scanner:** fixes to be more clear that you need to scan a command to start ([0575879](https://git.tuffraid.net/cowch/lst_v3/commits/05758791be7a50e90b5da05d4977e618c311f654)), closes [#16](https://git.tuffraid.net/cowch/lst_v3/issues/16)
* **scanner:** logut out corrections ([85e96f5](https://git.tuffraid.net/cowch/lst_v3/commits/85e96f5ed13a81fd466c6bbff31c539244750838)), closes [#17](https://git.tuffraid.net/cowch/lst_v3/issues/17)
* **table:** skelly table causing hydration error ([1bbf5c2](https://git.tuffraid.net/cowch/lst_v3/commits/1bbf5c2a4955107a36ace05595886d19cc8e64f4))
### 📝 Chore
* **mobile:** removed console log that shouldnt be there ([9631736](https://git.tuffraid.net/cowch/lst_v3/commits/9631736e263ed00189f8118f686690cab25f09d3))
### 📚 Documentation
* **scanner:** added in instructions on how to update the scanner ([b0c7277](https://git.tuffraid.net/cowch/lst_v3/commits/b0c7277a6cdb5becec3a994ea1d5cc2d7b0326aa))
* **scanner:** added in westbend and dayton commands to scan for updates ([eb9d77c](https://git.tuffraid.net/cowch/lst_v3/commits/eb9d77c3d4767fd961759662ef44c3e09e00946b))
### 🛠️ Code Refactor
* **api:** changes to call a helper api to quit and redirect if needed ([c64392f](https://git.tuffraid.net/cowch/lst_v3/commits/c64392f45769108aa4134c7fd865f3d4bc664179))
* **app:** changed ways we get data so we can have better reasons why app no worky ([30ff7b7](https://git.tuffraid.net/cowch/lst_v3/commits/30ff7b71d9d159ced263a5330d70d53b97393157))
* **mobile:** scanner response ([a9c6925](https://git.tuffraid.net/cowch/lst_v3/commits/a9c69250bd3272ad682751e41b671c119cb678f1)), closes [#16](https://git.tuffraid.net/cowch/lst_v3/issues/16)
* **scanner:** logging - version of app ([d61be61](https://git.tuffraid.net/cowch/lst_v3/commits/d61be61f4433a2be2678d724f4724301931614c9))
* **scanner:** more scanner admin stuff ([eb950d2](https://git.tuffraid.net/cowch/lst_v3/commits/eb950d2c29f692b806d5cc4ab7014bd59a726a8d))
* **scanner:** removed 69 as an option lol ([e7af3d1](https://git.tuffraid.net/cowch/lst_v3/commits/e7af3d11824b42915cf6789f9c508a727511d678))
* **servers:** server name now links to the actual server:port ([ebf1060](https://git.tuffraid.net/cowch/lst_v3/commits/ebf1060475d37627b371bc6c79507cdde411600b))
* **users:** some user refactoring and configuring ([342a97f](https://git.tuffraid.net/cowch/lst_v3/commits/342a97f6b1054443b9126186d2c7872fbd8586da))
### 📈 Project changes
* **mobile:** added in ehs config to make it more easy for users to update the scanner app on the fly ([dc95e50](https://git.tuffraid.net/cowch/lst_v3/commits/dc95e50a8412b4fbc629fd44fcb5c77295583ca8))
* **notification:** removal of more console logs that shouldnt be here ([51026e3](https://git.tuffraid.net/cowch/lst_v3/commits/51026e3e2cce4d7f696d26aae305b3fd221f5bb1))
* **servives:** helpers moved around ([e9e73c8](https://git.tuffraid.net/cowch/lst_v3/commits/e9e73c829c2e5726650c0ac7ffa6a9055dbc982b))
* **updateserver:** changes to actually add the new env stuff ([bcb7773](https://git.tuffraid.net/cowch/lst_v3/commits/bcb7773007894ac2f85fe2a0b47faf14c7b474ad))
## [0.0.2-alpha.10](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.9...v0.0.2-alpha.10) (2026-05-08)

View File

@@ -1,18 +1,16 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import build from "./admin.build.js";
import update from "./admin.updateServer.js";
import users from "./admin.users.js";
export const setupAdminRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/admin/build`, requireAuth, routeHitMiddleware, build);
app.use(
`${baseUrl}/api/admin/build`,
requireAuth,
routeHitMiddleware,
update,
);
app.use(`${baseUrl}/api/admin/build`, requireAuth, build);
app.use(`${baseUrl}/api/admin/build`, requireAuth, update);
app.use(`${baseUrl}/api/admin/user`, requireAuth, users);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,46 @@
/**
* To be able to run this we need to set our dev pc in the .env.
* if its empty just ignore it. this will just be the double catch
*/
import { fromNodeHeaders } from "better-auth/node";
import { Router } from "express";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { users } = await auth.api.listUsers({
query: {
limit: 50,
},
headers: fromNodeHeaders(req.headers),
});
// console.log(error);
// if (error) {
// return apiReturn(res, {
// success: false,
// level: "info",
// module: "admin",
// subModule: "user",
// message: `There was an error getting the users.`,
// data: users,
// status: 400,
// });
// }
return apiReturn(res, {
success: true,
level: "info",
module: "admin",
subModule: "users",
message: `Current active users.`,
data: users,
status: 200,
});
});
export default r;

View File

@@ -5,6 +5,7 @@ import express from "express";
import morgan from "morgan";
import { umamiConfig } from "./configs/umami.config.js";
import { createLogger } from "./logger/logger.controller.js";
import { routeHitMiddleware } from "./middleware/routeHit.middleware.js";
import { setupRoutes } from "./routeHandler.routes.js";
import { auth } from "./utils/auth.utils.js";
import { lstCors } from "./utils/cors.utils.js";
@@ -30,9 +31,9 @@ const createApp = async () => {
app.use(morgan("dev"));
app.set("trust proxy", true);
app.use(lstCors());
app.use(routeHitMiddleware);
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
app.use(express.json());
setupRoutes(baseUrl, app);
app.get(`${baseUrl}/api/lst-config.js`, (_, res) => {
res.type("application/javascript");
@@ -50,6 +51,8 @@ const createApp = async () => {
`);
});
setupRoutes(baseUrl, app);
app.use(
`${baseUrl}/app`,
express.static(join(__dirname, "../frontend/dist")),

View File

@@ -1,11 +1,11 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import login from "./login.route.js";
import register from "./register.route.js";
export const setupAuthRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/authentication/login`, login);
app.use(`${baseUrl}/api/authentication/register`, register);
};

View File

@@ -1,5 +1,5 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { datamartData } from "./datamartData.utlis.js";
import runQuery from "./getDatamart.route.js";
@@ -30,7 +30,7 @@ export const setupDatamartRoutes = (baseUrl: string, app: Express) => {
// });
//setup all the routes
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/datamart`, runQuery);
// just sending a get on datamart will return all the queries that we can call.

View File

@@ -4,30 +4,42 @@ import {
pgTable,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
export const analyticsDaily = pgTable("analytics_daily", {
id: uuid("id").defaultRandom().primaryKey(),
export const analyticsDaily = pgTable(
"analytics_daily",
{
id: uuid("id").defaultRandom().primaryKey(),
businessDate: date("business_date").notNull(),
businessDate: date("business_date", { mode: "string" }).notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),
module: text("module").notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),
module: text("module").notNull(),
totalHits: integer("total_hits").notNull(),
uniqueUsers: integer("unique_users").notNull(),
totalHits: integer("total_hits").notNull(),
uniqueUsers: integer("unique_users").notNull(),
successCount: integer("success_count").notNull(),
errorCount: integer("error_count").notNull(),
successCount: integer("success_count").notNull(),
errorCount: integer("error_count").notNull(),
avgDurationMs: integer("avg_duration_ms").notNull(),
maxDurationMs: integer("max_duration_ms").notNull(),
avgDurationMs: integer("avg_duration_ms").notNull(),
maxDurationMs: integer("max_duration_ms").notNull(),
firstHitAt: timestamp("first_hit_at").notNull(),
lastHitAt: timestamp("last_hit_at").notNull(),
firstHitAt: timestamp("first_hit_at").notNull(),
lastHitAt: timestamp("last_hit_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
},
(table) => [
unique("analytics_daily_business_route_unique").on(
table.businessDate,
table.method,
table.routePattern,
table.module,
),
],
);

View File

@@ -11,6 +11,7 @@ export const scanLog = pgTable("scan_log", {
commandDescription: text("command_description"),
runningNumber: text("running_number").default("0"),
status: text("status"),
scannerVersion: text("scanner_version").default("0"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_date").defaultNow(),
});

View File

@@ -1,6 +1,6 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./gpSqlRestart.route.js";
import start from "./gpSqlStart.route.js";
import stop from "./gpSqlStop.route.js";
@@ -8,12 +8,10 @@ export const setupGPSqlRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
// Apply auth to entire router
const router = Router();
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(start);
router.use(stop);
router.use(restart);
app.use(`${baseUrl}/api/system/gpSql`, router);
app.use(`${baseUrl}/api/system/gpSql`, requireAuth, router);
};

View File

@@ -0,0 +1,54 @@
import { eq } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanUser } from "../db/schema/scanUsers.js";
import { settings } from "../db/schema/settings.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
// scanners that are dedicated to specific users.
const SPECIAL_SCANNERS = [98];
const buildAllowedScannerIds = (scannerCount: number) => {
const generatedIds = Array.from({ length: scannerCount }, (_, i) => i + 1);
return Array.from(new Set([...generatedIds, ...SPECIAL_SCANNERS])).sort(
(a, b) => a - b,
);
};
r.get("/", async (_, res) => {
// get the scan users and setting
const scanusers = await db.select().from(scanUser);
const scannerIdSetting = await db
.select()
.from(settings)
.where(eq(settings.name, "scannerIds"));
const usedScannerIds = scanusers.map((x) => Number(x.scannerId));
const allowedScannerIds = buildAllowedScannerIds(
Number(scannerIdSetting[0]?.value ?? 0),
);
const availableScannerIds = allowedScannerIds.filter(
(id) => !usedScannerIds.includes(id),
);
const data = availableScannerIds.map((id) => ({
label: `${id}`,
value: id,
}));
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scanner",
message: `There are ${availableScannerIds.length} scanner id's`,
data,
status: 200,
});
});
export default r;

View File

@@ -38,7 +38,66 @@ router.get("/ehs", (_, res) => {
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader("Content-Disposition", `attachment; filename="EHS.apk}"`);
res.setHeader("Content-Disposition", `attachment; filename="EHS.apk"`);
return res.sendFile(apkPath);
});
router.get("/ehs/xml", (_, res) => {
const xmlPath = path.join(downloadDir, "enterprisehomescreen.xml");
if (!fs.existsSync(xmlPath)) {
return res.status(404).json({
success: false,
message: "EHS XML not found",
});
}
res.setHeader("Content-Type", "application/xml");
res.setHeader(
"Content-Disposition",
`attachment; filename="enterprisehomescreen.xml"`,
);
return res.sendFile(xmlPath);
});
router.get("/upgrade/android/13", (_, res) => {
const apkPath = path.join(
downloadDir,
"HE_FULL_UPDATE_13-51-16.00-TG-U00-STD-HEL-04.zip",
);
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-Type", "application/zip");
res.setHeader(
"Content-Disposition",
`attachment; filename="HE_FULL_UPDATE_13.zip"`,
);
return res.sendFile(apkPath);
});
router.get("/upgrade/android/14", (_, res) => {
const apkPath = path.join(
downloadDir,
"HE_FULL_UPDATE_14-38-04.00-UG-U15-STD-HEL-04.zip",
);
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-Type", "application/zip");
res.setHeader(
"Content-Disposition",
`attachment; filename="HE_FULL_UPDATE_14.zip"`,
);
return res.sendFile(apkPath);
});

View File

@@ -1,6 +1,12 @@
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { apiReturn, returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const router = Router();
@@ -9,7 +15,27 @@ router.post("/", async (req, res) => {
const lane = body.lane.split("#");
console.log(lane[2]);
// check if the plant has warehousing activated
const featureQ = sqlQuerySelector(`featureCheck`) as SqlQuery;
const { data: fd, error: fe } = await tryCatch(
prodQuery(featureQ.query, `Running feature check`),
);
if (fe) {
return returnFunc({
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: `feature check failed`,
data: fe as any,
notify: false,
});
}
console.log(fd);
const laneData = await runProdApi({
method: "post",
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",

View File

@@ -1,5 +1,6 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import available from "./availableScanIds.route.js";
import downloads from "./downloadApps.route.js";
import lanes from "./laneCheck.js";
import authPin from "./mobileAuth.route.js";
@@ -10,14 +11,13 @@ 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(routeHitMiddleware);
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);
app.use(`${baseUrl}/api/mobile/laneCheck`, lanes);
app.use(`${baseUrl}/api/mobile/version`, featureCheck("mobile"), version);
app.use(`${baseUrl}/api/mobile/apk`, featureCheck("mobile"), downloads);
app.use(`${baseUrl}/api/mobile/logs`, featureCheck("mobile"), logs);
app.use(`${baseUrl}/api/mobile/auth`, featureCheck("mobile"), authPin);
app.use(`${baseUrl}/api/mobile/pin`, featureCheck("mobile"), newPin);
app.use(`${baseUrl}/api/mobile/laneCheck`, featureCheck("mobile"), lanes);
app.use(`${baseUrl}/api/mobile/available`, featureCheck("mobile"), available);
// all other system should be under /api/system/*
};

View File

@@ -1,25 +1,34 @@
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js";
import { scanUser } from "../db/schema/scanUsers.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/", async (req, res) => {
const body = req.body;
try {
await db
.update(scanUser)
.set({ lastScan: sql`NOW()` })
.where(eq(scanUser.name, body.name));
} catch (error) {
console.log(error);
}
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,
user: body.user,
runningNumber: body.runningNumber,
scannerId: body.scannerId ?? "",
message: body.message ?? "",
prompt: body.prompt ?? "",
commandDescription: body.commandDescription ?? "",
status: body.status ?? "",
lines: body.lines ?? "",
user: body.user ?? "",
runningNumber: body.runningNumber ?? "",
scannerVersion: body.scannerVersion ?? "0",
})
.returning();

View File

@@ -0,0 +1,80 @@
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
// disable the jobs
const jobNames: string[] = [
"monitor_$_lots",
"monitor_$_lots_2",
"monitor$lots",
"Monitor_APO", //listen for people to cry this is no longer a thing
"Monitor_APO2",
"Monitor_AutoConsumeMaterials", // TODO: migrate to lst
"Monitor_AutoConsumeMaterials_iow1",
"Monitor_AutoConsumeMaterials_iow2",
"Monitor_BlockedINV_Loc",
"monitor_inv_cycle",
"monitor_inv_cycle_1",
"monitor_inv_cycle_2",
"monitor_edi_import", // TODO: migrate to lst -- for the query select count(*) from AlplaPROD_test3.dbo.T_EDIDokumente (nolock) where /* IdLieferant > 1 and */ add_date > DATEADD(MINUTE, -30, getdate())
"Monitor_Lot_Progression",
"Monitor_Lots", // TODO: migrate to lst -- this should be the one where we monitor the when a lot is assigned if its missing some data.
"Monitor_MinMax", // TODO:Migrate to lst
"Monitor_MinMax_iow2",
"Monitor_PM",
"Monitor_Purity",
"monitor_wastebookings", // TODO: Migrate
"LastPriceUpdate", // not even sure what this is
"GETLabelsCount", // seems like an old jc job
"jobforpuritycount", // was not even working correctly
"Monitor_EmptyAutoConsumLocations", // not sure who uses this one
"monitor_labelreprint", // Migrated but need to find out who really wants this
"test", // not even sure why this is active
"UpdateLastMoldUsed", // old jc inserts data into a table but not sure what its used for not linked to any other alert
"UpdateWhsePositions3", // old jc inserts data into a table but not sure what its used for not linked to any other alert
"UpdateWhsePositions4",
"delete_print", // i think this was in here for when we was having lag prints in iowa1
"INV_WHSE_1", // something random i wrote long time ago looks like an inv thing to see aged stuff
"INV_WHSE_2",
"laneAgeCheck", // another strange one thats been since moved to lst
"monitor_blocking_2",
"monitor_blocking", // already in lst
"monitor_min_inv", // do we still want this one? it has a description of: this checks m-f the min inventory of materials based on the min level set in stock
"Monitor_MixedLocations",
"Monitor_PM",
"Monitor_PM2",
"wrong_lots_1",
"wrong_lots_2",
"invenotry check", // spelling error one of my stupids
"monitor_hold_monitor",
"Monitor_Silo_adjustments",
"monitor_qualityLocMonitor", // validating with lima this is still needed
"Monitor_Stock_Change",
];
export const sqlJobCleanUp = async () => {
// running a query to disable jobs that are moved to lst to be better maintained
const sqlQuery = sqlQuerySelector("disableJob") as SqlQuery;
if (!sqlQuery.success) {
console.error("Failed to load the query: ", sqlQuery.message);
return;
}
for (const job of jobNames) {
const { error } = await tryCatch(
prodQuery(
sqlQuery.query.replace("[jobName]", `${job}`),
`Disabling job: ${job}`,
),
);
if (error) {
console.error(error);
}
//console.log(data);
}
};

View File

@@ -1,6 +1,6 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import manual from "./notification.manualTrigger.js";
import getNotifications from "./notification.route.js";
import updateNote from "./notification.update.route.js";
@@ -14,43 +14,43 @@ export const setupNotificationRoutes = (baseUrl: string, app: Express) => {
app.use(
`${baseUrl}/api/notification`,
requireAuth,
routeHitMiddleware,
getNotifications,
);
app.use(
`${baseUrl}/api/notification`,
requireAuth,
routeHitMiddleware,
updateNote,
);
app.use(
`${baseUrl}/api/notification/manual`,
requireAuth,
routeHitMiddleware,
manual,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
subs,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
newSub,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
updateSub,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
deleteSub,
);

View File

@@ -43,7 +43,7 @@ const parseZebraAlert = (body: any): PrinterEvent => {
};
};
r.post("/printer/listener/:printer", upload.any(), async (req, res) => {
r.post("/:printer", upload.any(), async (req, res) => {
const { printer: printerName } = req.params;
const event: PrinterEvent = parseZebraAlert(req.body);

View File

@@ -21,7 +21,7 @@ import { printerSync } from "./ocp.printer.manage.js";
const r = Router();
r.post("/printer/update", async (_, res) => {
r.post("/update", async (_, res) => {
printerSync();
return apiReturn(res, {
success: true,

View File

@@ -1,27 +1,16 @@
import { type Express, Router } from "express";
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import listener from "./ocp.printer.listener.js";
import update from "./ocp.printer.update.js";
export const setupOCPRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
const router = Router();
// is the feature even on?
router.use(featureCheck("ocp"));
// non auth routes up here
router.use(listener);
// auth routes below here
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(update);
//router.use("");
app.use(`${baseUrl}/api/ocp`, router);
app.use(`${baseUrl}/api/ocp/printer/listener`, featureCheck("ocp"), listener);
app.use(
`${baseUrl}/api/ocp/printer`,
featureCheck("ocp"),
requireAuth,
update,
);
};

View File

@@ -1,21 +1,16 @@
import { type Express, Router } from "express";
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getApt from "./opendockGetRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
// Apply auth to entire router
const router = Router();
// is the feature even on?
router.use(featureCheck("opendock_sync"));
// we need to make sure we are authenticated to see the releases
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(getApt);
app.use(`${baseUrl}/api/opendock`, router);
app.use(
`${baseUrl}/api/opendock`,
featureCheck("opendock_sync"),
requireAuth,
getApt,
);
};

View File

@@ -1,6 +1,6 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./prodSqlRestart.route.js";
import start from "./prodSqlStart.route.js";
import stop from "./prodSqlStop.route.js";
@@ -9,11 +9,8 @@ export const setupProdSqlRoutes = (baseUrl: string, app: Express) => {
// Apply auth to entire router
const router = Router();
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(start);
router.use(stop);
router.use(restart);
app.use(`${baseUrl}/api/system/prodSql`, router);
app.use(`${baseUrl}/api/system/prodSql/start`, requireAuth, start);
app.use(`${baseUrl}/api/system/prodSql/stop`, requireAuth, stop);
app.use(`${baseUrl}/api/system/prodSql/restart`, requireAuth, restart);
};

View File

@@ -4,7 +4,7 @@ import { closePool, connectProdSql } from "./prodSqlConnection.controller.js";
const r = Router();
r.post("/restart", async (_, res) => {
r.post("/", async (_, res) => {
await closePool();
await new Promise((r) => setTimeout(r, 2000));

View File

@@ -4,7 +4,7 @@ import { connectProdSql } from "./prodSqlConnection.controller.js";
const r = Router();
r.post("/start", async (_, res) => {
r.post("/", async (_, res) => {
const connect = await connectProdSql();
apiReturn(res, {
success: connect.success,

View File

@@ -4,7 +4,7 @@ import { closePool } from "./prodSqlConnection.controller.js";
const r = Router();
r.post("/stop", async (_, res) => {
r.post("/", async (_, res) => {
const connect = await closePool();
apiReturn(res, {
success: connect.success,

View File

@@ -0,0 +1,8 @@
/*
disables sql jobs.
*/
EXEC msdb.dbo.sp_update_job @job_name = N'[jobName]', @enabled = 0;
-- DECLARE @JobName varchar(max) = '[jobName]'
-- UPDATE msdb.dbo.sysjobs
-- SET enabled = 0
-- WHERE name = @JobName;

View File

@@ -16,6 +16,7 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
export const setupRoutes = (baseUrl: string, app: Express) => {
//routes that are on by default
setupMobileRoutes(baseUrl, app);
setupSystemRoutes(baseUrl, app);
setupAdminRoutes(baseUrl, app);
setupApiDocsRoutes(baseUrl, app);
@@ -28,5 +29,4 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app);
setupMobileRoutes(baseUrl, app);
};

View File

@@ -8,6 +8,7 @@ import { connectGPSql } from "./gpSql/gpSqlConnection.controller.js";
import { createLogger } from "./logger/logger.controller.js";
import { historicalSchedule } from "./logistics/logistics.historicalInv.js";
import { startNotifications } from "./notification/notification.controller.js";
import { sqlJobCleanUp } from "./notification/notification.SqlJobCleanUp.js";
import { createNotifications } from "./notification/notifications.master.js";
import { printerSync } from "./ocp/ocp.printer.manage.js";
import { monitorReleaseChanges } from "./opendock/openDockRreleaseMonitor.utils.js";
@@ -83,6 +84,9 @@ const start = async () => {
startNotifications();
serversChecks();
aggregateRouteHitsForBusinessDay();
// can be removed at a later date
sqlJobCleanUp();
}, 5 * 1000);
process.on("uncaughtException", async (err) => {

View File

@@ -346,6 +346,17 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
seedVersion: 1,
},
{
name: "scannerIds",
value: "10",
active: false,
description:
"How many scanners ids are setup for this, there should be a lst_scanner instance created.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
];
export const baseSettingValidationCheck = async () => {

View File

@@ -1,6 +1,6 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js";
@@ -8,7 +8,6 @@ import stats from "./stats.route.js";
export const setupSystemRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/stats`, stats);
app.use(`${baseUrl}/api/settings`, getSettings);
app.use(`${baseUrl}/api/servers`, getServers);

View File

@@ -1,6 +1,6 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./tcpRestart.route.js";
import start from "./tcpStart.route.js";
import stop from "./tcpStop.route.js";
@@ -8,12 +8,12 @@ import stop from "./tcpStop.route.js";
export const setupTCPRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/tcp/start`, requireAuth, routeHitMiddleware, start);
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, routeHitMiddleware, stop);
app.use(`${baseUrl}/api/tcp/start`, requireAuth, start);
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, stop);
app.use(
`${baseUrl}/api/tcp/restart`,
requireAuth,
routeHitMiddleware,
restart,
);

View File

@@ -61,7 +61,7 @@ export async function aggregateRouteHitsForBusinessDay() {
const rows = await db
.select({
businessDate: sql<string>`${businessDate}`,
businessDate: sql<string>`CAST(${businessDate} AS date)`,
method: analytics.method,
routePattern: analytics.routePattern,
module: sql<string>`COALESCE(${analytics.module}, 'unknown')`,
@@ -105,9 +105,16 @@ export async function aggregateRouteHitsForBusinessDay() {
};
}
const values = rows.map((row) => ({
...row,
businessDate: row.businessDate,
firstHitAt: new Date(row.firstHitAt),
lastHitAt: new Date(row.lastHitAt),
}));
await db
.insert(analyticsDaily)
.values(rows)
.values(values)
.onConflictDoUpdate({
target: [
analyticsDaily.businessDate,

View File

@@ -1,8 +1,9 @@
import { createAccessControl } from "better-auth/plugins/access";
import { adminAc } from "better-auth/plugins/admin/access";
export const statement = {
app: ["read", "create", "share", "update", "delete", "readAll"],
user: ["ban"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
} as const;
@@ -20,7 +21,8 @@ export const admin = ac.newRole({
export const systemAdmin = ac.newRole({
app: ["read", "create", "share", "update", "delete", "readAll"],
user: ["ban"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
...adminAc.statements,
});

View File

@@ -10,7 +10,7 @@ export async function generateUniquePin() {
const pin = generateSixDigitPin();
const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, pin), // ⚠️ we'll fix this below
where: (u, { eq }) => eq(u.pinHash, pin),
});
if (!existing)
@@ -37,3 +37,13 @@ export async function generateUniquePin() {
room: "",
});
}
// export const pinExists = async (pin: string | number) => {
// const existing = await db.query.scanUser.findFirst({
// where: (u, { eq }) => eq(u.pinHash, pin),
// });
// if (!existing) return true;
// return false;
// };

View File

@@ -1,9 +1,8 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getActiveJobs from "./cronerActiveJobs.route.js";
import jobStatusChange from "./cronerStatusChange.route.js";
export const setupUtilsRoutes = (baseUrl: string, app: Express) => {
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/utils/croner`, getActiveJobs);
app.use(`${baseUrl}/api/utils/croner`, jobStatusChange);
};

View File

@@ -7,6 +7,7 @@
<title>Logistics Support Tool</title>
</head>
<body>
<script>
const configScript = document.createElement("script");
configScript.src = `${window.location.origin}/lst/api/lst-config.js`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -19,11 +19,14 @@ export default function Header() {
const { data: session } = useSession();
const { signOut } = authClient;
const router = useRouterState();
const navigate = useNavigate();
const currentPath = router.location.href;
return (
<header className="sticky top-0 z-50 flex w-full items-center border-b bg-background">
<header
className={`sticky top-0 z-50 flex w-full items-center border-b ${session?.session.impersonatedBy ? "bg-amber-600" : "bg-background"} `}
>
<div className="flex justify-between w-full">
<div className="flex items-center gap-2 px-4">
<div className="flex flex-row">
@@ -48,6 +51,20 @@ export default function Header() {
<span className="font-semibold text-2xl">Logistics Support Tool</span>
</div>
<div className="m-1 flex gap-1">
<div>
{session?.session.impersonatedBy && (
<Button
onClick={async () => {
await authClient.admin.stopImpersonating();
await authClient.getSession();
window.location.assign("/lst/app/admin/users");
}}
>
Stop Impersonating
</Button>
)}
</div>
<div>
<ModeToggle />
</div>

View File

@@ -0,0 +1,51 @@
import { useRouter } from "@tanstack/react-router";
import { Card, CardContent, CardHeader } from "./ui/card";
export default function NotFound() {
const router = useRouter();
let url: string;
if (window.location.origin.includes("localhost")) {
url = `https://www.youtube.com/watch?v=dQw4w9WgXcQ`;
} else if (window.location.origin.includes("vms006")) {
url = `https://${window.location.hostname.replace("vms006", "prod.alpla.net/")}lst/app/old`;
} else {
url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
}
return (
<div className="flex items-center justify-center bg-background text-foreground">
<Card>
<CardHeader>
<p className="text-2xl">
Oops, Looks like you hit a link you shouldn't have
</p>
</CardHeader>
<CardContent>
<p className="mt-3 text-muted-foreground">
Your have tried to go to a page that you are not authorized to be
at.
</p>
<div className="flex justify-center">
<div>
<a href={`${url}`} target="_blank" rel="noopener">
<b>
<strong>OLD - LST Home</strong>
</b>
</a>
</div>
<div>
<button
type="button"
className="w-64"
onClick={() => router.navigate({ to: "/", replace: true })}
>
<strong>Home</strong>
</button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { Bell, Logs, Server, Settings, UsersRound } from "lucide-react";
import { getSettings } from "../../lib/queries/getSettings";
import {
SidebarGroup,
SidebarGroupContent,
@@ -23,6 +24,7 @@ import {
export default function AdminSidebar({ session }: any) {
const { setOpen } = useSidebar();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
const items = [
{
title: "Notifications",
@@ -68,9 +70,11 @@ export default function AdminSidebar({ session }: any) {
title: "Scan users",
url: "/admin/scanUsers",
icon: UsersRound,
role: ["systemAdmin", "admin"],
role: ["systemAdmin", "admin", "manager"],
module: "admin",
active: true,
active:
!isLoading &&
settings.filter((n: any) => n.name === "mobile")[0].active,
},
];
return (
@@ -79,9 +83,9 @@ export default function AdminSidebar({ session }: any) {
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<>
{item.role.includes(session.user.role) && (
<SidebarMenuItem key={item.title}>
<div key={item.title}>
{item.role.includes(session.user.role) && item.active && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
@@ -90,7 +94,7 @@ export default function AdminSidebar({ session }: any) {
</SidebarMenuButton>
</SidebarMenuItem>
)}
</>
</div>
))}
</SidebarMenu>
</SidebarGroupContent>

View File

@@ -1,11 +1,12 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { Link, useRouterState } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
import { ChevronRight, ScrollText } from "lucide-react";
import { getSettings } from "../../lib/queries/getSettings";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
import {
SidebarGroup,
SidebarGroupContent,
@@ -19,43 +20,55 @@ import {
useSidebar,
} from "../ui/sidebar";
const docs = [
{
title: "Notifications",
url: "/intro",
//icon,
isActive: window.location.pathname.includes("notifications") ?? false,
items: [
{
title: "Reprints",
url: "/reprints",
},
{
title: "New Blocking order",
url: "/qualityBlocking",
},
],
},
{
title: "Mobile",
url: "/updateInstructions",
isActive: false,
items: [
{
title: "Settings",
url: "/mobile-settings",
},
],
},
];
export default function DocBar() {
const { setOpen } = useSidebar();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
const pathname = useRouterState({
select: (s) => s.location.pathname,
});
const isNotifications = pathname.includes("notifications");
const docs = [
{
title: "Notifications",
url: "notifications/intro",
//icon,
isActive: true,
items: [
{
title: "Reprints",
icon: ScrollText,
url: "notifications/reprints",
},
{
title: "New Blocking order",
icon: ScrollText,
url: "notifications/qualityBlocking",
},
],
},
{
title: "Mobile",
url: "mobile/updateInstructions",
isActive:
!isLoading &&
settings.filter((n: any) => n.name === "mobile")[0].active,
items: [
{
title: "Update Instructions",
icon: ScrollText,
url: "mobile/updateInstructions",
},
// {
// title: "Settings",
// icon: ScrollText,
// url: "mobile/mobile-settings",
// },
],
},
];
return (
<SidebarGroup>
<SidebarGroupLabel>Docs</SidebarGroupLabel>
@@ -72,42 +85,44 @@ export default function DocBar() {
</SidebarMenu>
<SidebarMenu>
{docs.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={isNotifications}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
<Link
to={"/docs/$"}
params={{ _splat: `notifications${item.url}` }}
>
{item.title}
</Link>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<Link
to={"/docs/$"}
params={{ _splat: `notifications${subItem.url}` }}
>
{subItem.title}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<div key={item.title}>
{item.isActive && (
<Collapsible
asChild
defaultOpen={isNotifications}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
<Link to={"/docs/$"} params={{ _splat: `${item.url}` }}>
{item.title}
</Link>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<Link
to={"/docs/$"}
params={{ _splat: `${subItem.url}` }}
onClick={() => setOpen(false)}
>
<subItem.icon />
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)}
</div>
))}
</SidebarMenu>
</SidebarGroupContent>

View File

@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router";
import { ScanText, ScrollText } from "lucide-react";
import { ScanText } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
@@ -10,14 +10,14 @@ import {
useSidebar,
} from "../ui/sidebar";
export default function MobileBar({ session }: any) {
export default function MobileBar() {
const { setOpen } = useSidebar();
const items = [
{
title: "Update Instructions",
url: "/",
icon: ScrollText,
},
// {
// title: "Update Instructions",
// url: "/",
// icon: ScrollText,
// },
{
title: "Scan Log",
url: "/",
@@ -25,8 +25,6 @@ export default function MobileBar({ session }: any) {
},
];
console.log(session);
return (
<SidebarGroup>
<SidebarGroupLabel>Mobile</SidebarGroupLabel>

View File

@@ -1,3 +1,4 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import {
Sidebar,
SidebarContent,
@@ -6,12 +7,14 @@ import {
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useSession } from "@/lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings";
import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar";
import MobileBar from "./MobileBar";
export function AppSidebar() {
const { data: session } = useSession();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
return (
<Sidebar
@@ -24,10 +27,15 @@ export function AppSidebar() {
<SidebarMenuItem>
<SidebarContent>
<DocBar />
<MobileBar session={session} />
{!isLoading &&
settings.filter((n: any) => n.name === "mobile")[0].active && (
<MobileBar />
)}
{session &&
(session.user.role === "admin" ||
session.user.role === "systemAdmin") && (
session.user.role === "systemAdmin" ||
session.user.role === "manager") && (
<AdminSidebar session={session} />
)}
</SidebarContent>

View File

@@ -1,64 +1,67 @@
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";
import type * as React from "react";
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button";
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants };
export { Button, buttonVariants }

View File

@@ -0,0 +1,166 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,137 @@
import { useMutation } from "@tanstack/react-query";
import { Button } from "../../components/ui/button";
import { Separator } from "../../components/ui/separator";
export default function UpdateInstructions() {
const getFile = useMutation({
mutationFn: async () => {
// 1. Fetch the file from the public folder
const response = await fetch(
`/lst/app/stage-now/${window.LST_CONFIG?.server}-stageNow.pdf`,
);
if (!response.ok) throw new Error("Network response was not ok");
// 2. Convert to blob
return await response.blob();
},
onSuccess: (blob) => {
// 3. Create a temporary anchor element to trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${window.LST_CONFIG?.server}-stageNow.pdf`; // Desired filename
document.body.appendChild(a);
a.click();
// 4. Cleanup
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
},
});
return (
<div className="flex flex-row gap-2">
<div className="w-1/2">
<div className="flex flex-col gap-1 justify-center">
<div>
<p className="text-center text-3xl">
Updating the lst mobile scanner app
</p>
<p className="text-center text-sm">
NOTE: LST Mobile only works on TC8300
</p>
</div>
<div className="flex justify-center">
<Button
onClick={() => getFile.mutate()}
disabled={getFile.isPending}
>
{getFile.isPending ? "Downloading..." : "Get StageNow Codes"}
</Button>
</div>
</div>
<Separator className="m-3" />
<div>
<p className="text-2xl text-center">
How to know the scanner has an update?
</p>
<p>
The bottom part of the scanner will show a red or orange bar
indicating there is an update. As shown below
</p>
<div className="flex flex-row gap-2 justify-center">
<div className="w-1/2">
<img
src="/lst/app/imgs/docs/mobile/critical_update.png"
alt="Home"
className="max-w-[50%] h-auto"
/>
</div>
<div className="w-1/2">
<img
src="/lst/app/imgs/docs/mobile/update.png"
alt="Home"
className="max-w-[50%] h-auto"
/>
</div>
</div>
</div>
<Separator className="m-3" />
<div>
<p className="text-2xl text-center">
To update the scanner follow the below steps.
</p>
<p>Step 1) Tap the 3 dots top right of the home screen</p>
<img
src="/lst/app/imgs/docs/mobile/ehs_homeScreen.png"
alt="Home"
className="max-w-[25%] h-auto m-3"
/>
<p>Step 2) Tap tools</p>
<img
src="/lst/app/imgs/docs/mobile/ehs_menu.png"
alt="Home"
className="max-w-[25%] h-auto m-3"
/>
<p>Step 3) Tap Stage Now</p>
<img
src="/lst/app/imgs/docs/mobile/tools.png"
alt="Home"
className="max-w-[25%] h-auto m-3"
/>
<p>
Step 4) Scan the 3 barcode's to the right or from the printed sheet
</p>
<img
src="/lst/app/imgs/docs/mobile/stagenow.png"
alt="Home"
className="max-w-[25%] h-auto m-3"
/>
</div>
</div>
<div className="w-1/2">
<p>Scan Commands</p>
<Separator className="m-3" />
<div className="flex flex-col justify-center">
<img
src={`/lst/app/imgs/docs/mobile/${window.LST_CONFIG?.server}-1.png`}
alt="Home"
className="m-3"
/>
<img
src={`/lst/app/imgs/docs/mobile/${window.LST_CONFIG?.server}-2.png`}
alt="Home"
className="m-3"
/>
<img
src={`/lst/app/imgs/docs/mobile/${window.LST_CONFIG?.server}-3.png`}
alt="Home"
className="m-3"
/>
</div>
</div>
</div>
);
}

View File

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

View File

@@ -0,0 +1,40 @@
import type { Router } from "@tanstack/react-router";
import axios from "axios";
import { toast } from "sonner";
let appRouter: Router<any, any> | null = null;
export function setApiRouter(router: Router<any, any>) {
appRouter = router;
}
export const api = axios.create({
baseURL: "/lst/api",
withCredentials: true,
timeout: 15000,
});
api.interceptors.response.use(
(response) => response,
(error) => {
const isNetworkError =
error.code === "ERR_NETWORK" ||
error.code === "ECONNABORTED" ||
error.message === "Network Error" ||
error.message === "Failed to fetch" ||
!error.response;
if (error.response?.status === 403) {
// redirect, toast, or show forbidden page
toast.error("Unauthorized to be here");
appRouter?.navigate({ to: "/forbidden", replace: true });
}
if (isNetworkError) {
appRouter?.navigate({ to: "/app-down", replace: true });
}
return Promise.reject(error);
},
);

View File

@@ -1,6 +1,7 @@
import { redirect } from "@tanstack/react-router";
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import { ac, admin, systemAdmin, user } from "./auth-permissions";
import { ac, admin, manager, systemAdmin, user } from "./auth-permissions";
export const authClient = createAuthClient({
baseURL: `${window.location.origin}/lst/api/auth`,
@@ -10,11 +11,20 @@ export const authClient = createAuthClient({
roles: {
admin,
user,
manager,
systemAdmin,
},
}),
genericOAuthClient(),
],
fetchOptions: {
onError() {
redirect({
to: "/app-down",
replace: true,
});
},
},
});
export const { useSession, signUp, signIn, signOut } = authClient;

View File

@@ -1,21 +1,53 @@
import { createAccessControl } from "better-auth/plugins/access";
import { adminAc } from "better-auth/plugins/admin/access";
export const statement = {
project: ["create", "share", "update", "delete"],
user: ["ban"],
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
} as const;
export const ac = createAccessControl(statement);
export const user = ac.newRole({
project: ["create"],
app: ["read", "create"],
notifications: ["read", "create"],
});
export const manager = ac.newRole({
app: ["read", "create", "update"],
});
export const admin = ac.newRole({
project: ["create", "update"],
app: ["read", "create", "update"],
});
export const systemAdmin = ac.newRole({
project: ["create", "update", "delete"],
user: ["ban"],
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
...adminAc.statements,
});
/* example usage
const canCreateProject = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
},
});
// You can also check multiple resource permissions at the same time
const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
sale: ["create"]
},
});
*/

View File

@@ -13,9 +13,7 @@ const docsMap: Record<string, ComponentType> = {};
for (const path in modules) {
const mod = modules[path] as DocModule;
const slug = path
.replace("../docs/", "")
.replace(".tsx", "");
const slug = path.replace("../docs/", "").replace(".tsx", "");
// "notifications/intro"
docsMap[slug] = mod.default;

View File

@@ -1,25 +1,25 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function getScanUsers() {
return queryOptions({
queryKey: ["getScanUsers"],
queryFn: () => fetch(),
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
const dataFetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get("/lst/api/mobile/auth/user", {
withCredentials: true,
timeout: 5000,
});
const { data } = await api.get("/mobile/auth/user");
if (!data.success) {
throw new Error(data.message ?? "Failed to load scan users");
}
return data.data;
return data.data ?? [];
};

View File

@@ -0,0 +1,23 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getScannerIds() {
return queryOptions({
queryKey: ["getScannerIds"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await api.get("/mobile/available");
return data.data;
};

View File

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function getSettings() {
return queryOptions({
@@ -16,7 +17,7 @@ const fetch = async () => {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get("/lst/api/settings");
const { data } = await api.get("/settings");
return data.data;
};

View File

@@ -0,0 +1,40 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { authClient } from "../auth-client";
export function getUsers() {
return queryOptions({
queryKey: ["getUsers"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data, error } = await authClient.admin.listUsers({
query: {
// searchValue: "some name",
// searchField: "name",
// searchOperator: "contains",
limit: 100,
offset: 0,
sortBy: "name",
// sortDirection: "desc",
// filterField: "email",
// filterValue: "hello@example.com",
// filterOperator: "eq",
},
});
if (error) {
return error;
}
return data.users;
};

View File

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function notificationSubs(userId?: string) {
return queryOptions({
@@ -16,8 +17,8 @@ const fetch = async (userId?: string) => {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get(
`/lst/api/notification/sub${userId ? `?userId=${userId}` : ""}`,
const { data } = await api.get(
`/notification/sub${userId ? `?userId=${userId}` : ""}`,
);
return data.data;

View File

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function notifications() {
return queryOptions({
@@ -16,7 +17,7 @@ const fetch = async () => {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get("/lst/api/notification");
const { data } = await api.get("/notification");
return data.data;
};

View File

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function servers() {
return queryOptions({
@@ -16,7 +17,7 @@ const fetch = async () => {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get("/lst/api/servers");
const { data } = await api.get("/servers");
return data.data;
};

View File

@@ -17,11 +17,13 @@ export default function SkellyTable({ rows = 5, columns = 4 }: TableSkelly) {
<div className="rounded-md border">
<Table>
<TableHeader>
{Array.from({ length: columns }).map((_, i) => (
<TableHead key={i}>
<Skeleton className="h-4 w-[80px]" />
</TableHead>
))}
<TableRow>
{Array.from({ length: columns }).map((_, i) => (
<TableHead key={i}>
<Skeleton className="h-4 w-[80px]" />
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: rows }).map((_, r) => (

View File

@@ -3,6 +3,8 @@ import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import NotFound from "./components/NotFound";
import { setApiRouter } from "./lib/apiHelper";
import socket from "./lib/socket.io";
import { loadUmami } from "./lib/umami.utils";
// Import the generated route tree
@@ -13,8 +15,9 @@ const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: 0,
retry: 2,
refetchOnWindowFocus: true,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 5000),
},
},
});
@@ -27,8 +30,11 @@ const router = createRouter({
context: {
queryClient,
},
defaultNotFoundComponent: NotFound,
});
setApiRouter(router);
// Register the router instance for type safety
declare module "@tanstack/react-router" {
interface Register {

View File

@@ -9,10 +9,13 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as ForbiddenRouteImport } from './routes/forbidden'
import { Route as AppDownRouteImport } from './routes/app-down'
import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminUsersRouteImport } from './routes/admin/users'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminServersRouteImport } from './routes/admin/servers'
import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
@@ -23,6 +26,16 @@ import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile'
const ForbiddenRoute = ForbiddenRouteImport.update({
id: '/forbidden',
path: '/forbidden',
getParentRoute: () => rootRouteImport,
} as any)
const AppDownRoute = AppDownRouteImport.update({
id: '/app-down',
path: '/app-down',
getParentRoute: () => rootRouteImport,
} as any)
const AboutRoute = AboutRouteImport.update({
id: '/about',
path: '/about',
@@ -43,6 +56,11 @@ const DocsSplatRoute = DocsSplatRouteImport.update({
path: '/docs/$',
getParentRoute: () => rootRouteImport,
} as any)
const AdminUsersRoute = AdminUsersRouteImport.update({
id: '/admin/users',
path: '/admin/users',
getParentRoute: () => rootRouteImport,
} as any)
const AdminSettingsRoute = AdminSettingsRouteImport.update({
id: '/admin/settings',
path: '/admin/settings',
@@ -92,12 +110,15 @@ const authUserProfileRoute = authUserProfileRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute
@@ -107,12 +128,15 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute
@@ -123,12 +147,15 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/(auth)/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute
'/(auth)/user/profile': typeof authUserProfileRoute
@@ -140,12 +167,15 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/about'
| '/app-down'
| '/forbidden'
| '/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/admin/users'
| '/docs/$'
| '/docs/'
| '/user/profile'
@@ -155,12 +185,15 @@ export interface FileRouteTypes {
to:
| '/'
| '/about'
| '/app-down'
| '/forbidden'
| '/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/admin/users'
| '/docs/$'
| '/docs'
| '/user/profile'
@@ -170,12 +203,15 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/about'
| '/app-down'
| '/forbidden'
| '/(auth)/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/admin/users'
| '/docs/$'
| '/docs/'
| '/(auth)/user/profile'
@@ -186,12 +222,15 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
AppDownRoute: typeof AppDownRoute
ForbiddenRoute: typeof ForbiddenRoute
authLoginRoute: typeof authLoginRoute
AdminLogsRoute: typeof AdminLogsRoute
AdminNotificationsRoute: typeof AdminNotificationsRoute
AdminScanUsersRoute: typeof AdminScanUsersRoute
AdminServersRoute: typeof AdminServersRoute
AdminSettingsRoute: typeof AdminSettingsRoute
AdminUsersRoute: typeof AdminUsersRoute
DocsSplatRoute: typeof DocsSplatRoute
DocsIndexRoute: typeof DocsIndexRoute
authUserProfileRoute: typeof authUserProfileRoute
@@ -201,6 +240,20 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/forbidden': {
id: '/forbidden'
path: '/forbidden'
fullPath: '/forbidden'
preLoaderRoute: typeof ForbiddenRouteImport
parentRoute: typeof rootRouteImport
}
'/app-down': {
id: '/app-down'
path: '/app-down'
fullPath: '/app-down'
preLoaderRoute: typeof AppDownRouteImport
parentRoute: typeof rootRouteImport
}
'/about': {
id: '/about'
path: '/about'
@@ -229,6 +282,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/users': {
id: '/admin/users'
path: '/admin/users'
fullPath: '/admin/users'
preLoaderRoute: typeof AdminUsersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/settings': {
id: '/admin/settings'
path: '/admin/settings'
@@ -298,12 +358,15 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
AppDownRoute: AppDownRoute,
ForbiddenRoute: ForbiddenRoute,
authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute,
AdminScanUsersRoute: AdminScanUsersRoute,
AdminServersRoute: AdminServersRoute,
AdminSettingsRoute: AdminSettingsRoute,
AdminUsersRoute: AdminUsersRoute,
DocsSplatRoute: DocsSplatRoute,
DocsIndexRoute: DocsIndexRoute,
authUserProfileRoute: authUserProfileRoute,

View File

@@ -45,10 +45,12 @@ export default function NotificationsSubCard({ user }: any) {
let n: any = [];
if (data) {
n = data.map((i: any) => ({
label: i.name,
value: i.id,
}));
n = data
.filter((n: any) => n.active)
.map((i: any) => ({
label: i.name,
value: i.id,
}));
}
return (

View File

@@ -10,6 +10,7 @@ import {
} from "@/components/ui/card";
import { authClient, useSession } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff";
import { Spinner } from "../../components/ui/spinner";
import ChangePassword from "./-components/ChangePassword";
import NotificationsSubCard from "./-components/NotificationsSubCard";
@@ -37,6 +38,7 @@ export const Route = createFileRoute("/(auth)/user/profile")({
function RouteComponent() {
const { data: session } = useSession();
const form = useAppForm({
defaultValues: {
name: session?.user.name,

View File

@@ -0,0 +1,161 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../../../components/ui/dialog";
import { useAppForm } from "../../../lib/formSutff";
import { getScannerIds } from "../../../lib/queries/getScannerIds";
export default function NewScanUser({ refetch }: { refetch: any }) {
const [open, setOpen] = useState(false);
const { data, refetch: scannerFetch } = useSuspenseQuery(getScannerIds());
const form = useAppForm({
defaultValues: {
name: "",
scannerId: "",
pinNumber: "",
},
onSubmit: async ({ value }) => {
if (value.scannerId === "") {
toast.error(
"Scanner id is required please select a scanner id before submitting ",
);
return;
}
try {
const { data } = await axios.post(
"/lst/api/mobile/auth/user",
{
name: value.name,
pinNumber: value.pinNumber,
scannerId: value.scannerId,
},
{
withCredentials: true,
timeout: 15000,
validateStatus: () => true,
},
);
if (data.success) {
toast.success(
`${value.name}, was just created and can now log into the scanner with PIN: ${value.pinNumber}`,
);
form.reset();
setOpen(false);
refetch();
}
if (!data.success) {
toast.error(data.message);
return;
}
} catch (error) {
console.error(error);
}
},
});
const closeModel = (e: boolean) => {
setOpen(e);
if (!e) {
form.reset();
scannerFetch();
}
};
const openForm = () => {
setOpen(true);
scannerFetch();
};
let n: any = [];
if (data) {
n = data.map((i: any) => ({
label: i.label,
value: i.value.toString(),
}));
}
return (
<Dialog onOpenChange={(e) => closeModel(e)} open={open}>
<Button onClick={openForm}>Create new user</Button>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Create New Scan user.</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div className="mb-2">
<form.AppField name="name">
{(field) => (
<field.InputField
label="Name"
inputType="text"
required={true}
/>
)}
</form.AppField>
</div>
<div className="w-32">
<form.AppField name="scannerId">
{(field) => (
<field.SelectField
label="Scanner Id"
placeholder="Select New scanner Id"
options={n}
/>
)}
</form.AppField>
</div>
<div className="flex flex-row">
<div>
<form.AppField name="pinNumber">
{(field) => (
<field.InputField
label="Pin Number"
inputType="number"
required={true}
/>
)}
</form.AppField>
</div>
<div className="mt-9 ml-2">
<Button
type="button"
onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new");
form.setFieldValue("pinNumber", data.data[0].pin);
}}
>
New Pin
</Button>
</div>
</div>
<div className="flex justify-end mt-2 ">
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,16 +1,258 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { format } from "date-fns-tz";
import { Trash } from "lucide-react";
import { Suspense, useState } from "react";
import { toast } from "sonner";
import { Button } from "../../components/ui/button";
import { Spinner } from "../../components/ui/spinner";
import { authClient } from "../../lib/auth-client";
import { getScanUsers } from "../../lib/queries/getScanUsers";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable";
import NewScanUser from "./-components/NewScanUser";
export const Route = createFileRoute("/admin/scanUsers")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin", "manager"];
if (!session?.user) {
throw redirect({
to: "/",
search: {
redirect: location.href,
},
});
}
if (!allowedRole.includes(session.user.role as string)) {
throw redirect({
to: "/",
});
}
return { user: session.user };
},
component: RouteComponent,
});
const ScanUserTable = () => {
const { data } = useSuspenseQuery(getScanUsers());
console.log(data);
return <div>Hello "/admin/scanUsers"!</div>;
const updateSettings = async (
id: string,
data: Record<string, string | number | boolean | null>,
) => {
//console.log(id, data);
try {
const res = await axios.patch(`/lst/api/mobile/auth/user/${id}`, data, {
withCredentials: true,
timeout: 15000,
validateStatus: () => true,
});
toast.success(`User was just updated`);
return res;
} catch (err) {
toast.error("Error in updating the user");
return err;
}
};
const ScanUserTable = () => {
const { data, refetch } = useSuspenseQuery(getScanUsers());
const columnHelper = createColumnHelper<any>();
const updateSetting = useMutation({
mutationFn: ({
id,
field,
value,
}: {
id: string;
field: string;
value: string | number | boolean | null;
}) => updateSettings(id, { [field]: value }),
onSuccess: () => {
// refetch or update cache
refetch();
},
});
const columns = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("scannerId", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Scanner ID"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("pinNumber", {
header: ({ column }) => (
<SearchableHeader column={column} title="Pin Number" />
),
filterFn: "includesString",
cell: ({ row, getValue }) => (
<div className="flex flex-row gap-2">
<div>
<EditableCellInput
value={getValue()}
id={row.original.id}
field="value"
onSubmit={({ id, field, value }) => {
updateSetting.mutate({ id, field, value });
}}
/>
</div>
<div className="">
<Button
type="button"
onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new");
updateSetting.mutate({
id: row.original.id,
field: "pinNumber",
value: data.data[0].pin,
});
}}
>
New Pin
</Button>
</div>
</div>
),
}),
columnHelper.accessor("lastScan", {
header: ({ column }) => (
<SearchableHeader column={column} title="Last Scan" />
),
cell: (i) => <span>{format(i.getValue(), "M/d/yyyy HH:mm")}</span>,
}),
columnHelper.accessor("excludedCommand", {
header: ({ column }) => (
<SearchableHeader column={column} title="Command id's Not Allowed" />
),
cell: (i) => {
const commands = i.getValue().join();
return (
<span>{commands === "" ? "All commands allowed" : commands}</span>
);
},
}),
columnHelper.accessor("deleteUser", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Delete User"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => {
// biome-ignore lint: just removing the lint for now to get this going will maybe fix later
const [activeToggle, setActiveToggle] = useState(false);
const onTrigger = async () => {
setActiveToggle(true);
try {
const res = await axios.delete(
`/lst/api/mobile/auth/user/${i.row.original.id}`,
{
withCredentials: true,
timeout: 5000,
validateStatus: () => true,
},
);
if (res.data.success) {
toast.success(`${i.row.original.name} was deleted.`);
refetch();
setActiveToggle(false);
}
if (!res.data.success) {
toast.error(
`${i.row.original.name} encountered an error when trying to delete: ${res.data.message}`,
);
refetch();
setActiveToggle(false);
}
} catch (error) {
setActiveToggle(false);
console.error(error);
}
};
return (
<div>
<div className="flex items-center space-x-2">
<Button
variant="destructive"
disabled={activeToggle}
onClick={onTrigger}
>
{activeToggle ? (
<span>
<Spinner />
</span>
) : (
<span>
<Trash />
</span>
)}
</Button>
</div>
</div>
);
},
}),
];
return (
<div>
<div className="flex justify-end m-2">
<Suspense
fallback={
<div>
<p>Loading...</p>
</div>
}
>
<NewScanUser refetch={refetch} />
</Suspense>
</div>
<div>
<LstTable data={data} columns={columns} pageSize={50} />
</div>
</div>
);
};
// const NewUserForm = ()=>{
// const { data, refetch } = useSuspenseQuery(getScanUsers());
// }
function RouteComponent() {
return <ScanUserTable />;
//const { data: session } = useSession();
return (
<Suspense fallback={<SkellyTable />}>
<ScanUserTable />
</Suspense>
);
}

View File

@@ -55,7 +55,17 @@ const ServerTable = () => {
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
cell: (i) => (
<>
<a
href={`http://${i.row.original.server}:3000/lst/app`}
target="_blank"
rel="noopener"
>
{i.getValue()}
</a>
</>
),
}),
columnHelper.accessor("greatPlainsPlantCode", {
header: ({ column }) => (

View File

@@ -0,0 +1,156 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import { format } from "date-fns-tz";
import { Suspense } from "react";
import { Button } from "../../components/ui/button";
import { authClient, useSession } from "../../lib/auth-client";
import { getUsers } from "../../lib/queries/getUsers";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable";
import { trackLstEvent } from "../../lib/umami.utils";
export const Route = createFileRoute("/admin/users")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin"];
if (!session?.user) {
throw redirect({
to: "/",
search: {
redirect: location.href,
},
});
}
if (!allowedRole.includes(session.user.role as string)) {
throw redirect({
to: "/",
});
}
return { user: session.user };
},
component: RouteComponent,
});
const UserTable = () => {
const { data } = useSuspenseQuery(getUsers());
const { data: session } = useSession();
const columnHelper = createColumnHelper<any>();
const columns = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("email", {
header: ({ column }) => (
<SearchableHeader column={column} title="Email" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("role", {
header: ({ column }) => (
<SearchableHeader column={column} title="Role" searchable={false} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("updatedAt", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Updated at"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"),
}),
];
if (session && session.user.role === "systemAdmin") {
columns.push(
columnHelper.accessor("banned", {
header: ({ column }) => (
<SearchableHeader column={column} title="Banned" searchable={false} />
),
filterFn: "includesString",
cell: (i) => <span>{i.getValue() ? "True" : "False"}</span>,
}),
columnHelper.accessor("impersonate", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Impersonate User"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => {
const beSomeone = async () => {
trackLstEvent("impersonateUser_click", {
module: "users",
action: "click",
label: "impersonating user",
page: window.location.pathname,
});
const { data, error } = await authClient.admin.impersonateUser({
userId: i.row.original.id, // required
});
if (data) {
await authClient.getSession();
window.location.replace("/lst/app");
}
if (error) {
console.log(error);
}
};
const cantImpersonate = ["admin", "systemAdmin"];
if (cantImpersonate.includes(i.row.original.role)) return;
return <Button onClick={beSomeone}>Become user</Button>;
},
}),
);
}
return <LstTable data={data} columns={columns} pageSize={50} />;
};
function RouteComponent() {
// const createUser = async () => {
// const { data: newUser, error } = await authClient.admin.createUser({
// email: "cowch@gmail.com", // required
// password: "crazypassword", // required
// name: "James Smith", // required
// role: "manager",
// });
// };
// const besomeone = async () => {
// const { data, error } = await authClient.admin.impersonateUser({
// userId: "iswCNVzQ9cWulbmsaMbeX6e7fV6Eme6t", // required
// });
// await authClient.getSession();
// window.location.replace("/lst/app");
// };
return (
<Suspense fallback={<SkellyTable />}>
<UserTable />
</Suspense>
);
}

View File

@@ -0,0 +1,51 @@
import { createFileRoute, useRouter } from "@tanstack/react-router";
import z from "zod";
import { Button } from "../components/ui/button";
import { Card, CardContent, CardHeader } from "../components/ui/card";
import { trackLstEvent } from "../lib/umami.utils";
export const Route = createFileRoute("/app-down")({
validateSearch: z.object({
redirect: z.string().optional(),
}),
component: RouteComponent,
});
function RouteComponent() {
const search = Route.useSearch();
const redirectPath = search.redirect ?? "/";
const router = useRouter();
const click = () => {
trackLstEvent("app_down_click", {
module: "app",
action: "click",
label: "redirect",
page: window.location.pathname,
});
router.navigate({ to: redirectPath, replace: true });
};
return (
<div className="flex items-center justify-center bg-background text-foreground">
<Card>
<CardHeader>
<p className="text-2xl">Oops, you shouldn't have done that</p>
</CardHeader>
<CardContent>
<p className="mt-3 text-muted-foreground">
Your have tried to go to a page that you are not authorized to be
at.
</p>
</CardContent>
<div className=" flex justify-center">
<Button
className="mt-5 rounded-md bg-primary px-4 py-2 text-primary-foreground"
onClick={click}
>
Refresh
</Button>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { Button } from "../components/ui/button";
import { Card, CardContent, CardHeader } from "../components/ui/card";
import { trackLstEvent } from "../lib/umami.utils";
export const Route = createFileRoute("/forbidden")({
component: RouteComponent,
});
function RouteComponent() {
const click = () => {
trackLstEvent("forbidden_click", {
module: "forbidden",
action: "click",
label: "redirect",
page: window.location.pathname,
});
router.navigate({ to: "/", replace: true });
};
const router = useRouter();
return (
<div className="flex items-center justify-center bg-background text-foreground">
<Card>
<CardHeader>
<p className="text-2xl">Oops, you shouldn't have done that</p>
</CardHeader>
<CardContent>
<p className="mt-3 text-muted-foreground">
Your have tried to go to a page that you are not authorized to be
at.
</p>
</CardContent>
<div className=" flex justify-center">
<Button className="w-64" onClick={click}>
Go Back
</Button>
</div>
</Card>
</div>
);
}

View File

@@ -1,9 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import z from "zod";
import { Button } from "../components/ui/button";
import { useSession } from "../lib/auth-client";
import { trackLstEvent } from "../lib/umami.utils";
import { runtimeConfig, trackLstEvent } from "../lib/umami.utils";
export const Route = createFileRoute("/")({
validateSearch: z.object({
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/")({
});
function Index() {
const { isPending } = useSession();
const { data: session, isPending } = useSession();
if (isPending)
return <div className="flex justify-center mt-10">Loading...</div>;
@@ -38,6 +38,16 @@ function Index() {
});
};
const checkConfig = () => {
console.log(runtimeConfig);
trackLstEvent("config_click", {
module: "app",
action: "click",
label: "configCheck",
page: window.location.pathname,
});
};
return (
<div className="flex justify-center m-10 flex-col">
<h3 className="w-2xl text-3xl">Welcome Lst - V3</h3>
@@ -55,7 +65,7 @@ function Index() {
<strong>Click</strong>
</b>
</a>{" "}
<button onClick={click}>
<button onClick={click} type="button">
<a
href={`https://www.youtube.com/watch?v=dQw4w9WgXcQ`}
target="_blank"
@@ -67,6 +77,9 @@ function Index() {
</a>
</button>
</p>
{session && session.user.role === "systemAdmin" && (
<Button onClick={checkConfig}>Check config</Button>
)}
</div>
);
}

View File

@@ -15,8 +15,8 @@
"foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff"
},
"versionCode": 32,
"minSupportedVersionCode": 26,
"versionCode": 37,
"minSupportedVersionCode": 33,
"predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile"
},

View File

@@ -22,7 +22,7 @@ export default function TabsLayout() {
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
const port = parseInt(serverPort || "0", 10) >= 50000;
console.log(port);
if (!port) {
if (!user || !isUnlocked) {
return <Redirect href="/login" />;
@@ -58,14 +58,14 @@ export default function TabsLayout() {
// },
}}
/>
<Tabs.Screen
{/* <Tabs.Screen
name="ppoo"
options={{
title: "PPOO",
href: isNormalScanner ? null : "/(tabs)/ppoo",
tabBarIcon: ({ color, size }) => <Boxes size={size} color={color} />,
}}
/>
/> */}
<Tabs.Screen
name="laneCheck"
options={{

View File

@@ -1,18 +1,210 @@
import React from "react";
import { Text, View } from "react-native";
import axios from "axios";
import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import type React from "react";
import { useCallback, useEffect, useState } from "react";
import { ScrollView, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import { GlobalFooter } from "../../components/UpdateFooter";
import { Button } from "../../components/ui/button";
import { Card, CardContent, CardHeader } from "../../components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { useAppStore } from "../../hooks/useAppStore";
import { scannerFeedback } from "../../lib/feedbackScan";
import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner";
const InfoRow = ({
label,
value,
}: {
label: string;
value: React.ReactNode;
}) => {
return (
<View className="flex-row justify-between gap-4 py-2 border-b border-gray-200">
<Text className="text-sm text-gray-500">{label}</Text>
<Text className="text-sm font-medium text-gray-900 text-right flex-1">
{value}
</Text>
</View>
);
};
export default function PPOO() {
const [units, setUnits] = useState<any>(null);
const serverIp = useAppStore((s) => s.serverIp);
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
setUnits(null);
await scannerFeedback({
type: "scan",
sound: true,
vibrate: true,
led: true,
});
if (!scan.data.startsWith("loc")) {
Toast.show({
type: "error",
text1: "Scan error",
text2: "The last scan was not a lane please try again",
});
return;
}
try {
const res = await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/lanecheck`,
{
lane: "loc#1#0<",
},
{
timeout: 5000,
},
);
if (res.status === 200) {
setUnits(res.data);
Toast.show({
type: "info",
text1: "Lane Data",
text2: "All Loading Units from this lane will be listed below",
});
}
} catch (error) {
console.log(error);
Toast.show({
type: "error",
text1: "Lane Data",
text2: "Error getting lane data please try again",
});
}
},
[serverIp.trim],
);
useFocusEffect(
useCallback(() => {
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
//setUnits(null);
};
}, [handleScan]),
);
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>Ppo checks</Text>
{units ? (
// <SafeAreaView className={`flex-1 w-full items-center`}>
// <ScrollView className="w-full flex-1">
// <View className="flex items-center gap-2 w-full">
// {units.data?.map((i: any, index: any) => (
// <View key={`${i.runningNumber}-${index}`}>
// <Text>example</Text>
// </View>
// ))}
// </View>
// </ScrollView>
// </SafeAreaView>
<SafeAreaView className={`w-full items-center`}>
<View style={{ padding: 2 }}>
<Text>There Are {units.data.length} units in PPOO</Text>
</View>
<ScrollView className="w-full" style={{ marginBottom: 20 }}>
<View>
{units.data.map((i, index) => (
<View
key={`${i.runningNumber}-${index}`}
style={{
justifyContent: "center",
margin: 2,
}}
>
<Dialog>
<DialogTrigger>
<Card
className="w-full"
style={{
borderColor:
i.state === "QualityBlocked" ? "red" : undefined,
borderWidth: 4,
}}
>
<CardContent>
<Text>
{i.articleId} - {i.articleName}
</Text>
<Text>
Running Number: {i.runningNumber ?? "Non barcoded"}
</Text>
</CardContent>
</Card>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Details for Article {i.articleId}, Rn:
{i.runningNumber ?? "Non barcoded"}{" "}
</DialogTitle>
<DialogDescription>
<InfoRow
label="Production Date"
value={format(i.productionDate, "MM/dd/yyyy HH:mm")}
/>
<InfoRow label="Quantity" value={i.quantity} />
{i.state === "QualityBlocked" && (
<InfoRow
label="Defect"
value={i.mainDefectGroupDescription}
/>
)}
{i.state === "QualityBlocked" && (
<InfoRow
label="Description"
value={i.mainDefectDescription}
/>
)}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</View>
))}
</View>
</ScrollView>
</SafeAreaView>
) : (
<View className="mt-50">
<Text className="text-2xl text-center">
Please scan a lane to see all Units that are in the lane.
</Text>
</View>
)}
<View>
<GlobalFooter />
</View>
</View>
);
}

View File

@@ -1,6 +1,7 @@
import axios from "axios";
import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import Constants from "expo-constants";
import { Redirect, useFocusEffect, useRouter } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
@@ -23,15 +24,15 @@ const formatName = (name?: string) =>
export default function LSTScanner() {
const user = useMobileAuthStore((s) => s.user);
const logout = useMobileAuthStore((s) => s.logout);
const router = useRouter();
// TODO : move to off tcp stuff after od
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 build = Constants.expoConfig?.android?.versionCode ?? 1;
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
@@ -94,6 +95,7 @@ export default function LSTScanner() {
: scan.data.startsWith("loc")
? scan.data
: "0",
scannerVersion: build,
});
} catch (error) {
console.log(error);
@@ -147,6 +149,7 @@ export default function LSTScanner() {
user?.name,
user?.excludedCommand?.some,
user?.excludedCommand,
build,
],
);
@@ -154,6 +157,13 @@ export default function LSTScanner() {
setTagScans([]);
};
const logoutScanner = () => {
setTagScans([]);
setLastScan(null);
logout();
router.replace("/");
};
//console.log(lastScan);
useFocusEffect(
@@ -189,7 +199,11 @@ export default function LSTScanner() {
{!lastScan ? (
<View style={{ marginTop: 10, alignItems: "center" }}>
<Text className="text-xl font-bold">Ready to scan</Text>
<Text>Waiting for first scan...</Text>
<Text>Please Scan a command to start scanning...</Text>
<Text className="text-sm">
Scanning a label could cause errors due to incorrect previous
command scanned
</Text>
</View>
) : (
<View
@@ -228,7 +242,7 @@ export default function LSTScanner() {
<View className="m-2">
{user && (
<View className="items-center">
<Button title="Logout" onPress={logout} />
<Button title="Logout" onPress={logoutScanner} />
</View>
)}
</View>

View File

@@ -1,5 +1,6 @@
import axios from "axios";
import { format } from "date-fns-tz";
import Constants from "expo-constants";
import { useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native";
@@ -25,6 +26,7 @@ export default function ProdScanner() {
const serverIp = useAppStore((s) => s.serverIp);
const serverPort = useAppStore((s) => s.serverPort);
const [bgColor, setBGColor] = useState<string | null>(null);
const build = Constants.expoConfig?.android?.versionCode ?? 1;
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
@@ -62,6 +64,7 @@ export default function ProdScanner() {
: scan.data.startsWith("loc")
? scan.data
: "0",
scannerVersion: build,
};
try {
await axios.post(
@@ -112,7 +115,7 @@ export default function ProdScanner() {
setTagScans([]);
}
},
[scannerIdFromStore, serverIp, serverPort, setLastScan],
[scannerIdFromStore, serverIp, serverPort, setLastScan, build],
);
const clearScans = () => {

View File

@@ -6,7 +6,7 @@ import TcpSocket from "react-native-tcp-socket";
type TcpResponse = {
success: boolean;
message: string;
data: string[];
data: ScannerEvent;
};
type ScannerEvent = {
@@ -232,7 +232,7 @@ export async function sendTcpMessage(
timeoutMs = 5000,
): Promise<TcpResponse> {
return new Promise((resolve) => {
const responses: any = [];
//const responses: any = [];
const client = TcpSocket.createConnection({ host, port }, () => {
//console.log("Sending TCP (visible):", `${command}`);
@@ -240,17 +240,53 @@ export async function sendTcpMessage(
client.write(command);
});
const timeout = setTimeout(() => {
client.destroy();
let settled = false;
resolve({
const done = (response: TcpResponse) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
try {
client.destroy();
} catch {}
resolve(response);
};
// const timeout = setTimeout(() => {
// client.destroy();
// resolve({
// success: false,
// message: "TCP timeout",
// data: responses,
// });
// }, timeoutMs);
const timeout = setTimeout(() => {
done({
success: false,
message: "TCP timeout",
data: responses,
message: "No response from scanner server",
data: {
scannerId: "999",
commandDescription: "TCP Command",
prompt: command,
message: "Invalid command",
status: "error",
lines: [
"SYSTEM",
"TCP Command",
`Scan: ${command}`,
"Invalid command",
],
},
});
}, timeoutMs);
client.on("data", (data) => {
client.on("data", (data: any) => {
//console.log("TCP received:", text);
const parsed = parseScannerText(data);
//console.log("scanned:", parsed);
@@ -260,32 +296,69 @@ export async function sendTcpMessage(
const cleaned = parseScannerEvent(parsed);
//console.log(responses);
clearTimeout(timeout);
resolve({
success: true,
message: "TCP Response",
data: cleaned as any,
// clearTimeout(timeout);
// resolve({
// success: true,
// message: "TCP Response",
// data: cleaned as any,
// });
done({
success: cleaned.status !== "error",
message:
cleaned.status === "error"
? (cleaned.message ?? "TCP Error")
: "TCP Response",
data: cleaned,
});
});
client.on("error", (err) => {
clearTimeout(timeout);
client.destroy();
// resolve({
// success: false,
// message: err.message,
// data: ["Error", "Please try again"] as any,
// });
resolve({
done({
success: false,
message: err.message,
data: ["Error", "Please try again"],
data: {
scannerId: "SYSTEM",
commandDescription: "TCP Error",
prompt: command,
message: err.message,
status: "error",
lines: ["SYSTEM", "TCP Error", `Scan: ${command}`, err.message],
},
});
});
client.on("close", () => {
clearTimeout(timeout);
if (settled) return;
resolve({
success: true,
message: "TCP complete",
data: ["Error", "Please try again"],
// resolve({
// success: true,
// message: "TCP complete",
// data: ["Error", "Please try again"] as any,
// });
done({
success: false,
message: "TCP connection closed",
data: {
scannerId: "SYSTEM",
commandDescription: "TCP Closed",
prompt: command,
message: "Connection closed before response",
status: "error",
lines: [
"SYSTEM",
"TCP Closed",
`Scan: ${command}`,
"Connection closed before response",
],
},
});
});
});

View File

@@ -0,0 +1 @@
ALTER TABLE "analytics_daily" ADD CONSTRAINT "analytics_daily_business_route_unique" UNIQUE("business_date","method","route_pattern","module");

View File

@@ -0,0 +1 @@
ALTER TABLE "scan_log" ADD COLUMN "scanner_version" text DEFAULT '0';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -358,6 +358,20 @@
"when": 1778169641819,
"tag": "0050_concerned_vivisector",
"breakpoints": true
},
{
"idx": 51,
"version": "7",
"when": 1778525497824,
"tag": "0051_sad_war_machine",
"breakpoints": true
},
{
"idx": 52,
"version": "7",
"when": 1778533475205,
"tag": "0052_numerous_wasp",
"breakpoints": true
}
]
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "lst_v3",
"version": "0.0.2-alpha.10",
"version": "0.1.0-alpha.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lst_v3",
"version": "0.0.2-alpha.10",
"version": "0.1.0-alpha.0",
"license": "ISC",
"dependencies": {
"@dotenvx/dotenvx": "^1.57.0",

View File

@@ -1,6 +1,6 @@
{
"name": "lst_v3",
"version": "0.0.2-alpha.10",
"version": "0.1.0-alpha.0",
"description": "The tool that supports us in our everyday alplaprod",
"main": "index.js",
"scripts": {

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