refactor(mobile): intial addin of dockdoor scanning on mobile

This commit is contained in:
2026-06-01 14:22:40 -05:00
parent e6b92aeb10
commit 2a35381fe4
17 changed files with 591 additions and 54 deletions

View File

@@ -15,14 +15,14 @@
"foregroundImage": "./assets/adaptive-icon-white.png", "foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"versionCode": 39, "versionCode": 42,
"minSupportedVersionCode": 33, "minSupportedVersionCode": 33,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile", "package": "net.alpla.lst.mobile",
"permissions": [ "permissions": [
"android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE" "android.permission.READ_EXTERNAL_STORAGE"
] ]
}, },
"web": { "web": {
"output": "static", "output": "static",

View File

@@ -16,7 +16,7 @@
"@rn-primitives/portal": "^1.4.0", "@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0", "@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^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", "axios": "^1.15.0",
"babel-preset-expo": "^55.0.18", "babel-preset-expo": "^55.0.18",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -5318,9 +5318,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.100.9", "version": "5.100.14",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz",
"integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==", "integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -5328,12 +5328,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.100.9", "version": "5.100.14",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz",
"integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==", "integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.100.9" "@tanstack/query-core": "5.100.14"
}, },
"funding": { "funding": {
"type": "github", "type": "github",

View File

@@ -26,7 +26,7 @@
"@rn-primitives/portal": "^1.4.0", "@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0", "@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^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", "axios": "^1.15.0",
"babel-preset-expo": "^55.0.18", "babel-preset-expo": "^55.0.18",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View File

@@ -51,7 +51,7 @@ export default function TabsLayout() {
onPress: async () => { onPress: async () => {
// clear auth/session // clear auth/session
logoutScanner(); logoutScanner();
router.replace("/(tabs)/scanner"); router.replace("/");
// clear zustand/session stuff // clear zustand/session stuff
//useAuthStore.getState().reset(); //useAuthStore.getState().reset();

View File

@@ -1,26 +1,134 @@
import React from "react"; import { useSuspenseQuery } from "@tanstack/react-query";
import { Text, View } from "react-native"; import * as Device from "expo-device";
import { Button } from "../../components/ui/button"; 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() { import { Card, CardContent } from "../../components/ui/card";
const getInfo = async () => { import { getActiveLoadingOrders } from "../../lib/queryStuff/getActiveLoadingOrders";
const info = "ho"; 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 ( return (
<View <View className="flex">
style={{ <View
flex: 1, style={{
//justifyContent: "center", // flex: 1,
alignItems: "center", //justifyContent: "center",
marginTop: 50, alignItems: "center",
}} marginTop: 50,
> }}
<Text>Dock Scanning</Text> >
<Button onPress={getInfo}> <Text className="text-2xl text-bold">Dock Scanning</Text>
<Text>Check info</Text> <Button title="Update Loading Orders" onPress={updateLoadingOrders} />
</Button> </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>
); );
} }

View File

@@ -44,8 +44,6 @@ export default function PPOO() {
}); });
}, [items, sortDir]); }, [items, sortDir]);
//console.log(logsInfo);
return ( return (
<View className="flex items-center mt-2"> <View className="flex items-center mt-2">
<View className="flex m-2"> <View className="flex m-2">
@@ -61,7 +59,7 @@ export default function PPOO() {
<Text>Loading PPOO...</Text> <Text>Loading PPOO...</Text>
</View> </View>
) : ( ) : (
<SafeAreaView className="flex-1"> <SafeAreaView className="flex">
<ScrollView className="w-full"> <ScrollView className="w-full">
<View className="w-full flex-row flex-wrap gap-2 m-2"> <View className="w-full flex-row flex-wrap gap-2 m-2">
{sortedItems.map((i: any) => { {sortedItems.map((i: any) => {

View File

@@ -2,14 +2,18 @@ import { PortalHost } from "@rn-primitives/portal";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import "../../global.css"; import "../../global.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { useEffect } from "react"; import { useEffect } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import useDeviceLock from "../hooks/useDeviceCheck"; import useDeviceLock from "../hooks/useDeviceCheck";
import { useDeviceOrientationLock } from "../hooks/useDeviceOrientationLock";
import { queryClient } from "../lib/queryStuff/queryClient";
import { connectSocket } from "../lib/socket.io"; import { connectSocket } from "../lib/socket.io";
import { zebraScanner } from "../lib/ZebraScanner"; import { zebraScanner } from "../lib/ZebraScanner";
export default function RootLayout() { export default function RootLayout() {
useDeviceLock(); useDeviceLock();
useDeviceOrientationLock();
useEffect(() => { useEffect(() => {
zebraScanner.ensureProfile(); zebraScanner.ensureProfile();
@@ -18,15 +22,18 @@ export default function RootLayout() {
return ( return (
<> <>
<StatusBar style="dark" /> <QueryClientProvider client={queryClient}>
<Stack screenOptions={{ headerShown: false }}> <StatusBar style="dark" />
<Stack.Screen name="index" /> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="login" /> <Stack.Screen name="index" />
<Stack.Screen name="setup" /> <Stack.Screen name="login" />
<Stack.Screen name="updateScreen" /> <Stack.Screen name="setup" />
<Stack.Screen name="(tabs)" /> <Stack.Screen name="updateScreen" />
</Stack> <Stack.Screen name="(tabs)" />
<PortalHost /> </Stack>
<PortalHost />
</QueryClientProvider>
<Toast /> <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 axios from "axios";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { Settings } from "lucide-react-native";
import { useState } from "react"; import { useState } from "react";
import { Alert, Button, Text, View } from "react-native"; import { Alert, Button, Text, View } from "react-native";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import { ConfigButton } from "../components/ui/configButton";
import { Input } from "../components/ui/input"; import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth"; import { useMobileAuthStore } from "../hooks/useMobileAuth";
@@ -52,11 +54,6 @@ export default function Login() {
} }
}; };
const config = () => {
console.log("config");
return router.replace("/setup");
};
return ( return (
<View <View
style={{ style={{
@@ -67,9 +64,17 @@ export default function Login() {
}} }}
> >
<View className="flex items-center m-5"> <View className="flex items-center m-5">
<Text style={{ fontSize: 20, fontWeight: "600" }}> <View className="flex flex-row">
LST Scanner Login <View>
</Text> <Text style={{ fontSize: 20, fontWeight: "600" }} className="mt-2">
LST Scanner Login
</Text>
</View>
<View>
<ConfigButton />
</View>
</View>
<View className="w-64 p-4"> <View className="w-64 p-4">
<Input <Input
className="w-fit" className="w-fit"
@@ -89,7 +94,6 @@ export default function Login() {
</View> </View>
<View className="flex gap-2 flex-row"> <View className="flex gap-2 flex-row">
<Button title="Login" onPress={onLogin} /> <Button title="Login" onPress={onLogin} />
<Button title="Config" onPress={config} />
</View> </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

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