|
|
@@ -24,28 +24,24 @@ import { |
|
|
DialogActions, |
|
|
DialogActions, |
|
|
Grid, |
|
|
Grid, |
|
|
Snackbar, |
|
|
Snackbar, |
|
|
Select, |
|
|
|
|
|
MenuItem, |
|
|
|
|
|
FormControl, |
|
|
|
|
|
InputLabel, |
|
|
|
|
|
Autocomplete, |
|
|
Autocomplete, |
|
|
} 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 SaveIcon from "@mui/icons-material/Save"; |
|
|
import SaveIcon from "@mui/icons-material/Save"; |
|
|
import CancelIcon from "@mui/icons-material/Cancel"; |
|
|
|
|
|
import AddIcon from "@mui/icons-material/Add"; |
|
|
import AddIcon from "@mui/icons-material/Add"; |
|
|
|
|
|
import CheckIcon from "@mui/icons-material/Check"; |
|
|
|
|
|
import CloseIcon from "@mui/icons-material/Close"; |
|
|
import { useRouter, useSearchParams } from "next/navigation"; |
|
|
import { useRouter, useSearchParams } from "next/navigation"; |
|
|
import { useState, useEffect } from "react"; |
|
|
import { useState, useEffect } from "react"; |
|
|
import { useSession } from "next-auth/react"; |
|
|
import { useSession } from "next-auth/react"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions"; |
|
|
|
|
|
|
|
|
import type { ShopAndTruck, Truck } from "@/app/api/shop/actions"; |
|
|
import { |
|
|
import { |
|
|
fetchAllShopsClient, |
|
|
fetchAllShopsClient, |
|
|
findTruckLaneByShopIdClient, |
|
|
findTruckLaneByShopIdClient, |
|
|
updateTruckLaneClient, |
|
|
|
|
|
deleteTruckLaneClient, |
|
|
deleteTruckLaneClient, |
|
|
createTruckClient |
|
|
|
|
|
|
|
|
createTruckClient, |
|
|
|
|
|
findAllUniqueTruckLaneCombinationsClient, |
|
|
} from "@/app/api/shop/client"; |
|
|
} from "@/app/api/shop/client"; |
|
|
import type { SessionWithTokens } from "@/config/authConfig"; |
|
|
import type { SessionWithTokens } from "@/config/authConfig"; |
|
|
import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; |
|
|
import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; |
|
|
@@ -77,6 +73,32 @@ const parseDepartureTimeForBackend = (time: string): string => { |
|
|
return formatDepartureTime(timeStr); |
|
|
return formatDepartureTime(timeStr); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** Label for truck lane picker: code + optional remark (unique combo from API). */ |
|
|
|
|
|
const getTruckLaneOptionLabel = (lane: Truck): string => { |
|
|
|
|
|
const code = String(lane.truckLanceCode ?? "").trim(); |
|
|
|
|
|
const remark = |
|
|
|
|
|
lane.remark != null && String(lane.remark).trim() !== "" ? String(lane.remark).trim() : null; |
|
|
|
|
|
return remark ? `${code} — ${remark}` : code || "-"; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const isSameTruckLaneOption = (a: Truck | null, b: Truck | null): boolean => { |
|
|
|
|
|
if (a === b) return true; |
|
|
|
|
|
if (!a || !b) return false; |
|
|
|
|
|
const sameCode = String(a.truckLanceCode ?? "") === String(b.truckLanceCode ?? ""); |
|
|
|
|
|
const ra = a.remark != null ? String(a.remark) : ""; |
|
|
|
|
|
const rb = b.remark != null ? String(b.remark) : ""; |
|
|
|
|
|
return sameCode && ra === rb; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** Build HH:mm string from API departureTime for backend parsing. */ |
|
|
|
|
|
const departureTimeToStringForSave = (timeValue: Truck["departureTime"]): string => { |
|
|
|
|
|
const formatted = formatDepartureTime( |
|
|
|
|
|
Array.isArray(timeValue) ? timeValue : timeValue ? String(timeValue) : null |
|
|
|
|
|
); |
|
|
|
|
|
if (!formatted || formatted === "-") return ""; |
|
|
|
|
|
return parseDepartureTimeForBackend(formatted); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const ShopDetail: React.FC = () => { |
|
|
const ShopDetail: React.FC = () => { |
|
|
const { t } = useTranslation("common"); |
|
|
const { t } = useTranslation("common"); |
|
|
const router = useRouter(); |
|
|
const router = useRouter(); |
|
|
@@ -85,21 +107,15 @@ const ShopDetail: React.FC = () => { |
|
|
const { data: session, status: sessionStatus } = useSession() as { data: SessionWithTokens | null; status: string }; |
|
|
const { data: session, status: sessionStatus } = useSession() as { data: SessionWithTokens | null; status: string }; |
|
|
const [shopDetail, setShopDetail] = useState<ShopDetailData | null>(null); |
|
|
const [shopDetail, setShopDetail] = useState<ShopDetailData | null>(null); |
|
|
const [truckData, setTruckData] = useState<Truck[]>([]); |
|
|
const [truckData, setTruckData] = useState<Truck[]>([]); |
|
|
const [editedTruckData, setEditedTruckData] = useState<Truck[]>([]); |
|
|
|
|
|
const [loading, setLoading] = useState<boolean>(true); |
|
|
const [loading, setLoading] = useState<boolean>(true); |
|
|
const [error, setError] = useState<string | null>(null); |
|
|
const [error, setError] = useState<string | null>(null); |
|
|
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null); |
|
|
|
|
|
const [saving, setSaving] = useState<boolean>(false); |
|
|
const [saving, setSaving] = useState<boolean>(false); |
|
|
|
|
|
const [confirmingDeleteIndex, setConfirmingDeleteIndex] = useState<number | null>(null); |
|
|
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false); |
|
|
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false); |
|
|
const [newTruck, setNewTruck] = useState({ |
|
|
|
|
|
truckLanceCode: "", |
|
|
|
|
|
departureTime: "", |
|
|
|
|
|
loadingSequence: 0, |
|
|
|
|
|
districtReference: 0, |
|
|
|
|
|
storeId: "2F", |
|
|
|
|
|
remark: "", |
|
|
|
|
|
}); |
|
|
|
|
|
const [uniqueRemarks, setUniqueRemarks] = useState<string[]>([]); |
|
|
|
|
|
|
|
|
const [availableTruckLanes, setAvailableTruckLanes] = useState<Truck[]>([]); |
|
|
|
|
|
const [selectedTruckLane, setSelectedTruckLane] = useState<Truck | null>(null); |
|
|
|
|
|
const [addLoadingSequence, setAddLoadingSequence] = useState<number>(0); |
|
|
|
|
|
const [loadingTruckLanes, setLoadingTruckLanes] = useState<boolean>(false); |
|
|
const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false); |
|
|
const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false); |
|
|
const [snackbarMessage, setSnackbarMessage] = useState<string>(""); |
|
|
const [snackbarMessage, setSnackbarMessage] = useState<string>(""); |
|
|
|
|
|
|
|
|
@@ -168,16 +184,6 @@ const ShopDetail: React.FC = () => { |
|
|
// Fetch truck information using the Truck interface with numeric ID |
|
|
// Fetch truck information using the Truck interface with numeric ID |
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
setTruckData(trucks || []); |
|
|
setTruckData(trucks || []); |
|
|
setEditedTruckData(trucks || []); |
|
|
|
|
|
setEditingRowIndex(null); |
|
|
|
|
|
|
|
|
|
|
|
// Extract unique remarks from trucks for this shop |
|
|
|
|
|
const remarks = trucks |
|
|
|
|
|
?.map(t => t.remark) |
|
|
|
|
|
.filter((remark): remark is string => remark != null && String(remark).trim() !== "") |
|
|
|
|
|
.map(r => String(r).trim()) |
|
|
|
|
|
.filter((value, index, self) => self.indexOf(value) === index) || []; |
|
|
|
|
|
setUniqueRemarks(remarks); |
|
|
|
|
|
} catch (err: any) { |
|
|
} catch (err: any) { |
|
|
console.error("Failed to load shop detail:", err); |
|
|
console.error("Failed to load shop detail:", err); |
|
|
// Handle errors gracefully - don't trigger auto-logout |
|
|
// Handle errors gracefully - don't trigger auto-logout |
|
|
@@ -191,118 +197,7 @@ const ShopDetail: React.FC = () => { |
|
|
fetchShopDetail(); |
|
|
fetchShopDetail(); |
|
|
}, [shopId, sessionStatus, session]); |
|
|
}, [shopId, sessionStatus, session]); |
|
|
|
|
|
|
|
|
const handleEdit = (index: number) => { |
|
|
|
|
|
setEditingRowIndex(index); |
|
|
|
|
|
const updated = [...truckData]; |
|
|
|
|
|
updated[index] = { ...updated[index] }; |
|
|
|
|
|
// Normalize departureTime to HH:mm format for editing |
|
|
|
|
|
if (updated[index].departureTime) { |
|
|
|
|
|
const timeValue = updated[index].departureTime; |
|
|
|
|
|
const formatted = formatDepartureTime( |
|
|
|
|
|
Array.isArray(timeValue) ? timeValue : (timeValue ? String(timeValue) : null) |
|
|
|
|
|
); |
|
|
|
|
|
if (formatted !== "-") { |
|
|
|
|
|
updated[index].departureTime = formatted; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
// Ensure remark is initialized as string (not null/undefined) |
|
|
|
|
|
if (updated[index].remark == null) { |
|
|
|
|
|
updated[index].remark = ""; |
|
|
|
|
|
} |
|
|
|
|
|
setEditedTruckData(updated); |
|
|
|
|
|
setError(null); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const handleCancel = (index: number) => { |
|
|
|
|
|
setEditingRowIndex(null); |
|
|
|
|
|
setEditedTruckData([...truckData]); |
|
|
|
|
|
setError(null); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const handleSave = async (index: number) => { |
|
|
|
|
|
if (!shopId) { |
|
|
|
|
|
setError(t("Shop ID is required")); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const truck = editedTruckData[index]; |
|
|
|
|
|
if (!truck || !truck.id) { |
|
|
|
|
|
setError(t("Invalid shop data")); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setSaving(true); |
|
|
|
|
|
setError(null); |
|
|
|
|
|
try { |
|
|
|
|
|
// Use the departureTime from editedTruckData which is already in HH:mm format from the input field |
|
|
|
|
|
// If it's already a valid HH:mm string, use it directly; otherwise format it |
|
|
|
|
|
let departureTime = String(truck.departureTime || "").trim(); |
|
|
|
|
|
if (!departureTime || departureTime === "-") { |
|
|
|
|
|
departureTime = ""; |
|
|
|
|
|
} else if (!/^\d{1,2}:\d{2}$/.test(departureTime)) { |
|
|
|
|
|
// Only convert if it's not already in HH:mm format |
|
|
|
|
|
departureTime = parseDepartureTimeForBackend(departureTime); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Convert storeId to string format (2F or 4F) |
|
|
|
|
|
const storeIdStr = normalizeStoreId(truck.storeId) || "2F"; |
|
|
|
|
|
|
|
|
|
|
|
// Get remark value - use the remark from editedTruckData (user input) |
|
|
|
|
|
// Only send remark if storeId is "4F", otherwise send null |
|
|
|
|
|
let remarkValue: string | null = null; |
|
|
|
|
|
if (storeIdStr === "4F") { |
|
|
|
|
|
const remark = truck.remark; |
|
|
|
|
|
if (remark != null && String(remark).trim() !== "") { |
|
|
|
|
|
remarkValue = String(remark).trim(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await updateTruckLaneClient({ |
|
|
|
|
|
id: truck.id, |
|
|
|
|
|
truckLanceCode: String(truck.truckLanceCode || ""), |
|
|
|
|
|
departureTime: departureTime, |
|
|
|
|
|
loadingSequence: Number(truck.loadingSequence) || 0, |
|
|
|
|
|
districtReference: Number(truck.districtReference) || 0, |
|
|
|
|
|
storeId: storeIdStr, |
|
|
|
|
|
remark: remarkValue, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Refresh truck data after update |
|
|
|
|
|
const shopIdNum = parseInt(shopId, 10); |
|
|
|
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
|
|
|
setTruckData(trucks || []); |
|
|
|
|
|
setEditedTruckData(trucks || []); |
|
|
|
|
|
setEditingRowIndex(null); |
|
|
|
|
|
|
|
|
|
|
|
// Update unique remarks |
|
|
|
|
|
const remarks = trucks |
|
|
|
|
|
?.map(t => t.remark) |
|
|
|
|
|
.filter((remark): remark is string => remark != null && String(remark).trim() !== "") |
|
|
|
|
|
.map(r => String(r).trim()) |
|
|
|
|
|
.filter((value, index, self) => self.indexOf(value) === index) || []; |
|
|
|
|
|
setUniqueRemarks(remarks); |
|
|
|
|
|
} catch (err: any) { |
|
|
|
|
|
console.error("Failed to save truck data:", err); |
|
|
|
|
|
setError(err?.message ?? String(err) ?? t("Failed to save truck data")); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setSaving(false); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const handleTruckFieldChange = (index: number, field: keyof Truck, value: string | number) => { |
|
|
|
|
|
const updated = [...editedTruckData]; |
|
|
|
|
|
updated[index] = { |
|
|
|
|
|
...updated[index], |
|
|
|
|
|
[field]: value, |
|
|
|
|
|
}; |
|
|
|
|
|
setEditedTruckData(updated); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (truckId: number) => { |
|
|
const handleDelete = async (truckId: number) => { |
|
|
if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!shopId) { |
|
|
if (!shopId) { |
|
|
setError(t("Shop ID is required")); |
|
|
setError(t("Shop ID is required")); |
|
|
return; |
|
|
return; |
|
|
@@ -312,13 +207,11 @@ const ShopDetail: React.FC = () => { |
|
|
setError(null); |
|
|
setError(null); |
|
|
try { |
|
|
try { |
|
|
await deleteTruckLaneClient({ id: truckId }); |
|
|
await deleteTruckLaneClient({ id: truckId }); |
|
|
|
|
|
|
|
|
// Refresh truck data after delete |
|
|
|
|
|
|
|
|
setConfirmingDeleteIndex(null); |
|
|
|
|
|
|
|
|
const shopIdNum = parseInt(shopId, 10); |
|
|
const shopIdNum = parseInt(shopId, 10); |
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
setTruckData(trucks || []); |
|
|
setTruckData(trucks || []); |
|
|
setEditedTruckData(trucks || []); |
|
|
|
|
|
setEditingRowIndex(null); |
|
|
|
|
|
} catch (err: any) { |
|
|
} catch (err: any) { |
|
|
console.error("Failed to delete truck lane:", err); |
|
|
console.error("Failed to delete truck lane:", err); |
|
|
setError(err?.message ?? String(err) ?? t("Failed to delete truck lane")); |
|
|
setError(err?.message ?? String(err) ?? t("Failed to delete truck lane")); |
|
|
@@ -327,44 +220,49 @@ const ShopDetail: React.FC = () => { |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleOpenAddDialog = () => { |
|
|
|
|
|
setNewTruck({ |
|
|
|
|
|
truckLanceCode: "", |
|
|
|
|
|
departureTime: "", |
|
|
|
|
|
loadingSequence: 0, |
|
|
|
|
|
districtReference: 0, |
|
|
|
|
|
storeId: "2F", |
|
|
|
|
|
remark: "", |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
const handleOpenAddDialog = async () => { |
|
|
|
|
|
setSelectedTruckLane(null); |
|
|
|
|
|
setAddLoadingSequence(0); |
|
|
setAddDialogOpen(true); |
|
|
setAddDialogOpen(true); |
|
|
setError(null); |
|
|
setError(null); |
|
|
|
|
|
setLoadingTruckLanes(true); |
|
|
|
|
|
try { |
|
|
|
|
|
const lanes = (await findAllUniqueTruckLaneCombinationsClient()) as Truck[]; |
|
|
|
|
|
setAvailableTruckLanes(lanes || []); |
|
|
|
|
|
} catch (err: any) { |
|
|
|
|
|
console.error("Failed to load truck lanes:", err); |
|
|
|
|
|
setAvailableTruckLanes([]); |
|
|
|
|
|
setSnackbarMessage( |
|
|
|
|
|
err?.message ?? String(err) ?? t("Failed to load truck lanes") |
|
|
|
|
|
); |
|
|
|
|
|
setSnackbarOpen(true); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setLoadingTruckLanes(false); |
|
|
|
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleCloseAddDialog = () => { |
|
|
const handleCloseAddDialog = () => { |
|
|
setAddDialogOpen(false); |
|
|
setAddDialogOpen(false); |
|
|
setNewTruck({ |
|
|
|
|
|
truckLanceCode: "", |
|
|
|
|
|
departureTime: "", |
|
|
|
|
|
loadingSequence: 0, |
|
|
|
|
|
districtReference: 0, |
|
|
|
|
|
storeId: "2F", |
|
|
|
|
|
remark: "", |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
setSelectedTruckLane(null); |
|
|
|
|
|
setAddLoadingSequence(0); |
|
|
|
|
|
setAvailableTruckLanes([]); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const handleCreateTruck = async () => { |
|
|
const handleCreateTruck = async () => { |
|
|
// Validate all required fields |
|
|
|
|
|
const missingFields: string[] = []; |
|
|
const missingFields: string[] = []; |
|
|
|
|
|
|
|
|
if (!shopId || !shopDetail) { |
|
|
if (!shopId || !shopDetail) { |
|
|
missingFields.push(t("Shop Information")); |
|
|
missingFields.push(t("Shop Information")); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!newTruck.truckLanceCode.trim()) { |
|
|
|
|
|
|
|
|
if (!selectedTruckLane) { |
|
|
missingFields.push(t("TruckLance Code")); |
|
|
missingFields.push(t("TruckLance Code")); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!newTruck.departureTime) { |
|
|
|
|
|
|
|
|
const departureTime = selectedTruckLane |
|
|
|
|
|
? departureTimeToStringForSave(selectedTruckLane.departureTime) |
|
|
|
|
|
: ""; |
|
|
|
|
|
if (!departureTime) { |
|
|
missingFields.push(t("Departure Time")); |
|
|
missingFields.push(t("Departure Time")); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -375,36 +273,32 @@ const ShopDetail: React.FC = () => { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const lane = selectedTruckLane!; |
|
|
|
|
|
const storeIdStr = normalizeStoreId(lane.storeId) || "2F"; |
|
|
|
|
|
const remarkValue = |
|
|
|
|
|
storeIdStr === "4F" && lane.remark != null && String(lane.remark).trim() !== "" |
|
|
|
|
|
? String(lane.remark).trim() |
|
|
|
|
|
: null; |
|
|
|
|
|
|
|
|
setSaving(true); |
|
|
setSaving(true); |
|
|
setError(null); |
|
|
setError(null); |
|
|
try { |
|
|
try { |
|
|
const departureTime = parseDepartureTimeForBackend(newTruck.departureTime); |
|
|
|
|
|
|
|
|
|
|
|
await createTruckClient({ |
|
|
await createTruckClient({ |
|
|
store_id: newTruck.storeId, |
|
|
|
|
|
truckLanceCode: newTruck.truckLanceCode.trim(), |
|
|
|
|
|
|
|
|
store_id: storeIdStr, |
|
|
|
|
|
truckLanceCode: String(lane.truckLanceCode || "").trim(), |
|
|
departureTime: departureTime, |
|
|
departureTime: departureTime, |
|
|
shopId: shopDetail!.id, |
|
|
shopId: shopDetail!.id, |
|
|
shopName: String(shopDetail!.name), |
|
|
shopName: String(shopDetail!.name), |
|
|
shopCode: String(shopDetail!.code), |
|
|
shopCode: String(shopDetail!.code), |
|
|
loadingSequence: newTruck.loadingSequence, |
|
|
|
|
|
districtReference: newTruck.districtReference, |
|
|
|
|
|
remark: newTruck.storeId === "4F" ? (newTruck.remark?.trim() || null) : null, |
|
|
|
|
|
|
|
|
loadingSequence: addLoadingSequence, |
|
|
|
|
|
districtReference: Number(lane.districtReference) || 0, |
|
|
|
|
|
remark: remarkValue, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// Refresh truck data after create |
|
|
// Refresh truck data after create |
|
|
const shopIdNum = parseInt(shopId || "0", 10); |
|
|
const shopIdNum = parseInt(shopId || "0", 10); |
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
setTruckData(trucks || []); |
|
|
setTruckData(trucks || []); |
|
|
setEditedTruckData(trucks || []); |
|
|
|
|
|
|
|
|
|
|
|
// Update unique remarks |
|
|
|
|
|
const remarks = trucks |
|
|
|
|
|
?.map(t => t.remark) |
|
|
|
|
|
.filter((remark): remark is string => remark != null && String(remark).trim() !== "") |
|
|
|
|
|
.map(r => String(r).trim()) |
|
|
|
|
|
.filter((value, index, self) => self.indexOf(value) === index) || []; |
|
|
|
|
|
setUniqueRemarks(remarks); |
|
|
|
|
|
|
|
|
|
|
|
handleCloseAddDialog(); |
|
|
handleCloseAddDialog(); |
|
|
} catch (err: any) { |
|
|
} catch (err: any) { |
|
|
@@ -502,7 +396,7 @@ const ShopDetail: React.FC = () => { |
|
|
variant="contained" |
|
|
variant="contained" |
|
|
startIcon={<AddIcon />} |
|
|
startIcon={<AddIcon />} |
|
|
onClick={handleOpenAddDialog} |
|
|
onClick={handleOpenAddDialog} |
|
|
disabled={editingRowIndex !== null || saving} |
|
|
|
|
|
|
|
|
disabled={saving} |
|
|
> |
|
|
> |
|
|
{t("Add Truck Lane")} |
|
|
{t("Add Truck Lane")} |
|
|
</Button> |
|
|
</Button> |
|
|
@@ -517,7 +411,7 @@ const ShopDetail: React.FC = () => { |
|
|
<TableCell>{t("District Reference")}</TableCell> |
|
|
<TableCell>{t("District Reference")}</TableCell> |
|
|
<TableCell>{t("Store ID")}</TableCell> |
|
|
<TableCell>{t("Store ID")}</TableCell> |
|
|
<TableCell>{t("Remark")}</TableCell> |
|
|
<TableCell>{t("Remark")}</TableCell> |
|
|
<TableCell>{t("Actions")}</TableCell> |
|
|
|
|
|
|
|
|
<TableCell align="right">{t("Actions")}</TableCell> |
|
|
</TableRow> |
|
|
</TableRow> |
|
|
</TableHead> |
|
|
</TableHead> |
|
|
<TableBody> |
|
|
<TableBody> |
|
|
@@ -530,199 +424,70 @@ const ShopDetail: React.FC = () => { |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
</TableRow> |
|
|
</TableRow> |
|
|
) : ( |
|
|
) : ( |
|
|
truckData.map((truck, index) => { |
|
|
|
|
|
const isEditing = editingRowIndex === index; |
|
|
|
|
|
const displayTruck = isEditing ? editedTruckData[index] : truck; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<TableRow key={truck.id ?? `truck-${index}`}> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{isEditing ? ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
size="small" |
|
|
|
|
|
value={String(displayTruck?.truckLanceCode || "")} |
|
|
|
|
|
onChange={(e) => handleTruckFieldChange(index, "truckLanceCode", e.target.value)} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
/> |
|
|
|
|
|
) : ( |
|
|
|
|
|
String(truck.truckLanceCode || "-") |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{isEditing ? ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
size="small" |
|
|
|
|
|
type="time" |
|
|
|
|
|
value={(() => { |
|
|
|
|
|
const timeValue = displayTruck?.departureTime; |
|
|
|
|
|
const formatted = formatDepartureTime( |
|
|
|
|
|
Array.isArray(timeValue) ? timeValue : (timeValue ? String(timeValue) : null) |
|
|
|
|
|
); |
|
|
|
|
|
return formatted !== "-" ? formatted : ""; |
|
|
|
|
|
})()} |
|
|
|
|
|
onChange={(e) => handleTruckFieldChange(index, "departureTime", e.target.value)} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
InputLabelProps={{ |
|
|
|
|
|
shrink: true, |
|
|
|
|
|
}} |
|
|
|
|
|
inputProps={{ |
|
|
|
|
|
step: 300, // 5 minutes |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
) : ( |
|
|
|
|
|
formatDepartureTime( |
|
|
|
|
|
Array.isArray(truck.departureTime) ? truck.departureTime : (truck.departureTime ? String(truck.departureTime) : null) |
|
|
|
|
|
) |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{isEditing ? ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
size="small" |
|
|
|
|
|
type="number" |
|
|
|
|
|
value={displayTruck?.loadingSequence ?? 0} |
|
|
|
|
|
onChange={(e) => handleTruckFieldChange(index, "loadingSequence", parseInt(e.target.value) || 0)} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
/> |
|
|
|
|
|
) : ( |
|
|
|
|
|
truck.loadingSequence !== null && truck.loadingSequence !== undefined ? String(truck.loadingSequence) : "-" |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{isEditing ? ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
size="small" |
|
|
|
|
|
type="number" |
|
|
|
|
|
value={displayTruck?.districtReference ?? 0} |
|
|
|
|
|
onChange={(e) => handleTruckFieldChange(index, "districtReference", parseInt(e.target.value) || 0)} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
/> |
|
|
|
|
|
) : ( |
|
|
|
|
|
truck.districtReference !== null && truck.districtReference !== undefined ? String(truck.districtReference) : "-" |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{isEditing ? ( |
|
|
|
|
|
<FormControl size="small" fullWidth> |
|
|
|
|
|
<Select |
|
|
|
|
|
value={(() => { |
|
|
|
|
|
const storeId = displayTruck?.storeId; |
|
|
|
|
|
if (!storeId) return "2F"; |
|
|
|
|
|
const storeIdStr = typeof storeId === 'string' ? storeId : String(storeId); |
|
|
|
|
|
// Convert numeric values to string format |
|
|
|
|
|
if (storeIdStr === "2" || storeIdStr === "2F") return "2F"; |
|
|
|
|
|
if (storeIdStr === "4" || storeIdStr === "4F") return "4F"; |
|
|
|
|
|
return storeIdStr; |
|
|
|
|
|
})()} |
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
const newStoreId = e.target.value; |
|
|
|
|
|
handleTruckFieldChange(index, "storeId", newStoreId); |
|
|
|
|
|
// Clear remark if storeId changes from 4F to something else |
|
|
|
|
|
if (newStoreId !== "4F") { |
|
|
|
|
|
handleTruckFieldChange(index, "remark", ""); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<MenuItem value="2F">2F</MenuItem> |
|
|
|
|
|
<MenuItem value="4F">4F</MenuItem> |
|
|
|
|
|
</Select> |
|
|
|
|
|
</FormControl> |
|
|
|
|
|
) : ( |
|
|
|
|
|
normalizeStoreId(truck.storeId) |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{isEditing ? ( |
|
|
|
|
|
(() => { |
|
|
|
|
|
const storeIdStr = normalizeStoreId(displayTruck?.storeId) || "2F"; |
|
|
|
|
|
const isEditable = storeIdStr === "4F"; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
freeSolo |
|
|
|
|
|
size="small" |
|
|
|
|
|
disabled={!isEditable} |
|
|
|
|
|
options={uniqueRemarks} |
|
|
|
|
|
value={displayTruck?.remark ? String(displayTruck.remark) : ""} |
|
|
|
|
|
onChange={(event, newValue) => { |
|
|
|
|
|
if (isEditable) { |
|
|
|
|
|
const remarkValue = typeof newValue === 'string' ? newValue : (newValue || ""); |
|
|
|
|
|
handleTruckFieldChange(index, "remark", remarkValue); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
onInputChange={(event, newInputValue, reason) => { |
|
|
|
|
|
// Only update on user input, not when clearing or selecting |
|
|
|
|
|
if (isEditable && (reason === 'input' || reason === 'clear')) { |
|
|
|
|
|
handleTruckFieldChange(index, "remark", newInputValue); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
renderInput={(params) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
placeholder={isEditable ? t("Enter or select remark") : t("Not editable for this Store ID")} |
|
|
|
|
|
disabled={!isEditable} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
/> |
|
|
|
|
|
); |
|
|
|
|
|
})() |
|
|
|
|
|
) : ( |
|
|
|
|
|
String(truck.remark || "-") |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
<Stack direction="row" spacing={0.5}> |
|
|
|
|
|
{isEditing ? ( |
|
|
|
|
|
|
|
|
truckData.map((truck, index) => ( |
|
|
|
|
|
<TableRow key={truck.id ?? `truck-${index}`}> |
|
|
|
|
|
<TableCell>{String(truck.truckLanceCode || "-")}</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{formatDepartureTime( |
|
|
|
|
|
Array.isArray(truck.departureTime) |
|
|
|
|
|
? truck.departureTime |
|
|
|
|
|
: truck.departureTime |
|
|
|
|
|
? String(truck.departureTime) |
|
|
|
|
|
: null |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{truck.loadingSequence !== null && truck.loadingSequence !== undefined |
|
|
|
|
|
? String(truck.loadingSequence) |
|
|
|
|
|
: "-"} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{truck.districtReference !== null && truck.districtReference !== undefined |
|
|
|
|
|
? String(truck.districtReference) |
|
|
|
|
|
: "-"} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell>{normalizeStoreId(truck.storeId)}</TableCell> |
|
|
|
|
|
<TableCell>{String(truck.remark || "-")}</TableCell> |
|
|
|
|
|
<TableCell align="right"> |
|
|
|
|
|
<Stack direction="row" spacing={0.5} justifyContent="flex-end"> |
|
|
|
|
|
{truck.id && ( |
|
|
|
|
|
confirmingDeleteIndex === index ? ( |
|
|
<> |
|
|
<> |
|
|
<IconButton |
|
|
<IconButton |
|
|
color="primary" |
|
|
|
|
|
size="small" |
|
|
size="small" |
|
|
onClick={() => handleSave(index)} |
|
|
|
|
|
|
|
|
color="error" |
|
|
|
|
|
onClick={() => handleDelete(truck.id!)} |
|
|
disabled={saving} |
|
|
disabled={saving} |
|
|
title={t("Save changes")} |
|
|
|
|
|
|
|
|
title={t("Confirm delete")} |
|
|
> |
|
|
> |
|
|
<SaveIcon /> |
|
|
|
|
|
|
|
|
<CheckIcon /> |
|
|
</IconButton> |
|
|
</IconButton> |
|
|
<IconButton |
|
|
<IconButton |
|
|
color="default" |
|
|
|
|
|
size="small" |
|
|
size="small" |
|
|
onClick={() => handleCancel(index)} |
|
|
|
|
|
|
|
|
color="default" |
|
|
|
|
|
onClick={() => setConfirmingDeleteIndex(null)} |
|
|
disabled={saving} |
|
|
disabled={saving} |
|
|
title={t("Cancel editing")} |
|
|
|
|
|
|
|
|
title={t("Cancel")} |
|
|
> |
|
|
> |
|
|
<CancelIcon /> |
|
|
|
|
|
|
|
|
<CloseIcon /> |
|
|
</IconButton> |
|
|
</IconButton> |
|
|
</> |
|
|
</> |
|
|
) : ( |
|
|
) : ( |
|
|
<> |
|
|
|
|
|
<IconButton |
|
|
|
|
|
color="primary" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => handleEdit(index)} |
|
|
|
|
|
disabled={editingRowIndex !== null} |
|
|
|
|
|
title={t("Edit truck lane")} |
|
|
|
|
|
> |
|
|
|
|
|
<EditIcon /> |
|
|
|
|
|
</IconButton> |
|
|
|
|
|
{truck.id && ( |
|
|
|
|
|
<IconButton |
|
|
|
|
|
color="error" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => handleDelete(truck.id!)} |
|
|
|
|
|
disabled={saving || editingRowIndex !== null} |
|
|
|
|
|
title={t("Delete truck lane")} |
|
|
|
|
|
> |
|
|
|
|
|
<DeleteIcon /> |
|
|
|
|
|
</IconButton> |
|
|
|
|
|
)} |
|
|
|
|
|
</> |
|
|
|
|
|
)} |
|
|
|
|
|
</Stack> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
); |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
<IconButton |
|
|
|
|
|
color="error" |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => setConfirmingDeleteIndex(index)} |
|
|
|
|
|
disabled={saving || confirmingDeleteIndex !== null} |
|
|
|
|
|
title={t("Delete truck lane")} |
|
|
|
|
|
> |
|
|
|
|
|
<DeleteIcon /> |
|
|
|
|
|
</IconButton> |
|
|
|
|
|
) |
|
|
|
|
|
)} |
|
|
|
|
|
</Stack> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
)) |
|
|
)} |
|
|
)} |
|
|
</TableBody> |
|
|
</TableBody> |
|
|
</Table> |
|
|
</Table> |
|
|
@@ -737,97 +502,41 @@ const ShopDetail: React.FC = () => { |
|
|
<Box sx={{ pt: 2 }}> |
|
|
<Box sx={{ pt: 2 }}> |
|
|
<Grid container spacing={2}> |
|
|
<Grid container spacing={2}> |
|
|
<Grid item xs={12}> |
|
|
<Grid item xs={12}> |
|
|
<TextField |
|
|
|
|
|
label={t("TruckLance Code")} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
required |
|
|
|
|
|
value={newTruck.truckLanceCode} |
|
|
|
|
|
onChange={(e) => setNewTruck({ ...newTruck, truckLanceCode: e.target.value })} |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<TextField |
|
|
|
|
|
label={t("Departure Time")} |
|
|
|
|
|
type="time" |
|
|
|
|
|
fullWidth |
|
|
|
|
|
required |
|
|
|
|
|
value={newTruck.departureTime} |
|
|
|
|
|
onChange={(e) => setNewTruck({ ...newTruck, departureTime: e.target.value })} |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
InputLabelProps={{ |
|
|
|
|
|
shrink: true, |
|
|
|
|
|
}} |
|
|
|
|
|
inputProps={{ |
|
|
|
|
|
step: 300, // 5 minutes |
|
|
|
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
options={availableTruckLanes} |
|
|
|
|
|
loading={loadingTruckLanes} |
|
|
|
|
|
value={selectedTruckLane} |
|
|
|
|
|
onChange={(_event, newValue) => { |
|
|
|
|
|
setSelectedTruckLane(newValue); |
|
|
}} |
|
|
}} |
|
|
|
|
|
getOptionLabel={(option) => getTruckLaneOptionLabel(option)} |
|
|
|
|
|
isOptionEqualToValue={(option, value) => isSameTruckLaneOption(option, value)} |
|
|
|
|
|
disabled={saving || loadingTruckLanes} |
|
|
|
|
|
renderInput={(params) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
label={t("TruckLance Code")} |
|
|
|
|
|
required |
|
|
|
|
|
placeholder={t("Select a truck lane")} |
|
|
|
|
|
helperText={ |
|
|
|
|
|
!loadingTruckLanes && availableTruckLanes.length === 0 |
|
|
|
|
|
? t("No truck lanes available") |
|
|
|
|
|
: undefined |
|
|
|
|
|
} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
/> |
|
|
/> |
|
|
</Grid> |
|
|
</Grid> |
|
|
<Grid item xs={6}> |
|
|
|
|
|
|
|
|
<Grid item xs={12}> |
|
|
<TextField |
|
|
<TextField |
|
|
label={t("Loading Sequence")} |
|
|
label={t("Loading Sequence")} |
|
|
type="number" |
|
|
type="number" |
|
|
fullWidth |
|
|
fullWidth |
|
|
value={newTruck.loadingSequence} |
|
|
|
|
|
onChange={(e) => setNewTruck({ ...newTruck, loadingSequence: parseInt(e.target.value) || 0 })} |
|
|
|
|
|
|
|
|
value={addLoadingSequence} |
|
|
|
|
|
onChange={(e) => setAddLoadingSequence(parseInt(e.target.value, 10) || 0)} |
|
|
disabled={saving} |
|
|
disabled={saving} |
|
|
/> |
|
|
/> |
|
|
</Grid> |
|
|
</Grid> |
|
|
<Grid item xs={6}> |
|
|
|
|
|
<TextField |
|
|
|
|
|
label={t("District Reference")} |
|
|
|
|
|
type="number" |
|
|
|
|
|
fullWidth |
|
|
|
|
|
value={newTruck.districtReference} |
|
|
|
|
|
onChange={(e) => setNewTruck({ ...newTruck, districtReference: parseInt(e.target.value) || 0 })} |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={6}> |
|
|
|
|
|
<FormControl fullWidth> |
|
|
|
|
|
<InputLabel>{t("Store ID")}</InputLabel> |
|
|
|
|
|
<Select |
|
|
|
|
|
value={newTruck.storeId} |
|
|
|
|
|
label={t("Store ID")} |
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
const newStoreId = e.target.value; |
|
|
|
|
|
setNewTruck({ |
|
|
|
|
|
...newTruck, |
|
|
|
|
|
storeId: newStoreId, |
|
|
|
|
|
remark: newStoreId === "4F" ? newTruck.remark : "" |
|
|
|
|
|
}); |
|
|
|
|
|
}} |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
> |
|
|
|
|
|
<MenuItem value="2F">2F</MenuItem> |
|
|
|
|
|
<MenuItem value="4F">4F</MenuItem> |
|
|
|
|
|
</Select> |
|
|
|
|
|
</FormControl> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
{newTruck.storeId === "4F" && ( |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
freeSolo |
|
|
|
|
|
options={uniqueRemarks} |
|
|
|
|
|
value={newTruck.remark || ""} |
|
|
|
|
|
onChange={(event, newValue) => { |
|
|
|
|
|
setNewTruck({ ...newTruck, remark: newValue || "" }); |
|
|
|
|
|
}} |
|
|
|
|
|
onInputChange={(event, newInputValue) => { |
|
|
|
|
|
setNewTruck({ ...newTruck, remark: newInputValue }); |
|
|
|
|
|
}} |
|
|
|
|
|
renderInput={(params) => ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
{...params} |
|
|
|
|
|
label={t("Remark")} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
placeholder={t("Enter or select remark")} |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
)} |
|
|
|
|
|
</Grid> |
|
|
</Grid> |
|
|
</Box> |
|
|
</Box> |
|
|
</DialogContent> |
|
|
</DialogContent> |
|
|
|