|
|
@@ -19,16 +19,22 @@ import { |
|
|
IconButton, |
|
|
IconButton, |
|
|
Snackbar, |
|
|
Snackbar, |
|
|
TextField, |
|
|
TextField, |
|
|
|
|
|
Autocomplete, |
|
|
|
|
|
Dialog, |
|
|
|
|
|
DialogTitle, |
|
|
|
|
|
DialogContent, |
|
|
|
|
|
DialogActions, |
|
|
} from "@mui/material"; |
|
|
} from "@mui/material"; |
|
|
import DeleteIcon from "@mui/icons-material/Delete"; |
|
|
import DeleteIcon from "@mui/icons-material/Delete"; |
|
|
import EditIcon from "@mui/icons-material/Edit"; |
|
|
import EditIcon from "@mui/icons-material/Edit"; |
|
|
import SaveIcon from "@mui/icons-material/Save"; |
|
|
import SaveIcon from "@mui/icons-material/Save"; |
|
|
import CancelIcon from "@mui/icons-material/Cancel"; |
|
|
import CancelIcon from "@mui/icons-material/Cancel"; |
|
|
|
|
|
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, 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 |
|
|
// Utility function to format departureTime to HH:mm format |
|
|
const formatDepartureTime = (time: string | number[] | null | undefined): string => { |
|
|
const formatDepartureTime = (time: string | number[] | null | undefined): string => { |
|
|
@@ -72,12 +78,78 @@ const TruckLaneDetail: React.FC = () => { |
|
|
const [shopsLoading, setShopsLoading] = useState<boolean>(false); |
|
|
const [shopsLoading, setShopsLoading] = useState<boolean>(false); |
|
|
const [saving, setSaving] = useState<boolean>(false); |
|
|
const [saving, setSaving] = useState<boolean>(false); |
|
|
const [error, setError] = useState<string | null>(null); |
|
|
const [error, setError] = useState<string | null>(null); |
|
|
|
|
|
const [allShops, setAllShops] = useState<Shop[]>([]); |
|
|
|
|
|
const [uniqueRemarks, setUniqueRemarks] = useState<string[]>([]); |
|
|
|
|
|
const [uniqueShopCodes, setUniqueShopCodes] = useState<string[]>([]); |
|
|
|
|
|
const [uniqueShopNames, setUniqueShopNames] = useState<string[]>([]); |
|
|
|
|
|
const [addShopDialogOpen, setAddShopDialogOpen] = useState<boolean>(false); |
|
|
|
|
|
const [newShop, setNewShop] = useState({ |
|
|
|
|
|
shopName: "", |
|
|
|
|
|
shopCode: "", |
|
|
|
|
|
loadingSequence: 0, |
|
|
|
|
|
remark: "", |
|
|
|
|
|
}); |
|
|
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: "success" | "error" }>({ |
|
|
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: "success" | "error" }>({ |
|
|
open: false, |
|
|
open: false, |
|
|
message: "", |
|
|
message: "", |
|
|
severity: "success", |
|
|
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(() => { |
|
|
useEffect(() => { |
|
|
// Wait a bit to ensure searchParams are fully available |
|
|
// Wait a bit to ensure searchParams are fully available |
|
|
if (!truckLanceCodeParam) { |
|
|
if (!truckLanceCodeParam) { |
|
|
@@ -183,28 +255,55 @@ const TruckLaneDetail: React.FC = () => { |
|
|
setSaving(true); |
|
|
setSaving(true); |
|
|
setError(null); |
|
|
setError(null); |
|
|
try { |
|
|
try { |
|
|
// Get LoadingSequence from edited data - handle both PascalCase and camelCase |
|
|
|
|
|
|
|
|
// Get values from edited data |
|
|
const editedShop = editedShopsData[index]; |
|
|
const editedShop = editedShopsData[index]; |
|
|
const loadingSeq = (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence; |
|
|
const loadingSeq = (editedShop as any)?.LoadingSequence ?? (editedShop as any)?.loadingSequence; |
|
|
const loadingSequenceValue = (loadingSeq !== null && loadingSeq !== undefined) ? Number(loadingSeq) : 0; |
|
|
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) { |
|
|
if (!shop.truckId) { |
|
|
setSnackbar({ |
|
|
setSnackbar({ |
|
|
open: true, |
|
|
open: true, |
|
|
message: "Truck ID is required", |
|
|
|
|
|
|
|
|
message: t("Truck ID is required"), |
|
|
severity: "error", |
|
|
severity: "error", |
|
|
}); |
|
|
}); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
await updateLoadingSequenceClient({ |
|
|
|
|
|
|
|
|
await updateTruckShopDetailsClient({ |
|
|
id: shop.truckId, |
|
|
id: shop.truckId, |
|
|
|
|
|
shopId: shopIdValue, |
|
|
|
|
|
shopName: shopNameValue, |
|
|
|
|
|
shopCode: shopCodeValue, |
|
|
loadingSequence: loadingSequenceValue, |
|
|
loadingSequence: loadingSequenceValue, |
|
|
|
|
|
remark: remarkValue || null, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
setSnackbar({ |
|
|
setSnackbar({ |
|
|
open: true, |
|
|
open: true, |
|
|
message: t("Loading sequence updated successfully"), |
|
|
|
|
|
|
|
|
message: t("Truck shop details updated successfully"), |
|
|
severity: "success", |
|
|
severity: "success", |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
@@ -214,10 +313,10 @@ const TruckLaneDetail: React.FC = () => { |
|
|
} |
|
|
} |
|
|
setEditingRowIndex(null); |
|
|
setEditingRowIndex(null); |
|
|
} catch (err: any) { |
|
|
} catch (err: any) { |
|
|
console.error("Failed to save loading sequence:", err); |
|
|
|
|
|
|
|
|
console.error("Failed to save truck shop details:", err); |
|
|
setSnackbar({ |
|
|
setSnackbar({ |
|
|
open: true, |
|
|
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", |
|
|
severity: "error", |
|
|
}); |
|
|
}); |
|
|
} finally { |
|
|
} finally { |
|
|
@@ -235,6 +334,53 @@ const TruckLaneDetail: React.FC = () => { |
|
|
setEditedShopsData(updated); |
|
|
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) => { |
|
|
const handleDelete = async (truckIdToDelete: number) => { |
|
|
if (!window.confirm(t("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; |
|
|
return; |
|
|
@@ -266,6 +412,208 @@ const TruckLaneDetail: React.FC = () => { |
|
|
router.push("/settings/shop"); |
|
|
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) { |
|
|
if (loading) { |
|
|
return ( |
|
|
return ( |
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> |
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> |
|
|
@@ -361,9 +709,19 @@ const TruckLaneDetail: React.FC = () => { |
|
|
|
|
|
|
|
|
<Card sx={{ mt: 2 }}> |
|
|
<Card sx={{ mt: 2 }}> |
|
|
<CardContent> |
|
|
<CardContent> |
|
|
<Typography variant="h6" sx={{ mb: 2 }}> |
|
|
|
|
|
{t("Shops Using This Truck Lane")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> |
|
|
|
|
|
<Typography variant="h6"> |
|
|
|
|
|
{t("Shops Using This Truck Lane")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
startIcon={<AddIcon />} |
|
|
|
|
|
onClick={handleOpenAddShopDialog} |
|
|
|
|
|
disabled={saving || editingRowIndex !== null} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Add Shop")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
|
{shopsLoading ? ( |
|
|
{shopsLoading ? ( |
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> |
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> |
|
|
@@ -394,13 +752,143 @@ const TruckLaneDetail: React.FC = () => { |
|
|
shopsData.map((shop, index) => ( |
|
|
shopsData.map((shop, index) => ( |
|
|
<TableRow key={shop.id ?? `shop-${index}`}> |
|
|
<TableRow key={shop.id ?? `shop-${index}`}> |
|
|
<TableCell> |
|
|
<TableCell> |
|
|
{String(shop.name || "-")} |
|
|
|
|
|
|
|
|
{editingRowIndex === index ? ( |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
freeSolo |
|
|
|
|
|
size="small" |
|
|
|
|
|
options={uniqueShopNames} |
|
|
|
|
|
value={String(editedShopsData[index]?.name || "")} |
|
|
|
|
|
onChange={(event, newValue) => { |
|
|
|
|
|
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) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
placeholder={t("Search or select shop name")} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
/> |
|
|
|
|
|
) : ( |
|
|
|
|
|
String(shop.name || "-") |
|
|
|
|
|
)} |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell> |
|
|
<TableCell> |
|
|
{String(shop.code || "-")} |
|
|
|
|
|
|
|
|
{editingRowIndex === index ? ( |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
freeSolo |
|
|
|
|
|
size="small" |
|
|
|
|
|
options={uniqueShopCodes} |
|
|
|
|
|
value={String(editedShopsData[index]?.code || "")} |
|
|
|
|
|
onChange={(event, newValue) => { |
|
|
|
|
|
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) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
placeholder={t("Search or select shop code")} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
/> |
|
|
|
|
|
) : ( |
|
|
|
|
|
String(shop.code || "-") |
|
|
|
|
|
)} |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell> |
|
|
<TableCell> |
|
|
{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 ( |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
freeSolo |
|
|
|
|
|
size="small" |
|
|
|
|
|
options={uniqueRemarks} |
|
|
|
|
|
value={String(editedShopsData[index]?.remark || "")} |
|
|
|
|
|
onChange={(event, newValue) => { |
|
|
|
|
|
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) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
placeholder={isEditable ? t("Search or select remark") : t("Not editable for this Store ID")} |
|
|
|
|
|
disabled={saving || !isEditable} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
/> |
|
|
|
|
|
); |
|
|
|
|
|
})() |
|
|
|
|
|
) : ( |
|
|
|
|
|
String(shop.remark || "-") |
|
|
|
|
|
)} |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell> |
|
|
<TableCell> |
|
|
{editingRowIndex === index ? ( |
|
|
{editingRowIndex === index ? ( |
|
|
@@ -454,7 +942,7 @@ const TruckLaneDetail: React.FC = () => { |
|
|
size="small" |
|
|
size="small" |
|
|
color="primary" |
|
|
color="primary" |
|
|
onClick={() => handleEdit(index)} |
|
|
onClick={() => handleEdit(index)} |
|
|
title={t("Edit loading sequence")} |
|
|
|
|
|
|
|
|
title={t("Edit shop details")} |
|
|
> |
|
|
> |
|
|
<EditIcon /> |
|
|
<EditIcon /> |
|
|
</IconButton> |
|
|
</IconButton> |
|
|
@@ -482,6 +970,121 @@ const TruckLaneDetail: React.FC = () => { |
|
|
</CardContent> |
|
|
</CardContent> |
|
|
</Card> |
|
|
</Card> |
|
|
|
|
|
|
|
|
|
|
|
{/* Add Shop Dialog */} |
|
|
|
|
|
<Dialog open={addShopDialogOpen} onClose={handleCloseAddShopDialog} maxWidth="sm" fullWidth> |
|
|
|
|
|
<DialogTitle>{t("Add Shop to Truck Lane")}</DialogTitle> |
|
|
|
|
|
<DialogContent> |
|
|
|
|
|
<Box sx={{ pt: 2 }}> |
|
|
|
|
|
<Grid container spacing={2}> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
freeSolo |
|
|
|
|
|
options={uniqueShopNames} |
|
|
|
|
|
value={newShop.shopName} |
|
|
|
|
|
onChange={(event, newValue) => { |
|
|
|
|
|
handleNewShopNameChange(newValue); |
|
|
|
|
|
}} |
|
|
|
|
|
onInputChange={(event, newInputValue, reason) => { |
|
|
|
|
|
if (reason === 'input') { |
|
|
|
|
|
setNewShop({ ...newShop, shopName: newInputValue }); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
renderInput={(params) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
label={t("Shop Name")} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
required |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
placeholder={t("Search or select shop name")} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
freeSolo |
|
|
|
|
|
options={uniqueShopCodes} |
|
|
|
|
|
value={newShop.shopCode} |
|
|
|
|
|
onChange={(event, newValue) => { |
|
|
|
|
|
handleNewShopCodeChange(newValue); |
|
|
|
|
|
}} |
|
|
|
|
|
onInputChange={(event, newInputValue, reason) => { |
|
|
|
|
|
if (reason === 'input') { |
|
|
|
|
|
setNewShop({ ...newShop, shopCode: newInputValue }); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
renderInput={(params) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
label={t("Shop Code")} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
required |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
placeholder={t("Search or select shop code")} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<TextField |
|
|
|
|
|
label={t("Loading Sequence")} |
|
|
|
|
|
type="number" |
|
|
|
|
|
fullWidth |
|
|
|
|
|
value={newShop.loadingSequence} |
|
|
|
|
|
onChange={(e) => setNewShop({ ...newShop, loadingSequence: parseInt(e.target.value) || 0 })} |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
{(() => { |
|
|
|
|
|
const storeId = truckData?.storeId; |
|
|
|
|
|
const storeIdStr = storeId ? (typeof storeId === 'string' ? storeId : String(storeId)) : ""; |
|
|
|
|
|
const isEditable = storeIdStr === "4F" || storeIdStr === "4"; |
|
|
|
|
|
return isEditable ? ( |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
freeSolo |
|
|
|
|
|
options={uniqueRemarks} |
|
|
|
|
|
value={newShop.remark || ""} |
|
|
|
|
|
onChange={(event, newValue) => { |
|
|
|
|
|
setNewShop({ ...newShop, remark: typeof newValue === 'string' ? newValue : (newValue || "") }); |
|
|
|
|
|
}} |
|
|
|
|
|
onInputChange={(event, newInputValue, reason) => { |
|
|
|
|
|
if (reason === 'input') { |
|
|
|
|
|
setNewShop({ ...newShop, remark: newInputValue }); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
renderInput={(params) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
label={t("Remark")} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
placeholder={t("Search or select remark")} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
) : null; |
|
|
|
|
|
})()} |
|
|
|
|
|
</Grid> |
|
|
|
|
|
</Box> |
|
|
|
|
|
</DialogContent> |
|
|
|
|
|
<DialogActions> |
|
|
|
|
|
<Button onClick={handleCloseAddShopDialog} disabled={saving}> |
|
|
|
|
|
{t("Cancel")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
onClick={handleCreateShop} |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
startIcon={<SaveIcon />} |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
> |
|
|
|
|
|
{saving ? t("Submitting...") : t("Save")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</DialogActions> |
|
|
|
|
|
</Dialog> |
|
|
|
|
|
|
|
|
<Snackbar |
|
|
<Snackbar |
|
|
open={snackbar.open} |
|
|
open={snackbar.open} |
|
|
autoHideDuration={6000} |
|
|
autoHideDuration={6000} |
|
|
|