24 Commits

Author SHA1 Message Date
f635415b75 fix(opendock): ref wrong field oops
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m0s
2026-06-02 06:25:34 -05:00
40edbc3eae refactor(times): added a better view for times as we save all db as there respective timezone
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m50s
2026-06-01 16:57:38 -05:00
2558b2e5bb refactor(db): added timezone check in so it comes over correct based on the backend timezone 2026-06-01 16:05:36 -05:00
45a0dee9ca refactor(logs): refactored to show the submodule and stack as well to make it more easy to watch 2026-06-01 15:21:17 -05:00
bb7931d6c8 feat(opendock): added in a new article link system
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 4m11s
2026-06-01 14:26:51 -05:00
4f848bb649 fix(logistics): historical checks for no data errors when feature is activeed 2026-06-01 14:24:12 -05:00
f8335f5217 refactor(backend): dock door scanning socket and perms 2026-06-01 14:23:26 -05:00
2a35381fe4 refactor(mobile): intial addin of dockdoor scanning on mobile 2026-06-01 14:22:40 -05:00
e6b92aeb10 test(test): added in vitest to start doing more testing before deploying 2026-06-01 14:21:19 -05:00
da87e2e1d3 refactor(logger): included error in the stack version so we dont have to remove it all 2026-06-01 14:20:48 -05:00
3ef0f230dd fix(mobile): temp removed the autodownload as its causing issues 2026-05-29 06:16:52 -05:00
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
105 changed files with 12082 additions and 571 deletions

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

@@ -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
```
Rename the .env-example to .env
Update all the fields
```bash
npm run dev:db:migrate
npm run dev
```

View File

@@ -46,7 +46,8 @@ const createApp = async () => {
server: ${JSON.stringify(umamiConfig.server ?? "unknown")},
appVersion: ${JSON.stringify(umamiConfig.appVersion ?? "dev")},
umamiHost: ${JSON.stringify(umamiConfig.umamiHost ?? "")},
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")}
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")},
timezone: ${JSON.stringify(process.env.TIMEZONE ?? "America/Chicago")}
};
`);
});

View File

@@ -0,0 +1,68 @@
import express from "express";
import request from "supertest";
import { describe, expect, it, vi } from "vitest";
vi.mock("../db/db.controller.js", () => ({
db: {},
}));
vi.mock("../logger/logger.controller.js", () => ({
createLogger: () => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
}));
vi.mock("./datamart.controller.js", () => ({
runDatamartQuery: vi.fn(async ({ name, options }) => ({
success: true,
message: `Ran ${name}`,
data: {
name,
options,
},
})),
}));
import { runDatamartQuery } from "./datamart.controller.js";
import getDatamartRoute from "./getDatamart.route.js";
function createTestApp() {
const app = express();
app.use(express.json());
app.use("/datamart", getDatamartRoute);
return app;
}
describe("GET /datamart/:name", () => {
it("runs a datamart query by name and returns api response", async () => {
const app = createTestApp();
const res = await request(app).get("/datamart/orders").query({
value: "123",
});
expect(res.status).toBe(200);
expect(runDatamartQuery).toHaveBeenCalledWith({
name: "orders",
options: {
value: "123",
},
});
expect(res.body.success).toBe(true);
expect(res.body.module).toBe("datamart");
expect(res.body.subModule).toBe("query");
expect(res.body.data).toEqual({
name: "orders",
options: {
value: "123",
},
});
});
});

View File

