30 Commits

Author SHA1 Message Date
78f7b8a179 docs(app): updated last updated to the readme to show current progress
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m58s
2026-05-27 21:19:57 -05:00
1a3d8a7ddc docs(app): updated readme 2026-05-27 21:18:30 -05:00
2a648f6306 fix(app): type in the templates... they all looked the same
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m41s
2026-05-27 21:08:02 -05:00
69a9a81a88 ci(app): added more labels to the templates
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-27 21:05:57 -05:00
188fdd0eb7 ci(app): changed the issues templates to be easier to add to the part of the app needed
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m37s
2026-05-27 21:02:18 -05:00
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
36ac1dccb4 chore(release): 0.1.0-alpha.1
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m39s
Release and Build Image / release (push) Successful in 15s
2026-05-18 21:39:59 -05:00
514a44b6de refactor(servers): changed activeity around and trying to make use of it
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-18 21:38:08 -05:00
a7bb364a2f fix(settings): failed build due it dormant import
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m42s
2026-05-18 21:23:34 -05:00
047cc7cdf0 refactor(users): lots of auth stuff added to make it more easy to manage users
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 2m9s
2026-05-18 21:19:20 -05:00
8dc4d70e28 ci(app): added in chokidar to monitor folders 2026-05-18 21:18:42 -05:00
c8931c7249 fix(notifications): reprinting
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m20s
correction to external labeling

ref #20
2026-05-14 14:18:40 -05:00
143 changed files with 19882 additions and 566 deletions

View File

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

View File

@@ -0,0 +1,66 @@
---
name: Bug Report - Frontend
about: Report something that is broken or not working correctly
title: "[BUG - Frontend] "
ref: "main"
labels:
- bug
- frontend
---
# Summary
Briefly explain the issue.
---
# Steps To Reproduce
1. Go to ...
2. Click ...
3. Scan ...
4. Error occurs ...
---
# Expected Behavior
What should have happened?
---
# Actual Behavior
What actually happened instead?
---
# Severity
- [ ] Low
- [ ] Medium
- [ ] High
- [ ] Critical
---
# Environment
Example:
- Production
- Development
- Zebra Scanner
- Mobile Device
- Windows Server
- Docker
---
# Logs / Screenshots
Paste logs or upload screenshots here.
```txt
Paste logs here

View File

@@ -0,0 +1,66 @@
---
name: Bug Report - Mobile
about: Report something that is broken or not working correctly
title: "[BUG - Mobile] "
ref: "main"
labels:
- bug
- mobile
---
# Summary
Briefly explain the issue.
---
# Steps To Reproduce
1. Go to ...
2. Click ...
3. Scan ...
4. Error occurs ...
---
# Expected Behavior
What should have happened?
---
# Actual Behavior
What actually happened instead?
---
# Severity
- [ ] Low
- [ ] Medium
- [ ] High
- [ ] Critical
---
# Environment
Example:
- Production
- Development
- Zebra Scanner
- Mobile Device
- Windows Server
- Docker
---
# Logs / Screenshots
Paste logs or upload screenshots here.
```txt
Paste logs here

View File

@@ -1,11 +1,12 @@
---
name: Bug Report
name: Bug Report - Server
about: Report something that is broken or not working correctly
title: "[BUG] "
title: "[BUG - Server] "
ref: "main"
labels:
- bug
- server
---

View File

@@ -0,0 +1,48 @@
---
name: Enhancement - Frontend
about: Improve or refine an existing feature
title: "[ENHANCEMENT - Frontend] "
ref: "main"
labels:
- enhancement
- frontend
---
# Existing Feature
What current feature or workflow is being improved?
Example:
- Notifications
- Scanner Login
- Release Monitor
- Printing
- Auth
---
# Proposed Improvement
Describe the improvement.
---
# Expected Benefit
Why would this improvement help?
---
# Impact
- [ ] Small
- [ ] Medium
- [ ] Large
---
# Additional Notes
Anything else worth mentioning.

View File

@@ -1,11 +1,12 @@
---
name: Enhancement
name: Enhancement - Mobile
about: Improve or refine an existing feature
title: "[ENHANCEMENT] "
title: "[ENHANCEMENT - Mobile] "
ref: "main"
labels:
- enhancement
- mobile
---
# Existing Feature

