"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, Select, MenuItem, FormControl, InputLabel, Autocomplete, } 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 { useTranslation } from "react-i18next"; 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"; import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; 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 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 { t } = useTranslation("common"); 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(null); const [truckData, setTruckData] = useState([]); const [editedTruckData, setEditedTruckData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingRowIndex, setEditingRowIndex] = useState(null); const [saving, setSaving] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false); const [newTruck, setNewTruck] = useState({ truckLanceCode: "", departureTime: "", loadingSequence: 0, districtReference: 0, storeId: "2F", remark: "", }); const [uniqueRemarks, setUniqueRemarks] = useState([]); const [snackbarOpen, setSnackbarOpen] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(""); 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(t("Please log in to view shop details")); setLoading(false); return; } const fetchShopDetail = async () => { if (!shopId) { setError(t("Shop ID is required")); setLoading(false); return; } // Convert shopId to number for proper filtering const shopIdNum = parseInt(shopId, 10); if (isNaN(shopIdNum)) { setError(t("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(t("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); // 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) { console.error("Failed to load shop detail:", err); // Handle errors gracefully - don't trigger auto-logout const errorMessage = err?.message ?? String(err) ?? t("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] }; // 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) => { if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) { return; } if (!shopId) { setError(t("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) ?? t("Failed to delete truck lane")); } finally { setSaving(false); } }; const handleOpenAddDialog = () => { setNewTruck({ truckLanceCode: "", departureTime: "", loadingSequence: 0, districtReference: 0, storeId: "2F", remark: "", }); setAddDialogOpen(true); setError(null); }; const handleCloseAddDialog = () => { setAddDialogOpen(false); setNewTruck({ truckLanceCode: "", departureTime: "", loadingSequence: 0, districtReference: 0, storeId: "2F", remark: "", }); }; const handleCreateTruck = async () => { // Validate all required fields const missingFields: string[] = []; if (!shopId || !shopDetail) { missingFields.push(t("Shop Information")); } 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 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, remark: newTruck.storeId === "4F" ? (newTruck.remark?.trim() || null) : null, }); // Refresh truck data after create const shopIdNum = parseInt(shopId || "0", 10); const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[]; 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(); } 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 ( ); } if (error) { return ( {error} ); } if (!shopDetail) { return ( {t("Shop not found")} ); } return ( {t("Shop Information")} {t("Shop ID")} {shopDetail.id} {t("Name")} {shopDetail.name} {t("Code")} {shopDetail.code} {t("Addr1")} {shopDetail.addr1 || "-"} {t("Addr2")} {shopDetail.addr2 || "-"} {t("Addr3")} {shopDetail.addr3 || "-"} {t("Contact No")} {shopDetail.contactNo || "-"} {t("Contact Email")} {shopDetail.contactEmail || "-"} {t("Contact Name")} {shopDetail.contactName || "-"} {t("Truck Information")} {t("TruckLance Code")} {t("Departure Time")} {t("Loading Sequence")} {t("District Reference")} {t("Store ID")} {t("Remark")} {t("Actions")} {truckData.length === 0 ? ( {t("No Truck data available")} ) : ( truckData.map((truck, index) => { const isEditing = editingRowIndex === index; const displayTruck = isEditing ? editedTruckData[index] : truck; return ( {isEditing ? ( handleTruckFieldChange(index, "truckLanceCode", e.target.value)} fullWidth /> ) : ( String(truck.truckLanceCode || "-") )} {isEditing ? ( { 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) ) )} {isEditing ? ( handleTruckFieldChange(index, "loadingSequence", parseInt(e.target.value) || 0)} fullWidth /> ) : ( truck.loadingSequence !== null && truck.loadingSequence !== undefined ? String(truck.loadingSequence) : "-" )} {isEditing ? ( handleTruckFieldChange(index, "districtReference", parseInt(e.target.value) || 0)} fullWidth /> ) : ( truck.districtReference !== null && truck.districtReference !== undefined ? String(truck.districtReference) : "-" )} {isEditing ? ( ) : ( normalizeStoreId(truck.storeId) )} {isEditing ? ( (() => { const storeIdStr = normalizeStoreId(displayTruck?.storeId) || "2F"; const isEditable = storeIdStr === "4F"; return ( { 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) => ( )} /> ); })() ) : ( String(truck.remark || "-") )} {isEditing ? ( <> handleSave(index)} disabled={saving} title={t("Save changes")} > handleCancel(index)} disabled={saving} title={t("Cancel editing")} > ) : ( <> handleEdit(index)} disabled={editingRowIndex !== null} title={t("Edit truck lane")} > {truck.id && ( handleDelete(truck.id!)} disabled={saving || editingRowIndex !== null} title={t("Delete truck lane")} > )} )} ); }) )}
{/* 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 }} /> setNewTruck({ ...newTruck, loadingSequence: parseInt(e.target.value) || 0 })} disabled={saving} /> setNewTruck({ ...newTruck, districtReference: parseInt(e.target.value) || 0 })} disabled={saving} /> {t("Store ID")} {newTruck.storeId === "4F" && ( { setNewTruck({ ...newTruck, remark: newValue || "" }); }} onInputChange={(event, newInputValue) => { setNewTruck({ ...newTruck, remark: newInputValue }); }} renderInput={(params) => ( )} /> )} {/* Snackbar for notifications */} setSnackbarOpen(false)} anchorOrigin={{ vertical: 'top', horizontal: 'center' }} > setSnackbarOpen(false)} severity="warning" sx={{ width: '100%' }} > {snackbarMessage}
); }; export default ShopDetail;