@@ -17,15 +17,15 @@ export const alplaPurchaseHistory = pgTable("alpla_purchase_history", {
status: integer("status"),
statusText: text("status_text"),
journalNum: integer("journal_num"),
add_date: timestamp("add_date").defaultNow(),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user"),
upd_user: text("upd_user"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
remark: text("remark"),
approvedStatus: text("approved_status").default("new"),
position: jsonb("position").default([]),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
export const alplaPurchaseHistorySchema =

View File

@@ -3,7 +3,9 @@ import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const analytics = pgTable("analytics", {
id: uuid("id").defaultRandom().primaryKey(),
createdAt: timestamp("created_at").defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),

View File

@@ -16,13 +16,13 @@ export const jobAuditLog = pgTable(
id: uuid("id").defaultRandom().primaryKey(),
jobName: text("job_name"),
startedAt: timestamp("start_at"),
finishedAt: timestamp("finished_at"),
finishedAt: timestamp("finished_at", { withTimezone: true }),
durationMs: integer("duration_ms"),
status: text("status"), //success | error
errorMessage: text("error_message"),
errorStack: text("error_stack"),
metadata: jsonb("meta_data"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
},
(table) => {
return {

View File

@@ -15,7 +15,7 @@ export const user = pgTable("user", {
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),

View File

@@ -6,5 +6,5 @@ export const deploymentHistory = pgTable("deployment_history", {
buildNumber: integer("build_number").notNull(),
status: text("status").notNull(), // started, success, failed
message: text("message"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});

View File

@@ -28,11 +28,15 @@ export const analyticsDaily = pgTable(
avgDurationMs: integer("avg_duration_ms").notNull(),
maxDurationMs: integer("max_duration_ms").notNull(),
firstHitAt: timestamp("first_hit_at").notNull(),
lastHitAt: timestamp("last_hit_at").notNull(),
firstHitAt: timestamp("first_hit_at", { withTimezone: true }).notNull(),
lastHitAt: timestamp("last_hit_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => [
unique("analytics_daily_business_route_unique").on(

View File

@@ -18,9 +18,9 @@ export const datamart = pgTable("datamart", {
active: boolean("active").default(true),
options: text("options").default(""),
public: boolean("public_access").default(false),
add_date: timestamp("add_date").defaultNow(),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});

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", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date", { withTimezone: true }).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

@@ -20,7 +20,7 @@ export const invHistoricalData = pgTable("inv_historical_data", {
whseId: text("whse_id").default(""),
whseName: text("whse_name").default("missing whseName"),
upd_user: text("upd_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
});
export const invHistoricalDataSchema = createSelectSchema(invHistoricalData);

View File

@@ -18,7 +18,7 @@ export const logs = pgTable("logs", {
stack: jsonb("stack").default([]),
checked: boolean("checked").default(false),
hostname: text("hostname"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});
export const logSchema = createSelectSchema(logs);

View File

@@ -17,8 +17,12 @@ export const opendockApt = pgTable(
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(),
upd_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on(

View File

@@ -22,9 +22,13 @@ export const opendockArticleSetup = pgTable(
customerDescription: text("customer_description").notNull(),
loadType: loadTypeEnum("load_type").notNull().default("drop"),
dock: text("dock").notNull(),
upd_date: timestamp("upd_date").notNull().defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
},
(table) => ({

View File

@@ -6,9 +6,13 @@ 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_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
});

View File

@@ -7,5 +7,5 @@ export const printerLog = pgTable("printer_log", {
printerSN: text("printer_sn"),
condition: text("condition").notNull(),
message: text("message"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});

View File

@@ -28,8 +28,8 @@ export const printerData = pgTable(
printDelay: integer("printDelay").default(90),
processes: jsonb("processes").default([]),
printDelayOverride: boolean("print_delay_override").default(false), // this will be more for if we have the lot time active but want to over ride this single line for some reason
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => [
//uniqueIndex("emailUniqueIndex").on(sql`lower(${table.email})`),

View File

@@ -30,8 +30,8 @@ export const scanUser = pgTable(
role: mobileRoleEnum("role").notNull().default("user"),
active: boolean("active").default(true),
lastScan: timestamp("last_scan").defaultNow(),
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => ({
userNotificationUnique: unique("scan_user_unique").on(

View File

@@ -13,7 +13,7 @@ export const scanLog = pgTable("scan_log", {
status: text("status"),
scannerVersion: text("scanner_version").default("0"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_date").defaultNow(),
add_Date: timestamp("add_date", { withTimezone: true }).defaultNow(),
});
export const scanLogSchema = createSelectSchema(scanLog);

View File

@@ -22,7 +22,7 @@ export const serverData = pgTable(
contactPhone: text("contact_phone"),
active: boolean("active").default(true),
serverLoc: text("server_loc"),
lastUpdated: timestamp("last_updated").defaultNow(),
lastUpdated: timestamp("last_updated", { withTimezone: true }).defaultNow(),
buildNumber: integer("build_number"),
isUpgrading: boolean("is_upgrading").default(false),
},

View File

@@ -32,9 +32,9 @@ export const settings = pgTable(
settingType: settingType(),
seedVersion: integer("seed_version").default(1), // this is intended for if we want to update the settings.
add_User: text("add_User").default("LST_System").notNull(),
add_Date: timestamp("add_Date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_User").default("LST_System").notNull(),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => [
// uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),

View File

@@ -12,11 +12,11 @@ import type z from "zod";
export const appStats = pgTable("app_stats", {
id: text("id").primaryKey().default("primary"),
currentBuild: integer("current_build").notNull().default(1),
lastBuildAt: timestamp("last_build_at"),
lastDeployAt: timestamp("last_deploy_at"),
lastBuildAt: timestamp("last_build_at", { withTimezone: true }),
lastDeployAt: timestamp("last_deploy_at", { withTimezone: true }),
building: boolean("building").notNull().default(false),
updating: boolean("updating").notNull().default(false),
lastUpdated: timestamp("last_updated").defaultNow(),
lastUpdated: timestamp("last_updated", { withTimezone: true }).defaultNow(),
meta: jsonb("meta").$type<Record<string, unknown>>().default({}),
});

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,70 @@
import { eq, 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 endLoading = z.object({
loadingOrder: z.string(),
dockId: z.string(),
});
r.post("/", async (req, res) => {
// close the loading order
// clear the loading order off the dock
try {
const validated = endLoading.parse(req.body);
const { data, error } = await tryCatch(
db
.update(dockDoorScanners)
.set({
currentLoadingOrder: "",
upd_date: sql`NOW()`,
upd_user: req.user?.username,
})
.where(eq(dockDoorScanners.dockId, validated.dockId))
.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 closed.`,
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,25 @@
import { Router } from "express";
import { apiReturn } from "../utils/returnHelper.utils.js";
import loadUnit from "./dockdoor.loadUnits.js";
const r = Router();
r.post("/", async (req, res) => {
const unit = await loadUnit({
dockId: req.body.dockId,
runningNo: req.body.runningNo,
});
return apiReturn(res, {
success: unit.success,
level: "info",
module: "dockdoor",
subModule: "loadingUnit",
message: unit.message,
data: unit?.data ?? [],
status: unit.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,143 @@
// 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 { emitToRoom } from "../socket.io/roomEmitter.socket.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;
runningNo?: string;
};
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 unitToScan = data.sscc
? { sscc: data.sscc?.slice(2) }
: { runningNo: Number(data.runningNo) };
const prod = (await runProdApi({
method: "post",
endpoint: `/public/v1.0/OutboundDeliveries/LoadingOrders/${dock[0]?.currentLoadingOrder}/LoadUnit`,
data: [unitToScan],
})) as any;
//emitToRoom(`dockDoorLoading:${data.dockId}`, prod?.data ?? []);
if (!prod?.success) {
emitToRoom(`dockDoorLoading:${data.dockId}`, prod?.data.errors[0]);
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message: `Unit encountered an error while loading`,
data: prod?.data.errors[0] as any,
notify: false,
//room: `dockDoorLoading:${data.dockId}`,
});
} else {
const emitData = {
message: `The unit ${prod.data.message.messageParams.runningNo} was loaded.`,
data: prod.data,
code: 0,
};
emitToRoom(`dockDoorLoading:${data.dockId}`, emitData as any);
return returnFunc({
success: true,
level: "info",
module: "dockdoor",
subModule: "loadUnit",
message: `Unit added to loading order`,
data: [
{
message: `The unit ${prod.data.message.messageParams.runningNo} was loaded.`,
data: prod.data,
code: 0,
},
] as any,
notify: false,
//room: `dockDoorLoading:${data.dockId}`,
});
}
} catch (error) {
console.log(error);
return returnFunc({
success: true,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message: `Failed to load unit`,
data: error as any,
notify: false,
room: `dockDoorLoading:${data.dockId}`,
});
}
};
export default loadUnit;

View File

@@ -0,0 +1,48 @@
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 load from "./dockdoor.loadUnits.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/finishOrder`,
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,
);
app.use(
`${baseUrl}/api/dockDoor/loadUnit`,
featureCheck("dockDoorScanning"),
load,
);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,66 @@
import { eq, 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,
})
.where(eq(dockDoorScanners.dockId, validated.dockId))
.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

@@ -37,7 +37,7 @@ const dbStream = new Writable({
subModule: obj?.subModule?.toLowerCase(),
hostname: obj?.hostname?.toLowerCase(),
message: obj.msg,
stack: obj?.stack,
stack: obj?.stack || obj?.error, // this will add in the error or stack depending on how we pass it.
})
.returning(),
);

View File

@@ -49,7 +49,7 @@ const historicalInvImport = async () => {
});
}
if (data?.length === 0) {
if (data.length === 0) {
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
if (!avSQLQuery.success) {
@@ -139,7 +139,7 @@ const historicalInvImport = async () => {
subModule: "inv",
message: `Error adding historical data to lst db`,
data: errorImport as any,
notify: true,
notify: false,
});
}

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

@@ -31,6 +31,14 @@ type Releases = {
DeliveryAddressHumanReadableId: string;
AdditionalInformation1: string;
};
// TODO: add these docs into the db
const actaulDocks = [
{ name: "cermac", dockId: "bcb17fae-0b1a-47a7-9fbf-594c5ebccce9" },
{ name: "matrix", dockId: "3e32cbfc-49f4-4138-b491-9d5df9c94754" },
{ name: "gerber", dockId: "9109e789-6c15-4cd9-87cb-de1b18627b6d" },
{ name: "rb", dockId: "6be02526-6183-4789-a73f-e0aa155e6d1e" },
];
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
@@ -64,6 +72,7 @@ let lastCheck = formatInTimeZone(
// };
const postRelease = async (release: Releases) => {
log.debug({}, `Release: ${release.ReleaseNumber} is about to be validated`);
if (!odToken.odToken) {
log.info({}, "Getting Auth Token");
await getToken();
@@ -82,21 +91,55 @@ const postRelease = async (release: Releases) => {
where: (u, { eq }) => eq(u.name, "defaultLoadType"),
});
// check if the release has the data in it
// check if the release has the loadtype in it
const releaseLoadtypeCheck = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("drop");
// allowed to schedule now, as long as we see od in here somewhere
const releaseOkToSchedule = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("od");
// dock was sent over
const releaseDockInfo = actaulDocks.some((dock) =>
(release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes(dock.name.toLowerCase()),
);
const opendDockArticleCheck = await db.query.opendockArticleSetup.findFirst({
where: (table, { and, eq }) =>
and(
eq(table.av, release.LineItemArticleWeight),
eq(table.av, release.LineItemHumanReadableId),
eq(table.customer, release.DeliveryAddressHumanReadableId),
),
});
// selected dock
const releaseDocks = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim());
const matchedDock = actaulDocks.find((dock) =>
releaseDocks.includes(dock.name.toLowerCase()),
);
const setDock =
// validate we dont have the dock in the release
releaseDockInfo
? matchedDock?.dockId
: // validate we dont have the dock in the aritcle check
(actaulDocks.find((d) => d.name === opendDockArticleCheck?.dock)
?.dockId ?? process.env.DEFAULT_DOCK);
// TODO: add in docks from lst db here to make it more universal for the team
/**
* ReleaseState
@@ -127,7 +170,7 @@ const postRelease = async (release: Releases) => {
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
dockId: setDock, // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber],
//refNumber: release.ReleaseNumber,
start: release.DeliveryDate,
@@ -403,7 +446,12 @@ const postRelease = async (release: Releases) => {
return;
}
}
} else {
} else if (
(releaseLoadtypeCheck ||
opendDockArticleCheck?.loadType === "drop" ||
defaultDock?.value === "drop") &&
releaseOkToSchedule
) {
try {
const response = await axios.post(
`${process.env.OPENDOCK_URL}/appointment`,
@@ -458,6 +506,76 @@ const postRelease = async (release: Releases) => {
return;
}
} else {
// try {
// const response = await axios.post(
// `${process.env.OPENDOCK_URL}/appointment`,
// newDockApt,
// {
// headers: {
// "content-type": "application/json; charset=utf-8",
// Authorization: `Bearer ${odToken.odToken}`,
// },
// },
// );
// // we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
// // this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.
// if (response.status === 400) {
// log.error({}, response.data.data.message);
// return;
// }
// // the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
// // TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
// //console.info(response.data.data, "Was Created");
// 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 created`);
// } catch (e) {
// log.error({ stack: e }, "Error creating new release");
// }
// // biome-ignore lint/suspicious/noExplicitAny: to many possibilities
// } catch (e: any) {
// log.error(
// { stack: e?.response?.data },
// `Error posting new release to opendock, ${release.ReleaseNumber}`,
// );
// return;
// }
log.info(
{
stack: {
release: release.ReleaseNumber,
releaseLoadtypeCheck,
articleLoadType: opendDockArticleCheck?.loadType,
defaultLoadType: defaultDock?.value,
releaseOkToSchedule,
},
},
`Skipping OpenDock post - release: ${release.ReleaseNumber} is not allowed to schedule`,
);
return;
}
await delay(750); // rate limit protection

View File

@@ -1,11 +1,80 @@
SELECT count(*) as activated
FROM [test1_AlplaPROD2.0_Read].[support].[FeatureActivation]
where feature in (108,7)
where feature in (7)
/*
as more features get activated and need to have this checked to include the new endpoints add here so we can check this.
108 = waste
7 = warehousing
[DefaultTranslation("Blocking")]
Blocking = 1,
[DefaultTranslation("Users")]
UserManagement = 2,
[DefaultTranslation("Complaint Handling")]
ComplaintHandling = 3,
[DefaultTranslation("Demand Management")]
DemandManagement = 4,
[DefaultTranslation("Issue Material")]
IssueMaterial = 5,
[DefaultTranslation("Production Controlling")]
ProductionControlling = 6,
[DefaultTranslation("Warehousing")]
Warehousing = 7,
[DefaultTranslation("Outbound Deliveries")]
OutboundDeliveries = 8,
[DefaultTranslation("Production Scheduling")]
ProductionScheduling = 9,
[DefaultTranslation("Advanced Scheduling")]
AdvancedScheduling = 10,
[DefaultTranslation("Material Requirements Planning")]
MaterialRequirementsPlanning = 11,
[DefaultTranslation("Production Labelling")]
ProductionLabelling = 12,
[SpecialProcess]
[DefaultTranslation("Accounting")]
Accounting = 100,
[SpecialProcess]
[DefaultTranslation("Irradiation")]
Irradiation = 101,
[SpecialProcess]
[DefaultTranslation("Central Moulds")]
CentralMoulds = 102,
[SpecialProcess]
[DefaultTranslation("Maintenance")]
Maintenance = 103,
[SpecialProcess]
[DefaultTranslation("Disable Manual Bookings")]
DisableManualBookings = 104,
[SpecialProcess]
[DefaultTranslation("Purchasing")]
Purchasing = 105,
[SpecialProcess]
[DefaultTranslation("Tracing")]
Tracing = 106,
[SpecialProcess]
[DefaultTranslation("AlplaERP (D365)")]
AlplaErp = 107,
[SpecialProcess]
[DefaultTranslation("AI chatbot")]
AiChatBot = 108
*/

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

@@ -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,57 @@
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"
| "dockDoorLoading:2";
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: {},
"dockDoorLoading:2": {},
};
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 +88,22 @@ 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;
},
},
// TODO: add in dynamic room seeding
"dockDoorLoading:2": {
seed: async (limit) => {
console.log(limit);
return [];
},
},
};

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

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,23 @@ export const startTCPServer = async () => {
printerListen(printerData as PrinterData);
}
// check if its a dock door scanner
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

@@ -13,7 +13,14 @@ 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, manager, systemAdmin, user } from "./auth.permissions.js";
import {
ac,
admin,
manager,
systemAdmin,
transport,
user,
} from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js";
@@ -164,6 +171,7 @@ export const auth = betterAuth({
admin,
user,
manager,
transport,
systemAdmin,
},
}),

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

@@ -0,0 +1,49 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -11,9 +11,12 @@ type RoomErrorPayload = {
message?: string;
};
type UpdateMode = "append" | "replace";
export function useSocketRoom<T>(
roomId: string,
getKey?: (item: T) => string | number,
updateMode: UpdateMode = "append",
) {
const [data, setData] = useState<T[]>([]);
const [info, setInfo] = useState(
@@ -44,7 +47,13 @@ export function useSocketRoom<T>(
// protects against other room updates hitting this hook
if (payload.roomId !== roomId) return;
// resetting room data for rooms that just need updated data.
if (updateMode === "replace") {
setData(payload.payloads);
} else {
setData((prev) => [...payload.payloads, ...prev]);
}
setInfo("");
}
@@ -53,14 +62,14 @@ export function useSocketRoom<T>(
setInfo(err.message ?? "Room error");
}
if (!socket.connected) {
socket.connect();
}
socket.on("connect", handleConnect);
socket.on("room-update", handleUpdate);
socket.on("room-error", handleError);
if (!socket.connected && socket.disconnected) {
socket.connect();
}
// If already connected, join immediately
if (socket.connected) {
socket.emit("join-room", roomId);
@@ -74,7 +83,18 @@ export function useSocketRoom<T>(
socket.off("room-update", handleUpdate);
socket.off("room-error", handleError);
};
}, [roomId]);
}, [roomId, updateMode]);
return { data, info, clearRoom };
}
/*
const isDockDoorPage = location.pathname.startsWith("/dockdoor");
useSocketRoom(
dockId ? `dockdoor:${dockId}` : null,
isDockDoorPage,
);
*/

View File

@@ -5,6 +5,7 @@ type RuntimeConfig = {
appVersion: string;
umamiHost: string;
umamiWebsiteId: string;
timezone: string;
};
declare global {
@@ -23,6 +24,7 @@ export const runtimeConfig: RuntimeConfig = {
appVersion: window.LST_CONFIG?.appVersion ?? "dev",
umamiHost: window.LST_CONFIG?.umamiHost ?? "",
umamiWebsiteId: window.LST_CONFIG?.umamiWebsiteId ?? "",
timezone: window.LST_CONFIG?.timezone ?? "America/Chicago",
};
export function loadUmami() {

View File

@@ -1,6 +1,16 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import { formatInTimeZone } from "date-fns-tz";
import { useSocketRoom } from "@/hooks/socket.io.hook";
import { authClient } from "@/lib/auth-client";
import { Badge } from "../../components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
export const Route = createFileRoute("/admin/logs")({
beforeLoad: async ({ location }) => {
@@ -37,117 +47,168 @@ interface LogEntry {
[key: string]: any; // catch any extra fields
}
function LevelBadge({ level }: { level: number }) {
const config: Record<number, { label: string; className: string }> = {
10: { label: "TRACE", className: "bg-gray-100 text-gray-600" },
20: { label: "DEBUG", className: "bg-blue-100 text-blue-700" },
30: { label: "INFO", className: "bg-green-100 text-green-700" },
40: { label: "WARN", className: "bg-yellow-100 text-yellow-700" },
50: { label: "ERROR", className: "bg-red-100 text-red-700" },
60: { label: "FATAL", className: "bg-purple-100 text-purple-700" },
function LevelBadge({ level }: { level: string }) {
const config: Record<string, { label: string; className: string }> = {
trace: { label: "TRACE", className: "bg-gray-100 text-gray-600" },
debug: { label: "DEBUG", className: "bg-blue-50 text-blue-700" },
info: { label: "INFO", className: "bg-green-50 text-green-700" },
warn: { label: "WARN", className: "bg-yellow-100 text-yellow-700" },
error: { label: "ERROR", className: "bg-red-50 text-red-700" },
fatal: { label: "FATAL", className: "bg-purple-100 text-purple-700" },
};
const { label, className } = config[level] ?? {
label: String(level),
className: "bg-gray-100",
};
const badge = config[level];
return (
<span className={`px-2 py-0.5 rounded text-xs font-medium ${className}`}>
{label}
</span>
<Badge
className={`rounded px-2 py-0.5 text-xs font-medium ${
badge?.className ?? "bg-gray-100 text-gray-600"
}`}
>
{badge?.label ?? level}
</Badge>
);
}
function RouteComponent() {
const { data: logs, info: logsInfo } = useSocketRoom<LogEntry>("logs");
//const { user } = Route.useRouteContext();
//const router = useRouter();
// const [logs, setLogs] = useState<LogEntry[]>([]);
// const [logsInfo, setLogInfo] = useState(
// "No logs yet — join the room to start receiving",
// );
const { data: logs } = useSocketRoom<LogEntry>("logs");
const columnHelper = createColumnHelper<any>();
// useEffect(() => {
// // Connect if not already connected
// if (!socket.connected) {
// socket.connect();
// }
// socket.on("connect", () => {
// socket.emit("join-room", "logs");
// });
// socket.emit("join-room", "logs");
// socket.on(
// "room-update",
// (data: { payloads: LogEntry[]; roomId: string }) => {
// setLogs((prev) => [...data.payloads, ...prev]);
// },
// );
// socket.on("room-error", (data) => {
// setLogInfo(data.message);
// });
// // socket.on("logs", (data) => {
// // console.log(data);
// // setLogs((prev) => [...data.payloads, ...prev]);
// // });
// // Cleanup listeners on unmount
// return () => {
// socket.emit("leave-room", "logs");
// socket.off("room-update");
// socket.off("room-error");
// socket.off("logs");
// };
// }, []);
return (
<div>
{/* Log Table */}
<div className="rounded border overflow-auto max-h-[600px]">
<table className="w-full text-sm">
<thead className="bg-muted sticky top-0">
<tr>
<th className="text-left px-3 py-2">Time</th>
<th className="text-left px-3 py-2">Level</th>
<th className="text-left px-3 py-2">Module</th>
<th className="text-left px-3 py-2">Host</th>
<th className="text-left px-3 py-2">Message</th>
</tr>
</thead>
<tbody>
{logs.length === 0 ? (
<tr>
<td
colSpan={6}
className="text-center py-6 text-muted-foreground"
>
{logsInfo}
</td>
</tr>
const column = [
columnHelper.accessor("createdAt", {
header: ({ column }) => <SearchableHeader column={column} title="Time" />,
filterFn: "includesString",
cell: (i) =>
formatInTimeZone(
i.getValue(),
`${window.LST_CONFIG?.timezone}`,
"MM/dd/yyyy HH:mm:ss",
),
}),
columnHelper.accessor("level", {
header: ({ column }) => (
<SearchableHeader column={column} title="Level" searchable={true} />
),
filterFn: "includesString",
cell: (i) => <LevelBadge level={i.getValue()} />,
}),
columnHelper.accessor("module", {
header: ({ column }) => (
<SearchableHeader column={column} title="Module" />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("subModule", {
header: ({ column }) => (
<SearchableHeader column={column} title="Submodule" />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("hostname", {
header: ({ column }) => (
<SearchableHeader column={column} title="Client" />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("message", {
header: ({ column }) => (
<SearchableHeader column={column} title="Message" />
),
filterFn: "includesString",
cell: (i) => {
return i.getValue().length > 50 ? (
<Tooltip>
<TooltipTrigger>
<p>{i.getValue().slice(0, 50)}...</p>
</TooltipTrigger>
<TooltipContent>
<p className="text-wrap max-w-48">{i.getValue()}</p>
</TooltipContent>
</Tooltip>
) : (
logs.map((log, i) => (
<tr
key={`${log.id}-${i}`}
className="border-t hover:bg-muted/50"
i.getValue()
);
},
}),
columnHelper.accessor("stack", {
header: ({ column }) => (
<SearchableHeader column={column} title="Stack" />
),
filterFn: "includesString",
cell: (i) => {
const stack = i.row.original.stack;
if (!stack) return <span className="text-muted-foreground"></span>;
return (
<Tooltip>
<TooltipTrigger>
<p className="max-w-[350px] truncate text-left font-mono text-xs text-muted-foreground hover:text-foreground">
{stack.length !== 0 ? "Hover to see stack error" : "-"}
</p>
</TooltipTrigger>
<TooltipContent
side="left"
align="start"
className="max-w-[900px] bg-zinc-950 text-zinc-50"
>
<td className="px-3 py-2 whitespace-nowrap">
{new Date(log.createdAt).toLocaleTimeString()}
</td>
<td className="px-3 py-2">
<LevelBadge level={log.level} />
</td>
<td className="px-3 py-2">{log.module}</td>
<td className="px-3 py-2">{log.hostname}</td>
<td className="px-3 py-2 max-w-sm truncate">{log.message}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
<pre className="max-h-[500px] overflow-auto whitespace-pre-wrap rounded-md p-3 text-xs leading-relaxed">
<code>{JSON.stringify(stack, null, 2)}</code>
</pre>
</TooltipContent>
</Tooltip>
);
},
}),
];
return (
// <div>
// {/* Log Table */}
// <div className="rounded border overflow-auto max-h-[600px]">
// <table className="w-full text-sm">
// <thead className="bg-muted sticky top-0">
// <tr>
// <th className="text-left px-3 py-2">Time</th>
// <th className="text-left px-3 py-2">Level</th>
// <th className="text-left px-3 py-2">Module</th>
// <th className="text-left px-3 py-2">Host</th>
// <th className="text-left px-3 py-2">Message</th>
// </tr>
// </thead>
// <tbody>
// {logs.length === 0 ? (
// <tr>
// <td
// colSpan={6}
// className="text-center py-6 text-muted-foreground"
// >
// {logsInfo}
// </td>
// </tr>
// ) : (
// logs.map((log, i) => (
// <tr
// key={`${log.id}-${i}`}
// className="border-t hover:bg-muted/50"
// >
// <td className="px-3 py-2 whitespace-nowrap">
// {new Date(log.createdAt).toLocaleTimeString()}
// </td>
// <td className="px-3 py-2">
// <LevelBadge level={log.level} />
// </td>
// <td className="px-3 py-2">{log.module}</td>
// <td className="px-3 py-2">{log.hostname}</td>
// <td className="px-3 py-2 max-w-sm truncate">{log.message}</td>
// </tr>
// ))
// )}
// </tbody>
// </table>
// </div>
// </div>
<LstTable data={logs} columns={column} pageSize={50} />
);
}

View File

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

View File

@@ -1,6 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import z from "zod";
import { useSession } from "../lib/auth-client";
import { trackLstEvent } from "../lib/umami.utils";

View File

@@ -132,6 +132,10 @@ export default function NewArticleLink({ refetch }: { refetch: any }) {
label: "Matrix",
value: "matrix",
},
{
label: "RB",
value: "rb",
},
];
return (

View File

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

View File

@@ -16,7 +16,7 @@
"@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0",
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-query": "^5.100.14",
"axios": "^1.15.0",
"babel-preset-expo": "^55.0.18",
"class-variance-authority": "^0.7.1",
@@ -35,6 +35,7 @@
"expo-image": "~55.0.8",
"expo-linking": "~55.0.13",
"expo-router": "~55.0.12",
"expo-screen-orientation": "~55.0.16",
"expo-splash-screen": "~55.0.18",
"expo-status-bar": "~55.0.5",
"expo-symbols": "~55.0.7",
@@ -46,6 +47,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.4",
"react-native-blob-util": "^0.24.9",
"react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
@@ -5316,9 +5318,9 @@
"license": "MIT"
},
"node_modules/@tanstack/query-core": {
"version": "5.100.9",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz",
"integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==",
"version": "5.100.14",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz",
"integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==",
"license": "MIT",
"funding": {
"type": "github",
@@ -5326,12 +5328,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.100.9",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz",
"integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==",
"version": "5.100.14",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz",
"integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.100.9"
"@tanstack/query-core": "5.100.14"
},
"funding": {
"type": "github",
@@ -6132,6 +6134,11 @@
}
}
},
"node_modules/base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -8171,6 +8178,16 @@
"node": ">=10"
}
},
"node_modules/expo-screen-orientation": {
"version": "55.0.16",
"resolved": "https://registry.npmjs.org/expo-screen-orientation/-/expo-screen-orientation-55.0.16.tgz",
"integrity": "sha512-I9NIqb2zAkHsK/CxdmMdmgSFP7E1v++8z/Mj2X9j1AuK6l55yOma/JHo905KU3x2zPm9/l1BTzmMA320tiBebg==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-server": {
"version": "55.0.9",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.9.tgz",
@@ -12821,6 +12838,77 @@
}
}
},
"node_modules/react-native-blob-util": {
"version": "0.24.9",
"resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.24.9.tgz",
"integrity": "sha512-tG3+m0WhVdBGifvxSFxZDVqtr85D0fGBJU6E4UxmK3tU+RabJZTumXEn8k7jn5/NFe8OhQhPjtBEZ11ZJ6L7Vw==",
"license": "MIT",
"dependencies": {
"base-64": "0.1.0",
"glob": "13.0.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ronradtke"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-blob-util/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/react-native-blob-util/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/react-native-blob-util/node_modules/glob": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz",
"integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==",
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.1.2",
"minipass": "^7.1.2",
"path-scurry": "^2.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/react-native-blob-util/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/react-native-css-interop": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.2.3.tgz",

View File

@@ -26,7 +26,7 @@
"@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0",
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-query": "^5.100.14",
"axios": "^1.15.0",
"babel-preset-expo": "^55.0.18",
"class-variance-authority": "^0.7.1",
@@ -45,6 +45,7 @@
"expo-image": "~55.0.8",
"expo-linking": "~55.0.13",
"expo-router": "~55.0.12",
"expo-screen-orientation": "~55.0.16",
"expo-splash-screen": "~55.0.18",
"expo-status-bar": "~55.0.5",
"expo-symbols": "~55.0.7",
@@ -56,6 +57,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.4",
"react-native-blob-util": "^0.24.9",
"react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",

View File

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

View File

@@ -1,26 +1,134 @@
import React from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
import { useSuspenseQuery } from "@tanstack/react-query";
import * as Device from "expo-device";
import { Link } from "expo-router";
import {
Button,
ScrollView,
Text,
useWindowDimensions,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
export default function LaneCheck() {
const getInfo = async () => {
const info = "ho";
import { Card, CardContent } from "../../components/ui/card";
import { getActiveLoadingOrders } from "../../lib/queryStuff/getActiveLoadingOrders";
import { getDocks } from "../../lib/queryStuff/getDocks";
console.log(info);
export default function DockScan() {
const { data } = useSuspenseQuery(getDocks());
const {
data: loadingOrders,
refetch,
isLoading,
} = useSuspenseQuery(getActiveLoadingOrders());
const { width } = useWindowDimensions();
const isTablet =
Device.modelName?.toLowerCase().includes("et40") ||
Device.modelName?.toLowerCase().includes("et45");
const columns = isTablet ? 3 : 1;
const gap = 8;
const cardWidth =
columns === 1 ? width - 16 : (width - gap * (columns + 1)) / columns;
const updateLoadingOrders = () => {
refetch();
Toast.show({
type: "success",
text1: `Refreshing Loading Orders`,
});
};
if (isLoading)
return (
<SafeAreaView>
<Text>Loading...</Text>
</SafeAreaView>
);
return (
<View className="flex">
<View
style={{
flex: 1,
// flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>Dock Scanning</Text>
<Button onPress={getInfo}>
<Text>Check info</Text>
</Button>
<Text className="text-2xl text-bold">Dock Scanning</Text>
<Button title="Update Loading Orders" onPress={updateLoadingOrders} />
</View>
<View>
<SafeAreaView className="flex">
<ScrollView className="w-full">
<View className="w-full flex-row flex-wrap gap-2 m-2">
{data.map((i: any) => {
const loadingPlan =
i.currentLoadingOrder !== ""
? loadingOrders.filter(
(x: any) => x.id === Number(i.currentLoadingOrder),
)
: [];
return (
<View key={i.id}>
<Link
href={{
pathname: "/dock/[id]",
params: {
id: i.dockId.toString(),
currentLoading: i.currentLoadingOrder,
},
}}
>
<Card
style={{
borderWidth: 4,
width: cardWidth,
}}
>
<CardContent>
<Text>{i.name}</Text>
{i.currentLoadingOrder === "" ? (
<Text>Tap to active new loading order</Text>
) : (
<View>
<Text>
Current Loading order : {i.currentLoadingOrder}
</Text>
{loadingPlan && loadingPlan.length > 0 && (
<View>
<Text>
{`${loadingPlan[0].loadingPlanItems[0].articleId} - ${loadingPlan[0].loadingPlanItems[0].articleDescription}`}
</Text>
<Text>
Current Loaded :{" "}
{
loadingPlan[0].loadingPlanItems[0]
.loadedQuantityLUs
}{" "}
/{" "}
{
loadingPlan[0].loadingPlanItems[0]
.plannedQuantityLUs
}
</Text>
</View>
)}
</View>
)}
</CardContent>
</Card>
</Link>
</View>
);
})}
</View>
</ScrollView>
</SafeAreaView>
</View>
</View>
);
}

View File

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

View File

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

View File

@@ -2,19 +2,27 @@ import { PortalHost } from "@rn-primitives/portal";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import "../../global.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { useEffect } from "react";
import Toast from "react-native-toast-message";
import useDeviceLock from "../hooks/useDeviceCheck";
import { useDeviceOrientationLock } from "../hooks/useDeviceOrientationLock";
import { queryClient } from "../lib/queryStuff/queryClient";
import { connectSocket } from "../lib/socket.io";
import { zebraScanner } from "../lib/ZebraScanner";
export default function RootLayout() {
useDeviceLock();
useDeviceOrientationLock();
useEffect(() => {
zebraScanner.ensureProfile();
connectSocket();
}, []);
return (
<>
<QueryClientProvider client={queryClient}>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
@@ -24,6 +32,8 @@ export default function RootLayout() {
<Stack.Screen name="(tabs)" />
</Stack>
<PortalHost />
</QueryClientProvider>
<Toast />
</>
);

View File

@@ -0,0 +1,169 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import * as Device from "expo-device";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useState } from "react";
import {
Button,
Pressable,
ScrollView,
Text,
useWindowDimensions,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import { Card, CardContent } from "../../components/ui/card";
import { api } from "../../lib/apiHelper";
import { getActiveLoadingOrders } from "../../lib/queryStuff/getActiveLoadingOrders";
export default function DockPage() {
const { id, currentLoading } = useLocalSearchParams<{
id: string;
currentLoading: string;
}>();
const router = useRouter();
const [active] = useState(currentLoading !== "");
const { width } = useWindowDimensions();
const isTablet =
Device.modelName?.toLowerCase().includes("et40") ||
Device.modelName?.toLowerCase().includes("et45");
const columns = isTablet ? 3 : 1;
const gap = 8;
const cardWidth =
columns === 1 ? width - 16 : (width - gap * (columns + 1)) / columns;
const {
data: loadingOrders,
refetch,
isLoading,
} = useSuspenseQuery(getActiveLoadingOrders());
const dockFilter = loadingOrders.filter((i: any) => i.dockId === Number(id));
// add in start loading order, if this is already on the dock we will disabled and change to view current pallets
const startLoad = async (loadingOrder: string, dockId: string) => {
try {
const res = await api.post("/dockDoor/startLoad", {
loadingOrder,
dockId,
});
if (res.status === 200) {
Toast.show({ type: "success", text1: res.data.message });
refetch();
return;
}
} catch (error) {
Toast.show({
type: "error",
text1: JSON.stringify(error),
});
}
};
const endLoad = async (loadingOrder: string, dockId: string) => {
try {
const res = await api.post("/dockDoor/endLoad", {
loadingOrder,
dockId,
});
if (res.status === 200) {
Toast.show({ type: "success", text1: res.data.message });
refetch();
return;
}
} catch (error) {
Toast.show({
type: "error",
text1: JSON.stringify(error),
});
}
};
// add in ending loading order disabeled until all pallets are loaded.
if (isLoading)
return (
<SafeAreaView>
<Text>Loading</Text>
</SafeAreaView>
);
return (
<SafeAreaView>
<View className="flex flex-row justify-between gap-1 ml-1 mr-1">
<View>
<Pressable
onPress={() => router.back()}
className="self-start rounded-xl bg-gray-200 px-4 py-2"
>
<Text className="font-semibold"> Back</Text>
</Pressable>
</View>
<Text className="text-xl mt-1">{dockFilter[0].dockDescription}</Text>
<View>
<Pressable
onPress={() =>
router.push({
pathname: "/dock/scans/[scanner]",
params: {
scanner: id,
},
})
}
className="self-start rounded-xl bg-gray-200 px-4 py-2"
>
<Text className="font-semibold">Scans</Text>
</Pressable>
</View>
</View>
<ScrollView>
<View className="w-full flex-row flex-wrap gap-2 m-2">
{dockFilter.map((i: any) => {
return (
<View key={i.id}>
<Card
style={{
borderWidth: 4,
width: cardWidth,
}}
>
<CardContent>
<View>
<Text>Loading Order: {dockFilter[0].id}</Text>
<Text>
{`${dockFilter[0].loadingPlanItems[0].articleId} - ${dockFilter[0].loadingPlanItems[0].articleDescription}`}
</Text>
<Text>
Current Loaded :{" "}
{dockFilter[0].loadingPlanItems[0].loadedQuantityLUs} /{" "}
{dockFilter[0].loadingPlanItems[0].plannedQuantityLUs}
</Text>
</View>
<View className="mt-2 flex flex-row gap-2 justify-between">
<Button
title="Start Load"
onPress={() =>
startLoad(dockFilter[0].id.toString(), id)
}
disabled={active}
/>
<Button
title="End Load"
onPress={() => endLoad(dockFilter[0].id.toString(), id)}
disabled={active}
/>
</View>
</CardContent>
</Card>
</View>
);
})}
</View>
</ScrollView>
</SafeAreaView>
);
}

View File

@@ -0,0 +1,71 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useRouter } from "expo-router";
import { Pressable, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Card } from "../../../components/ui/card";
import { useSocketRoom } from "../../../hooks/socket.io.hook";
import { getActiveLoadingOrders } from "../../../lib/queryStuff/getActiveLoadingOrders";
export default function DockPage() {
const { scanner } = useLocalSearchParams<{
scanner: string;
}>();
const { data: loadingOrders, isLoading } = useSuspenseQuery(
getActiveLoadingOrders(),
);
const { data } = useSocketRoom<any>(
`dockDoorLoading:${scanner}`,
undefined,
"append",
) as any;
const dockFilter = loadingOrders.filter(
(i: any) => i.dockId === Number(scanner),
);
const router = useRouter();
if (isLoading)
return (
<SafeAreaView>
<Text>Loading...</Text>
</SafeAreaView>
);
return (
<SafeAreaView className="w-full">
<View className="flex flex-row justify-between gap-1 ml-1 mr-1">
<View>
<Pressable
onPress={() => router.back()}
className="self-start rounded-xl bg-gray-200 px-4 py-2"
>
<Text className="font-semibold"> Back</Text>
</Pressable>
</View>
<Text className="text-xl mt-1">{dockFilter[0].dockDescription}</Text>
<View>
<Pressable
onPress={() =>
router.replace({
pathname: "/(tabs)/dockScan",
})
}
className="self-start rounded-xl bg-gray-200 px-4 py-2"
>
<Text className="font-semibold">Docks</Text>
</Pressable>
</View>
<View>
{data.map((i: any, index: any) => {
return (
<View key={index} className="m-2">
<Card>
<Text>{JSON.stringify(i)}</Text>
</Card>
</View>
);
})}
</View>
</View>
</SafeAreaView>
);
}

View File

@@ -1,9 +1,11 @@
import axios from "axios";
import { useRouter } from "expo-router";
import { Settings } from "lucide-react-native";
import { useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import Toast from "react-native-toast-message";
import { ConfigButton } from "../components/ui/configButton";
import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
@@ -52,11 +54,6 @@ export default function Login() {
}
};
const config = () => {
console.log("config");
return router.replace("/setup");
};
return (
<View
style={{
@@ -67,9 +64,17 @@ export default function Login() {
}}
>
<View className="flex items-center m-5">
<Text style={{ fontSize: 20, fontWeight: "600" }}>
<View className="flex flex-row">
<View>
<Text style={{ fontSize: 20, fontWeight: "600" }} className="mt-2">
LST Scanner Login
</Text>
</View>
<View>
<ConfigButton />
</View>
</View>
<View className="w-64 p-4">
<Input
className="w-fit"
@@ -89,7 +94,6 @@ export default function Login() {
</View>
<View className="flex gap-2 flex-row">
<Button title="Login" onPress={onLogin} />
<Button title="Config" onPress={config} />
</View>
</View>
);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
import { useRouter } from "expo-router";
import { Settings } from "lucide-react-native";
import { Pressable } from "react-native";
export function ConfigButton() {
const router = useRouter();
const config = () => {
console.log("config");
return router.replace("/setup");
};
return (
<Pressable
onPress={config}
className="h-12 w-12 items-center justify-center rounded-x"
>
<Settings color="black" size={24} />
</Pressable>
);
}

View File

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

View File

@@ -1,6 +1,7 @@
import axios from "axios";
import Constants from "expo-constants";
import { useEffect, useRef, useState } from "react";
import { setApiConfig } from "../lib/apiHelper";
import { devDelay } from "../lib/devMode";
import { versionCheck } from "../lib/versionValidation";
import { useAppStore } from "./useAppStore";
@@ -26,6 +27,11 @@ export function useAppStartup() {
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
setApiConfig({
serverIp,
serverPort,
});
useEffect(() => {
if (!hasHydrated) {
setStatus("loading");

View File

@@ -0,0 +1,32 @@
import * as Device from "expo-device";
import * as ScreenOrientation from "expo-screen-orientation";
import { useEffect } from "react";
const LANDSCAPE_MODELS = ["ET45", "ET40"]; // tablets
const PORTRAIT_MODELS = ["TC21", "TC26", "TC8300"]; // scanners
const isTabletModel = (modelName?: string | null) => {
const model = modelName?.toUpperCase() ?? "";
return LANDSCAPE_MODELS.some((m) => model.includes(m));
};
export function useDeviceOrientationLock() {
useEffect(() => {
async function lockOrientation() {
try {
const model = Device.modelName;
await ScreenOrientation.lockAsync(
isTabletModel(model)
? ScreenOrientation.OrientationLock.LANDSCAPE
: ScreenOrientation.OrientationLock.PORTRAIT_UP,
);
} catch (err) {
console.warn("Failed to lock orientation", err);
}
}
void lockOrientation();
}, []);
}

View File

@@ -0,0 +1,63 @@
import axios from "axios";
import { router } from "expo-router";
type ApiConfig = {
serverIp: string;
serverPort: string | number;
};
let currentConfig: ApiConfig | null = null;
export function setApiConfig(config: ApiConfig) {
currentConfig = config;
}
function getBaseUrl() {
if (!currentConfig) {
throw new Error("API config not initialized");
}
console.log(
`http://${currentConfig.serverIp}:${currentConfig.serverPort}/lst/api`,
);
return `http://${currentConfig.serverIp}:${currentConfig.serverPort}/lst/api`;
}
export const api = axios.create({
timeout: 15000,
});
api.interceptors.request.use((config) => {
config.baseURL = getBaseUrl();
return config;
});
api.interceptors.response.use(
(response) => response,
(error) => {
const isNetworkError =
error.code === "ERR_NETWORK" ||
error.code === "ECONNABORTED" ||
error.message === "Network Error" ||
error.message === "Failed to fetch" ||
!error.response;
// unauthorized
if (error.response?.status === 401) {
router.replace("/login");
}
// forbidden
if (error.response?.status === 403) {
router.replace("/");
}
// app/server offline
if (isNetworkError) {
router.replace("/");
}
console.log(error);
return Promise.reject(error);
},
);

View File

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

View File

@@ -0,0 +1,21 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getActiveLoadingOrders() {
return queryOptions({
queryKey: ["getActiveLoadingOrders"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
const { data } = await api.get("/dockDoor/activeLoadingOrders");
if (!data.success) {
throw new Error(data.message ?? "Failed to load articles");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,21 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getDocks() {
return queryOptions({
queryKey: ["getDocks"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
const { data } = await api.get("/dockDoor/scanners");
if (!data.success) {
throw new Error(data.message ?? "Failed to load articles");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,17 @@
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
gcTime: 5 * 60_000,
retry: 2,
refetchOnWindowFocus: false,
refetchOnReconnect: true,
refetchOnMount: false,
},
mutations: {
retry: 0,
},
},
});

View File

@@ -0,0 +1,34 @@
import { io, type Socket } from "socket.io-client";
import { useAppStore } from "../hooks/useAppStore";
let socket: Socket | null = null;
export function getSocket() {
const { serverIp, serverPort } = useAppStore.getState();
//const port = Number(serverPort) >= 50000 ? "3000" : serverPort;
const url = `http://${serverIp}:${serverPort}`;
if (!socket) {
socket = io(url, {
path: "/lst/api/socket.io",
withCredentials: true,
autoConnect: true,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
}
return socket;
}
export function connectSocket() {
const socket = getSocket();
if (!socket.connected && socket.disconnected) {
socket.connect();
}
return socket;
}

View File

@@ -1,6 +1,8 @@
import axios from "axios";
import Constants from "expo-constants";
import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck";
import { downloadLatestApk } from "./lastestVersion";
export type ServerVersionInfo = {
packageName: string;
@@ -60,13 +62,11 @@ export const versionCheck = async () => {
setServerVersion(res.data);
}
// const build = Constants.expoConfig?.android?.versionCode ?? 1;
const build = Constants.expoConfig?.android?.versionCode ?? 1;
// if (build < res.data.minSupportedVersionCode) {
// setStartupRoute("/updateScreen");
// setReady(true);
// return;
// }
if (build < res.data.versionCode) {
//await downloadLatestApk(serverIp, port);
}
} catch (error) {
console.log("Version check error:", error);
}

View File

@@ -0,0 +1,14 @@
CREATE TABLE "dock_door_scanners" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"ip" text NOT NULL,
"name" text,
"dock_id" text,
"dock_name" text NOT NULL,
"active" boolean DEFAULT true,
"current_loading_order" text DEFAULT '',
"add_date" timestamp DEFAULT now(),
"add_user" text DEFAULT 'lst-system',
"upd_date" timestamp DEFAULT now(),
"upd_user" text DEFAULT 'lst-system',
CONSTRAINT "dock_door_scanners_name_unique" UNIQUE("name")
);

View File

@@ -0,0 +1 @@
ALTER TABLE "dock_door_scanners" DROP COLUMN "dock_name";

View File

@@ -0,0 +1,69 @@
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "add_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "add_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "updated_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "analytics" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "analytics" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "job_audit_log" ALTER COLUMN "finished_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "job_audit_log" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "job_audit_log" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "user" ALTER COLUMN "updated_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "user" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "deployment_history" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "deployment_history" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "analytics_daily" ALTER COLUMN "first_hit_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "analytics_daily" ALTER COLUMN "last_hit_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "analytics_daily" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "analytics_daily" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "analytics_daily" ALTER COLUMN "updated_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "analytics_daily" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "datamart" ALTER COLUMN "add_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "datamart" ALTER COLUMN "add_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "datamart" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "datamart" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "dock_door_scanners" ALTER COLUMN "add_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "dock_door_scanners" ALTER COLUMN "add_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "dock_door_scanners" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "dock_door_scanners" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "inv_historical_data" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "inv_historical_data" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "logs" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "logs" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "opendock_apt" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "opendock_apt" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "opendock_apt" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "opendock_apt" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "opendock_article_setup" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "opendock_article_setup" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "opendock_article_setup" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "opendock_article_setup" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "opendock_dock_setup" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "opendock_dock_setup" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "opendock_dock_setup" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "opendock_dock_setup" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "printer_log" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "printer_log" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "printer_data" ALTER COLUMN "add_Date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "printer_data" ALTER COLUMN "add_Date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "printer_data" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "printer_data" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "scan_log" ALTER COLUMN "add_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "scan_log" ALTER COLUMN "add_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "add_Date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "add_Date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "server_data" ALTER COLUMN "last_updated" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "server_data" ALTER COLUMN "last_updated" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "settings" ALTER COLUMN "add_Date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "settings" ALTER COLUMN "add_Date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "settings" ALTER COLUMN "upd_date" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "settings" ALTER COLUMN "upd_date" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "app_stats" ALTER COLUMN "last_build_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "app_stats" ALTER COLUMN "last_deploy_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "app_stats" ALTER COLUMN "last_updated" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "app_stats" ALTER COLUMN "last_updated" SET DEFAULT now();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -400,6 +400,27 @@
"when": 1779454561527,
"tag": "0056_shallow_chimera",
"breakpoints": true
},
{
"idx": 57,
"version": "7",
"when": 1779843750556,
"tag": "0057_worthless_trish_tilby",
"breakpoints": true
},
{
"idx": 58,
"version": "7",
"when": 1779846894283,
"tag": "0058_damp_donald_blake",
"breakpoints": true
},
{
"idx": 59,
"version": "7",
"when": 1780349486711,
"tag": "0059_sparkling_joystick",
"breakpoints": true
}
]
}

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