|
|
|
@@ -0,0 +1,761 @@ |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import { |
|
|
|
Box, |
|
|
|
Card, |
|
|
|
CardContent, |
|
|
|
Typography, |
|
|
|
CircularProgress, |
|
|
|
Alert, |
|
|
|
Button, |
|
|
|
Table, |
|
|
|
TableBody, |
|
|
|
TableCell, |
|
|
|
TableContainer, |
|
|
|
TableHead, |
|
|
|
TableRow, |
|
|
|
Paper, |
|
|
|
TextField, |
|
|
|
Stack, |
|
|
|
IconButton, |
|
|
|
Dialog, |
|
|
|
DialogTitle, |
|
|
|
DialogContent, |
|
|
|
DialogActions, |
|
|
|
Grid, |
|
|
|
Snackbar, |
|
|
|
} 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 { useRouter, useSearchParams } from "next/navigation"; |
|
|
|
import { useState, useEffect } from "react"; |
|
|
|
import { useSession } from "next-auth/react"; |
|
|
|
import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions"; |
|
|
|
import { |
|
|
|
fetchAllShopsClient, |
|
|
|
findTruckLaneByShopIdClient, |
|
|
|
updateTruckLaneClient, |
|
|
|
deleteTruckLaneClient, |
|
|
|
createTruckClient |
|
|
|
} from "@/app/api/shop/client"; |
|
|
|
import type { SessionWithTokens } from "@/config/authConfig"; |
|
|
|
|
|
|
|
type ShopDetailData = { |
|
|
|
id: number; |
|
|
|
name: String; |
|
|
|
code: String; |
|
|
|
addr1: String; |
|
|
|
addr2: String; |
|
|
|
addr3: String; |
|
|
|
contactNo: number; |
|
|
|
type: String; |
|
|
|
contactEmail: String; |
|
|
|
contactName: String; |
|
|
|
}; |
|
|
|
|
|
|
|
// Utility function to format departureTime to HH:mm format |
|
|
|
const formatDepartureTime = (time: string | number[] | null | undefined): string => { |
|
|
|
if (!time) return "-"; |
|
|
|
|
|
|
|
// Handle array format [hours, minutes] from API |
|
|
|
if (Array.isArray(time) && time.length >= 2) { |
|
|
|
const hours = time[0]; |
|
|
|
const minutes = time[1]; |
|
|
|
if (typeof hours === 'number' && typeof minutes === 'number' && |
|
|
|
hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) { |
|
|
|
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const timeStr = String(time).trim(); |
|
|
|
if (!timeStr || timeStr === "-") return "-"; |
|
|
|
|
|
|
|
// If already in HH:mm format, return as is |
|
|
|
if (/^\d{1,2}:\d{2}$/.test(timeStr)) { |
|
|
|
const [hours, minutes] = timeStr.split(":"); |
|
|
|
return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`; |
|
|
|
} |
|
|
|
|
|
|
|
// Handle decimal format (e.g., "17,0" or "17.0" representing hours) |
|
|
|
const decimalMatch = timeStr.match(/^(\d+)[,.](\d+)$/); |
|
|
|
if (decimalMatch) { |
|
|
|
const hours = parseInt(decimalMatch[1], 10); |
|
|
|
const minutes = Math.round(parseFloat(`0.${decimalMatch[2]}`) * 60); |
|
|
|
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; |
|
|
|
} |
|
|
|
|
|
|
|
// Handle single number as hours (e.g., "17" -> "17:00") |
|
|
|
const hoursOnly = parseInt(timeStr, 10); |
|
|
|
if (!isNaN(hoursOnly) && hoursOnly >= 0 && hoursOnly <= 23) { |
|
|
|
return `${hoursOnly.toString().padStart(2, "0")}:00`; |
|
|
|
} |
|
|
|
|
|
|
|
// Try to parse as ISO time string or other formats |
|
|
|
try { |
|
|
|
// If it's already a valid time string, try to extract hours and minutes |
|
|
|
const parts = timeStr.split(/[:,\s]/); |
|
|
|
if (parts.length >= 2) { |
|
|
|
const h = parseInt(parts[0], 10); |
|
|
|
const m = parseInt(parts[1], 10); |
|
|
|
if (!isNaN(h) && !isNaN(m) && h >= 0 && h <= 23 && m >= 0 && m <= 59) { |
|
|
|
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}`; |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
// If parsing fails, return original string |
|
|
|
} |
|
|
|
|
|
|
|
return timeStr; |
|
|
|
}; |
|
|
|
|
|
|
|
// Utility function to convert HH:mm format to the format expected by backend |
|
|
|
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); |
|
|
|
}; |
|
|
|
|
|
|
|
const ShopDetail: React.FC = () => { |
|
|
|
const router = useRouter(); |
|
|
|
const searchParams = useSearchParams(); |
|
|
|
const shopId = searchParams.get("id"); |
|
|
|
const { data: session, status: sessionStatus } = useSession() as { data: SessionWithTokens | null; status: string }; |
|
|
|
const [shopDetail, setShopDetail] = useState<ShopDetailData | null>(null); |
|
|
|
const [truckData, setTruckData] = useState<Truck[]>([]); |
|
|
|
const [editedTruckData, setEditedTruckData] = useState<Truck[]>([]); |
|
|
|
const [loading, setLoading] = useState<boolean>(true); |
|
|
|
const [error, setError] = useState<string | null>(null); |
|
|
|
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null); |
|
|
|
const [saving, setSaving] = useState<boolean>(false); |
|
|
|
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false); |
|
|
|
const [newTruck, setNewTruck] = useState({ |
|
|
|
truckLanceCode: "", |
|
|
|
departureTime: "", |
|
|
|
loadingSequence: 0, |
|
|
|
districtReference: 0, |
|
|
|
storeId: 2, |
|
|
|
}); |
|
|
|
const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false); |
|
|
|
const [snackbarMessage, setSnackbarMessage] = useState<string>(""); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
// Wait for session to be ready before making API calls |
|
|
|
if (sessionStatus === "loading") { |
|
|
|
return; // Still loading session |
|
|
|
} |
|
|
|
|
|
|
|
// If session is unauthenticated, don't make API calls (middleware will handle redirect) |
|
|
|
if (sessionStatus === "unauthenticated" || !session) { |
|
|
|
setError("Please log in to view shop details"); |
|
|
|
setLoading(false); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const fetchShopDetail = async () => { |
|
|
|
if (!shopId) { |
|
|
|
setError("Shop ID is required"); |
|
|
|
setLoading(false); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Convert shopId to number for proper filtering |
|
|
|
const shopIdNum = parseInt(shopId, 10); |
|
|
|
if (isNaN(shopIdNum)) { |
|
|
|
setError("Invalid Shop ID"); |
|
|
|
setLoading(false); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
setLoading(true); |
|
|
|
setError(null); |
|
|
|
try { |
|
|
|
// Fetch shop information - try with ID parameter first, then filter if needed |
|
|
|
let shopDataResponse = await fetchAllShopsClient({ id: shopIdNum }) as ShopAndTruck[]; |
|
|
|
|
|
|
|
// If no results with ID parameter, fetch all and filter client-side |
|
|
|
if (!shopDataResponse || shopDataResponse.length === 0) { |
|
|
|
shopDataResponse = await fetchAllShopsClient() as ShopAndTruck[]; |
|
|
|
} |
|
|
|
|
|
|
|
// Filter to find the shop with matching ID (in case API doesn't filter properly) |
|
|
|
const shopData = shopDataResponse?.find((item) => item.id === shopIdNum); |
|
|
|
|
|
|
|
if (shopData) { |
|
|
|
// Set shop detail info |
|
|
|
setShopDetail({ |
|
|
|
id: shopData.id ?? 0, |
|
|
|
name: shopData.name ?? "", |
|
|
|
code: shopData.code ?? "", |
|
|
|
addr1: shopData.addr1 ?? "", |
|
|
|
addr2: shopData.addr2 ?? "", |
|
|
|
addr3: shopData.addr3 ?? "", |
|
|
|
contactNo: shopData.contactNo ?? 0, |
|
|
|
type: shopData.type ?? "", |
|
|
|
contactEmail: shopData.contactEmail ?? "", |
|
|
|
contactName: shopData.contactName ?? "", |
|
|
|
}); |
|
|
|
} else { |
|
|
|
setError("Shop not found"); |
|
|
|
setLoading(false); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Fetch truck information using the Truck interface with numeric ID |
|
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
|
setTruckData(trucks || []); |
|
|
|
setEditedTruckData(trucks || []); |
|
|
|
setEditingRowIndex(null); |
|
|
|
} catch (err: any) { |
|
|
|
console.error("Failed to load shop detail:", err); |
|
|
|
// Handle errors gracefully - don't trigger auto-logout |
|
|
|
const errorMessage = err?.message ?? String(err) ?? "Failed to load shop details"; |
|
|
|
setError(errorMessage); |
|
|
|
} finally { |
|
|
|
setLoading(false); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
fetchShopDetail(); |
|
|
|
}, [shopId, sessionStatus, session]); |
|
|
|
|
|
|
|
const handleEdit = (index: number) => { |
|
|
|
setEditingRowIndex(index); |
|
|
|
const updated = [...truckData]; |
|
|
|
updated[index] = { ...updated[index] }; |
|
|
|
setEditedTruckData(updated); |
|
|
|
setError(null); |
|
|
|
}; |
|
|
|
|
|
|
|
const handleCancel = (index: number) => { |
|
|
|
setEditingRowIndex(null); |
|
|
|
setEditedTruckData([...truckData]); |
|
|
|
setError(null); |
|
|
|
}; |
|
|
|
|
|
|
|
const handleSave = async (index: number) => { |
|
|
|
if (!shopId) { |
|
|
|
setError("Shop ID is required"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const truck = editedTruckData[index]; |
|
|
|
if (!truck || !truck.id) { |
|
|
|
setError("Invalid truck data"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
setSaving(true); |
|
|
|
setError(null); |
|
|
|
try { |
|
|
|
// Convert departureTime to proper format if needed |
|
|
|
const departureTime = parseDepartureTimeForBackend(String(truck.departureTime || "")); |
|
|
|
|
|
|
|
await updateTruckLaneClient({ |
|
|
|
id: truck.id, |
|
|
|
truckLanceCode: String(truck.truckLanceCode || ""), |
|
|
|
departureTime: departureTime, |
|
|
|
loadingSequence: Number(truck.loadingSequence) || 0, |
|
|
|
districtReference: Number(truck.districtReference) || 0, |
|
|
|
}); |
|
|
|
|
|
|
|
// Refresh truck data after update |
|
|
|
const shopIdNum = parseInt(shopId, 10); |
|
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
|
setTruckData(trucks || []); |
|
|
|
setEditedTruckData(trucks || []); |
|
|
|
setEditingRowIndex(null); |
|
|
|
} catch (err: any) { |
|
|
|
console.error("Failed to save truck data:", err); |
|
|
|
setError(err?.message ?? String(err) ?? "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) => { |
|
|
|
if (!window.confirm("Are you sure you want to delete this truck lane?")) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (!shopId) { |
|
|
|
setError("Shop ID is required"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
setSaving(true); |
|
|
|
setError(null); |
|
|
|
try { |
|
|
|
await deleteTruckLaneClient({ id: truckId }); |
|
|
|
|
|
|
|
// Refresh truck data after delete |
|
|
|
const shopIdNum = parseInt(shopId, 10); |
|
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
|
setTruckData(trucks || []); |
|
|
|
setEditedTruckData(trucks || []); |
|
|
|
setEditingRowIndex(null); |
|
|
|
} catch (err: any) { |
|
|
|
console.error("Failed to delete truck lane:", err); |
|
|
|
setError(err?.message ?? String(err) ?? "Failed to delete truck lane"); |
|
|
|
} finally { |
|
|
|
setSaving(false); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleOpenAddDialog = () => { |
|
|
|
setNewTruck({ |
|
|
|
truckLanceCode: "", |
|
|
|
departureTime: "", |
|
|
|
loadingSequence: 0, |
|
|
|
districtReference: 0, |
|
|
|
storeId: 2, |
|
|
|
}); |
|
|
|
setAddDialogOpen(true); |
|
|
|
setError(null); |
|
|
|
}; |
|
|
|
|
|
|
|
const handleCloseAddDialog = () => { |
|
|
|
setAddDialogOpen(false); |
|
|
|
setNewTruck({ |
|
|
|
truckLanceCode: "", |
|
|
|
departureTime: "", |
|
|
|
loadingSequence: 0, |
|
|
|
districtReference: 0, |
|
|
|
storeId: 2, |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
const handleCreateTruck = async () => { |
|
|
|
// Validate all required fields |
|
|
|
const missingFields: string[] = []; |
|
|
|
|
|
|
|
if (!shopId || !shopDetail) { |
|
|
|
missingFields.push("Shop information"); |
|
|
|
} |
|
|
|
|
|
|
|
if (!newTruck.truckLanceCode.trim()) { |
|
|
|
missingFields.push("TruckLance Code"); |
|
|
|
} |
|
|
|
|
|
|
|
if (!newTruck.departureTime) { |
|
|
|
missingFields.push("Departure Time"); |
|
|
|
} |
|
|
|
|
|
|
|
if (missingFields.length > 0) { |
|
|
|
const message = `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 createTruckClient({ |
|
|
|
store_id: newTruck.storeId, |
|
|
|
truckLanceCode: newTruck.truckLanceCode.trim(), |
|
|
|
departureTime: departureTime, |
|
|
|
shopId: shopDetail!.id, |
|
|
|
shopName: String(shopDetail!.name), |
|
|
|
shopCode: String(shopDetail!.code), |
|
|
|
loadingSequence: newTruck.loadingSequence, |
|
|
|
districtReference: newTruck.districtReference, |
|
|
|
}); |
|
|
|
|
|
|
|
// Refresh truck data after create |
|
|
|
const shopIdNum = parseInt(shopId || "0", 10); |
|
|
|
const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; |
|
|
|
setTruckData(trucks || []); |
|
|
|
setEditedTruckData(trucks || []); |
|
|
|
handleCloseAddDialog(); |
|
|
|
} catch (err: any) { |
|
|
|
console.error("Failed to create truck:", err); |
|
|
|
setError(err?.message ?? String(err) ?? "Failed to create truck"); |
|
|
|
} finally { |
|
|
|
setSaving(false); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
if (loading) { |
|
|
|
return ( |
|
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> |
|
|
|
<CircularProgress /> |
|
|
|
</Box> |
|
|
|
); |
|
|
|
} |
|
|
|
if (error) { |
|
|
|
return ( |
|
|
|
<Box> |
|
|
|
<Alert severity="error" sx={{ mb: 2 }}> |
|
|
|
{error} |
|
|
|
</Alert> |
|
|
|
<Button onClick={() => router.back()}>Go Back</Button> |
|
|
|
</Box> |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
if (!shopDetail) { |
|
|
|
return ( |
|
|
|
<Box> |
|
|
|
<Alert severity="warning" sx={{ mb: 2 }}> |
|
|
|
Shop not found |
|
|
|
</Alert> |
|
|
|
<Button onClick={() => router.back()}>Go Back</Button> |
|
|
|
</Box> |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<Box> |
|
|
|
<Card sx={{ mb: 2 }}> |
|
|
|
<CardContent> |
|
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> |
|
|
|
<Typography variant="h6">Shop Information</Typography> |
|
|
|
<Button onClick={() => router.back()}>Back</Button> |
|
|
|
</Box> |
|
|
|
|
|
|
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary" fontWeight="bold">Shop ID</Typography> |
|
|
|
<Typography variant="body1" fontWeight="medium">{shopDetail.id}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Name</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.name}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Code</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.code}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Addr1</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.addr1 || "-"}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Addr2</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.addr2 || "-"}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Addr3</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.addr3 || "-"}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Contact No</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.contactNo || "-"}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Type</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.type || "-"}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Contact Email</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.contactEmail || "-"}</Typography> |
|
|
|
</Box> |
|
|
|
<Box> |
|
|
|
<Typography variant="subtitle2" color="text.secondary">Contact Name</Typography> |
|
|
|
<Typography variant="body1">{shopDetail.contactName || "-"}</Typography> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
</CardContent> |
|
|
|
</Card> |
|
|
|
|
|
|
|
<Card> |
|
|
|
<CardContent> |
|
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> |
|
|
|
<Typography variant="h6">Truck Information</Typography> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
startIcon={<AddIcon />} |
|
|
|
onClick={handleOpenAddDialog} |
|
|
|
disabled={editingRowIndex !== null || saving} |
|
|
|
> |
|
|
|
Add Truck Lane |
|
|
|
</Button> |
|
|
|
</Box> |
|
|
|
<TableContainer component={Paper}> |
|
|
|
<Table> |
|
|
|
<TableHead> |
|
|
|
<TableRow> |
|
|
|
<TableCell>TruckLance Code</TableCell> |
|
|
|
<TableCell>Departure Time</TableCell> |
|
|
|
<TableCell>Loading Sequence</TableCell> |
|
|
|
<TableCell>District Reference</TableCell> |
|
|
|
<TableCell>Store ID</TableCell> |
|
|
|
<TableCell>Actions</TableCell> |
|
|
|
</TableRow> |
|
|
|
</TableHead> |
|
|
|
<TableBody> |
|
|
|
{truckData.length === 0 ? ( |
|
|
|
<TableRow> |
|
|
|
<TableCell colSpan={6} align="center"> |
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
No Truck data available |
|
|
|
</Typography> |
|
|
|
</TableCell> |
|
|
|
</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 ? ( |
|
|
|
<TextField |
|
|
|
size="small" |
|
|
|
type="number" |
|
|
|
value={displayTruck?.storeId ?? 0} |
|
|
|
onChange={(e) => handleTruckFieldChange(index, "storeId", parseInt(e.target.value) || 0)} |
|
|
|
fullWidth |
|
|
|
/> |
|
|
|
) : ( |
|
|
|
truck.storeId !== null && truck.storeId !== undefined ? String(truck.storeId) : "-" |
|
|
|
)} |
|
|
|
</TableCell> |
|
|
|
<TableCell> |
|
|
|
<Stack direction="row" spacing={0.5}> |
|
|
|
{isEditing ? ( |
|
|
|
<> |
|
|
|
<IconButton |
|
|
|
color="primary" |
|
|
|
size="small" |
|
|
|
onClick={() => handleSave(index)} |
|
|
|
disabled={saving} |
|
|
|
title="Save changes" |
|
|
|
> |
|
|
|
<SaveIcon /> |
|
|
|
</IconButton> |
|
|
|
<IconButton |
|
|
|
color="default" |
|
|
|
size="small" |
|
|
|
onClick={() => handleCancel(index)} |
|
|
|
disabled={saving} |
|
|
|
title="Cancel editing" |
|
|
|
> |
|
|
|
<CancelIcon /> |
|
|
|
</IconButton> |
|
|
|
</> |
|
|
|
) : ( |
|
|
|
<> |
|
|
|
<IconButton |
|
|
|
color="primary" |
|
|
|
size="small" |
|
|
|
onClick={() => handleEdit(index)} |
|
|
|
disabled={editingRowIndex !== null} |
|
|
|
title="Edit truck lane" |
|
|
|
> |
|
|
|
<EditIcon /> |
|
|
|
</IconButton> |
|
|
|
{truck.id && ( |
|
|
|
<IconButton |
|
|
|
color="error" |
|
|
|
size="small" |
|
|
|
onClick={() => handleDelete(truck.id!)} |
|
|
|
disabled={saving || editingRowIndex !== null} |
|
|
|
title="Delete truck lane" |
|
|
|
> |
|
|
|
<DeleteIcon /> |
|
|
|
</IconButton> |
|
|
|
)} |
|
|
|
</> |
|
|
|
)} |
|
|
|
</Stack> |
|
|
|
</TableCell> |
|
|
|
</TableRow> |
|
|
|
); |
|
|
|
}) |
|
|
|
)} |
|
|
|
</TableBody> |
|
|
|
</Table> |
|
|
|
</TableContainer> |
|
|
|
</CardContent> |
|
|
|
</Card> |
|
|
|
|
|
|
|
{/* Add Truck Dialog */} |
|
|
|
<Dialog open={addDialogOpen} onClose={handleCloseAddDialog} maxWidth="sm" fullWidth> |
|
|
|
<DialogTitle>Add New Truck Lane</DialogTitle> |
|
|
|
<DialogContent> |
|
|
|
<Box sx={{ pt: 2 }}> |
|
|
|
<Grid container spacing={2}> |
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
label="TruckLance Code" |
|
|
|
fullWidth |
|
|
|
required |
|
|
|
value={newTruck.truckLanceCode} |
|
|
|
onChange={(e) => setNewTruck({ ...newTruck, truckLanceCode: e.target.value })} |
|
|
|
disabled={saving} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={12}> |
|
|
|
<TextField |
|
|
|
label="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 |
|
|
|
}} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={6}> |
|
|
|
<TextField |
|
|
|
label="Loading Sequence" |
|
|
|
type="number" |
|
|
|
fullWidth |
|
|
|
value={newTruck.loadingSequence} |
|
|
|
onChange={(e) => setNewTruck({ ...newTruck, loadingSequence: parseInt(e.target.value) || 0 })} |
|
|
|
disabled={saving} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={6}> |
|
|
|
<TextField |
|
|
|
label="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}> |
|
|
|
<TextField |
|
|
|
label="Store ID" |
|
|
|
type="number" |
|
|
|
fullWidth |
|
|
|
value={newTruck.storeId} |
|
|
|
onChange={(e) => setNewTruck({ ...newTruck, storeId: parseInt(e.target.value) || 2 })} |
|
|
|
disabled={saving} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
</Grid> |
|
|
|
</Box> |
|
|
|
</DialogContent> |
|
|
|
<DialogActions> |
|
|
|
<Button onClick={handleCloseAddDialog} disabled={saving}> |
|
|
|
Cancel |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
onClick={handleCreateTruck} |
|
|
|
variant="contained" |
|
|
|
startIcon={<SaveIcon />} |
|
|
|
disabled={saving} |
|
|
|
> |
|
|
|
{saving ? "Saving..." : "Save"} |
|
|
|
</Button> |
|
|
|
</DialogActions> |
|
|
|
</Dialog> |
|
|
|
|
|
|
|
{/* Snackbar for notifications */} |
|
|
|
<Snackbar |
|
|
|
open={snackbarOpen} |
|
|
|
autoHideDuration={6000} |
|
|
|
onClose={() => setSnackbarOpen(false)} |
|
|
|
anchorOrigin={{ vertical: 'top', horizontal: 'center' }} |
|
|
|
> |
|
|
|
<Alert |
|
|
|
onClose={() => setSnackbarOpen(false)} |
|
|
|
severity="warning" |
|
|
|
sx={{ width: '100%' }} |
|
|
|
> |
|
|
|
{snackbarMessage} |
|
|
|
</Alert> |
|
|
|
</Snackbar> |
|
|
|
</Box> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
export default ShopDetail; |
|
|
|
|