16 Commits

Author SHA1 Message Date
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
83 changed files with 3774 additions and 228 deletions

2
.gitignore vendored
View File

@@ -9,7 +9,7 @@ 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

View File

@@ -3,16 +3,14 @@ import { requireAuth } from "../middleware/auth.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, build);
app.use(
`${baseUrl}/api/admin/build`,
requireAuth,
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

@@ -34,7 +34,6 @@ const createApp = async () => {
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");
@@ -52,6 +51,8 @@ const createApp = async () => {
`);
});
setupRoutes(baseUrl, app);
app.use(
`${baseUrl}/app`,
express.static(join(__dirname, "../frontend/dist")),

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

@@ -8,11 +8,10 @@ export const setupGPSqlRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
// Apply auth to entire router
const router = Router();
router.use(requireAuth);
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

@@ -8,7 +8,7 @@ import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
// scanners that are dedicated to specific users.
const SPECIAL_SCANNERS = [69, 98];
const SPECIAL_SCANNERS = [98];
const buildAllowedScannerIds = (scannerCount: number) => {
const generatedIds = Array.from({ length: scannerCount }, (_, i) => i + 1);

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,4 +1,5 @@
import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import available from "./availableScanIds.route.js";
import downloads from "./downloadApps.route.js";
import lanes from "./laneCheck.js";
@@ -10,13 +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(`${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/available`, available);
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,14 +1,22 @@
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({
@@ -20,6 +28,7 @@ router.post("/", async (req, res) => {
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

@@ -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,4 +1,4 @@
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";
@@ -6,20 +6,11 @@ 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);
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,4 +1,4 @@
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";
@@ -6,15 +6,11 @@ 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);
router.use(getApt);
app.use(`${baseUrl}/api/opendock`, router);
app.use(
`${baseUrl}/api/opendock`,
featureCheck("opendock_sync"),
requireAuth,
getApt,
);
};

View File

@@ -10,9 +10,7 @@ export const setupProdSqlRoutes = (baseUrl: string, app: Express) => {
const router = Router();
router.use(requireAuth);
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

@@ -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

@@ -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",
@@ -70,7 +72,9 @@ export default function AdminSidebar({ session }: any) {
icon: UsersRound,
role: ["systemAdmin", "admin", "manager"],
module: "admin",
active: true,
active:
!isLoading &&
settings.filter((n: any) => n.name === "mobile")[0].active,
},
];
return (
@@ -80,7 +84,7 @@ export default function AdminSidebar({ session }: any) {
<SidebarMenu>
{items.map((item) => (
<div key={item.title}>
{item.role.includes(session.user.role) && (
{item.role.includes(session.user.role) && item.active && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>

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: "/",

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,7 +27,11 @@ 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" ||

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;
@@ -23,4 +21,4 @@ for (const path in modules) {
export function getDoc(slug: string) {
return docsMap[slug];
}
}

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

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function getScannerIds() {
return queryOptions({
@@ -16,10 +17,7 @@ const fetch = async () => {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get("/lst/api/mobile/available", {
withCredentials: true,
timeout: 5000,
});
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

@@ -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,6 +9,8 @@
// 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'
@@ -24,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',
@@ -98,6 +110,8 @@ 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
@@ -114,6 +128,8 @@ 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
@@ -131,6 +147,8 @@ 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
@@ -149,6 +167,8 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/about'
| '/app-down'
| '/forbidden'
| '/login'
| '/admin/logs'
| '/admin/notifications'
@@ -165,6 +185,8 @@ export interface FileRouteTypes {
to:
| '/'
| '/about'
| '/app-down'
| '/forbidden'
| '/login'
| '/admin/logs'
| '/admin/notifications'
@@ -181,6 +203,8 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/about'
| '/app-down'
| '/forbidden'
| '/(auth)/login'
| '/admin/logs'
| '/admin/notifications'
@@ -198,6 +222,8 @@ 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
@@ -214,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'
@@ -318,6 +358,8 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
AppDownRoute: AppDownRoute,
ForbiddenRoute: ForbiddenRoute,
authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute,

View File

@@ -45,14 +45,14 @@ 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,
}));
}
console.log(n);
return (
<div>
<Card className="p-3 w-lg">

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

@@ -3,7 +3,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { format } from "date-fns-tz";
import { CircleFadingArrowUp, Trash } from "lucide-react";
import { Trash } from "lucide-react";
import { Suspense, useState } from "react";
import { toast } from "sonner";
import { Button } from "../../components/ui/button";
@@ -19,7 +19,7 @@ import NewScanUser from "./-components/NewScanUser";
export const Route = createFileRoute("/admin/scanUsers")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin"];
const allowedRole = ["systemAdmin", "admin", "manager"];
if (!session?.user) {
throw redirect({
@@ -111,7 +111,7 @@ const ScanUserTable = () => {
<div>
<EditableCellInput
value={getValue()}
id={row.original.name}
id={row.original.id}
field="value"
onSubmit={({ id, field, value }) => {
updateSetting.mutate({ id, field, value });

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,7 +15,7 @@
"foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff"
},
"versionCode": 33,
"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,5 +1,6 @@
import axios from "axios";
import { format } from "date-fns-tz";
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";
@@ -31,6 +32,7 @@ export default function LSTScanner() {
const [tagScans, setTagScans] = useState<any>([]);
const serverIp = useAppStore((s) => s.serverIp);
const [bgColor, setBGColor] = useState<string | null>(null);
const build = Constants.expoConfig?.android?.versionCode ?? 1;
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
@@ -93,6 +95,7 @@ export default function LSTScanner() {
: scan.data.startsWith("loc")
? scan.data
: "0",
scannerVersion: build,
});
} catch (error) {
console.log(error);
@@ -146,6 +149,7 @@ export default function LSTScanner() {
user?.name,
user?.excludedCommand?.some,
user?.excludedCommand,
build,
],
);

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 "scan_log" ADD COLUMN "scanner_version" text DEFAULT '0';

File diff suppressed because it is too large Load Diff

View File

@@ -365,6 +365,13 @@
"when": 1778525497824,
"tag": "0051_sad_war_machine",
"breakpoints": true
},
{
"idx": 52,
"version": "7",
"when": 1778533475205,
"tag": "0052_numerous_wasp",
"breakpoints": true
}
]
}

View File

@@ -15,7 +15,7 @@ param (
# server migrations get - reminder to add to old version in pkg "start:lst": "cd lstV2 && npm start",
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LST_app" -option "install" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start"
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LSTV2" -option "install" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start:lst"
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LSTV3_app" -option "install" -appPath "D:\LST_V3" -description "Logistics Support Tool" -command "run start"
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LST_ctl" -option "delete" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start:lst"
$nssmPath = $AppPath + "\nssm.exe"

View File

@@ -87,7 +87,15 @@ function Update-Server {
param ($Server, $Token, $Destination, $BuildFile)
function Fix-Env {
$envFile = ".env"
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$envFile = Join-Path $Path ".env"
Write-Host "Checking env file: $envFile"
if (-not (Test-Path $envFile)) {
Write-Host ".env not found, creating..."
@@ -197,7 +205,7 @@ function Update-Server {
Write-Host "Install/update completed."
# update the env to include the new and missing things silly people and wanting things fixed :(
Fix-Env #-Path $LocalPath
Fix-Env -Path $LocalPath
# do the migrations
# Push-Location $LocalPath