FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

439 rivejä
15 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Card,
  5. CardContent,
  6. Typography,
  7. Table,
  8. TableBody,
  9. TableCell,
  10. TableContainer,
  11. TableHead,
  12. TablePagination,
  13. TableRow,
  14. Paper,
  15. Button,
  16. CircularProgress,
  17. Alert,
  18. Dialog,
  19. DialogTitle,
  20. DialogContent,
  21. DialogActions,
  22. TextField,
  23. Grid,
  24. FormControl,
  25. InputLabel,
  26. Select,
  27. MenuItem,
  28. Snackbar,
  29. } from "@mui/material";
  30. import AddIcon from "@mui/icons-material/Add";
  31. import SaveIcon from "@mui/icons-material/Save";
  32. import { useState, useMemo } from "react";
  33. import { useRouter } from "next/navigation";
  34. import { useTranslation } from "react-i18next";
  35. import { findAllUniqueTruckLaneCombinationsClient, createTruckWithoutShopClient } from "@/app/api/shop/client";
  36. import type { Truck } from "@/app/api/shop/actions";
  37. import SearchBox, { Criterion } from "../SearchBox";
  38. import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil";
  39. type SearchQuery = {
  40. truckLanceCode: string;
  41. departureTime: string;
  42. storeId: string;
  43. };
  44. type SearchParamNames = keyof SearchQuery;
  45. const TruckLane: React.FC = () => {
  46. const { t } = useTranslation("common");
  47. const router = useRouter();
  48. const [truckData, setTruckData] = useState<Truck[]>([]);
  49. const [loading, setLoading] = useState<boolean>(false);
  50. const [error, setError] = useState<string | null>(null);
  51. const [filters, setFilters] = useState<Record<string, string>>({});
  52. const [page, setPage] = useState(0);
  53. const [rowsPerPage, setRowsPerPage] = useState(10);
  54. const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false);
  55. const [newTruck, setNewTruck] = useState({
  56. truckLanceCode: "",
  57. departureTime: "",
  58. storeId: "2F",
  59. });
  60. const [saving, setSaving] = useState<boolean>(false);
  61. const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false);
  62. const [snackbarMessage, setSnackbarMessage] = useState<string>("");
  63. // Client-side filtered rows (contains-matching)
  64. const filteredRows = useMemo(() => {
  65. const fKeys = Object.keys(filters).filter((k) => String(filters[k] ?? "").trim() !== "");
  66. if (fKeys.length === 0) return truckData;
  67. return truckData.filter((truck) => {
  68. for (const key of fKeys) {
  69. const filterValue = String(filters[key] ?? "").trim().toLowerCase();
  70. if (key === "truckLanceCode") {
  71. const truckCode = String(truck.truckLanceCode ?? "").trim().toLowerCase();
  72. if (!truckCode.includes(filterValue)) return false;
  73. } else if (key === "departureTime") {
  74. const formattedTime = formatDepartureTime(
  75. Array.isArray(truck.departureTime)
  76. ? truck.departureTime
  77. : (truck.departureTime ? String(truck.departureTime) : null)
  78. );
  79. if (!formattedTime.toLowerCase().includes(filterValue)) return false;
  80. } else if (key === "storeId") {
  81. const displayStoreId = normalizeStoreId(truck.storeId);
  82. if (!displayStoreId.toLowerCase().includes(filterValue)) return false;
  83. }
  84. }
  85. return true;
  86. });
  87. }, [truckData, filters]);
  88. // Paginated rows
  89. const paginatedRows = useMemo(() => {
  90. const startIndex = page * rowsPerPage;
  91. return filteredRows.slice(startIndex, startIndex + rowsPerPage);
  92. }, [filteredRows, page, rowsPerPage]);
  93. const handleSearch = async (inputs: Record<string, string>) => {
  94. setLoading(true);
  95. setError(null);
  96. try {
  97. const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
  98. const uniqueCodes = new Map<string, Truck>();
  99. (data || []).forEach((truck) => {
  100. const code = String(truck.truckLanceCode ?? "").trim();
  101. if (code && !uniqueCodes.has(code)) {
  102. uniqueCodes.set(code, truck);
  103. }
  104. });
  105. setTruckData(Array.from(uniqueCodes.values()));
  106. setFilters(inputs);
  107. setPage(0);
  108. } catch (err: any) {
  109. console.error("Failed to load truck lanes:", err);
  110. setError(err?.message ?? String(err) ?? t("Failed to load truck lanes"));
  111. } finally {
  112. setLoading(false);
  113. }
  114. };
  115. const handlePageChange = (event: unknown, newPage: number) => {
  116. setPage(newPage);
  117. };
  118. const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  119. setRowsPerPage(parseInt(event.target.value, 10));
  120. setPage(0); // Reset to first page when changing rows per page
  121. };
  122. const handleViewDetail = (truck: Truck) => {
  123. // Navigate to truck lane detail page using truckLanceCode
  124. const truckLanceCode = String(truck.truckLanceCode || "").trim();
  125. if (truckLanceCode) {
  126. // Use router.push with proper URL encoding
  127. const url = new URL(`/settings/shop/truckdetail`, window.location.origin);
  128. url.searchParams.set("truckLanceCode", truckLanceCode);
  129. router.push(url.pathname + url.search);
  130. }
  131. };
  132. const handleOpenAddDialog = () => {
  133. setNewTruck({
  134. truckLanceCode: "",
  135. departureTime: "",
  136. storeId: "2F",
  137. });
  138. setAddDialogOpen(true);
  139. setError(null);
  140. };
  141. const handleCloseAddDialog = () => {
  142. setAddDialogOpen(false);
  143. setNewTruck({
  144. truckLanceCode: "",
  145. departureTime: "",
  146. storeId: "2F",
  147. });
  148. };
  149. const handleCreateTruck = async () => {
  150. // Validate all required fields
  151. const missingFields: string[] = [];
  152. if (!newTruck.truckLanceCode.trim()) {
  153. missingFields.push(t("TruckLance Code"));
  154. }
  155. if (!newTruck.departureTime) {
  156. missingFields.push(t("Departure Time"));
  157. }
  158. if (missingFields.length > 0) {
  159. const message = `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`;
  160. setSnackbarMessage(message);
  161. setSnackbarOpen(true);
  162. return;
  163. }
  164. // Check if truckLanceCode already exists
  165. const trimmedCode = newTruck.truckLanceCode.trim();
  166. const existingTruck = truckData.find(
  167. (truck) => String(truck.truckLanceCode || "").trim().toLowerCase() === trimmedCode.toLowerCase()
  168. );
  169. if (existingTruck) {
  170. setSnackbarMessage(t("Truck lane code already exists. Please use a different code."));
  171. setSnackbarOpen(true);
  172. return;
  173. }
  174. setSaving(true);
  175. setError(null);
  176. try {
  177. await createTruckWithoutShopClient({
  178. store_id: newTruck.storeId,
  179. truckLanceCode: newTruck.truckLanceCode.trim(),
  180. departureTime: newTruck.departureTime.trim(),
  181. loadingSequence: 0,
  182. districtReference: null,
  183. remark: null,
  184. });
  185. // Refresh truck data after create
  186. const data = await findAllUniqueTruckLaneCombinationsClient() as Truck[];
  187. const uniqueCodes = new Map<string, Truck>();
  188. data.forEach((truck) => {
  189. const code = String(truck.truckLanceCode ?? "").trim();
  190. if (code && !uniqueCodes.has(code)) {
  191. uniqueCodes.set(code, truck);
  192. }
  193. });
  194. setTruckData(Array.from(uniqueCodes.values()));
  195. handleCloseAddDialog();
  196. } catch (err: unknown) {
  197. console.error("Failed to create truck:", err);
  198. const errorMessage = err instanceof Error ? err.message : String(err);
  199. setError(errorMessage || t("Failed to create truck"));
  200. } finally {
  201. setSaving(false);
  202. }
  203. };
  204. const criteria: Criterion<SearchParamNames>[] = [
  205. { type: "text", label: t("TruckLance Code"), paramName: "truckLanceCode" },
  206. { type: "time", label: t("Departure Time"), paramName: "departureTime" },
  207. { type: "text", label: t("Store ID"), paramName: "storeId" },
  208. ];
  209. return (
  210. <Box>
  211. <Card sx={{ mb: 2 }}>
  212. <CardContent>
  213. <SearchBox
  214. criteria={criteria as Criterion<string>[]}
  215. onSearch={handleSearch}
  216. onReset={() => {
  217. setTruckData([]);
  218. setFilters({});
  219. }}
  220. />
  221. </CardContent>
  222. </Card>
  223. <Card>
  224. <CardContent>
  225. <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
  226. <Typography variant="h6">{t("Truck Lane")}</Typography>
  227. <Button
  228. variant="contained"
  229. startIcon={<AddIcon />}
  230. onClick={handleOpenAddDialog}
  231. disabled={saving}
  232. >
  233. {t("Add Truck Lane")}
  234. </Button>
  235. </Box>
  236. {error && (
  237. <Alert severity="error" sx={{ mb: 2 }}>
  238. {error}
  239. </Alert>
  240. )}
  241. {loading ? (
  242. <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
  243. <CircularProgress />
  244. </Box>
  245. ) : (
  246. <TableContainer component={Paper}>
  247. <Table>
  248. <TableHead>
  249. <TableRow>
  250. <TableCell sx={{ width: "250px", minWidth: "250px", maxWidth: "250px" }}>
  251. {t("TruckLance Code")}
  252. </TableCell>
  253. <TableCell sx={{ width: "200px", minWidth: "200px", maxWidth: "200px" }}>
  254. {t("Departure Time")}
  255. </TableCell>
  256. <TableCell sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}>
  257. {t("Store ID")}
  258. </TableCell>
  259. <TableCell align="right" sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}>
  260. {t("Actions")}
  261. </TableCell>
  262. </TableRow>
  263. </TableHead>
  264. <TableBody>
  265. {paginatedRows.length === 0 ? (
  266. <TableRow>
  267. <TableCell colSpan={4} align="center">
  268. <Typography variant="body2" color="text.secondary">
  269. {t("No Truck Lane data available")}
  270. </Typography>
  271. </TableCell>
  272. </TableRow>
  273. ) : (
  274. paginatedRows.map((truck) => (
  275. <TableRow key={truck.id ?? `truck-${truck.truckLanceCode}`}>
  276. <TableCell sx={{ width: "250px", minWidth: "250px", maxWidth: "250px" }}>
  277. {String(truck.truckLanceCode ?? "-")}
  278. </TableCell>
  279. <TableCell sx={{ width: "200px", minWidth: "200px", maxWidth: "200px" }}>
  280. {formatDepartureTime(
  281. Array.isArray(truck.departureTime)
  282. ? truck.departureTime
  283. : (truck.departureTime ? String(truck.departureTime) : null)
  284. )}
  285. </TableCell>
  286. <TableCell sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}>
  287. {normalizeStoreId(
  288. truck.storeId ? (typeof truck.storeId === 'string' || truck.storeId instanceof String
  289. ? String(truck.storeId)
  290. : String(truck.storeId)) : null
  291. )}
  292. </TableCell>
  293. <TableCell align="right" sx={{ width: "150px", minWidth: "150px", maxWidth: "150px" }}>
  294. <Button
  295. size="small"
  296. variant="outlined"
  297. onClick={() => handleViewDetail(truck)}
  298. >
  299. {t("View Detail")}
  300. </Button>
  301. </TableCell>
  302. </TableRow>
  303. ))
  304. )}
  305. </TableBody>
  306. </Table>
  307. <TablePagination
  308. component="div"
  309. count={filteredRows.length}
  310. page={page}
  311. onPageChange={handlePageChange}
  312. rowsPerPage={rowsPerPage}
  313. onRowsPerPageChange={handleRowsPerPageChange}
  314. rowsPerPageOptions={[5, 10, 25, 50]}
  315. />
  316. </TableContainer>
  317. )}
  318. </CardContent>
  319. </Card>
  320. {/* Add Truck Dialog */}
  321. <Dialog open={addDialogOpen} onClose={handleCloseAddDialog} maxWidth="sm" fullWidth>
  322. <DialogTitle>{t("Add New Truck Lane")}</DialogTitle>
  323. <DialogContent>
  324. <Box sx={{ pt: 2 }}>
  325. <Grid container spacing={2}>
  326. <Grid item xs={12}>
  327. <TextField
  328. label={t("TruckLance Code")}
  329. fullWidth
  330. required
  331. value={newTruck.truckLanceCode}
  332. onChange={(e) => setNewTruck({ ...newTruck, truckLanceCode: e.target.value })}
  333. disabled={saving}
  334. />
  335. </Grid>
  336. <Grid item xs={12}>
  337. <TextField
  338. label={t("Departure Time")}
  339. type="time"
  340. fullWidth
  341. required
  342. value={newTruck.departureTime}
  343. onChange={(e) => setNewTruck({ ...newTruck, departureTime: e.target.value })}
  344. disabled={saving}
  345. InputLabelProps={{
  346. shrink: true,
  347. }}
  348. inputProps={{
  349. step: 300, // 5 minutes
  350. }}
  351. />
  352. </Grid>
  353. <Grid item xs={12}>
  354. <FormControl fullWidth>
  355. <InputLabel>{t("Store ID")}</InputLabel>
  356. <Select
  357. value={newTruck.storeId}
  358. label={t("Store ID")}
  359. onChange={(e) => {
  360. setNewTruck({
  361. ...newTruck,
  362. storeId: e.target.value
  363. });
  364. }}
  365. disabled={saving}
  366. >
  367. <MenuItem value="2F">2F</MenuItem>
  368. <MenuItem value="4F">4F</MenuItem>
  369. </Select>
  370. </FormControl>
  371. </Grid>
  372. </Grid>
  373. </Box>
  374. </DialogContent>
  375. <DialogActions>
  376. <Button onClick={handleCloseAddDialog} disabled={saving}>
  377. {t("Cancel")}
  378. </Button>
  379. <Button
  380. onClick={handleCreateTruck}
  381. variant="contained"
  382. startIcon={<SaveIcon />}
  383. disabled={saving}
  384. >
  385. {saving ? t("Submitting...") : t("Save")}
  386. </Button>
  387. </DialogActions>
  388. </Dialog>
  389. {/* Snackbar for notifications */}
  390. <Snackbar
  391. open={snackbarOpen}
  392. autoHideDuration={6000}
  393. onClose={() => setSnackbarOpen(false)}
  394. anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
  395. >
  396. <Alert
  397. onClose={() => setSnackbarOpen(false)}
  398. severity="warning"
  399. sx={{ width: '100%' }}
  400. >
  401. {snackbarMessage}
  402. </Alert>
  403. </Snackbar>
  404. </Box>
  405. );
  406. };
  407. export default TruckLane;