| @@ -151,3 +151,45 @@ export const calculateWeight = (qty: number, uom: Uom) => { | |||||
| export const returnWeightUnit = (uom: Uom) => { | export const returnWeightUnit = (uom: Uom) => { | ||||
| return uom.unit4 || uom.unit3 || uom.unit2 || uom.unit1; | 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, | InputLabel, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useState, useMemo, useCallback, useEffect } from "react"; | import { useState, useMemo, useCallback, useEffect } from "react"; | ||||
| import { useRouter } from "next/navigation"; | |||||
| import { useRouter, useSearchParams } from "next/navigation"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
| @@ -43,6 +43,7 @@ type SearchParamNames = keyof SearchQuery; | |||||
| const Shop: React.FC = () => { | const Shop: React.FC = () => { | ||||
| const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const searchParams = useSearchParams(); | |||||
| const [activeTab, setActiveTab] = useState<number>(0); | const [activeTab, setActiveTab] = useState<number>(0); | ||||
| const [rows, setRows] = useState<ShopRow[]>([]); | const [rows, setRows] = useState<ShopRow[]>([]); | ||||
| const [loading, setLoading] = useState<boolean>(false); | const [loading, setLoading] = useState<boolean>(false); | ||||
| @@ -235,26 +236,33 @@ const Shop: React.FC = () => { | |||||
| name: "id", | name: "id", | ||||
| label: t("id"), | label: t("id"), | ||||
| type: "integer", | type: "integer", | ||||
| sx: { width: "100px", minWidth: "100px", maxWidth: "100px" }, | |||||
| renderCell: (item) => String(item.id ?? ""), | renderCell: (item) => String(item.id ?? ""), | ||||
| }, | }, | ||||
| { | { | ||||
| name: "code", | name: "code", | ||||
| label: t("Code"), | label: t("Code"), | ||||
| sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, | |||||
| renderCell: (item) => String(item.code ?? ""), | renderCell: (item) => String(item.code ?? ""), | ||||
| }, | }, | ||||
| { | { | ||||
| name: "name", | name: "name", | ||||
| label: t("Name"), | label: t("Name"), | ||||
| sx: { width: "200px", minWidth: "200px", maxWidth: "200px" }, | |||||
| renderCell: (item) => String(item.name ?? ""), | renderCell: (item) => String(item.name ?? ""), | ||||
| }, | }, | ||||
| { | { | ||||
| name: "addr3", | name: "addr3", | ||||
| label: t("Addr3"), | label: t("Addr3"), | ||||
| sx: { width: "200px", minWidth: "200px", maxWidth: "200px" }, | |||||
| renderCell: (item) => String((item as any).addr3 ?? ""), | renderCell: (item) => String((item as any).addr3 ?? ""), | ||||
| }, | }, | ||||
| { | { | ||||
| name: "truckLanceStatus", | name: "truckLanceStatus", | ||||
| label: t("TruckLance Status"), | label: t("TruckLance Status"), | ||||
| align: "center", | |||||
| headerAlign: "center", | |||||
| sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, | |||||
| renderCell: (item) => { | renderCell: (item) => { | ||||
| const status = item.truckLanceStatus; | const status = item.truckLanceStatus; | ||||
| if (status === "complete") { | if (status === "complete") { | ||||
| @@ -269,7 +277,9 @@ const Shop: React.FC = () => { | |||||
| { | { | ||||
| name: "actions", | name: "actions", | ||||
| label: t("Actions"), | label: t("Actions"), | ||||
| align: "right", | |||||
| headerAlign: "right", | headerAlign: "right", | ||||
| sx: { width: "150px", minWidth: "150px", maxWidth: "150px" }, | |||||
| renderCell: (item) => ( | renderCell: (item) => ( | ||||
| <Button | <Button | ||||
| size="small" | 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(() => { | useEffect(() => { | ||||
| if (activeTab === 0) { | if (activeTab === 0) { | ||||
| fetchAllShops(); | fetchAllShops(); | ||||
| @@ -290,82 +311,99 @@ const Shop: React.FC = () => { | |||||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | ||||
| setActiveTab(newValue); | 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 ( | return ( | ||||
| <Box> | <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> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -48,6 +48,7 @@ import { | |||||
| createTruckClient | createTruckClient | ||||
| } from "@/app/api/shop/client"; | } from "@/app/api/shop/client"; | ||||
| import type { SessionWithTokens } from "@/config/authConfig"; | import type { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; | |||||
| type ShopDetailData = { | type ShopDetailData = { | ||||
| id: number; | id: number; | ||||
| @@ -62,61 +63,6 @@ type ShopDetailData = { | |||||
| contactName: String; | 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 | // Utility function to convert HH:mm format to the format expected by backend | ||||
| const parseDepartureTimeForBackend = (time: string): string => { | const parseDepartureTimeForBackend = (time: string): string => { | ||||
| if (!time) return ""; | if (!time) return ""; | ||||
| @@ -299,7 +245,7 @@ const ShopDetail: React.FC = () => { | |||||
| } | } | ||||
| // Convert storeId to string format (2F or 4F) | // 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) | // Get remark value - use the remark from editedTruckData (user input) | ||||
| // Only send remark if storeId is "4F", otherwise send null | // Only send remark if storeId is "4F", otherwise send null | ||||
| @@ -482,7 +428,7 @@ const ShopDetail: React.FC = () => { | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | <Alert severity="error" sx={{ mb: 2 }}> | ||||
| {error} | {error} | ||||
| </Alert> | </Alert> | ||||
| <Button onClick={() => router.back()}>{t("Back")}</Button> | |||||
| <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -493,7 +439,7 @@ const ShopDetail: React.FC = () => { | |||||
| <Alert severity="warning" sx={{ mb: 2 }}> | <Alert severity="warning" sx={{ mb: 2 }}> | ||||
| {t("Shop not found")} | {t("Shop not found")} | ||||
| </Alert> | </Alert> | ||||
| <Button onClick={() => router.back()}>{t("Back")}</Button> | |||||
| <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -504,7 +450,7 @@ const ShopDetail: React.FC = () => { | |||||
| <CardContent> | <CardContent> | ||||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | ||||
| <Typography variant="h6">{t("Shop Information")}</Typography> | <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> | ||||
| <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | ||||
| @@ -682,22 +628,13 @@ const ShopDetail: React.FC = () => { | |||||
| </Select> | </Select> | ||||
| </FormControl> | </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> | ||||
| <TableCell> | <TableCell> | ||||
| {isEditing ? ( | {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"; | const isEditable = storeIdStr === "4F"; | ||||
| return ( | return ( | ||||
| @@ -36,46 +36,7 @@ import { useTranslation } from "react-i18next"; | |||||
| import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client"; | import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client"; | ||||
| import type { Truck } from "@/app/api/shop/actions"; | import type { Truck } from "@/app/api/shop/actions"; | ||||
| import SearchBox, { Criterion } from "../SearchBox"; | 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 = { | type SearchQuery = { | ||||
| truckLanceCode: string; | truckLanceCode: string; | ||||
| @@ -128,39 +89,34 @@ const TruckLane: React.FC = () => { | |||||
| }; | }; | ||||
| fetchTruckLanes(); | fetchTruckLanes(); | ||||
| }, []); | |||||
| }, [t]); | |||||
| // Client-side filtered rows (contains-matching) | // Client-side filtered rows (contains-matching) | ||||
| const filteredRows = useMemo(() => { | 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( | 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 true; | ||||
| }); | }); | ||||
| return normalized; | |||||
| }, [truckData, filters]); | }, [truckData, filters]); | ||||
| // Paginated rows | // Paginated rows | ||||
| @@ -235,12 +191,10 @@ const TruckLane: React.FC = () => { | |||||
| setSaving(true); | setSaving(true); | ||||
| setError(null); | setError(null); | ||||
| try { | try { | ||||
| const departureTime = parseDepartureTimeForBackend(newTruck.departureTime); | |||||
| await createTruckWithoutShopClient({ | await createTruckWithoutShopClient({ | ||||
| store_id: newTruck.storeId, | store_id: newTruck.storeId, | ||||
| truckLanceCode: newTruck.truckLanceCode.trim(), | truckLanceCode: newTruck.truckLanceCode.trim(), | ||||
| departureTime: departureTime, | |||||
| departureTime: newTruck.departureTime.trim(), | |||||
| loadingSequence: 0, | loadingSequence: 0, | ||||
| districtReference: null, | districtReference: null, | ||||
| remark: null, | remark: null, | ||||
| @@ -249,8 +203,8 @@ const TruckLane: React.FC = () => { | |||||
| // Refresh truck data after create | // Refresh truck data after create | ||||
| const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; | ||||
| const uniqueCodes = new Map<string, 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)) { | if (code && !uniqueCodes.has(code)) { | ||||
| uniqueCodes.set(code, truck); | uniqueCodes.set(code, truck); | ||||
| } | } | ||||
| @@ -258,9 +212,10 @@ const TruckLane: React.FC = () => { | |||||
| setTruckData(Array.from(uniqueCodes.values())); | setTruckData(Array.from(uniqueCodes.values())); | ||||
| handleCloseAddDialog(); | handleCloseAddDialog(); | ||||
| } catch (err: any) { | |||||
| } catch (err: unknown) { | |||||
| console.error("Failed to create truck:", err); | 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 { | } finally { | ||||
| setSaving(false); | setSaving(false); | ||||
| } | } | ||||
| @@ -322,10 +277,18 @@ const TruckLane: React.FC = () => { | |||||
| <Table> | <Table> | ||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <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> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| @@ -338,40 +301,36 @@ const TruckLane: React.FC = () => { | |||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </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> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -33,34 +33,21 @@ import AddIcon from "@mui/icons-material/Add"; | |||||
| import { useState, useEffect } from "react"; | import { useState, useEffect } from "react"; | ||||
| import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
| import { useTranslation } from "react-i18next"; | 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"; | 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 TruckLaneDetail: React.FC = () => { | ||||
| const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
| @@ -95,59 +82,35 @@ const TruckLaneDetail: React.FC = () => { | |||||
| severity: "success", | severity: "success", | ||||
| }); | }); | ||||
| // Fetch autocomplete data on mount | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // Fetch unique shop names and codes from truck table | |||||
| const fetchShopNamesFromTrucks = async () => { | |||||
| const fetchAutocompleteData = async () => { | |||||
| try { | 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) | // Convert to Shop format (id will be 0 since we don't have shop IDs from truck table) | ||||
| const shopList: Shop[] = shopData.map((shop) => ({ | const shopList: Shop[] = shopData.map((shop) => ({ | ||||
| id: 0, // No shop ID available from truck table | |||||
| id: 0, | |||||
| name: shop.name || "", | name: shop.name || "", | ||||
| code: shop.code || "", | code: shop.code || "", | ||||
| addr3: "", | addr3: "", | ||||
| })); | })); | ||||
| setAllShops(shopList); | 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 || []); | 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 || []); | 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 || []); | 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(() => { | useEffect(() => { | ||||
| @@ -409,7 +372,7 @@ const TruckLaneDetail: React.FC = () => { | |||||
| }; | }; | ||||
| const handleBack = () => { | const handleBack = () => { | ||||
| router.push("/settings/shop"); | |||||
| router.push("/settings/shop?tab=1"); | |||||
| }; | }; | ||||
| const handleOpenAddShopDialog = () => { | const handleOpenAddShopDialog = () => { | ||||
| @@ -515,11 +478,10 @@ const TruckLaneDetail: React.FC = () => { | |||||
| setError(null); | setError(null); | ||||
| try { | try { | ||||
| // Get storeId from truckData | // 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 | // Get departureTime from truckData | ||||
| let departureTimeStr = ""; | 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 ( | return ( | ||||
| <Box> | <Box> | ||||
| @@ -854,9 +816,10 @@ const TruckLaneDetail: React.FC = () => { | |||||
| <TableCell> | <TableCell> | ||||
| {editingRowIndex === index ? ( | {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 ( | return ( | ||||
| <Autocomplete | <Autocomplete | ||||
| freeSolo | freeSolo | ||||
| @@ -1037,9 +1000,10 @@ const TruckLaneDetail: React.FC = () => { | |||||
| /> | /> | ||||
| </Grid> | </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 ? ( | return isEditable ? ( | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Autocomplete | <Autocomplete | ||||
| @@ -1095,6 +1059,4 @@ const TruckLaneDetail: React.FC = () => { | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default TruckLaneDetail; | |||||
| export default TruckLaneDetail; | |||||