FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

484 строки
21 KiB

  1. "use client";
  2. import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from "@mui/material";
  3. import { useCallback, useEffect, useMemo, useRef, useState } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import { useSession } from "next-auth/react";
  6. import { SessionWithTokens } from "@/config/authConfig";
  7. import type { StoreLaneSummary, LaneRow, LaneBtn } from "@/app/api/pickOrder/actions";
  8. import {
  9. assignByDeliveryOrderPickOrderId,
  10. assignWorkbenchByLane,
  11. fetchWorkbenchReleasedDoPickOrdersForSelection,
  12. fetchWorkbenchReleasedDoPickOrdersForSelectionToday,
  13. fetchWorkbenchStoreLaneSummary,
  14. } from "@/app/api/doworkbench/actions";
  15. import Swal from "sweetalert2";
  16. import dayjs from "dayjs";
  17. import ReleasedDoPickOrderSelectModal from "@/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal";
  18. interface Props {
  19. onPickOrderAssigned?: () => void;
  20. onSwitchToDetailTab?: () => void;
  21. initialReleaseType?: string;
  22. }
  23. type LaneSlot4F = { truckDepartureTime: string; lane: LaneBtn };
  24. type TruckGroup4F = { truckLanceCode: string; slots: (LaneSlot4F & { sequenceIndex: number })[] };
  25. const WorkbenchFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSwitchToDetailTab, initialReleaseType = "batch" }) => {
  26. const { t } = useTranslation("pickOrder");
  27. const { data: session } = useSession() as { data: SessionWithTokens | null };
  28. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  29. const [selectedStore, setSelectedStore] = useState<string>("2/F");
  30. const [selectedTruck, setSelectedTruck] = useState<string>("");
  31. const [modalOpen, setModalOpen] = useState(false);
  32. const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]);
  33. const [truckCounts4F, setTruckCounts4F] = useState<{ truck: string; count: number }[]>([]);
  34. const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null);
  35. const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null);
  36. const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("today");
  37. const [isLoadingSummary, setIsLoadingSummary] = useState(false);
  38. const [isAssigning, setIsAssigning] = useState(false);
  39. const [isDefaultTruck, setIsDefaultTruck] = useState(false);
  40. const [beforeTodayTruckXCount, setBeforeTodayTruckXCount] = useState(0);
  41. const [selectedDate, setSelectedDate] = useState<string>("today");
  42. const [releaseType, setReleaseType] = useState<string>(initialReleaseType);
  43. const [ticketFloor, setTicketFloor] = useState<"2/F" | "4/F">("2/F");
  44. const defaultTruckCount = summary4F?.defaultTruckCount ?? 0;
  45. const hasLoggedRef = useRef(false);
  46. const fullReadyLoggedRef = useRef(false);
  47. const pendingRef = useRef(0);
  48. const workbenchReleasedListBridge = useMemo(
  49. () => ({
  50. loadBeforeToday: fetchWorkbenchReleasedDoPickOrdersForSelection,
  51. loadToday: fetchWorkbenchReleasedDoPickOrdersForSelectionToday,
  52. assignByListItemId: assignByDeliveryOrderPickOrderId,
  53. }),
  54. [],
  55. );
  56. const startFullTimer = () => {
  57. if (typeof window === "undefined") return;
  58. const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
  59. if (!(window as any)[key]) {
  60. (window as any)[key] = true;
  61. console.time("[FG] FloorLanePanel full ready");
  62. }
  63. };
  64. const tryEndFullTimer = () => {
  65. if (typeof window === "undefined") return;
  66. const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
  67. if ((window as any)[key] && !fullReadyLoggedRef.current && pendingRef.current === 0) {
  68. fullReadyLoggedRef.current = true;
  69. console.timeEnd("[FG] FloorLanePanel full ready");
  70. delete (window as any)[key];
  71. }
  72. };
  73. const loadSummaries = useCallback(async () => {
  74. setIsLoadingSummary(true);
  75. pendingRef.current += 1;
  76. startFullTimer();
  77. try {
  78. let dateParam: string | undefined;
  79. if (selectedDate === "today") dateParam = dayjs().format("YYYY-MM-DD");
  80. else if (selectedDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
  81. else if (selectedDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
  82. const [s2, s4] = await Promise.all([
  83. fetchWorkbenchStoreLaneSummary("2/F", dateParam, releaseType),
  84. fetchWorkbenchStoreLaneSummary("4/F", dateParam, releaseType),
  85. ]);
  86. setSummary2F(s2);
  87. setSummary4F(s4);
  88. } catch (error) {
  89. console.error("Error loading summaries:", error);
  90. } finally {
  91. setIsLoadingSummary(false);
  92. pendingRef.current -= 1;
  93. tryEndFullTimer();
  94. if (!hasLoggedRef.current) {
  95. hasLoggedRef.current = true;
  96. }
  97. }
  98. }, [selectedDate, releaseType]);
  99. useEffect(() => {
  100. void loadSummaries();
  101. }, [loadSummaries]);
  102. useEffect(() => {
  103. const loadCounts = async () => {
  104. pendingRef.current += 1;
  105. startFullTimer();
  106. try {
  107. const [list2F, list4F] = await Promise.all([
  108. fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "2/F"),
  109. fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "4/F"),
  110. ]);
  111. const groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
  112. const map: Record<string, number> = {};
  113. list.forEach((item) => {
  114. const lane = item.truckLanceCode || "-";
  115. map[lane] = (map[lane] || 0) + 1;
  116. });
  117. return Object.entries(map)
  118. .map(([truck, count]) => ({ truck, count }))
  119. .sort((a, b) => a.truck.localeCompare(b.truck));
  120. };
  121. setTruckCounts2F(groupByTruck(list2F));
  122. setTruckCounts4F(groupByTruck(list4F));
  123. } catch (e) {
  124. console.error("Error loading counts:", e);
  125. setTruckCounts2F([]);
  126. setTruckCounts4F([]);
  127. } finally {
  128. pendingRef.current -= 1;
  129. tryEndFullTimer();
  130. }
  131. };
  132. void loadCounts();
  133. }, [loadSummaries]);
  134. useEffect(() => {
  135. const loadBeforeTodayTruckX = async () => {
  136. pendingRef.current += 1;
  137. startFullTimer();
  138. try {
  139. const list = await fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, undefined, "車線-X");
  140. setBeforeTodayTruckXCount(list.length);
  141. } catch {
  142. setBeforeTodayTruckXCount(0);
  143. } finally {
  144. pendingRef.current -= 1;
  145. tryEndFullTimer();
  146. }
  147. };
  148. void loadBeforeTodayTruckX();
  149. }, []);
  150. const handleAssignByLane = useCallback(
  151. async (storeId: string, truckDepartureTime: string, truckLanceCode: string, requiredDate: string) => {
  152. if (!currentUserId) return;
  153. let dateParam: string | undefined;
  154. if (requiredDate === "today") dateParam = dayjs().format("YYYY-MM-DD");
  155. else if (requiredDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
  156. else if (requiredDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
  157. setIsAssigning(true);
  158. try {
  159. const res = await assignWorkbenchByLane({
  160. userId: currentUserId,
  161. storeId,
  162. truckLanceCode,
  163. truckDepartureTime,
  164. requiredDate: dateParam,
  165. });
  166. console.log("assignByLane result:", res);
  167. if (res.code === "SUCCESS") {
  168. window.dispatchEvent(new CustomEvent("pickOrderAssigned"));
  169. void loadSummaries();
  170. onPickOrderAssigned?.();
  171. onSwitchToDetailTab?.();
  172. }
  173. } catch {
  174. await Swal.fire({ icon: "error", title: t("Error"), text: t("Error occurred during assignment."), confirmButtonText: t("Confirm"), confirmButtonColor: "#8dba00" });
  175. } finally {
  176. setIsAssigning(false);
  177. }
  178. },
  179. [currentUserId, loadSummaries, onPickOrderAssigned, onSwitchToDetailTab, t],
  180. );
  181. const handleLaneButtonClick = useCallback(
  182. async (
  183. storeId: string,
  184. truckDepartureTime: string,
  185. truckLanceCode: string,
  186. loadingSequence: number | null | undefined,
  187. requiredDate: string,
  188. unassigned: number,
  189. total: number,
  190. ) => {
  191. let dateDisplay = requiredDate;
  192. if (requiredDate === "today") dateDisplay = dayjs().format("YYYY-MM-DD");
  193. else if (requiredDate === "tomorrow") dateDisplay = dayjs().add(1, "day").format("YYYY-MM-DD");
  194. else if (requiredDate === "dayAfterTomorrow") dateDisplay = dayjs().add(2, "day").format("YYYY-MM-DD");
  195. const result = await Swal.fire({
  196. title: t("Confirm Assignment"),
  197. html: `<div style="text-align: left; padding: 10px 0;">
  198. <p><strong>${t("Store")}:</strong> ${storeId}</p>
  199. <p><strong>${t("Lane Code")}:</strong> ${truckLanceCode}</p>
  200. ${loadingSequence != null ? `<p><strong>${t("Loading Sequence")}:</strong> ${loadingSequence}</p>` : ""}
  201. <p><strong>${t("Departure Time")}:</strong> ${truckDepartureTime}</p>
  202. <p><strong>${t("Required Date")}:</strong> ${dateDisplay}</p>
  203. <p><strong>${t("Available Orders")}:</strong> ${unassigned}/${total}</p>
  204. </div>`,
  205. icon: "question",
  206. showCancelButton: true,
  207. confirmButtonText: t("Confirm"),
  208. cancelButtonText: t("Cancel"),
  209. confirmButtonColor: "#8dba00",
  210. cancelButtonColor: "#F04438",
  211. });
  212. if (result.isConfirmed) {
  213. await handleAssignByLane(storeId, truckDepartureTime, truckLanceCode, requiredDate);
  214. }
  215. },
  216. [handleAssignByLane, t],
  217. );
  218. const getDateLabel = (offset: number) => dayjs().add(offset, "day").format("YYYY-MM-DD");
  219. const truckGroups4F = useMemo((): TruckGroup4F[] => {
  220. const rows = summary4F?.rows as LaneRow[] | undefined;
  221. if (!rows?.length) return [];
  222. const map = new Map<string, LaneSlot4F[]>();
  223. for (const row of rows) {
  224. for (const lane of row.lanes) {
  225. const list = map.get(lane.truckLanceCode);
  226. const slot: LaneSlot4F = { truckDepartureTime: row.truckDepartureTime, lane };
  227. if (list) list.push(slot);
  228. else map.set(lane.truckLanceCode, [slot]);
  229. }
  230. }
  231. return Array.from(map.entries()).map(([truckLanceCode, slots]) => ({
  232. truckLanceCode,
  233. slots: slots
  234. .slice()
  235. .sort((a, b) => (a.lane.loadingSequence ?? 999) - (b.lane.loadingSequence ?? 999))
  236. .map((s, i) => ({ ...s, sequenceIndex: i + 1 })),
  237. }));
  238. }, [summary4F?.rows]);
  239. const renderNoEntry = () => (
  240. <Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600, fontSize: "1rem", textAlign: "center", py: 1 }}>
  241. {t("No entries available")}
  242. </Typography>
  243. );
  244. return (
  245. <Box sx={{ mb: 2 }}>
  246. <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "flex-start" }}>
  247. <Box sx={{ maxWidth: 300 }}>
  248. <FormControl fullWidth size="small">
  249. <InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
  250. <Select labelId="date-select-label" value={selectedDate} label={t("Select Date")} onChange={(e) => setSelectedDate(e.target.value)}>
  251. <MenuItem value="today">{t("Today")} ({getDateLabel(0)})</MenuItem>
  252. <MenuItem value="tomorrow">{t("Tomorrow")} ({getDateLabel(1)})</MenuItem>
  253. <MenuItem value="dayAfterTomorrow">{t("Day After Tomorrow")} ({getDateLabel(2)})</MenuItem>
  254. </Select>
  255. </FormControl>
  256. </Box>
  257. <Box sx={{ minWidth: 140, maxWidth: 300 }}>
  258. <FormControl fullWidth size="small">
  259. <InputLabel id="release-type-select-label">{t("Release Type")}</InputLabel>
  260. <Select labelId="release-type-select-label" value={releaseType} label={t("Release Type")} onChange={(e) => setReleaseType(e.target.value)}>
  261. <MenuItem value="batch">{t("Batch")}</MenuItem>
  262. <MenuItem value="single">{t("Single")}</MenuItem>
  263. </Select>
  264. </FormControl>
  265. </Box>
  266. <Box sx={{ minWidth: 120, maxWidth: 200 }}>
  267. <FormControl fullWidth size="small">
  268. <InputLabel id="ticket-floor-select-label">{t("Floor ticket")}</InputLabel>
  269. <Select labelId="ticket-floor-select-label" value={ticketFloor} label={t("Floor ticket")} onChange={(e) => setTicketFloor(e.target.value as "2/F" | "4/F")}>
  270. <MenuItem value="2/F">{t("2F ticket")}</MenuItem>
  271. <MenuItem value="4/F">{t("4F ticket")}</MenuItem>
  272. </Select>
  273. </FormControl>
  274. </Box>
  275. </Stack>
  276. <Grid container spacing={2}>
  277. {ticketFloor === "2/F" && (
  278. <Grid item xs={12}>
  279. <Stack direction="row" spacing={2} alignItems="flex-start">
  280. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography>
  281. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  282. {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !summary2F?.rows?.length ? renderNoEntry() : (
  283. <Grid container spacing={1}>
  284. {summary2F.rows.map((row) => (
  285. <Grid item xs={12} key={row.truckDepartureTime}>
  286. <Stack direction={{ xs: "column", sm: "row" }} spacing={1} alignItems={{ xs: "stretch", sm: "center" }} sx={{ border: "1px solid #e0e0e0", borderRadius: 0.5, p: 1, backgroundColor: "#fff" }}>
  287. <Typography variant="body2" sx={{ fontWeight: 600, minWidth: { sm: 60 } }}>{row.truckDepartureTime}</Typography>
  288. <Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}>
  289. {row.lanes.map((lane) => (
  290. <Button key={`${row.truckDepartureTime}-${lane.truckLanceCode}`} variant="outlined" disabled={lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick("2/F", row.truckDepartureTime, lane.truckLanceCode, null, selectedDate, lane.unassigned, lane.total)}>
  291. {`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
  292. </Button>
  293. ))}
  294. </Stack>
  295. </Stack>
  296. </Grid>
  297. ))}
  298. </Grid>
  299. )}
  300. </Box>
  301. </Stack>
  302. </Grid>
  303. )}
  304. {ticketFloor === "4/F" && (
  305. <Grid item xs={12}>
  306. <Stack direction="row" spacing={2} alignItems="flex-start">
  307. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography>
  308. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  309. {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !truckGroups4F.length ? renderNoEntry() : (
  310. <Grid container spacing={1}>
  311. {truckGroups4F.map(({ truckLanceCode, slots }) => (
  312. <Grid item xs={12} key={truckLanceCode}>
  313. <Stack direction={{ xs: "column", sm: "row" }} spacing={1} alignItems={{ xs: "stretch", sm: "center" }} sx={{ border: "1px solid #e0e0e0", borderRadius: 0.5, p: 1, backgroundColor: "#fff" }}>
  314. <Typography variant="body2" sx={{ fontWeight: 700, minWidth: { sm: 160 } }}>{truckLanceCode}</Typography>
  315. <Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}>
  316. {slots.map((slot) => {
  317. const handlerName = (slot.lane.handlerName ?? "").trim();
  318. return (
  319. <Button key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.truckDepartureTime}`} variant="outlined" disabled={slot.lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick("4/F", slot.truckDepartureTime, slot.lane.truckLanceCode, slot.lane.loadingSequence ?? null, selectedDate, slot.lane.unassigned, slot.lane.total)}>
  320. {`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})${handlerName ? ` ${handlerName}` : ""}`}
  321. </Button>
  322. );
  323. })}
  324. </Stack>
  325. </Stack>
  326. </Grid>
  327. ))}
  328. </Grid>
  329. )}
  330. </Box>
  331. </Stack>
  332. </Grid>
  333. )}
  334. <Grid item xs={12}>
  335. <Box sx={{ py: 2, mt: 1, mb: 0.5, borderTop: "1px solid #e0e0e0" }}>
  336. <Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 0.5 }}>
  337. {t("Not yet finished released do pick orders")}
  338. </Typography>
  339. <Typography variant="body2" color="text.secondary">
  340. {t("Released orders not yet completed - click lane to select and assign")}
  341. </Typography>
  342. </Box>
  343. </Grid>
  344. {ticketFloor === "2/F" && (
  345. <Grid item xs={12}>
  346. <Stack direction="row" spacing={2} alignItems="flex-start">
  347. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography>
  348. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  349. {truckCounts2F.length === 0 ? renderNoEntry() : (
  350. <Grid container spacing={1}>
  351. {truckCounts2F.map(({ truck, count }) => (
  352. <Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}>
  353. <Button
  354. variant="outlined"
  355. onClick={() => {
  356. setIsDefaultTruck(false);
  357. setSelectedStore("2/F");
  358. setSelectedTruck(truck);
  359. setModalOpen(true);
  360. }}
  361. sx={{ flex: 1 }}
  362. >
  363. {`${truck} (${count})`}
  364. </Button>
  365. </Grid>
  366. ))}
  367. </Grid>
  368. )}
  369. </Box>
  370. </Stack>
  371. </Grid>
  372. )}
  373. {ticketFloor === "4/F" && (
  374. <Grid item xs={12}>
  375. <Stack direction="row" spacing={2} alignItems="flex-start">
  376. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography>
  377. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  378. {truckCounts4F.length === 0 ? renderNoEntry() : (
  379. <Grid container spacing={1}>
  380. {truckCounts4F.map(({ truck, count }) => (
  381. <Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}>
  382. <Button
  383. variant="outlined"
  384. onClick={() => {
  385. setIsDefaultTruck(false);
  386. setSelectedStore("4/F");
  387. setSelectedTruck(truck);
  388. setModalOpen(true);
  389. }}
  390. sx={{ flex: 1 }}
  391. >
  392. {`${truck} (${count})`}
  393. </Button>
  394. </Grid>
  395. ))}
  396. </Grid>
  397. )}
  398. </Box>
  399. </Stack>
  400. </Grid>
  401. )}
  402. <Grid item xs={12}>
  403. <Stack direction="row" spacing={2} alignItems="flex-start">
  404. <Typography sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{t("Truck X")}</Typography>
  405. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  406. {beforeTodayTruckXCount === 0 && defaultTruckCount === 0 ? renderNoEntry() : (
  407. <Stack direction="row" spacing={1}>
  408. {defaultTruckCount > 0 && (
  409. <Button
  410. variant="outlined"
  411. onClick={() => {
  412. setSelectedStore("");
  413. setSelectedTruck("車線-X");
  414. setIsDefaultTruck(true);
  415. setDefaultDateScope("today");
  416. setModalOpen(true);
  417. }}
  418. >
  419. {`${t("Today")} (${defaultTruckCount})`}
  420. </Button>
  421. )}
  422. {beforeTodayTruckXCount > 0 && (
  423. <Button
  424. variant="outlined"
  425. onClick={() => {
  426. setSelectedStore("4/F");
  427. setSelectedTruck("車線-X");
  428. setIsDefaultTruck(true);
  429. setDefaultDateScope("before");
  430. setModalOpen(true);
  431. }}
  432. >
  433. {`${t("車線-X")} (${beforeTodayTruckXCount})`}
  434. </Button>
  435. )}
  436. </Stack>
  437. )}
  438. </Box>
  439. </Stack>
  440. </Grid>
  441. <ReleasedDoPickOrderSelectModal
  442. open={modalOpen}
  443. storeId={selectedStore}
  444. truck={selectedTruck}
  445. isDefaultTruck={isDefaultTruck}
  446. defaultDateScope={defaultDateScope}
  447. listBridge={workbenchReleasedListBridge}
  448. onClose={() => setModalOpen(false)}
  449. onAssigned={() => {
  450. void loadSummaries();
  451. onPickOrderAssigned?.();
  452. onSwitchToDetailTab?.();
  453. }}
  454. />
  455. </Grid>
  456. </Box>
  457. );
  458. };
  459. export default WorkbenchFloorLanePanel;