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

800 строки
34 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. fetchWorkbenchEtraLaneSummary,
  12. fetchWorkbenchReleasedDoPickOrdersForSelection,
  13. fetchWorkbenchReleasedDoPickOrdersForSelectionToday,
  14. fetchWorkbenchStoreLaneSummary,
  15. type WorkbenchEtraShopLaneGroup,
  16. } from "@/app/api/doworkbench/actions";
  17. import Swal from "sweetalert2";
  18. import dayjs from "dayjs";
  19. import ReleasedDoPickOrderSelectModal from "@/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal";
  20. import {
  21. DEFAULT_WORKBENCH_LANE_PANEL_PREFS,
  22. type WorkbenchLanePanelPrefs,
  23. } from "./workbenchLanePanelPrefs";
  24. import WorkbenchEtraMergeDialog from "./WorkbenchEtraMergeDialog";
  25. interface Props {
  26. onPickOrderAssigned?: () => void;
  27. onSwitchToDetailTab?: () => void;
  28. initialReleaseType?: string;
  29. /** When true (workbench tab "Etra"), only the Etra shop×lane grid is shown; use top tab to return to normal. */
  30. etraOnly?: boolean;
  31. /** With [etraOnly], navigates to normal assign tab (tab 0). */
  32. onRequestNormalLaneTab?: () => void;
  33. /** Lifted from DoWorkbenchTabs — persists date/floor/release across workbench tab switches. */
  34. lanePanelPrefs?: WorkbenchLanePanelPrefs;
  35. onLanePanelPrefsChange?: (prefs: WorkbenchLanePanelPrefs) => void;
  36. }
  37. type LaneSlot4F = { truckDepartureTime: string; lane: LaneBtn };
  38. type TruckGroup4F = { truckLanceCode: string; slots: (LaneSlot4F & { sequenceIndex: number })[] };
  39. const WorkbenchFloorLanePanel: React.FC<Props> = ({
  40. onPickOrderAssigned,
  41. onSwitchToDetailTab,
  42. initialReleaseType = "batch",
  43. etraOnly = false,
  44. onRequestNormalLaneTab,
  45. lanePanelPrefs: lanePanelPrefsProp,
  46. onLanePanelPrefsChange,
  47. }) => {
  48. const { t } = useTranslation("pickOrder");
  49. const { data: session } = useSession() as { data: SessionWithTokens | null };
  50. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  51. const [selectedStore, setSelectedStore] = useState<string>("2/F");
  52. const [selectedTruck, setSelectedTruck] = useState<string>("");
  53. const [modalOpen, setModalOpen] = useState(false);
  54. const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]);
  55. const [truckCounts4F, setTruckCounts4F] = useState<{ truck: string; count: number }[]>([]);
  56. const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null);
  57. const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null);
  58. const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("today");
  59. const [isLoadingSummary, setIsLoadingSummary] = useState(false);
  60. const [isAssigning, setIsAssigning] = useState(false);
  61. const [isDefaultTruck, setIsDefaultTruck] = useState(false);
  62. const [beforeTodayTruckXCount, setBeforeTodayTruckXCount] = useState(0);
  63. const [internalLanePrefs, setInternalLanePrefs] = useState<WorkbenchLanePanelPrefs>(() => ({
  64. ...DEFAULT_WORKBENCH_LANE_PANEL_PREFS,
  65. releaseType: initialReleaseType,
  66. }));
  67. const lanePanelPrefs = lanePanelPrefsProp ?? internalLanePrefs;
  68. const setLanePanelPrefs = onLanePanelPrefsChange ?? setInternalLanePrefs;
  69. const selectedDate = lanePanelPrefs.selectedDate;
  70. const releaseType = lanePanelPrefs.releaseType;
  71. const ticketFloor = lanePanelPrefs.ticketFloor;
  72. const patchLanePanelPrefs = useCallback(
  73. (patch: Partial<WorkbenchLanePanelPrefs>) => {
  74. setLanePanelPrefs({ ...lanePanelPrefs, ...patch });
  75. },
  76. [lanePanelPrefs, setLanePanelPrefs],
  77. );
  78. const [isExtraView, setisExtraView] = useState(false);
  79. const [etraGroups, setEtraGroups] = useState<WorkbenchEtraShopLaneGroup[]>([]);
  80. const [isLoadingEtra, setIsLoadingEtra] = useState(false);
  81. const [modalReleaseTypeFilter, setModalReleaseTypeFilter] = useState<string | undefined>(undefined);
  82. const [modalFilterRequiredDeliveryDate, setModalFilterRequiredDeliveryDate] = useState<string | undefined>(undefined);
  83. const [modalInitialShopSearch, setModalInitialShopSearch] = useState<string | undefined>(undefined);
  84. const [modalTruckXFloor, setModalTruckXFloor] = useState<string | undefined>(undefined);
  85. const ticketFloorApiKey = useMemo(
  86. () => ticketFloor.replace("/", "").trim().toUpperCase(),
  87. [ticketFloor],
  88. );
  89. const defaultTruckCount =
  90. (ticketFloor === "2/F" ? summary2F?.defaultTruckCount : summary4F?.defaultTruckCount) ?? 0;
  91. const etraEnterInFlightRef = useRef(false);
  92. const [etraMergeDialogOpen, setEtraMergeDialogOpen] = useState(false);
  93. const inEtraUi = useMemo(() => etraOnly || isExtraView, [etraOnly, isExtraView]);
  94. const selectedDeliveryDateYmd = useMemo(() => {
  95. if (selectedDate === "today") return dayjs().format("YYYY-MM-DD");
  96. if (selectedDate === "tomorrow") return dayjs().add(1, "day").format("YYYY-MM-DD");
  97. if (selectedDate === "dayAfterTomorrow") return dayjs().add(2, "day").format("YYYY-MM-DD");
  98. return dayjs().format("YYYY-MM-DD");
  99. }, [selectedDate]);
  100. const hasLoggedRef = useRef(false);
  101. const fullReadyLoggedRef = useRef(false);
  102. const pendingRef = useRef(0);
  103. const workbenchReleasedListBridge = useMemo(
  104. () => ({
  105. loadBeforeToday: (
  106. shopName?: string,
  107. storeId?: string,
  108. truck?: string,
  109. releaseType?: string,
  110. floor?: string
  111. ) => fetchWorkbenchReleasedDoPickOrdersForSelection(shopName, storeId, truck, releaseType, floor),
  112. loadToday: (
  113. shopName?: string,
  114. storeId?: string,
  115. truck?: string,
  116. requiredDeliveryDate?: string,
  117. releaseType?: string,
  118. floor?: string
  119. ) =>
  120. fetchWorkbenchReleasedDoPickOrdersForSelectionToday(
  121. shopName,
  122. storeId,
  123. truck,
  124. requiredDeliveryDate,
  125. releaseType,
  126. floor
  127. ),
  128. assignByListItemId: assignByDeliveryOrderPickOrderId,
  129. }),
  130. [],
  131. );
  132. const startFullTimer = () => {
  133. if (typeof window === "undefined") return;
  134. const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
  135. if (!(window as any)[key]) {
  136. (window as any)[key] = true;
  137. console.time("[FG] FloorLanePanel full ready");
  138. }
  139. };
  140. const tryEndFullTimer = () => {
  141. if (typeof window === "undefined") return;
  142. const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
  143. if ((window as any)[key] && !fullReadyLoggedRef.current && pendingRef.current === 0) {
  144. fullReadyLoggedRef.current = true;
  145. console.timeEnd("[FG] FloorLanePanel full ready");
  146. delete (window as any)[key];
  147. }
  148. };
  149. const loadSummaries = useCallback(async () => {
  150. setIsLoadingSummary(true);
  151. pendingRef.current += 1;
  152. startFullTimer();
  153. try {
  154. let dateParam: string | undefined;
  155. if (selectedDate === "today") dateParam = dayjs().format("YYYY-MM-DD");
  156. else if (selectedDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
  157. else if (selectedDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
  158. const [s2, s4] = await Promise.all([
  159. fetchWorkbenchStoreLaneSummary("2/F", dateParam, releaseType),
  160. fetchWorkbenchStoreLaneSummary("4/F", dateParam, releaseType),
  161. ]);
  162. setSummary2F(s2);
  163. setSummary4F(s4);
  164. } catch (error) {
  165. console.error("Error loading summaries:", error);
  166. } finally {
  167. setIsLoadingSummary(false);
  168. pendingRef.current -= 1;
  169. tryEndFullTimer();
  170. if (!hasLoggedRef.current) {
  171. hasLoggedRef.current = true;
  172. }
  173. }
  174. }, [selectedDate, releaseType]);
  175. const loadEtraSummaries = useCallback(async () => {
  176. setIsLoadingEtra(true);
  177. pendingRef.current += 1;
  178. startFullTimer();
  179. try {
  180. let dateParam: string | undefined;
  181. if (selectedDate === "today") dateParam = dayjs().format("YYYY-MM-DD");
  182. else if (selectedDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
  183. else if (selectedDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
  184. const data = await fetchWorkbenchEtraLaneSummary(dateParam);
  185. setEtraGroups(data);
  186. } catch (error) {
  187. console.error("Error loading Etra summary:", error);
  188. setEtraGroups([]);
  189. } finally {
  190. setIsLoadingEtra(false);
  191. pendingRef.current -= 1;
  192. tryEndFullTimer();
  193. }
  194. }, [selectedDate]);
  195. useEffect(() => {
  196. if (inEtraUi) void loadEtraSummaries();
  197. else void loadSummaries();
  198. }, [inEtraUi, loadEtraSummaries, loadSummaries]);
  199. useEffect(() => {
  200. const loadCounts = async () => {
  201. if (inEtraUi) return;
  202. pendingRef.current += 1;
  203. startFullTimer();
  204. try {
  205. const [list2F, list4F] = await Promise.all([
  206. fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "2/F"),
  207. fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "4/F"),
  208. ]);
  209. const groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
  210. const map: Record<string, number> = {};
  211. list.forEach((item) => {
  212. const lane = item.truckLanceCode || "-";
  213. map[lane] = (map[lane] || 0) + 1;
  214. });
  215. return Object.entries(map)
  216. .map(([truck, count]) => ({ truck, count }))
  217. .sort((a, b) => a.truck.localeCompare(b.truck));
  218. };
  219. setTruckCounts2F(groupByTruck(list2F));
  220. setTruckCounts4F(groupByTruck(list4F));
  221. } catch (e) {
  222. console.error("Error loading counts:", e);
  223. setTruckCounts2F([]);
  224. setTruckCounts4F([]);
  225. } finally {
  226. pendingRef.current -= 1;
  227. tryEndFullTimer();
  228. }
  229. };
  230. void loadCounts();
  231. }, [inEtraUi, loadSummaries]);
  232. useEffect(() => {
  233. const loadBeforeTodayTruckX = async () => {
  234. if (inEtraUi) return;
  235. pendingRef.current += 1;
  236. startFullTimer();
  237. try {
  238. // storeId stays undefined (Truck X); floor splits by DO supplier preferred floor.
  239. const list = await fetchWorkbenchReleasedDoPickOrdersForSelection(
  240. undefined,
  241. undefined,
  242. "車線-X",
  243. undefined,
  244. ticketFloorApiKey
  245. );
  246. setBeforeTodayTruckXCount(list.length);
  247. } catch {
  248. setBeforeTodayTruckXCount(0);
  249. } finally {
  250. pendingRef.current -= 1;
  251. tryEndFullTimer();
  252. }
  253. };
  254. void loadBeforeTodayTruckX();
  255. }, [inEtraUi, ticketFloorApiKey]);
  256. const clearModalEtraContext = useCallback(() => {
  257. setModalReleaseTypeFilter(undefined);
  258. setModalFilterRequiredDeliveryDate(undefined);
  259. setModalInitialShopSearch(undefined);
  260. setModalTruckXFloor(undefined);
  261. }, []);
  262. const openEnterEtraView = useCallback(async () => {
  263. if (etraEnterInFlightRef.current) return;
  264. etraEnterInFlightRef.current = true;
  265. try {
  266. /*
  267. const r = await Swal.fire({
  268. title: t("Enter isExtra workbench view?"),
  269. text: t("Etra view groups all add-on tickets by shop and lane for the selected date."),
  270. icon: "question",
  271. showCancelButton: true,
  272. confirmButtonText: t("Confirm"),
  273. cancelButtonText: t("Cancel"),
  274. confirmButtonColor: "#8dba00",
  275. cancelButtonColor: "#F04438",
  276. });
  277. if (r.isConfirmed) setisExtraView(true);
  278. */
  279. setisExtraView(true);
  280. } finally {
  281. etraEnterInFlightRef.current = false;
  282. }
  283. }, []);
  284. const handleAssignByLane = useCallback(
  285. async (
  286. storeId: string,
  287. truckDepartureTime: string,
  288. truckLanceCode: string,
  289. loadingSequence: number | null | undefined,
  290. requiredDate: string,
  291. ) => {
  292. if (!currentUserId) return;
  293. let dateParam: string | undefined;
  294. if (requiredDate === "today") dateParam = dayjs().format("YYYY-MM-DD");
  295. else if (requiredDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
  296. else if (requiredDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
  297. setIsAssigning(true);
  298. try {
  299. const res = await assignWorkbenchByLane({
  300. userId: currentUserId,
  301. storeId,
  302. truckLanceCode,
  303. truckDepartureTime,
  304. loadingSequence,
  305. requiredDate: dateParam,
  306. releaseType: inEtraUi ? "isExtra" : releaseType,
  307. });
  308. console.log("assignByLane result:", res);
  309. if (res.code === "SUCCESS") {
  310. window.dispatchEvent(new CustomEvent("pickOrderAssigned"));
  311. void (inEtraUi ? loadEtraSummaries() : loadSummaries());
  312. onPickOrderAssigned?.();
  313. onSwitchToDetailTab?.();
  314. }
  315. } catch {
  316. await Swal.fire({ icon: "error", title: t("Error"), text: t("Error occurred during assignment."), confirmButtonText: t("Confirm"), confirmButtonColor: "#8dba00" });
  317. } finally {
  318. setIsAssigning(false);
  319. }
  320. },
  321. [currentUserId, inEtraUi, loadEtraSummaries, loadSummaries, onPickOrderAssigned, onSwitchToDetailTab, releaseType, t],
  322. );
  323. const handleLaneButtonClick = useCallback(
  324. async (
  325. storeId: string,
  326. truckDepartureTime: string,
  327. truckLanceCode: string,
  328. loadingSequence: number | null | undefined,
  329. requiredDate: string,
  330. unassigned: number,
  331. total: number,
  332. ) => {
  333. let dateDisplay = requiredDate;
  334. if (requiredDate === "today") dateDisplay = dayjs().format("YYYY-MM-DD");
  335. else if (requiredDate === "tomorrow") dateDisplay = dayjs().add(1, "day").format("YYYY-MM-DD");
  336. else if (requiredDate === "dayAfterTomorrow") dateDisplay = dayjs().add(2, "day").format("YYYY-MM-DD");
  337. const result = await Swal.fire({
  338. title: t("Confirm Assignment"),
  339. html: `<div style="text-align: left; padding: 10px 0;">
  340. <p><strong>${t("Store")}:</strong> ${storeId}</p>
  341. <p><strong>${t("Lane Code")}:</strong> ${truckLanceCode}</p>
  342. ${loadingSequence != null ? `<p><strong>${t("Loading Sequence")}:</strong> ${loadingSequence}</p>` : ""}
  343. <p><strong>${t("Departure Time")}:</strong> ${truckDepartureTime}</p>
  344. <p><strong>${t("Required Date")}:</strong> ${dateDisplay}</p>
  345. <p><strong>${t("Available Orders")}:</strong> ${unassigned}/${total}</p>
  346. </div>`,
  347. icon: "question",
  348. showCancelButton: true,
  349. confirmButtonText: t("Confirm"),
  350. cancelButtonText: t("Cancel"),
  351. confirmButtonColor: "#8dba00",
  352. cancelButtonColor: "#F04438",
  353. });
  354. if (result.isConfirmed) {
  355. await handleAssignByLane(storeId, truckDepartureTime, truckLanceCode, loadingSequence, requiredDate);
  356. }
  357. },
  358. [handleAssignByLane, t],
  359. );
  360. const getDateLabel = (offset: number) => dayjs().add(offset, "day").format("YYYY-MM-DD");
  361. const truckGroups4F = useMemo((): TruckGroup4F[] => {
  362. const rows = summary4F?.rows as LaneRow[] | undefined;
  363. if (!rows?.length) return [];
  364. const map = new Map<string, LaneSlot4F[]>();
  365. for (const row of rows) {
  366. for (const lane of row.lanes) {
  367. const list = map.get(lane.truckLanceCode);
  368. const slot: LaneSlot4F = { truckDepartureTime: row.truckDepartureTime, lane };
  369. if (list) list.push(slot);
  370. else map.set(lane.truckLanceCode, [slot]);
  371. }
  372. }
  373. return Array.from(map.entries()).map(([truckLanceCode, slots]) => ({
  374. truckLanceCode,
  375. slots: slots
  376. .slice()
  377. .sort((a, b) => (a.lane.loadingSequence ?? 999) - (b.lane.loadingSequence ?? 999))
  378. .map((s, i) => ({ ...s, sequenceIndex: i + 1 })),
  379. }));
  380. }, [summary4F?.rows]);
  381. const renderNoEntry = () => (
  382. <Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600, fontSize: "1rem", textAlign: "center", py: 1 }}>
  383. {t("No entries available")}
  384. </Typography>
  385. );
  386. return (
  387. <Box sx={{ mb: 2 }}>
  388. <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "flex-start", flexWrap: "wrap" }}>
  389. <Box sx={{ maxWidth: 300 }}>
  390. <FormControl fullWidth size="small">
  391. <InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
  392. <Select labelId="date-select-label" value={selectedDate} label={t("Select Date")} onChange={(e) => patchLanePanelPrefs({ selectedDate: e.target.value as WorkbenchLanePanelPrefs["selectedDate"] })}>
  393. <MenuItem value="today">{t("Today")} ({getDateLabel(0)})</MenuItem>
  394. <MenuItem value="tomorrow">{t("Tomorrow")} ({getDateLabel(1)})</MenuItem>
  395. <MenuItem value="dayAfterTomorrow">{t("Day After Tomorrow")} ({getDateLabel(2)})</MenuItem>
  396. </Select>
  397. </FormControl>
  398. </Box>
  399. {inEtraUi && (
  400. <Box sx={{ display: "flex", alignItems: "center", pt: 0.5 }}>
  401. <Button variant="contained" color="secondary" onClick={() => setEtraMergeDialogOpen(true)}>
  402. {t("Merge Etra ticket")}
  403. </Button>
  404. </Box>
  405. )}
  406. {!inEtraUi && (
  407. <>
  408. <Box sx={{ minWidth: 140, maxWidth: 300 }}>
  409. <FormControl fullWidth size="small">
  410. <InputLabel id="release-type-select-label">{t("Release Type")}</InputLabel>
  411. <Select labelId="release-type-select-label" value={releaseType} label={t("Release Type")} onChange={(e) => patchLanePanelPrefs({ releaseType: e.target.value })}>
  412. <MenuItem value="batch">{t("Batch")}</MenuItem>
  413. <MenuItem value="single">{t("Single")}</MenuItem>
  414. </Select>
  415. </FormControl>
  416. </Box>
  417. <Box sx={{ minWidth: 120, maxWidth: 200 }}>
  418. <FormControl fullWidth size="small">
  419. <InputLabel id="ticket-floor-select-label">{t("Floor ticket")}</InputLabel>
  420. <Select labelId="ticket-floor-select-label" value={ticketFloor} label={t("Floor ticket")} onChange={(e) => patchLanePanelPrefs({ ticketFloor: e.target.value as WorkbenchLanePanelPrefs["ticketFloor"] })}>
  421. <MenuItem value="2/F">{t("2F ticket")}</MenuItem>
  422. <MenuItem value="4/F">{t("4F ticket")}</MenuItem>
  423. </Select>
  424. </FormControl>
  425. </Box>
  426. {/*
  427. <Box sx={{ display: "flex", alignItems: "center", pt: 0.5 }}>
  428. <Button variant="contained" color="secondary" onClick={() => void openEnterEtraView()}>
  429. {t("isExtra order")}
  430. </Button>
  431. </Box>
  432. */}
  433. </>
  434. )}
  435. {/*
  436. {inEtraUi && !etraOnly && (
  437. <Box sx={{ display: "flex", alignItems: "center", pt: 0.5 }}>
  438. <Button
  439. variant="outlined"
  440. onClick={() => {
  441. clearModalEtraContext();
  442. setisExtraView(false);
  443. }}
  444. >
  445. {t("Exit Etra view")}
  446. </Button>
  447. </Box>
  448. )}
  449. {etraOnly && onRequestNormalLaneTab && (
  450. <Box sx={{ display: "flex", alignItems: "center", pt: 0.5 }}>
  451. <Button variant="outlined" onClick={() => onRequestNormalLaneTab()}>
  452. {t("Back to normal assign tab")}
  453. </Button>
  454. </Box>
  455. )}
  456. */}
  457. </Stack>
  458. {!inEtraUi ? (
  459. <Grid container spacing={2}>
  460. {ticketFloor === "2/F" && (
  461. <Grid item xs={12}>
  462. <Stack direction="row" spacing={2} alignItems="flex-start">
  463. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography>
  464. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  465. {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !summary2F?.rows?.length ? renderNoEntry() : (
  466. <Grid container spacing={1}>
  467. {summary2F.rows.map((row) => (
  468. <Grid item xs={12} key={row.truckDepartureTime}>
  469. <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" }}>
  470. <Typography variant="body2" sx={{ fontWeight: 600, minWidth: { sm: 60 } }}>{row.truckDepartureTime}</Typography>
  471. <Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}>
  472. {row.lanes.map((lane) => (
  473. <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)}>
  474. {`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
  475. </Button>
  476. ))}
  477. </Stack>
  478. </Stack>
  479. </Grid>
  480. ))}
  481. </Grid>
  482. )}
  483. </Box>
  484. </Stack>
  485. </Grid>
  486. )}
  487. {ticketFloor === "4/F" && (
  488. <Grid item xs={12}>
  489. <Stack direction="row" spacing={2} alignItems="flex-start">
  490. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography>
  491. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  492. {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !truckGroups4F.length ? renderNoEntry() : (
  493. <Grid container spacing={1}>
  494. {truckGroups4F.map(({ truckLanceCode, slots }) => (
  495. <Grid item xs={12} key={truckLanceCode}>
  496. <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" }}>
  497. <Typography variant="body2" sx={{ fontWeight: 700, minWidth: { sm: 160 } }}>{truckLanceCode}</Typography>
  498. <Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}>
  499. {slots.map((slot) => {
  500. const handlerName = (slot.lane.handlerName ?? "").trim();
  501. return (
  502. <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)}>
  503. {`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})${handlerName ? ` ${handlerName}` : ""}`}
  504. </Button>
  505. );
  506. })}
  507. </Stack>
  508. </Stack>
  509. </Grid>
  510. ))}
  511. </Grid>
  512. )}
  513. </Box>
  514. </Stack>
  515. </Grid>
  516. )}
  517. <Grid item xs={12}>
  518. <Stack direction="row" spacing={2} alignItems="flex-start">
  519. <Stack sx={{ minWidth: 60, pt: 1 }} spacing={0.25}>
  520. <Typography sx={{ fontWeight: 600 }}>{t("Truck X")}</Typography>
  521. {/*
  522. <Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.2 }}>
  523. {t("Required Date")}: {selectedDeliveryDateYmd}
  524. </Typography>
  525. */}
  526. </Stack>
  527. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  528. {defaultTruckCount === 0 ? (
  529. renderNoEntry()
  530. ) : (
  531. <Button
  532. variant="outlined"
  533. onClick={() => {
  534. clearModalEtraContext();
  535. setSelectedStore("");
  536. setSelectedTruck("車線-X");
  537. setIsDefaultTruck(true);
  538. setDefaultDateScope("today");
  539. setModalTruckXFloor(ticketFloorApiKey);
  540. setModalOpen(true);
  541. }}
  542. >
  543. {/*{`${selectedDeliveryDateYmd} (${defaultTruckCount})`}*/}
  544. {t("車線-X")}({defaultTruckCount})
  545. </Button>
  546. )}
  547. </Box>
  548. </Stack>
  549. </Grid>
  550. <Grid item xs={12}>
  551. <Box sx={{ py: 2, mt: 1, mb: 0.5, borderTop: "1px solid #e0e0e0" }}>
  552. <Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 0.5 }}>
  553. {t("Not yet finished released do pick orders")}
  554. </Typography>
  555. <Typography variant="body2" color="text.secondary">
  556. {t("Released orders not yet completed - click lane to select and assign")}
  557. </Typography>
  558. </Box>
  559. </Grid>
  560. {ticketFloor === "2/F" && (
  561. <Grid item xs={12}>
  562. <Stack direction="row" spacing={2} alignItems="flex-start">
  563. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography>
  564. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  565. {truckCounts2F.length === 0 ? renderNoEntry() : (
  566. <Grid container spacing={1}>
  567. {truckCounts2F.map(({ truck, count }) => (
  568. <Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}>
  569. <Button
  570. variant="outlined"
  571. onClick={() => {
  572. clearModalEtraContext();
  573. setIsDefaultTruck(false);
  574. setSelectedStore("2/F");
  575. setSelectedTruck(truck);
  576. setModalOpen(true);
  577. }}
  578. sx={{ flex: 1 }}
  579. >
  580. {`${truck} (${count})`}
  581. </Button>
  582. </Grid>
  583. ))}
  584. </Grid>
  585. )}
  586. </Box>
  587. </Stack>
  588. </Grid>
  589. )}
  590. {ticketFloor === "4/F" && (
  591. <Grid item xs={12}>
  592. <Stack direction="row" spacing={2} alignItems="flex-start">
  593. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography>
  594. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  595. {truckCounts4F.length === 0 ? renderNoEntry() : (
  596. <Grid container spacing={1}>
  597. {truckCounts4F.map(({ truck, count }) => (
  598. <Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}>
  599. <Button
  600. variant="outlined"
  601. onClick={() => {
  602. clearModalEtraContext();
  603. setIsDefaultTruck(false);
  604. setSelectedStore("4/F");
  605. setSelectedTruck(truck);
  606. setModalOpen(true);
  607. }}
  608. sx={{ flex: 1 }}
  609. >
  610. {`${truck} (${count})`}
  611. </Button>
  612. </Grid>
  613. ))}
  614. </Grid>
  615. )}
  616. </Box>
  617. </Stack>
  618. </Grid>
  619. )}
  620. <Grid item xs={12}>
  621. <Stack direction="row" spacing={2} alignItems="flex-start">
  622. <Stack sx={{ minWidth: 60, pt: 1 }} spacing={0.25}>
  623. <Typography sx={{ fontWeight: 600 }}>{t("Truck X")}</Typography>
  624. <Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.2 }}>
  625. {t("Before today")}
  626. </Typography>
  627. </Stack>
  628. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
  629. {beforeTodayTruckXCount === 0 ? (
  630. renderNoEntry()
  631. ) : (
  632. <Button
  633. variant="outlined"
  634. onClick={() => {
  635. clearModalEtraContext();
  636. setSelectedStore("");
  637. setSelectedTruck("車線-X");
  638. setIsDefaultTruck(true);
  639. setDefaultDateScope("before");
  640. setModalTruckXFloor(ticketFloorApiKey);
  641. setModalOpen(true);
  642. }}
  643. >
  644. {`${t("車線-X")} (${beforeTodayTruckXCount})`}
  645. </Button>
  646. )}
  647. </Box>
  648. </Stack>
  649. </Grid>
  650. </Grid>
  651. ) : (
  652. <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 2, backgroundColor: "#fafafa" }}>
  653. {isLoadingEtra ? (
  654. <Typography variant="caption">{t("Loading...")}</Typography>
  655. ) : etraGroups.length === 0 ? (
  656. renderNoEntry()
  657. ) : (
  658. <Grid container spacing={2}>
  659. {etraGroups.flatMap((group) =>
  660. group.lanes.map((lane, li) => {
  661. const sid = (lane.storeId ?? "").trim();
  662. const dep = (lane.truckDepartureTime ?? "").trim();
  663. const is4F = sid.replace(/\//g, "").toUpperCase() === "4F";
  664. const labelCore =
  665. is4F && lane.loadingSequence != null
  666. ? `${t("Loading sequence n", { n: lane.loadingSequence })} (${lane.unassigned}/${lane.total})`
  667. : `${dep ? `${dep} ` : ""}${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`;
  668. const handlerName = (lane.handlerName ?? "").trim();
  669. const shopCode = (group.shopCode ?? "").trim();
  670. const shopName = (group.shopName ?? "").trim();
  671. const shopPrimary =
  672. shopCode && shopName && shopCode !== shopName
  673. ? `${shopCode} · ${shopName}`
  674. : shopName || shopCode || t("Shop");
  675. const laneSecondary = `${labelCore}${handlerName ? ` · ${handlerName}` : ""}`;
  676. const tileKey = `${shopCode}|${shopName}|${lane.truckLanceCode}|${dep}|${lane.loadingSequence ?? ""}|${li}`;
  677. return (
  678. <Grid item xs={12} sm={6} md={4} lg={3} key={tileKey}>
  679. <Button
  680. fullWidth
  681. variant="outlined"
  682. disabled={lane.unassigned === 0 || !sid}
  683. onClick={() => {
  684. setModalReleaseTypeFilter("isExtra");
  685. setModalFilterRequiredDeliveryDate(selectedDeliveryDateYmd);
  686. setModalInitialShopSearch((group.shopName || group.shopCode || "").trim() || undefined);
  687. setSelectedStore(sid);
  688. setSelectedTruck(lane.truckLanceCode);
  689. setIsDefaultTruck(false);
  690. setDefaultDateScope("today");
  691. setModalOpen(true);
  692. }}
  693. sx={{
  694. height: "100%",
  695. minHeight: 72,
  696. py: 1.25,
  697. px: 1.5,
  698. display: "flex",
  699. flexDirection: "column",
  700. alignItems: "stretch",
  701. justifyContent: "flex-start",
  702. textAlign: "left",
  703. textTransform: "none",
  704. whiteSpace: "normal",
  705. borderColor: "secondary.main",
  706. }}
  707. >
  708. <Typography
  709. variant="subtitle2"
  710. component="span"
  711. color="secondary"
  712. sx={{ fontWeight: 700, lineHeight: 1.35, wordBreak: "break-word" }}
  713. >
  714. {shopPrimary}
  715. </Typography>
  716. <Typography
  717. variant="caption"
  718. component="span"
  719. color="text.secondary"
  720. sx={{ mt: 0.5, lineHeight: 1.35, wordBreak: "break-word" }}
  721. >
  722. {laneSecondary}
  723. </Typography>
  724. </Button>
  725. </Grid>
  726. );
  727. }),
  728. )}
  729. </Grid>
  730. )}
  731. </Box>
  732. )}
  733. <ReleasedDoPickOrderSelectModal
  734. open={modalOpen}
  735. storeId={selectedStore}
  736. truck={selectedTruck}
  737. isDefaultTruck={isDefaultTruck}
  738. defaultDateScope={defaultDateScope}
  739. defaultTruckRequiredDeliveryDate={selectedDeliveryDateYmd}
  740. listBridge={workbenchReleasedListBridge}
  741. releaseTypeFilter={modalReleaseTypeFilter}
  742. filterRequiredDeliveryDate={modalFilterRequiredDeliveryDate}
  743. initialShopSearch={modalInitialShopSearch}
  744. truckXFloor={modalTruckXFloor}
  745. onClose={() => {
  746. setModalOpen(false);
  747. clearModalEtraContext();
  748. }}
  749. onAssigned={() => {
  750. if (inEtraUi) void loadEtraSummaries();
  751. else void loadSummaries();
  752. onPickOrderAssigned?.();
  753. onSwitchToDetailTab?.();
  754. }}
  755. />
  756. <WorkbenchEtraMergeDialog
  757. open={etraMergeDialogOpen}
  758. onClose={() => setEtraMergeDialogOpen(false)}
  759. initialDateYmd={selectedDeliveryDateYmd}
  760. onMerged={() => {
  761. void loadEtraSummaries();
  762. onPickOrderAssigned?.();
  763. window.dispatchEvent(new Event("pickOrderAssigned"));
  764. }}
  765. />
  766. </Box>
  767. );
  768. };
  769. export default WorkbenchFloorLanePanel;