From 187c88d1e5f77efb271a7da52373f358d11f5dae Mon Sep 17 00:00:00 2001 From: "Tommy\\2Fi-Staff" Date: Fri, 9 Jan 2026 17:48:12 +0800 Subject: [PATCH] Add "add shop in trucklane"&"add trucklane" --- src/app/api/shop/actions.ts | 81 ++- src/app/api/shop/client.ts | 39 +- src/components/Shop/TruckLane.tsx | 208 +++++++- src/components/Shop/TruckLaneDetail.tsx | 633 +++++++++++++++++++++++- src/i18n/en/common.json | 16 +- src/i18n/zh/common.json | 14 +- 6 files changed, 961 insertions(+), 30 deletions(-) diff --git a/src/app/api/shop/actions.ts b/src/app/api/shop/actions.ts index ab12a8d..a927342 100644 --- a/src/app/api/shop/actions.ts +++ b/src/app/api/shop/actions.ts @@ -46,6 +46,8 @@ export interface Truck{ districtReference: Number; storeId: Number | String; remark?: String | null; + shopName?: String | null; + shopCode?: String | null; } export interface SaveTruckLane { @@ -62,9 +64,13 @@ export interface DeleteTruckLane { id: number; } -export interface UpdateLoadingSequenceRequest { +export interface UpdateTruckShopDetailsRequest { id: number; + shopId?: number | null; + shopName: string | null; + shopCode: string | null; loadingSequence: number; + remark?: string | null; } export interface SaveTruckRequest { @@ -80,6 +86,15 @@ export interface SaveTruckRequest { remark?: string | null; } +export interface CreateTruckWithoutShopRequest { + store_id: string; + truckLanceCode: string; + departureTime: string; + loadingSequence?: number; + districtReference?: number | null; + remark?: string | null; +} + export interface MessageResponse { id: number | null; name: string | null; @@ -137,7 +152,7 @@ export const deleteTruckLaneAction = async (data: DeleteTruckLane) => { }; export const createTruckAction = async (data: SaveTruckRequest) => { - const endpoint = `${BASE_API_URL}/truck/create`; + const endpoint = `${BASE_API_URL}/truck/createTruckInShop`; return serverFetchJson(endpoint, { method: "POST", @@ -175,12 +190,68 @@ export const findAllShopsByTruckLanceCodeAction = cache(async (truckLanceCode: s }); }); -export const updateLoadingSequenceAction = async (data: UpdateLoadingSequenceRequest) => { - const endpoint = `${BASE_API_URL}/truck/updateLoadingSequence`; +export const findAllByTruckLanceCodeAndDeletedFalseAction = cache(async (truckLanceCode: string) => { + const endpoint = `${BASE_API_URL}/truck/findAllByTruckLanceCodeAndDeletedFalse`; + const url = `${endpoint}?truckLanceCode=${encodeURIComponent(truckLanceCode)}`; + + return serverFetchJson(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const updateTruckShopDetailsAction = async (data: UpdateTruckShopDetailsRequest) => { + const endpoint = `${BASE_API_URL}/truck/updateTruckShopDetails`; + + return serverFetchJson(endpoint, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); +}; + +export const createTruckWithoutShopAction = async (data: CreateTruckWithoutShopRequest) => { + const endpoint = `${BASE_API_URL}/truck/createTruckWithoutShop`; return serverFetchJson(endpoint, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); -}; \ No newline at end of file +}; + +export const findAllUniqueShopNamesAndCodesFromTrucksAction = cache(async () => { + const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopNamesAndCodesFromTrucks`; + + return serverFetchJson>(endpoint, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const findAllUniqueRemarksFromTrucksAction = cache(async () => { + const endpoint = `${BASE_API_URL}/truck/findAllUniqueRemarksFromTrucks`; + + return serverFetchJson(endpoint, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const findAllUniqueShopCodesFromTrucksAction = cache(async () => { + const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopCodesFromTrucks`; + + return serverFetchJson(endpoint, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); + +export const findAllUniqueShopNamesFromTrucksAction = cache(async () => { + const endpoint = `${BASE_API_URL}/truck/findAllUniqueShopNamesFromTrucks`; + + return serverFetchJson(endpoint, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); +}); \ No newline at end of file diff --git a/src/app/api/shop/client.ts b/src/app/api/shop/client.ts index 4d046d6..5b9fa87 100644 --- a/src/app/api/shop/client.ts +++ b/src/app/api/shop/client.ts @@ -9,11 +9,18 @@ import { findAllUniqueTruckLaneCombinationsAction, findAllShopsByTruckLanceCodeAndRemarkAction, findAllShopsByTruckLanceCodeAction, - updateLoadingSequenceAction, + createTruckWithoutShopAction, + updateTruckShopDetailsAction, + findAllUniqueShopNamesAndCodesFromTrucksAction, + findAllUniqueRemarksFromTrucksAction, + findAllUniqueShopCodesFromTrucksAction, + findAllUniqueShopNamesFromTrucksAction, + findAllByTruckLanceCodeAndDeletedFalseAction, type SaveTruckLane, type DeleteTruckLane, type SaveTruckRequest, - type UpdateLoadingSequenceRequest, + type UpdateTruckShopDetailsRequest, + type CreateTruckWithoutShopRequest, type MessageResponse } from "./actions"; @@ -49,8 +56,32 @@ export const findAllShopsByTruckLanceCodeClient = async (truckLanceCode: string) return await findAllShopsByTruckLanceCodeAction(truckLanceCode); }; -export const updateLoadingSequenceClient = async (data: UpdateLoadingSequenceRequest): Promise => { - return await updateLoadingSequenceAction(data); +export const findAllByTruckLanceCodeAndDeletedFalseClient = async (truckLanceCode: string) => { + return await findAllByTruckLanceCodeAndDeletedFalseAction(truckLanceCode); +}; + +export const updateTruckShopDetailsClient = async (data: UpdateTruckShopDetailsRequest): Promise => { + return await updateTruckShopDetailsAction(data); +}; + +export const createTruckWithoutShopClient = async (data: CreateTruckWithoutShopRequest): Promise => { + return await createTruckWithoutShopAction(data); +}; + +export const findAllUniqueShopNamesAndCodesFromTrucksClient = async () => { + return await findAllUniqueShopNamesAndCodesFromTrucksAction(); +}; + +export const findAllUniqueRemarksFromTrucksClient = async () => { + return await findAllUniqueRemarksFromTrucksAction(); +}; + +export const findAllUniqueShopCodesFromTrucksClient = async () => { + return await findAllUniqueShopCodesFromTrucksAction(); +}; + +export const findAllUniqueShopNamesFromTrucksClient = async () => { + return await findAllUniqueShopNamesFromTrucksAction(); }; export default fetchAllShopsClient; diff --git a/src/components/Shop/TruckLane.tsx b/src/components/Shop/TruckLane.tsx index dd29e6a..3f3d60b 100644 --- a/src/components/Shop/TruckLane.tsx +++ b/src/components/Shop/TruckLane.tsx @@ -16,11 +16,24 @@ import { Button, CircularProgress, Alert, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + Snackbar, } from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; +import SaveIcon from "@mui/icons-material/Save"; import { useState, useEffect, useMemo } from "react"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; -import { findAllUniqueTruckLaneCombinationsClient } from "@/app/api/shop/client"; +import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client"; import type { Truck } from "@/app/api/shop/actions"; import SearchBox, { Criterion } from "../SearchBox"; @@ -50,6 +63,20 @@ const formatDepartureTime = (time: string | number[] | null | undefined): string 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); +}; + type SearchQuery = { truckLanceCode: string; departureTime: string; @@ -67,6 +94,15 @@ const TruckLane: React.FC = () => { const [filters, setFilters] = useState>({}); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); + const [addDialogOpen, setAddDialogOpen] = useState(false); + const [newTruck, setNewTruck] = useState({ + truckLanceCode: "", + departureTime: "", + storeId: "2F", + }); + const [saving, setSaving] = useState(false); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); useEffect(() => { const fetchTruckLanes = async () => { @@ -158,6 +194,78 @@ const TruckLane: React.FC = () => { } }; + const handleOpenAddDialog = () => { + setNewTruck({ + truckLanceCode: "", + departureTime: "", + storeId: "2F", + }); + setAddDialogOpen(true); + setError(null); + }; + + const handleCloseAddDialog = () => { + setAddDialogOpen(false); + setNewTruck({ + truckLanceCode: "", + departureTime: "", + storeId: "2F", + }); + }; + + const handleCreateTruck = async () => { + // Validate all required fields + const missingFields: string[] = []; + + if (!newTruck.truckLanceCode.trim()) { + missingFields.push(t("TruckLance Code")); + } + + if (!newTruck.departureTime) { + missingFields.push(t("Departure Time")); + } + + if (missingFields.length > 0) { + const message = `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`; + setSnackbarMessage(message); + setSnackbarOpen(true); + return; + } + + setSaving(true); + setError(null); + try { + const departureTime = parseDepartureTimeForBackend(newTruck.departureTime); + + await createTruckWithoutShopClient({ + store_id: newTruck.storeId, + truckLanceCode: newTruck.truckLanceCode.trim(), + departureTime: departureTime, + loadingSequence: 0, + districtReference: null, + remark: null, + }); + + // Refresh truck data after create + const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[]; + const uniqueCodes = new Map(); + (data || []).forEach((truck) => { + const code = String(truck.truckLanceCode || "").trim(); + if (code && !uniqueCodes.has(code)) { + uniqueCodes.set(code, truck); + } + }); + setTruckData(Array.from(uniqueCodes.values())); + + handleCloseAddDialog(); + } catch (err: any) { + console.error("Failed to create truck:", err); + setError(err?.message ?? String(err) ?? t("Failed to create truck")); + } finally { + setSaving(false); + } + }; + if (loading) { return ( @@ -198,7 +306,17 @@ const TruckLane: React.FC = () => { - {t("Truck Lane")} + + {t("Truck Lane")} + + @@ -269,6 +387,92 @@ const TruckLane: React.FC = () => { + + {/* Add Truck Dialog */} + + {t("Add New Truck Lane")} + + + + + setNewTruck({ ...newTruck, truckLanceCode: e.target.value })} + disabled={saving} + /> + + + setNewTruck({ ...newTruck, departureTime: e.target.value })} + disabled={saving} + InputLabelProps={{ + shrink: true, + }} + inputProps={{ + step: 300, // 5 minutes + }} + /> + + + + {t("Store ID")} + + + + + + + + + + + + + {/* Snackbar for notifications */} + setSnackbarOpen(false)} + anchorOrigin={{ vertical: 'top', horizontal: 'center' }} + > + setSnackbarOpen(false)} + severity="warning" + sx={{ width: '100%' }} + > + {snackbarMessage} + + ); }; diff --git a/src/components/Shop/TruckLaneDetail.tsx b/src/components/Shop/TruckLaneDetail.tsx index 7b37704..85659f7 100644 --- a/src/components/Shop/TruckLaneDetail.tsx +++ b/src/components/Shop/TruckLaneDetail.tsx @@ -19,16 +19,22 @@ import { IconButton, Snackbar, TextField, + Autocomplete, + Dialog, + DialogTitle, + DialogContent, + DialogActions, } 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 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, updateLoadingSequenceClient } from "@/app/api/shop/client"; -import type { Truck, ShopAndTruck } from "@/app/api/shop/actions"; +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 => { @@ -72,12 +78,78 @@ const TruckLaneDetail: React.FC = () => { const [shopsLoading, setShopsLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); + const [allShops, setAllShops] = useState([]); + const [uniqueRemarks, setUniqueRemarks] = useState([]); + const [uniqueShopCodes, setUniqueShopCodes] = useState([]); + const [uniqueShopNames, setUniqueShopNames] = useState([]); + const [addShopDialogOpen, setAddShopDialogOpen] = useState(false); + const [newShop, setNewShop] = useState({ + shopName: "", + shopCode: "", + loadingSequence: 0, + remark: "", + }); const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: "success" | "error" }>({ open: false, message: "", severity: "success", }); + useEffect(() => { + // Fetch unique shop names and codes from truck table + const fetchShopNamesFromTrucks = async () => { + try { + const shopData = await findAllUniqueShopNamesAndCodesFromTrucksClient() as Array<{ name: string; code: 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 + 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); + } + }; + + fetchShopNamesFromTrucks(); + fetchRemarksFromTrucks(); + fetchShopCodesFromTrucks(); + fetchShopNamesFromTrucksOnly(); + }, []); + useEffect(() => { // Wait a bit to ensure searchParams are fully available if (!truckLanceCodeParam) { @@ -183,28 +255,55 @@ const TruckLaneDetail: React.FC = () => { setSaving(true); setError(null); try { - // Get LoadingSequence from edited data - handle both PascalCase and camelCase + // Get values from edited data const editedShop = editedShopsData[index]; const loadingSeq = (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence; const loadingSequenceValue = (loadingSeq !== null && loadingSeq !== undefined) ? Number(loadingSeq) : 0; + + // Get shopName and shopCode from edited data + const shopNameValue = editedShop.name ? String(editedShop.name).trim() : null; + const shopCodeValue = editedShop.code ? String(editedShop.code).trim() : null; + const remarkValue = editedShop.remark ? String(editedShop.remark).trim() : null; + + // Get shopId from editedShop.id (which was set when shopName or shopCode was selected) + // If not found, try to find it from shop table by shopCode + let shopIdValue: number | null = null; + if (editedShop.id && editedShop.id > 0) { + shopIdValue = editedShop.id; + } else if (shopCodeValue) { + // If shopId is 0 (from truck table), try to find it from shop table + try { + const allShopsFromShopTable = await fetchAllShopsClient() as ShopAndTruck[]; + const foundShop = allShopsFromShopTable.find(s => String(s.code).trim() === shopCodeValue); + if (foundShop) { + shopIdValue = foundShop.id; + } + } catch (err) { + console.error("Failed to lookup shopId:", err); + } + } if (!shop.truckId) { setSnackbar({ open: true, - message: "Truck ID is required", + message: t("Truck ID is required"), severity: "error", }); return; } - await updateLoadingSequenceClient({ + await updateTruckShopDetailsClient({ id: shop.truckId, + shopId: shopIdValue, + shopName: shopNameValue, + shopCode: shopCodeValue, loadingSequence: loadingSequenceValue, + remark: remarkValue || null, }); setSnackbar({ open: true, - message: t("Loading sequence updated successfully"), + message: t("Truck shop details updated successfully"), severity: "success", }); @@ -214,10 +313,10 @@ const TruckLaneDetail: React.FC = () => { } setEditingRowIndex(null); } catch (err: any) { - console.error("Failed to save loading sequence:", err); + console.error("Failed to save truck shop details:", err); setSnackbar({ open: true, - message: err?.message ?? String(err) ?? t("Failed to save loading sequence"), + message: err?.message ?? String(err) ?? t("Failed to save truck shop details"), severity: "error", }); } finally { @@ -235,6 +334,53 @@ const TruckLaneDetail: React.FC = () => { setEditedShopsData(updated); }; + const handleShopNameChange = (index: number, shop: Shop | null) => { + const updated = [...editedShopsData]; + if (shop) { + updated[index] = { + ...updated[index], + name: shop.name, + code: shop.code, + id: shop.id, // Store shopId for later use + }; + } else { + updated[index] = { + ...updated[index], + name: "", + code: "", + }; + } + setEditedShopsData(updated); + }; + + const handleShopCodeChange = (index: number, shop: Shop | null) => { + const updated = [...editedShopsData]; + if (shop) { + updated[index] = { + ...updated[index], + name: shop.name, + code: shop.code, + id: shop.id, // Store shopId for later use + }; + } else { + updated[index] = { + ...updated[index], + name: "", + code: "", + }; + } + setEditedShopsData(updated); + }; + + const handleRemarkChange = (index: number, value: string) => { + const updated = [...editedShopsData]; + updated[index] = { + ...updated[index], + remark: value, + }; + setEditedShopsData(updated); + }; + const handleDelete = async (truckIdToDelete: number) => { if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) { return; @@ -266,6 +412,208 @@ const TruckLaneDetail: React.FC = () => { router.push("/settings/shop"); }; + const handleOpenAddShopDialog = () => { + setNewShop({ + shopName: "", + shopCode: "", + loadingSequence: 0, + remark: "", + }); + setAddShopDialogOpen(true); + setError(null); + }; + + const handleCloseAddShopDialog = () => { + setAddShopDialogOpen(false); + setNewShop({ + shopName: "", + shopCode: "", + loadingSequence: 0, + remark: "", + }); + }; + + const handleNewShopNameChange = (newValue: string | null) => { + if (newValue && typeof newValue === 'string') { + // When a name is selected, try to find matching shop code + const matchingShop = allShops.find(s => String(s.name) === newValue); + if (matchingShop) { + setNewShop({ + ...newShop, + shopName: newValue, + shopCode: String(matchingShop.code || ""), + }); + } else { + setNewShop({ + ...newShop, + shopName: newValue, + }); + } + } else if (newValue === null) { + setNewShop({ + ...newShop, + shopName: "", + }); + } + }; + + const handleNewShopCodeChange = (newValue: string | null) => { + if (newValue && typeof newValue === 'string') { + // When a code is selected, try to find matching shop name + const matchingShop = allShops.find(s => String(s.code) === newValue); + if (matchingShop) { + setNewShop({ + ...newShop, + shopCode: newValue, + shopName: String(matchingShop.name || ""), + }); + } else { + setNewShop({ + ...newShop, + shopCode: newValue, + }); + } + } else if (newValue === null) { + setNewShop({ + ...newShop, + shopCode: "", + }); + } + }; + + const handleCreateShop = async () => { + // Validate required fields + const missingFields: string[] = []; + + if (!newShop.shopName.trim()) { + missingFields.push(t("Shop Name")); + } + + if (!newShop.shopCode.trim()) { + missingFields.push(t("Shop Code")); + } + + if (missingFields.length > 0) { + setSnackbar({ + open: true, + message: `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`, + severity: "error", + }); + return; + } + + if (!truckData || !truckLanceCode) { + setSnackbar({ + open: true, + message: t("Truck lane information is required"), + severity: "error", + }); + return; + } + + setSaving(true); + 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; + + // Get departureTime from truckData + let departureTimeStr = ""; + if (truckData.departureTime) { + if (Array.isArray(truckData.departureTime)) { + const [hours, minutes] = truckData.departureTime; + departureTimeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; + } else { + departureTimeStr = String(truckData.departureTime); + } + } + + // Look up shopId from shop table by shopCode + let shopIdValue: number | null = null; + try { + const allShopsFromShopTable = await fetchAllShopsClient() as ShopAndTruck[]; + const foundShop = allShopsFromShopTable.find(s => String(s.code).trim() === newShop.shopCode.trim()); + if (foundShop) { + shopIdValue = foundShop.id; + } + } catch (err) { + console.error("Failed to lookup shopId:", err); + } + + // Get remark - only if storeId is "4F" + const remarkValue = displayStoreId === "4F" ? (newShop.remark?.trim() || null) : null; + + // Check if there's an "Unassign" row for this truck lane that should be replaced + let unassignTruck: Truck | null = null; + try { + const allTrucks = await findAllByTruckLanceCodeAndDeletedFalseClient(String(truckData.truckLanceCode || "")) as Truck[]; + unassignTruck = allTrucks.find(t => + String(t.shopName || "").trim() === "Unassign" && + String(t.shopCode || "").trim() === "Unassign" + ) || null; + } catch (err) { + console.error("Failed to check for Unassign truck:", err); + } + + if (unassignTruck && unassignTruck.id) { + // Update the existing "Unassign" row instead of creating a new one + await updateTruckShopDetailsClient({ + id: unassignTruck.id, + shopId: shopIdValue || null, + shopName: newShop.shopName.trim(), + shopCode: newShop.shopCode.trim(), + loadingSequence: newShop.loadingSequence, + remark: remarkValue, + }); + + setSnackbar({ + open: true, + message: t("Shop added to truck lane successfully"), + severity: "success", + }); + } else { + // No "Unassign" row found, create a new one + await createTruckClient({ + store_id: displayStoreId, + truckLanceCode: String(truckData.truckLanceCode || ""), + departureTime: departureTimeStr, + shopId: shopIdValue || 0, + shopName: newShop.shopName.trim(), + shopCode: newShop.shopCode.trim(), + loadingSequence: newShop.loadingSequence, + remark: remarkValue, + districtReference: null, + }); + + setSnackbar({ + open: true, + message: t("Shop added to truck lane successfully"), + severity: "success", + }); + } + + // Refresh the shops list + if (truckLanceCode) { + await fetchShopsByTruckLane(truckLanceCode); + } + + handleCloseAddShopDialog(); + } catch (err: any) { + console.error("Failed to create shop in truck lane:", err); + setSnackbar({ + open: true, + message: err?.message ?? String(err) ?? t("Failed to create shop in truck lane"), + severity: "error", + }); + } finally { + setSaving(false); + } + }; + if (loading) { return ( @@ -361,9 +709,19 @@ const TruckLaneDetail: React.FC = () => { - - {t("Shops Using This Truck Lane")} - + + + {t("Shops Using This Truck Lane")} + + + {shopsLoading ? ( @@ -394,13 +752,143 @@ const TruckLaneDetail: React.FC = () => { shopsData.map((shop, index) => ( - {String(shop.name || "-")} + {editingRowIndex === index ? ( + { + if (newValue && typeof newValue === 'string') { + // When a name is selected, try to find matching shop code + const matchingShop = allShops.find(s => String(s.name) === newValue); + if (matchingShop) { + handleShopNameChange(index, matchingShop); + } else { + // If no matching shop found, just update the name + const updated = [...editedShopsData]; + updated[index] = { + ...updated[index], + name: newValue, + }; + setEditedShopsData(updated); + } + } else if (newValue === null) { + handleShopNameChange(index, null); + } + }} + onInputChange={(event, newInputValue, reason) => { + if (reason === 'input') { + // Allow free text input + const updated = [...editedShopsData]; + updated[index] = { + ...updated[index], + name: newInputValue, + }; + setEditedShopsData(updated); + } + }} + renderInput={(params) => ( + + )} + /> + ) : ( + String(shop.name || "-") + )} - {String(shop.code || "-")} + {editingRowIndex === index ? ( + { + if (newValue && typeof newValue === 'string') { + // When a code is selected, try to find matching shop name + const matchingShop = allShops.find(s => String(s.code) === newValue); + if (matchingShop) { + handleShopCodeChange(index, matchingShop); + } else { + // If no matching shop found, just update the code + const updated = [...editedShopsData]; + updated[index] = { + ...updated[index], + code: newValue, + }; + setEditedShopsData(updated); + } + } else if (newValue === null) { + handleShopCodeChange(index, null); + } + }} + onInputChange={(event, newInputValue, reason) => { + if (reason === 'input') { + // Allow free text input + const updated = [...editedShopsData]; + updated[index] = { + ...updated[index], + code: newInputValue, + }; + setEditedShopsData(updated); + } + }} + renderInput={(params) => ( + + )} + /> + ) : ( + String(shop.code || "-") + )} - {String(shop.remark || "-")} + {editingRowIndex === index ? ( + (() => { + const storeId = truckData.storeId; + const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : ""; + const isEditable = storeIdStr === "4F" || storeIdStr === "4"; + return ( + { + if (isEditable) { + const remarkValue = typeof newValue === 'string' ? newValue : (newValue || ""); + handleRemarkChange(index, remarkValue); + } + }} + onInputChange={(event, newInputValue, reason) => { + if (isEditable && reason === 'input') { + handleRemarkChange(index, newInputValue); + } + }} + disabled={saving || !isEditable} + renderInput={(params) => ( + + )} + /> + ); + })() + ) : ( + String(shop.remark || "-") + )} {editingRowIndex === index ? ( @@ -454,7 +942,7 @@ const TruckLaneDetail: React.FC = () => { size="small" color="primary" onClick={() => handleEdit(index)} - title={t("Edit loading sequence")} + title={t("Edit shop details")} > @@ -482,6 +970,121 @@ const TruckLaneDetail: React.FC = () => { + {/* Add Shop Dialog */} + + {t("Add Shop to Truck Lane")} + + + + + { + handleNewShopNameChange(newValue); + }} + onInputChange={(event, newInputValue, reason) => { + if (reason === 'input') { + setNewShop({ ...newShop, shopName: newInputValue }); + } + }} + renderInput={(params) => ( + + )} + /> + + + { + handleNewShopCodeChange(newValue); + }} + onInputChange={(event, newInputValue, reason) => { + if (reason === 'input') { + setNewShop({ ...newShop, shopCode: newInputValue }); + } + }} + renderInput={(params) => ( + + )} + /> + + + setNewShop({ ...newShop, loadingSequence: parseInt(e.target.value) || 0 })} + disabled={saving} + /> + + {(() => { + const storeId = truckData?.storeId; + const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : ""; + const isEditable = storeIdStr === "4F" || storeIdStr === "4"; + return isEditable ? ( + + { + setNewShop({ ...newShop, remark: typeof newValue === 'string' ? newValue : (newValue || "") }); + }} + onInputChange={(event, newInputValue, reason) => { + if (reason === 'input') { + setNewShop({ ...newShop, remark: newInputValue }); + } + }} + renderInput={(params) => ( + + )} + /> + + ) : null; + })()} + + + + + + + + +