|
- "use client";
-
- import {
- Box,
- Card,
- CardContent,
- Typography,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TablePagination,
- TableRow,
- Paper,
- Button,
- CircularProgress,
- Alert,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
- TextField,
- Grid,
- FormControl,
- InputLabel,
- Select,
- MenuItem,
- Snackbar,
- } from "@mui/material";
- import AddIcon from "@mui/icons-material/Add";
- import SaveIcon from "@mui/icons-material/Save";
- import { useState, useMemo } from "react";
- import { useRouter } from "next/navigation";
- import { useTranslation } from "react-i18next";
- import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client";
- import type { Truck } from "@/app/api/shop/actions";
- import SearchBox, { Criterion } from "../SearchBox";
- import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil";
-
- type SearchQuery = {
- truckLanceCode: string;
- departureTime: string;
- storeId: string;
- };
-
- type SearchParamNames = keyof SearchQuery;
-
- const TruckLane: React.FC = () => {
- const { t } = useTranslation("common");
- const router = useRouter();
- const [truckData, setTruckData] = useState<Truck[]>([]);
- const [loading, setLoading] = useState<boolean>(false);
- const [error, setError] = useState<string | null>(null);
- const [filters, setFilters] = useState<Record<string, string>>({});
- const [page, setPage] = useState(0);
- const [rowsPerPage, setRowsPerPage] = useState(10);
- const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false);
- const [newTruck, setNewTruck] = useState({
- truckLanceCode: "",
- departureTime: "",
- storeId: "2F",
- });
- const [saving, setSaving] = useState<boolean>(false);
- const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false);
- const [snackbarMessage, setSnackbarMessage] = useState<string>("");
-
- // Client-side filtered rows (contains-matching)
- const filteredRows = useMemo(() => {
- const fKeys = Object.keys(filters).filter((k) => String(filters[k] ?? "").trim() !== "");
- if (fKeys.length === 0) return truckData;
-
- return truckData.filter((truck) => {
- for (const key of fKeys) {
- const filterValue = String(filters[key] ?? "").trim().toLowerCase();
-
- if (key === "truckLanceCode") {
- const truckCode = String(truck.truckLanceCode ?? "").trim().toLowerCase();
- if (!truckCode.includes(filterValue)) return false;
- } else if (key === "departureTime") {
- const formattedTime = formatDepartureTime(
- Array.isArray(truck.departureTime)
- ? truck.departureTime
- : (truck.departureTime ? String(truck.departureTime) : null)
- );
- if (!formattedTime.toLowerCase().includes(filterValue)) return false;
- } else if (key === "storeId") {
- const displayStoreId = normalizeStoreId(truck.storeId);
- if (!displayStoreId.toLowerCase().includes(filterValue)) return false;
- }
- }
- return true;
- });
- }, [truckData, filters]);
-
- // Paginated rows
- const paginatedRows = useMemo(() => {
- const startIndex = page * rowsPerPage;
- return filteredRows.slice(startIndex, startIndex + rowsPerPage);
- }, [filteredRows, page, rowsPerPage]);
-
- const handleSearch = async (inputs: Record<string, string>) => {
- setLoading(true);
- setError(null);
- try {
- const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
- const uniqueCodes = new Map<string, Truck>();
- (data || []).forEach((truck) => {
- const code = String(truck.truckLanceCode ?? "").trim();
- if (code && !uniqueCodes.has(code)) {
- uniqueCodes.set(code, truck);
- }
- });
- setTruckData(Array.from(uniqueCodes.values()));
- setFilters(inputs);
- setPage(0);
- } catch (err: any) {
- console.error("Failed to load truck lanes:", err);
- setError(err?.message ?? String(err) ?? t("Failed to load truck lanes"));
- } finally {
- setLoading(false);
- }
- };
-
- const handlePageChange = (event: unknown, newPage: number) => {
- setPage(newPage);
- };
-
- const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- setRowsPerPage(parseInt(event.target.value, 10));
- setPage(0); // Reset to first page when changing rows per page
- };
-
- const handleViewDetail = (truck: Truck) => {
- // Navigate to truck lane detail page using truckLanceCode
- const truckLanceCode = String(truck.truckLanceCode || "").trim();
- if (truckLanceCode) {
- // Use router.push with proper URL encoding
- const url = new URL(`/settings/shop/truckdetail`, window.location.origin);
- url.searchParams.set("truckLanceCode", truckLanceCode);
- router.push(url.pathname + url.search);
- }
- };
-
- const handleOpenAddDialog = () => {
- setNewTruck({
- truckLanceCode: "",
- departureTime: "",
- storeId: "2F",
- });
- setAddDialogOpen(true);
- setError(null);
- };
-
- const handleCloseAddDialog = () => {
- setAddDialogOpen(false);
- setNewTruck({
- truckLanceCode: "",
- departureTime: "",
- storeId: "2F",
- });
- };
-
- const handleCreateTruck = async () => {
- // Validate all required fields
- const missingFields: string[] = [];
-
- 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;
- }
-
- // Check if truckLanceCode already exists
- const trimmedCode = newTruck.truckLanceCode.trim();
- const existingTruck = truckData.find(
- (truck) => String(truck.truckLanceCode || "").trim().toLowerCase() === trimmedCode.toLowerCase()
- );
-
- if (existingTruck) {
- setSnackbarMessage(t("Truck lane code already exists. Please use a different code."));
- setSnackbarOpen(true);
- return;
- }
-
- setSaving(true);
- setError(null);
- try {
- await createTruckWithoutShopClient({
- store_id: newTruck.storeId,
- truckLanceCode: newTruck.truckLanceCode.trim(),
- departureTime: newTruck.departureTime.trim(),
- loadingSequence: 0,
- districtReference: null,
- remark: null,
- });
-
- // Refresh truck data after create
- const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
- const uniqueCodes = new Map<string, Truck>();
- data.forEach((truck) => {
- const code = String(truck.truckLanceCode ?? "").trim();
- if (code && !uniqueCodes.has(code)) {
- uniqueCodes.set(code, truck);
- }
- });
- setTruckData(Array.from(uniqueCodes.values()));
-
- handleCloseAddDialog();
- } catch (err: unknown) {
- console.error("Failed to create truck:", err);
- const errorMessage = err instanceof Error ? err.message : String(err);
- setError(errorMessage || t("Failed to create truck"));
- } finally {
- setSaving(false);
- }
- };
-
- const criteria: Criterion<SearchParamNames>[] = [
- { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" },
- { type: "time", label: t("Departure Time"), paramName: "departureTime" },
- { type: "text", label: t("Store ID"), paramName: "storeId" },
- ];
-
- return (
- <Box>
- <Card sx={{ mb: 2 }}>
- <CardContent>
- <SearchBox
- criteria={criteria as Criterion<string>[]}
- onSearch={handleSearch}
- onReset={() => {
- setTruckData([]);
- setFilters({});
- }}
- />
- </CardContent>
- </Card>
-
- <Card>
- <CardContent>
- <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
- <Typography variant="h6">{t("Truck Lane")}</Typography>
- <Button
- variant="contained"
- startIcon={<AddIcon />}
- onClick={handleOpenAddDialog}
- disabled={saving}
- >
- {t("Add Truck Lane")}
- </Button>
- </Box>
- {error && (
- <Alert severity="error" sx={{ mb: 2 }}>
- {error}
- </Alert>
- )}
-
- {loading ? (
- <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
- <CircularProgress />
- </Box>
- ) : (
- <TableContainer component={Paper}>
- <Table>
- <TableHead>
- <TableRow>
- <TableCell sx={{ width: "250px", minWidth: "250px", maxWidth: "250px" }}>
- {t("TruckLance Code")}
- </TableCell>
- <TableCell sx={{ width: "200px", minWidth: "200px", maxWidth: "200px" }}>
- {t("Departure Time")}
- </TableCell>
- <TableCell sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}>
- {t("Store ID")}
- </TableCell>
- <TableCell align="right" sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}>
- {t("Actions")}
- </TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {paginatedRows.length === 0 ? (
- <TableRow>
- <TableCell colSpan={4} align="center">
- <Typography variant="body2" color="text.secondary">
- {t("No Truck Lane data available")}
- </Typography>
- </TableCell>
- </TableRow>
- ) : (
- paginatedRows.map((truck) => (
- <TableRow key={truck.id ?? `truck-${truck.truckLanceCode}`}>
- <TableCell sx={{ width: "250px", minWidth: "250px", maxWidth: "250px" }}>
- {String(truck.truckLanceCode ?? "-")}
- </TableCell>
- <TableCell sx={{ width: "200px", minWidth: "200px", maxWidth: "200px" }}>
- {formatDepartureTime(
- Array.isArray(truck.departureTime)
- ? truck.departureTime
- : (truck.departureTime ? String(truck.departureTime) : null)
- )}
- </TableCell>
- <TableCell sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}>
- {normalizeStoreId(
- truck.storeId ? (typeof truck.storeId === 'string' || truck.storeId instanceof String
- ? String(truck.storeId)
- : String(truck.storeId)) : null
- )}
- </TableCell>
- <TableCell align="right" sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}>
- <Button
- size="small"
- variant="outlined"
- onClick={() => handleViewDetail(truck)}
- >
- {t("View Detail")}
- </Button>
- </TableCell>
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- <TablePagination
- component="div"
- count={filteredRows.length}
- page={page}
- onPageChange={handlePageChange}
- rowsPerPage={rowsPerPage}
- onRowsPerPageChange={handleRowsPerPageChange}
- rowsPerPageOptions={[5, 10, 25, 50]}
- />
- </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}>
- <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
- }}
- />
- </Grid>
- <Grid item xs={12}>
- <FormControl fullWidth>
- <InputLabel>{t("Store ID")}</InputLabel>
- <Select
- value={newTruck.storeId}
- label={t("Store ID")}
- onChange={(e) => {
- setNewTruck({
- ...newTruck,
- storeId: e.target.value
- });
- }}
- disabled={saving}
- >
- <MenuItem value="2F">2F</MenuItem>
- <MenuItem value="4F">4F</MenuItem>
- </Select>
- </FormControl>
- </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 TruckLane;
|