FPSMS-frontend
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

ShopDetail.tsx 32 KiB

hace 1 mes
hace 1 mes
hace 1 mes
hace 1 mes
hace 1 mes
hace 1 mes
hace 1 mes
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  1. "use client";
  2. import {
  3. Box,
  4. Card,
  5. CardContent,
  6. Typography,
  7. CircularProgress,
  8. Alert,
  9. Button,
  10. Table,
  11. TableBody,
  12. TableCell,
  13. TableContainer,
  14. TableHead,
  15. TableRow,
  16. Paper,
  17. TextField,
  18. Stack,
  19. IconButton,
  20. Dialog,
  21. DialogTitle,
  22. DialogContent,
  23. DialogActions,
  24. Grid,
  25. Snackbar,
  26. Select,
  27. MenuItem,
  28. FormControl,
  29. InputLabel,
  30. Autocomplete,
  31. } from "@mui/material";
  32. import DeleteIcon from "@mui/icons-material/Delete";
  33. import EditIcon from "@mui/icons-material/Edit";
  34. import SaveIcon from "@mui/icons-material/Save";
  35. import CancelIcon from "@mui/icons-material/Cancel";
  36. import AddIcon from "@mui/icons-material/Add";
  37. import { useRouter, useSearchParams } from "next/navigation";
  38. import { useState, useEffect } from "react";
  39. import { useSession } from "next-auth/react";
  40. import { useTranslation } from "react-i18next";
  41. import type { Shop, ShopAndTruck, Truck } from "@/app/api/shop/actions";
  42. import {
  43. fetchAllShopsClient,
  44. findTruckLaneByShopIdClient,
  45. updateTruckLaneClient,
  46. deleteTruckLaneClient,
  47. createTruckClient
  48. } from "@/app/api/shop/client";
  49. import type { SessionWithTokens } from "@/config/authConfig";
  50. import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil";
  51. type ShopDetailData = {
  52. id: number;
  53. name: String;
  54. code: String;
  55. addr1: String;
  56. addr2: String;
  57. addr3: String;
  58. contactNo: number;
  59. type: String;
  60. contactEmail: String;
  61. contactName: String;
  62. };
  63. // Utility function to convert HH:mm format to the format expected by backend
  64. const parseDepartureTimeForBackend = (time: string): string => {
  65. if (!time) return "";
  66. const timeStr = String(time).trim();
  67. // If already in HH:mm format, return as is
  68. if (/^\d{1,2}:\d{2}$/.test(timeStr)) {
  69. return timeStr;
  70. }
  71. // Try to format it
  72. return formatDepartureTime(timeStr);
  73. };
  74. const ShopDetail: React.FC = () => {
  75. const { t } = useTranslation("common");
  76. const router = useRouter();
  77. const searchParams = useSearchParams();
  78. const shopId = searchParams.get("id");
  79. const { data: session, status: sessionStatus } = useSession() as { data: SessionWithTokens | null; status: string };
  80. const [shopDetail, setShopDetail] = useState<ShopDetailData | null>(null);
  81. const [truckData, setTruckData] = useState<Truck[]>([]);
  82. const [editedTruckData, setEditedTruckData] = useState<Truck[]>([]);
  83. const [loading, setLoading] = useState<boolean>(true);
  84. const [error, setError] = useState<string | null>(null);
  85. const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
  86. const [saving, setSaving] = useState<boolean>(false);
  87. const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false);
  88. const [newTruck, setNewTruck] = useState({
  89. truckLanceCode: "",
  90. departureTime: "",
  91. loadingSequence: 0,
  92. districtReference: 0,
  93. storeId: "2F",
  94. remark: "",
  95. });
  96. const [uniqueRemarks, setUniqueRemarks] = useState<string[]>([]);
  97. const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false);
  98. const [snackbarMessage, setSnackbarMessage] = useState<string>("");
  99. useEffect(() => {
  100. // Wait for session to be ready before making API calls
  101. if (sessionStatus === "loading") {
  102. return; // Still loading session
  103. }
  104. // If session is unauthenticated, don't make API calls (middleware will handle redirect)
  105. if (sessionStatus === "unauthenticated" || !session) {
  106. setError(t("Please log in to view shop details"));
  107. setLoading(false);
  108. return;
  109. }
  110. const fetchShopDetail = async () => {
  111. if (!shopId) {
  112. setError(t("Shop ID is required"));
  113. setLoading(false);
  114. return;
  115. }
  116. // Convert shopId to number for proper filtering
  117. const shopIdNum = parseInt(shopId, 10);
  118. if (isNaN(shopIdNum)) {
  119. setError(t("Invalid Shop ID"));
  120. setLoading(false);
  121. return;
  122. }
  123. setLoading(true);
  124. setError(null);
  125. try {
  126. // Fetch shop information - try with ID parameter first, then filter if needed
  127. let shopDataResponse = await fetchAllShopsClient({ id: shopIdNum }) as ShopAndTruck[];
  128. // If no results with ID parameter, fetch all and filter client-side
  129. if (!shopDataResponse || shopDataResponse.length === 0) {
  130. shopDataResponse = await fetchAllShopsClient() as ShopAndTruck[];
  131. }
  132. // Filter to find the shop with matching ID (in case API doesn't filter properly)
  133. const shopData = shopDataResponse?.find((item) => item.id === shopIdNum);
  134. if (shopData) {
  135. // Set shop detail info
  136. setShopDetail({
  137. id: shopData.id ?? 0,
  138. name: shopData.name ?? "",
  139. code: shopData.code ?? "",
  140. addr1: shopData.addr1 ?? "",
  141. addr2: shopData.addr2 ?? "",
  142. addr3: shopData.addr3 ?? "",
  143. contactNo: shopData.contactNo ?? 0,
  144. type: shopData.type ?? "",
  145. contactEmail: shopData.contactEmail ?? "",
  146. contactName: shopData.contactName ?? "",
  147. });
  148. } else {
  149. setError(t("Shop not found"));
  150. setLoading(false);
  151. return;
  152. }
  153. // Fetch truck information using the Truck interface with numeric ID
  154. const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[];
  155. setTruckData(trucks || []);
  156. setEditedTruckData(trucks || []);
  157. setEditingRowIndex(null);
  158. // Extract unique remarks from trucks for this shop
  159. const remarks = trucks
  160. ?.map(t => t.remark)
  161. .filter((remark): remark is string => remark != null && String(remark).trim() !== "")
  162. .map(r => String(r).trim())
  163. .filter((value, index, self) => self.indexOf(value) === index) || [];
  164. setUniqueRemarks(remarks);
  165. } catch (err: any) {
  166. console.error("Failed to load shop detail:", err);
  167. // Handle errors gracefully - don't trigger auto-logout
  168. const errorMessage = err?.message ?? String(err) ?? t("Failed to load shop details");
  169. setError(errorMessage);
  170. } finally {
  171. setLoading(false);
  172. }
  173. };
  174. fetchShopDetail();
  175. }, [shopId, sessionStatus, session]);
  176. const handleEdit = (index: number) => {
  177. setEditingRowIndex(index);
  178. const updated = [...truckData];
  179. updated[index] = { ...updated[index] };
  180. // Normalize departureTime to HH:mm format for editing
  181. if (updated[index].departureTime) {
  182. const timeValue = updated[index].departureTime;
  183. const formatted = formatDepartureTime(
  184. Array.isArray(timeValue) ? timeValue : (timeValue ? String(timeValue) : null)
  185. );
  186. if (formatted !== "-") {
  187. updated[index].departureTime = formatted;
  188. }
  189. }
  190. // Ensure remark is initialized as string (not null/undefined)
  191. if (updated[index].remark == null) {
  192. updated[index].remark = "";
  193. }
  194. setEditedTruckData(updated);
  195. setError(null);
  196. };
  197. const handleCancel = (index: number) => {
  198. setEditingRowIndex(null);
  199. setEditedTruckData([...truckData]);
  200. setError(null);
  201. };
  202. const handleSave = async (index: number) => {
  203. if (!shopId) {
  204. setError(t("Shop ID is required"));
  205. return;
  206. }
  207. const truck = editedTruckData[index];
  208. if (!truck || !truck.id) {
  209. setError(t("Invalid shop data"));
  210. return;
  211. }
  212. setSaving(true);
  213. setError(null);
  214. try {
  215. // Use the departureTime from editedTruckData which is already in HH:mm format from the input field
  216. // If it's already a valid HH:mm string, use it directly; otherwise format it
  217. let departureTime = String(truck.departureTime || "").trim();
  218. if (!departureTime || departureTime === "-") {
  219. departureTime = "";
  220. } else if (!/^\d{1,2}:\d{2}$/.test(departureTime)) {
  221. // Only convert if it's not already in HH:mm format
  222. departureTime = parseDepartureTimeForBackend(departureTime);
  223. }
  224. // Convert storeId to string format (2F or 4F)
  225. const storeIdStr = normalizeStoreId(truck.storeId) || "2F";
  226. // Get remark value - use the remark from editedTruckData (user input)
  227. // Only send remark if storeId is "4F", otherwise send null
  228. let remarkValue: string | null = null;
  229. if (storeIdStr === "4F") {
  230. const remark = truck.remark;
  231. if (remark != null && String(remark).trim() !== "") {
  232. remarkValue = String(remark).trim();
  233. }
  234. }
  235. await updateTruckLaneClient({
  236. id: truck.id,
  237. truckLanceCode: String(truck.truckLanceCode || ""),
  238. departureTime: departureTime,
  239. loadingSequence: Number(truck.loadingSequence) || 0,
  240. districtReference: Number(truck.districtReference) || 0,
  241. storeId: storeIdStr,
  242. remark: remarkValue,
  243. });
  244. // Refresh truck data after update
  245. const shopIdNum = parseInt(shopId, 10);
  246. const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[];
  247. setTruckData(trucks || []);
  248. setEditedTruckData(trucks || []);
  249. setEditingRowIndex(null);
  250. // Update unique remarks
  251. const remarks = trucks
  252. ?.map(t => t.remark)
  253. .filter((remark): remark is string => remark != null && String(remark).trim() !== "")
  254. .map(r => String(r).trim())
  255. .filter((value, index, self) => self.indexOf(value) === index) || [];
  256. setUniqueRemarks(remarks);
  257. } catch (err: any) {
  258. console.error("Failed to save truck data:", err);
  259. setError(err?.message ?? String(err) ?? t("Failed to save truck data"));
  260. } finally {
  261. setSaving(false);
  262. }
  263. };
  264. const handleTruckFieldChange = (index: number, field: keyof Truck, value: string | number) => {
  265. const updated = [...editedTruckData];
  266. updated[index] = {
  267. ...updated[index],
  268. [field]: value,
  269. };
  270. setEditedTruckData(updated);
  271. };
  272. const handleDelete = async (truckId: number) => {
  273. if (!window.confirm(t("Are you sure you want to delete this truck lane?"))) {
  274. return;
  275. }
  276. if (!shopId) {
  277. setError(t("Shop ID is required"));
  278. return;
  279. }
  280. setSaving(true);
  281. setError(null);
  282. try {
  283. await deleteTruckLaneClient({ id: truckId });
  284. // Refresh truck data after delete
  285. const shopIdNum = parseInt(shopId, 10);
  286. const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[];
  287. setTruckData(trucks || []);
  288. setEditedTruckData(trucks || []);
  289. setEditingRowIndex(null);
  290. } catch (err: any) {
  291. console.error("Failed to delete truck lane:", err);
  292. setError(err?.message ?? String(err) ?? t("Failed to delete truck lane"));
  293. } finally {
  294. setSaving(false);
  295. }
  296. };
  297. const handleOpenAddDialog = () => {
  298. setNewTruck({
  299. truckLanceCode: "",
  300. departureTime: "",
  301. loadingSequence: 0,
  302. districtReference: 0,
  303. storeId: "2F",
  304. remark: "",
  305. });
  306. setAddDialogOpen(true);
  307. setError(null);
  308. };
  309. const handleCloseAddDialog = () => {
  310. setAddDialogOpen(false);
  311. setNewTruck({
  312. truckLanceCode: "",
  313. departureTime: "",
  314. loadingSequence: 0,
  315. districtReference: 0,
  316. storeId: "2F",
  317. remark: "",
  318. });
  319. };
  320. const handleCreateTruck = async () => {
  321. // Validate all required fields
  322. const missingFields: string[] = [];
  323. if (!shopId || !shopDetail) {
  324. missingFields.push(t("Shop Information"));
  325. }
  326. if (!newTruck.truckLanceCode.trim()) {
  327. missingFields.push(t("TruckLance Code"));
  328. }
  329. if (!newTruck.departureTime) {
  330. missingFields.push(t("Departure Time"));
  331. }
  332. if (missingFields.length > 0) {
  333. const message = `${t("Please fill in the following required fields:")} ${missingFields.join(", ")}`;
  334. setSnackbarMessage(message);
  335. setSnackbarOpen(true);
  336. return;
  337. }
  338. setSaving(true);
  339. setError(null);
  340. try {
  341. const departureTime = parseDepartureTimeForBackend(newTruck.departureTime);
  342. await createTruckClient({
  343. store_id: newTruck.storeId,
  344. truckLanceCode: newTruck.truckLanceCode.trim(),
  345. departureTime: departureTime,
  346. shopId: shopDetail!.id,
  347. shopName: String(shopDetail!.name),
  348. shopCode: String(shopDetail!.code),
  349. loadingSequence: newTruck.loadingSequence,
  350. districtReference: newTruck.districtReference,
  351. remark: newTruck.storeId === "4F" ? (newTruck.remark?.trim() || null) : null,
  352. });
  353. // Refresh truck data after create
  354. const shopIdNum = parseInt(shopId || "0", 10);
  355. const trucks = await findTruckLaneByShopIdClient(shopIdNum) as Truck[];
  356. setTruckData(trucks || []);
  357. setEditedTruckData(trucks || []);
  358. // Update unique remarks
  359. const remarks = trucks
  360. ?.map(t => t.remark)
  361. .filter((remark): remark is string => remark != null && String(remark).trim() !== "")
  362. .map(r => String(r).trim())
  363. .filter((value, index, self) => self.indexOf(value) === index) || [];
  364. setUniqueRemarks(remarks);
  365. handleCloseAddDialog();
  366. } catch (err: any) {
  367. console.error("Failed to create truck:", err);
  368. setError(err?.message ?? String(err) ?? t("Failed to create truck"));
  369. } finally {
  370. setSaving(false);
  371. }
  372. };
  373. if (loading) {
  374. return (
  375. <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
  376. <CircularProgress />
  377. </Box>
  378. );
  379. }
  380. if (error) {
  381. return (
  382. <Box>
  383. <Alert severity="error" sx={{ mb: 2 }}>
  384. {error}
  385. </Alert>
  386. <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button>
  387. </Box>
  388. );
  389. }
  390. if (!shopDetail) {
  391. return (
  392. <Box>
  393. <Alert severity="warning" sx={{ mb: 2 }}>
  394. {t("Shop not found")}
  395. </Alert>
  396. <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button>
  397. </Box>
  398. );
  399. }
  400. return (
  401. <Box>
  402. <Card sx={{ mb: 2 }}>
  403. <CardContent>
  404. <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
  405. <Typography variant="h6">{t("Shop Information")}</Typography>
  406. <Button onClick={() => router.push("/settings/shop?tab=0")}>{t("Back")}</Button>
  407. </Box>
  408. <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
  409. <Box>
  410. <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">{t("Shop ID")}</Typography>
  411. <Typography variant="body1" fontWeight="medium">{shopDetail.id}</Typography>
  412. </Box>
  413. <Box>
  414. <Typography variant="subtitle2" color="text.secondary">{t("Name")}</Typography>
  415. <Typography variant="body1">{shopDetail.name}</Typography>
  416. </Box>
  417. <Box>
  418. <Typography variant="subtitle2" color="text.secondary">{t("Code")}</Typography>
  419. <Typography variant="body1">{shopDetail.code}</Typography>
  420. </Box>
  421. <Box>
  422. <Typography variant="subtitle2" color="text.secondary">{t("Addr1")}</Typography>
  423. <Typography variant="body1">{shopDetail.addr1 || "-"}</Typography>
  424. </Box>
  425. <Box>
  426. <Typography variant="subtitle2" color="text.secondary">{t("Addr2")}</Typography>
  427. <Typography variant="body1">{shopDetail.addr2 || "-"}</Typography>
  428. </Box>
  429. <Box>
  430. <Typography variant="subtitle2" color="text.secondary">{t("Addr3")}</Typography>
  431. <Typography variant="body1">{shopDetail.addr3 || "-"}</Typography>
  432. </Box>
  433. <Box>
  434. <Typography variant="subtitle2" color="text.secondary">{t("Contact No")}</Typography>
  435. <Typography variant="body1">{shopDetail.contactNo || "-"}</Typography>
  436. </Box>
  437. <Box>
  438. <Typography variant="subtitle2" color="text.secondary">{t("Contact Email")}</Typography>
  439. <Typography variant="body1">{shopDetail.contactEmail || "-"}</Typography>
  440. </Box>
  441. <Box>
  442. <Typography variant="subtitle2" color="text.secondary">{t("Contact Name")}</Typography>
  443. <Typography variant="body1">{shopDetail.contactName || "-"}</Typography>
  444. </Box>
  445. </Box>
  446. </CardContent>
  447. </Card>
  448. <Card>
  449. <CardContent>
  450. <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
  451. <Typography variant="h6">{t("Truck Information")}</Typography>
  452. <Button
  453. variant="contained"
  454. startIcon={<AddIcon />}
  455. onClick={handleOpenAddDialog}
  456. disabled={editingRowIndex !== null || saving}
  457. >
  458. {t("Add Truck Lane")}
  459. </Button>
  460. </Box>
  461. <TableContainer component={Paper}>
  462. <Table>
  463. <TableHead>
  464. <TableRow>
  465. <TableCell>{t("TruckLance Code")}</TableCell>
  466. <TableCell>{t("Departure Time")}</TableCell>
  467. <TableCell>{t("Loading Sequence")}</TableCell>
  468. <TableCell>{t("District Reference")}</TableCell>
  469. <TableCell>{t("Store ID")}</TableCell>
  470. <TableCell>{t("Remark")}</TableCell>
  471. <TableCell>{t("Actions")}</TableCell>
  472. </TableRow>
  473. </TableHead>
  474. <TableBody>
  475. {truckData.length === 0 ? (
  476. <TableRow>
  477. <TableCell colSpan={7} align="center">
  478. <Typography variant="body2" color="text.secondary">
  479. {t("No Truck data available")}
  480. </Typography>
  481. </TableCell>
  482. </TableRow>
  483. ) : (
  484. truckData.map((truck, index) => {
  485. const isEditing = editingRowIndex === index;
  486. const displayTruck = isEditing ? editedTruckData[index] : truck;
  487. return (
  488. <TableRow key={truck.id ?? `truck-${index}`}>
  489. <TableCell>
  490. {isEditing ? (
  491. <TextField
  492. size="small"
  493. value={String(displayTruck?.truckLanceCode || "")}
  494. onChange={(e) => handleTruckFieldChange(index, "truckLanceCode", e.target.value)}
  495. fullWidth
  496. />
  497. ) : (
  498. String(truck.truckLanceCode || "-")
  499. )}
  500. </TableCell>
  501. <TableCell>
  502. {isEditing ? (
  503. <TextField
  504. size="small"
  505. type="time"
  506. value={(() => {
  507. const timeValue = displayTruck?.departureTime;
  508. const formatted = formatDepartureTime(
  509. Array.isArray(timeValue) ? timeValue : (timeValue ? String(timeValue) : null)
  510. );
  511. return formatted !== "-" ? formatted : "";
  512. })()}
  513. onChange={(e) => handleTruckFieldChange(index, "departureTime", e.target.value)}
  514. fullWidth
  515. InputLabelProps={{
  516. shrink: true,
  517. }}
  518. inputProps={{
  519. step: 300, // 5 minutes
  520. }}
  521. />
  522. ) : (
  523. formatDepartureTime(
  524. Array.isArray(truck.departureTime) ? truck.departureTime : (truck.departureTime ? String(truck.departureTime) : null)
  525. )
  526. )}
  527. </TableCell>
  528. <TableCell>
  529. {isEditing ? (
  530. <TextField
  531. size="small"
  532. type="number"
  533. value={displayTruck?.loadingSequence ?? 0}
  534. onChange={(e) => handleTruckFieldChange(index, "loadingSequence", parseInt(e.target.value) || 0)}
  535. fullWidth
  536. />
  537. ) : (
  538. truck.loadingSequence !== null && truck.loadingSequence !== undefined ? String(truck.loadingSequence) : "-"
  539. )}
  540. </TableCell>
  541. <TableCell>
  542. {isEditing ? (
  543. <TextField
  544. size="small"
  545. type="number"
  546. value={displayTruck?.districtReference ?? 0}
  547. onChange={(e) => handleTruckFieldChange(index, "districtReference", parseInt(e.target.value) || 0)}
  548. fullWidth
  549. />
  550. ) : (
  551. truck.districtReference !== null && truck.districtReference !== undefined ? String(truck.districtReference) : "-"
  552. )}
  553. </TableCell>
  554. <TableCell>
  555. {isEditing ? (
  556. <FormControl size="small" fullWidth>
  557. <Select
  558. value={(() => {
  559. const storeId = displayTruck?.storeId;
  560. if (!storeId) return "2F";
  561. const storeIdStr = typeof storeId === 'string' ? storeId : String(storeId);
  562. // Convert numeric values to string format
  563. if (storeIdStr === "2" || storeIdStr === "2F") return "2F";
  564. if (storeIdStr === "4" || storeIdStr === "4F") return "4F";
  565. return storeIdStr;
  566. })()}
  567. onChange={(e) => {
  568. const newStoreId = e.target.value;
  569. handleTruckFieldChange(index, "storeId", newStoreId);
  570. // Clear remark if storeId changes from 4F to something else
  571. if (newStoreId !== "4F") {
  572. handleTruckFieldChange(index, "remark", "");
  573. }
  574. }}
  575. >
  576. <MenuItem value="2F">2F</MenuItem>
  577. <MenuItem value="4F">4F</MenuItem>
  578. </Select>
  579. </FormControl>
  580. ) : (
  581. normalizeStoreId(truck.storeId)
  582. )}
  583. </TableCell>
  584. <TableCell>
  585. {isEditing ? (
  586. (() => {
  587. const storeIdStr = normalizeStoreId(displayTruck?.storeId) || "2F";
  588. const isEditable = storeIdStr === "4F";
  589. return (
  590. <Autocomplete
  591. freeSolo
  592. size="small"
  593. disabled={!isEditable}
  594. options={uniqueRemarks}
  595. value={displayTruck?.remark ? String(displayTruck.remark) : ""}
  596. onChange={(event, newValue) => {
  597. if (isEditable) {
  598. const remarkValue = typeof newValue === 'string' ? newValue : (newValue || "");
  599. handleTruckFieldChange(index, "remark", remarkValue);
  600. }
  601. }}
  602. onInputChange={(event, newInputValue, reason) => {
  603. // Only update on user input, not when clearing or selecting
  604. if (isEditable && (reason === 'input' || reason === 'clear')) {
  605. handleTruckFieldChange(index, "remark", newInputValue);
  606. }
  607. }}
  608. renderInput={(params) => (
  609. <TextField
  610. {...params}
  611. fullWidth
  612. placeholder={isEditable ? t("Enter or select remark") : t("Not editable for this Store ID")}
  613. disabled={!isEditable}
  614. />
  615. )}
  616. />
  617. );
  618. })()
  619. ) : (
  620. String(truck.remark || "-")
  621. )}
  622. </TableCell>
  623. <TableCell>
  624. <Stack direction="row" spacing={0.5}>
  625. {isEditing ? (
  626. <>
  627. <IconButton
  628. color="primary"
  629. size="small"
  630. onClick={() => handleSave(index)}
  631. disabled={saving}
  632. title={t("Save changes")}
  633. >
  634. <SaveIcon />
  635. </IconButton>
  636. <IconButton
  637. color="default"
  638. size="small"
  639. onClick={() => handleCancel(index)}
  640. disabled={saving}
  641. title={t("Cancel editing")}
  642. >
  643. <CancelIcon />
  644. </IconButton>
  645. </>
  646. ) : (
  647. <>
  648. <IconButton
  649. color="primary"
  650. size="small"
  651. onClick={() => handleEdit(index)}
  652. disabled={editingRowIndex !== null}
  653. title={t("Edit truck lane")}
  654. >
  655. <EditIcon />
  656. </IconButton>
  657. {truck.id && (
  658. <IconButton
  659. color="error"
  660. size="small"
  661. onClick={() => handleDelete(truck.id!)}
  662. disabled={saving || editingRowIndex !== null}
  663. title={t("Delete truck lane")}
  664. >
  665. <DeleteIcon />
  666. </IconButton>
  667. )}
  668. </>
  669. )}
  670. </Stack>
  671. </TableCell>
  672. </TableRow>
  673. );
  674. })
  675. )}
  676. </TableBody>
  677. </Table>
  678. </TableContainer>
  679. </CardContent>
  680. </Card>
  681. {/* Add Truck Dialog */}
  682. <Dialog open={addDialogOpen} onClose={handleCloseAddDialog} maxWidth="sm" fullWidth>
  683. <DialogTitle>{t("Add New Truck Lane")}</DialogTitle>
  684. <DialogContent>
  685. <Box sx={{ pt: 2 }}>
  686. <Grid container spacing={2}>
  687. <Grid item xs={12}>
  688. <TextField
  689. label={t("TruckLance Code")}
  690. fullWidth
  691. required
  692. value={newTruck.truckLanceCode}
  693. onChange={(e) => setNewTruck({ ...newTruck, truckLanceCode: e.target.value })}
  694. disabled={saving}
  695. />
  696. </Grid>
  697. <Grid item xs={12}>
  698. <TextField
  699. label={t("Departure Time")}
  700. type="time"
  701. fullWidth
  702. required
  703. value={newTruck.departureTime}
  704. onChange={(e) => setNewTruck({ ...newTruck, departureTime: e.target.value })}
  705. disabled={saving}
  706. InputLabelProps={{
  707. shrink: true,
  708. }}
  709. inputProps={{
  710. step: 300, // 5 minutes
  711. }}
  712. />
  713. </Grid>
  714. <Grid item xs={6}>
  715. <TextField
  716. label={t("Loading Sequence")}
  717. type="number"
  718. fullWidth
  719. value={newTruck.loadingSequence}
  720. onChange={(e) => setNewTruck({ ...newTruck, loadingSequence: parseInt(e.target.value) || 0 })}
  721. disabled={saving}
  722. />
  723. </Grid>
  724. <Grid item xs={6}>
  725. <TextField
  726. label={t("District Reference")}
  727. type="number"
  728. fullWidth
  729. value={newTruck.districtReference}
  730. onChange={(e) => setNewTruck({ ...newTruck, districtReference: parseInt(e.target.value) || 0 })}
  731. disabled={saving}
  732. />
  733. </Grid>
  734. <Grid item xs={6}>
  735. <FormControl fullWidth>
  736. <InputLabel>{t("Store ID")}</InputLabel>
  737. <Select
  738. value={newTruck.storeId}
  739. label={t("Store ID")}
  740. onChange={(e) => {
  741. const newStoreId = e.target.value;
  742. setNewTruck({
  743. ...newTruck,
  744. storeId: newStoreId,
  745. remark: newStoreId === "4F" ? newTruck.remark : ""
  746. });
  747. }}
  748. disabled={saving}
  749. >
  750. <MenuItem value="2F">2F</MenuItem>
  751. <MenuItem value="4F">4F</MenuItem>
  752. </Select>
  753. </FormControl>
  754. </Grid>
  755. {newTruck.storeId === "4F" && (
  756. <Grid item xs={12}>
  757. <Autocomplete
  758. freeSolo
  759. options={uniqueRemarks}
  760. value={newTruck.remark || ""}
  761. onChange={(event, newValue) => {
  762. setNewTruck({ ...newTruck, remark: newValue || "" });
  763. }}
  764. onInputChange={(event, newInputValue) => {
  765. setNewTruck({ ...newTruck, remark: newInputValue });
  766. }}
  767. renderInput={(params) => (
  768. <TextField
  769. {...params}
  770. label={t("Remark")}
  771. fullWidth
  772. placeholder={t("Enter or select remark")}
  773. disabled={saving}
  774. />
  775. )}
  776. />
  777. </Grid>
  778. )}
  779. </Grid>
  780. </Box>
  781. </DialogContent>
  782. <DialogActions>
  783. <Button onClick={handleCloseAddDialog} disabled={saving}>
  784. {t("Cancel")}
  785. </Button>
  786. <Button
  787. onClick={handleCreateTruck}
  788. variant="contained"
  789. startIcon={<SaveIcon />}
  790. disabled={saving}
  791. >
  792. {saving ? t("Submitting...") : t("Save")}
  793. </Button>
  794. </DialogActions>
  795. </Dialog>
  796. {/* Snackbar for notifications */}
  797. <Snackbar
  798. open={snackbarOpen}
  799. autoHideDuration={6000}
  800. onClose={() => setSnackbarOpen(false)}
  801. anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
  802. >
  803. <Alert
  804. onClose={() => setSnackbarOpen(false)}
  805. severity="warning"
  806. sx={{ width: '100%' }}
  807. >
  808. {snackbarMessage}
  809. </Alert>
  810. </Snackbar>
  811. </Box>
  812. );
  813. };
  814. export default ShopDetail;