11 Commits

Author SHA1 Message Date
e9b0101095 ci(template): bug in getting the template to work correctly
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m28s
2026-05-07 09:01:15 -05:00
ca885fb01a ci(templates): added in templates for the repo to make it more easy to manage and add in new ideas
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-05-07 08:50:06 -05:00
edb3668548 refactor(scanner): added toasts in to make it look better
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m25s
2026-05-06 19:42:52 -05:00
87803eed43 feat(scanner): added in lanechecks 2026-05-06 19:42:22 -05:00
e61038e004 chore(release): 0.0.2-alpha.9
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m42s
Release and Build Image / release (push) Successful in 28s
2026-05-06 13:34:30 -05:00
d99449ddc4 test(scanner): lane check
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m41s
2026-05-06 13:30:58 -05:00
3552ca31f9 build(builds): changed to ip as its on the same server
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4s
2026-05-06 12:27:20 -05:00
b578f05d64 build(release): bypass cloudflare upload limit
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 3m43s
2026-05-06 12:17:42 -05:00
4ca74de279 refactor(mobile): valildation of server after each scan
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 3m40s
2026-05-06 12:10:14 -05:00
12412536d1 refactor(scanner): finished login stuff for current routes 2026-05-06 12:09:47 -05:00
a38e2e0339 refactor(scanner): added in running number 2026-05-06 12:09:09 -05:00
36 changed files with 6093 additions and 2579 deletions

View File

@@ -0,0 +1,66 @@
---
name: Bug Report
about: Report something that is broken or not working correctly
title: "[BUG] "
ref: "main"
labels:
- bug
---
# 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

View File

@@ -0,0 +1,47 @@
---
name: Enhancement
about: Improve or refine an existing feature
title: "[ENHANCEMENT] "
ref: "main"
labels:
- enhancement
---
# 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,40 @@
---
name: Feature Request
about: Suggest a brand new feature or module
title: "[FEATURE] "
ref: "main"
labels:
- feature
---
# 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

@@ -12,20 +12,20 @@ jobs:
steps:
- name: Checkout (local)
run: |
git clone https://git.tuffraid.net/cowch/lst_v3.git .
git clone http://10.75.9.150:3100/cowch/lst_v3.git .
git checkout ${{ gitea.sha }}
- name: Login to registry
run: echo "${{ secrets.PASSWORD }}" | docker login git.tuffraid.net -u "cowch" --password-stdin
run: echo "${{ secrets.PASSWORD }}" | docker login 10.75.9.150:3100 -u "cowch" --password-stdin
- name: Build image
run: |
docker build \
-t git.tuffraid.net/cowch/lst_v3:latest \
-t git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }} \
-t 10.75.9.150:3100/cowch/lst_v3:latest \
-t 10.75.9.150:3100/cowch/lst_v3:${{ gitea.sha }} \
.
- name: Push
run: |
docker push git.tuffraid.net/cowch/lst_v3:latest
docker push git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }}
docker push 10.75.9.150:3100/cowch/lst_v3:latest
docker push 10.75.9.150:3100/cowch/lst_v3:${{ gitea.sha }}

View File

@@ -14,12 +14,12 @@ jobs:
# Examples:
# http://gitea.internal.lan:3000
# https://gitea-origin.yourdomain.local
GITEA_INTERNAL_URL: "https://git.tuffraid.net"
GITEA_INTERNAL_URL: "http://10.75.9.150:3100" #"https://git.tuffraid.net"
# Internal/origin registry host. Usually same host as above, but without protocol.
# Example:
# gitea.internal:3000
REGISTRY_HOST: "git.tuffraid.net"
REGISTRY_HOST: "10.75.9.150:3100" #"git.tuffraid.net"
steps:
- name: Check out repository

View File