View File

@@ -0,0 +1,48 @@
---
name: Enhancement - Server
about: Improve or refine an existing feature
title: "[ENHANCEMENT - Server] "
ref: "main"
labels:
- enhancement
- server
---
# Existing Feature
What current feature or workflow is being improved?
Example:
- Notifications
- Scanner Login
- Release Monitor
- Printing
- Auth
---
# Proposed Improvement
Describe the improvement.
---
# Expected Benefit
Why would this improvement help?
---
# Impact
- [ ] Small
- [ ] Medium
- [ ] Large
---
# Additional Notes
Anything else worth mentioning.

View File

@@ -0,0 +1,41 @@
---
name: Feature Request - Frontend
about: Suggest a brand new feature or module
title: "[FEATURE - Frontend] "
ref: "main"
labels:
- feature
- frontend
---
# Problem Statement
What problem are you trying to solve?
---
# Proposed Solution
Describe the feature you would like added.
---
# Alternatives Considered
Any other ideas, workarounds, or approaches?
---
# Priority
- [ ] Nice to Have
- [ ] Medium Priority
- [ ] High Priority
- [ ] Critical
---
# Additional Context
Add mockups, screenshots, examples, or notes here.

View File

@@ -1,11 +1,12 @@
---
name: Feature Request
name: Feature Request - Mobile
about: Suggest a brand new feature or module
title: "[FEATURE] "
title: "[FEATURE - Mobile] "
ref: "main"
labels:
- feature
- mobile
---
# Problem Statement

View File

@@ -0,0 +1,41 @@
---
name: Feature Request - Server
about: Suggest a brand new feature or module
title: "[FEATURE - Server] "
ref: "main"
labels:
- feature
- server
---
# Problem Statement
What problem are you trying to solve?
---
# Proposed Solution
Describe the feature you would like added.
---
# Alternatives Considered
Any other ideas, workarounds, or approaches?
---
# Priority
- [ ] Nice to Have
- [ ] Medium Priority
- [ ] High Priority
- [ ] Critical
---
# Additional Context
Add mockups, screenshots, examples, or notes here.

View File

