19 Commits

Author SHA1 Message Date
db28635c8c fix(mobile): ui over lapping
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m1s
the ui elements would over lap and cause visual issues with the scanning and seeing the old labels

closes #25
2026-05-27 20:57:49 -05:00
bcdf9566bc refactor(mobile): moved logout to the tab bar 2026-05-27 20:56:35 -05:00
c15ee070e7 refactor(mobile): setup - added button to go home as it caused confustion 2026-05-27 20:56:08 -05:00
347edb7078 fix(mobile users): corrected and endpoint that prevented us from change the pin number 2026-05-27 20:55:25 -05:00
fe0b1573f3 feat(mobile): dock door scanning backend added
ref #12
2026-05-27 20:54:47 -05:00
9c0ef1f5df fix(mobile): scan log incorrect user ref 2026-05-27 20:53:07 -05:00
8b076949a7 feat(warehousing): ppoo monitoring added
this will monitor ppoo every 45 seconds as long as someone is on the page.

closes #13
2026-05-27 20:52:34 -05:00
6d0fb8aee4 feat(mobile): added auto download of latest
this will predownload the latest if its there, this will speed up the update as the user will only
need to scan a single command and it will install restart app
2026-05-27 20:50:07 -05:00
3a0c729b9a chore(release): 0.1.0-alpha.2
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m45s
Release and Build Image / release (push) Successful in 15s
2026-05-23 11:42:48 -05:00
057a570e43 fix(docs): wrong location for images
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m39s
2026-05-23 11:41:43 -05:00
52974aa0b4 refactor(mobile): added missing error to the scanner
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m55s
2026-05-23 11:22:44 -05:00
ecfbda9036 fix(mobile): correction to axios helper 2026-05-23 11:22:25 -05:00
389211186f feat(opendock): added in new article link setup for fine tuning how od works 2026-05-23 11:22:02 -05:00
3a27fd8542 docs(mobile): updated imgs to be a little smaller 2026-05-23 11:21:19 -05:00
1f6637c512 fix(build): crashes when files changed :(
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m34s
BREAKING CHANGE: gives a rabbit hole error

closes #24
2026-05-21 21:51:21 -05:00
1840ac5e58 feat(opendock): scheduing updates
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m39s
ref #23
2026-05-21 21:42:18 -05:00
636daaed0a fix(sql queries): disable job would error so now we will check if it exists before trying to kill it
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m37s
2026-05-20 20:49:54 -05:00
71c83062cb ci(docker): changes to the ignore file 2026-05-20 20:49:21 -05:00
cd67c4de80 refactor(opendock): changes to how we do the intergration scheduling
ref #23
2026-05-20 20:49:00 -05:00
119 changed files with 18959 additions and 455 deletions

View File

@@ -9,4 +9,4 @@ builds
testFiles testFiles
nssm.exe nssm.exe
postgresql-17.9-2-windows-x64.exe postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.msi VSCodeSetup-x64-1.120.0.msi

View File

@@ -1,5 +1,41 @@
# All Changes to LST can be found below. # All Changes to LST can be found below.
## [0.1.0-alpha.2](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.1...v0.1.0-alpha.2) (2026-05-23)
### ⚠ BREAKING CHANGES
* **build:** gives a rabbit hole error
### 🌟 Enhancements
* **opendock:** added in new article link setup for fine tuning how od works ([3892111](https://git.tuffraid.net/cowch/lst_v3/commits/389211186f00cb8a6fdd5de092a944fa7e5898aa))
* **opendock:** scheduing updates ([1840ac5](https://git.tuffraid.net/cowch/lst_v3/commits/1840ac5e580c726c452216480b6e14e7c52a0f35)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
### 🐛 Bug fixes
* **build:** crashes when files changed :( ([1f6637c](https://git.tuffraid.net/cowch/lst_v3/commits/1f6637c512dcd465c5000f8d1baaa8e76766edc1)), closes [#24](https://git.tuffraid.net/cowch/lst_v3/issues/24)
* **docs:** wrong location for images ([057a570](https://git.tuffraid.net/cowch/lst_v3/commits/057a570e43a8e1763652d98244c90999c3fccd42))
* **mobile:** correction to axios helper ([ecfbda9](https://git.tuffraid.net/cowch/lst_v3/commits/ecfbda9036f3d68c93e9c1d81021efa8093f18e2))
* **sql queries:** disable job would error so now we will check if it exists before trying to kill it ([636daae](https://git.tuffraid.net/cowch/lst_v3/commits/636daaed0adeda908e7e850a4f5bb20d7bbef861))
### 📚 Documentation
* **mobile:** updated imgs to be a little smaller ([3a27fd8](https://git.tuffraid.net/cowch/lst_v3/commits/3a27fd8542c3fa4ad5520532c2f10c6e3eaa951c))
### 🛠️ Code Refactor
* **mobile:** added missing error to the scanner ([52974aa](https://git.tuffraid.net/cowch/lst_v3/commits/52974aa0b4f21431777b773200a57f185b4babd2))
* **opendock:** changes to how we do the intergration scheduling ([cd67c4d](https://git.tuffraid.net/cowch/lst_v3/commits/cd67c4de80b6f0244afc639a7360e9dc2ba97a21)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
### 📈 Project changes
* **docker:** changes to the ignore file ([71c8306](https://git.tuffraid.net/cowch/lst_v3/commits/71c83062cb644796ebbfd845084ac6c019206faa))
## [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) ## [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)

View File

@@ -1,7 +1,8 @@
import { drizzle } from "drizzle-orm/postgres-js"; import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres"; import postgres from "postgres";
import * as opendockAVCheck from "./schema/opendock_articleSetup.js";
import * as scanUserSchema from "./schema/scanUsers.js"; import * as scanUserSchema from "./schema/scanUsers.js";
import * as settingsSchema from "./schema/settings.schema.js";
const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`; const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`;
@@ -20,5 +21,7 @@ const queryClient = postgres(dbURL, {
export const db = drizzle(queryClient, { export const db = drizzle(queryClient, {
schema: { schema: {
...scanUserSchema, ...scanUserSchema,
...settingsSchema,
...opendockAVCheck,
}, },
}); });

View File

@@ -0,0 +1,22 @@
import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const dockDoorScanners = pgTable("dock_door_scanners", {
id: uuid("id").defaultRandom().primaryKey(),
ip: text("ip").notNull(),
name: text("name").unique(),
dockId: text("dock_id"),
active: boolean("active").default(true),
currentLoadingOrder: text("current_loading_order").default(""),
add_date: timestamp("add_date").defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});
export const dockDoorScannersSchema = createSelectSchema(dockDoorScanners);
export const newDockDoorScannersSchema = createInsertSchema(dockDoorScanners);
export type DockDoorScanners = z.infer<typeof dockDoorScannersSchema>;
export type NewDockDoorScanners = z.infer<typeof newDockDoorScannersSchema>;

View File

@@ -14,14 +14,13 @@ export const opendockApt = pgTable(
"opendock_apt", "opendock_apt",
{ {
id: uuid("id").defaultRandom().primaryKey(), id: uuid("id").defaultRandom().primaryKey(),
release: integer("release").notNull().unique(), release: integer("release").notNull().unique("opendock_apt_release_unique"),
openDockAptId: text("open_dock_apt_id").notNull(), openDockAptId: text("open_dock_apt_id").notNull(),
appointment: jsonb("appointment").notNull().default([]), appointment: jsonb("appointment").notNull().default([]),
upd_date: timestamp("upd_date").notNull().defaultNow(), upd_date: timestamp("upd_date").notNull().defaultNow(),
createdAt: timestamp("created_at").notNull().defaultNow(), createdAt: timestamp("created_at").notNull().defaultNow(),
}, },
(table) => ({ (table) => ({
releaseIdx: index("opendock_apt_release_idx").on(table.release),
openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on( openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on(
table.openDockAptId, table.openDockAptId,
), ),

View File

@@ -0,0 +1,46 @@
import {
integer,
pgEnum,
pgTable,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const loadTypeEnum = pgEnum("load_type", ["drop", "live"]);
export const opendockArticleSetup = pgTable(
"opendock_article_setup",
{
id: uuid("id").defaultRandom().primaryKey(),
av: integer("av").notNull(),
description: text("description").notNull(),
customer: text("customer").notNull(), // customer should be a concat of the ID - Desc
customerDescription: text("customer_description").notNull(),
loadType: loadTypeEnum("load_type").notNull().default("drop"),
dock: text("dock").notNull(),
upd_date: timestamp("upd_date").notNull().defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
},
(table) => ({
uniqueAvCustomer: unique("uq_opendock_article_setup_av_customer").on(
table.av,
table.customer,
),
}),
);
export const opendockArticleSetupSchema =
createSelectSchema(opendockArticleSetup);
export const newOpendockArticleSetupSchema =
createInsertSchema(opendockArticleSetup);
export type OpendockArticleSetup = z.infer<typeof opendockArticleSetupSchema>;
export type NewOpendockArticleSetup = z.infer<
typeof newOpendockArticleSetupSchema
>;

View File

@@ -0,0 +1,21 @@
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const opendockDockSetup = pgTable("opendock_dock_setup", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
dockID: text("dock_id").notNull(),
upd_date: timestamp("upd_date").notNull().defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
});
export const opendockDockSetupSchema = createSelectSchema(opendockDockSetup);
export const newOpendockDockSetupSchema = createInsertSchema(opendockDockSetup);
export type OpendockArticleSetup = z.infer<typeof opendockDockSetupSchema>;
export type NewOpendockArticleSetup = z.infer<
typeof newOpendockDockSetupSchema
>;

View File

@@ -38,7 +38,7 @@ export const settings = pgTable(
}, },
(table) => [ (table) => [
// uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`), // uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),
uniqueIndex("name").on(table.name), uniqueIndex("settings_name_unique").on(table.name),
], ],
); );

View File

@@ -0,0 +1,35 @@
import { addDays, subDays } from "date-fns";
import { format } from "date-fns-tz";
import { Router } from "express";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.get("/", async (_, res) => {
const orders = await runProdApi({
method: "post",
endpoint: "/public/v1.0/OutboundDeliveries/LoadingOrders/Search",
data: [
{
loadingDateFrom: format(subDays(new Date(Date.now()), 3), "yyyy-MM-dd"),
loadingDateTo: format(addDays(new Date(Date.now()), 3), "yyyy-MM-dd"),
states: [
1, // planned
],
//isCommissioned: true,
},
],
});
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "current Active loading orders",
message: `Current active loading loaders.`,
data: orders?.data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,18 @@
import { Router } from "express";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.post("/", async (req, res) => {
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `Release x is being closed now. the bol should come out at the default printer.`,
data: req.body ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,89 @@
// sends the units from the dock door scanner here.
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
// validate we are active
type Data = {
dockId?: string;
sscc?: string;
runningNr?: string;
};
export const loadUnit = async (data: Data) => {
// are we even active at this time?
const dockDoorActive = await db.query.settings.findFirst({
where: (u, { eq }) => eq(u.name, "dockDoorScanning"),
});
if (!dockDoorActive?.active) {
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadunit",
message: "Dock door scanning feature is not active.",
data: [],
notify: false,
room: "",
});
}
// check if its a valids an sscc
if (data.sscc === "noread") {
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message:
"Failed to load the unit to the truck, there was no pallet read.",
data: [],
notify: false,
room: `dockDoorLoading${data.dockId}`,
});
}
// check if we currently have a loading order attached to the dock door.
const dock = await db
.select()
.from(dockDoorScanners)
.where(eq(dockDoorScanners.dockId, data.dockId as string));
if (dock[0]?.currentLoadingOrder === "") {
return returnFunc({
success: true,
level: "error",
module: "dockdoor",
subModule: "loadingOrders",
message:
"There are know current active loading orders please start one and try again.",
data: [],
notify: false,
room: `dockDoorLoading${data.dockId}`,
});
}
// TODO: pallet validation, check if we are on hold, then check if we have been in the staging warehouse for more than x time.
// if on hold stop the scan and send a bad read with the reason its on hold and what its on hold for, including coa.
// if precheck is active then check if we have a warehouse, then check if the pallet was in the warehouse for greater than the define min, all fails send a warning and still do the scan
// add the loading units
try {
const prod = await runProdApi({
method: "post",
endpoint: `/public/v1.0/OutboundDeliveries/LoadingOrders/${dock[0]?.currentLoadingOrder}/LoadUnit`,
data: [{ sscc: data.sscc?.slice(2) }],
});
console.log(prod?.data);
} catch (error) {
console.log(error);
}
};

View File

@@ -0,0 +1,43 @@
import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import activeLoadingOrders from "./dockdoor.activeLoadingOrders.route.js";
import closeLoadingOrder from "./dockdoor.closeLoadingOrder.route.js";
import startLoad from "./dockdoor.startLoad.route.js";
import prodDocks from "./dockdoors.docks.route.js";
import docks from "./dockdoors.route.js";
export const setupDockDoorRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(
`${baseUrl}/api/dockDoor/scanners`,
featureCheck("dockDoorScanning"),
docks,
);
app.use(
`${baseUrl}/api/dockDoor/closeLoadingOrder`,
featureCheck("dockDoorScanning"),
closeLoadingOrder,
);
app.use(
`${baseUrl}/api/dockDoor/activeLoadingOrders`,
featureCheck("dockDoorScanning"),
activeLoadingOrders,
);
app.use(
`${baseUrl}/api/dockDoor/startLoad`,
featureCheck("dockDoorScanning"),
startLoad,
);
app.use(
`${baseUrl}/api/dockDoor/docks`,
featureCheck("dockDoorScanning"),
prodDocks,
);
// TODO : add manual way to add pallets
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,65 @@
import { sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const startLoading = z.object({
loadingOrder: z.string(),
dockId: z.string(),
});
r.post("/", async (req, res) => {
try {
const validated = startLoading.parse(req.body);
const { data, error } = await tryCatch(
db
.update(dockDoorScanners)
.set({
currentLoadingOrder: validated.loadingOrder,
upd_date: sql`NOW()`,
upd_user: req.user?.username,
})
.returning(),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to updating the dock.`,
data: (error as any) ?? [],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "loadingOrder",
message: `Loading order ${validated.loadingOrder} was just added to dockId ${validated.dockId}.`,
data: data ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to start loading order.`,
data: (error as any) ?? [],
status: 400,
});
}
});
export default r;

View File

@@ -0,0 +1,54 @@
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (_, res) => {
const activeDocks = sqlQuerySelector(`outbound.docks`) as SqlQuery;
if (!activeDocks.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "docks",
message: `There was an error getting the docks query.`,
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(activeDocks.query, "Current Active Docks"),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error getting the docks.`,
data: (error as any) ?? ([] as any),
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "docks",
message: `Current active docks.`,
data: (data.data as any) ?? ([] as any),
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,76 @@
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
const newDockScanner = z.object({
ip: z.string(),
name: z.string(),
dockId: z.string(),
});
r.get("/", async (_, res) => {
try {
const docks = await db
.select()
.from(dockDoorScanners)
.orderBy(dockDoorScanners.name);
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `All dock Doors.`,
data: docks ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error adding in the new dock.`,
data: error ?? ([] as any),
status: 200,
});
}
});
r.post("/", requireAuth, async (req, res) => {
try {
const validated = newDockScanner.parse(req.body);
const newDock = await db
.insert(dockDoorScanners)
.values(validated)
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `${validated.name} was just added.`,
data: newDock ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error adding in the new dock.`,
data: error ?? ([] as any),
status: 200,
});
}
});
export default r;

View File

@@ -62,7 +62,27 @@ router.get("/ehs/xml", (_, res) => {
return res.sendFile(xmlPath); return res.sendFile(xmlPath);
}); });
router.get("/upgrade/android/13", (_, res) => { router.get("/android/upgrade/11", (_, res) => {
const apkPath = path.join(
downloadDir,
"HE_FULL_UPDATE_11-70-20.00-RG-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_11.zip"`,
);
return res.sendFile(apkPath);
});
router.get("/android/upgrade/13", (_, res) => {
const apkPath = path.join( const apkPath = path.join(
downloadDir, downloadDir,
"HE_FULL_UPDATE_13-51-16.00-TG-U00-STD-HEL-04.zip", "HE_FULL_UPDATE_13-51-16.00-TG-U00-STD-HEL-04.zip",
@@ -82,7 +102,7 @@ router.get("/upgrade/android/13", (_, res) => {
return res.sendFile(apkPath); return res.sendFile(apkPath);
}); });
router.get("/upgrade/android/14", (_, res) => { router.get("/android/upgrade/14", (_, res) => {
const apkPath = path.join( const apkPath = path.join(
downloadDir, downloadDir,
"HE_FULL_UPDATE_14-38-04.00-UG-U15-STD-HEL-04.zip", "HE_FULL_UPDATE_14-38-04.00-UG-U15-STD-HEL-04.zip",

View File

@@ -13,7 +13,7 @@ router.post("/", async (req, res) => {
await db await db
.update(scanUser) .update(scanUser)
.set({ lastScan: sql`NOW()` }) .set({ lastScan: sql`NOW()` })
.where(eq(scanUser.name, body.name)); .where(eq(scanUser.name, body.user));
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -3,7 +3,7 @@ import { addHours } from "date-fns";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
import { eq, sql } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock.schema.js"; import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { settings } from "../db/schema/settings.schema.js"; import { settings } from "../db/schema/settings.schema.js";
import { createLogger } from "../logger/logger.controller.js"; import { createLogger } from "../logger/logger.controller.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js"; import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
@@ -27,6 +27,9 @@ type Releases = {
Quantity: number; Quantity: number;
LineItemArticleWeight: number; LineItemArticleWeight: number;
CustomerReleaseNumber: string; CustomerReleaseNumber: string;
DeliveryAddressDescription: string;
DeliveryAddressHumanReadableId: string;
AdditionalInformation1: string;
}; };
const timeZone = process.env.TIMEZONE as string; const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
@@ -73,6 +76,28 @@ const postRelease = async (release: Releases) => {
log.info({}, "Refreshing Auth Token"); log.info({}, "Refreshing Auth Token");
await getToken(); await getToken();
} }
// load validation checks
const defaultDock = await db.query.settings.findFirst({
where: (u, { eq }) => eq(u.name, "defaultLoadType"),
});
// check if the release has the data in it
const releaseLoadtypeCheck = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("drop");
const opendDockArticleCheck = await db.query.opendockArticleSetup.findFirst({
where: (table, { and, eq }) =>
and(
eq(table.av, release.LineItemArticleWeight),
eq(table.customer, release.DeliveryAddressHumanReadableId),
),
});
// TODO: add in docks from lst db here to make it more universal for the team
/** /**
* ReleaseState * ReleaseState
* 0 = open * 0 = open
@@ -101,6 +126,7 @@ const postRelease = async (release: Releases) => {
: release.DeliveryState === 4 && "Completed", : release.DeliveryState === 4 && "Completed",
userId: process.env.DEFAULT_CARRIER, // this should be the carrierid userId: process.env.DEFAULT_CARRIER, // this should be the carrierid
loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one
// TODO: look in the remarks in the release and if its says
dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber], refNumbers: [release.ReleaseNumber],
//refNumber: release.ReleaseNumber, //refNumber: release.ReleaseNumber,
@@ -115,6 +141,19 @@ const postRelease = async (release: Releases) => {
}, },
units: null, units: null,
customFields: [ customFields: [
{
name: "strCustomer",
type: "str",
label: "Customer",
value: `${release.DeliveryAddressDescription}`,
description: "Who is the customer ",
placeholder: "",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
{ {
name: "strArticle", name: "strArticle",
type: "str", type: "str",
@@ -190,6 +229,72 @@ const postRelease = async (release: Releases) => {
if (existing) { if (existing) {
const id = existing.openDockAptId; const id = existing.openDockAptId;
if (
(releaseLoadtypeCheck ||
opendDockArticleCheck?.loadType === "drop" ||
defaultDock?.value === "drop") &&
(release.DeliveryState === 0 || release.DeliveryState === 1)
) {
const setArrival = { ...newDockApt, status: "Arrived" };
// set to arrived
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
setArrival,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(
{ stack: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
// set to inprogress
await delay(1500);
try { try {
const response = await axios.patch( const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`, `${process.env.OPENDOCK_URL}/appointment/${id}`,
@@ -229,7 +334,7 @@ const postRelease = async (release: Releases) => {
log.info({}, `${release.ReleaseNumber} was updated`); log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) { } catch (e) {
log.error( log.error(
{ error: e }, { stack: e },
`Error updating the release: ${release.ReleaseNumber}`, `Error updating the release: ${release.ReleaseNumber}`,
); );
} }
@@ -237,12 +342,67 @@ const postRelease = async (release: Releases) => {
} catch (e: any) { } catch (e: any) {
//console.info(newDockApt); //console.info(newDockApt);
log.error( log.error(
{ error: e.response.data }, { stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`, `An error has occurred during patching of the release: ${release.ReleaseNumber}`,
); );
return; return;
} }
} else {
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(
{ stack: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
}
} else { } else {
try { try {
const response = await axios.post( const response = await axios.post(
@@ -287,13 +447,13 @@ const postRelease = async (release: Releases) => {
log.info({}, `${release.ReleaseNumber} was created`); log.info({}, `${release.ReleaseNumber} was created`);
} catch (e) { } catch (e) {
log.error({ error: e }, "Error creating new release"); log.error({ stack: e }, "Error creating new release");
} }
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities // biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) { } catch (e: any) {
log.error( log.error(
{ error: e?.response?.data }, { stack: e?.response?.data },
"Error posting new release to opendock", `Error posting new release to opendock, ${release.ReleaseNumber}`,
); );
return; return;

View File

@@ -0,0 +1,277 @@
import { desc, eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import {
type NewOpendockArticleSetup,
opendockArticleSetup,
} from "../db/schema/opendock_articleSetup.js";
import { opendockDockSetup } from "../db/schema/opendock_docks.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const newArticleLink = z.object({
av: z.number().int(),
description: z.string(),
customer: z.string().min(1).max(32),
customerDescription: z.string().min(2).max(100),
loadType: z
.enum(["drop", "live"])
.optional()
.describe("What roles are available to use."),
dock: z
//.record(z.string(), z.unknown())
.string()
.optional()
.describe(
"This allows us to add extra fields to the data to parse against",
),
});
const newDockLink = z.object({
name: z.string(),
dockID: z.string(),
});
r.post("/", async (req, res) => {
try {
const validated = newArticleLink.parse(req.body) as NewOpendockArticleSetup;
const newLink = await db
.insert(opendockArticleSetup)
.values({
av: validated.av,
description: validated.description,
customer: validated.customer,
customerDescription: validated.customerDescription,
loadType: validated.loadType,
dock: validated.dock,
add_user: req.user?.username ?? "lst_user",
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `${validated.av} was just added `,
data: newLink as any,
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",
message: "Internal Server Error adding article link",
data: [err],
status: 400, //connect.success ? 200 : 400,
});
}
});
r.patch("/:id", async (req, res) => {
const { id } = req.params;
const updates: Record<string, unknown | null> = {};
if (req.body?.loadType !== undefined) {
updates.loadType = req.body.loadType;
}
if (req.body?.dock !== undefined) {
updates.dock = req.body.dock;
}
updates.upd_user = req.user?.username || "lst_user";
updates.upd_date = sql`NOW()`;
const updatedSetting = await db
.update(opendockArticleSetup)
.set(updates)
.where(eq(opendockArticleSetup.id, id))
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `${updatedSetting[0]?.av} was just updated. `,
data: updatedSetting,
status: 200,
});
});
r.delete("/:id", async (req, res) => {
const { id } = req.params;
const removeLink = await db
.delete(opendockArticleSetup)
.where(eq(opendockArticleSetup.id, id))
.returning();
return apiReturn(res, {
success: false,
level: "info", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",
message: "Article link was deleted",
data: removeLink,
status: 200, //connect.success ? 200 : 400,
});
});
r.get("/", async (_, res) => {
const { data } = await tryCatch(
db
.select()
.from(opendockArticleSetup)
.orderBy(desc(opendockArticleSetup.customer))
.limit(1500),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `All links`,
data: data ?? [],
status: 200,
});
});
r.get("/customers/:av", async (req, res) => {
const { av } = req.params;
const avSQLQuery = sqlQuerySelector(`opendock.addressLink`) as SqlQuery;
if (!avSQLQuery.success) {
return apiReturn(res, {
success: true,
level: "error",
module: "opendock",
subModule: "articleCheck",
message: avSQLQuery.message,
data: [],
status: 200,
});
}
const { data } = await tryCatch(
prodQuery(
avSQLQuery.query.replace("[articleCheck]", av),
"openDock addressLink",
),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `All customers linked to av: ${av}`,
data: data?.data ?? ([] as any),
status: 200,
});
});
r.post("/dock", async (req, res) => {
try {
const validated = newDockLink.parse(req.body) as any;
const newLink = await db
.insert(opendockDockSetup)
.values({
name: validated.name,
dockID: validated.dockID,
add_user: req.user?.username ?? "lst_user",
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `${validated.name} was just added `,
data: newLink as any,
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",
message: "Internal Server Error adding dock link",
data: [err],
status: 400, //connect.success ? 200 : 400,
});
}
});
r.get("/dock", async (_, res) => {
const { data } = await tryCatch(
db
.select()
.from(opendockDockSetup)
.orderBy(desc(opendockDockSetup.name))
.limit(1500),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `All dock links`,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -1,6 +1,7 @@
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 { featureCheck } from "../middleware/featureActive.middleware.js"; import { featureCheck } from "../middleware/featureActive.middleware.js";
import articleCheck from "./opendock.articleCheck.route.js";
import getApt from "./opendockGetRelease.route.js"; import getApt from "./opendockGetRelease.route.js";
@@ -13,4 +14,11 @@ export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
requireAuth, requireAuth,
getApt, getApt,
); );
app.use(
`${baseUrl}/api/opendock/articleCheck`,
featureCheck("opendock_sync"),
requireAuth,
articleCheck,
);
}; };

View File

@@ -1,7 +1,7 @@
import { desc, gte, sql } from "drizzle-orm"; import { desc, gte, 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 { opendockApt } from "../db/schema/opendock.schema.js"; import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js"; import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js"; import { tryCatch } from "../utils/trycatch.utils.js";

View File

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

View File

@@ -0,0 +1,34 @@
/*
This will return all address with a sales price.
*/
WITH ranked AS (
SELECT
av.id,
av.humanReadableId as av,
av.Alias as description,
-- CONCAT(ad.HumanReadableId, ' - ',ad.Name) as customer ,
ad.HumanReadableId as customer,
ad.Name as customerDescription,
ROW_NUMBER() OVER (
PARTITION BY AddressId, sp.articleId
ORDER BY ValidAfter DESC
) AS rn
FROM [test1_AlplaPROD2.0_Read].[masterData].[SalesPrice] as sp (nolock)
/* av */
left join
[test1_AlplaPROD2.0_Read].[masterData].[Article] as av (nolock) on
av.id = sp.articleId
/* address */
left join
[test1_AlplaPROD2.0_Read].[masterData].[Address] as ad (nolock) on
ad.id = AddressId
)
SELECT *
FROM ranked
WHERE rn = 1
and ranked.av = '[articleCheck]'
order by customerDescription

View File

@@ -0,0 +1,6 @@
USE [test1_AlplaPROD2.0_Read]
SELECT *
FROM [masterData].[Dock] (nolock)
where active = 1
order by Description desc

View File

@@ -21,7 +21,7 @@ SELECT
,[MainMaterialId] ,[MainMaterialId]
,[MainMaterialHumanReadableId] ,[MainMaterialHumanReadableId]
,[MainMaterialDescription] ,[MainMaterialDescription]
,[AdditionalInformation1] ,[AdditionalInformation1] -- we will use this to reference as the first check
,[AdditionalInformation2] ,[AdditionalInformation2]
,[D365SupplierLot] ,[D365SupplierLot]
,[TradeUnits] ,[TradeUnits]
@@ -49,7 +49,7 @@ SELECT
,[PaymentTermsDescription] ,[PaymentTermsDescription]
,[Remark] ,[Remark]
,[DeliveryAddressId] ,[DeliveryAddressId]
,[DeliveryAddressHumanReadableId] ,[DeliveryAddressHumanReadableId] --use this to validate with the new drop or live check
,[DeliveryAddressDescription] ,[DeliveryAddressDescription]
,[DeliveryStreetName] ,[DeliveryStreetName]
,[DeliveryAddressZip] ,[DeliveryAddressZip]

View File

@@ -4,6 +4,7 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups // import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js"; import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js"; import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupDockDoorRoutes } from "./dockdoorScanning/dockdoor.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js"; import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js"; import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js"; import { setupNotificationRoutes } from "./notification/notification.routes.js";
@@ -29,4 +30,5 @@ 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);
setupDockDoorRoutes(baseUrl, app);
}; };

View File

@@ -26,6 +26,7 @@ import {
} from "./utils/analyticRouteHits.utils.js"; } from "./utils/analyticRouteHits.utils.js";
import { createCronJob } from "./utils/croner.utils.js"; import { createCronJob } from "./utils/croner.utils.js";
import { sendEmail } from "./utils/sendEmail.utils.js"; import { sendEmail } from "./utils/sendEmail.utils.js";
import { ppooMonitoring } from "./warehousing/warehousing.ppooMonitor.js";
const port = Number(process.env.PORT) || 3000; const port = Number(process.env.PORT) || 3000;
export let systemSettings: Setting[] = []; export let systemSettings: Setting[] = [];
@@ -78,6 +79,10 @@ const start = async () => {
runRouteHitAnalyticsCron(), runRouteHitAnalyticsCron(),
); );
createCronJob("ppooMonitor", "*/45 * * * * *", async () =>
ppooMonitoring(),
);
createCronJob("cleanHitsUp", "0 0 7 * * *", () => cleanupOldRouteHits()); createCronJob("cleanHitsUp", "0 0 7 * * *", () => cleanupOldRouteHits());
// one shots only needed to run on server startups // one shots only needed to run on server startups
createNotifications(); createNotifications();

View File

@@ -1,4 +1,4 @@
import type { RoomId } from "./types.socket.js"; import type { RoomId } from "./roomDefinitions.socket.js";
export const MAX_HISTORY = 50; export const MAX_HISTORY = 50;
export const FLUSH_INTERVAL = 100; // 50ms change higher if needed export const FLUSH_INTERVAL = 100; // 50ms change higher if needed

View File

@@ -1,17 +1,50 @@
import { desc } from "drizzle-orm"; import { desc } from "drizzle-orm";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js"; import { logs } from "../db/schema/logs.schema.js";
import type { RoomId } from "./types.socket.js"; import { ppoRun } from "../warehousing/warehousing.ppooMonitor.js";
type RoomDefinition<T = unknown> = { type RoomDefinition<T = unknown> = {
seed: (limit: number) => Promise<T[]>; seed: (limit: number) => Promise<T[]>;
}; };
export const protectedRooms: any = { export type StaticRoomId = "logs" | "labels" | "admin" | "admin:build" | "ppoo";
export type DynamicRoomId = `dockDoorLoading:${string}`;
export type RoomId = StaticRoomId | DynamicRoomId;
export type RoomConfig = {
requiresAuth?: boolean;
role?: string[];
seed?: (limit: number, roomId: RoomId) => Promise<unknown[]>;
};
export const protectedRooms: Record<StaticRoomId, RoomConfig> = {
logs: { requiresAuth: true, role: ["admin", "systemAdmin"] }, logs: { requiresAuth: true, role: ["admin", "systemAdmin"] },
//admin: { requiresAuth: false, role: ["admin", "systemAdmin"] }, //admin: { requiresAuth: false, role: ["admin", "systemAdmin"] },
labels: {},
admin: {},
"admin:build": {},
ppoo: {},
}; };
export function getRoomConfig(roomId: string): RoomConfig | null {
if (roomId in protectedRooms) {
return protectedRooms[roomId as StaticRoomId];
}
if (roomId.startsWith("dockDoorLoading:")) {
const dockId = roomId.split(":")[1];
if (!dockId) return null;
return {
requiresAuth: true,
role: ["admin", "systemAdmin", "dockDoor"],
};
}
return null;
}
export const roomDefinition: Record<RoomId, RoomDefinition> = { export const roomDefinition: Record<RoomId, RoomDefinition> = {
logs: { logs: {
seed: async (limit) => { seed: async (limit) => {
@@ -48,4 +81,14 @@ export const roomDefinition: Record<RoomId, RoomDefinition> = {
return []; return [];
}, },
}, },
ppoo: {
seed: async (limit) => {
console.log(limit);
return {
type: "snapshot",
items: await ppoRun(),
createdAt: new Date().toISOString(),
} as any;
},
},
}; };

View File

@@ -1,6 +1,6 @@
// the emitter setup // the emitter setup
import type { RoomId } from "./types.socket.js"; import type { RoomId } from "./roomDefinitions.socket.js";
let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null; let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null;

View File

@@ -7,18 +7,33 @@ import {
roomFlushTimers, roomFlushTimers,
roomHistory, roomHistory,
} from "./roomCache.socket.js"; } from "./roomCache.socket.js";
import { roomDefinition } from "./roomDefinitions.socket.js"; import { type RoomId, roomDefinition } from "./roomDefinitions.socket.js";
import type { RoomId } from "./types.socket.js";
// get the db data if not exiting already // get the db data if not exiting already
const log = createLogger({ module: "socket.io", subModule: "roomService" }); const log = createLogger({ module: "socket.io", subModule: "roomService" });
let ioRef: Server | null = null;
export const registerRoomService = (io: Server) => {
ioRef = io;
};
export const hasRoomMembers = (roomId: string): boolean => {
if (!ioRef) return false;
return (ioRef.sockets.adapter.rooms.get(roomId)?.size ?? 0) > 0;
};
export const getRoomMemberCount = (roomId: string): number => {
if (!ioRef) return 0;
return ioRef.sockets.adapter.rooms.get(roomId)?.size ?? 0;
};
export const preseedRoom = async (roomId: RoomId) => { export const preseedRoom = async (roomId: RoomId) => {
if (roomHistory.has(roomId)) { if (roomHistory.has(roomId)) {
return roomHistory.get(roomId); return roomHistory.get(roomId);
} }
const roomDef = roomDefinition[roomId]; const roomDef = roomDefinition[roomId] as any;
if (!roomDef) { if (!roomDef) {
log.error({}, `Room ${roomId} is not defined`); log.error({}, `Room ${roomId} is not defined`);
@@ -32,7 +47,7 @@ export const preseedRoom = async (roomId: RoomId) => {
}; };
export const createRoomEmitter = (io: Server) => { export const createRoomEmitter = (io: Server) => {
const addDataToRoom = <T>(roomId: RoomId, payload: T) => { const addDataToRoom = <T>(roomId: RoomId, payload: T[]) => {
if (!roomHistory.has(roomId)) { if (!roomHistory.has(roomId)) {
roomHistory.set(roomId, []); roomHistory.set(roomId, []);
} }

View File

@@ -7,7 +7,11 @@ import { Server } from "socket.io";
import { createLogger } from "../logger/logger.controller.js"; import { createLogger } from "../logger/logger.controller.js";
import { allowedOrigins } from "../utils/cors.utils.js"; import { allowedOrigins } from "../utils/cors.utils.js";
import { registerEmitter } from "./roomEmitter.socket.js"; import { registerEmitter } from "./roomEmitter.socket.js";
import { createRoomEmitter, preseedRoom } from "./roomService.socket.js"; import {
createRoomEmitter,
preseedRoom,
registerRoomService,
} from "./roomService.socket.js";
//const __filename = fileURLToPath(import.meta.url); //const __filename = fileURLToPath(import.meta.url);
//const __dirname = dirname(__filename); //const __dirname = dirname(__filename);
@@ -15,7 +19,7 @@ const log = createLogger({ module: "socket.io", subModule: "setup" });
import { auth } from "../utils/auth.utils.js"; import { auth } from "../utils/auth.utils.js";
//import type { Session, User } from "better-auth"; // adjust if needed //import type { Session, User } from "better-auth"; // adjust if needed
import { protectedRooms } from "./roomDefinitions.socket.js"; import { getRoomConfig } from "./roomDefinitions.socket.js";
// declare module "socket.io" { // declare module "socket.io" {
// interface Socket { // interface Socket {
@@ -33,6 +37,9 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
}, },
}); });
// manage members of the rooms.
registerRoomService(io);
// ✅ Create emitter instance // ✅ Create emitter instance
const { addDataToRoom } = createRoomEmitter(io); const { addDataToRoom } = createRoomEmitter(io);
registerEmitter(addDataToRoom); registerEmitter(addDataToRoom);
@@ -78,38 +85,76 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
version: "1.0.0", version: "1.0.0",
}); });
s.on("join-room", async (rn) => { // s.on("join-room", async (rn) => {
const config = protectedRooms[rn]; // const config = protectedRooms[rn];
if (config?.requiresAuth && !s.user) { // if (config?.requiresAuth && !s.user) {
// return s.emit("room-error", {
// room: rn,
// message: "Authentication required",
// });
// }
// const roles = Array.isArray(config?.role) ? config?.role : [config?.role];
// //if (config?.role && s.user?.role !== config.role) {
// if (config?.role && !roles.includes(s.user?.role)) {
// return s.emit("room-error", {
// roomId: rn,
// message: `Not authorized to be in room: ${rn}`,
// });
// }
// s.join(rn);
// // get room seeded
// const history = await preseedRoom(rn);
// log.info({}, `User joined ${rn}: ${s.id}`);
// // send the intial data
// s.emit("room-update", {
// roomId: rn,
// payloads: history,
// initial: true,
// });
// });
s.on("join-room", async (rn: string) => {
const config = getRoomConfig(rn);
if (!config) {
return s.emit("room-error", { return s.emit("room-error", {
room: rn, roomId: rn,
message: `Unknown room: ${rn}`,
});
}
if (config.requiresAuth && !s.user) {
return s.emit("room-error", {
roomId: rn,
message: "Authentication required", message: "Authentication required",
}); });
} }
const roles = Array.isArray(config?.role) ? config?.role : [config?.role]; const roles = Array.isArray(config.role) ? config.role : [];
//if (config?.role && s.user?.role !== config.role) { if (roles.length > 0 && !roles.includes(s.user?.role)) {
if (config?.role && !roles.includes(s.user?.role)) {
return s.emit("room-error", { return s.emit("room-error", {
roomId: rn, roomId: rn,
message: `Not authorized to be in room: ${rn}`, message: `Not authorized to be in room: ${rn}`,
}); });
} }
s.join(rn); s.join(rn);
// get room seeded const history = await preseedRoom(rn as any);
const history = await preseedRoom(rn);
log.info({}, `User joined ${rn}: ${s.id}`); log.info({}, `User joined ${rn}: ${s.id}`);
// send the intial data
s.emit("room-update", { s.emit("room-update", {
roomId: rn, roomId: rn,
payloads: history, payloads: history,
initial: true, initial: true,
}); });
}); });
s.on("leave-room", (room) => { s.on("leave-room", (room) => {
s.leave(room); s.leave(room);
log.info({}, `${s.id} left room: ${room}`); log.info({}, `${s.id} left room: ${room}`);

View File

@@ -1 +0,0 @@
export type RoomId = "logs" | "labels" | "admin" | "admin:build"; //| "alerts" | "metrics";

View File

@@ -162,6 +162,17 @@ const servers: NewServerData[] = [
serverLoc: "D$\\LST_V3", serverLoc: "D$\\LST_V3",
buildNumber: 1, buildNumber: 1,
}, },
{
name: "Salt Lake City",
server: "USSLC1VMS006",
plantToken: "usslc1",
idAddress: "10.202.0.26",
greatPlainsPlantCode: "70",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
]; ];
// notes here for when it comes time to pull in all the server address info [test1_AlplaPROD2.0_Read].[masterData].[Plant] has everything from every server :D // notes here for when it comes time to pull in all the server address info [test1_AlplaPROD2.0_Read].[masterData].[Plant] has everything from every server :D

View File

@@ -86,8 +86,40 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, seedVersion: 1,
}, },
{
name: "dockDoorScanning",
value: "0",
active: false,
description: "dock door scanning",
moduleName: "dockDoorScanning",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings // standard settings
{
name: "stagingWarehouse",
value: "30218",
active: true,
description:
"The warehouse we will use for staging, validation that we did our prechecks if required",
moduleName: "dockDoorScanning",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "precheck",
value: "5",
active: false,
description:
"Precheck is required, the value is in minute, 5 min should be 5",
moduleName: "dockDoorScanning",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{ {
name: "prolinkCheck", name: "prolinkCheck",
value: "1", value: "1",
@@ -357,6 +389,17 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, seedVersion: 1,
}, },
{
name: "defaultLoadType",
value: "drop",
active: false,
description:
"What is the default load type we will use for creating new apt: drop or live are the current options.",
moduleName: "opendock",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
]; ];
export const baseSettingValidationCheck = async () => { export const baseSettingValidationCheck = async () => {

View File

@@ -1,7 +1,9 @@
import net from "node:net"; import net from "node:net";
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 { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { printerData } from "../db/schema/printers.schema.js"; import { printerData } from "../db/schema/printers.schema.js";
import { loadUnit } from "../dockdoorScanning/dockdoor.loadUnits.js";
import { createLogger } from "../logger/logger.controller.js"; import { createLogger } from "../logger/logger.controller.js";
import { delay } from "../utils/delay.utils.js"; import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js"; import { returnFunc } from "../utils/returnHelper.utils.js";
@@ -14,6 +16,7 @@ export let isServerRunning = false;
const port = parseInt(process.env.TCP_PORT ?? "2222", 10); const port = parseInt(process.env.TCP_PORT ?? "2222", 10);
// This is the parser for zebra scanners
const parseTcpAlert = (input: string) => { const parseTcpAlert = (input: string) => {
// guard // guard
const colonIndex = input.indexOf(":"); const colonIndex = input.indexOf(":");
@@ -74,6 +77,24 @@ export const startTCPServer = async () => {
printerListen(printerData as PrinterData); printerListen(printerData as PrinterData);
} }
// check if its a dock door scanner
// TODO: move to the db and get real info lol
const dockdoorScanners = await db.select().from(dockDoorScanners);
if (dockdoorScanners.some((s) => s.ip === ip.replace("::ffff:", ""))) {
console.log("dock door logic");
const currentDock = dockdoorScanners.filter(
(s) => s.ip === ip.replace("::ffff:", ""),
);
// send the data + dock scan over
loadUnit({
dockId: currentDock[0]?.dockId ?? "0",
sscc: data.toString(),
});
}
}); });
socket.on("end", () => { socket.on("end", () => {

View File

@@ -3,11 +3,12 @@ import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
export const statement = { export const statement = {
...defaultStatements, ...defaultStatements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "update", "delete", "readAll"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"], logistics: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"], mobile: ["read", "create", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"], openDock: ["read", "create", "update", "delete"],
notifications: ["read", "create", "update", "delete", "readAll"],
} as const; } as const;
export const ac = createAccessControl(statement); export const ac = createAccessControl(statement);
@@ -15,24 +16,50 @@ export const ac = createAccessControl(statement);
export const user = ac.newRole({ export const user = ac.newRole({
app: ["read", "create"], app: ["read", "create"],
notifications: ["read", "create"], notifications: ["read", "create"],
openDock: ["read"],
}); });
export const manager = ac.newRole({ export const manager = ac.newRole({
app: ["read", "create", "update"], app: ["read", "create", "update"],
mobile: ["read", "create", "update"], mobile: ["read", "create", "update"],
openDock: ["read", "create", "update"],
});
export const transport = ac.newRole({
app: ["read", "create", "update"],
openDock: ["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"], mobile: ["read", "create", "update"],
user: ["create", "update"], user: ["create", "update", "ban"],
openDock: ["read", "create", "update"],
}); });
export const systemAdmin = ac.newRole({ export const systemAdmin = ac.newRole({
...adminAc.statements, ...adminAc.statements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "update", "delete", "readAll"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"], mobile: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"], logistics: ["read", "create", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"], notifications: ["read", "create", "update", "delete", "readAll"],
openDock: ["read", "create", "update", "delete"],
}); });
/* example usage
const canCreateProject = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
},
});
// You can also check multiple resource permissions at the same time
const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
sale: ["create"]
},
});
*/

View File

@@ -17,6 +17,7 @@ export const allowedOrigins = [
`http://${process.env.PROD_SERVER}:3100`, // temp `http://${process.env.PROD_SERVER}:3100`, // temp
`http://usmcd1olp082:3000`, `http://usmcd1olp082:3000`,
`${process.env.EXTERNAL_URL}`, // internal docker `${process.env.EXTERNAL_URL}`, // internal docker
"chrome-extension://mddoackclclnbkmofficmmepfnadolfa",
]; ];
export const lstCors = () => { export const lstCors = () => {
return cors({ return cors({

View File

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

View File

@@ -0,0 +1,29 @@
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { hasRoomMembers } from "../socket.io/roomService.socket.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
export const ppoRun = async () => {
const laneData = await runProdApi({
method: "post",
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",
data: [
{
laneIds: ["0"],
},
],
});
return laneData?.data ?? [];
};
export const ppooMonitoring = async () => {
if (!hasRoomMembers(`ppoo`)) {
return;
}
emitToRoom("ppoo", {
type: "snapshot",
items: await ppoRun(),
createdAt: new Date().toISOString(),
} as any);
};

View File

@@ -8,6 +8,7 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@base-ui/react": "^1.5.0",
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-form": "^1.28.5", "@tanstack/react-form": "^1.28.5",
@@ -34,6 +35,7 @@
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"vite-imagetools": "^10.0.0",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
@@ -439,6 +441,15 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.28.6", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -484,6 +495,66 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@base-ui/react": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.5.0.tgz",
"integrity": "sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2",
"@base-ui/utils": "0.2.9",
"@floating-ui/react-dom": "^2.1.8",
"@floating-ui/utils": "^0.2.11",
"use-sync-external-store": "^1.6.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@date-fns/tz": "^1.2.0",
"@types/react": "^17 || ^18 || ^19",
"date-fns": "^4.0.0",
"react": "^17 || ^18 || ^19",
"react-dom": "^17 || ^18 || ^19"
},
"peerDependenciesMeta": {
"@date-fns/tz": {
"optional": true
},
"@types/react": {
"optional": true
},
"date-fns": {
"optional": true
}
}
},
"node_modules/@base-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2",
"@floating-ui/utils": "^0.2.11",
"reselect": "^5.1.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"@types/react": "^17 || ^18 || ^19",
"react": "^17 || ^18 || ^19",
"react-dom": "^17 || ^18 || ^19"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@better-auth/utils": { "node_modules/@better-auth/utils": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.1.tgz", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.1.tgz",
@@ -649,6 +720,16 @@
"node": "^16.13.0 || >=18.0.0" "node": "^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.4", "version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
@@ -1333,6 +1414,519 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@inquirer/ansi": { "node_modules/@inquirer/ansi": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
@@ -3163,6 +3757,28 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
@@ -6672,6 +7288,12 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/esutils": { "node_modules/esutils": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -7511,6 +8133,15 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/imagetools-core": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/imagetools-core/-/imagetools-core-9.1.0.tgz",
"integrity": "sha512-xQjs+2vrxLnAjCq+omuNkd5UQTld9/bP8+YT0LyYTlKfuSQtgUBvqhUwGugzSAh6sCdN+LnROMuLswn5hZ9Fhg==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -10460,6 +11091,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/reselect": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
"license": "MIT"
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -10638,7 +11275,6 @@
"version": "7.7.4", "version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -10804,6 +11440,50 @@
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
}, },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -11837,6 +12517,23 @@
} }
} }
}, },
"node_modules/vite-imagetools": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/vite-imagetools/-/vite-imagetools-10.0.0.tgz",
"integrity": "sha512-+83L32YPU/2BOHWhudO2+9T5HBvb3+0qHoUNN7fb0+XcAoXilx7aE25cDPWU5kBi5Yc750zYCvHxgfyR+tAuMA==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.5",
"imagetools-core": "^9.1.0",
"sharp": "^0.34.1"
},
"engines": {
"node": ">=22.0.0"
},
"peerDependencies": {
"vite": ">=7.0.0"
}
},
"node_modules/web-streams-polyfill": { "node_modules/web-streams-polyfill": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",

View File

@@ -10,6 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.5.0",
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-form": "^1.28.5", "@tanstack/react-form": "^1.28.5",
@@ -21,6 +22,8 @@
"better-auth": "^1.5.5", "better-auth": "^1.5.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
@@ -34,9 +37,8 @@
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"zod": "^4.3.6", "vite-imagetools": "^10.0.0",
"date-fns": "^4.1.0", "zod": "^4.3.6"
"date-fns-tz": "^3.2.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,94 @@
import { useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { ChevronRight, Link as link } from "lucide-react";
import { permissionQuery } from "../../lib/queries/permsCheck";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from "../ui/sidebar";
export default function TransportationBar() {
const { data: canCreate = false } = useQuery(
permissionQuery({
openDock: ["create"],
}),
);
const { setOpen } = useSidebar();
const items = [
{
title: "Open Dock",
url: "/transportation",
//icon,
isActive: canCreate,
items: [
{
title: "ArticleLink",
icon: link,
url: "/transportation/opendock",
},
],
},
];
return (
<SidebarGroup>
<SidebarGroupLabel>Transportation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<div key={item.title}>
{item.isActive && (
<Collapsible
asChild
//defaultOpen={isNotifications}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.title}
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<Link
to={subItem.url}
onClick={() => setOpen(false)}
>
<subItem.icon />
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)}
</div>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@@ -1,4 +1,4 @@
import { useSuspenseQuery } from "@tanstack/react-query"; import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -8,13 +8,20 @@ import {
} 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 { getSettings } from "../../lib/queries/getSettings";
import { permissionQuery } from "../../lib/queries/permsCheck";
import AdminSidebar from "./AdminBar"; import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar"; import DocBar from "./DocBar";
import MobileBar from "./MobileBar"; import MobileBar from "./MobileBar";
import TransportationBar from "./TransportationBar";
export function AppSidebar() { export function AppSidebar() {
const { data: session } = useSession(); const { data: session } = useSession();
const { data: settings, isLoading } = useSuspenseQuery(getSettings()); const { data: settings, isLoading } = useSuspenseQuery(getSettings());
const { data: canRead = false } = useQuery(
permissionQuery({
openDock: ["read"],
}),
);
return ( return (
<Sidebar <Sidebar
@@ -32,6 +39,11 @@ export function AppSidebar() {
<MobileBar /> <MobileBar />
)} )}
{!isLoading &&
settings.filter((n: any) => n.name === "opendock_sync")[0]
?.active &&
canRead && <TransportationBar />}
{session && {session &&
(session.user.role === "admin" || (session.user.role === "admin" ||
session.user.role === "systemAdmin" || session.user.role === "systemAdmin" ||

View File

@@ -0,0 +1,299 @@
"use client"
import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group"
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
const Combobox = ComboboxPrimitive.Root
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
}
function ComboboxTrigger({
className,
children,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
data-slot="combobox-trigger"
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</ComboboxPrimitive.Trigger>
)
}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot="combobox-clear"
render={<InputGroupButton variant="ghost" size="icon-xs" />}
className={cn(className)}
{...props}
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.Clear>
)
}
function ComboboxInput({
className,
children,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean
showClear?: boolean
}) {
return (
<InputGroup className={cn("w-auto", className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align="inline-end">
{showTrigger && (
<InputGroupButton
size="icon-xs"
variant="ghost"
asChild
data-slot="input-group-button"
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
disabled={disabled}
>
<ComboboxTrigger />
</InputGroupButton>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
)
}
function ComboboxContent({
className,
side = "bottom",
sideOffset = 6,
align = "start",
alignOffset = 0,
anchor,
...props
}: ComboboxPrimitive.Popup.Props &
Pick<
ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) {
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn("group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none 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}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
)
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot="combobox-list"
className={cn(
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
className
)}
{...props}
/>
)
}
function ComboboxItem({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</ComboboxPrimitive.ItemIndicator>
</ComboboxPrimitive.Item>
)
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
)
}
function ComboboxLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
)
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
className={cn(
"hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
className
)}
{...props}
/>
)
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
className
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
render={<Button variant="ghost" size="icon-xs" />}
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.ChipRemove>
)}
</ComboboxPrimitive.Chip>
)
}
function ComboboxChipsInput({
className,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn("min-w-16 flex-1 outline-none", className)}
{...props}
/>
)
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null)
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
}

View File

@@ -0,0 +1,154 @@
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
/* eslint-disable */
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className,
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
},
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
},
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className,
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className,
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
};

View File

@@ -1,6 +1,6 @@
import type * as React from "react"; import * as React from "react"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (
@@ -8,14 +8,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", className
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className,
)} )}
{...props} {...props}
/> />
); )
} }
export { Input }; export { Input }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -1,33 +1,49 @@
import { useMutation } from "@tanstack/react-query"; //import { useMutation } from "@tanstack/react-query";
import { Button } from "../../components/ui/button"; //import { Button } from "../../components/ui/button";
import { Separator } from "../../components/ui/separator"; import { Separator } from "../../components/ui/separator";
// image imports
const images = import.meta.glob(
"/public/imgs/docs/mobile/*.{png,jpg,jpeg,webp}",
{
eager: true,
query: "w=300;600;1200&format=webp",
import: "default",
},
) as Record<string, string>;
const server = window.LST_CONFIG?.server;
const firstScan = images[`/public/imgs/docs/mobile/${server}-1.png`];
const secondScan = images[`/public/imgs/docs/mobile/${server}-2.png`];
const thirdScan = images[`/public/imgs/docs/mobile/${server}-3.png`];
export default function UpdateInstructions() { export default function UpdateInstructions() {
const getFile = useMutation({ // const getFile = useMutation({
mutationFn: async () => { // mutationFn: async () => {
// 1. Fetch the file from the public folder // // 1. Fetch the file from the public folder
const response = await fetch( // const response = await fetch(
`/lst/app/stage-now/${window.LST_CONFIG?.server}-stageNow.pdf`, // `/lst/app/stage-now/${window.LST_CONFIG?.server}-stageNow.pdf`,
); // );
if (!response.ok) throw new Error("Network response was not ok"); // if (!response.ok) throw new Error("Network response was not ok");
// 2. Convert to blob // // 2. Convert to blob
return await response.blob(); // return await response.blob();
}, // },
onSuccess: (blob) => { // onSuccess: (blob) => {
// 3. Create a temporary anchor element to trigger download // // 3. Create a temporary anchor element to trigger download
const url = window.URL.createObjectURL(blob); // const url = window.URL.createObjectURL(blob);
const a = document.createElement("a"); // const a = document.createElement("a");
a.href = url; // a.href = url;
a.download = `${window.LST_CONFIG?.server}-stageNow.pdf`; // Desired filename // a.download = `${window.LST_CONFIG?.server}-stageNow.pdf`; // Desired filename
document.body.appendChild(a); // document.body.appendChild(a);
a.click(); // a.click();
// 4. Cleanup // // 4. Cleanup
window.URL.revokeObjectURL(url); // window.URL.revokeObjectURL(url);
document.body.removeChild(a); // document.body.removeChild(a);
}, // },
}); // });
return ( return (
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
@@ -41,14 +57,14 @@ export default function UpdateInstructions() {
NOTE: LST Mobile only works on TC8300 NOTE: LST Mobile only works on TC8300
</p> </p>
</div> </div>
<div className="flex justify-center"> {/* <div className="flex justify-center">
<Button <Button
onClick={() => getFile.mutate()} onClick={() => getFile.mutate()}
disabled={getFile.isPending} disabled={getFile.isPending}
> >
{getFile.isPending ? "Downloading..." : "Get StageNow Codes"} {getFile.isPending ? "Downloading..." : "Get StageNow Codes"}
</Button> </Button>
</div> </div> */}
</div> </div>
<Separator className="m-3" /> <Separator className="m-3" />
<div> <div>
@@ -115,21 +131,9 @@ export default function UpdateInstructions() {
<p>Scan Commands</p> <p>Scan Commands</p>
<Separator className="m-3" /> <Separator className="m-3" />
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center">
<img <img src={firstScan} alt="First Scan" className="m-3" />
src={`/lst/app/imgs/docs/mobile/${window.LST_CONFIG?.server}-1.png`} <img src={secondScan} alt="Second Scan" className="m-3" />
alt="Home" <img src={thirdScan} alt="Third Scan" className="m-3" />
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> </div>
</div> </div>

View File

@@ -13,6 +13,7 @@ type RoomErrorPayload = {
export function useSocketRoom<T>( export function useSocketRoom<T>(
roomId: string, roomId: string,
enabled = true,
getKey?: (item: T) => string | number, getKey?: (item: T) => string | number,
) { ) {
const [data, setData] = useState<T[]>([]); const [data, setData] = useState<T[]>([]);
@@ -35,6 +36,7 @@ export function useSocketRoom<T>(
); );
useEffect(() => { useEffect(() => {
if (!roomId || !enabled) return;
function handleConnect() { function handleConnect() {
socket.emit("join-room", roomId); socket.emit("join-room", roomId);
setInfo(`Joined room: ${roomId}`); setInfo(`Joined room: ${roomId}`);
@@ -74,7 +76,18 @@ export function useSocketRoom<T>(
socket.off("room-update", handleUpdate); socket.off("room-update", handleUpdate);
socket.off("room-error", handleError); socket.off("room-error", handleError);
}; };
}, [roomId]); }, [roomId, enabled]);
return { data, info, clearRoom }; return { data, info, clearRoom };
} }
/*
const isDockDoorPage = location.pathname.startsWith("/dockdoor");
useSocketRoom(
dockId ? `dockdoor:${dockId}` : null,
isDockDoorPage,
);
*/

View File

@@ -13,17 +13,19 @@ type SelectableRole = {
export const selectableRoles: SelectableRole[] = [ export const selectableRoles: SelectableRole[] = [
{ label: "User", value: "user" }, { label: "User", value: "user" },
{ label: "Manager", value: "manager" }, { label: "Manager", value: "manager" },
{ label: "Transport", value: "transport" },
{ label: "Admin", value: "admin" }, { label: "Admin", value: "admin" },
{ label: "System Admin", value: "systemAdmin" }, { label: "System Admin", value: "systemAdmin" },
]; ];
export const statement = { export const statement = {
...defaultStatements, ...defaultStatements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "update", "delete", "readAll"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"], logistics: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"], mobile: ["read", "create", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"], openDock: ["read", "create", "update", "delete"],
notifications: ["read", "create", "update", "delete", "readAll"],
} as const; } as const;
export const ac = createAccessControl(statement); export const ac = createAccessControl(statement);
@@ -31,41 +33,79 @@ export const ac = createAccessControl(statement);
export const user = ac.newRole({ export const user = ac.newRole({
app: ["read", "create"], app: ["read", "create"],
notifications: ["read", "create"], notifications: ["read", "create"],
openDock: ["read"],
}); });
export const manager = ac.newRole({ export const manager = ac.newRole({
app: ["read", "create", "update"], app: ["read", "create", "update"],
mobile: ["read", "create", "update"], mobile: ["read", "create", "update"],
openDock: ["read", "create", "update"],
});
export const transport = ac.newRole({
app: ["read", "create", "update"],
openDock: ["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"], mobile: ["read", "create", "update"],
user: ["create", "update"], user: ["create", "update", "ban"],
openDock: ["read", "create", "update"],
}); });
export const systemAdmin = ac.newRole({ export const systemAdmin = ac.newRole({
...adminAc.statements, ...adminAc.statements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "update", "delete", "readAll"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"], mobile: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"], logistics: ["read", "create", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"], notifications: ["read", "create", "update", "delete", "readAll"],
openDock: ["read", "create", "update", "delete"],
}); });
/* example usage /*
const canCreateProject = await authClient.admin.hasPermission({
inside a component
const { data: canImpersonate = false } = useQuery(
permissionQuery({
user: ["impersonate"],
logistics: ["delete"]
}),
);
on the before load use this example
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
//const allowedRole = ["systemAdmin", "admin", "manager"];
const canAccess = await authClient.admin.hasPermission({
permissions: { permissions: {
project: ["create"], opendock: ["create"],
}, logistics: ["delete"]
});
// You can also check multiple resource permissions at the same time
const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
sale: ["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 };
},
*/ */

View File

@@ -0,0 +1,84 @@
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "../../components/ui/combobox";
import { Label } from "../../components/ui/label";
import { useFieldContext } from ".";
import { FieldErrors } from "./Errors.Field";
// type SelectOption = {
// value: string;
// label: string;
// };
type ComboBoxFieldProps = {
data: string[];
label: string;
placeholder: string;
};
export const ComboBoxField = ({
data = [],
label,
placeholder = "",
}: ComboBoxFieldProps) => {
const field = useFieldContext<string>();
return (
<div className="grid gap-3">
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
{/* <Select
value={field.state.value}
onValueChange={(value) => field.handleChange(value)}
>
<SelectTrigger
id={field.name}
onBlur={field.handleBlur}
className="w-min-2/3 w-max-fit"
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent position={"popper"}>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select> */}
<Combobox
items={data}
//value={field.state.value ?? ""}
// onValueChange={(value) => {
// console.log(value);
// field.handleChange(value);
// }}
>
<ComboboxInput placeholder={placeholder} />
{/* <ComboboxInput
//showTrigger={false}
placeholder={placeholder}
onBlur={field.handleBlur}
showClear
/> */}
<ComboboxContent className="max-h-72 overflow-y-auto">
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -18,12 +18,14 @@ type SelectFieldProps = {
label: string; label: string;
options: SelectOption[]; options: SelectOption[];
placeholder?: string; placeholder?: string;
disabled?: boolean;
}; };
export const SelectField = ({ export const SelectField = ({
label, label,
options, options,
placeholder, placeholder,
disabled = false,
}: SelectFieldProps) => { }: SelectFieldProps) => {
const field = useFieldContext<string>(); const field = useFieldContext<string>();
@@ -34,6 +36,7 @@ export const SelectField = ({
<Select <Select
value={field.state.value} value={field.state.value}
onValueChange={(value) => field.handleChange(value)} onValueChange={(value) => field.handleChange(value)}
disabled={disabled}
> >
<SelectTrigger <SelectTrigger
id={field.name} id={field.name}

View File

@@ -1,5 +1,6 @@
import { createFormHook, createFormHookContexts } from "@tanstack/react-form"; import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
import { CheckboxField } from "./CheckBox.Field"; import { CheckboxField } from "./CheckBox.Field";
import { ComboBoxField } from "./ComboBox.Field";
import { DynamicInputField } from "./DynamicInput.Field"; import { DynamicInputField } from "./DynamicInput.Field";
import { InputField } from "./Input.Field"; import { InputField } from "./Input.Field";
import { InputPasswordField } from "./InputPassword.Field"; import { InputPasswordField } from "./InputPassword.Field";
@@ -21,6 +22,7 @@ export const { useAppForm } = createFormHook({
//Searchable, //Searchable,
SwitchField, SwitchField,
DynamicInputField, DynamicInputField,
ComboBoxField,
}, },
formComponents: { SubmitButton }, formComponents: { SubmitButton },
fieldContext, fieldContext,

View File

@@ -0,0 +1,25 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getActiveArticle() {
return queryOptions({
queryKey: ["getActiveArticle"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await api.get("/datamart/activeArticles");
if (!data.success) {
throw new Error(data.message ?? "Failed to load articles");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,25 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getArticleLinks() {
return queryOptions({
queryKey: ["getArticleLinks"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await api.get("/opendock/articleCheck");
if (!data.success) {
throw new Error(data.message ?? "Failed to load article links");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,26 @@
import { queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getCustomerByAv(av: string) {
return queryOptions({
queryKey: ["getCustomerByAv", av],
queryFn: () => dataFetch(av),
staleTime: 5000,
enabled: !!av,
refetchOnWindowFocus: true,
//placeholderData: keepPreviousData,
});
}
const dataFetch = async (av: string) => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await api.get(`/opendock/articleCheck/customers/${av}`);
if (!data.success) {
throw new Error(data.message ?? "Failed to load customers");
}
return data.data ?? [];
};

View File

@@ -22,6 +22,7 @@ import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications' import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications'
import { Route as AdminLogsRouteImport } from './routes/admin/logs' import { Route as AdminLogsRouteImport } from './routes/admin/logs'
import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as TransportationOpendockIndexRouteImport } from './routes/transportation/opendock/index'
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup' 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'
@@ -91,6 +92,12 @@ const authLoginRoute = authLoginRouteImport.update({
path: '/login', path: '/login',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const TransportationOpendockIndexRoute =
TransportationOpendockIndexRouteImport.update({
id: '/transportation/opendock/',
path: '/transportation/opendock/',
getParentRoute: () => rootRouteImport,
} as any)
const authUserSignupRoute = authUserSignupRouteImport.update({ const authUserSignupRoute = authUserSignupRouteImport.update({
id: '/(auth)/user/signup', id: '/(auth)/user/signup',
path: '/user/signup', path: '/user/signup',
@@ -124,6 +131,7 @@ export interface FileRoutesByFullPath {
'/user/profile': typeof authUserProfileRoute '/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute '/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute '/user/signup': typeof authUserSignupRoute
'/transportation/opendock/': typeof TransportationOpendockIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
@@ -142,6 +150,7 @@ export interface FileRoutesByTo {
'/user/profile': typeof authUserProfileRoute '/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute '/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute '/user/signup': typeof authUserSignupRoute
'/transportation/opendock': typeof TransportationOpendockIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
@@ -161,6 +170,7 @@ export interface FileRoutesById {
'/(auth)/user/profile': typeof authUserProfileRoute '/(auth)/user/profile': typeof authUserProfileRoute
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute '/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
'/(auth)/user/signup': typeof authUserSignupRoute '/(auth)/user/signup': typeof authUserSignupRoute
'/transportation/opendock/': typeof TransportationOpendockIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
@@ -181,6 +191,7 @@ export interface FileRouteTypes {
| '/user/profile' | '/user/profile'
| '/user/resetpassword' | '/user/resetpassword'
| '/user/signup' | '/user/signup'
| '/transportation/opendock/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
@@ -199,6 +210,7 @@ export interface FileRouteTypes {
| '/user/profile' | '/user/profile'
| '/user/resetpassword' | '/user/resetpassword'
| '/user/signup' | '/user/signup'
| '/transportation/opendock'
id: id:
| '__root__' | '__root__'
| '/' | '/'
@@ -217,6 +229,7 @@ export interface FileRouteTypes {
| '/(auth)/user/profile' | '/(auth)/user/profile'
| '/(auth)/user/resetpassword' | '/(auth)/user/resetpassword'
| '/(auth)/user/signup' | '/(auth)/user/signup'
| '/transportation/opendock/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
@@ -236,6 +249,7 @@ export interface RootRouteChildren {
authUserProfileRoute: typeof authUserProfileRoute authUserProfileRoute: typeof authUserProfileRoute
authUserResetpasswordRoute: typeof authUserResetpasswordRoute authUserResetpasswordRoute: typeof authUserResetpasswordRoute
authUserSignupRoute: typeof authUserSignupRoute authUserSignupRoute: typeof authUserSignupRoute
TransportationOpendockIndexRoute: typeof TransportationOpendockIndexRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -331,6 +345,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authLoginRouteImport preLoaderRoute: typeof authLoginRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/transportation/opendock/': {
id: '/transportation/opendock/'
path: '/transportation/opendock'
fullPath: '/transportation/opendock/'
preLoaderRoute: typeof TransportationOpendockIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/(auth)/user/signup': { '/(auth)/user/signup': {
id: '/(auth)/user/signup' id: '/(auth)/user/signup'
path: '/user/signup' path: '/user/signup'
@@ -372,6 +393,7 @@ const rootRouteChildren: RootRouteChildren = {
authUserProfileRoute: authUserProfileRoute, authUserProfileRoute: authUserProfileRoute,
authUserResetpasswordRoute: authUserResetpasswordRoute, authUserResetpasswordRoute: authUserResetpasswordRoute,
authUserSignupRoute: authUserSignupRoute, authUserSignupRoute: authUserSignupRoute,
TransportationOpendockIndexRoute: TransportationOpendockIndexRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View File

@@ -1,5 +1,4 @@
import { useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "../../../components/ui/button"; import { Button } from "../../../components/ui/button";
@@ -33,7 +32,7 @@ export default function NewScanUser({ refetch }: { refetch: any }) {
try { try {
const { data } = await api.post( const { data } = await api.post(
"/lst/api/mobile/auth/user", "/mobile/auth/user",
{ {
name: value.name, name: value.name,
pinNumber: value.pinNumber, pinNumber: value.pinNumber,
@@ -141,7 +140,7 @@ export default function NewScanUser({ refetch }: { refetch: any }) {
<Button <Button
type="button" type="button"
onClick={async () => { onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new"); const { data } = await api.get("/mobile/pin/new");
form.setFieldValue("pinNumber", data.data[0].pin); form.setFieldValue("pinNumber", data.data[0].pin);
}} }}

View File

@@ -1,7 +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 { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import { Trash } from "lucide-react"; import { Trash } from "lucide-react";
import { Suspense, useState } from "react"; import { Suspense, useState } from "react";
@@ -56,7 +55,7 @@ const updateSettings = async (
) => { ) => {
//console.log(id, data); //console.log(id, data);
try { try {
const res = await axios.patch(`/mobile/auth/user/${id}`, data, { const res = await api.patch(`/mobile/auth/user/${id}`, data, {
withCredentials: true, withCredentials: true,
timeout: 15000, timeout: 15000,
validateStatus: () => true, validateStatus: () => true,
@@ -121,7 +120,7 @@ const ScanUserTable = () => {
<EditableCellInput <EditableCellInput
value={getValue()} value={getValue()}
id={row.original.id} id={row.original.id}
field="value" field="pinNumber"
onSubmit={({ id, field, value }) => { onSubmit={({ id, field, value }) => {
updateSetting.mutate({ id, field, value }); updateSetting.mutate({ id, field, value });
}} }}

View File

@@ -1,9 +1,8 @@
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 { runtimeConfig, trackLstEvent } from "../lib/umami.utils"; import { trackLstEvent } from "../lib/umami.utils";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
validateSearch: z.object({ validateSearch: z.object({
@@ -14,7 +13,7 @@ export const Route = createFileRoute("/")({
}); });
function Index() { function Index() {
const { data: session, isPending } = useSession(); const { 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,15 +37,15 @@ function Index() {
}); });
}; };
const checkConfig = () => { // const checkConfig = () => {
console.log(runtimeConfig); // console.log(runtimeConfig);
trackLstEvent("config_click", { // trackLstEvent("config_click", {
module: "app", // module: "app",
action: "click", // action: "click",
label: "configCheck", // label: "configCheck",
page: window.location.pathname, // page: window.location.pathname,
}); // });
}; // };
return ( return (
<div className="flex justify-center m-10 flex-col"> <div className="flex justify-center m-10 flex-col">
@@ -77,9 +76,9 @@ function Index() {
</a> </a>
</button> </button>
</p> </p>
{session && session.user.role === "systemAdmin" && ( {/* {session && session.user.role === "systemAdmin" && (
<Button onClick={checkConfig}>Check config</Button> <Button onClick={checkConfig}>Check config</Button>
)} )} */}
</div> </div>
); );
} }

View File

@@ -0,0 +1,241 @@
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
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 { getActiveArticle } from "../../../../lib/queries/getActiveArticles";
import { getCustomerByAv } from "../../../../lib/queries/getCustomerByAv";
export default function NewArticleLink({ refetch }: { refetch: any }) {
const [open, setOpen] = useState(false);
const [selectedAv, setSelectedAv] = useState<string>("");
const { data: articleData } = useSuspenseQuery(getActiveArticle());
const {
data: customerData,
isPending,
isLoading,
} = useQuery(getCustomerByAv(selectedAv.split(" - ")[0]));
const form = useAppForm({
defaultValues: {
av: "",
description: "",
customer: "",
customerDescription: "",
loadType: "",
dock: "",
},
onSubmit: async ({ value }) => {
const corrected = {
av: parseInt(value.av.split(" - ")[0], 10),
description: value.av.split(" - ")[1],
customer: value.customer.split(" - ")[0],
customerDescription: value.customer.split(" - ")[1],
loadType: value.loadType,
dock: value.dock,
};
try {
const res = await api.post("/opendock/articleCheck", corrected, {
validateStatus: () => true,
});
if (res.data.success) {
toast.success(`The link for ${value.av} was just created :D`);
refetch();
form.reset();
setSelectedAv("");
setOpen(false);
}
if (!res.data.success) {
toast.error(
"The article customer combo are not allowed to be created twice please select a different customer.",
);
form.setFieldValue("customer", "");
console.log(res.data);
return;
}
} catch (error) {
console.log(error);
}
},
});
const closeModel = (e: boolean) => {
setOpen(e);
if (!e) {
form.reset();
setSelectedAv("");
}
};
const openForm = () => {
setOpen(true);
form.reset;
setSelectedAv("");
};
let n: any = [];
if (articleData) {
n = articleData.map((i: any) => ({
label: `${i.article} - ${i.Bezeichnung}`,
value: `${i.article} - ${i.Bezeichnung}`,
}));
}
let c: any = [];
if ((selectedAv && !isPending) || !isLoading) {
const cusData = customerData ?? [];
c = cusData.map((i: any) => ({
label: `${i.customer} - ${i.customerDescription}`,
value: `${i.customer} - ${i.customerDescription}`,
}));
}
// TODO: get this from lst as well once we get the actual docks in to link to.
// this will be live || drop but also the actaul load types so we can have a little more refined times
const loadType = [
{
label: "Live",
value: "live",
},
{
label: "Drop",
value: "drop",
},
];
//TODO: get the docks from lst to help refine and actually link the dock correctly
const dock = [
{
label: "Cermac",
value: "cermac",
},
{
label: "Gerber",
value: "gerber",
},
{
label: "Matrix",
value: "matrix",
},
];
return (
<Dialog onOpenChange={(e) => closeModel(e)} open={open}>
<Button onClick={openForm}>Create Article link</Button>
<DialogContent showCloseButton={false} className="min-w-2xl">
<DialogHeader>
<DialogTitle>Create Article Link.</DialogTitle>
<DialogDescription>
Create the fine tuned per article setup, selecting an av will pull
in only the sales prices for the av, After filling in the form all{" "}
<p className="underline">NEW</p> release created will use this as
the new default settings.
</DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div className="w-fit">
<div className="w-fill">
<form.AppField
name="av"
listeners={{
onChange: ({ value }) => {
setSelectedAv(value);
if (form.getFieldValue("customer")) {
form.setFieldValue("customer", "");
}
},
}}
>
{(field) => (
<field.SelectField
label="Select Article"
placeholder="Select av to link"
options={n}
/>
)}
</form.AppField>
</div>
</div>
<div className="w-fit">
<div className="w-fill">
<form.AppField name="customer">
{(field) => (
<field.SelectField
label="Select Customer"
placeholder={
!selectedAv
? "Select AV first"
: isLoading
? "Loading customers..."
: c.length === 0
? "No customers to select"
: "Select customer"
}
options={c}
disabled={!selectedAv || (isLoading && c.length > 0)}
/>
)}
</form.AppField>
</div>
</div>
<div className=" flex flex-row w-fit mt-3">
<div className="w-fill">
<form.AppField name="loadType">
{(field) => (
<field.SelectField
label="Select Load Type"
placeholder={"Select LoadType"}
options={loadType}
disabled={!selectedAv}
/>
)}
</form.AppField>
</div>
<div className="w-fill">
<form.AppField name="dock">
{(field) => (
<field.SelectField
label="Select Dock"
placeholder={"Select dock"}
options={dock}
disabled={!selectedAv}
/>
)}
</form.AppField>
</div>
</div>
<div className="flex justify-end mt-2 ">
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,122 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import { Suspense } from "react";
import { authClient } from "../../../lib/auth-client";
import { getArticleLinks } from "../../../lib/queries/getArticleLinks";
import LstTable from "../../../lib/tableStuff/LstTable";
import SearchableHeader from "../../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../../lib/tableStuff/SkellyTable";
import NewArticleLink from "./-components/NewArticleLink";
export const Route = createFileRoute("/transportation/opendock/")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
//const allowedRole = ["systemAdmin", "admin", "manager"];
const canAccess = await authClient.admin.hasPermission({
permissions: {
openDock: ["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 ArticleLinkTable = () => {
const { data, refetch } = useSuspenseQuery(getArticleLinks());
const columnHelper = createColumnHelper<any>();
const columns = [
columnHelper.accessor("av", {
header: ({ column }) => (
<SearchableHeader column={column} title="Article" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("description", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Description"
searchable={true}
/>
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("customer", {
header: ({ column }) => (
<SearchableHeader column={column} title="Customer" searchable={true} />
),
filterFn: "includesString",
cell: (i) => (
<span>
{i.row.original.customer} - {i.row.original.customerDescription}
</span>
),
}),
columnHelper.accessor("loadType", {
header: ({ column }) => (
<SearchableHeader column={column} title="Load Type" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("dock", {
header: ({ column }) => (
<SearchableHeader column={column} title="Dock" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
];
return (
<div>
<div>
<div className="flex justify-end m-2">
<Suspense
fallback={
<div>
<p>Loading...</p>
</div>
}
>
<NewArticleLink refetch={refetch} />
</Suspense>
</div>
<div>
<LstTable data={data} columns={columns} pageSize={50} />
</div>
</div>
</div>
);
};
function RouteComponent() {
return (
<Suspense fallback={<SkellyTable />}>
<ArticleLinkTable />
</Suspense>
);
}

View File

@@ -3,7 +3,7 @@ import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-plugin/vite"; import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { imagetools } from "vite-imagetools";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
@@ -14,6 +14,7 @@ export default defineConfig({
autoCodeSplitting: true, autoCodeSplitting: true,
}), }),
react(), react(),
imagetools(),
], ],
resolve: { resolve: {
alias: { alias: {

View File

@@ -3,7 +3,7 @@
"name": "LST mobile", "name": "LST mobile",
"slug": "lst-mobile", "slug": "lst-mobile",
"version": "0.11.1-alpha", "version": "0.11.1-alpha",
"orientation": "portrait", "orientation": "default",
"icon": "./assets/icon_white.png", "icon": "./assets/icon_white.png",
"scheme": "lstmobile", "scheme": "lstmobile",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
@@ -15,10 +15,14 @@
"foregroundImage": "./assets/adaptive-icon-white.png", "foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"versionCode": 37, "versionCode": 39,
"minSupportedVersionCode": 33, "minSupportedVersionCode": 33,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile" "package": "net.alpla.lst.mobile",
"permissions": [
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE"
]
}, },
"web": { "web": {
"output": "static", "output": "static",

View File

@@ -35,6 +35,7 @@
"expo-image": "~55.0.8", "expo-image": "~55.0.8",
"expo-linking": "~55.0.13", "expo-linking": "~55.0.13",
"expo-router": "~55.0.12", "expo-router": "~55.0.12",
"expo-screen-orientation": "~55.0.16",
"expo-splash-screen": "~55.0.18", "expo-splash-screen": "~55.0.18",
"expo-status-bar": "~55.0.5", "expo-status-bar": "~55.0.5",
"expo-symbols": "~55.0.7", "expo-symbols": "~55.0.7",
@@ -46,6 +47,7 @@
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-blob-util": "^0.24.9",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "4.2.1", "react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2", "react-native-safe-area-context": "~5.6.2",
@@ -6132,6 +6134,11 @@
} }
} }
}, },
"node_modules/base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -8171,6 +8178,16 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/expo-screen-orientation": {
"version": "55.0.16",
"resolved": "https://registry.npmjs.org/expo-screen-orientation/-/expo-screen-orientation-55.0.16.tgz",
"integrity": "sha512-I9NIqb2zAkHsK/CxdmMdmgSFP7E1v++8z/Mj2X9j1AuK6l55yOma/JHo905KU3x2zPm9/l1BTzmMA320tiBebg==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-server": { "node_modules/expo-server": {
"version": "55.0.9", "version": "55.0.9",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.9.tgz", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.9.tgz",
@@ -12821,6 +12838,77 @@
} }
} }
}, },
"node_modules/react-native-blob-util": {
"version": "0.24.9",
"resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.24.9.tgz",
"integrity": "sha512-tG3+m0WhVdBGifvxSFxZDVqtr85D0fGBJU6E4UxmK3tU+RabJZTumXEn8k7jn5/NFe8OhQhPjtBEZ11ZJ6L7Vw==",
"license": "MIT",
"dependencies": {
"base-64": "0.1.0",
"glob": "13.0.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ronradtke"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-blob-util/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/react-native-blob-util/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/react-native-blob-util/node_modules/glob": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz",
"integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==",
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.1.2",
"minipass": "^7.1.2",
"path-scurry": "^2.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/react-native-blob-util/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/react-native-css-interop": { "node_modules/react-native-css-interop": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.2.3.tgz", "resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.2.3.tgz",

View File

@@ -45,6 +45,7 @@
"expo-image": "~55.0.8", "expo-image": "~55.0.8",
"expo-linking": "~55.0.13", "expo-linking": "~55.0.13",
"expo-router": "~55.0.12", "expo-router": "~55.0.12",
"expo-screen-orientation": "~55.0.16",
"expo-splash-screen": "~55.0.18", "expo-splash-screen": "~55.0.18",
"expo-status-bar": "~55.0.5", "expo-status-bar": "~55.0.5",
"expo-symbols": "~55.0.7", "expo-symbols": "~55.0.7",
@@ -56,6 +57,7 @@
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-blob-util": "^0.24.9",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "4.2.1", "react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2", "react-native-safe-area-context": "~5.6.2",

View File

@@ -1,12 +1,14 @@
import { Redirect, Tabs } from "expo-router"; import { Redirect, Tabs, useRouter } from "expo-router";
import { import {
Boxes, Boxes,
Container, Container,
Home, Home,
LogOut,
Logs, Logs,
Rows4, Rows4,
Settings, Settings,
} from "lucide-react-native"; } from "lucide-react-native";
import { Alert } from "react-native";
import { useAppStore } from "../../hooks/useAppStore"; import { useAppStore } from "../../hooks/useAppStore";
import { useMobileAuthStore } from "../../hooks/useMobileAuth"; import { useMobileAuthStore } from "../../hooks/useMobileAuth";
@@ -20,6 +22,8 @@ export default function TabsLayout() {
const serverPort = useAppStore((s) => s.serverPort); const serverPort = useAppStore((s) => s.serverPort);
const user = useMobileAuthStore((s) => s.user); const user = useMobileAuthStore((s) => s.user);
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked); const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
const logoutScanner = useMobileAuthStore((s) => s.logout);
const router = useRouter();
const port = parseInt(serverPort || "0", 10) >= 50000; const port = parseInt(serverPort || "0", 10) >= 50000;
@@ -36,6 +40,32 @@ export default function TabsLayout() {
return role ? allowed.includes(role) : false; return role ? allowed.includes(role) : false;
}; };
const logout = async () => {
try {
// optional confirm
Alert.alert("Logout", "Are you sure?", [
{ text: "Cancel", style: "cancel" },
{
text: "Logout",
style: "destructive",
onPress: async () => {
// clear auth/session
logoutScanner();
router.replace("/(tabs)/scanner");
// clear zustand/session stuff
//useAuthStore.getState().reset();
// maybe clear async storage too
// await AsyncStorage.clear();
},
},
]);
} catch (err) {
console.error(err);
}
};
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
@@ -58,19 +88,25 @@ 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 || !hasRole(["admin", "manager"])
// ? 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={{
title: "Lane Check", title: "Lane Check",
href: isNormalScanner ? null : "/(tabs)/laneCheck", // href:
// isNormalScanner || !hasRole(["admin", "manager"])
// ? null
// : "/(tabs)/laneCheck",
tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />, tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />,
}} }}
/> />
@@ -106,6 +142,7 @@ export default function TabsLayout() {
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs", parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
}} }}
/> */} /> */}
<Tabs.Screen <Tabs.Screen
name="config" name="config"
options={{ options={{
@@ -115,6 +152,22 @@ export default function TabsLayout() {
), ),
}} }}
/> />
<Tabs.Screen
name="logout"
options={{
title: "Logout",
tabBarIcon: ({ color, size }) => <LogOut color={color} size={size} />,
}}
listeners={{
tabPress: (e) => {
// stop navigation
e.preventDefault();
// run logout logic
logout();
},
}}
/>
</Tabs> </Tabs>
); );
} }

View File

@@ -0,0 +1,3 @@
export default function LogoutScreen() {
return null;
}

View File

@@ -1,154 +1,83 @@
import axios from "axios";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import * as Device from "expo-device";
import { useFocusEffect } from "expo-router"; import { useFocusEffect } from "expo-router";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { ScrollView, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import { GlobalFooter } from "../../components/UpdateFooter";
import { Button } from "../../components/ui/button";
import { Card, CardContent, CardHeader } from "../../components/ui/card";
import { import {
Dialog, Button,
DialogContent, ScrollView,
DialogDescription, Text,
DialogHeader, useWindowDimensions,
DialogTitle, View,
DialogTrigger, } from "react-native";
} from "../../components/ui/dialog"; import { SafeAreaView } from "react-native-safe-area-context";
import { useAppStore } from "../../hooks/useAppStore"; import { Card, CardContent } from "../../components/ui/card";
import { scannerFeedback } from "../../lib/feedbackScan"; import { useSocketRoom } from "../../hooks/socket.io.hook";
import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner";
const InfoRow = ({ type PPOO = {
label, type: string;
value, items: any;
}: { createdAt: Date;
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 { data } = useSocketRoom<any>("ppoo", undefined, "replace") as any;
const serverIp = useAppStore((s) => s.serverIp); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
const handleScan = useCallback( const { width } = useWindowDimensions();
async (scan: ZebraScanResult) => { const isTablet =
setUnits(null); Device.modelName?.toLowerCase().includes("et40") ||
await scannerFeedback({ Device.modelName?.toLowerCase().includes("et45");
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; const columns = isTablet ? 3 : 1;
}
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( const gap = 8;
useCallback(() => { const cardWidth =
zebraScanner.startListening(); columns === 1 ? width - 16 : (width - gap * (columns + 1)) / columns;
const sub = zebraScanner.addScanListener((scan) => { const items = data?.items ?? [];
//console.log("SCAN:", scan); const sortedItems = useMemo(() => {
handleScan(scan); return [...items].sort((a, b) => {
const aDate = new Date(a.lastMovingDate).getTime();
const bDate = new Date(b.lastMovingDate).getTime();
return sortDir === "asc" ? aDate - bDate : bDate - aDate;
}); });
}, [items, sortDir]);
return () => { //console.log(logsInfo);
sub.remove();
zebraScanner.stopListening();
//setUnits(null);
};
}, [handleScan]),
);
return ( return (
<View <View className="flex items-center mt-2">
style={{ <View className="flex m-2">
//justifyContent: "center", <Button
alignItems: "center", onPress={() =>
marginTop: 50, setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))
}} }
> title={`Sort: ${sortDir}`}
{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> </View>
<ScrollView className="w-full" style={{ marginBottom: 20 }}> {sortedItems.length === 0 ? (
<View> <View className="flex items-center">
{units.data.map((i, index) => ( <Text>Loading PPOO...</Text>
<View </View>
key={`${i.runningNumber}-${index}`} ) : (
style={{ <SafeAreaView className="flex-1">
justifyContent: "center", <ScrollView className="w-full">
margin: 2, <View className="w-full flex-row flex-wrap gap-2 m-2">
}} {sortedItems.map((i: any) => {
> return (
<Dialog> <View key={i.runningNumber}>
<DialogTrigger>
<Card <Card
className="w-full" //className={isTablet ? "w-[32%]" : "w-full"}
style={{ style={{
borderColor: borderColor:
i.state === "QualityBlocked" ? "red" : undefined, i.mainDefectId === 864
? "blue"
: i.state === "QualityBlocked"
? "red"
: undefined,
borderWidth: 4, borderWidth: 4,
width: cardWidth,
}} }}
> >
<CardContent> <CardContent>
@@ -159,52 +88,18 @@ export default function PPOO() {
<Text> <Text>
Running Number: {i.runningNumber ?? "Non barcoded"} Running Number: {i.runningNumber ?? "Non barcoded"}
</Text> </Text>
<Text>
Date: {format(i.lastMovingDate, "M/d/yyyy HH:mm")}
</Text>
</CardContent> </CardContent>
</Card> </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>
))} );
})}
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </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>
); );
} }

View File

@@ -5,12 +5,15 @@ import "../../global.css";
import { useEffect } from "react"; import { useEffect } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import useDeviceLock from "../hooks/useDeviceCheck"; import useDeviceLock from "../hooks/useDeviceCheck";
import { connectSocket } from "../lib/socket.io";
import { zebraScanner } from "../lib/ZebraScanner"; import { zebraScanner } from "../lib/ZebraScanner";
export default function RootLayout() { export default function RootLayout() {
useDeviceLock(); useDeviceLock();
useEffect(() => { useEffect(() => {
zebraScanner.ensureProfile(); zebraScanner.ensureProfile();
connectSocket();
}, []); }, []);
return ( return (

View File

@@ -99,9 +99,16 @@ export default function Setup() {
justifyContent: "center", justifyContent: "center",
padding: 3, padding: 3,
borderRadius: 8, borderRadius: 8,
gap: 3,
}} }}
> >
<Button title="Submit" onPress={authCheck} /> <Button title="Submit" onPress={authCheck} />
<Button
title="Home"
onPress={() => {
router.push("/(tabs)/scanner");
}}
/>
</View> </View>
</View> </View>
) : ( ) : (
@@ -145,14 +152,7 @@ export default function Setup() {
</View> </View>
)} )}
<View <View className="flex gap-2 flex-row">
style={{
flexDirection: "row",
justifyContent: "center",
padding: 3,
gap: 3,
}}
>
<Button title="Save Config" onPress={handleSave} /> <Button title="Save Config" onPress={handleSave} />
<Button <Button
title="Home" title="Home"

View File

@@ -182,14 +182,17 @@ export default function LSTScanner() {
}, [handleScan]), }, [handleScan]),
); );
return ( return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}> <View className={`${bgColor ?? ""} flex-1 w-full`}>
<View style={{ alignItems: "center", margin: 5 }}> <View className="flex gap-2 items-center">
<View className="flex flex-col gap-2 items-center">
<Text style={{ fontSize: 14, fontWeight: "600" }}> <Text style={{ fontSize: 14, fontWeight: "600" }}>
User: {formatName(user?.name ?? "")} Lst user: {formatName(user?.name ?? "")}
</Text> </Text>
<Text style={{ fontSize: 18, fontWeight: "600" }}> {/* <Text style={{ fontSize: 14, fontWeight: "600" }}>
LST Scanner id: {user?.scannerId} LST Scanner id: {user?.scannerId}
</Text> </Text> */}
</View>
<View <View
style={{ style={{
marginTop: 5, marginTop: 5,
@@ -197,8 +200,8 @@ export default function LSTScanner() {
}} }}
> >
{!lastScan ? ( {!lastScan ? (
<View style={{ marginTop: 10, alignItems: "center" }}> <View style={{ marginTop: 2, alignItems: "center" }}>
<Text className="text-xl font-bold">Ready to scan</Text> <Text className="text-lg font-bold">Ready to scan</Text>
<Text>Please Scan a command to start scanning...</Text> <Text>Please Scan a command to start scanning...</Text>
<Text className="text-sm"> <Text className="text-sm">
Scanning a label could cause errors due to incorrect previous Scanning a label could cause errors due to incorrect previous
@@ -208,7 +211,7 @@ export default function LSTScanner() {
) : ( ) : (
<View <View
style={{ style={{
marginTop: 10, marginTop: 2,
alignItems: "center", alignItems: "center",
}} }}
> >
@@ -217,10 +220,10 @@ export default function LSTScanner() {
.map((i) => { .map((i) => {
return ( return (
<View <View
style={{ marginTop: 10, alignItems: "center" }} style={{ marginTop: 2, alignItems: "center" }}
key={i} key={i}
> >
<Text style={{ fontSize: 18, fontWeight: "600" }}> <Text style={{ fontSize: 12, fontWeight: "600" }}>
{i} {i}
</Text> </Text>
</View> </View>
@@ -237,18 +240,17 @@ export default function LSTScanner() {
color={bgColor} color={bgColor}
clearScan={clearScans} clearScan={clearScans}
/> />
<GlobalFooter />
</View> </View>
<View className="m-2"> {/* <View className="m-2">
{user && ( {user && (
<View className="items-center"> <View className="items-center">
<Button title="Logout" onPress={logoutScanner} /> <Button title="Logout" onPress={logoutScanner} />
</View> </View>
)} )}
</View> </View> */}
<View> {/* <View style={{ maxHeight: 75 }} className="flex-1 bg-slate-500"></View> */}
<GlobalFooter />
</View>
</View> </View>
); );
} }

View File

@@ -13,28 +13,30 @@ export function GlobalFooter() {
if (serverVersion && serverVersion?.versionCode <= build) return; if (serverVersion && serverVersion?.versionCode <= build) return;
return ( return (
<View> <View>
<View> {(hasUpdate || shouldUpdate) && (
<View className="bg-slate-500">
{hasUpdate && ( {hasUpdate && (
<View className="items-center h-[75px] bg-[#EB091A]"> <View className="items-center h-[75px] bg-[#EB091A] justify-center">
<Link href={"/updateScreen"}> <Link href="/updateScreen">
<Text className="h-[75px] font-medium text-base text-wrap text-center"> <Text className="font-medium text-base text-center">
Critical updates pending, once you are completed with your task Critical updates pending, once you are completed with your
please click me for instructions to update task please click me for instructions to update
</Text> </Text>
</Link> </Link>
</View> </View>
)} )}
{!hasUpdate && shouldUpdate && ( {!hasUpdate && shouldUpdate && (
<View className="bg-[#FDBA74]"> <View className="bg-[#FDBA74] py-2 items-center">
<Link href={"/updateScreen"}> <Link href="/updateScreen">
<Text className="h-[16] font-medium text-base text-wrap text-center"> <Text className="font-medium text-base text-center">
There is an update click me for instructions There is an update click me for instructions
</Text> </Text>
</Link> </Link>
</View> </View>
)} )}
</View> </View>
)}
</View> </View>
); );
} }

View File

@@ -0,0 +1,136 @@
import { useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { getSocket } from "../lib/socket.io";
type RoomUpdatePayload<T> = {
roomId: string;
payloads: T[];
};
type RoomErrorPayload = {
roomId?: string;
message?: string;
};
type UpdateMode = "append" | "replace";
export function useSocketRoom<T>(
roomId: string,
getKey?: (item: T) => string | number,
updateMode: UpdateMode = "append",
) {
const [data, setData] = useState<T[]>([]);
const [info, setInfo] = useState(
"No data yet — join the room to start receiving",
);
const clearRoom = useCallback(
(id?: string | number) => {
if (id !== undefined && getKey) {
setData((prev) => prev.filter((item) => getKey(item) !== id));
setInfo(`Removed item ${id}`);
return;
}
setData([]);
setInfo("Room data cleared");
},
[getKey],
);
useFocusEffect(
useCallback(() => {
const socket = getSocket();
function handleConnect() {
socket.emit("join-room", roomId);
setInfo(`Joined room: ${roomId}`);
}
function handleUpdate(payload: RoomUpdatePayload<T>) {
// protects against other room updates hitting this hook
if (payload.roomId !== roomId) return;
// resetting room data for rooms that just need updated data.
if (updateMode === "replace") {
setData(payload.payloads);
} else {
setData((prev) => [...payload.payloads, ...prev]);
}
setInfo("");
}
function handleError(err: RoomErrorPayload) {
if (err.roomId && err.roomId !== roomId) return;
setInfo(err.message ?? "Room error");
}
socket.on("connect", handleConnect);
socket.on("room-update", handleUpdate);
socket.on("room-error", handleError);
if (!socket.connected && socket.disconnected) {
socket.connect();
}
// If already connected, join immediately
if (socket.connected) {
socket.emit("join-room", roomId);
setInfo(`Joined room: ${roomId}`);
}
return () => {
socket.emit("leave-room", roomId);
socket.off("connect", handleConnect);
socket.off("room-update", handleUpdate);
socket.off("room-error", handleError);
};
}, [roomId, updateMode]),
);
// useEffect(() => {
// const socket = getSocket();
// function handleConnect() {
// socket.emit("join-room", roomId);
// setInfo(`Joined room: ${roomId}`);
// }
// function handleUpdate(payload: RoomUpdatePayload<T>) {
// // protects against other room updates hitting this hook
// if (payload.roomId !== roomId) return;
// setData((prev) => [...payload.payloads, ...prev]);
// setInfo("");
// }
// function handleError(err: RoomErrorPayload) {
// if (err.roomId && err.roomId !== roomId) return;
// setInfo(err.message ?? "Room error");
// }
// if (!socket.connected && socket.disconnected) {
// socket.connect();
// }
// // If already connected, join immediately
// if (socket.connected) {
// socket.emit("join-room", roomId);
// setInfo(`Joined room: ${roomId}`);
// }
// socket.on("connect", handleConnect);
// socket.on("room-update", handleUpdate);
// socket.on("room-error", handleError);
// return () => {
// socket.emit("leave-room", roomId);
// console.log("leaving Room");
// socket.off("connect", handleConnect);
// socket.off("room-update", handleUpdate);
// socket.off("room-error", handleError);
// };
// }, [roomId]);
return { data, info, clearRoom };
}

View File

@@ -0,0 +1,50 @@
import ReactNativeBlobUtil from "react-native-blob-util";
export async function downloadLatestApk(serverIp: string, port: string) {
const url = `http://${serverIp}:${port}/lst/api/mobile/apk/latest`;
const apkPath = `${ReactNativeBlobUtil.fs.dirs.DownloadDir}/lst-mobile.apk`;
// delete old apk if it exists
const exists = await ReactNativeBlobUtil.fs.exists(apkPath);
if (exists) {
const stat = await ReactNativeBlobUtil.fs.stat(apkPath);
// last modified time
const lastModified = Number(stat.lastModified);
// current time
const now = Date.now();
// 5 minutes in ms
const fiveMinutes = 5 * 60 * 1000;
// skip download if file is fresh
if (now - lastModified < fiveMinutes) {
console.log("APK already downloaded recently, skipping.");
return {
skipped: true,
path: apkPath,
};
}
// delete old apk before redownload
await ReactNativeBlobUtil.fs.unlink(apkPath);
}
const res = await ReactNativeBlobUtil.config({
addAndroidDownloads: {
useDownloadManager: true,
notification: true,
title: "LST Mobile Update",
description: "Downloading update for StageNow install",
mime: "application/vnd.android.package-archive",
path: apkPath,
mediaScannable: true,
},
}).fetch("GET", url);
return res.path();
}

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