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

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

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
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>Dock Scanning</Text>
<Button onPress={getInfo}>
<Text>Check info</Text>
</Button>
<View className="flex">
<View
style={{
// flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<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

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

View File

@@ -2,14 +2,18 @@ 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();
@@ -18,15 +22,18 @@ export default function RootLayout() {
return (
<>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="login" />
<Stack.Screen name="setup" />
<Stack.Screen name="updateScreen" />
<Stack.Screen name="(tabs)" />
</Stack>
<PortalHost />
<QueryClientProvider client={queryClient}>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="login" />
<Stack.Screen name="setup" />
<Stack.Screen name="updateScreen" />
<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" }}>
LST Scanner Login
</Text>
<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

@@ -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 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,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,
},
},
});