@@ -1,5 +1,60 @@
# 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)
### 🐛 Bug fixes
* **notifications:** reprinting ([c8931c7](https://git.tuffraid.net/cowch/lst_v3/commits/c8931c7249b8f532b5dd37df3271da98f14ee710)), closes [#20](https://git.tuffraid.net/cowch/lst_v3/issues/20)
* **settings:** failed build due it dormant import ([a7bb364](https://git.tuffraid.net/cowch/lst_v3/commits/a7bb364a2fd49d96b6195aca0cd58ba57c58f3a6))
### 🛠️ Code Refactor
* **servers:** changed activeity around and trying to make use of it ([514a44b](https://git.tuffraid.net/cowch/lst_v3/commits/514a44b6de3efe8dd8b308d98bdbc82e31ed8427))
* **users:** lots of auth stuff added to make it more easy to manage users ([047cc7c](https://git.tuffraid.net/cowch/lst_v3/commits/047cc7cdf036c39a89a0b87ab59dda8328efe0c0))
### 📈 Project changes
* **app:** added in chokidar to monitor folders ([8dc4d70](https://git.tuffraid.net/cowch/lst_v3/commits/8dc4d70e2827f0a40d2f54886fd757c8a2dc5ac4))
## [0.1.0-alpha.0](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.10...v0.1.0-alpha.0) (2026-05-14)

View File

@@ -7,7 +7,7 @@
Quick summary of current rewrite/migration goal.
- **Phase:** Backend rewrite
- **Last updated:** 2026-04-06
- **Last updated:** 2026-05-27
---
@@ -17,20 +17,20 @@ Quick summary of current rewrite/migration goal.
|----------|--------------|--------|
| User Authentication | ~~Login~~, ~~Signup~~, API Key | 🟨 In Progress |
| User Profile | ~~Edit profile~~, upload avatar | 🟨 In Progress |
| User Admin | Edit user, create user, remove user, alplaprod user integration | ⏳ Not Started |
| User Admin | ~~Edit user~~, ~~create user~~, remove user, alplaprod user integration | ⏳ Not Started |
| Notifications | ~~Subscribe~~, ~~Create~~, ~~Update~~, ~~~~Remove~~, Manual Trigger | 🟨 In Progress |
| Datamart | ~~Create~~, ~~Update~~, ~~Run~~, Deactivate | 🟨 In Progress |
| Frontend | Analytics and charts | ⏳ Not Started |
| Docs | Instructions and trouble shooting | ⏳ Not Started |
| One Click Print | Get printers, monitor printers, label process, material process, Special processes | ⏳ Not Started |
| One Click Print | ~~Get printers~~, monitor printers, label process, material process, Special processes | 🟨 In Progress |
| Silo Adjustments | Create, History, Comments | ⏳ Not Started |
| Demand Management | Orders, Forecast, Special Mappings, Create trucks, Load Trucks (tablet scanning) | ⏳ Not Started |
| Open Docks | Integrations | ⏳ Not Started |
| Open Docks | Integrations | 🟨 In Progress |
| Transport Insight | Integrations | ⏳ Not Started |
| Quality Request Tool | Add Pallet, Monitor for moved, status changes, alerts | ⏳ Not Started |
| Logistics | Consume material, return and print, label info, relocate | ⏳ Not Started |
| EOM | Endpoints, Report Pull for finance | ⏳ Not Started |
| OCME | Custom integration | ⏳ Not Started |
| ~~OCME~~ | ~~Custom integration~~ | Canceled |
| API Migration | Moving to new REST endpoints | 🔧 In Progress |
| System | Tests,Builds, Updates, Remote Logging, DB Backups, Alerting | ⏳ Not Started |
@@ -47,4 +47,13 @@ How to run the current version of the app.
git clone https://git.tuffraid.net/cowch/lst_v3.git
cd lst_v3
npm install
npm run dev
```
Rename the .env-example to .env
Update all the fields
```bash
npm run dev:db:migrate
npm run dev
```

View File

@@ -1,7 +1,8 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as opendockAVCheck from "./schema/opendock_articleSetup.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}`;
@@ -20,5 +21,7 @@ const queryClient = postgres(dbURL, {
export const db = drizzle(queryClient, {
schema: {
...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",
{
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(),
appointment: jsonb("appointment").notNull().default([]),
upd_date: timestamp("upd_date").notNull().defaultNow(),
createdAt: timestamp("created_at").notNull().defaultNow(),
},
(table) => ({
releaseIdx: index("opendock_apt_release_idx").on(table.release),
openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on(
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) => [
// 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);
});
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(
downloadDir,
"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);
});
router.get("/upgrade/android/14", (_, res) => {
router.get("/android/upgrade/14", (_, res) => {
const apkPath = path.join(
downloadDir,
"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
.update(scanUser)
.set({ lastScan: sql`NOW()` })
.where(eq(scanUser.name, body.name));
.where(eq(scanUser.name, body.user));
} catch (error) {
console.log(error);
}

View File

@@ -3,7 +3,7 @@ import { addHours } from "date-fns";
import { formatInTimeZone } from "date-fns-tz";
import { eq, sql } from "drizzle-orm";
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 { createLogger } from "../logger/logger.controller.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
@@ -27,6 +27,9 @@ type Releases = {
Quantity: number;
LineItemArticleWeight: number;
CustomerReleaseNumber: string;
DeliveryAddressDescription: string;
DeliveryAddressHumanReadableId: string;
AdditionalInformation1: string;
};
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
@@ -73,6 +76,28 @@ const postRelease = async (release: Releases) => {
log.info({}, "Refreshing Auth Token");
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
* 0 = open
@@ -101,6 +126,7 @@ const postRelease = async (release: Releases) => {
: release.DeliveryState === 4 && "Completed",
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
// 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
refNumbers: [release.ReleaseNumber],
//refNumber: release.ReleaseNumber,
@@ -115,6 +141,19 @@ const postRelease = async (release: Releases) => {
},
units: null,
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",
type: "str",
@@ -190,58 +229,179 @@ const postRelease = async (release: Releases) => {
if (existing) {
const id = existing.openDockAptId;
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);
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;
}
// update the release in the db leaving as insert just incase something weird happened
// set to inprogress
await delay(1500);
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
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,
upd_date: sql`NOW()`,
},
})
.returning();
})
.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.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(
{ error: e },
`Error updating the release: ${release.ReleaseNumber}`,
{ stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ error: e.response.data },
`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 {
try {
@@ -287,13 +447,13 @@ const postRelease = async (release: Releases) => {
log.info({}, `${release.ReleaseNumber} was created`);
} 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
} catch (e: any) {
log.error(
{ error: e?.response?.data },
"Error posting new release to opendock",
{ stack: e?.response?.data },
`Error posting new release to opendock, ${release.ReleaseNumber}`,
);
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 { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import articleCheck from "./opendock.articleCheck.route.js";
import getApt from "./opendockGetRelease.route.js";
@@ -13,4 +14,11 @@ export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
requireAuth,
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 { Router } from "express";
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 { tryCatch } from "../utils/trycatch.utils.js";

View File

@@ -1,8 +1,21 @@
/*
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]'
-- UPDATE msdb.dbo.sysjobs
-- 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]
,[MainMaterialHumanReadableId]
,[MainMaterialDescription]
,[AdditionalInformation1]
,[AdditionalInformation1] -- we will use this to reference as the first check
,[AdditionalInformation2]
,[D365SupplierLot]
,[TradeUnits]
@@ -47,9 +47,9 @@ SELECT
,[PaymentTermsId]
,[PaymentTermsHumanReadableId]
,[PaymentTermsDescription]
,[Remark]
,[Remark]
,[DeliveryAddressId]
,[DeliveryAddressHumanReadableId]
,[DeliveryAddressHumanReadableId] --use this to validate with the new drop or live check
,[DeliveryAddressDescription]
,[DeliveryStreetName]
,[DeliveryAddressZip]

View File

@@ -1,16 +1,17 @@
use [test1_AlplaPROD2.0_Read]
SELECT
--JSON_VALUE(content, '$.EntityId') as labelId
JSON_VALUE(content, '$.EntityId') as labelId,
a.id
,ActorName
,FORMAT(PrintDate, 'yyyy-MM-dd HH:mm') as printDate
--,FORMAT(l.PrintDate, 'yyyy-MM-dd HH:mm') as printDate
,Format(COALESCE(l.PrintDate, e.ProductionDate), 'yyyy-MM-dd HH:mm') as printDate
,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime
,l.ArticleHumanReadableId as av
,l.ArticleDescription as alias
,PrintedCopies
,p.name as printerName
,RunningNumber
,COALESCE(l.ArticleHumanReadableId,e.ArticleHumanReadableId) as av
,COALESCE(l.ArticleDescription, av.Name) as alias
,COALESCE(l.PrintedCopies, 0) as PrintedCopies
,COALESCE(p.name,'External Label not tracked') as printerName
,COALESCE(l.RunningNumber, e.RunningNumber) as runningNumber
--,*
FROM [support].[AuditLog] (nolock) as a
@@ -18,10 +19,20 @@ left join
[labelling].[InternalLabel] (nolock) as l on
l.id = JSON_VALUE(content, '$.EntityId')
OUTER APPLY (
SELECT TOP 1 *
FROM labelling.ExternalLabel e
WHERE e.id = JSON_VALUE(a.content, '$.EntityId')
ORDER BY e.Id DESC
) e
left join
[masterData].[printer] (nolock) as p on
p.id = l.PrinterId
left join
[masterData].[article] (nolock) as av on
av.HumanReadableId = e.ArticleHumanReadableId
where message like '%reprint%'
and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET())
and a.id > [ignoreList]

View File

@@ -4,6 +4,7 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupDockDoorRoutes } from "./dockdoorScanning/dockdoor.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js";
@@ -29,4 +30,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app);
setupDockDoorRoutes(baseUrl, app);
};

View File

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

View File

@@ -1,17 +1,50 @@
import { desc } from "drizzle-orm";
import { db } from "../db/db.controller.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> = {
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"] },
//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> = {
logs: {
seed: async (limit) => {
@@ -48,4 +81,14 @@ export const roomDefinition: Record<RoomId, RoomDefinition> = {
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
import type { RoomId } from "./types.socket.js";
import type { RoomId } from "./roomDefinitions.socket.js";
let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null;

View File

@@ -7,18 +7,33 @@ import {
roomFlushTimers,
roomHistory,
} from "./roomCache.socket.js";
import { roomDefinition } from "./roomDefinitions.socket.js";
import type { RoomId } from "./types.socket.js";
import { type RoomId, roomDefinition } from "./roomDefinitions.socket.js";
// get the db data if not exiting already
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) => {
if (roomHistory.has(roomId)) {
return roomHistory.get(roomId);
}
const roomDef = roomDefinition[roomId];
const roomDef = roomDefinition[roomId] as any;
if (!roomDef) {
log.error({}, `Room ${roomId} is not defined`);
@@ -32,7 +47,7 @@ export const preseedRoom = async (roomId: RoomId) => {
};
export const createRoomEmitter = (io: Server) => {
const addDataToRoom = <T>(roomId: RoomId, payload: T) => {
const addDataToRoom = <T>(roomId: RoomId, payload: T[]) => {
if (!roomHistory.has(roomId)) {
roomHistory.set(roomId, []);
}

View File

@@ -7,7 +7,11 @@ import { Server } from "socket.io";
import { createLogger } from "../logger/logger.controller.js";
import { allowedOrigins } from "../utils/cors.utils.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 __dirname = dirname(__filename);
@@ -15,7 +19,7 @@ const log = createLogger({ module: "socket.io", subModule: "setup" });
import { auth } from "../utils/auth.utils.js";
//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" {
// interface Socket {
@@ -33,6 +37,9 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
},
});
// manage members of the rooms.
registerRoomService(io);
// ✅ Create emitter instance
const { addDataToRoom } = createRoomEmitter(io);
registerEmitter(addDataToRoom);
@@ -78,38 +85,76 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
version: "1.0.0",
});
s.on("join-room", async (rn) => {
const config = protectedRooms[rn];
// s.on("join-room", async (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", {
room: rn,
roomId: rn,
message: `Unknown room: ${rn}`,
});
}
if (config.requiresAuth && !s.user) {
return s.emit("room-error", {
roomId: rn,
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 (config?.role && !roles.includes(s.user?.role)) {
if (roles.length > 0 && !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);
const history = await preseedRoom(rn as any);
log.info({}, `User joined ${rn}: ${s.id}`);
// send the intial data
s.emit("room-update", {
roomId: rn,
payloads: history,
initial: true,
});
});
s.on("leave-room", (room) => {
s.leave(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",
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

View File

@@ -86,8 +86,40 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
seedVersion: 1,
},
{
name: "dockDoorScanning",
value: "0",
active: false,
description: "dock door scanning",
moduleName: "dockDoorScanning",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// 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",
value: "1",
@@ -357,6 +389,17 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
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 () => {

View File

@@ -1,7 +1,9 @@
import net from "node:net";
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.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 { delay } from "../utils/delay.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);
// This is the parser for zebra scanners
const parseTcpAlert = (input: string) => {
// guard
const colonIndex = input.indexOf(":");
@@ -74,6 +77,24 @@ export const startTCPServer = async () => {
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", () => {

View File

@@ -1,11 +1,14 @@
import { createAccessControl } from "better-auth/plugins/access";
import { adminAc } from "better-auth/plugins/admin/access";
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
export const statement = {
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
...defaultStatements,
app: ["read", "create", "update", "delete", "readAll"],
quality: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "update", "delete", "readAll"],
openDock: ["read", "create", "update", "delete"],
notifications: ["read", "create", "update", "delete", "readAll"],
} as const;
export const ac = createAccessControl(statement);
@@ -13,16 +16,50 @@ export const ac = createAccessControl(statement);
export const user = ac.newRole({
app: ["read", "create"],
notifications: ["read", "create"],
openDock: ["read"],
});
export const manager = ac.newRole({
app: ["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({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
user: ["create", "update", "ban"],
openDock: ["read", "create", "update"],
});
export const systemAdmin = ac.newRole({
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
...adminAc.statements,
app: ["read", "create", "update", "delete", "readAll"],
quality: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "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

@@ -13,7 +13,7 @@ import {
//import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import * as rawSchema from "../db/schema/auth.schema.js";
import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
import { ac, admin, manager, systemAdmin, user } from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js";
@@ -163,6 +163,7 @@ export const auth = betterAuth({
roles: {
admin,
user,
manager,
systemAdmin,
},
}),

View File

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

View File

@@ -16,7 +16,8 @@ export interface ReturnHelper<T = unknown[]> {
| "tcp"
| "logistics"
| "admin"
| "mobile";
| "mobile"
| "dockdoor";
subModule: string;
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",
"version": "0.0.0",
"dependencies": {
"@base-ui/react": "^1.5.0",
"@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-form": "^1.28.5",
@@ -34,6 +35,7 @@
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
"vite-imagetools": "^10.0.0",
"zod": "^4.3.6"
},
"devDependencies": {
@@ -439,6 +441,15 @@
"@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": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -484,6 +495,66 @@
"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": {
"version": "0.3.1",
"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_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": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
@@ -1333,6 +1414,519 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
@@ -3163,6 +3757,28 @@
"dev": true,
"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": {
"version": "4.59.0",
"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"
}
},
"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": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -7511,6 +8133,15 @@
"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": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -10460,6 +11091,12 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -10638,7 +11275,6 @@
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -10804,6 +11440,50 @@
"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": {
"version": "2.0.0",
"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": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",

View File

@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-form": "^1.28.5",
@@ -21,6 +22,8 @@
"better-auth": "^1.5.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"lucide-react": "^0.577.0",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
@@ -34,9 +37,8 @@
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
"zod": "^4.3.6",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0"
"vite-imagetools": "^10.0.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@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 {
Sidebar,
SidebarContent,
@@ -8,13 +8,20 @@ import {
} from "@/components/ui/sidebar";
import { useSession } from "@/lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings";
import { permissionQuery } from "../../lib/queries/permsCheck";
import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar";
import MobileBar from "./MobileBar";
import TransportationBar from "./TransportationBar";
export function AppSidebar() {
const { data: session } = useSession();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
const { data: canRead = false } = useQuery(
permissionQuery({
openDock: ["read"],
}),
);
return (
<Sidebar
@@ -32,6 +39,11 @@ export function AppSidebar() {
<MobileBar />
)}
{!isLoading &&
settings.filter((n: any) => n.name === "opendock_sync")[0]
?.active &&
canRead && <TransportationBar />}
{session &&
(session.user.role === "admin" ||
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,21 +1,19 @@
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">) {
return (
<input
type={type}
data-slot="input"
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",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
/>
);
return (
<input
type={type}
data-slot="input"
className={cn(
"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",
className
)}
{...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,5 +1,4 @@
import { Tooltip as TooltipPrimitive } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils";
@@ -40,7 +39,7 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
@@ -52,4 +51,4 @@ function TooltipContent({
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };

View File

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

View File

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

View File

@@ -31,6 +31,13 @@ api.interceptors.response.use(
appRouter?.navigate({ to: "/forbidden", replace: true });
}
if (error.response?.status === 401) {
// redirect, toast, or show forbidden page
toast.error("Unauthorized to be here");
appRouter?.navigate({ to: "/login", replace: true });
}
if (isNetworkError) {
appRouter?.navigate({ to: "/app-down", replace: true });
}

View File

@@ -1,5 +1,10 @@
import { redirect } from "@tanstack/react-router";
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
import {
adminClient,
genericOAuthClient,
usernameClient,
} from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import { ac, admin, manager, systemAdmin, user } from "./auth-permissions";
@@ -16,6 +21,7 @@ export const authClient = createAuthClient({
},
}),
genericOAuthClient(),
usernameClient(),
],
fetchOptions: {
onError() {

View File

@@ -1,13 +1,31 @@
import { createAccessControl } from "better-auth/plugins/access";
import { adminAc } from "better-auth/plugins/admin/access";
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
/*
When new perms are added based on there criteria make sure they are added here as well
*/
type SelectableRole = {
label: string;
value: string;
};
export const selectableRoles: SelectableRole[] = [
{ label: "User", value: "user" },
{ label: "Manager", value: "manager" },
{ label: "Transport", value: "transport" },
{ label: "Admin", value: "admin" },
{ label: "System Admin", value: "systemAdmin" },
];
export const statement = {
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
...defaultStatements,
app: ["read", "create", "update", "delete", "readAll"],
quality: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "update", "delete", "readAll"],
openDock: ["read", "create", "update", "delete"],
notifications: ["read", "create", "update", "delete", "readAll"],
} as const;
export const ac = createAccessControl(statement);
@@ -15,39 +33,79 @@ export const ac = createAccessControl(statement);
export const user = ac.newRole({
app: ["read", "create"],
notifications: ["read", "create"],
openDock: ["read"],
});
export const manager = ac.newRole({
app: ["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({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
user: ["create", "update", "ban"],
openDock: ["read", "create", "update"],
});
export const systemAdmin = ac.newRole({
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
...adminAc.statements,
app: ["read", "create", "update", "delete", "readAll"],
quality: ["read", "create", "update", "delete", "readAll"],
mobile: ["read", "create", "update", "delete", "readAll"],
logistics: ["read", "create", "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"]
},
});
/*
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: {
opendock: ["create"],
logistics: ["delete"]
},
});
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;
options: SelectOption[];
placeholder?: string;
disabled?: boolean;
};
export const SelectField = ({
label,
options,
placeholder,
disabled = false,
}: SelectFieldProps) => {
const field = useFieldContext<string>();
@@ -34,6 +36,7 @@ export const SelectField = ({
<Select
value={field.state.value}
onValueChange={(value) => field.handleChange(value)}
disabled={disabled}
>
<SelectTrigger
id={field.name}

View File

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

@@ -0,0 +1,16 @@
import { queryOptions } from "@tanstack/react-query";
import { authClient } from "@/lib/auth-client";
export function permissionQuery(permissions: Record<string, string[]>) {
return queryOptions({
queryKey: ["permission", permissions],
queryFn: async () => {
const result = await authClient.admin.hasPermission({
permissions,
});
return result.data?.success ?? false;
},
staleTime: 30_000,
});
}

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

View File

@@ -29,30 +29,43 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
const form = useAppForm({
defaultValues: {
email: loginEmail,
login: loginEmail,
password: "",
rememberMe: rememberMe,
},
onSubmit: async ({ value }) => {
// set remember me incase we want it later
const loginValue = value.login.trim();
const isEmailLogin = loginValue.includes("@");
if (value.rememberMe) {
localStorage.setItem("rememberMe", value.rememberMe.toString());
localStorage.setItem("loginEmail", value.email.toLocaleLowerCase());
localStorage.setItem("loginEmail", loginValue.toLocaleLowerCase());
} else {
localStorage.removeItem("rememberMe");
localStorage.removeItem("loginEmail");
}
try {
const login = await authClient.signIn.email({
email: value.email,
password: value.password,
fetchOptions: {
onSuccess: () => {
navigate({ to: redirectPath ?? "/" });
},
},
});
const login = isEmailLogin
? await authClient.signIn.email({
email: loginValue.toLowerCase(),
password: value.password,
fetchOptions: {
onSuccess: () => {
navigate({ to: redirectPath ?? "/" });
},
},
})
: await authClient.signIn.username({
username: loginValue,
password: value.password,
fetchOptions: {
onSuccess: () => {
navigate({ to: redirectPath ?? "/" });
},
},
});
if (login.error) {
toast.error(`${login.error?.message}`);
@@ -95,11 +108,11 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
form.handleSubmit();
}}
>
<form.AppField name="email">
<form.AppField name="login">
{(field) => (
<field.InputField
label="Email"
inputType="email"
label="Username or Email Address"
inputType="text"
required={rememberMe}
/>
)}

View File

@@ -9,6 +9,7 @@ import {
} from "@/components/ui/card";
import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff";
import { Separator } from "../../components/ui/separator";
export const Route = createFileRoute("/(auth)/user/signup")({
component: RouteComponent,
@@ -22,6 +23,7 @@ function RouteComponent() {
email: "",
password: "",
confirmPassword: "",
username: "",
},
onSubmit: async ({ value }) => {
if (value.password !== value.confirmPassword) {
@@ -33,6 +35,7 @@ function RouteComponent() {
name: value.name,
email: value.email,
password: value.password,
username: value.username ?? value.name,
callbackURL: `${window.location.origin}/lst/app`,
});
@@ -71,6 +74,15 @@ function RouteComponent() {
/>
)}
</form.AppField>
<div className="m-2">
<p>Username is option if left blank it will be your name</p>
</div>
<Separator />
<form.AppField name="username">
{(field) => (
<field.InputField label="Username" inputType="text" />
)}
</form.AppField>
{/* Email */}
<form.AppField name="email">

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