diff --git a/src/app/(main)/settings/shop/detail/page.tsx b/src/app/(main)/settings/shop/detail/page.tsx index bf37a8e..02c2334 100644 --- a/src/app/(main)/settings/shop/detail/page.tsx +++ b/src/app/(main)/settings/shop/detail/page.tsx @@ -4,9 +4,9 @@ import { I18nProvider, getServerI18n } from "@/i18n"; import GeneralLoading from "@/components/General/GeneralLoading"; export default async function ShopDetailPage() { - const { t } = await getServerI18n("shop"); + const { t } = await getServerI18n("shop", "common"); return ( - + }> diff --git a/src/app/(main)/settings/shop/page.tsx b/src/app/(main)/settings/shop/page.tsx index c3996b9..c4e8175 100644 --- a/src/app/(main)/settings/shop/page.tsx +++ b/src/app/(main)/settings/shop/page.tsx @@ -8,9 +8,9 @@ import { notFound } from "next/navigation"; export default async function ShopPage() { - const { t } = await getServerI18n("shop"); + const { t } = await getServerI18n("shop", "common"); return ( - + }> diff --git a/src/app/(main)/settings/shop/truckdetail/page.tsx b/src/app/(main)/settings/shop/truckdetail/page.tsx new file mode 100644 index 0000000..8c50c6d --- /dev/null +++ b/src/app/(main)/settings/shop/truckdetail/page.tsx @@ -0,0 +1,16 @@ +import { Suspense } from "react"; +import TruckLaneDetail from "@/components/Shop/TruckLaneDetail"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import GeneralLoading from "@/components/General/GeneralLoading"; + +export default async function TruckLaneDetailPage() { + const { t } = await getServerI18n("shop", "common"); + return ( + + }> + + + + ); +} + diff --git a/src/app/api/shop/actions.ts b/src/app/api/shop/actions.ts index 559a269..19231be 100644 --- a/src/app/api/shop/actions.ts +++ b/src/app/api/shop/actions.ts @@ -24,9 +24,11 @@ export interface ShopAndTruck{ contactName: String; truckLanceCode: String; DepartureTime: String; - LoadingSequence: number; + LoadingSequence?: number | null; districtReference: Number; - Store_id: Number + Store_id: Number; + remark?: String | null; + truckId?: number; } export interface Shop{ @@ -60,6 +62,11 @@ export interface DeleteTruckLane { id: number; } +export interface UpdateLoadingSequenceRequest { + id: number; + loadingSequence: number; +} + export interface SaveTruckRequest { id?: number | null; store_id: string; @@ -132,6 +139,35 @@ export const deleteTruckLaneAction = async (data: DeleteTruckLane) => { export const createTruckAction = async (data: SaveTruckRequest) => { const endpoint = `${BASE_API_URL}/truck/create`; + return serverFetchJson(endpoint, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); +}; + +export const findAllUniqueTruckLaneCombinationsAction = cache(async () => { + const endpoint = `${BASE_API_URL}/truck/findAllUniqueTruckLanceCodeAndRemarkCombinations`; + + return serverFetchJson(endpoint, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const findAllShopsByTruckLanceCodeAndRemarkAction = cache(async (truckLanceCode: string, remark: string) => { + const endpoint = `${BASE_API_URL}/truck/findAllFromShopAndTruckByTruckLanceCodeAndRemarkAndDeletedFalse`; + const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}&remark=${encodeURIComponent(remark)}`; + + return serverFetchJson(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => { + const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`; + return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), diff --git a/src/app/api/shop/client.ts b/src/app/api/shop/client.ts index bbd3fe5..6f2da57 100644 --- a/src/app/api/shop/client.ts +++ b/src/app/api/shop/client.ts @@ -6,9 +6,13 @@ import { updateTruckLaneAction, deleteTruckLaneAction, createTruckAction, + findAllUniqueTruckLaneCombinationsAction, + findAllShopsByTruckLanceCodeAndRemarkAction, + updateLoadingSequenceAction, type SaveTruckLane, type DeleteTruckLane, type SaveTruckRequest, + type UpdateLoadingSequenceRequest, type MessageResponse } from "./actions"; @@ -32,4 +36,16 @@ export const createTruckClient = async (data: SaveTruckRequest): Promise { + return await findAllUniqueTruckLaneCombinationsAction(); +}; + +export const findAllShopsByTruckLanceCodeAndRemarkClient = async (truckLanceCode: string, remark: string) => { + return await findAllShopsByTruckLanceCodeAndRemarkAction(truckLanceCode, remark); +}; + +export const updateLoadingSequenceClient = async (data: UpdateLoadingSequenceRequest): Promise => { + return await updateLoadingSequenceAction(data); +}; + export default fetchAllShopsClient; diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 4043043..0a872e4 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -16,7 +16,10 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/qcItem": "Qc Item", "/settings/qrCodeHandle": "QR Code Handle", "/settings/rss": "Demand Forecast Setting", - "/settings/equipment": "Equipment", + "/settings/equipment": "Equipment", + "/settings/shop": "Shop", + "/settings/shop/detail": "Shop Detail", + "/settings/shop/truckdetail": "Truck Lane Detail", "/scheduling/rough": "Demand Forecast", "/scheduling/rough/edit": "FG & Material Demand Forecast Detail", "/scheduling/detailed": "Detail Scheduling", diff --git a/src/components/Shop/Shop.tsx b/src/components/Shop/Shop.tsx index 0eb2605..ff4136c 100644 --- a/src/components/Shop/Shop.tsx +++ b/src/components/Shop/Shop.tsx @@ -10,14 +10,22 @@ import { Alert, CircularProgress, Chip, + Tabs, + Tab, + Select, + MenuItem, + FormControl, + InputLabel, } from "@mui/material"; import { useState, useMemo, useCallback, useEffect } from "react"; import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; import SearchBox, { Criterion } from "../SearchBox"; import SearchResults, { Column } from "../SearchResults"; import { defaultPagingController } from "../SearchResults/SearchResults"; import { fetchAllShopsClient } from "@/app/api/shop/client"; import type { Shop, ShopAndTruck } from "@/app/api/shop/actions"; +import TruckLane from "./TruckLane"; type ShopRow = Shop & { actions?: string; @@ -33,17 +41,20 @@ type SearchQuery = { type SearchParamNames = keyof SearchQuery; const Shop: React.FC = () => { + const { t } = useTranslation("common"); const router = useRouter(); + const [activeTab, setActiveTab] = useState(0); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [filters, setFilters] = useState>({}); + const [statusFilter, setStatusFilter] = useState("all"); const [pagingController, setPagingController] = useState(defaultPagingController); - // client-side filtered rows (contains-matching) + // client-side filtered rows (contains-matching + status filter) const filteredRows = useMemo(() => { const fKeys = Object.keys(filters || {}).filter((k) => String((filters as any)[k]).trim() !== ""); - const normalized = (rows || []).filter((r) => { + let normalized = (rows || []).filter((r) => { // apply contains matching for each active filter for (const k of fKeys) { const v = String((filters as any)[k] ?? "").trim(); @@ -63,8 +74,16 @@ const Shop: React.FC = () => { } return true; }); + + // Apply status filter + if (statusFilter !== "all") { + normalized = normalized.filter((r) => { + return r.truckLanceStatus === statusFilter; + }); + } + return normalized; - }, [rows, filters]); + }, [rows, filters, statusFilter]); // Check if a shop has missing truckLanceCode data const checkTruckLanceStatus = useCallback((shopTrucks: ShopAndTruck[]): "complete" | "missing" | "no-truck" => { @@ -72,16 +91,73 @@ const Shop: React.FC = () => { return "no-truck"; } + // Check if shop has any actual truck lanes (not just null entries from LEFT JOIN) + // A shop with no trucks will have entries with null truckLanceCode + const hasAnyTruckLane = shopTrucks.some((truck) => { + const truckLanceCode = (truck as any).truckLanceCode; + return truckLanceCode != null && String(truckLanceCode).trim() !== ""; + }); + + if (!hasAnyTruckLane) { + return "no-truck"; + } + // Check each truckLanceCode entry for missing data for (const truck of shopTrucks) { - const hasTruckLanceCode = truck.truckLanceCode && String(truck.truckLanceCode).trim() !== ""; - const hasDepartureTime = truck.DepartureTime && String(truck.DepartureTime).trim() !== ""; - const hasLoadingSequence = truck.LoadingSequence !== null && truck.LoadingSequence !== undefined; - const hasDistrictReference = truck.districtReference !== null && truck.districtReference !== undefined; - const hasStoreId = truck.Store_id !== null && truck.Store_id !== undefined; + // Skip entries without truckLanceCode (they're from LEFT JOIN when no trucks exist) + const truckLanceCode = (truck as any).truckLanceCode; + if (!truckLanceCode || String(truckLanceCode).trim() === "") { + continue; // Skip this entry, it's not a real truck lane + } + + // Check truckLanceCode: must exist and not be empty (already validated above) + const hasTruckLanceCode = truckLanceCode != null && String(truckLanceCode).trim() !== ""; + + // Check departureTime: must exist and not be empty + // Can be array format [hours, minutes] or string format + const departureTime = (truck as any).departureTime || (truck as any).DepartureTime; + let hasDepartureTime = false; + if (departureTime != null) { + if (Array.isArray(departureTime) && departureTime.length >= 2) { + // Array format [hours, minutes] + hasDepartureTime = true; + } else { + // String format + const timeStr = String(departureTime).trim(); + hasDepartureTime = timeStr !== "" && timeStr !== "-"; + } + } + + // Check loadingSequence: must exist and not be 0 + const loadingSeq = (truck as any).loadingSequence || (truck as any).LoadingSequence; + const loadingSeqNum = loadingSeq != null && loadingSeq !== undefined ? Number(loadingSeq) : null; + const hasLoadingSequence = loadingSeqNum !== null && !isNaN(loadingSeqNum) && loadingSeqNum !== 0; + + // Check districtReference: must exist and not be 0 + const districtRef = (truck as any).districtReference; + const districtRefNum = districtRef != null && districtRef !== undefined ? Number(districtRef) : null; + const hasDistrictReference = districtRefNum !== null && !isNaN(districtRefNum) && districtRefNum !== 0; + + // Check storeId: must exist and not be 0 (can be string "2F"/"4F" or number) + // Actual field name in JSON is store_id (underscore, lowercase) + const storeId = (truck as any).store_id || (truck as any).storeId || (truck as any).Store_id; + let storeIdValid = false; + if (storeId != null && storeId !== undefined && storeId !== "") { + const storeIdStr = String(storeId).trim(); + // If it's "2F" or "4F", it's valid (not 0) + if (storeIdStr === "2F" || storeIdStr === "4F") { + storeIdValid = true; + } else { + const storeIdNum = Number(storeId); + // If it's a valid number and not 0, it's valid + if (!isNaN(storeIdNum) && storeIdNum !== 0) { + storeIdValid = true; + } + } + } - // If any required field is missing, return "missing" - if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !hasDistrictReference || !hasStoreId) { + // If any required field is missing or equals 0, return "missing" + if (!hasTruckLanceCode || !hasDepartureTime || !hasLoadingSequence || !hasDistrictReference || !storeIdValid) { return "missing"; } } @@ -149,50 +225,50 @@ const Shop: React.FC = () => { ); const criteria: Criterion[] = [ - { type: "text", label: "id", paramName: "id" }, - { type: "text", label: "code", paramName: "code" }, - { type: "text", label: "name", paramName: "name" }, + { type: "text", label: t("id"), paramName: "id" }, + { type: "text", label: t("code"), paramName: "code" }, + { type: "text", label: t("name"), paramName: "name" }, ]; const columns: Column[] = [ { name: "id", - label: "Id", + label: t("id"), type: "integer", renderCell: (item) => String(item.id ?? ""), }, { name: "code", - label: "Code", + label: t("Code"), renderCell: (item) => String(item.code ?? ""), }, { name: "name", - label: "Name", + label: t("Name"), renderCell: (item) => String(item.name ?? ""), }, { name: "addr3", - label: "Addr3", + label: t("Addr3"), renderCell: (item) => String((item as any).addr3 ?? ""), }, { name: "truckLanceStatus", - label: "TruckLance Status", + label: t("TruckLance Status"), renderCell: (item) => { const status = item.truckLanceStatus; if (status === "complete") { - return ; + return ; } else if (status === "missing") { - return ; + return ; } else { - return ; + return ; } }, }, { name: "actions", - label: "Actions", + label: t("Actions"), headerAlign: "right", renderCell: (item) => ( ), }, ]; useEffect(() => { - fetchAllShops(); - }, []); + if (activeTab === 0) { + fetchAllShops(); + } + }, [activeTab]); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue); + }; return ( - []} - onSearch={handleSearch} - onReset={() => { - setRows([]); - setFilters({}); + - - - - - - - Shop - - {error && ( - - {error} - - )} + > + + + - {loading ? ( - - - - ) : ( - []} + onSearch={handleSearch} + onReset={() => { + setRows([]); + setFilters({}); + }} /> )} + + {activeTab === 0 && ( + + + + {t("Shop")} + + {t("Filter by Status")} + + + + {error && ( + + {error} + + )} + + {loading ? ( + + + + ) : ( + + )} + + + )} + + {activeTab === 1 && ( + + )} ); }; diff --git a/src/components/Shop/ShopDetail.tsx b/src/components/Shop/ShopDetail.tsx index f2e4282..c03d153 100644 --- a/src/components/Shop/ShopDetail.tsx +++ b/src/components/Shop/ShopDetail.tsx @@ -38,6 +38,7 @@ import AddIcon from "@mui/icons-material/Add"; import { useRouter, useSearchParams } from "next/navigation"; import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; +import { useTranslation } from "react-i18next"; import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions"; import { fetchAllShopsClient, @@ -131,6 +132,7 @@ const parseDepartureTimeForBackend = (time: string): string => { }; const ShopDetail: React.FC = () => { + const { t } = useTranslation("common"); const router = useRouter(); const searchParams = useSearchParams(); const shopId = searchParams.get("id"); @@ -163,14 +165,14 @@ const ShopDetail: React.FC = () => { // If session is unauthenticated, don't make API calls (middleware will handle redirect) if (sessionStatus === "unauthenticated" || !session) { - setError("Please log in to view shop details"); + setError(t("Please log in to view shop details")); setLoading(false); return; } const fetchShopDetail = async () => { if (!shopId) { - setError("Shop ID is required"); + setError(t("Shop ID is required")); setLoading(false); return; } @@ -178,7 +180,7 @@ const ShopDetail: React.FC = () => { // Convert shopId to number for proper filtering const shopIdNum = parseInt(shopId, 10); if (isNaN(shopIdNum)) { - setError("Invalid Shop ID"); + setError(t("Invalid Shop ID")); setLoading(false); return; } @@ -212,7 +214,7 @@ const ShopDetail: React.FC = () => { contactName: shopData.contactName ?? "", }); } else { - setError("Shop not found"); + setError(t("Shop not found")); setLoading(false); return; } @@ -233,7 +235,7 @@ const ShopDetail: React.FC = () => { } catch (err: any) { console.error("Failed to load shop detail:", err); // Handle errors gracefully - don't trigger auto-logout - const errorMessage = err?.message ?? String(err) ?? "Failed to load shop details"; + const errorMessage = err?.message ?? String(err) ?? t("Failed to load shop details"); setError(errorMessage); } finally { setLoading(false); @@ -273,13 +275,13 @@ const ShopDetail: React.FC = () => { const handleSave = async (index: number) => { if (!shopId) { - setError("Shop ID is required"); + setError(t("Shop ID is required")); return; } const truck = editedTruckData[index]; if (!truck || !truck.id) { - setError("Invalid truck data"); + setError(t("Invalid shop data")); return; } @@ -335,7 +337,7 @@ const ShopDetail: React.FC = () => { setUniqueRemarks(remarks); } catch (err: any) { console.error("Failed to save truck data:", err); - setError(err?.message ?? String(err) ?? "Failed to save truck data"); + setError(err?.message ?? String(err) ?? t("Failed to save truck data")); } finally { setSaving(false); } @@ -351,12 +353,12 @@ const ShopDetail: React.FC = () => { }; const handleDelete = async (truckId: number) => { - if (!window.confirm("Are you sure you want to delete this truck lane?")) { + if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) { return; } if (!shopId) { - setError("Shop ID is required"); + setError(t("Shop ID is required")); return; } @@ -373,7 +375,7 @@ const ShopDetail: React.FC = () => { setEditingRowIndex(null); } catch (err: any) { console.error("Failed to delete truck lane:", err); - setError(err?.message ?? String(err) ?? "Failed to delete truck lane"); + setError(err?.message ?? String(err) ?? t("Failed to delete truck lane")); } finally { setSaving(false); } @@ -409,19 +411,19 @@ const ShopDetail: React.FC = () => { const missingFields: string[] = []; if (!shopId || !shopDetail) { - missingFields.push("Shop information"); + missingFields.push(t("Shop Information")); } if (!newTruck.truckLanceCode.trim()) { - missingFields.push("TruckLance Code"); + missingFields.push(t("TruckLance Code")); } if (!newTruck.departureTime) { - missingFields.push("Departure Time"); + missingFields.push(t("Departure Time")); } if (missingFields.length > 0) { - const message = `Please fill in the following required fields: ${missingFields.join(", ")}`; + const message = `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`; setSnackbarMessage(message); setSnackbarOpen(true); return; @@ -461,7 +463,7 @@ const ShopDetail: React.FC = () => { handleCloseAddDialog(); } catch (err: any) { console.error("Failed to create truck:", err); - setError(err?.message ?? String(err) ?? "Failed to create truck"); + setError(err?.message ?? String(err) ?? t("Failed to create truck")); } finally { setSaving(false); } @@ -475,12 +477,12 @@ const ShopDetail: React.FC = () => { ); } if (error) { - return ( + return ( {error} - + ); } @@ -489,9 +491,9 @@ const ShopDetail: React.FC = () => { return ( - Shop not found + {t("Shop not found")} - + ); } @@ -501,49 +503,45 @@ const ShopDetail: React.FC = () => { - Shop Information - + {t("Shop Information")} + - Shop ID + {t("Shop ID")} {shopDetail.id} - Name + {t("Name")} {shopDetail.name} - Code + {t("Code")} {shopDetail.code} - Addr1 + {t("Addr1")} {shopDetail.addr1 || "-"} - Addr2 + {t("Addr2")} {shopDetail.addr2 || "-"} - Addr3 + {t("Addr3")} {shopDetail.addr3 || "-"} - Contact No + {t("Contact No")} {shopDetail.contactNo || "-"} - Type - {shopDetail.type || "-"} - - - Contact Email + {t("Contact Email")} {shopDetail.contactEmail || "-"} - Contact Name + {t("Contact Name")} {shopDetail.contactName || "-"} @@ -553,27 +551,27 @@ const ShopDetail: React.FC = () => { - Truck Information + {t("Truck Information")} - TruckLance Code - Departure Time - Loading Sequence - District Reference - Store ID - Remark - Actions + {t("TruckLance Code")} + {t("Departure Time")} + {t("Loading Sequence")} + {t("District Reference")} + {t("Store ID")} + {t("Remark")} + {t("Actions")} @@ -581,7 +579,7 @@ const ShopDetail: React.FC = () => { - No Truck data available + {t("No Truck data available")} @@ -725,7 +723,7 @@ const ShopDetail: React.FC = () => { )} @@ -745,7 +743,7 @@ const ShopDetail: React.FC = () => { size="small" onClick={() => handleSave(index)} disabled={saving} - title="Save changes" + title={t("Save changes")} > @@ -754,7 +752,7 @@ const ShopDetail: React.FC = () => { size="small" onClick={() => handleCancel(index)} disabled={saving} - title="Cancel editing" + title={t("Cancel editing")} > @@ -766,7 +764,7 @@ const ShopDetail: React.FC = () => { size="small" onClick={() => handleEdit(index)} disabled={editingRowIndex !== null} - title="Edit truck lane" + title={t("Edit truck lane")} > @@ -776,7 +774,7 @@ const ShopDetail: React.FC = () => { size="small" onClick={() => handleDelete(truck.id!)} disabled={saving || editingRowIndex !== null} - title="Delete truck lane" + title={t("Delete truck lane")} > @@ -797,13 +795,13 @@ const ShopDetail: React.FC = () => { {/* Add Truck Dialog */} - Add New Truck Lane + {t("Add New Truck Lane")} { { { { - Store ID + {t("Store ID")}
+ + + {t("TruckLance Code")} + {t("Departure Time")} + {t("Store ID")} + {t("Remark")} + {t("Actions")} + + + + {paginatedRows.length === 0 ? ( + + + + {t("No Truck Lane data available")} + + + + ) : ( + 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 ( + + + {String(truck.truckLanceCode || "-")} + + + {formatDepartureTime( + Array.isArray(truck.departureTime) + ? truck.departureTime + : (truck.departureTime ? String(truck.departureTime) : null) + )} + + + {displayStoreId} + + + {String(truck.remark || "-")} + + + + + + ); + }) + )} + +
+ +
+
+
+ + ); +}; + +export default TruckLane; + diff --git a/src/components/Shop/TruckLaneDetail.tsx b/src/components/Shop/TruckLaneDetail.tsx new file mode 100644 index 0000000..b903087 --- /dev/null +++ b/src/components/Shop/TruckLaneDetail.tsx @@ -0,0 +1,474 @@ +"use client"; + +import { + Box, + Card, + CardContent, + Typography, + Button, + CircularProgress, + Alert, + Grid, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + IconButton, + Snackbar, + TextField, +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; +import SaveIcon from "@mui/icons-material/Save"; +import CancelIcon from "@mui/icons-material/Cancel"; +import { useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { findAllUniqueTruckLaneCombinationsClient, findAllShopsByTruckLanceCodeAndRemarkClient, deleteTruckLaneClient, updateLoadingSequenceClient } from "@/app/api/shop/client"; +import type { Truck, ShopAndTruck } 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; +}; + +const TruckLaneDetail: React.FC = () => { + const { t } = useTranslation("common"); + const router = useRouter(); + const searchParams = useSearchParams(); + const truckId = searchParams.get("id"); + + const [truckData, setTruckData] = useState(null); + const [shopsData, setShopsData] = useState([]); + const [editedShopsData, setEditedShopsData] = useState([]); + const [editingRowIndex, setEditingRowIndex] = useState(null); + const [loading, setLoading] = useState(true); + const [shopsLoading, setShopsLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: "success" | "error" }>({ + open: false, + message: "", + severity: "success", + }); + + useEffect(() => { + const fetchTruckLaneDetail = async () => { + if (!truckId) { + setError(t("Truck ID is required")); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + try { + const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; + const truck = data.find((t) => t.id?.toString() === truckId); + + if (truck) { + setTruckData(truck); + // Fetch shops using this truck lane + await fetchShopsByTruckLane(String(truck.truckLanceCode || ""), String(truck.remark || "")); + } else { + setError(t("Truck lane not found")); + } + } catch (err: any) { + console.error("Failed to load truck lane detail:", err); + setError(err?.message ?? String(err) ?? t("Failed to load truck lane detail")); + } finally { + setLoading(false); + } + }; + + fetchTruckLaneDetail(); + }, [truckId]); + + const fetchShopsByTruckLane = async (truckLanceCode: string, remark: string) => { + setShopsLoading(true); + try { + const shops = await findAllShopsByTruckLanceCodeAndRemarkClient(truckLanceCode, remark || ""); + setShopsData(shops || []); + setEditedShopsData(shops || []); + } catch (err: any) { + console.error("Failed to load shops:", err); + setSnackbar({ + open: true, + message: err?.message ?? String(err) ?? t("Failed to load shops"), + severity: "error", + }); + } finally { + setShopsLoading(false); + } + }; + + const handleEdit = (index: number) => { + setEditingRowIndex(index); + const updated = [...shopsData]; + updated[index] = { ...updated[index] }; + setEditedShopsData(updated); + }; + + const handleCancel = (index: number) => { + setEditingRowIndex(null); + setEditedShopsData([...shopsData]); + }; + + const handleSave = async (index: number) => { + const shop = editedShopsData[index]; + if (!shop || !shop.truckId) { + setSnackbar({ + open: true, + message: t("Invalid shop data"), + severity: "error", + }); + return; + } + + setSaving(true); + setError(null); + try { + // Get LoadingSequence from edited data - handle both PascalCase and camelCase + const editedShop = editedShopsData[index]; + const loadingSeq = (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence; + const loadingSequenceValue = (loadingSeq !== null && loadingSeq !== undefined) ? Number(loadingSeq) : 0; + + if (!shop.truckId) { + setSnackbar({ + open: true, + message: "Truck ID is required", + severity: "error", + }); + return; + } + + await updateLoadingSequenceClient({ + id: shop.truckId, + loadingSequence: loadingSequenceValue, + }); + + setSnackbar({ + open: true, + message: t("Loading sequence updated successfully"), + severity: "success", + }); + + // Refresh the shops list + if (truckData) { + await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || "")); + } + setEditingRowIndex(null); + } catch (err: any) { + console.error("Failed to save loading sequence:", err); + setSnackbar({ + open: true, + message: err?.message ?? String(err) ?? t("Failed to save loading sequence"), + severity: "error", + }); + } finally { + setSaving(false); + } + }; + + const handleLoadingSequenceChange = (index: number, value: string) => { + const updated = [...editedShopsData]; + const numValue = parseInt(value, 10); + updated[index] = { + ...updated[index], + LoadingSequence: isNaN(numValue) ? 0 : numValue, + }; + setEditedShopsData(updated); + }; + + const handleDelete = async (truckIdToDelete: number) => { + if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) { + return; + } + + try { + await deleteTruckLaneClient({ id: truckIdToDelete }); + setSnackbar({ + open: true, + message: t("Truck lane deleted successfully"), + severity: "success", + }); + + // Refresh the shops list + if (truckData) { + await fetchShopsByTruckLane(String(truckData.truckLanceCode || ""), String(truckData.remark || "")); + } + } catch (err: any) { + console.error("Failed to delete truck lane:", err); + setSnackbar({ + open: true, + message: err?.message ?? String(err) ?? t("Failed to delete truck lane"), + severity: "error", + }); + } + }; + + const handleBack = () => { + router.push("/settings/shop"); + }; + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + {error} + + + + ); + } + + if (!truckData) { + return ( + + + {t("No truck lane data available")} + + + + ); + } + + 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; + + return ( + + + + + {t("Truck Lane Detail")} + + + + + + + + + + + + {t("TruckLance Code")} + + + {String(truckData.truckLanceCode || "-")} + + + + + + {t("Departure Time")} + + + {formatDepartureTime( + Array.isArray(truckData.departureTime) + ? truckData.departureTime + : (truckData.departureTime ? String(truckData.departureTime) : null) + )} + + + + + + {t("Store ID")} + + + {displayStoreId} + + + + + + {t("Remark")} + + + {String(truckData.remark || "-")} + + + + + + + + + + + {t("Shops Using This Truck Lane")} + + + {shopsLoading ? ( + + + + ) : ( + + + + + {t("Shop Name")} + {t("Shop Code")} + {t("Loading Sequence")} + {t("Remark")} + {t("Actions")} + + + + {shopsData.length === 0 ? ( + + + + {t("No shops found using this truck lane")} + + + + ) : ( + shopsData.map((shop, index) => ( + + + {String(shop.name || "-")} + + + {String(shop.code || "-")} + + + {editingRowIndex === index ? ( + { + const editedShop = editedShopsData[index]; + return (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence ?? 0; + })()} + onChange={(e) => handleLoadingSequenceChange(index, e.target.value)} + disabled={saving} + sx={{ width: 100 }} + /> + ) : ( + (() => { + // Handle both PascalCase and camelCase, and check for 0 as valid value + const loadingSeq = (shop as any).LoadingSequence ?? (shop as any).loadingSequence; + return (loadingSeq !== null && loadingSeq !== undefined) + ? String(loadingSeq) + : "-"; + })() + )} + + + {String(shop.remark || "-")} + + + + {editingRowIndex === index ? ( + <> + handleSave(index)} + disabled={saving} + title={t("Save changes")} + > + + + handleCancel(index)} + disabled={saving} + title={t("Cancel editing")} + > + + + + ) : ( + <> + handleEdit(index)} + title={t("Edit loading sequence")} + > + + + {shop.truckId && ( + handleDelete(shop.truckId!)} + title={t("Delete truck lane")} + > + + + )} + + )} + + + + )) + )} + +
+
+ )} +
+
+ + setSnackbar({ ...snackbar, open: false })} + message={snackbar.message} + /> +
+ ); +}; + +export default TruckLaneDetail; + + diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 82f74f4..7e8a122 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -296,5 +296,75 @@ "Total lines: ": "總數量:", "Balance": "可用數量", "Submitting...": "提交中...", - "Batch Count": "批數" + "Batch Count": "批數", + "Shop": "店鋪", + "Shop Information": "店鋪資訊", + "Shop Name": "店鋪名稱", + "Shop Code": "店鋪編號", + "Truck Lane": "卡車路線", + "Truck Lane Detail": "卡車路線詳情", + "TruckLance Code": "卡車路線編號", + "TruckLance Status": "卡車路線狀態", + "Departure Time": "出發時間", + "Loading Sequence": "裝載順序", + "District Reference": "區域參考", + "Store ID": "店鋪ID", + "Remark": "備註", + "Actions": "操作", + "View Detail": "查看詳情", + "Back": "返回", + "Back to Truck Lane List": "返回卡車路線列表", + "Back to List": "返回列表", + "Add Truck Lane": "新增卡車路線", + "Add New Truck Lane": "新增卡車路線", + "Truck Information": "卡車資訊", + "No Truck data available": "沒有卡車資料", + "No shops found using this truck lane": "沒有找到使用此卡車路線的店鋪", + "Shops Using This Truck Lane": "使用此卡車路線的店鋪", + "Complete": "完成", + "Missing Data": "缺少資料", + "No TruckLance": "無卡車路線", + "Edit shop truck lane": "編輯店鋪卡車路線", + "Delete truck lane": "刪除卡車路線", + "Edit loading sequence": "編輯裝載順序", + "Save changes": "儲存變更", + "Cancel editing": "取消編輯", + "Edit truck lane": "編輯卡車路線", + "Truck ID is required": "需要卡車ID", + "Truck lane not found": "找不到卡車路線", + "No truck lane data available": "沒有卡車路線資料", + "Failed to load truck lanes": "載入卡車路線失敗", + "Failed to load shops": "載入店鋪失敗", + "Loading sequence updated successfully": "裝載順序更新成功", + "Failed to save loading sequence": "儲存裝載順序失敗", + "Truck lane deleted successfully": "卡車路線刪除成功", + "Failed to delete truck lane": "刪除卡車路線失敗", + "Are you sure you want to delete this truck lane?": "您確定要刪除此卡車路線嗎?", + "Invalid shop data": "無效的店鋪資料", + "Contact No": "聯絡電話", + "Contact Email": "聯絡郵箱", + "Contact Name": "聯絡人", + "Addr1": "地址1", + "Addr2": "地址2", + "Addr3": "地址3", + "Shop not found": "找不到店鋪", + "Shop ID is required": "需要店鋪ID", + "Invalid Shop ID": "無效的店鋪ID", + "Failed to load shop detail": "載入店鋪詳情失敗", + "Failed to load shop details": "載入店鋪詳情失敗", + "Failed to save truck data": "儲存卡車資料失敗", + "Failed to delete truck lane": "刪除卡車路線失敗", + "Failed to create truck": "建立卡車失敗", + "Please fill in the following required fields:": "請填寫以下必填欄位:", + "TruckLance Code": "卡車路線編號", + "Enter or select remark": "輸入或選擇備註", + "Not editable for this Store ID": "此店鋪ID不可編輯", + "No Truck Lane data available": "沒有卡車路線資料", + "Please log in to view shop details": "請登入以查看店鋪詳情", + "Invalid truck data": "無效的卡車資料", + "Failed to load truck lane detail": "載入卡車路線詳情失敗", + "Shop Detail": "店鋪詳情", + "Truck Lane Detail": "卡車路線詳情", + "Filter by Status": "按狀態篩選", + "All": "全部" } \ No newline at end of file