@@ -1,5 +1,25 @@
# All Changes to LST can be found below.
## [0.0.2-alpha.9](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.8...v0.0.2-alpha.9) (2026-05-06)
### 🛠️ Code Refactor
* **mobile:** valildation of server after each scan ([4ca74de](https://git.tuffraid.net/cowch/lst_v3/commits/4ca74de2795cea7244e38697d16afe2822164ed6))
* **scanner:** added in running number ([a38e2e0](https://git.tuffraid.net/cowch/lst_v3/commits/a38e2e033977b725538e9a9046098d94194d549e))
* **scanner:** finished login stuff for current routes ([1241253](https://git.tuffraid.net/cowch/lst_v3/commits/12412536d10981013053c39d156c6c9cb0babd11))
### 📝 Testing Code
* **scanner:** lane check ([d99449d](https://git.tuffraid.net/cowch/lst_v3/commits/d99449ddc4e2777c1b0fe9189ba0a7c01fe1dd8f))
### 📈 Project Builds
* **builds:** changed to ip as its on the same server ([3552ca3](https://git.tuffraid.net/cowch/lst_v3/commits/3552ca31f9f7b3bcbe557a145e7eb154bfdae79c))
* **release:** bypass cloudflare upload limit ([b578f05](https://git.tuffraid.net/cowch/lst_v3/commits/b578f05d6482f9b6f30febeee6ab0b708a70f68b))
## [0.0.2-alpha.8](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.7...v0.0.2-alpha.8) (2026-05-06)

View File

@@ -9,6 +9,7 @@ export const scanLog = pgTable("scan_log", {
message: text("message").notNull(),
prompt: text("prompt"),
commandDescription: text("command_description"),
runningNumber: text("running_number").default("0"),
status: text("status"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_Date").defaultNow(),

View File

@@ -0,0 +1,34 @@
import { Router } from "express";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/", async (req, res) => {
const body = req.body;
const lane = body.lane.split("#");
console.log(lane[2]);
const laneData = await runProdApi({
method: "post",
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",
data: [
{
laneIds: [lane[2]],
},
],
});
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "lane check",
message: `all data for lane Id: ${lane}`,
data: laneData?.data ?? [],
status: 200,
});
});
export default router;

View File

@@ -1,5 +1,6 @@
import type { Express } from "express";
import downloads from "./donwloadApps.route.js";
import lanes from "./laneCheck.js";
import authPin from "./mobileAuth.route.js";
import newPin from "./mobilePin.route.js";
import logs from "./scanLogs.route.js";
@@ -12,6 +13,7 @@ export const setupMobileRoutes = (baseUrl: string, app: Express) => {
app.use(`${baseUrl}/api/mobile/logs`, logs);
app.use(`${baseUrl}/api/mobile/auth`, authPin);
app.use(`${baseUrl}/api/mobile/pin`, newPin);
app.use(`${baseUrl}/api/mobile/laneCheck`, lanes);
// all other system should be under /api/system/*
};

View File

@@ -263,6 +263,10 @@ r.patch("/user/:id", requireAuth, async (req, res) => {
updates.active = req.body.active;
}
if (req.body?.excludedCommand !== undefined) {
updates.excludedCommand = req.body.excludedCommand;
}
if (req.body?.role !== undefined) {
updates.role = req.body.role;
}

View File

@@ -18,6 +18,8 @@ router.post("/", async (req, res) => {
commandDescription: body.commandDescription,
status: body.status,
lines: body.lines,
user: body.user,
runningNumber: body.runningNumber,
})
.returning();

View File

@@ -15,8 +15,8 @@
"foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff"
},
"versionCode": 24,
"minSupportedVersionCode": 21,
"versionCode": 31,
"minSupportedVersionCode": 26,
"predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile"
},

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint",
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat clean && gradlew.bat assembleRelease && npm run copy:apk",
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:mobile": "cd scripts && node runBuild.ts",
"build:mobile:bump": "cd scripts && node runBuild.ts --bump",
@@ -22,6 +22,7 @@
"@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.33",
"@rn-primitives/dialog": "^1.4.0",
"@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0",
@@ -56,10 +57,11 @@
"react-dom": "19.2.0",
"react-native": "0.83.4",
"react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "^4.2.1",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-tcp-socket": "^6.4.1",
"react-native-toast-message": "^2.3.3",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.7.2",
"socket.io-client": "^4.8.3",

View File

@@ -15,10 +15,12 @@ export default function TabsLayout() {
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
const port = parseInt(serverPort || "0", 10) >= 50000;
if (!user || (!isUnlocked && !port)) {
console.log(port);
if (!port) {
if (!user || !isUnlocked) {
return <Redirect href="/login" />;
}
}
const isNormalScanner = parseInt(serverPort || "0", 10) >= 50000;

View File

@@ -1,15 +1,97 @@
import React, { useCallback, useEffect } from "react";
import { Text, View } from "react-native";
import axios from "axios";
import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import type React from "react";
import { useCallback, useEffect, useState } from "react";
import { ScrollView, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import { GlobalFooter } from "../../components/UpdateFooter";
import { Button } from "../../components/ui/button";
import { Card, CardContent, CardHeader } from "../../components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { useAppStore } from "../../hooks/useAppStore";
import { scannerFeedback } from "../../lib/feedbackScan";
import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner";
export default function LaneCheck() {
const handleScan = useCallback(async (scan: ZebraScanResult) => {
console.log(scan);
}, []);
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>
);
};
useEffect(() => {
zebraScanner.ensureProfile();
export default function LaneCheck() {
const [units, setUnits] = useState<any>(null);
const serverIp = useAppStore((s) => s.serverIp);
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
setUnits(null);
await scannerFeedback({
type: "scan",
sound: true,
vibrate: true,
led: true,
});
if (!scan.data.startsWith("loc")) {
Toast.show({
type: "error",
text1: "Scan error",
text2: "The last scan was not a lane please try again",
});
return;
}
try {
const res = await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/lanecheck`,
{
lane: scan.data,
},
{
timeout: 5000,
},
);
if (res.status === 200) {
setUnits(res.data);
Toast.show({
type: "info",
text1: "Lane Data",
text2: "All Loading Units from this lane will be listed below",
});
}
} catch (error) {
console.log(error);
Toast.show({
type: "error",
text1: "Lane Data",
text2: "Error getting lane data please try again",
});
}
},
[serverIp.trim],
);
useFocusEffect(
useCallback(() => {
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
@@ -20,18 +102,109 @@ export default function LaneCheck() {
return () => {
sub.remove();
zebraScanner.stopListening();
//setUnits(null);
};
}, [handleScan]);
}, [handleScan]),
);
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>LaneChecks</Text>
{units ? (
// <SafeAreaView className={`flex-1 w-full items-center`}>
// <ScrollView className="w-full flex-1">
// <View className="flex items-center gap-2 w-full">
// {units.data?.map((i: any, index: any) => (
// <View key={`${i.runningNumber}-${index}`}>
// <Text>example</Text>
// </View>
// ))}
// </View>
// </ScrollView>
// </SafeAreaView>
<SafeAreaView className={`w-full items-center`}>
<View style={{ padding: 2 }}>
<Text>There Are {units.data.length} units in this lane</Text>
</View>
<ScrollView className="w-full" style={{ marginBottom: 20 }}>
<View>
{units.data.map((i, index) => (
<View
key={`${i.runningNumber}-${index}`}
style={{
justifyContent: "center",
margin: 2,
}}
>
<Dialog>
<DialogTrigger>
<Card
className="w-full"
style={{
borderColor:
i.state === "QualityBlocked" ? "red" : undefined,
borderWidth: 4,
}}
>
<CardContent>
<Text>
{i.articleId} - {i.articleName}
</Text>
<Text>
Running Number: {i.runningNumber ?? "Non barcoded"}
</Text>
</CardContent>
</Card>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Details for Article {i.articleId}, Rn:
{i.runningNumber ?? "Non barcoded"}{" "}
</DialogTitle>
<DialogDescription>
<InfoRow
label="Production Date"
value={format(i.productionDate, "MM/dd/yyyy HH:mm")}
/>
<InfoRow label="Quantity" value={i.quantity} />
{i.state === "QualityBlocked" && (
<InfoRow
label="Defect"
value={i.mainDefectGroupDescription}
/>
)}
{i.state === "QualityBlocked" && (
<InfoRow
label="Description"
value={i.mainDefectDescription}
/>
)}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</View>
))}
</View>
</ScrollView>
</SafeAreaView>
) : (
<View className="mt-50">
<Text className="text-2xl text-center">
Please scan a lane to see all Units that are in the lane.
</Text>
</View>
)}
<View>
<GlobalFooter />
</View>
</View>
);
}

View File

@@ -2,10 +2,16 @@ import { PortalHost } from "@rn-primitives/portal";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import "../../global.css";
import { useEffect } from "react";
import Toast from "react-native-toast-message";
import useDeviceLock from "../hooks/useDeviceCheck";
import { zebraScanner } from "../lib/ZebraScanner";
export default function RootLayout() {
useDeviceLock();
useEffect(() => {
zebraScanner.ensureProfile();
}, []);
return (
<>
@@ -18,6 +24,7 @@ export default function RootLayout() {
<Stack.Screen name="(tabs)" />
</Stack>
<PortalHost />
<Toast />
</>
);
}

View File

@@ -2,11 +2,15 @@ import axios from "axios";
import { useRouter } from "expo-router";
import { useState } from "react";
import { Button, Text, View } from "react-native";
import { Alert, Button, Text, View } from "react-native";
import Toast from "react-native-toast-message";
import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
const formatName = (name?: string) =>
name ? name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() : "";
export default function Login() {
// doing this causes rerender and sub
//const { setUser } = useMobileAuthStore();
@@ -33,11 +37,18 @@ export default function Login() {
if (res.status === 200) {
// this way to set the user is direct and basically a 1 shot
Toast.show({
type: "success",
text1: `Welcome back ${formatName(res.data.data.name)}`,
});
useMobileAuthStore.getState().setUser(res.data.data);
return router.replace("/(tabs)/scanner");
}
} catch (error) {
console.log(error);
//Alert.alert("Login Error", `Invalid pin please try again`);
Toast.show({ type: "error", text1: `Invalid pin please try again` });
}
};
@@ -70,7 +81,7 @@ export default function Login() {
</View>
</View>
<View>
<Text>
<Text className="p-3">
Warning: If you are logged into another scanner you will encounter
scan errors, please do not try to log into more than 1 scanner at a
time.

View File

@@ -2,6 +2,7 @@ import Constants from "expo-constants";
import { useRouter } from "expo-router";
import { useState } from "react";
import { Alert, Button, Text, TextInput, View } from "react-native";
import Toast from "react-native-toast-message";
import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck";
@@ -25,18 +26,29 @@ export default function Setup() {
const server = useServerStore((s) => s.serverVersion);
// TODO: if on lst version and the user is manager or admin just login
const authCheck = () => {
if (pin === "6971") {
setAuth(true);
} else {
Alert.alert("Incorrect pin entered please try again");
//Alert.alert("Incorrect pin entered please try again");
Toast.show({
type: "error",
text1: "Incorrect pin entered please try again",
});
setPin("");
}
};
const handleSave = async () => {
if (!serverIp.trim() || !serverPort.trim()) {
Alert.alert("Missing info", "Please fill in both fields.");
//Alert.alert("Missing info", "Please fill in both fields.");
Toast.show({
type: "error",
text1: "Missing info",
text2: "Please fill in both fields.",
});
return;
}
@@ -48,7 +60,12 @@ export default function Setup() {
isRegistered: true,
});
Alert.alert("Saved", "Config saved to device.");
//Alert.alert("Saved", "Config saved to device.");
Toast.show({
type: "info",
text1: "Saved",
text2: "Config saved to device.",
});
//router.replace("/");
};
return (

View File

@@ -1,5 +1,6 @@
import axios from "axios";
import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
@@ -7,6 +8,7 @@ import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan";
import { versionCheck } from "../lib/versionValidation";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter";
@@ -45,10 +47,15 @@ export default function LSTScanner() {
scan.data.toLowerCase().includes(cmd.toLowerCase()),
);
console.log(user?.excludedCommand);
if (isAlphaStart && isExcluded) {
Alert.alert(
`Command: ${scan.data} is not allowed to be used, please contact logistics if this is an error`,
"Command not allowed",
`Command: ${scan.data}\n\nPlease contact logistics if this is an error`,
);
return;
}
let commandToSend = `${STX}${user?.scannerId}@${scan.data}${ETX}`;
@@ -68,16 +75,26 @@ export default function LSTScanner() {
const scanned = (await sendTcpMessage(
commandToSend,
serverIp,
parseInt(serverPort || "0", 10),
50004,
)) as any;
// send the logs to lst but allow it to time out if it dose not exist just bc.
const logInfo = { ...scanned, user: user?.name };
try {
await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/logs`,
logInfo,
);
await axios.post(`http://${serverIp.trim()}:3000/lst/api/mobile/logs`, {
scannerId: user?.scannerId ?? "0",
message: scanned.data.message,
prompt: scanned.data.prompt,
commandDescription: scanned.data.commandDescription,
status: scanned.data.status,
lines: scanned.data.lines,
user: user?.name ?? "prodScan",
runningNumber: scan.data.startsWith("000")
? parseInt(scan.data.slice(10, -1) || "000", 10).toString()
: scan.data.startsWith("loc")
? scan.data
: "0",
});
} catch (error) {
console.log(error);
}
@@ -90,7 +107,15 @@ export default function LSTScanner() {
vibrate: true,
led: true,
});
setBGColor("bg-green-500");
// version check
versionCheck();
// auth update
useMobileAuthStore.getState().updateLastScan();
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
@@ -117,7 +142,6 @@ export default function LSTScanner() {
},
[
serverIp,
serverPort,
setLastScan,
user?.scannerId,
user?.name,
@@ -132,8 +156,8 @@ export default function LSTScanner() {
//console.log(lastScan);
useEffect(() => {
zebraScanner.ensureProfile();
useFocusEffect(
useCallback(() => {
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
@@ -145,7 +169,8 @@ export default function LSTScanner() {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
}, [handleScan]),
);
return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View style={{ alignItems: "center", margin: 5 }}>

View File

@@ -1,11 +1,14 @@
import axios from "axios";
import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan";
import { versionCheck } from "../lib/versionValidation";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter";
@@ -52,11 +55,18 @@ export default function ProdScanner() {
parseInt(serverPort || "0", 10),
)) as any;
// send the logs to lst but allow it to time out if it dose not exist just bc.
const data = {
...scanned.data,
runningNumber: scan.data.startsWith("000")
? parseInt(scan.data.slice(10, -1) || "000", 10).toString()
: scan.data.startsWith("loc")
? scan.data
: "0",
};
try {
await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/logs`,
scanned,
data,
);
} catch (error) {
console.log(error);
@@ -71,6 +81,13 @@ export default function ProdScanner() {
led: true,
});
setBGColor("bg-green-500");
// version check
versionCheck();
// auth update
useMobileAuthStore.getState().updateLastScan();
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
@@ -104,8 +121,8 @@ export default function ProdScanner() {
//console.log(lastScan);
useEffect(() => {
zebraScanner.ensureProfile();
useFocusEffect(
useCallback(() => {
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
@@ -117,7 +134,8 @@ export default function ProdScanner() {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
}, [handleScan]),
);
return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View>

View File

@@ -0,0 +1,140 @@
import { Icon } from '@/components/ui/icon';
import { NativeOnlyAnimatedView } from '@/components/ui/native-only-animated-view';
import { cn } from '@/lib/utils';
import * as DialogPrimitive from '@rn-primitives/dialog';
import { X } from 'lucide-react-native';
import * as React from 'react';
import { Platform, Text, View, type ViewProps } from 'react-native';
import { FadeIn, FadeOut } from 'react-native-reanimated';
import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const FullWindowOverlay = Platform.OS === 'ios' ? RNFullWindowOverlay : React.Fragment;
function DialogOverlay({
className,
children,
...props
}: Omit<React.ComponentProps<typeof DialogPrimitive.Overlay>, 'asChild'> & {
children?: React.ReactNode;
}) {
return (
<FullWindowOverlay>
<DialogPrimitive.Overlay
className={cn(
'absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center bg-black/50 p-2',
Platform.select({
web: 'animate-in fade-in-0 fixed cursor-default [&>*]:cursor-auto',
}),
className
)}
{...props}
asChild={Platform.OS !== 'web'}>
<NativeOnlyAnimatedView entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)}>
<NativeOnlyAnimatedView entering={FadeIn.delay(50)} exiting={FadeOut.duration(150)}>
<>{children}</>
</NativeOnlyAnimatedView>
</NativeOnlyAnimatedView>
</DialogPrimitive.Overlay>
</FullWindowOverlay>
);
}
function DialogContent({
className,
portalHost,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
portalHost?: string;
}) {
return (
<DialogPortal hostName={portalHost}>
<DialogOverlay>
<DialogPrimitive.Content
className={cn(
'bg-background border-border z-50 mx-auto flex w-full max-w-[calc(100%-2rem)] flex-col gap-4 rounded-lg border p-6 shadow-lg shadow-black/5 sm:max-w-lg',
Platform.select({
web: 'animate-in fade-in-0 zoom-in-95 duration-200',
}),
className
)}
{...props}>
<>{children}</>
<DialogPrimitive.Close
className={cn(
'absolute right-4 top-4 rounded opacity-70 active:opacity-100',
Platform.select({
web: 'ring-offset-background focus:ring-ring data-[state=open]:bg-accent transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2',
})
)}
hitSlop={12}>
<Icon
as={X}
className={cn('text-accent-foreground web:pointer-events-none size-4 shrink-0')}
/>
<Text className="sr-only">Close</Text>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogOverlay>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: ViewProps) {
return (
<View className={cn('flex flex-col gap-2 text-center sm:text-left', className)} {...props} />
);
}
function DialogFooter({ className, ...props }: ViewProps) {
return (
<View
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
className={cn('text-foreground text-lg font-semibold leading-none', className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -0,0 +1,57 @@
import { TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import type { LucideIcon, LucideProps } from 'lucide-react-native';
import { cssInterop } from 'nativewind';
import * as React from 'react';
type IconProps = LucideProps & {
as: LucideIcon;
} & React.RefAttributes<LucideIcon>;
function IconImpl({ as: IconComponent, ...props }: IconProps) {
return <IconComponent {...props} />;
}
cssInterop(IconImpl, {
className: {
target: 'style',
nativeStyleToProp: {
height: 'size',
width: 'size',
},
},
});
/**
* A wrapper component for Lucide icons with Nativewind `className` support via `cssInterop`.
*
* This component allows you to render any Lucide icon while applying utility classes
* using `nativewind`. It avoids the need to wrap or configure each icon individually.
*
* @component
* @example
* ```tsx
* import { ArrowRight } from 'lucide-react-native';
* import { Icon } from '@/registry/components/ui/icon';
*
* <Icon as={ArrowRight} className="text-red-500" size={16} />
* ```
*
* @param {LucideIcon} as - The Lucide icon component to render.
* @param {string} className - Utility classes to style the icon using Nativewind.
* @param {number} size - Icon size (defaults to 14).
* @param {...LucideProps} ...props - Additional Lucide icon props passed to the "as" icon.
*/
function Icon({ as: IconComponent, className, size = 14, ...props }: IconProps) {
const textClass = React.useContext(TextClassContext);
return (
<IconImpl
as={IconComponent}
className={cn('text-foreground', textClass, className)}
size={size}
{...props}
/>
);
}
export { Icon };

View File

@@ -0,0 +1,23 @@
import { Platform } from 'react-native';
import Animated from 'react-native-reanimated';
/**
* This component is used to wrap animated views that should only be animated on native.
* @param props - The props for the animated view.
* @returns The animated view if the platform is native, otherwise the children.
* @example
* <NativeOnlyAnimatedView entering={FadeIn} exiting={FadeOut}>
* <Text>I am only animated on native</Text>
* </NativeOnlyAnimatedView>
*/
function NativeOnlyAnimatedView(
props: React.ComponentProps<typeof Animated.View> & React.RefAttributes<typeof Animated.View>
) {
if (Platform.OS === 'web') {
return <>{props.children as React.ReactNode}</>;
} else {
return <Animated.View {...props} />;
}
}
export { NativeOnlyAnimatedView };

View File

@@ -1,5 +1,5 @@
import { cn } from '@/lib/utils';
import * as Slot from '@rn-primitives/slot';
import { Slot } from '@rn-primitives/slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { Platform, Text as RNText, type Role } from 'react-native';
@@ -70,11 +70,12 @@ function Text({
variant = 'default',
...props
}: React.ComponentProps<typeof RNText> &
React.RefAttributes<typeof RNText> &
TextVariantProps & {
asChild?: boolean;
}) {
const textClass = React.useContext(TextClassContext);
const Component = asChild ? Slot.Text : RNText;
const Component = asChild ? Slot : RNText;
return (
<Component
className={cn(textVariants({ variant }), textClass, className)}

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import Constants from "expo-constants";
import { useEffect, useRef, useState } from "react";
import { devDelay } from "../lib/devMode";
import { versionCheck } from "../lib/versionValidation";
import { useAppStore } from "./useAppStore";
import { useServerStore } from "./useServerCheck";
@@ -24,7 +25,6 @@ export function useAppStartup() {
const hasHydrated = useAppStore((s) => s.hasHydrated);
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
const setServerVersion = useServerStore((s) => s.setServerVersion);
useEffect(() => {
if (!hasHydrated) {
@@ -62,29 +62,7 @@ export function useAppStartup() {
return;
}
const port =
parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort;
try {
const res = await axios.get(
`http://${serverIp}:${port}/lst/api/mobile/version`,
{ timeout: 5000 },
);
if (res.status === 200) {
setServerVersion(res.data);
}
const build = Constants.expoConfig?.android?.versionCode ?? 1;
if (build < res.data.minSupportedVersionCode) {
setStartupRoute("/updateScreen");
setReady(true);
return;
}
} catch (error) {
console.log("Version check error:", error);
}
await versionCheck();
setStatus("scannerMode");
await devDelay(1500);
@@ -123,7 +101,7 @@ export function useAppStartup() {
return () => {
cancelled = true;
};
}, [hasHydrated, serverIp, serverPort, setServerVersion]);
}, [hasHydrated, serverIp, serverPort]);
return {
ready,

View File

@@ -19,7 +19,11 @@ export default function useDeviceLock() {
nextAppState === "background" || nextAppState === "inactive";
if (wasActive && isNowInactive) {
useMobileAuthStore.getState().lock();
const auth = useMobileAuthStore.getState();
if (auth.shouldLockForIdle()) {
auth.lock();
}
}
appStateRef.current = nextAppState;

View File

@@ -1,5 +1,7 @@
import { create } from "zustand";
const ONE_HOUR = 1000 * 60 * 60;
type MobileUser = {
id: string;
name: string;
@@ -11,19 +13,40 @@ type MobileUser = {
type AuthState = {
user: MobileUser | null;
isUnlocked: boolean;
lastScanAt: number | null;
setUser: (user: MobileUser) => void;
updateLastScan: () => void;
lock: () => void;
logout: () => void;
shouldLockForIdle: () => boolean;
};
export const useMobileAuthStore = create<AuthState>((set) => ({
export const useMobileAuthStore = create<AuthState>((set, get) => ({
user: null,
isUnlocked: false,
lastScanAt: null,
setUser: (user) => set({ user, isUnlocked: true }),
setUser: (user) =>
set({
user,
isUnlocked: true,
lastScanAt: Date.now(),
}),
updateLastScan: () => set({ lastScanAt: Date.now() }),
lock: () => set({ isUnlocked: false }),
logout: () => set({ user: null, isUnlocked: false }),
logout: () =>
set({
user: null,
isUnlocked: false,
lastScanAt: null,
}),
shouldLockForIdle: () => {
const lastScanAt = get().lastScanAt;
if (!lastScanAt) return true;
return Date.now() - lastScanAt > ONE_HOUR;
},
}));

View File

@@ -37,4 +37,7 @@ export const zebraScanner = {
): EmitterSubscription {
return scannerEmitter.addListener("barcodeScanned", callback);
},
disableScannerInput() {
ZebraScanner.disableScannerInput();
},
};

View File

@@ -1,4 +1,6 @@
import axios from "axios";
import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck";
export type ServerVersionInfo = {
packageName: string;
@@ -18,7 +20,7 @@ export type StartupStatus =
export function evaluateVersion(
appBuildCode: number,
server: ServerVersionInfo
server: ServerVersionInfo,
): StartupStatus {
if (appBuildCode < server.minSupportedVersionCode) {
return {
@@ -41,3 +43,31 @@ export function evaluateVersion(
server,
};
}
export const versionCheck = async () => {
const { setServerVersion } = useServerStore.getState();
const { serverPort, serverIp } = useAppStore.getState();
const port = parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort;
try {
const res = await axios.get(
`http://${serverIp}:${port}/lst/api/mobile/version`,
{ timeout: 5000 },
);
if (res.status === 200) {
setServerVersion(res.data);
}
// const build = Constants.expoConfig?.android?.versionCode ?? 1;
// if (build < res.data.minSupportedVersionCode) {
// setStartupRoute("/updateScreen");
// setReady(true);
// return;
// }
} catch (error) {
console.log("Version check error:", error);
}
};

View File

@@ -0,0 +1 @@
ALTER TABLE "scan_log" ADD COLUMN "running_number" text DEFAULT '0';

File diff suppressed because it is too large Load Diff

View File

@@ -330,6 +330,13 @@
"when": 1778059910210,
"tag": "0046_chemical_the_leader",
"breakpoints": true
},
{
"idx": 47,
"version": "7",
"when": 1778068577325,
"tag": "0047_spotty_queen_noir",
"breakpoints": true
}
]
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "lst_v3",
"version": "0.0.2-alpha.8",
"version": "0.0.2-alpha.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lst_v3",
"version": "0.0.2-alpha.8",
"version": "0.0.2-alpha.9",
"license": "ISC",
"dependencies": {
"@dotenvx/dotenvx": "^1.57.0",

View File

@@ -1,6 +1,6 @@
{
"name": "lst_v3",
"version": "0.0.2-alpha.8",
"version": "0.0.2-alpha.9",
"description": "The tool that supports us in our everyday alplaprod",
"main": "index.js",
"scripts": {