|
- "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,
- Autocomplete,
- } from "@mui/material";
- import DeleteIcon from "@mui/icons-material/Delete";
- import SaveIcon from "@mui/icons-material/Save";
- 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 { useState, useEffect } from "react";
- import { useSession } from "next-auth/react";
- import { useTranslation } from "react-i18next";
- import type { ShopAndTruck, Truck } from "@/app/api/shop/actions";
- import {
- fetchAllShopsClient,
- findTruckLaneByShopIdClient,
- deleteTruckLaneClient,
- createTruckClient,
- findAllUniqueTruckLaneCombinationsClient,
- } 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);
- };
-
- /** 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 { 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<ShopDetailData | null>(null);
- const [truckData, setTruckData] = useState<Truck[]>([]);
- const [loading, setLoading] = useState<boolean>(true);
- const [error, setError] = useState<string | null>(null);
- const [saving, setSaving] = useState<boolean>(false);
- const [confirmingDeleteIndex, setConfirmingDeleteIndex] = useState<number | null>(null);
- const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false);
- 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 [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(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 || []);
- } 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 handleDelete = async (truckId: number) => {
- if (!shopId) {
- setError(t("Shop ID is required"));
- return;
- }
-
- setSaving(true);
- setError(null);
- try {
- await deleteTruckLaneClient({ id: truckId });
- setConfirmingDeleteIndex(null);
-
- const shopIdNum = parseInt(shopId, 10);
- const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[];
- setTruckData(trucks || []);
- } 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 = async () => {
- setSelectedTruckLane(null);
- setAddLoadingSequence(0);
- setAddDialogOpen(true);
- 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 = () => {
- setAddDialogOpen(false);
- setSelectedTruckLane(null);
- setAddLoadingSequence(0);
- setAvailableTruckLanes([]);
- };
-
- const handleCreateTruck = async () => {
- const missingFields: string[] = [];
-
- if (!shopId || !shopDetail) {
- missingFields.push(t("Shop Information"));
- }
-
- if (!selectedTruckLane) {
- missingFields.push(t("TruckLance Code"));
- }
-
- const departureTime = selectedTruckLane
- ? departureTimeToStringForSave(selectedTruckLane.departureTime)
- : "";
- if (!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;
- }
-
- 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);
- setError(null);
- try {
- await createTruckClient({
- store_id: storeIdStr,
- truckLanceCode: String(lane.truckLanceCode || "").trim(),
- departureTime: departureTime,
- shopId: shopDetail!.id,
- shopName: String(shopDetail!.name),
- shopCode: String(shopDetail!.code),
- loadingSequence: addLoadingSequence,
- districtReference:
- lane.districtReference != null && String(lane.districtReference).trim() !== ""
- ? String(lane.districtReference)
- : null,
- remark: remarkValue,
- });
-
- // Refresh truck data after create
- const shopIdNum = parseInt(shopId || "0", 10);
- const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[];
- setTruckData(trucks || []);
-
- 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 (
- <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.push("/settings/shop?tab=0")}>{t("Back")}</Button>
- </Box>
- );
- }
-
- if (!shopDetail) {
- return (
- <Box>
- <Alert severity="warning" sx={{ mb: 2 }}>
- {t("Shop not found")}
- </Alert>
- <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button>
- </Box>
- );
- }
-
- return (
- <Box>
- <Card sx={{ mb: 2 }}>
- <CardContent>
- <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
- <Typography variant="h6">{t("Shop Information")}</Typography>
- <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button>
- </Box>
-
- <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
- <Box>
- <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">{t("Shop ID")}</Typography>
- <Typography variant="body1" fontWeight="medium">{shopDetail.id}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">{t("Name")}</Typography>
- <Typography variant="body1">{shopDetail.name}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">{t("Code")}</Typography>
- <Typography variant="body1">{shopDetail.code}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">{t("Addr1")}</Typography>
- <Typography variant="body1">{shopDetail.addr1 || "-"}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">{t("Addr2")}</Typography>
- <Typography variant="body1">{shopDetail.addr2 || "-"}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">{t("Addr3")}</Typography>
- <Typography variant="body1">{shopDetail.addr3 || "-"}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">{t("Contact No")}</Typography>
- <Typography variant="body1">{shopDetail.contactNo || "-"}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">{t("Contact Email")}</Typography>
- <Typography variant="body1">{shopDetail.contactEmail || "-"}</Typography>
- </Box>
- <Box>
- <Typography variant="subtitle2" color="text.secondary">{t("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">{t("Truck Information")}</Typography>
- <Button
- variant="contained"
- startIcon={<AddIcon />}
- onClick={handleOpenAddDialog}
- disabled={saving}
- >
- {t("Add Truck Lane")}
- </Button>
- </Box>
- <TableContainer component={Paper}>
- <Table>
- <TableHead>
- <TableRow>
- <TableCell>{t("TruckLance Code")}</TableCell>
- <TableCell>{t("Departure Time")}</TableCell>
- <TableCell>{t("Loading Sequence")}</TableCell>
- <TableCell>{t("District Reference")}</TableCell>
- <TableCell>{t("Store ID")}</TableCell>
- <TableCell>{t("Remark")}</TableCell>
- <TableCell align="right">{t("Actions")}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {truckData.length === 0 ? (
- <TableRow>
- <TableCell colSpan={7} align="center">
- <Typography variant="body2" color="text.secondary">
- {t("No Truck data available")}
- </Typography>
- </TableCell>
- </TableRow>
- ) : (
- 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
- size="small"
- color="error"
- onClick={() => handleDelete(truck.id!)}
- disabled={saving}
- title={t("Confirm delete")}
- >
- <CheckIcon />
- </IconButton>
- <IconButton
- size="small"
- color="default"
- onClick={() => setConfirmingDeleteIndex(null)}
- disabled={saving}
- title={t("Cancel")}
- >
- <CloseIcon />
- </IconButton>
- </>
- ) : (
- <IconButton
- color="error"
- size="small"
- onClick={() => setConfirmingDeleteIndex(index)}
- disabled={saving || confirmingDeleteIndex !== null}
- title={t("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>{t("Add New Truck Lane")}</DialogTitle>
- <DialogContent>
- <Box sx={{ pt: 2 }}>
- <Grid container spacing={2}>
- <Grid item xs={12}>
- <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 item xs={12}>
- <TextField
- label={t("Loading Sequence")}
- type="number"
- fullWidth
- value={addLoadingSequence}
- onChange={(e) => setAddLoadingSequence(parseInt(e.target.value, 10) || 0)}
- disabled={saving}
- />
- </Grid>
- </Grid>
- </Box>
- </DialogContent>
- <DialogActions>
- <Button onClick={handleCloseAddDialog} disabled={saving}>
- {t("Cancel")}
- </Button>
- <Button
- onClick={handleCreateTruck}
- variant="contained"
- startIcon={<SaveIcon />}
- disabled={saving}
- >
- {saving ? t("Submitting...") : t("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;
|