| @@ -151,3 +151,45 @@ export const calculateWeight = (qty: number, uom: Uom) => { | |||
| export const returnWeightUnit = (uom: Uom) => { | |||
| return uom.unit4 || uom.unit3 || uom.unit2 || uom.unit1; | |||
| }; | |||
| /** | |||
| * Formats departure time to HH:mm format | |||
| * Handles array format [hours, minutes] from API and string formats | |||
| */ | |||
| export const formatDepartureTime = (time: string | number[] | String | Number | null | undefined): string => { | |||
| if (!time) return "-"; | |||
| // Handle array format [hours, minutes] from API | |||
| if (Array.isArray(time) && time.length >= 2) { | |||
| const hours = time[0]; | |||
| const minutes = time[1]; | |||
| if (typeof hours === 'number' && typeof minutes === 'number' && | |||
| hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) { | |||
| return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; | |||
| } | |||
| } | |||
| const timeStr = String(time).trim(); | |||
| if (!timeStr || timeStr === "-") return "-"; | |||
| // If already in HH:mm format, return as is | |||
| if (/^\d{1,2}:\d{2}$/.test(timeStr)) { | |||
| const [hours, minutes] = timeStr.split(":"); | |||
| return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`; | |||
| } | |||
| return timeStr; | |||
| }; | |||
| /** | |||
| * Normalizes store ID to display format (2F or 4F) | |||
| */ | |||
| export const normalizeStoreId = (storeId: string | number | String | Number | null | undefined): string => { | |||
| if (!storeId) return "-"; | |||
| const storeIdStr = typeof storeId === 'string' || storeId instanceof String | |||
| ? String(storeId) | |||
| : String(storeId); | |||
| if (storeIdStr === "2" || storeIdStr === "2F") return "2F"; | |||
| if (storeIdStr === "4" || storeIdStr === "4F") return "4F"; | |||
| return storeIdStr; | |||
| }; | |||
| @@ -18,7 +18,7 @@ import { | |||
| InputLabel, | |||
| } from "@mui/material"; | |||
| import { useState, useMemo, useCallback, useEffect } from "react"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| @@ -43,6 +43,7 @@ type SearchParamNames = keyof SearchQuery; | |||
| const Shop: React.FC = () => { | |||
| const { t } = useTranslation("common"); | |||
| const router = useRouter(); | |||
| const searchParams = useSearchParams(); | |||
| const [activeTab, setActiveTab] = useState<number>(0); | |||
| const [rows, setRows] = useState<ShopRow[]>([]); | |||
| const [loading, setLoading] = useState<boolean>(false); | |||
| @@ -235,26 +236,33 @@ const Shop: React.FC = () => { | |||
| name: "id", | |||
| label: t("id"), | |||
| type: "integer", | |||
| sx: { width: "100px", minWidth: "100px", maxWidth: "100px" }, | |||
| renderCell: (item) => String(item.id ?? ""), | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Code"), | |||
| sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, | |||
| renderCell: (item) => String(item.code ?? ""), | |||
| }, | |||
| { | |||
| name: "name", | |||
| label: t("Name"), | |||
| sx: { width: "200px", minWidth: "200px", maxWidth: "200px" }, | |||
| renderCell: (item) => String(item.name ?? ""), | |||
| }, | |||
| { | |||
| name: "addr3", | |||
| label: t("Addr3"), | |||
| sx: { width: "200px", minWidth: "200px", maxWidth: "200px" }, | |||
| renderCell: (item) => String((item as any).addr3 ?? ""), | |||
| }, | |||
| { | |||
| name: "truckLanceStatus", | |||
| label: t("TruckLance Status"), | |||
| align: "center", | |||
| headerAlign: "center", | |||
| sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, | |||
| renderCell: (item) => { | |||
| const status = item.truckLanceStatus; | |||
| if (status === "complete") { | |||
| @@ -269,7 +277,9 @@ const Shop: React.FC = () => { | |||
| { | |||
| name: "actions", | |||
| label: t("Actions"), | |||
| align: "right", | |||
| headerAlign: "right", | |||
| sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, | |||
| renderCell: (item) => ( | |||
| <Button | |||
| size="small" | |||
| @@ -282,6 +292,17 @@ const Shop: React.FC = () => { | |||
| }, | |||
| ]; | |||
| // Initialize activeTab from URL parameter | |||
| useEffect(() => { | |||
| const tabParam = searchParams.get("tab"); | |||
| if (tabParam !== null) { | |||
| const tabIndex = parseInt(tabParam, 10); | |||
| if (!isNaN(tabIndex) && (tabIndex === 0 || tabIndex === 1)) { | |||
| setActiveTab(tabIndex); | |||
| } | |||
| } | |||
| }, [searchParams]); | |||
| useEffect(() => { | |||
| if (activeTab === 0) { | |||
| fetchAllShops(); | |||
| @@ -290,82 +311,99 @@ const Shop: React.FC = () => { | |||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | |||
| setActiveTab(newValue); | |||
| // Update URL to reflect the selected tab | |||
| const url = new URL(window.location.href); | |||
| url.searchParams.set("tab", String(newValue)); | |||
| router.push(url.pathname + url.search); | |||
| }; | |||
| return ( | |||
| <Box> | |||
| <Card sx={{ mb: 2 }}> | |||
| <CardContent> | |||
| <Tabs | |||
| value={activeTab} | |||
| onChange={handleTabChange} | |||
| sx={{ | |||
| mb: 3, | |||
| borderBottom: 1, | |||
| borderColor: 'divider' | |||
| }} | |||
| > | |||
| <Tab label={t("Shop")} /> | |||
| <Tab label={t("Truck Lane")} /> | |||
| </Tabs> | |||
| {/* Header section with title */} | |||
| <Box sx={{ | |||
| p: 2, | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| <Typography variant="h4"> | |||
| 店鋪路線管理 | |||
| </Typography> | |||
| </Box> | |||
| {/* Tabs section */} | |||
| <Box sx={{ | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| <Tabs | |||
| value={activeTab} | |||
| onChange={handleTabChange} | |||
| > | |||
| <Tab label={t("Shop")} /> | |||
| <Tab label={t("Truck Lane")} /> | |||
| </Tabs> | |||
| </Box> | |||
| {activeTab === 0 && ( | |||
| <SearchBox | |||
| criteria={criteria as Criterion<string>[]} | |||
| onSearch={handleSearch} | |||
| onReset={() => { | |||
| setRows([]); | |||
| setFilters({}); | |||
| }} | |||
| /> | |||
| )} | |||
| </CardContent> | |||
| </Card> | |||
| {/* Content section */} | |||
| <Box sx={{ p: 2 }}> | |||
| {activeTab === 0 && ( | |||
| <> | |||
| <Card sx={{ mb: 2 }}> | |||
| <CardContent> | |||
| <SearchBox | |||
| criteria={criteria as Criterion<string>[]} | |||
| onSearch={handleSearch} | |||
| onReset={() => { | |||
| setRows([]); | |||
| setFilters({}); | |||
| }} | |||
| /> | |||
| </CardContent> | |||
| </Card> | |||
| {activeTab === 0 && ( | |||
| <Card> | |||
| <CardContent> | |||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}> | |||
| <Typography variant="h6">{t("Shop")}</Typography> | |||
| <FormControl size="small" sx={{ minWidth: 200 }}> | |||
| <InputLabel>{t("Filter by Status")}</InputLabel> | |||
| <Select | |||
| value={statusFilter} | |||
| label={t("Filter by Status")} | |||
| onChange={(e) => setStatusFilter(e.target.value)} | |||
| > | |||
| <MenuItem value="all">{t("All")}</MenuItem> | |||
| <MenuItem value="complete">{t("Complete")}</MenuItem> | |||
| <MenuItem value="missing">{t("Missing Data")}</MenuItem> | |||
| <MenuItem value="no-truck">{t("No TruckLance")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Stack> | |||
| {error && ( | |||
| <Alert severity="error" sx={{ mb: 2 }}> | |||
| {error} | |||
| </Alert> | |||
| )} | |||
| <Card> | |||
| <CardContent> | |||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}> | |||
| <Typography variant="h6">{t("Shop")}</Typography> | |||
| <FormControl size="small" sx={{ minWidth: 200 }}> | |||
| <InputLabel>{t("Filter by Status")}</InputLabel> | |||
| <Select | |||
| value={statusFilter} | |||
| label={t("Filter by Status")} | |||
| onChange={(e) => setStatusFilter(e.target.value)} | |||
| > | |||
| <MenuItem value="all">{t("All")}</MenuItem> | |||
| <MenuItem value="complete">{t("Complete")}</MenuItem> | |||
| <MenuItem value="missing">{t("Missing Data")}</MenuItem> | |||
| <MenuItem value="no-truck">{t("No TruckLance")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Stack> | |||
| {error && ( | |||
| <Alert severity="error" sx={{ mb: 2 }}> | |||
| {error} | |||
| </Alert> | |||
| )} | |||
| {loading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <SearchResults | |||
| items={filteredRows} | |||
| columns={columns} | |||
| pagingController={pagingController} | |||
| setPagingController={setPagingController} | |||
| /> | |||
| )} | |||
| </CardContent> | |||
| </Card> | |||
| )} | |||
| {loading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <SearchResults | |||
| items={filteredRows} | |||
| columns={columns} | |||
| pagingController={pagingController} | |||
| setPagingController={setPagingController} | |||
| /> | |||
| )} | |||
| </CardContent> | |||
| </Card> | |||
| </> | |||
| )} | |||
| {activeTab === 1 && ( | |||
| <TruckLane /> | |||
| )} | |||
| {activeTab === 1 && ( | |||
| <TruckLane /> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| }; | |||
| @@ -48,6 +48,7 @@ import { | |||
| createTruckClient | |||
| } from "@/app/api/shop/client"; | |||
| import type { SessionWithTokens } from "@/config/authConfig"; | |||
| import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; | |||
| type ShopDetailData = { | |||
| id: number; | |||
| @@ -62,61 +63,6 @@ type ShopDetailData = { | |||
| contactName: String; | |||
| }; | |||
| // Utility function to format departureTime to HH:mm format | |||
| const formatDepartureTime = (time: string | number[] | null | undefined): string => { | |||
| if (!time) return "-"; | |||
| // Handle array format [hours, minutes] from API | |||
| if (Array.isArray(time) && time.length >= 2) { | |||
| const hours = time[0]; | |||
| const minutes = time[1]; | |||
| if (typeof hours === 'number' && typeof minutes === 'number' && | |||
| hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) { | |||
| return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; | |||
| } | |||
| } | |||
| const timeStr = String(time).trim(); | |||
| if (!timeStr || timeStr === "-") return "-"; | |||
| // If already in HH:mm format, return as is | |||
| if (/^\d{1,2}:\d{2}$/.test(timeStr)) { | |||
| const [hours, minutes] = timeStr.split(":"); | |||
| return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`; | |||
| } | |||
| // Handle decimal format (e.g., "17,0" or "17.0" representing hours) | |||
| const decimalMatch = timeStr.match(/^(\d+)[,.](\d+)$/); | |||
| if (decimalMatch) { | |||
| const hours = parseInt(decimalMatch[1], 10); | |||
| const minutes = Math.round(parseFloat(`0.${decimalMatch[2]}`) * 60); | |||
| return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; | |||
| } | |||
| // Handle single number as hours (e.g., "17" -> "17:00") | |||
| const hoursOnly = parseInt(timeStr, 10); | |||
| if (!isNaN(hoursOnly) && hoursOnly >= 0 && hoursOnly <= 23) { | |||
| return `${hoursOnly.toString().padStart(2, "0")}:00`; | |||
| } | |||
| // Try to parse as ISO time string or other formats | |||
| try { | |||
| // If it's already a valid time string, try to extract hours and minutes | |||
| const parts = timeStr.split(/[:,\s]/); | |||
| if (parts.length >= 2) { | |||
| const h = parseInt(parts[0], 10); | |||
| const m = parseInt(parts[1], 10); | |||
| if (!isNaN(h) && !isNaN(m) && h >= 0 && h <= 23 && m >= 0 && m <= 59) { | |||
| return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}`; | |||
| } | |||
| } | |||
| } catch (e) { | |||
| // If parsing fails, return original string | |||
| } | |||
| return timeStr; | |||
| }; | |||
| // Utility function to convert HH:mm format to the format expected by backend | |||
| const parseDepartureTimeForBackend = (time: string): string => { | |||
| if (!time) return ""; | |||
| @@ -299,7 +245,7 @@ const ShopDetail: React.FC = () => { | |||
| } | |||
| // Convert storeId to string format (2F or 4F) | |||
| const storeIdStr = truck.storeId ? (typeof truck.storeId === 'string' ? truck.storeId : String(truck.storeId) === "2" ? "2F" : String(truck.storeId) === "4" ? "4F" : String(truck.storeId)) : "2F"; | |||
| const storeIdStr = normalizeStoreId(truck.storeId) || "2F"; | |||
| // Get remark value - use the remark from editedTruckData (user input) | |||
| // Only send remark if storeId is "4F", otherwise send null | |||
| @@ -482,7 +428,7 @@ const ShopDetail: React.FC = () => { | |||
| <Alert severity="error" sx={{ mb: 2 }}> | |||
| {error} | |||
| </Alert> | |||
| <Button onClick={() => router.back()}>{t("Back")}</Button> | |||
| <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button> | |||
| </Box> | |||
| ); | |||
| } | |||
| @@ -493,7 +439,7 @@ const ShopDetail: React.FC = () => { | |||
| <Alert severity="warning" sx={{ mb: 2 }}> | |||
| {t("Shop not found")} | |||
| </Alert> | |||
| <Button onClick={() => router.back()}>{t("Back")}</Button> | |||
| <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button> | |||
| </Box> | |||
| ); | |||
| } | |||
| @@ -504,7 +450,7 @@ const ShopDetail: React.FC = () => { | |||
| <CardContent> | |||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | |||
| <Typography variant="h6">{t("Shop Information")}</Typography> | |||
| <Button onClick={() => router.back()}>{t("Back")}</Button> | |||
| <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button> | |||
| </Box> | |||
| <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | |||
| @@ -682,22 +628,13 @@ const ShopDetail: React.FC = () => { | |||
| </Select> | |||
| </FormControl> | |||
| ) : ( | |||
| (() => { | |||
| const storeId = truck.storeId; | |||
| if (storeId === null || storeId === undefined) return "-"; | |||
| const storeIdStr = typeof storeId === 'string' ? storeId : String(storeId); | |||
| // Convert numeric values to display format | |||
| if (storeIdStr === "2" || storeIdStr === "2F") return "2F"; | |||
| if (storeIdStr === "4" || storeIdStr === "4F") return "4F"; | |||
| return storeIdStr; | |||
| })() | |||
| normalizeStoreId(truck.storeId) | |||
| )} | |||
| </TableCell> | |||
| <TableCell> | |||
| {isEditing ? ( | |||
| (() => { | |||
| const storeId = displayTruck?.storeId; | |||
| const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId) === "2" ? "2F" : String(storeId) === "4" ? "4F" : String(storeId)) : "2F"; | |||
| const storeIdStr = normalizeStoreId(displayTruck?.storeId) || "2F"; | |||
| const isEditable = storeIdStr === "4F"; | |||
| return ( | |||
| @@ -36,46 +36,7 @@ import { useTranslation } from "react-i18next"; | |||
| import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client"; | |||
| import type { Truck } from "@/app/api/shop/actions"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| // Utility function to format departureTime to HH:mm format | |||
| const formatDepartureTime = (time: string | number[] | null | undefined): string => { | |||
| if (!time) return "-"; | |||
| // Handle array format [hours, minutes] from API | |||
| if (Array.isArray(time) && time.length >= 2) { | |||
| const hours = time[0]; | |||
| const minutes = time[1]; | |||
| if (typeof hours === 'number' && typeof minutes === 'number' && | |||
| hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) { | |||
| return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; | |||
| } | |||
| } | |||
| const timeStr = String(time).trim(); | |||
| if (!timeStr || timeStr === "-") return "-"; | |||
| // If already in HH:mm format, return as is | |||
| if (/^\d{1,2}:\d{2}$/.test(timeStr)) { | |||
| const [hours, minutes] = timeStr.split(":"); | |||
| return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`; | |||
| } | |||
| return timeStr; | |||
| }; | |||
| // Utility function to convert HH:mm format to the format expected by backend | |||
| const parseDepartureTimeForBackend = (time: string): string => { | |||
| if (!time) return ""; | |||
| const timeStr = String(time).trim(); | |||
| // If already in HH:mm format, return as is | |||
| if (/^\d{1,2}:\d{2}$/.test(timeStr)) { | |||
| return timeStr; | |||
| } | |||
| // Try to format it | |||
| return formatDepartureTime(timeStr); | |||
| }; | |||
| import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; | |||
| type SearchQuery = { | |||
| truckLanceCode: string; | |||
| @@ -128,39 +89,34 @@ const TruckLane: React.FC = () => { | |||
| }; | |||
| fetchTruckLanes(); | |||
| }, []); | |||
| }, [t]); | |||
| // Client-side filtered rows (contains-matching) | |||
| const filteredRows = useMemo(() => { | |||
| const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== ""); | |||
| const normalized = (truckData || []).filter((r) => { | |||
| // Apply contains matching for each active filter | |||
| for (const k of fKeys) { | |||
| const v = String((filters as any)[k] ?? "").trim(); | |||
| const fKeys = Object.keys(filters).filter((k) => String(filters[k] ?? "").trim() !== ""); | |||
| if (fKeys.length === 0) return truckData; | |||
| return truckData.filter((truck) => { | |||
| for (const key of fKeys) { | |||
| const filterValue = String(filters[key] ?? "").trim().toLowerCase(); | |||
| if (k === "truckLanceCode") { | |||
| const rv = String((r as any).truckLanceCode ?? "").trim(); | |||
| if (!rv.toLowerCase().includes(v.toLowerCase())) return false; | |||
| } else if (k === "departureTime") { | |||
| if (key === "truckLanceCode") { | |||
| const truckCode = String(truck.truckLanceCode ?? "").trim().toLowerCase(); | |||
| if (!truckCode.includes(filterValue)) return false; | |||
| } else if (key === "departureTime") { | |||
| const formattedTime = formatDepartureTime( | |||
| Array.isArray(r.departureTime) | |||
| ? r.departureTime | |||
| : (r.departureTime ? String(r.departureTime) : null) | |||
| Array.isArray(truck.departureTime) | |||
| ? truck.departureTime | |||
| : (truck.departureTime ? String(truck.departureTime) : null) | |||
| ); | |||
| if (!formattedTime.toLowerCase().includes(v.toLowerCase())) return false; | |||
| } else if (k === "storeId") { | |||
| const rv = String((r as any).storeId ?? "").trim(); | |||
| const storeIdStr = typeof rv === 'string' ? rv : String(rv); | |||
| // Convert numeric values to display format for comparison | |||
| let displayStoreId = storeIdStr; | |||
| if (storeIdStr === "2" || storeIdStr === "2F") displayStoreId = "2F"; | |||
| if (storeIdStr === "4" || storeIdStr === "4F") displayStoreId = "4F"; | |||
| if (!displayStoreId.toLowerCase().includes(v.toLowerCase())) return false; | |||
| if (!formattedTime.toLowerCase().includes(filterValue)) return false; | |||
| } else if (key === "storeId") { | |||
| const displayStoreId = normalizeStoreId(truck.storeId); | |||
| if (!displayStoreId.toLowerCase().includes(filterValue)) return false; | |||
| } | |||
| } | |||
| return true; | |||
| }); | |||
| return normalized; | |||
| }, [truckData, filters]); | |||
| // Paginated rows | |||
| @@ -235,12 +191,10 @@ const TruckLane: React.FC = () => { | |||
| setSaving(true); | |||
| setError(null); | |||
| try { | |||
| const departureTime = parseDepartureTimeForBackend(newTruck.departureTime); | |||
| await createTruckWithoutShopClient({ | |||
| store_id: newTruck.storeId, | |||
| truckLanceCode: newTruck.truckLanceCode.trim(), | |||
| departureTime: departureTime, | |||
| departureTime: newTruck.departureTime.trim(), | |||
| loadingSequence: 0, | |||
| districtReference: null, | |||
| remark: null, | |||
| @@ -249,8 +203,8 @@ const TruckLane: React.FC = () => { | |||
| // Refresh truck data after create | |||
| const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | |||
| const uniqueCodes = new Map<string, Truck>(); | |||
| (data || []).forEach((truck) => { | |||
| const code = String(truck.truckLanceCode || "").trim(); | |||
| data.forEach((truck) => { | |||
| const code = String(truck.truckLanceCode ?? "").trim(); | |||
| if (code && !uniqueCodes.has(code)) { | |||
| uniqueCodes.set(code, truck); | |||
| } | |||
| @@ -258,9 +212,10 @@ const TruckLane: React.FC = () => { | |||
| setTruckData(Array.from(uniqueCodes.values())); | |||
| handleCloseAddDialog(); | |||
| } catch (err: any) { | |||
| } catch (err: unknown) { | |||
| console.error("Failed to create truck:", err); | |||
| setError(err?.message ?? String(err) ?? t("Failed to create truck")); | |||
| const errorMessage = err instanceof Error ? err.message : String(err); | |||
| setError(errorMessage || t("Failed to create truck")); | |||
| } finally { | |||
| setSaving(false); | |||
| } | |||
| @@ -322,10 +277,18 @@ const TruckLane: React.FC = () => { | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell>{t("TruckLance Code")}</TableCell> | |||
| <TableCell>{t("Departure Time")}</TableCell> | |||
| <TableCell>{t("Store ID")}</TableCell> | |||
| <TableCell align="right">{t("Actions")}</TableCell> | |||
| <TableCell sx={{ width: "250px", minWidth: "250px", maxWidth: "250px" }}> | |||
| {t("TruckLance Code")} | |||
| </TableCell> | |||
| <TableCell sx={{ width: "200px", minWidth: "200px", maxWidth: "200px" }}> | |||
| {t("Departure Time")} | |||
| </TableCell> | |||
| <TableCell sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}> | |||
| {t("Store ID")} | |||
| </TableCell> | |||
| <TableCell align="right" sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}> | |||
| {t("Actions")} | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| @@ -338,40 +301,36 @@ const TruckLane: React.FC = () => { | |||
| </TableCell> | |||
| </TableRow> | |||
| ) : ( | |||
| paginatedRows.map((truck, index) => { | |||
| const storeId = truck.storeId; | |||
| const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "-"; | |||
| const displayStoreId = storeIdStr === "2" || storeIdStr === "2F" ? "2F" | |||
| : storeIdStr === "4" || storeIdStr === "4F" ? "4F" | |||
| : storeIdStr; | |||
| return ( | |||
| <TableRow key={truck.id ?? `truck-${index}`}> | |||
| <TableCell> | |||
| {String(truck.truckLanceCode || "-")} | |||
| </TableCell> | |||
| <TableCell> | |||
| {formatDepartureTime( | |||
| Array.isArray(truck.departureTime) | |||
| ? truck.departureTime | |||
| : (truck.departureTime ? String(truck.departureTime) : null) | |||
| )} | |||
| </TableCell> | |||
| <TableCell> | |||
| {displayStoreId} | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| onClick={() => handleViewDetail(truck)} | |||
| > | |||
| {t("View Detail")} | |||
| </Button> | |||
| </TableCell> | |||
| </TableRow> | |||
| ); | |||
| }) | |||
| paginatedRows.map((truck) => ( | |||
| <TableRow key={truck.id ?? `truck-${truck.truckLanceCode}`}> | |||
| <TableCell sx={{ width: "250px", minWidth: "250px", maxWidth: "250px" }}> | |||
| {String(truck.truckLanceCode ?? "-")} | |||
| </TableCell> | |||
| <TableCell sx={{ width: "200px", minWidth: "200px", maxWidth: "200px" }}> | |||
| {formatDepartureTime( | |||
| Array.isArray(truck.departureTime) | |||
| ? truck.departureTime | |||
| : (truck.departureTime ? String(truck.departureTime) : null) | |||
| )} | |||
| </TableCell> | |||
| <TableCell sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}> | |||
| {normalizeStoreId( | |||
| truck.storeId ? (typeof truck.storeId === 'string' || truck.storeId instanceof String | |||
| ? String(truck.storeId) | |||
| : String(truck.storeId)) : null | |||
| )} | |||
| </TableCell> | |||
| <TableCell align="right" sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}> | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| onClick={() => handleViewDetail(truck)} | |||
| > | |||
| {t("View Detail")} | |||
| </Button> | |||
| </TableCell> | |||
| </TableRow> | |||
| )) | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| @@ -33,34 +33,21 @@ import AddIcon from "@mui/icons-material/Add"; | |||
| import { useState, useEffect } from "react"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeClient, deleteTruckLaneClient, updateTruckShopDetailsClient, fetchAllShopsClient, findAllUniqueShopNamesAndCodesFromTrucksClient, findAllUniqueRemarksFromTrucksClient, findAllUniqueShopCodesFromTrucksClient, findAllUniqueShopNamesFromTrucksClient, createTruckClient, findAllByTruckLanceCodeAndDeletedFalseClient } from "@/app/api/shop/client"; | |||
| import { | |||
| findAllUniqueTruckLaneCombinationsClient, | |||
| findAllShopsByTruckLanceCodeClient, | |||
| deleteTruckLaneClient, | |||
| updateTruckShopDetailsClient, | |||
| fetchAllShopsClient, | |||
| findAllUniqueShopNamesAndCodesFromTrucksClient, | |||
| findAllUniqueRemarksFromTrucksClient, | |||
| findAllUniqueShopCodesFromTrucksClient, | |||
| findAllUniqueShopNamesFromTrucksClient, | |||
| createTruckClient, | |||
| findAllByTruckLanceCodeAndDeletedFalseClient, | |||
| } from "@/app/api/shop/client"; | |||
| import type { Truck, ShopAndTruck, Shop } from "@/app/api/shop/actions"; | |||
| // Utility function to format departureTime to HH:mm format | |||
| const formatDepartureTime = (time: string | number[] | null | undefined): string => { | |||
| if (!time) return "-"; | |||
| // Handle array format [hours, minutes] from API | |||
| if (Array.isArray(time) && time.length >= 2) { | |||
| const hours = time[0]; | |||
| const minutes = time[1]; | |||
| if (typeof hours === 'number' && typeof minutes === 'number' && | |||
| hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) { | |||
| return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; | |||
| } | |||
| } | |||
| const timeStr = String(time).trim(); | |||
| if (!timeStr || timeStr === "-") return "-"; | |||
| // If already in HH:mm format, return as is | |||
| if (/^\d{1,2}:\d{2}$/.test(timeStr)) { | |||
| const [hours, minutes] = timeStr.split(":"); | |||
| return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`; | |||
| } | |||
| return timeStr; | |||
| }; | |||
| import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; | |||
| const TruckLaneDetail: React.FC = () => { | |||
| const { t } = useTranslation("common"); | |||
| @@ -95,59 +82,35 @@ const TruckLaneDetail: React.FC = () => { | |||
| severity: "success", | |||
| }); | |||
| // Fetch autocomplete data on mount | |||
| useEffect(() => { | |||
| // Fetch unique shop names and codes from truck table | |||
| const fetchShopNamesFromTrucks = async () => { | |||
| const fetchAutocompleteData = async () => { | |||
| try { | |||
| const shopData = await findAllUniqueShopNamesAndCodesFromTrucksClient() as Array<{ name: string; code: string }>; | |||
| const [shopData, remarks, codes, names] = await Promise.all([ | |||
| findAllUniqueShopNamesAndCodesFromTrucksClient() as Promise<Array<{ name: string; code: string }>>, | |||
| findAllUniqueRemarksFromTrucksClient() as Promise<string[]>, | |||
| findAllUniqueShopCodesFromTrucksClient() as Promise<string[]>, | |||
| findAllUniqueShopNamesFromTrucksClient() as Promise<string[]>, | |||
| ]); | |||
| // Convert to Shop format (id will be 0 since we don't have shop IDs from truck table) | |||
| const shopList: Shop[] = shopData.map((shop) => ({ | |||
| id: 0, // No shop ID available from truck table | |||
| id: 0, | |||
| name: shop.name || "", | |||
| code: shop.code || "", | |||
| addr3: "", | |||
| })); | |||
| setAllShops(shopList); | |||
| } catch (err: any) { | |||
| console.error("Failed to load shop names from trucks:", err); | |||
| } | |||
| }; | |||
| // Fetch unique remarks from truck table | |||
| const fetchRemarksFromTrucks = async () => { | |||
| try { | |||
| const remarks = await findAllUniqueRemarksFromTrucksClient() as string[]; | |||
| setUniqueRemarks(remarks || []); | |||
| } catch (err: any) { | |||
| console.error("Failed to load remarks from trucks:", err); | |||
| } | |||
| }; | |||
| // Fetch unique shop codes from truck table | |||
| const fetchShopCodesFromTrucks = async () => { | |||
| try { | |||
| const codes = await findAllUniqueShopCodesFromTrucksClient() as string[]; | |||
| setUniqueShopCodes(codes || []); | |||
| } catch (err: any) { | |||
| console.error("Failed to load shop codes from trucks:", err); | |||
| } | |||
| }; | |||
| // Fetch unique shop names from truck table | |||
| const fetchShopNamesFromTrucksOnly = async () => { | |||
| try { | |||
| const names = await findAllUniqueShopNamesFromTrucksClient() as string[]; | |||
| setUniqueShopNames(names || []); | |||
| } catch (err: any) { | |||
| console.error("Failed to load shop names from trucks:", err); | |||
| } catch (err) { | |||
| console.error("Failed to load autocomplete data:", err); | |||
| } | |||
| }; | |||
| fetchShopNamesFromTrucks(); | |||
| fetchRemarksFromTrucks(); | |||
| fetchShopCodesFromTrucks(); | |||
| fetchShopNamesFromTrucksOnly(); | |||
| fetchAutocompleteData(); | |||
| }, []); | |||
| useEffect(() => { | |||
| @@ -409,7 +372,7 @@ const TruckLaneDetail: React.FC = () => { | |||
| }; | |||
| const handleBack = () => { | |||
| router.push("/settings/shop"); | |||
| router.push("/settings/shop?tab=1"); | |||
| }; | |||
| const handleOpenAddShopDialog = () => { | |||
| @@ -515,11 +478,10 @@ const TruckLaneDetail: React.FC = () => { | |||
| setError(null); | |||
| try { | |||
| // Get storeId from truckData | |||
| const storeId = truckData.storeId; | |||
| const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "2F"; | |||
| const displayStoreId = storeIdStr === "2" || storeIdStr === "2F" ? "2F" | |||
| : storeIdStr === "4" || storeIdStr === "4F" ? "4F" | |||
| : storeIdStr; | |||
| const storeIdValue = truckData.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String | |||
| ? String(truckData.storeId) | |||
| : String(truckData.storeId)) : "2F"; | |||
| const displayStoreId = normalizeStoreId(storeIdValue) || "2F"; | |||
| // Get departureTime from truckData | |||
| let departureTimeStr = ""; | |||
| @@ -648,11 +610,11 @@ const TruckLaneDetail: React.FC = () => { | |||
| ); | |||
| } | |||
| const storeId = truckData.storeId; | |||
| const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : "-"; | |||
| const displayStoreId = storeIdStr === "2" || storeIdStr === "2F" ? "2F" | |||
| : storeIdStr === "4" || storeIdStr === "4F" ? "4F" | |||
| : storeIdStr; | |||
| const displayStoreId = normalizeStoreId( | |||
| truckData.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String | |||
| ? String(truckData.storeId) | |||
| : String(truckData.storeId)) : null | |||
| ); | |||
| return ( | |||
| <Box> | |||
| @@ -854,9 +816,10 @@ const TruckLaneDetail: React.FC = () => { | |||
| <TableCell> | |||
| {editingRowIndex === index ? ( | |||
| (() => { | |||
| const storeId = truckData.storeId; | |||
| const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : ""; | |||
| const isEditable = storeIdStr === "4F" || storeIdStr === "4"; | |||
| const storeIdValue = truckData.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String | |||
| ? String(truckData.storeId) | |||
| : String(truckData.storeId)) : null; | |||
| const isEditable = normalizeStoreId(storeIdValue) === "4F"; | |||
| return ( | |||
| <Autocomplete | |||
| freeSolo | |||
| @@ -1037,9 +1000,10 @@ const TruckLaneDetail: React.FC = () => { | |||
| /> | |||
| </Grid> | |||
| {(() => { | |||
| const storeId = truckData?.storeId; | |||
| const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : ""; | |||
| const isEditable = storeIdStr === "4F" || storeIdStr === "4"; | |||
| const storeIdValue = truckData?.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String | |||
| ? String(truckData.storeId) | |||
| : String(truckData.storeId)) : null; | |||
| const isEditable = normalizeStoreId(storeIdValue) === "4F"; | |||
| return isEditable ? ( | |||
| <Grid item xs={12}> | |||
| <Autocomplete | |||
| @@ -1095,6 +1059,4 @@ const TruckLaneDetail: React.FC = () => { | |||
| ); | |||
| }; | |||
| export default TruckLaneDetail; | |||
| export default TruckLaneDetail; | |||