Compare commits
33 Commits
6b515c608f
...
v0.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 36ac1dccb4 | |||
| 514a44b6de | |||
| a7bb364a2f | |||
| 047cc7cdf0 | |||
| 8dc4d70e28 | |||
| c8931c7249 | |||
| 67f36c5499 | |||
| ebf1060475 | |||
| c64392f457 | |||
| e9e73c829c | |||
| bcb7773007 | |||
| eb950d2c29 | |||
| 2616acf106 | |||
| 30ff7b71d9 | |||
| e7af3d1182 | |||
| 3e66c3920d | |||
| eb9d77c3d4 | |||
| 342a97f6b1 | |||
| b0c7277a6c | |||
| dc95e50a84 | |||
| d2a9e1d110 | |||
| a9c69250bd | |||
| d61be61f44 | |||
| f5bae2c0c2 | |||
| 05758791be | |||
| 51026e3e2c | |||
| 9631736e26 | |||
| ce9d8eaaf5 | |||
| 1bbf5c2a49 | |||
| 13718fe702 | |||
| 0de2579942 | |||
| 7c31b43a4a | |||
| 85e96f5ed1 |
3
.gitignore
vendored
@@ -9,8 +9,9 @@ downloads
|
|||||||
.scriptCreds
|
.scriptCreds
|
||||||
node-v24.14.0-x64.msi
|
node-v24.14.0-x64.msi
|
||||||
postgresql-17.9-2-windows-x64.exe
|
postgresql-17.9-2-windows-x64.exe
|
||||||
VSCodeUserSetup-x64-1.112.0.exe
|
VSCodeSetup-x64-1.120.0.exe
|
||||||
nssm.exe
|
nssm.exe
|
||||||
|
frontend/.tanstack
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
|||||||
76
CHANGELOG.md
@@ -1,5 +1,81 @@
|
|||||||
# All Changes to LST can be found below.
|
# All Changes to LST can be found below.
|
||||||
|
|
||||||
|
## [0.1.0-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.0...v0.1.0-alpha.1) (2026-05-19)
|
||||||
|
|
||||||
|
|
||||||
|
### 🐛 Bug fixes
|
||||||
|
|
||||||
|
* **notifications:** reprinting ([c8931c7](https://git.tuffraid.net/cowch/lst_v3/commits/c8931c7249b8f532b5dd37df3271da98f14ee710)), closes [#20](https://git.tuffraid.net/cowch/lst_v3/issues/20)
|
||||||
|
* **settings:** failed build due it dormant import ([a7bb364](https://git.tuffraid.net/cowch/lst_v3/commits/a7bb364a2fd49d96b6195aca0cd58ba57c58f3a6))
|
||||||
|
|
||||||
|
|
||||||
|
### 🛠️ Code Refactor
|
||||||
|
|
||||||
|
* **servers:** changed activeity around and trying to make use of it ([514a44b](https://git.tuffraid.net/cowch/lst_v3/commits/514a44b6de3efe8dd8b308d98bdbc82e31ed8427))
|
||||||
|
* **users:** lots of auth stuff added to make it more easy to manage users ([047cc7c](https://git.tuffraid.net/cowch/lst_v3/commits/047cc7cdf036c39a89a0b87ab59dda8328efe0c0))
|
||||||
|
|
||||||
|
|
||||||
|
### 📈 Project changes
|
||||||
|
|
||||||
|
* **app:** added in chokidar to monitor folders ([8dc4d70](https://git.tuffraid.net/cowch/lst_v3/commits/8dc4d70e2827f0a40d2f54886fd757c8a2dc5ac4))
|
||||||
|
|
||||||
|
## [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)
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import build from "./admin.build.js";
|
import build from "./admin.build.js";
|
||||||
import update from "./admin.updateServer.js";
|
import update from "./admin.updateServer.js";
|
||||||
|
import users from "./admin.users.js";
|
||||||
|
|
||||||
export const setupAdminRoutes = (baseUrl: string, app: Express) => {
|
export const setupAdminRoutes = (baseUrl: string, app: Express) => {
|
||||||
//stats will be like this as we dont need to change this
|
//stats will be like this as we dont need to change this
|
||||||
app.use(`${baseUrl}/api/admin/build`, requireAuth, routeHitMiddleware, build);
|
|
||||||
app.use(
|
app.use(`${baseUrl}/api/admin/build`, requireAuth, build);
|
||||||
`${baseUrl}/api/admin/build`,
|
app.use(`${baseUrl}/api/admin/build`, requireAuth, update);
|
||||||
requireAuth,
|
app.use(`${baseUrl}/api/admin/user`, requireAuth, users);
|
||||||
routeHitMiddleware,
|
|
||||||
update,
|
|
||||||
);
|
|
||||||
|
|
||||||
// all other system should be under /api/system/*
|
// all other system should be under /api/system/*
|
||||||
};
|
};
|
||||||
|
|||||||
46
backend/admin/admin.users.ts
Normal 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;
|
||||||
@@ -5,6 +5,7 @@ import express from "express";
|
|||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
import { umamiConfig } from "./configs/umami.config.js";
|
import { umamiConfig } from "./configs/umami.config.js";
|
||||||
import { createLogger } from "./logger/logger.controller.js";
|
import { createLogger } from "./logger/logger.controller.js";
|
||||||
|
import { routeHitMiddleware } from "./middleware/routeHit.middleware.js";
|
||||||
import { setupRoutes } from "./routeHandler.routes.js";
|
import { setupRoutes } from "./routeHandler.routes.js";
|
||||||
import { auth } from "./utils/auth.utils.js";
|
import { auth } from "./utils/auth.utils.js";
|
||||||
import { lstCors } from "./utils/cors.utils.js";
|
import { lstCors } from "./utils/cors.utils.js";
|
||||||
@@ -30,9 +31,9 @@ const createApp = async () => {
|
|||||||
app.use(morgan("dev"));
|
app.use(morgan("dev"));
|
||||||
app.set("trust proxy", true);
|
app.set("trust proxy", true);
|
||||||
app.use(lstCors());
|
app.use(lstCors());
|
||||||
|
app.use(routeHitMiddleware);
|
||||||
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
|
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
setupRoutes(baseUrl, app);
|
|
||||||
|
|
||||||
app.get(`${baseUrl}/api/lst-config.js`, (_, res) => {
|
app.get(`${baseUrl}/api/lst-config.js`, (_, res) => {
|
||||||
res.type("application/javascript");
|
res.type("application/javascript");
|
||||||
@@ -50,6 +51,8 @@ const createApp = async () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupRoutes(baseUrl, app);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/app`,
|
`${baseUrl}/app`,
|
||||||
express.static(join(__dirname, "../frontend/dist")),
|
express.static(join(__dirname, "../frontend/dist")),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import login from "./login.route.js";
|
import login from "./login.route.js";
|
||||||
import register from "./register.route.js";
|
import register from "./register.route.js";
|
||||||
|
|
||||||
export const setupAuthRoutes = (baseUrl: string, app: Express) => {
|
export const setupAuthRoutes = (baseUrl: string, app: Express) => {
|
||||||
//setup all the routes
|
//setup all the routes
|
||||||
app.use(routeHitMiddleware);
|
|
||||||
app.use(`${baseUrl}/api/authentication/login`, login);
|
app.use(`${baseUrl}/api/authentication/login`, login);
|
||||||
app.use(`${baseUrl}/api/authentication/register`, register);
|
app.use(`${baseUrl}/api/authentication/register`, register);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||||
import { datamartData } from "./datamartData.utlis.js";
|
import { datamartData } from "./datamartData.utlis.js";
|
||||||
import runQuery from "./getDatamart.route.js";
|
import runQuery from "./getDatamart.route.js";
|
||||||
@@ -30,7 +30,7 @@ export const setupDatamartRoutes = (baseUrl: string, app: Express) => {
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
//setup all the routes
|
//setup all the routes
|
||||||
app.use(routeHitMiddleware);
|
|
||||||
app.use(`${baseUrl}/api/datamart`, runQuery);
|
app.use(`${baseUrl}/api/datamart`, runQuery);
|
||||||
|
|
||||||
// just sending a get on datamart will return all the queries that we can call.
|
// just sending a get on datamart will return all the queries that we can call.
|
||||||
|
|||||||
@@ -4,30 +4,42 @@ import {
|
|||||||
pgTable,
|
pgTable,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
unique,
|
||||||
uuid,
|
uuid,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const analyticsDaily = pgTable("analytics_daily", {
|
export const analyticsDaily = pgTable(
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
"analytics_daily",
|
||||||
|
{
|
||||||
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
|
||||||
businessDate: date("business_date").notNull(),
|
businessDate: date("business_date", { mode: "string" }).notNull(),
|
||||||
|
|
||||||
method: text("method").notNull(),
|
method: text("method").notNull(),
|
||||||
routePattern: text("route_pattern").notNull(),
|
routePattern: text("route_pattern").notNull(),
|
||||||
module: text("module").notNull(),
|
module: text("module").notNull(),
|
||||||
|
|
||||||
totalHits: integer("total_hits").notNull(),
|
totalHits: integer("total_hits").notNull(),
|
||||||
uniqueUsers: integer("unique_users").notNull(),
|
uniqueUsers: integer("unique_users").notNull(),
|
||||||
|
|
||||||
successCount: integer("success_count").notNull(),
|
successCount: integer("success_count").notNull(),
|
||||||
errorCount: integer("error_count").notNull(),
|
errorCount: integer("error_count").notNull(),
|
||||||
|
|
||||||
avgDurationMs: integer("avg_duration_ms").notNull(),
|
avgDurationMs: integer("avg_duration_ms").notNull(),
|
||||||
maxDurationMs: integer("max_duration_ms").notNull(),
|
maxDurationMs: integer("max_duration_ms").notNull(),
|
||||||
|
|
||||||
firstHitAt: timestamp("first_hit_at").notNull(),
|
firstHitAt: timestamp("first_hit_at").notNull(),
|
||||||
lastHitAt: timestamp("last_hit_at").notNull(),
|
lastHitAt: timestamp("last_hit_at").notNull(),
|
||||||
|
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updated_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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const scanLog = pgTable("scan_log", {
|
|||||||
commandDescription: text("command_description"),
|
commandDescription: text("command_description"),
|
||||||
runningNumber: text("running_number").default("0"),
|
runningNumber: text("running_number").default("0"),
|
||||||
status: text("status"),
|
status: text("status"),
|
||||||
|
scannerVersion: text("scanner_version").default("0"),
|
||||||
lines: jsonb("lines").default([]),
|
lines: jsonb("lines").default([]),
|
||||||
add_Date: timestamp("add_date").defaultNow(),
|
add_Date: timestamp("add_date").defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type Express, Router } from "express";
|
import { type Express, Router } from "express";
|
||||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import restart from "./gpSqlRestart.route.js";
|
import restart from "./gpSqlRestart.route.js";
|
||||||
import start from "./gpSqlStart.route.js";
|
import start from "./gpSqlStart.route.js";
|
||||||
import stop from "./gpSqlStop.route.js";
|
import stop from "./gpSqlStop.route.js";
|
||||||
@@ -8,12 +8,10 @@ export const setupGPSqlRoutes = (baseUrl: string, app: Express) => {
|
|||||||
//setup all the routes
|
//setup all the routes
|
||||||
// Apply auth to entire router
|
// Apply auth to entire router
|
||||||
const router = Router();
|
const router = Router();
|
||||||
router.use(requireAuth);
|
|
||||||
app.use(routeHitMiddleware);
|
|
||||||
|
|
||||||
router.use(start);
|
router.use(start);
|
||||||
router.use(stop);
|
router.use(stop);
|
||||||
router.use(restart);
|
router.use(restart);
|
||||||
|
|
||||||
app.use(`${baseUrl}/api/system/gpSql`, router);
|
app.use(`${baseUrl}/api/system/gpSql`, requireAuth, router);
|
||||||
};
|
};
|
||||||
|
|||||||
54
backend/mobile/availableScanIds.route.ts
Normal 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;
|
||||||
@@ -38,7 +38,66 @@ router.get("/ehs", (_, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader("Content-Type", "application/vnd.android.package-archive");
|
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);
|
return res.sendFile(apkPath);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Router } from "express";
|
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 { 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();
|
const router = Router();
|
||||||
|
|
||||||
@@ -9,7 +15,27 @@ router.post("/", async (req, res) => {
|
|||||||
|
|
||||||
const lane = body.lane.split("#");
|
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({
|
const laneData = await runProdApi({
|
||||||
method: "post",
|
method: "post",
|
||||||
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",
|
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Express } from "express";
|
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 downloads from "./downloadApps.route.js";
|
||||||
import lanes from "./laneCheck.js";
|
import lanes from "./laneCheck.js";
|
||||||
import authPin from "./mobileAuth.route.js";
|
import authPin from "./mobileAuth.route.js";
|
||||||
@@ -10,14 +11,13 @@ import version from "./version.route.js";
|
|||||||
export const setupMobileRoutes = (baseUrl: string, app: Express) => {
|
export const setupMobileRoutes = (baseUrl: string, app: Express) => {
|
||||||
//stats will be like this as we dont need to change this
|
//stats will be like this as we dont need to change this
|
||||||
|
|
||||||
app.use(routeHitMiddleware);
|
app.use(`${baseUrl}/api/mobile/version`, featureCheck("mobile"), version);
|
||||||
|
app.use(`${baseUrl}/api/mobile/apk`, featureCheck("mobile"), downloads);
|
||||||
app.use(`${baseUrl}/api/mobile/version`, version);
|
app.use(`${baseUrl}/api/mobile/logs`, featureCheck("mobile"), logs);
|
||||||
app.use(`${baseUrl}/api/mobile/apk`, downloads);
|
app.use(`${baseUrl}/api/mobile/auth`, featureCheck("mobile"), authPin);
|
||||||
app.use(`${baseUrl}/api/mobile/logs`, logs);
|
app.use(`${baseUrl}/api/mobile/pin`, featureCheck("mobile"), newPin);
|
||||||
app.use(`${baseUrl}/api/mobile/auth`, authPin);
|
app.use(`${baseUrl}/api/mobile/laneCheck`, featureCheck("mobile"), lanes);
|
||||||
app.use(`${baseUrl}/api/mobile/pin`, newPin);
|
app.use(`${baseUrl}/api/mobile/available`, featureCheck("mobile"), available);
|
||||||
app.use(`${baseUrl}/api/mobile/laneCheck`, lanes);
|
|
||||||
|
|
||||||
// all other system should be under /api/system/*
|
// all other system should be under /api/system/*
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
|
import { eq, sql } from "drizzle-orm";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
|
||||||
import { db } from "../db/db.controller.js";
|
import { db } from "../db/db.controller.js";
|
||||||
import { scanLog } from "../db/schema/scanlog.schema.js";
|
import { scanLog } from "../db/schema/scanlog.schema.js";
|
||||||
|
import { scanUser } from "../db/schema/scanUsers.js";
|
||||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/", async (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
const body = req.body;
|
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
|
const newLog = await db
|
||||||
.insert(scanLog)
|
.insert(scanLog)
|
||||||
.values({
|
.values({
|
||||||
scannerId: body.scannerId,
|
scannerId: body.scannerId ?? "",
|
||||||
message: body.message,
|
message: body.message ?? "",
|
||||||
prompt: body.prompt,
|
prompt: body.prompt ?? "",
|
||||||
commandDescription: body.commandDescription,
|
commandDescription: body.commandDescription ?? "",
|
||||||
status: body.status,
|
status: body.status ?? "",
|
||||||
lines: body.lines,
|
lines: body.lines ?? "",
|
||||||
user: body.user,
|
user: body.user ?? "",
|
||||||
runningNumber: body.runningNumber,
|
runningNumber: body.runningNumber ?? "",
|
||||||
|
scannerVersion: body.scannerVersion ?? "0",
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
80
backend/notification/notification.SqlJobCleanUp.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import manual from "./notification.manualTrigger.js";
|
import manual from "./notification.manualTrigger.js";
|
||||||
import getNotifications from "./notification.route.js";
|
import getNotifications from "./notification.route.js";
|
||||||
import updateNote from "./notification.update.route.js";
|
import updateNote from "./notification.update.route.js";
|
||||||
@@ -14,43 +14,43 @@ export const setupNotificationRoutes = (baseUrl: string, app: Express) => {
|
|||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/api/notification`,
|
`${baseUrl}/api/notification`,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
routeHitMiddleware,
|
|
||||||
getNotifications,
|
getNotifications,
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/api/notification`,
|
`${baseUrl}/api/notification`,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
routeHitMiddleware,
|
|
||||||
updateNote,
|
updateNote,
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/api/notification/manual`,
|
`${baseUrl}/api/notification/manual`,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
routeHitMiddleware,
|
|
||||||
manual,
|
manual,
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/api/notification/sub`,
|
`${baseUrl}/api/notification/sub`,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
routeHitMiddleware,
|
|
||||||
subs,
|
subs,
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/api/notification/sub`,
|
`${baseUrl}/api/notification/sub`,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
routeHitMiddleware,
|
|
||||||
newSub,
|
newSub,
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/api/notification/sub`,
|
`${baseUrl}/api/notification/sub`,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
routeHitMiddleware,
|
|
||||||
updateSub,
|
updateSub,
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/api/notification/sub`,
|
`${baseUrl}/api/notification/sub`,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
routeHitMiddleware,
|
|
||||||
deleteSub,
|
deleteSub,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 { printer: printerName } = req.params;
|
||||||
const event: PrinterEvent = parseZebraAlert(req.body);
|
const event: PrinterEvent = parseZebraAlert(req.body);
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { printerSync } from "./ocp.printer.manage.js";
|
|||||||
|
|
||||||
const r = Router();
|
const r = Router();
|
||||||
|
|
||||||
r.post("/printer/update", async (_, res) => {
|
r.post("/update", async (_, res) => {
|
||||||
printerSync();
|
printerSync();
|
||||||
return apiReturn(res, {
|
return apiReturn(res, {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import { type Express, Router } from "express";
|
import type { Express } from "express";
|
||||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||||
import { featureCheck } from "../middleware/featureActive.middleware.js";
|
import { featureCheck } from "../middleware/featureActive.middleware.js";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import listener from "./ocp.printer.listener.js";
|
import listener from "./ocp.printer.listener.js";
|
||||||
import update from "./ocp.printer.update.js";
|
import update from "./ocp.printer.update.js";
|
||||||
|
|
||||||
export const setupOCPRoutes = (baseUrl: string, app: Express) => {
|
export const setupOCPRoutes = (baseUrl: string, app: Express) => {
|
||||||
//setup all the routes
|
app.use(`${baseUrl}/api/ocp/printer/listener`, featureCheck("ocp"), listener);
|
||||||
const router = Router();
|
app.use(
|
||||||
|
`${baseUrl}/api/ocp/printer`,
|
||||||
// is the feature even on?
|
featureCheck("ocp"),
|
||||||
router.use(featureCheck("ocp"));
|
requireAuth,
|
||||||
|
update,
|
||||||
// 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);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import { type Express, Router } from "express";
|
import type { Express } from "express";
|
||||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||||
import { featureCheck } from "../middleware/featureActive.middleware.js";
|
import { featureCheck } from "../middleware/featureActive.middleware.js";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import getApt from "./opendockGetRelease.route.js";
|
import getApt from "./opendockGetRelease.route.js";
|
||||||
|
|
||||||
export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
|
export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
|
||||||
//setup all the routes
|
//setup all the routes
|
||||||
// Apply auth to entire router
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
// is the feature even on?
|
app.use(
|
||||||
router.use(featureCheck("opendock_sync"));
|
`${baseUrl}/api/opendock`,
|
||||||
|
featureCheck("opendock_sync"),
|
||||||
// we need to make sure we are authenticated to see the releases
|
requireAuth,
|
||||||
router.use(requireAuth);
|
getApt,
|
||||||
app.use(routeHitMiddleware);
|
);
|
||||||
|
|
||||||
router.use(getApt);
|
|
||||||
app.use(`${baseUrl}/api/opendock`, router);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type Express, Router } from "express";
|
import { type Express, Router } from "express";
|
||||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import restart from "./prodSqlRestart.route.js";
|
import restart from "./prodSqlRestart.route.js";
|
||||||
import start from "./prodSqlStart.route.js";
|
import start from "./prodSqlStart.route.js";
|
||||||
import stop from "./prodSqlStop.route.js";
|
import stop from "./prodSqlStop.route.js";
|
||||||
@@ -9,11 +9,8 @@ export const setupProdSqlRoutes = (baseUrl: string, app: Express) => {
|
|||||||
// Apply auth to entire router
|
// Apply auth to entire router
|
||||||
const router = Router();
|
const router = Router();
|
||||||
router.use(requireAuth);
|
router.use(requireAuth);
|
||||||
app.use(routeHitMiddleware);
|
|
||||||
|
|
||||||
router.use(start);
|
app.use(`${baseUrl}/api/system/prodSql/start`, requireAuth, start);
|
||||||
router.use(stop);
|
app.use(`${baseUrl}/api/system/prodSql/stop`, requireAuth, stop);
|
||||||
router.use(restart);
|
app.use(`${baseUrl}/api/system/prodSql/restart`, requireAuth, restart);
|
||||||
|
|
||||||
app.use(`${baseUrl}/api/system/prodSql`, router);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { closePool, connectProdSql } from "./prodSqlConnection.controller.js";
|
|||||||
|
|
||||||
const r = Router();
|
const r = Router();
|
||||||
|
|
||||||
r.post("/restart", async (_, res) => {
|
r.post("/", async (_, res) => {
|
||||||
await closePool();
|
await closePool();
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
await new Promise((r) => setTimeout(r, 2000));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { connectProdSql } from "./prodSqlConnection.controller.js";
|
|||||||
|
|
||||||
const r = Router();
|
const r = Router();
|
||||||
|
|
||||||
r.post("/start", async (_, res) => {
|
r.post("/", async (_, res) => {
|
||||||
const connect = await connectProdSql();
|
const connect = await connectProdSql();
|
||||||
apiReturn(res, {
|
apiReturn(res, {
|
||||||
success: connect.success,
|
success: connect.success,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { closePool } from "./prodSqlConnection.controller.js";
|
|||||||
|
|
||||||
const r = Router();
|
const r = Router();
|
||||||
|
|
||||||
r.post("/stop", async (_, res) => {
|
r.post("/", async (_, res) => {
|
||||||
const connect = await closePool();
|
const connect = await closePool();
|
||||||
apiReturn(res, {
|
apiReturn(res, {
|
||||||
success: connect.success,
|
success: connect.success,
|
||||||
|
|||||||
8
backend/prodSql/queries/disableJob.sql
Normal 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;
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
use [test1_AlplaPROD2.0_Read]
|
use [test1_AlplaPROD2.0_Read]
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
--JSON_VALUE(content, '$.EntityId') as labelId
|
JSON_VALUE(content, '$.EntityId') as labelId,
|
||||||
a.id
|
a.id
|
||||||
,ActorName
|
,ActorName
|
||||||
,FORMAT(PrintDate, 'yyyy-MM-dd HH:mm') as printDate
|
--,FORMAT(l.PrintDate, 'yyyy-MM-dd HH:mm') as printDate
|
||||||
|
,Format(COALESCE(l.PrintDate, e.ProductionDate), 'yyyy-MM-dd HH:mm') as printDate
|
||||||
,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime
|
,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime
|
||||||
,l.ArticleHumanReadableId as av
|
,COALESCE(l.ArticleHumanReadableId,e.ArticleHumanReadableId) as av
|
||||||
,l.ArticleDescription as alias
|
,COALESCE(l.ArticleDescription, av.Name) as alias
|
||||||
,PrintedCopies
|
,COALESCE(l.PrintedCopies, 0) as PrintedCopies
|
||||||
,p.name as printerName
|
,COALESCE(p.name,'External Label not tracked') as printerName
|
||||||
,RunningNumber
|
,COALESCE(l.RunningNumber, e.RunningNumber) as runningNumber
|
||||||
--,*
|
--,*
|
||||||
FROM [support].[AuditLog] (nolock) as a
|
FROM [support].[AuditLog] (nolock) as a
|
||||||
|
|
||||||
@@ -18,10 +19,20 @@ left join
|
|||||||
[labelling].[InternalLabel] (nolock) as l on
|
[labelling].[InternalLabel] (nolock) as l on
|
||||||
l.id = JSON_VALUE(content, '$.EntityId')
|
l.id = JSON_VALUE(content, '$.EntityId')
|
||||||
|
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP 1 *
|
||||||
|
FROM labelling.ExternalLabel e
|
||||||
|
WHERE e.id = JSON_VALUE(a.content, '$.EntityId')
|
||||||
|
ORDER BY e.Id DESC
|
||||||
|
) e
|
||||||
|
|
||||||
left join
|
left join
|
||||||
[masterData].[printer] (nolock) as p on
|
[masterData].[printer] (nolock) as p on
|
||||||
p.id = l.PrinterId
|
p.id = l.PrinterId
|
||||||
|
|
||||||
|
left join
|
||||||
|
[masterData].[article] (nolock) as av on
|
||||||
|
av.HumanReadableId = e.ArticleHumanReadableId
|
||||||
where message like '%reprint%'
|
where message like '%reprint%'
|
||||||
and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET())
|
and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET())
|
||||||
and a.id > [ignoreList]
|
and a.id > [ignoreList]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
|
|||||||
|
|
||||||
export const setupRoutes = (baseUrl: string, app: Express) => {
|
export const setupRoutes = (baseUrl: string, app: Express) => {
|
||||||
//routes that are on by default
|
//routes that are on by default
|
||||||
|
setupMobileRoutes(baseUrl, app);
|
||||||
setupSystemRoutes(baseUrl, app);
|
setupSystemRoutes(baseUrl, app);
|
||||||
setupAdminRoutes(baseUrl, app);
|
setupAdminRoutes(baseUrl, app);
|
||||||
setupApiDocsRoutes(baseUrl, app);
|
setupApiDocsRoutes(baseUrl, app);
|
||||||
@@ -28,5 +29,4 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
|
|||||||
setupNotificationRoutes(baseUrl, app);
|
setupNotificationRoutes(baseUrl, app);
|
||||||
setupOCPRoutes(baseUrl, app);
|
setupOCPRoutes(baseUrl, app);
|
||||||
setupTCPRoutes(baseUrl, app);
|
setupTCPRoutes(baseUrl, app);
|
||||||
setupMobileRoutes(baseUrl, app);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { connectGPSql } from "./gpSql/gpSqlConnection.controller.js";
|
|||||||
import { createLogger } from "./logger/logger.controller.js";
|
import { createLogger } from "./logger/logger.controller.js";
|
||||||
import { historicalSchedule } from "./logistics/logistics.historicalInv.js";
|
import { historicalSchedule } from "./logistics/logistics.historicalInv.js";
|
||||||
import { startNotifications } from "./notification/notification.controller.js";
|
import { startNotifications } from "./notification/notification.controller.js";
|
||||||
|
import { sqlJobCleanUp } from "./notification/notification.SqlJobCleanUp.js";
|
||||||
import { createNotifications } from "./notification/notifications.master.js";
|
import { createNotifications } from "./notification/notifications.master.js";
|
||||||
import { printerSync } from "./ocp/ocp.printer.manage.js";
|
import { printerSync } from "./ocp/ocp.printer.manage.js";
|
||||||
import { monitorReleaseChanges } from "./opendock/openDockRreleaseMonitor.utils.js";
|
import { monitorReleaseChanges } from "./opendock/openDockRreleaseMonitor.utils.js";
|
||||||
@@ -83,6 +84,9 @@ const start = async () => {
|
|||||||
startNotifications();
|
startNotifications();
|
||||||
serversChecks();
|
serversChecks();
|
||||||
aggregateRouteHitsForBusinessDay();
|
aggregateRouteHitsForBusinessDay();
|
||||||
|
|
||||||
|
// can be removed at a later date
|
||||||
|
sqlJobCleanUp();
|
||||||
}, 5 * 1000);
|
}, 5 * 1000);
|
||||||
|
|
||||||
process.on("uncaughtException", async (err) => {
|
process.on("uncaughtException", async (err) => {
|
||||||
|
|||||||
@@ -346,6 +346,17 @@ const newSettings: NewSetting[] = [
|
|||||||
roles: ["admin"],
|
roles: ["admin"],
|
||||||
seedVersion: 1,
|
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 () => {
|
export const baseSettingValidationCheck = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import getServers from "./serverData.route.js";
|
import getServers from "./serverData.route.js";
|
||||||
import getSettings from "./settings.route.js";
|
import getSettings from "./settings.route.js";
|
||||||
import updSetting from "./settingsUpdate.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) => {
|
export const setupSystemRoutes = (baseUrl: string, app: Express) => {
|
||||||
//stats will be like this as we dont need to change this
|
//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/stats`, stats);
|
||||||
app.use(`${baseUrl}/api/settings`, getSettings);
|
app.use(`${baseUrl}/api/settings`, getSettings);
|
||||||
app.use(`${baseUrl}/api/servers`, getServers);
|
app.use(`${baseUrl}/api/servers`, getServers);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import restart from "./tcpRestart.route.js";
|
import restart from "./tcpRestart.route.js";
|
||||||
import start from "./tcpStart.route.js";
|
import start from "./tcpStart.route.js";
|
||||||
import stop from "./tcpStop.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) => {
|
export const setupTCPRoutes = (baseUrl: string, app: Express) => {
|
||||||
//stats will be like this as we dont need to change this
|
//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/start`, requireAuth, start);
|
||||||
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, routeHitMiddleware, stop);
|
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, stop);
|
||||||
app.use(
|
app.use(
|
||||||
`${baseUrl}/api/tcp/restart`,
|
`${baseUrl}/api/tcp/restart`,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
routeHitMiddleware,
|
|
||||||
restart,
|
restart,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export async function aggregateRouteHitsForBusinessDay() {
|
|||||||
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
businessDate: sql<string>`${businessDate}`,
|
businessDate: sql<string>`CAST(${businessDate} AS date)`,
|
||||||
method: analytics.method,
|
method: analytics.method,
|
||||||
routePattern: analytics.routePattern,
|
routePattern: analytics.routePattern,
|
||||||
module: sql<string>`COALESCE(${analytics.module}, 'unknown')`,
|
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
|
await db
|
||||||
.insert(analyticsDaily)
|
.insert(analyticsDaily)
|
||||||
.values(rows)
|
.values(values)
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: [
|
target: [
|
||||||
analyticsDaily.businessDate,
|
analyticsDaily.businessDate,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { createAccessControl } from "better-auth/plugins/access";
|
import { createAccessControl } from "better-auth/plugins/access";
|
||||||
|
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
|
||||||
|
|
||||||
export const statement = {
|
export const statement = {
|
||||||
|
...defaultStatements,
|
||||||
app: ["read", "create", "share", "update", "delete", "readAll"],
|
app: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
user: ["ban"],
|
|
||||||
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
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"],
|
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -14,13 +17,22 @@ export const user = ac.newRole({
|
|||||||
notifications: ["read", "create"],
|
notifications: ["read", "create"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const manager = ac.newRole({
|
||||||
|
app: ["read", "create", "update"],
|
||||||
|
mobile: ["read", "create", "update"],
|
||||||
|
});
|
||||||
|
|
||||||
export const admin = ac.newRole({
|
export const admin = ac.newRole({
|
||||||
app: ["read", "create", "update"],
|
app: ["read", "create", "update"],
|
||||||
|
mobile: ["read", "create", "update"],
|
||||||
|
user: ["create", "update"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const systemAdmin = ac.newRole({
|
export const systemAdmin = ac.newRole({
|
||||||
|
...adminAc.statements,
|
||||||
app: ["read", "create", "share", "update", "delete", "readAll"],
|
app: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
user: ["ban"],
|
|
||||||
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
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"],
|
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
//import { eq } from "drizzle-orm";
|
//import { eq } from "drizzle-orm";
|
||||||
import { db } from "../db/db.controller.js";
|
import { db } from "../db/db.controller.js";
|
||||||
import * as rawSchema from "../db/schema/auth.schema.js";
|
import * as rawSchema from "../db/schema/auth.schema.js";
|
||||||
import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
|
import { ac, admin, manager, systemAdmin, user } from "./auth.permissions.js";
|
||||||
import { allowedOrigins } from "./cors.utils.js";
|
import { allowedOrigins } from "./cors.utils.js";
|
||||||
import { sendEmail } from "./sendEmail.utils.js";
|
import { sendEmail } from "./sendEmail.utils.js";
|
||||||
|
|
||||||
@@ -163,6 +163,7 @@ export const auth = betterAuth({
|
|||||||
roles: {
|
roles: {
|
||||||
admin,
|
admin,
|
||||||
user,
|
user,
|
||||||
|
manager,
|
||||||
systemAdmin,
|
systemAdmin,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function generateUniquePin() {
|
|||||||
const pin = generateSixDigitPin();
|
const pin = generateSixDigitPin();
|
||||||
|
|
||||||
const existing = await db.query.scanUser.findFirst({
|
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)
|
if (!existing)
|
||||||
@@ -37,3 +37,13 @@ export async function generateUniquePin() {
|
|||||||
room: "",
|
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;
|
||||||
|
// };
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
|
|
||||||
import getActiveJobs from "./cronerActiveJobs.route.js";
|
import getActiveJobs from "./cronerActiveJobs.route.js";
|
||||||
import jobStatusChange from "./cronerStatusChange.route.js";
|
import jobStatusChange from "./cronerStatusChange.route.js";
|
||||||
export const setupUtilsRoutes = (baseUrl: string, app: Express) => {
|
export const setupUtilsRoutes = (baseUrl: string, app: Express) => {
|
||||||
app.use(routeHitMiddleware);
|
|
||||||
app.use(`${baseUrl}/api/utils/croner`, getActiveJobs);
|
app.use(`${baseUrl}/api/utils/croner`, getActiveJobs);
|
||||||
app.use(`${baseUrl}/api/utils/croner`, jobStatusChange);
|
app.use(`${baseUrl}/api/utils/croner`, jobStatusChange);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Logistics Support Tool</title>
|
<title>Logistics Support Tool</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const configScript = document.createElement("script");
|
const configScript = document.createElement("script");
|
||||||
configScript.src = `${window.location.origin}/lst/api/lst-config.js`;
|
configScript.src = `${window.location.origin}/lst/api/lst-config.js`;
|
||||||
|
|||||||
BIN
frontend/public/imgs/docs/mobile/critical_update.png
Normal file
|
After Width: | Height: | Size: 483 KiB |
BIN
frontend/public/imgs/docs/mobile/ehs_homeScreen.png
Normal file
|
After Width: | Height: | Size: 613 KiB |
BIN
frontend/public/imgs/docs/mobile/ehs_menu.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
frontend/public/imgs/docs/mobile/stagenow.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
frontend/public/imgs/docs/mobile/test2-1.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
frontend/public/imgs/docs/mobile/test2-2.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
frontend/public/imgs/docs/mobile/test2-3.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/public/imgs/docs/mobile/tools.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
frontend/public/imgs/docs/mobile/update.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
frontend/public/imgs/docs/mobile/usday1-1.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
frontend/public/imgs/docs/mobile/usday1-2.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
frontend/public/imgs/docs/mobile/usday1-3.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/public/imgs/docs/mobile/usweb1-1.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
frontend/public/imgs/docs/mobile/usweb1-2.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
frontend/public/imgs/docs/mobile/usweb1-3.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/public/stage-now/test2-stageNow.pdf
Normal file
BIN
frontend/public/stage-now/usday1-stageNow.pdf
Normal file
BIN
frontend/public/stage-now/usweb1-stageNow.pdf
Normal file
@@ -19,11 +19,14 @@ export default function Header() {
|
|||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { signOut } = authClient;
|
const { signOut } = authClient;
|
||||||
const router = useRouterState();
|
const router = useRouterState();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const currentPath = router.location.href;
|
const currentPath = router.location.href;
|
||||||
|
|
||||||
return (
|
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 justify-between w-full">
|
||||||
<div className="flex items-center gap-2 px-4">
|
<div className="flex items-center gap-2 px-4">
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
@@ -48,6 +51,20 @@ export default function Header() {
|
|||||||
<span className="font-semibold text-2xl">Logistics Support Tool</span>
|
<span className="font-semibold text-2xl">Logistics Support Tool</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="m-1 flex gap-1">
|
<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>
|
<div>
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
51
frontend/src/components/NotFound.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { Bell, Logs, Server, Settings, UsersRound } from "lucide-react";
|
import { Bell, Logs, Server, Settings, UsersRound } from "lucide-react";
|
||||||
|
import { getSettings } from "../../lib/queries/getSettings";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
|
|
||||||
export default function AdminSidebar({ session }: any) {
|
export default function AdminSidebar({ session }: any) {
|
||||||
const { setOpen } = useSidebar();
|
const { setOpen } = useSidebar();
|
||||||
|
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
title: "Notifications",
|
title: "Notifications",
|
||||||
@@ -68,9 +70,11 @@ export default function AdminSidebar({ session }: any) {
|
|||||||
title: "Scan users",
|
title: "Scan users",
|
||||||
url: "/admin/scanUsers",
|
url: "/admin/scanUsers",
|
||||||
icon: UsersRound,
|
icon: UsersRound,
|
||||||
role: ["systemAdmin", "admin"],
|
role: ["systemAdmin", "admin", "manager"],
|
||||||
module: "admin",
|
module: "admin",
|
||||||
active: true,
|
active:
|
||||||
|
!isLoading &&
|
||||||
|
settings.filter((n: any) => n.name === "mobile")[0].active,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
@@ -79,9 +83,9 @@ export default function AdminSidebar({ session }: any) {
|
|||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<>
|
<div key={item.title}>
|
||||||
{item.role.includes(session.user.role) && (
|
{item.role.includes(session.user.role) && item.active && (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarMenuButton asChild>
|
||||||
<Link to={item.url} onClick={() => setOpen(false)}>
|
<Link to={item.url} onClick={() => setOpen(false)}>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
@@ -90,7 +94,7 @@ export default function AdminSidebar({ session }: any) {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { Link, useRouterState } from "@tanstack/react-router";
|
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 {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "../ui/collapsible";
|
} from "../ui/collapsible";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
@@ -19,43 +20,55 @@ import {
|
|||||||
useSidebar,
|
useSidebar,
|
||||||
} from "../ui/sidebar";
|
} 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() {
|
export default function DocBar() {
|
||||||
const { setOpen } = useSidebar();
|
const { setOpen } = useSidebar();
|
||||||
|
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
|
||||||
const pathname = useRouterState({
|
const pathname = useRouterState({
|
||||||
select: (s) => s.location.pathname,
|
select: (s) => s.location.pathname,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isNotifications = pathname.includes("notifications");
|
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 (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Docs</SidebarGroupLabel>
|
<SidebarGroupLabel>Docs</SidebarGroupLabel>
|
||||||
@@ -72,42 +85,44 @@ export default function DocBar() {
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{docs.map((item) => (
|
{docs.map((item) => (
|
||||||
<Collapsible
|
<div key={item.title}>
|
||||||
key={item.title}
|
{item.isActive && (
|
||||||
asChild
|
<Collapsible
|
||||||
defaultOpen={isNotifications}
|
asChild
|
||||||
className="group/collapsible"
|
defaultOpen={isNotifications}
|
||||||
>
|
className="group/collapsible"
|
||||||
<SidebarMenuItem>
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
<CollapsibleTrigger asChild>
|
||||||
<Link
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
to={"/docs/$"}
|
<Link to={"/docs/$"} params={{ _splat: `${item.url}` }}>
|
||||||
params={{ _splat: `notifications${item.url}` }}
|
{item.title}
|
||||||
>
|
</Link>
|
||||||
{item.title}
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
</Link>
|
</SidebarMenuButton>
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
</CollapsibleTrigger>
|
||||||
</SidebarMenuButton>
|
<CollapsibleContent>
|
||||||
</CollapsibleTrigger>
|
<SidebarMenuSub>
|
||||||
<CollapsibleContent>
|
{item.items?.map((subItem) => (
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
{item.items?.map((subItem) => (
|
<SidebarMenuSubButton asChild>
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
<Link
|
||||||
<SidebarMenuSubButton asChild>
|
to={"/docs/$"}
|
||||||
<Link
|
params={{ _splat: `${subItem.url}` }}
|
||||||
to={"/docs/$"}
|
onClick={() => setOpen(false)}
|
||||||
params={{ _splat: `notifications${subItem.url}` }}
|
>
|
||||||
>
|
<subItem.icon />
|
||||||
{subItem.title}
|
<span>{subItem.title}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { ScanText, ScrollText } from "lucide-react";
|
import { ScanText } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
@@ -10,14 +10,14 @@ import {
|
|||||||
useSidebar,
|
useSidebar,
|
||||||
} from "../ui/sidebar";
|
} from "../ui/sidebar";
|
||||||
|
|
||||||
export default function MobileBar({ session }: any) {
|
export default function MobileBar() {
|
||||||
const { setOpen } = useSidebar();
|
const { setOpen } = useSidebar();
|
||||||
const items = [
|
const items = [
|
||||||
{
|
// {
|
||||||
title: "Update Instructions",
|
// title: "Update Instructions",
|
||||||
url: "/",
|
// url: "/",
|
||||||
icon: ScrollText,
|
// icon: ScrollText,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: "Scan Log",
|
title: "Scan Log",
|
||||||
url: "/",
|
url: "/",
|
||||||
@@ -25,8 +25,6 @@ export default function MobileBar({ session }: any) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log(session);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Mobile</SidebarGroupLabel>
|
<SidebarGroupLabel>Mobile</SidebarGroupLabel>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -6,12 +7,14 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
|
import { getSettings } from "../../lib/queries/getSettings";
|
||||||
import AdminSidebar from "./AdminBar";
|
import AdminSidebar from "./AdminBar";
|
||||||
import DocBar from "./DocBar";
|
import DocBar from "./DocBar";
|
||||||
import MobileBar from "./MobileBar";
|
import MobileBar from "./MobileBar";
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
@@ -24,10 +27,15 @@ export function AppSidebar() {
|
|||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<DocBar />
|
<DocBar />
|
||||||
<MobileBar session={session} />
|
{!isLoading &&
|
||||||
|
settings.filter((n: any) => n.name === "mobile")[0].active && (
|
||||||
|
<MobileBar />
|
||||||
|
)}
|
||||||
|
|
||||||
{session &&
|
{session &&
|
||||||
(session.user.role === "admin" ||
|
(session.user.role === "admin" ||
|
||||||
session.user.role === "systemAdmin") && (
|
session.user.role === "systemAdmin" ||
|
||||||
|
session.user.role === "manager") && (
|
||||||
<AdminSidebar session={session} />
|
<AdminSidebar session={session} />
|
||||||
)}
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|||||||
@@ -1,64 +1,67 @@
|
|||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import * as React from "react"
|
||||||
import { Slot } from "radix-ui";
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import type * as React from "react";
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
destructive:
|
outline:
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
"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",
|
||||||
outline:
|
secondary:
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
secondary:
|
ghost:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
ghost:
|
destructive:
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"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",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
default:
|
||||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
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",
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
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",
|
||||||
icon: "size-9",
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
icon: "size-8",
|
||||||
"icon-sm": "size-8",
|
"icon-xs":
|
||||||
"icon-lg": "size-10",
|
"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",
|
||||||
defaultVariants: {
|
"icon-lg": "size-9",
|
||||||
variant: "default",
|
},
|
||||||
size: "default",
|
},
|
||||||
},
|
defaultVariants: {
|
||||||
},
|
variant: "default",
|
||||||
);
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
size = "default",
|
size = "default",
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot.Root : "button";
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button, buttonVariants }
|
||||||
|
|||||||
166
frontend/src/components/ui/dialog.tsx
Normal 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,
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||||
import type * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ function TooltipContent({
|
|||||||
data-slot="tooltip-content"
|
data-slot="tooltip-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -52,4 +51,4 @@ function TooltipContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
||||||
|
|||||||
137
frontend/src/docs/mobile/updateInstructions.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function updateInstructions() {
|
|
||||||
return <div>updateInstructions</div>;
|
|
||||||
}
|
|
||||||
47
frontend/src/lib/apiHelper.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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 (error.response?.status === 401) {
|
||||||
|
// redirect, toast, or show forbidden page
|
||||||
|
toast.error("Unauthorized to be here");
|
||||||
|
|
||||||
|
appRouter?.navigate({ to: "/login", replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNetworkError) {
|
||||||
|
appRouter?.navigate({ to: "/app-down", replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
|
import { redirect } from "@tanstack/react-router";
|
||||||
|
import {
|
||||||
|
adminClient,
|
||||||
|
genericOAuthClient,
|
||||||
|
usernameClient,
|
||||||
|
} from "better-auth/client/plugins";
|
||||||
|
|
||||||
import { createAuthClient } from "better-auth/react";
|
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({
|
export const authClient = createAuthClient({
|
||||||
baseURL: `${window.location.origin}/lst/api/auth`,
|
baseURL: `${window.location.origin}/lst/api/auth`,
|
||||||
@@ -10,11 +16,21 @@ export const authClient = createAuthClient({
|
|||||||
roles: {
|
roles: {
|
||||||
admin,
|
admin,
|
||||||
user,
|
user,
|
||||||
|
manager,
|
||||||
systemAdmin,
|
systemAdmin,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
genericOAuthClient(),
|
genericOAuthClient(),
|
||||||
|
usernameClient(),
|
||||||
],
|
],
|
||||||
|
fetchOptions: {
|
||||||
|
onError() {
|
||||||
|
redirect({
|
||||||
|
to: "/app-down",
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useSession, signUp, signIn, signOut } = authClient;
|
export const { useSession, signUp, signIn, signOut } = authClient;
|
||||||
|
|||||||
@@ -1,21 +1,71 @@
|
|||||||
import { createAccessControl } from "better-auth/plugins/access";
|
import { createAccessControl } from "better-auth/plugins/access";
|
||||||
|
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
|
||||||
|
|
||||||
|
/*
|
||||||
|
When new perms are added based on there criteria make sure they are added here as well
|
||||||
|
*/
|
||||||
|
|
||||||
|
type SelectableRole = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectableRoles: SelectableRole[] = [
|
||||||
|
{ label: "User", value: "user" },
|
||||||
|
{ label: "Manager", value: "manager" },
|
||||||
|
{ label: "Admin", value: "admin" },
|
||||||
|
{ label: "System Admin", value: "systemAdmin" },
|
||||||
|
];
|
||||||
|
|
||||||
export const statement = {
|
export const statement = {
|
||||||
project: ["create", "share", "update", "delete"],
|
...defaultStatements,
|
||||||
user: ["ban"],
|
app: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
|
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;
|
} as const;
|
||||||
|
|
||||||
export const ac = createAccessControl(statement);
|
export const ac = createAccessControl(statement);
|
||||||
|
|
||||||
export const user = ac.newRole({
|
export const user = ac.newRole({
|
||||||
project: ["create"],
|
app: ["read", "create"],
|
||||||
|
notifications: ["read", "create"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const manager = ac.newRole({
|
||||||
|
app: ["read", "create", "update"],
|
||||||
|
mobile: ["read", "create", "update"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const admin = ac.newRole({
|
export const admin = ac.newRole({
|
||||||
project: ["create", "update"],
|
app: ["read", "create", "update"],
|
||||||
|
mobile: ["read", "create", "update"],
|
||||||
|
user: ["create", "update"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const systemAdmin = ac.newRole({
|
export const systemAdmin = ac.newRole({
|
||||||
project: ["create", "update", "delete"],
|
...adminAc.statements,
|
||||||
user: ["ban"],
|
app: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
|
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"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* 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"]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ const docsMap: Record<string, ComponentType> = {};
|
|||||||
for (const path in modules) {
|
for (const path in modules) {
|
||||||
const mod = modules[path] as DocModule;
|
const mod = modules[path] as DocModule;
|
||||||
|
|
||||||
const slug = path
|
const slug = path.replace("../docs/", "").replace(".tsx", "");
|
||||||
.replace("../docs/", "")
|
|
||||||
.replace(".tsx", "");
|
|
||||||
|
|
||||||
// "notifications/intro"
|
// "notifications/intro"
|
||||||
docsMap[slug] = mod.default;
|
docsMap[slug] = mod.default;
|
||||||
@@ -23,4 +21,4 @@ for (const path in modules) {
|
|||||||
|
|
||||||
export function getDoc(slug: string) {
|
export function getDoc(slug: string) {
|
||||||
return docsMap[slug];
|
return docsMap[slug];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import { api } from "../apiHelper";
|
||||||
|
|
||||||
export function getScanUsers() {
|
export function getScanUsers() {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryKey: ["getScanUsers"],
|
queryKey: ["getScanUsers"],
|
||||||
queryFn: () => fetch(),
|
queryFn: () => dataFetch(),
|
||||||
staleTime: 5000,
|
staleTime: 5000,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetch = async () => {
|
const dataFetch = async () => {
|
||||||
if (window.location.hostname === "localhost") {
|
if (window.location.hostname === "localhost") {
|
||||||
await new Promise((res) => setTimeout(res, 1500));
|
await new Promise((res) => setTimeout(res, 1500));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await axios.get("/lst/api/mobile/auth/user", {
|
const { data } = await api.get("/mobile/auth/user");
|
||||||
withCredentials: true,
|
if (!data.success) {
|
||||||
timeout: 5000,
|
throw new Error(data.message ?? "Failed to load scan users");
|
||||||
});
|
}
|
||||||
|
|
||||||
return data.data;
|
return data.data ?? [];
|
||||||
};
|
};
|
||||||
|
|||||||
23
frontend/src/lib/queries/getScannerIds.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
|
||||||
|
import { api } from "../apiHelper";
|
||||||
|
|
||||||
export function getSettings() {
|
export function getSettings() {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
@@ -16,7 +17,7 @@ const fetch = async () => {
|
|||||||
await new Promise((res) => setTimeout(res, 1500));
|
await new Promise((res) => setTimeout(res, 1500));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await axios.get("/lst/api/settings");
|
const { data } = await api.get("/settings");
|
||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
};
|
};
|
||||||
|
|||||||
40
frontend/src/lib/queries/getUsers.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
|
||||||
|
import { api } from "../apiHelper";
|
||||||
|
|
||||||
export function notificationSubs(userId?: string) {
|
export function notificationSubs(userId?: string) {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
@@ -16,8 +17,8 @@ const fetch = async (userId?: string) => {
|
|||||||
await new Promise((res) => setTimeout(res, 1500));
|
await new Promise((res) => setTimeout(res, 1500));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await axios.get(
|
const { data } = await api.get(
|
||||||
`/lst/api/notification/sub${userId ? `?userId=${userId}` : ""}`,
|
`/notification/sub${userId ? `?userId=${userId}` : ""}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
|
||||||
|
import { api } from "../apiHelper";
|
||||||
|
|
||||||
export function notifications() {
|
export function notifications() {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
@@ -16,7 +17,7 @@ const fetch = async () => {
|
|||||||
await new Promise((res) => setTimeout(res, 1500));
|
await new Promise((res) => setTimeout(res, 1500));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await axios.get("/lst/api/notification");
|
const { data } = await api.get("/notification");
|
||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
};
|
};
|
||||||
|
|||||||
16
frontend/src/lib/queries/permsCheck.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
export function permissionQuery(permissions: Record<string, string[]>) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["permission", permissions],
|
||||||
|
queryFn: async () => {
|
||||||
|
const result = await authClient.admin.hasPermission({
|
||||||
|
permissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.data?.success ?? false;
|
||||||
|
},
|
||||||
|
staleTime: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
|
||||||
|
import { api } from "../apiHelper";
|
||||||
|
|
||||||
export function servers() {
|
export function servers() {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
@@ -16,7 +17,7 @@ const fetch = async () => {
|
|||||||
await new Promise((res) => setTimeout(res, 1500));
|
await new Promise((res) => setTimeout(res, 1500));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await axios.get("/lst/api/servers");
|
const { data } = await api.get("/servers");
|
||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ export default function SkellyTable({ rows = 5, columns = 4 }: TableSkelly) {
|
|||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{Array.from({ length: columns }).map((_, i) => (
|
<TableRow>
|
||||||
<TableHead key={i}>
|
{Array.from({ length: columns }).map((_, i) => (
|
||||||
<Skeleton className="h-4 w-[80px]" />
|
<TableHead key={i}>
|
||||||
</TableHead>
|
<Skeleton className="h-4 w-[80px]" />
|
||||||
))}
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{Array.from({ length: rows }).map((_, r) => (
|
{Array.from({ length: rows }).map((_, r) => (
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { StrictMode } from "react";
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||||
|
import NotFound from "./components/NotFound";
|
||||||
|
import { setApiRouter } from "./lib/apiHelper";
|
||||||
import socket from "./lib/socket.io";
|
import socket from "./lib/socket.io";
|
||||||
import { loadUmami } from "./lib/umami.utils";
|
import { loadUmami } from "./lib/umami.utils";
|
||||||
// Import the generated route tree
|
// Import the generated route tree
|
||||||
@@ -13,8 +15,9 @@ const queryClient = new QueryClient({
|
|||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
retry: 0,
|
retry: 2,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
|
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 5000),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -27,8 +30,11 @@ const router = createRouter({
|
|||||||
context: {
|
context: {
|
||||||
queryClient,
|
queryClient,
|
||||||
},
|
},
|
||||||
|
defaultNotFoundComponent: NotFound,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setApiRouter(router);
|
||||||
|
|
||||||
// Register the router instance for type safety
|
// Register the router instance for type safety
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
interface Register {
|
interface Register {
|
||||||
|
|||||||
@@ -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.
|
// 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 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 AboutRouteImport } from './routes/about'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as DocsIndexRouteImport } from './routes/docs/index'
|
import { Route as DocsIndexRouteImport } from './routes/docs/index'
|
||||||
import { Route as DocsSplatRouteImport } from './routes/docs/$'
|
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 AdminSettingsRouteImport } from './routes/admin/settings'
|
||||||
import { Route as AdminServersRouteImport } from './routes/admin/servers'
|
import { Route as AdminServersRouteImport } from './routes/admin/servers'
|
||||||
import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
|
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 authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
|
||||||
import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile'
|
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({
|
const AboutRoute = AboutRouteImport.update({
|
||||||
id: '/about',
|
id: '/about',
|
||||||
path: '/about',
|
path: '/about',
|
||||||
@@ -43,6 +56,11 @@ const DocsSplatRoute = DocsSplatRouteImport.update({
|
|||||||
path: '/docs/$',
|
path: '/docs/$',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AdminUsersRoute = AdminUsersRouteImport.update({
|
||||||
|
id: '/admin/users',
|
||||||
|
path: '/admin/users',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const AdminSettingsRoute = AdminSettingsRouteImport.update({
|
const AdminSettingsRoute = AdminSettingsRouteImport.update({
|
||||||
id: '/admin/settings',
|
id: '/admin/settings',
|
||||||
path: '/admin/settings',
|
path: '/admin/settings',
|
||||||
@@ -92,12 +110,15 @@ const authUserProfileRoute = authUserProfileRouteImport.update({
|
|||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
|
'/app-down': typeof AppDownRoute
|
||||||
|
'/forbidden': typeof ForbiddenRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
'/admin/notifications': typeof AdminNotificationsRoute
|
'/admin/notifications': typeof AdminNotificationsRoute
|
||||||
'/admin/scanUsers': typeof AdminScanUsersRoute
|
'/admin/scanUsers': typeof AdminScanUsersRoute
|
||||||
'/admin/servers': typeof AdminServersRoute
|
'/admin/servers': typeof AdminServersRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/docs/$': typeof DocsSplatRoute
|
'/docs/$': typeof DocsSplatRoute
|
||||||
'/docs/': typeof DocsIndexRoute
|
'/docs/': typeof DocsIndexRoute
|
||||||
'/user/profile': typeof authUserProfileRoute
|
'/user/profile': typeof authUserProfileRoute
|
||||||
@@ -107,12 +128,15 @@ export interface FileRoutesByFullPath {
|
|||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
|
'/app-down': typeof AppDownRoute
|
||||||
|
'/forbidden': typeof ForbiddenRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
'/admin/notifications': typeof AdminNotificationsRoute
|
'/admin/notifications': typeof AdminNotificationsRoute
|
||||||
'/admin/scanUsers': typeof AdminScanUsersRoute
|
'/admin/scanUsers': typeof AdminScanUsersRoute
|
||||||
'/admin/servers': typeof AdminServersRoute
|
'/admin/servers': typeof AdminServersRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/docs/$': typeof DocsSplatRoute
|
'/docs/$': typeof DocsSplatRoute
|
||||||
'/docs': typeof DocsIndexRoute
|
'/docs': typeof DocsIndexRoute
|
||||||
'/user/profile': typeof authUserProfileRoute
|
'/user/profile': typeof authUserProfileRoute
|
||||||
@@ -123,12 +147,15 @@ export interface FileRoutesById {
|
|||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
|
'/app-down': typeof AppDownRoute
|
||||||
|
'/forbidden': typeof ForbiddenRoute
|
||||||
'/(auth)/login': typeof authLoginRoute
|
'/(auth)/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
'/admin/notifications': typeof AdminNotificationsRoute
|
'/admin/notifications': typeof AdminNotificationsRoute
|
||||||
'/admin/scanUsers': typeof AdminScanUsersRoute
|
'/admin/scanUsers': typeof AdminScanUsersRoute
|
||||||
'/admin/servers': typeof AdminServersRoute
|
'/admin/servers': typeof AdminServersRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/docs/$': typeof DocsSplatRoute
|
'/docs/$': typeof DocsSplatRoute
|
||||||
'/docs/': typeof DocsIndexRoute
|
'/docs/': typeof DocsIndexRoute
|
||||||
'/(auth)/user/profile': typeof authUserProfileRoute
|
'/(auth)/user/profile': typeof authUserProfileRoute
|
||||||
@@ -140,12 +167,15 @@ export interface FileRouteTypes {
|
|||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/about'
|
| '/about'
|
||||||
|
| '/app-down'
|
||||||
|
| '/forbidden'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/admin/logs'
|
| '/admin/logs'
|
||||||
| '/admin/notifications'
|
| '/admin/notifications'
|
||||||
| '/admin/scanUsers'
|
| '/admin/scanUsers'
|
||||||
| '/admin/servers'
|
| '/admin/servers'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
|
| '/admin/users'
|
||||||
| '/docs/$'
|
| '/docs/$'
|
||||||
| '/docs/'
|
| '/docs/'
|
||||||
| '/user/profile'
|
| '/user/profile'
|
||||||
@@ -155,12 +185,15 @@ export interface FileRouteTypes {
|
|||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/about'
|
| '/about'
|
||||||
|
| '/app-down'
|
||||||
|
| '/forbidden'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/admin/logs'
|
| '/admin/logs'
|
||||||
| '/admin/notifications'
|
| '/admin/notifications'
|
||||||
| '/admin/scanUsers'
|
| '/admin/scanUsers'
|
||||||
| '/admin/servers'
|
| '/admin/servers'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
|
| '/admin/users'
|
||||||
| '/docs/$'
|
| '/docs/$'
|
||||||
| '/docs'
|
| '/docs'
|
||||||
| '/user/profile'
|
| '/user/profile'
|
||||||
@@ -170,12 +203,15 @@ export interface FileRouteTypes {
|
|||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/about'
|
| '/about'
|
||||||
|
| '/app-down'
|
||||||
|
| '/forbidden'
|
||||||
| '/(auth)/login'
|
| '/(auth)/login'
|
||||||
| '/admin/logs'
|
| '/admin/logs'
|
||||||
| '/admin/notifications'
|
| '/admin/notifications'
|
||||||
| '/admin/scanUsers'
|
| '/admin/scanUsers'
|
||||||
| '/admin/servers'
|
| '/admin/servers'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
|
| '/admin/users'
|
||||||
| '/docs/$'
|
| '/docs/$'
|
||||||
| '/docs/'
|
| '/docs/'
|
||||||
| '/(auth)/user/profile'
|
| '/(auth)/user/profile'
|
||||||
@@ -186,12 +222,15 @@ export interface FileRouteTypes {
|
|||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AboutRoute: typeof AboutRoute
|
AboutRoute: typeof AboutRoute
|
||||||
|
AppDownRoute: typeof AppDownRoute
|
||||||
|
ForbiddenRoute: typeof ForbiddenRoute
|
||||||
authLoginRoute: typeof authLoginRoute
|
authLoginRoute: typeof authLoginRoute
|
||||||
AdminLogsRoute: typeof AdminLogsRoute
|
AdminLogsRoute: typeof AdminLogsRoute
|
||||||
AdminNotificationsRoute: typeof AdminNotificationsRoute
|
AdminNotificationsRoute: typeof AdminNotificationsRoute
|
||||||
AdminScanUsersRoute: typeof AdminScanUsersRoute
|
AdminScanUsersRoute: typeof AdminScanUsersRoute
|
||||||
AdminServersRoute: typeof AdminServersRoute
|
AdminServersRoute: typeof AdminServersRoute
|
||||||
AdminSettingsRoute: typeof AdminSettingsRoute
|
AdminSettingsRoute: typeof AdminSettingsRoute
|
||||||
|
AdminUsersRoute: typeof AdminUsersRoute
|
||||||
DocsSplatRoute: typeof DocsSplatRoute
|
DocsSplatRoute: typeof DocsSplatRoute
|
||||||
DocsIndexRoute: typeof DocsIndexRoute
|
DocsIndexRoute: typeof DocsIndexRoute
|
||||||
authUserProfileRoute: typeof authUserProfileRoute
|
authUserProfileRoute: typeof authUserProfileRoute
|
||||||
@@ -201,6 +240,20 @@ export interface RootRouteChildren {
|
|||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
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': {
|
'/about': {
|
||||||
id: '/about'
|
id: '/about'
|
||||||
path: '/about'
|
path: '/about'
|
||||||
@@ -229,6 +282,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DocsSplatRouteImport
|
preLoaderRoute: typeof DocsSplatRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/admin/users': {
|
||||||
|
id: '/admin/users'
|
||||||
|
path: '/admin/users'
|
||||||
|
fullPath: '/admin/users'
|
||||||
|
preLoaderRoute: typeof AdminUsersRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/admin/settings': {
|
'/admin/settings': {
|
||||||
id: '/admin/settings'
|
id: '/admin/settings'
|
||||||
path: '/admin/settings'
|
path: '/admin/settings'
|
||||||
@@ -298,12 +358,15 @@ declare module '@tanstack/react-router' {
|
|||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AboutRoute: AboutRoute,
|
AboutRoute: AboutRoute,
|
||||||
|
AppDownRoute: AppDownRoute,
|
||||||
|
ForbiddenRoute: ForbiddenRoute,
|
||||||
authLoginRoute: authLoginRoute,
|
authLoginRoute: authLoginRoute,
|
||||||
AdminLogsRoute: AdminLogsRoute,
|
AdminLogsRoute: AdminLogsRoute,
|
||||||
AdminNotificationsRoute: AdminNotificationsRoute,
|
AdminNotificationsRoute: AdminNotificationsRoute,
|
||||||
AdminScanUsersRoute: AdminScanUsersRoute,
|
AdminScanUsersRoute: AdminScanUsersRoute,
|
||||||
AdminServersRoute: AdminServersRoute,
|
AdminServersRoute: AdminServersRoute,
|
||||||
AdminSettingsRoute: AdminSettingsRoute,
|
AdminSettingsRoute: AdminSettingsRoute,
|
||||||
|
AdminUsersRoute: AdminUsersRoute,
|
||||||
DocsSplatRoute: DocsSplatRoute,
|
DocsSplatRoute: DocsSplatRoute,
|
||||||
DocsIndexRoute: DocsIndexRoute,
|
DocsIndexRoute: DocsIndexRoute,
|
||||||
authUserProfileRoute: authUserProfileRoute,
|
authUserProfileRoute: authUserProfileRoute,
|
||||||
|
|||||||
@@ -29,30 +29,43 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
|||||||
|
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: loginEmail,
|
login: loginEmail,
|
||||||
password: "",
|
password: "",
|
||||||
rememberMe: rememberMe,
|
rememberMe: rememberMe,
|
||||||
},
|
},
|
||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
// set remember me incase we want it later
|
// set remember me incase we want it later
|
||||||
|
const loginValue = value.login.trim();
|
||||||
|
const isEmailLogin = loginValue.includes("@");
|
||||||
|
|
||||||
if (value.rememberMe) {
|
if (value.rememberMe) {
|
||||||
localStorage.setItem("rememberMe", value.rememberMe.toString());
|
localStorage.setItem("rememberMe", value.rememberMe.toString());
|
||||||
localStorage.setItem("loginEmail", value.email.toLocaleLowerCase());
|
localStorage.setItem("loginEmail", loginValue.toLocaleLowerCase());
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("rememberMe");
|
localStorage.removeItem("rememberMe");
|
||||||
localStorage.removeItem("loginEmail");
|
localStorage.removeItem("loginEmail");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const login = await authClient.signIn.email({
|
const login = isEmailLogin
|
||||||
email: value.email,
|
? await authClient.signIn.email({
|
||||||
password: value.password,
|
email: loginValue.toLowerCase(),
|
||||||
fetchOptions: {
|
password: value.password,
|
||||||
onSuccess: () => {
|
fetchOptions: {
|
||||||
navigate({ to: redirectPath ?? "/" });
|
onSuccess: () => {
|
||||||
},
|
navigate({ to: redirectPath ?? "/" });
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
: await authClient.signIn.username({
|
||||||
|
username: loginValue,
|
||||||
|
password: value.password,
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate({ to: redirectPath ?? "/" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (login.error) {
|
if (login.error) {
|
||||||
toast.error(`${login.error?.message}`);
|
toast.error(`${login.error?.message}`);
|
||||||
@@ -95,11 +108,11 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
|
|||||||
form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<form.AppField name="email">
|
<form.AppField name="login">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
<field.InputField
|
<field.InputField
|
||||||
label="Email"
|
label="Username or Email Address"
|
||||||
inputType="email"
|
inputType="text"
|
||||||
required={rememberMe}
|
required={rememberMe}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ export default function NotificationsSubCard({ user }: any) {
|
|||||||
|
|
||||||
let n: any = [];
|
let n: any = [];
|
||||||
if (data) {
|
if (data) {
|
||||||
n = data.map((i: any) => ({
|
n = data
|
||||||
label: i.name,
|
.filter((n: any) => n.active)
|
||||||
value: i.id,
|
.map((i: any) => ({
|
||||||
}));
|
label: i.name,
|
||||||
|
value: i.id,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { authClient, useSession } from "@/lib/auth-client";
|
import { authClient, useSession } from "@/lib/auth-client";
|
||||||
import { useAppForm } from "@/lib/formSutff";
|
import { useAppForm } from "@/lib/formSutff";
|
||||||
|
|
||||||
import { Spinner } from "../../components/ui/spinner";
|
import { Spinner } from "../../components/ui/spinner";
|
||||||
import ChangePassword from "./-components/ChangePassword";
|
import ChangePassword from "./-components/ChangePassword";
|
||||||
import NotificationsSubCard from "./-components/NotificationsSubCard";
|
import NotificationsSubCard from "./-components/NotificationsSubCard";
|
||||||
@@ -37,6 +38,7 @@ export const Route = createFileRoute("/(auth)/user/profile")({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: session?.user.name,
|
name: session?.user.name,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { useAppForm } from "@/lib/formSutff";
|
import { useAppForm } from "@/lib/formSutff";
|
||||||
|
import { Separator } from "../../components/ui/separator";
|
||||||
|
|
||||||
export const Route = createFileRoute("/(auth)/user/signup")({
|
export const Route = createFileRoute("/(auth)/user/signup")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
@@ -22,6 +23,7 @@ function RouteComponent() {
|
|||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
|
username: "",
|
||||||
},
|
},
|
||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
if (value.password !== value.confirmPassword) {
|
if (value.password !== value.confirmPassword) {
|
||||||
@@ -33,6 +35,7 @@ function RouteComponent() {
|
|||||||
name: value.name,
|
name: value.name,
|
||||||
email: value.email,
|
email: value.email,
|
||||||
password: value.password,
|
password: value.password,
|
||||||
|
username: value.username ?? value.name,
|
||||||
callbackURL: `${window.location.origin}/lst/app`,
|
callbackURL: `${window.location.origin}/lst/app`,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +74,15 @@ function RouteComponent() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
|
<div className="m-2">
|
||||||
|
<p>Username is option if left blank it will be your name</p>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<form.AppField name="username">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField label="Username" inputType="text" />
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
<form.AppField name="email">
|
<form.AppField name="email">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Header from "@/components/Header";
|
|||||||
import { AppSidebar } from "@/components/Sidebar/sidebar";
|
import { AppSidebar } from "@/components/Sidebar/sidebar";
|
||||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { ThemeProvider } from "@/lib/theme-provider";
|
import { ThemeProvider } from "@/lib/theme-provider";
|
||||||
|
import { TooltipProvider } from "../components/ui/tooltip";
|
||||||
import { useSession } from "../lib/auth-client";
|
import { useSession } from "../lib/auth-client";
|
||||||
|
|
||||||
const RootLayout = () => {
|
const RootLayout = () => {
|
||||||
@@ -14,16 +15,17 @@ const RootLayout = () => {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SidebarProvider className="flex flex-col" defaultOpen={false}>
|
<SidebarProvider className="flex flex-col" defaultOpen={false}>
|
||||||
<Header />
|
<Header />
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className="relative min-h-[calc(100svh-var(--header-height))]">
|
||||||
|
<AppSidebar />
|
||||||
|
|
||||||
<div className="relative min-h-[calc(100svh-var(--header-height))]">
|
<main className="w-full p-4">
|
||||||
<AppSidebar />
|
<div className="mx-auto w-full max-w-7xl">
|
||||||
|
<Outlet />
|
||||||
<main className="w-full p-4">
|
</div>
|
||||||
<div className="mx-auto w-full max-w-7xl">
|
</main>
|
||||||
<Outlet />
|
</div>
|
||||||
</div>
|
</TooltipProvider>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster expand richColors closeButton />
|
<Toaster expand richColors closeButton />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
162
frontend/src/routes/admin/-components/NewScanUser.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
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 { api } from "../../../lib/apiHelper";
|
||||||
|
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 api.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
frontend/src/routes/admin/-components/Newuser.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
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 { authClient } from "../../../lib/auth-client";
|
||||||
|
import { selectableRoles } from "../../../lib/auth-permissions";
|
||||||
|
import { useAppForm } from "../../../lib/formSutff";
|
||||||
|
|
||||||
|
export default function NewUser({ refetch }: { refetch: any }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
role: "",
|
||||||
|
username: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
if (value.name === "" || value.email === "" || value.password === "") {
|
||||||
|
toast.error("Missing Mandatory data please try again ");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await authClient.admin.createUser({
|
||||||
|
email: value.email, // required
|
||||||
|
password: value.password, // required
|
||||||
|
name: value.name, // required
|
||||||
|
role: (value.role ?? "user") as any,
|
||||||
|
data: { username: value.username },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data?.user) {
|
||||||
|
toast.success(`${value.name}, was just created `);
|
||||||
|
form.reset();
|
||||||
|
setOpen(false);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeModel = (e: boolean) => {
|
||||||
|
setOpen(e);
|
||||||
|
|
||||||
|
if (!e) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openForm = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
<p>
|
||||||
|
Username can be your windows or anything, if you do not fill this
|
||||||
|
out your name is used as your username
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<form.AppField name="username">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField label="Username" inputType="text" />
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<form.AppField name="email">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Email"
|
||||||
|
inputType="email"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<form.AppField name="password">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Password"
|
||||||
|
inputType="text"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-32">
|
||||||
|
<form.AppField name="role">
|
||||||
|
{(field) => (
|
||||||
|
<field.SelectField
|
||||||
|
label="Roles"
|
||||||
|
placeholder="Select role"
|
||||||
|
options={selectableRoles}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-2 ">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Submit</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "../../components/ui/tooltip";
|
} from "../../components/ui/tooltip";
|
||||||
|
import { api } from "../../lib/apiHelper";
|
||||||
import { authClient } from "../../lib/auth-client";
|
import { authClient } from "../../lib/auth-client";
|
||||||
import { notificationSubs } from "../../lib/queries/notificationSubs";
|
import { notificationSubs } from "../../lib/queries/notificationSubs";
|
||||||
import { notifications } from "../../lib/queries/notifications";
|
import { notifications } from "../../lib/queries/notifications";
|
||||||
@@ -36,7 +37,7 @@ const updateNotifications = async (
|
|||||||
//console.log(id, data);
|
//console.log(id, data);
|
||||||
try {
|
try {
|
||||||
const res = await axios.patch(
|
const res = await axios.patch(
|
||||||
`/lst/api/notification/${id}`,
|
`/notification/${id}`,
|
||||||
{ interval: data.interval },
|
{ interval: data.interval },
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@@ -110,7 +111,7 @@ const NotificationTable = () => {
|
|||||||
|
|
||||||
const removeNotification = async (ns: any) => {
|
const removeNotification = async (ns: any) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.delete(`/lst/api/notification/sub`, {
|
const res = await api.delete(`/notification/sub`, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
data: {
|
data: {
|
||||||
userId: ns.userId,
|
userId: ns.userId,
|
||||||
@@ -168,7 +169,7 @@ const NotificationTable = () => {
|
|||||||
setActiveToggle(e);
|
setActiveToggle(e);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.patch(
|
const res = await api.patch(
|
||||||
`/lst/api/notification/${i.row.original.id}`,
|
`/lst/api/notification/${i.row.original.id}`,
|
||||||
{
|
{
|
||||||
active: !activeToggle,
|
active: !activeToggle,
|
||||||
|
|||||||
@@ -1,16 +1,267 @@
|
|||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
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 { api } from "../../lib/apiHelper";
|
||||||
|
import { authClient } from "../../lib/auth-client";
|
||||||
import { getScanUsers } from "../../lib/queries/getScanUsers";
|
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")({
|
export const Route = createFileRoute("/admin/scanUsers")({
|
||||||
|
beforeLoad: async ({ location }) => {
|
||||||
|
const { data: session } = await authClient.getSession();
|
||||||
|
//const allowedRole = ["systemAdmin", "admin", "manager"];
|
||||||
|
|
||||||
|
const canAccess = await authClient.admin.hasPermission({
|
||||||
|
permissions: {
|
||||||
|
mobile: ["create"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/",
|
||||||
|
search: {
|
||||||
|
redirect: location.href,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (!allowedRole.includes(session.user.role as string)) {
|
||||||
|
|
||||||
|
if (!canAccess) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: session.user };
|
||||||
|
},
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ScanUserTable = () => {
|
const updateSettings = async (
|
||||||
const { data } = useSuspenseQuery(getScanUsers());
|
id: string,
|
||||||
console.log(data);
|
data: Record<string, string | number | boolean | null>,
|
||||||
return <div>Hello "/admin/scanUsers"!</div>;
|
) => {
|
||||||
|
//console.log(id, data);
|
||||||
|
try {
|
||||||
|
const res = await axios.patch(`/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 api.get("/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 api.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() {
|
function RouteComponent() {
|
||||||
return <ScanUserTable />;
|
//const { data: session } = useSession();
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<SkellyTable />}>
|
||||||
|
<ScanUserTable />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
|
||||||
import { format } from "date-fns-tz";
|
import { format } from "date-fns-tz";
|
||||||
import { CircleFadingArrowUp, Trash } from "lucide-react";
|
import { CircleFadingArrowUp, Trash } from "lucide-react";
|
||||||
import { Suspense, useState } from "react";
|
import { Suspense, useState } from "react";
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "../../components/ui/tooltip";
|
} from "../../components/ui/tooltip";
|
||||||
import { useSocketRoom } from "../../hooks/socket.io.hook";
|
import { useSocketRoom } from "../../hooks/socket.io.hook";
|
||||||
|
import { api } from "../../lib/apiHelper";
|
||||||
import { authClient } from "../../lib/auth-client";
|
import { authClient } from "../../lib/auth-client";
|
||||||
import { servers } from "../../lib/queries/servers";
|
import { servers } from "../../lib/queries/servers";
|
||||||
import LstTable from "../../lib/tableStuff/LstTable";
|
import LstTable from "../../lib/tableStuff/LstTable";
|
||||||
@@ -55,7 +56,17 @@ const ServerTable = () => {
|
|||||||
<SearchableHeader column={column} title="Name" searchable={true} />
|
<SearchableHeader column={column} title="Name" searchable={true} />
|
||||||
),
|
),
|
||||||
filterFn: "includesString",
|
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", {
|
columnHelper.accessor("greatPlainsPlantCode", {
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
@@ -101,19 +112,20 @@ const ServerTable = () => {
|
|||||||
const [activeToggle, setActiveToggle] = useState(false);
|
const [activeToggle, setActiveToggle] = useState(false);
|
||||||
|
|
||||||
const onToggle = async () => {
|
const onToggle = async () => {
|
||||||
setActiveToggle(true);
|
|
||||||
toast.success(
|
toast.success(
|
||||||
`${i.row.original.name} just started the upgrade monitor logs for errors.`,
|
`${i.row.original.name} just started the upgrade monitor logs for errors.`,
|
||||||
);
|
);
|
||||||
|
setActiveToggle(activeToggle);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
const res = await api.post(
|
||||||
`/lst/api/admin/build/updateServer`,
|
`/admin/build/updateServer`,
|
||||||
{
|
{
|
||||||
server: i.row.original.server,
|
server: i.row.original.server,
|
||||||
destination: i.row.original.serverLoc,
|
destination: i.row.original.serverLoc,
|
||||||
token: i.row.original.plantToken,
|
token: i.row.original.plantToken,
|
||||||
},
|
},
|
||||||
{ withCredentials: true },
|
{ withCredentials: true, timeout: 5 * 60 * 1000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
@@ -208,8 +220,8 @@ function RouteComponent() {
|
|||||||
];
|
];
|
||||||
const triggerBuild = async () => {
|
const triggerBuild = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
const res = await api.post(
|
||||||
`/lst/api/admin/build/release`,
|
`/admin/build/release`,
|
||||||
|
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import { Suspense, useMemo } from "react";
|
import { Suspense, useMemo } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -24,6 +22,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "../../components/ui/tooltip";
|
} from "../../components/ui/tooltip";
|
||||||
|
import { api } from "../../lib/apiHelper";
|
||||||
import { authClient } from "../../lib/auth-client";
|
import { authClient } from "../../lib/auth-client";
|
||||||
import { getSettings } from "../../lib/queries/getSettings";
|
import { getSettings } from "../../lib/queries/getSettings";
|
||||||
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
|
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
|
||||||
@@ -48,7 +47,7 @@ const updateSettings = async (
|
|||||||
) => {
|
) => {
|
||||||
//console.log(id, data);
|
//console.log(id, data);
|
||||||
try {
|
try {
|
||||||
const res = await axios.patch(`/lst/api/settings/${id}`, data, {
|
const res = await api.patch(`/settings/${id}`, data, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
toast.success(`Setting just updated`);
|
toast.success(`Setting just updated`);
|
||||||
|
|||||||
337
frontend/src/routes/admin/users.tsx
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
import { useMutation, useQuery, 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 { KeyRound } from "lucide-react";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "../../components/ui/button";
|
||||||
|
import { Input } from "../../components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "../../components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "../../components/ui/tooltip";
|
||||||
|
import { authClient } from "../../lib/auth-client";
|
||||||
|
import { selectableRoles } from "../../lib/auth-permissions";
|
||||||
|
import { getUsers } from "../../lib/queries/getUsers";
|
||||||
|
import { permissionQuery } from "../../lib/queries/permsCheck";
|
||||||
|
import LstTable from "../../lib/tableStuff/LstTable";
|
||||||
|
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
|
||||||
|
import SkellyTable from "../../lib/tableStuff/SkellyTable";
|
||||||
|
import { trackLstEvent } from "../../lib/umami.utils";
|
||||||
|
import NewUser from "./-components/Newuser";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/admin/users")({
|
||||||
|
beforeLoad: async ({ location }) => {
|
||||||
|
const { data: session } = await authClient.getSession();
|
||||||
|
// const allowedRole = ["systemAdmin", "admin"];
|
||||||
|
const canAccess = await authClient.admin.hasPermission({
|
||||||
|
permissions: {
|
||||||
|
user: ["create"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/",
|
||||||
|
search: {
|
||||||
|
redirect: location.href,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (!allowedRole.includes(session.user.role as string)) {
|
||||||
|
if (!canAccess) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: session.user };
|
||||||
|
},
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserTable = () => {
|
||||||
|
const { data, refetch } = useSuspenseQuery(getUsers());
|
||||||
|
//const { data: session } = useSession();
|
||||||
|
const { data: canImpersonate = false } = useQuery(
|
||||||
|
permissionQuery({
|
||||||
|
user: ["impersonate"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { data: canUpdate = false } = useQuery(
|
||||||
|
permissionQuery({
|
||||||
|
user: ["update"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatePassword = useMutation({
|
||||||
|
mutationFn: async ({ user, password }: { user: any; password: string }) => {
|
||||||
|
return authClient.admin.setUserPassword({
|
||||||
|
userId: user.id,
|
||||||
|
newPassword: password,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Password updated");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRoleChange = async (row: any, newRole: string) => {
|
||||||
|
//console.log("update this user", row, newRole);
|
||||||
|
|
||||||
|
const { data, error } = await authClient.admin.updateUser({
|
||||||
|
userId: row.id,
|
||||||
|
data: { role: newRole },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(`${data.name}, role was just changed to: ${newRole}`);
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
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("username", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Username" 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("role", {
|
||||||
|
header: ({ column }) => <SearchableHeader column={column} title="Role" />,
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: ({ row, getValue }) => {
|
||||||
|
const currentRole = getValue();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={currentRole}
|
||||||
|
onValueChange={(newRole) => {
|
||||||
|
handleRoleChange(row.original, newRole);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Select role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{selectableRoles.map((role) => (
|
||||||
|
<SelectItem key={role.value} value={role.value}>
|
||||||
|
{role.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (canUpdate) {
|
||||||
|
columns.push(
|
||||||
|
columnHelper.accessor("changePassword", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Change Password" />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="New password"
|
||||||
|
className="w-[200px]"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key !== "Enter") return;
|
||||||
|
|
||||||
|
const password = e.currentTarget.value.trim();
|
||||||
|
|
||||||
|
if (!password) return;
|
||||||
|
|
||||||
|
updatePassword.mutate({
|
||||||
|
user: row.original,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
e.currentTarget.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
onClick={(e) => {
|
||||||
|
const input =
|
||||||
|
e.currentTarget.parentElement?.querySelector("input");
|
||||||
|
|
||||||
|
const password = input?.value.trim();
|
||||||
|
|
||||||
|
if (!password) return;
|
||||||
|
|
||||||
|
updatePassword.mutate({
|
||||||
|
user: row.original,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KeyRound className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Update Password, fill out and press enter or update here
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canImpersonate) {
|
||||||
|
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>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(
|
||||||
|
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"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-end m-2">
|
||||||
|
<NewUser refetch={refetch} />
|
||||||
|
</div>
|
||||||
|
<LstTable data={data} columns={columns} pageSize={50} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
frontend/src/routes/app-down.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
frontend/src/routes/forbidden.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import { Button } from "../components/ui/button";
|
||||||
import { useSession } from "../lib/auth-client";
|
import { useSession } from "../lib/auth-client";
|
||||||
import { trackLstEvent } from "../lib/umami.utils";
|
import { runtimeConfig, trackLstEvent } from "../lib/umami.utils";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
validateSearch: z.object({
|
validateSearch: z.object({
|
||||||
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
const { isPending } = useSession();
|
const { data: session, isPending } = useSession();
|
||||||
|
|
||||||
if (isPending)
|
if (isPending)
|
||||||
return <div className="flex justify-center mt-10">Loading...</div>;
|
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 (
|
return (
|
||||||
<div className="flex justify-center m-10 flex-col">
|
<div className="flex justify-center m-10 flex-col">
|
||||||
<h3 className="w-2xl text-3xl">Welcome Lst - V3</h3>
|
<h3 className="w-2xl text-3xl">Welcome Lst - V3</h3>
|
||||||
@@ -55,7 +65,7 @@ function Index() {
|
|||||||
<strong>Click</strong>
|
<strong>Click</strong>
|
||||||
</b>
|
</b>
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
<button onClick={click}>
|
<button onClick={click} type="button">
|
||||||
<a
|
<a
|
||||||
href={`https://www.youtube.com/watch?v=dQw4w9WgXcQ`}
|
href={`https://www.youtube.com/watch?v=dQw4w9WgXcQ`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -67,6 +77,9 @@ function Index() {
|
|||||||
</a>
|
</a>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
{session && session.user.role === "systemAdmin" && (
|
||||||
|
<Button onClick={checkConfig}>Check config</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
"foregroundImage": "./assets/adaptive-icon-white.png",
|
"foregroundImage": "./assets/adaptive-icon-white.png",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"versionCode": 32,
|
"versionCode": 37,
|
||||||
"minSupportedVersionCode": 26,
|
"minSupportedVersionCode": 33,
|
||||||
"predictiveBackGestureEnabled": false,
|
"predictiveBackGestureEnabled": false,
|
||||||
"package": "net.alpla.lst.mobile"
|
"package": "net.alpla.lst.mobile"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function TabsLayout() {
|
|||||||
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
|
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
|
||||||
|
|
||||||
const port = parseInt(serverPort || "0", 10) >= 50000;
|
const port = parseInt(serverPort || "0", 10) >= 50000;
|
||||||
console.log(port);
|
|
||||||
if (!port) {
|
if (!port) {
|
||||||
if (!user || !isUnlocked) {
|
if (!user || !isUnlocked) {
|
||||||
return <Redirect href="/login" />;
|
return <Redirect href="/login" />;
|
||||||
@@ -58,14 +58,14 @@ export default function TabsLayout() {
|
|||||||
// },
|
// },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
{/* <Tabs.Screen
|
||||||
name="ppoo"
|
name="ppoo"
|
||||||
options={{
|
options={{
|
||||||
title: "PPOO",
|
title: "PPOO",
|
||||||
href: isNormalScanner ? null : "/(tabs)/ppoo",
|
href: isNormalScanner ? null : "/(tabs)/ppoo",
|
||||||
tabBarIcon: ({ color, size }) => <Boxes size={size} color={color} />,
|
tabBarIcon: ({ color, size }) => <Boxes size={size} color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="laneCheck"
|
name="laneCheck"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -1,18 +1,210 @@
|
|||||||
import React from "react";
|
import axios from "axios";
|
||||||
import { Text, View } from "react-native";
|
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 { 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() {
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
|
||||||
//justifyContent: "center",
|
//justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginTop: 50,
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||