FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

FinishedGoodFloorLanePanel.tsx 33 KiB

4ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
5ヶ月前
1ヶ月前
1ヶ月前
5ヶ月前
1ヶ月前
5ヶ月前
5ヶ月前
1ヶ月前
6ヶ月前
5ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
5ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  1. "use client";
  2. import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel ,Tooltip} from "@mui/material";
  3. import { useCallback, useEffect, useMemo, useState, useRef } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import { useSession } from "next-auth/react";
  6. import { SessionWithTokens } from "@/config/authConfig";
  7. import { fetchStoreLaneSummary,fetchReleasedDoPickOrdersForSelection,fetchReleasedDoPickOrderCountByStore, assignByLane, type StoreLaneSummary, type LaneRow, type LaneBtn } from "@/app/api/pickOrder/actions";
  8. import Swal from "sweetalert2";
  9. import dayjs from "dayjs";
  10. import ReleasedDoPickOrderSelectModal from "./ReleasedDoPickOrderSelectModal";
  11. interface Props {
  12. onPickOrderAssigned?: () => void;
  13. onSwitchToDetailTab?: () => void;
  14. }
  15. type LaneSlot4F = { truckDepartureTime: string; lane: LaneBtn };
  16. type TruckGroup4F = { truckLanceCode: string; slots: (LaneSlot4F & { sequenceIndex: number })[] };
  17. const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSwitchToDetailTab }) => {
  18. const { t } = useTranslation("pickOrder");
  19. const { data: session } = useSession() as { data: SessionWithTokens | null };
  20. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  21. const [selectedStore, setSelectedStore] = useState<string>("2/F");
  22. const [selectedTruck, setSelectedTruck] = useState<string>("");
  23. const [selectedDefaultTruck, setSelectedDefaultTruck] = useState<string>("");
  24. const [modalOpen, setModalOpen] = useState(false);
  25. const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]);
  26. const [truckCounts4F, setTruckCounts4F] = useState<{ truck: string; count: number }[]>([]);
  27. const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null);
  28. const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null);
  29. // 其他 state 旁邊加一組:
  30. const hasLoggedRef = useRef(false);
  31. const fullReadyLoggedRef = useRef(false);
  32. const pendingRef = useRef(0);
  33. const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("today");
  34. const [isLoadingSummary, setIsLoadingSummary] = useState(false);
  35. const [isAssigning, setIsAssigning] = useState(false);
  36. const [isDefaultTruck, setIsDefaultTruck] = useState(false);
  37. //const [selectedDate, setSelectedDate] = useState<string>("today");
  38. const defaultTruckCount = summary4F?.defaultTruckCount ?? 0;
  39. const [beforeTodayTruckXCount, setBeforeTodayTruckXCount] = useState(0);
  40. const [selectedDate, setSelectedDate] = useState<string>("today");
  41. const [releaseType, setReleaseType] = useState<string>("batch");
  42. const [ticketFloor, setTicketFloor] = useState<"2/F" | "4/F">("2/F");
  43. const startFullTimer = () => {
  44. if (typeof window === "undefined") return;
  45. const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
  46. if (!(window as any)[key]) {
  47. (window as any)[key] = true;
  48. console.time("[FG] FloorLanePanel full ready");
  49. }
  50. };
  51. const tryEndFullTimer = () => {
  52. if (typeof window === "undefined") return;
  53. const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
  54. if ((window as any)[key] && !fullReadyLoggedRef.current && pendingRef.current === 0) {
  55. fullReadyLoggedRef.current = true;
  56. console.timeEnd("[FG] FloorLanePanel full ready");
  57. delete (window as any)[key];
  58. }
  59. };
  60. const loadSummaries = useCallback(async () => {
  61. setIsLoadingSummary(true);
  62. pendingRef.current += 1;
  63. startFullTimer();
  64. try {
  65. // Convert selectedDate to the format needed
  66. let dateParam: string | undefined;
  67. if (selectedDate === "today") {
  68. dateParam = dayjs().format('YYYY-MM-DD');
  69. } else if (selectedDate === "tomorrow") {
  70. dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD');
  71. } else if (selectedDate === "dayAfterTomorrow") {
  72. dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD');
  73. }
  74. const [s2, s4] = await Promise.all([
  75. fetchStoreLaneSummary("2/F", dateParam, releaseType),
  76. fetchStoreLaneSummary("4/F", dateParam, releaseType)
  77. ]);
  78. setSummary2F(s2);
  79. setSummary4F(s4);
  80. } catch (error) {
  81. console.error("Error loading summaries:", error);
  82. } finally {
  83. setIsLoadingSummary(false);
  84. // ⭐ 新增:this async 结束,pending--,尝试结束 full ready 计时
  85. pendingRef.current -= 1;
  86. tryEndFullTimer();
  87. if (!hasLoggedRef.current) {
  88. hasLoggedRef.current = true;
  89. if (typeof window !== "undefined") {
  90. const key = "__FG_FLOOR_PANEL_TIMER_STARTED__" as const;
  91. if ((window as any)[key]) {
  92. console.timeEnd("[FG] FloorLanePanel initial load");
  93. delete (window as any)[key];
  94. } else {
  95. console.log("Timer '[FG] FloorLanePanel initial load' already ended or never started, skip.");
  96. }
  97. }
  98. }
  99. }
  100. }, [selectedDate, releaseType]);
  101. // 初始化
  102. useEffect(() => {
  103. loadSummaries();
  104. }, [loadSummaries]);
  105. useEffect(() => {
  106. const loadCounts = async () => {
  107. pendingRef.current += 1;
  108. startFullTimer();
  109. try {
  110. const [list2F, list4F] = await Promise.all([
  111. fetchReleasedDoPickOrdersForSelection(undefined, "2/F"),
  112. fetchReleasedDoPickOrdersForSelection(undefined, "4/F"),
  113. ]);
  114. const groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
  115. const map: Record<string, number> = {};
  116. list.forEach((item) => {
  117. const t = item.truckLanceCode || "-";
  118. map[t] = (map[t] || 0) + 1;
  119. });
  120. return Object.entries(map)
  121. .map(([truck, count]) => ({ truck, count }))
  122. .sort((a, b) => a.truck.localeCompare(b.truck));
  123. };
  124. setTruckCounts2F(groupByTruck(list2F));
  125. setTruckCounts4F(groupByTruck(list4F));
  126. } catch (e) {
  127. console.error("Error loading counts:", e);
  128. setTruckCounts2F([]);
  129. setTruckCounts4F([]);
  130. }finally {
  131. // ⭐ 新增:结束时 pending--,尝试结束 full ready 计时
  132. pendingRef.current -= 1;
  133. tryEndFullTimer();
  134. }
  135. };
  136. loadCounts();
  137. }, [loadSummaries]);
  138. useEffect(() => {
  139. const loadBeforeTodayTruckX = async () => {
  140. pendingRef.current += 1;
  141. startFullTimer();
  142. try {
  143. const list = await fetchReleasedDoPickOrdersForSelection(
  144. undefined, // shopName
  145. undefined, // storeId: Truck X 的 store_id 是 null
  146. "車線-X" // 只看 Truck X
  147. );
  148. setBeforeTodayTruckXCount(list.length);
  149. } catch (e) {
  150. console.error("Error loading beforeTodayTruckX:", e);
  151. setBeforeTodayTruckXCount(0);
  152. }finally {
  153. // ⭐ 新增:结束时 pending--,尝试结束 full ready 计时
  154. pendingRef.current -= 1;
  155. tryEndFullTimer();
  156. }
  157. };
  158. loadBeforeTodayTruckX();
  159. }, []);
  160. const handleAssignByLane = useCallback(async (
  161. storeId: string,
  162. truckDepartureTime: string,
  163. truckLanceCode: string,
  164. loadingSequence: number | null | undefined,
  165. requiredDate: string
  166. ) => {
  167. if (!currentUserId) {
  168. console.error("Missing user id in session");
  169. return;
  170. }
  171. let dateParam: string | undefined;
  172. if (requiredDate === "today") {
  173. dateParam = dayjs().format('YYYY-MM-DD');
  174. } else if (requiredDate === "tomorrow") {
  175. dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD');
  176. } else if (requiredDate === "dayAfterTomorrow") {
  177. dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD');
  178. }
  179. setIsAssigning(true);
  180. try {
  181. const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, loadingSequence ?? null, dateParam);
  182. if (res.code === "SUCCESS") {
  183. console.log(" Successfully assigned pick order from lane", truckLanceCode);
  184. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  185. loadSummaries(); // 刷新按钮状态
  186. onPickOrderAssigned?.();
  187. onSwitchToDetailTab?.();
  188. } else if (res.code === "USER_BUSY") {
  189. Swal.fire({
  190. icon: "warning",
  191. title: t("Warning"),
  192. text: t("You already have a pick order in progess. Please complete it first before taking next pick order."),
  193. confirmButtonText: t("Confirm"),
  194. confirmButtonColor: "#8dba00"
  195. });
  196. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  197. } else if (res.code === "NO_ORDERS") {
  198. Swal.fire({
  199. icon: "info",
  200. title: t("Info"),
  201. text: t("No available pick order(s) for this lane."),
  202. confirmButtonText: t("Confirm"),
  203. confirmButtonColor: "#8dba00"
  204. });
  205. } else {
  206. console.log("ℹ️ Assignment result:", res.message);
  207. }
  208. } catch (error) {
  209. console.error("❌ Error assigning by lane:", error);
  210. Swal.fire({
  211. icon: "error",
  212. title: t("Error"),
  213. text: t("Error occurred during assignment."),
  214. confirmButtonText: t("Confirm"),
  215. confirmButtonColor: "#8dba00"
  216. });
  217. } finally {
  218. setIsAssigning(false);
  219. }
  220. }, [currentUserId, t, selectedDate, onPickOrderAssigned, onSwitchToDetailTab, loadSummaries]);
  221. const handleLaneButtonClick = useCallback(async (
  222. storeId: string,
  223. truckDepartureTime: string,
  224. truckLanceCode: string,
  225. loadingSequence: number | null | undefined,
  226. requiredDate: string,
  227. unassigned: number,
  228. total: number
  229. ) => {
  230. // Format the date for display
  231. let dateDisplay: string;
  232. if (requiredDate === "today") {
  233. dateDisplay = dayjs().format('YYYY-MM-DD');
  234. } else if (requiredDate === "tomorrow") {
  235. dateDisplay = dayjs().add(1, 'day').format('YYYY-MM-DD');
  236. } else if (requiredDate === "dayAfterTomorrow") {
  237. dateDisplay = dayjs().add(2, 'day').format('YYYY-MM-DD');
  238. } else {
  239. dateDisplay = requiredDate;
  240. }
  241. // Show confirmation dialog
  242. const result = await Swal.fire({
  243. title: t("Confirm Assignment"),
  244. html: `
  245. <div style="text-align: left; padding: 10px 0;">
  246. <p><strong>${t("Store")}:</strong> ${storeId}</p>
  247. <p><strong>${t("Lane Code")}:</strong> ${truckLanceCode}</p>
  248. ${loadingSequence != null ? `<p><strong>${t("Loading Sequence")}:</strong> ${loadingSequence}</p>` : ``}
  249. <p><strong>${t("Departure Time")}:</strong> ${truckDepartureTime}</p>
  250. <p><strong>${t("Required Date")}:</strong> ${dateDisplay}</p>
  251. <p><strong>${t("Available Orders")}:</strong> ${unassigned}/${total}</p>
  252. </div>
  253. `,
  254. icon: "question",
  255. showCancelButton: true,
  256. confirmButtonText: t("Confirm"),
  257. cancelButtonText: t("Cancel"),
  258. confirmButtonColor: "#8dba00",
  259. cancelButtonColor: "#F04438",
  260. reverseButtons: true
  261. });
  262. // Only proceed if user confirmed
  263. if (result.isConfirmed) {
  264. await handleAssignByLane(storeId, truckDepartureTime, truckLanceCode, loadingSequence, requiredDate);
  265. }
  266. }, [handleAssignByLane, t]);
  267. const getDateLabel = (offset: number) => {
  268. return dayjs().add(offset, 'day').format('YYYY-MM-DD');
  269. };
  270. // Flatten rows to create one box per lane
  271. const flattenRows = (rows: any[]) => {
  272. const flattened: any[] = [];
  273. rows.forEach(row => {
  274. row.lanes.forEach((lane: any) => {
  275. flattened.push({
  276. truckDepartureTime: row.truckDepartureTime,
  277. lane: lane
  278. });
  279. });
  280. });
  281. return flattened;
  282. };
  283. /** 4/F:依車線匯總,同車多筆依 API 出現順序為裝載序(出發時間相同時仍可分序)。 */
  284. const truckGroups4F = useMemo((): TruckGroup4F[] => {
  285. const rows = summary4F?.rows as LaneRow[] | undefined;
  286. if (!rows?.length) return [];
  287. const map = new Map<string, LaneSlot4F[]>();
  288. for (const row of rows) {
  289. for (const lane of row.lanes) {
  290. const code = lane.truckLanceCode;
  291. const list = map.get(code);
  292. const slot: LaneSlot4F = { truckDepartureTime: row.truckDepartureTime, lane };
  293. if (list) list.push(slot);
  294. else map.set(code, [slot]);
  295. }
  296. }
  297. return Array.from(map.entries())
  298. .sort(([a], [b]) => a.localeCompare(b))
  299. .map(([truckLanceCode, slots]) => ({
  300. truckLanceCode,
  301. slots: slots
  302. .slice()
  303. .sort((a, b) => (a.lane.loadingSequence ?? 999) - (b.lane.loadingSequence ?? 999))
  304. .map((s: LaneSlot4F, i: number) => ({ ...s, sequenceIndex: i + 1 })),
  305. }));
  306. }, [summary4F?.rows]);
  307. return (
  308. <Box sx={{ mb: 2 }}>
  309. {/* Date Selector Dropdown and Legend */}
  310. <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}>
  311. <Box sx={{ maxWidth: 300 }}>
  312. <FormControl fullWidth size="small">
  313. <InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
  314. <Select
  315. labelId="date-select-label"
  316. id="date-select"
  317. value={selectedDate}
  318. label={t("Select Date")}
  319. onChange={(e) => { {
  320. setSelectedDate(e.target.value);
  321. loadSummaries();
  322. }}}
  323. >
  324. <MenuItem value="today">
  325. {t("Today")} ({getDateLabel(0)})
  326. </MenuItem>
  327. <MenuItem value="tomorrow">
  328. {t("Tomorrow")} ({getDateLabel(1)})
  329. </MenuItem>
  330. <MenuItem value="dayAfterTomorrow">
  331. {t("Day After Tomorrow")} ({getDateLabel(2)})
  332. </MenuItem>
  333. </Select>
  334. </FormControl>
  335. </Box>
  336. <Box sx={{minWidth: 140, maxWidth: 300 }}>
  337. <FormControl fullWidth size="small">
  338. <InputLabel id="release-type-select-label">{t("Release Type")}</InputLabel>
  339. <Select
  340. labelId="release-type-select-label"
  341. id="release-type-select"
  342. value={releaseType}
  343. label={t("Release Type")}
  344. onChange={(e) => { {
  345. setReleaseType(e.target.value);
  346. loadSummaries();
  347. }}}
  348. >
  349. <MenuItem value="batch">
  350. {t("Batch")}
  351. </MenuItem>
  352. <MenuItem value="single">
  353. {t("Single")}
  354. </MenuItem>
  355. </Select>
  356. </FormControl>
  357. </Box>
  358. <Box sx={{ minWidth: 120, maxWidth: 200 }}>
  359. <FormControl fullWidth size="small">
  360. <InputLabel id="ticket-floor-select-label">{t("Floor ticket")}</InputLabel>
  361. <Select
  362. labelId="ticket-floor-select-label"
  363. id="ticket-floor-select"
  364. value={ticketFloor}
  365. label={t("Floor ticket")}
  366. onChange={(e) => setTicketFloor(e.target.value as "2/F" | "4/F")}
  367. >
  368. <MenuItem value="2/F">{t("2F ticket")}</MenuItem>
  369. <MenuItem value="4/F">{t("4F ticket")}</MenuItem>
  370. </Select>
  371. </FormControl>
  372. </Box>
  373. <Box
  374. sx={{
  375. p: 1,
  376. backgroundColor: '#fafafa',
  377. borderRadius: 1,
  378. border: '1px solid #e0e0e0',
  379. flex: 1,
  380. maxWidth: 400
  381. }}
  382. >
  383. <Typography variant="body2" sx={{ display: 'block', color: 'text.secondary', fontWeight: 600 }}>
  384. {t("EDT - Lane Code (Unassigned/Total)")}
  385. </Typography>
  386. </Box>
  387. </Stack>
  388. {/* Grid containing both floors */}
  389. <Grid container spacing={2}>
  390. {/* 2/F 楼层面板 */}
  391. {ticketFloor === "2/F" && (
  392. <Grid item xs={12}>
  393. <Stack direction="row" spacing={2} alignItems="flex-start">
  394. {/* Floor Label */}
  395. <Typography
  396. variant="h6"
  397. sx={{
  398. fontWeight: 600,
  399. minWidth: 60,
  400. pt: 1
  401. }}
  402. >
  403. 2/F
  404. </Typography>
  405. {/* Content Box */}
  406. <Box
  407. sx={{
  408. border: '1px solid #e0e0e0',
  409. borderRadius: 1,
  410. p: 1,
  411. backgroundColor: '#fafafa',
  412. flex: 1
  413. }}
  414. >
  415. {isLoadingSummary ? (
  416. <Typography variant="caption"> {t("Loading...")}</Typography>
  417. ) : !summary2F?.rows || summary2F.rows.length === 0 ? (
  418. <Typography
  419. variant="body2"
  420. color="text.secondary"
  421. sx={{
  422. fontWeight: 600,
  423. fontSize: '1rem',
  424. textAlign: 'center',
  425. py: 1
  426. }}
  427. >
  428. {t("No entries available")}
  429. </Typography>
  430. ) : (
  431. <Grid container spacing={1}>
  432. {summary2F.rows.map((row) => (
  433. <Grid item xs={12} key={row.truckDepartureTime}>
  434. <Stack
  435. direction={{ xs: "column", sm: "row" }}
  436. spacing={1}
  437. alignItems={{ xs: "stretch", sm: "center" }}
  438. sx={{
  439. border: "1px solid #e0e0e0",
  440. borderRadius: 0.5,
  441. p: 1,
  442. backgroundColor: "#fff",
  443. }}
  444. >
  445. <Typography
  446. variant="body2"
  447. sx={{
  448. fontWeight: 600,
  449. fontSize: "1rem",
  450. minWidth: { sm: 60 },
  451. whiteSpace: "nowrap",
  452. pt: { xs: 0, sm: 0.5 },
  453. }}
  454. >
  455. {row.truckDepartureTime}
  456. </Typography>
  457. <Stack
  458. direction="row"
  459. flexWrap="wrap"
  460. sx={{ flex: 1, gap: 1 }}
  461. >
  462. {row.lanes.map((lane) => (
  463. <Button
  464. key={`${row.truckDepartureTime}-${lane.truckLanceCode}`}
  465. variant="outlined"
  466. size="medium"
  467. disabled={lane.unassigned === 0 || isAssigning}
  468. onClick={() =>
  469. handleLaneButtonClick(
  470. "2/F",
  471. row.truckDepartureTime,
  472. lane.truckLanceCode,
  473. null,
  474. selectedDate,
  475. lane.unassigned,
  476. lane.total
  477. )
  478. }
  479. sx={{
  480. fontSize: "1.1rem",
  481. py: 1,
  482. px: 1.5,
  483. borderWidth: 1,
  484. borderColor: "#ccc",
  485. fontWeight: 500,
  486. "&:hover": {
  487. borderColor: "#999",
  488. backgroundColor: "#f5f5f5",
  489. },
  490. }}
  491. >
  492. {`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
  493. </Button>
  494. ))}
  495. </Stack>
  496. </Stack>
  497. </Grid>
  498. ))}
  499. </Grid>
  500. )}
  501. </Box>
  502. </Stack>
  503. </Grid>
  504. )}
  505. {ticketFloor === "4/F" && (
  506. <Grid item xs={12}>
  507. <Stack direction="row" spacing={2} alignItems="flex-start">
  508. {/* Floor Label */}
  509. <Typography
  510. variant="h6"
  511. sx={{
  512. fontWeight: 600,
  513. minWidth: 60,
  514. pt: 1
  515. }}
  516. >
  517. 4/F
  518. </Typography>
  519. {/* Content Box */}
  520. <Box
  521. sx={{
  522. border: '1px solid #e0e0e0',
  523. borderRadius: 1,
  524. p: 1,
  525. backgroundColor: '#fafafa',
  526. flex: 1
  527. }}
  528. >
  529. {isLoadingSummary ? (
  530. <Typography variant="caption">{t("Loading...")}</Typography>
  531. ) : truckGroups4F.length === 0 ? (
  532. <Typography
  533. variant="body2"
  534. color="text.secondary"
  535. sx={{
  536. fontWeight: 600,
  537. fontSize: '1rem',
  538. textAlign: 'center',
  539. py: 1
  540. }}
  541. >
  542. {t("No entries available")}
  543. </Typography>
  544. ) : (
  545. <Grid container spacing={1}>
  546. {truckGroups4F.map(({ truckLanceCode, slots }) => (
  547. <Grid item xs={12} key={truckLanceCode}>
  548. <Stack
  549. direction={{ xs: "column", sm: "row" }}
  550. spacing={1}
  551. alignItems={{ xs: "stretch", sm: "center" }}
  552. sx={{
  553. border: "1px solid #e0e0e0",
  554. borderRadius: 0.5,
  555. p: 1,
  556. backgroundColor: "#fff",
  557. }}
  558. >
  559. <Typography
  560. variant="body2"
  561. sx={{
  562. fontWeight: 700,
  563. fontSize: "1rem",
  564. minWidth: { sm: 160 },
  565. pt: { xs: 0, sm: 0.5 },
  566. }}
  567. >
  568. {truckLanceCode}
  569. </Typography>
  570. <Stack
  571. direction="row"
  572. flexWrap="wrap"
  573. sx={{ flex: 1, gap: 1 }}
  574. >
  575. {slots.map((slot) => (
  576. <Button
  577. key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.lane.truckLanceCode}-${slot.truckDepartureTime}`}
  578. variant="outlined"
  579. size="medium"
  580. disabled={slot.lane.unassigned === 0 || isAssigning}
  581. onClick={() =>
  582. handleLaneButtonClick(
  583. "4/F",
  584. slot.truckDepartureTime,
  585. slot.lane.truckLanceCode,
  586. slot.lane.loadingSequence ?? null,
  587. selectedDate,
  588. slot.lane.unassigned,
  589. slot.lane.total
  590. )
  591. }
  592. sx={{
  593. fontSize: "1rem",
  594. py: 0.75,
  595. px: 1.25,
  596. borderWidth: 1,
  597. borderColor: "#ccc",
  598. fontWeight: 500,
  599. "&:hover": {
  600. borderColor: "#999",
  601. backgroundColor: "#f5f5f5",
  602. },
  603. }}
  604. >
  605. {`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})`}
  606. </Button>
  607. ))}
  608. </Stack>
  609. </Stack>
  610. </Grid>
  611. ))}
  612. </Grid>
  613. )}
  614. </Box>
  615. </Stack>
  616. </Grid>
  617. )}
  618. {/* 4/F Today default lane*/}
  619. <Grid item xs={12}>
  620. <Stack direction="row" spacing={2} alignItems="flex-start">
  621. <Typography sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{t("Truck X")}</Typography>
  622. <Box
  623. sx={{
  624. border: '1px solid #e0e0e0',
  625. borderRadius: 1,
  626. p: 1,
  627. backgroundColor: '#fafafa',
  628. flex: 1
  629. }}
  630. >
  631. {defaultTruckCount === 0 ? (
  632. <Typography >{t("No entries available")}</Typography>
  633. ) : (
  634. <Button
  635. variant="outlined"
  636. size="medium"
  637. onClick={() => {
  638. // Truck X 綁 4/F,如果你要放在 4/F 區塊
  639. setSelectedStore("");
  640. // 真正的 Truck lane code:車線-X
  641. setSelectedTruck("車線-X");
  642. // 告訴 modal 這是 default truck 模式
  643. setIsDefaultTruck(true);
  644. // 打開 modal
  645. setModalOpen(true);
  646. setDefaultDateScope("today");
  647. }}
  648. >
  649. {`${t("Truck X")} (${defaultTruckCount})`}
  650. </Button>
  651. )}
  652. </Box>
  653. </Stack>
  654. </Grid>
  655. {/* 2/F 未完成已放單 - 與上方相同 UI */}
  656. <Grid item xs={12}>
  657. <Box
  658. sx={{
  659. py: 2,
  660. mt: 1,
  661. mb: 0.5,
  662. borderTop: "1px solid #e0e0e0",
  663. }}
  664. >
  665. <Typography
  666. variant="subtitle1"
  667. sx={{ fontWeight: 600, mb: 0.5 }}
  668. >
  669. {t("Not yet finished released do pick orders")}
  670. </Typography>
  671. <Typography variant="body2" color="text.secondary">
  672. {t("Released orders not yet completed - click lane to select and assign")}
  673. </Typography>
  674. </Box>
  675. </Grid>
  676. {ticketFloor === "2/F" && (
  677. <Grid item xs={12}>
  678. <Stack direction="row" spacing={2} alignItems="flex-start">
  679. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>
  680. 2/F
  681. </Typography>
  682. <Box
  683. sx={{
  684. border: "1px solid #e0e0e0",
  685. borderRadius: 1,
  686. p: 1,
  687. backgroundColor: "#fafafa",
  688. flex: 1,
  689. }}
  690. >
  691. {truckCounts2F.length === 0 ? (
  692. <Typography
  693. variant="body2"
  694. color="text.secondary"
  695. sx={{
  696. fontWeight: 600,
  697. fontSize: "1rem",
  698. textAlign: "center",
  699. py: 1,
  700. }}
  701. >
  702. {t("No entries available")}
  703. </Typography>
  704. ) : (
  705. <Grid container spacing={1}>
  706. {truckCounts2F.map(({ truck, count }) => (
  707. <Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}>
  708. <Button
  709. variant="outlined"
  710. size="medium"
  711. onClick={() => {
  712. setIsDefaultTruck(false);
  713. setSelectedStore("2/F");
  714. setSelectedTruck(truck);
  715. setModalOpen(true);
  716. }}
  717. sx={{
  718. flex: 1,
  719. fontSize: "1.1rem",
  720. py: 1,
  721. px: 1.5,
  722. borderWidth: 1,
  723. borderColor: "#ccc",
  724. fontWeight: 500,
  725. "&:hover": {
  726. borderColor: "#999",
  727. backgroundColor: "#f5f5f5",
  728. },
  729. }}
  730. >
  731. {`${truck} (${count})`}
  732. </Button>
  733. </Grid>
  734. ))}
  735. </Grid>
  736. )}
  737. </Box>
  738. </Stack>
  739. </Grid>
  740. )}
  741. {ticketFloor === "4/F" && (
  742. <Grid item xs={12}>
  743. <Stack direction="row" spacing={2} alignItems="flex-start">
  744. <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>
  745. 4/F
  746. </Typography>
  747. <Box
  748. sx={{
  749. border: "1px solid #e0e0e0",
  750. borderRadius: 1,
  751. p: 1,
  752. backgroundColor: "#fafafa",
  753. flex: 1,
  754. }}
  755. >
  756. {truckCounts4F.length === 0 ? (
  757. <Typography
  758. variant="body2"
  759. color="text.secondary"
  760. sx={{
  761. fontWeight: 600,
  762. fontSize: "1rem",
  763. textAlign: "center",
  764. py: 1,
  765. }}
  766. >
  767. {t("No entries available")}
  768. </Typography>
  769. ) : (
  770. <Grid container spacing={1}>
  771. {truckCounts4F.map(({ truck, count }) => (
  772. <Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}>
  773. <Button
  774. variant="outlined"
  775. size="medium"
  776. onClick={() => {
  777. setIsDefaultTruck(false);
  778. setSelectedStore("4/F");
  779. setSelectedTruck(truck);
  780. setModalOpen(true);
  781. }}
  782. sx={{
  783. flex: 1,
  784. fontSize: "1.1rem",
  785. py: 1,
  786. px: 1.5,
  787. borderWidth: 1,
  788. borderColor: "#ccc",
  789. fontWeight: 500,
  790. "&:hover": {
  791. borderColor: "#999",
  792. backgroundColor: "#f5f5f5",
  793. },
  794. }}
  795. >
  796. {`${truck} (${count})`}
  797. </Button>
  798. </Grid>
  799. ))}
  800. </Grid>
  801. )}
  802. </Box>
  803. </Stack>
  804. </Grid>
  805. )}
  806. <Grid item xs={12}>
  807. <Stack direction="row" spacing={2}>
  808. <Typography sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{t("Truck X")} </Typography>
  809. <Box
  810. sx={{
  811. border: '1px solid #e0e0e0',
  812. borderRadius: 1,
  813. p: 1,
  814. backgroundColor: '#fafafa',
  815. flex: 1
  816. }}
  817. >
  818. {beforeTodayTruckXCount === 0 ? (
  819. <Typography>{t("No entries available")}</Typography>
  820. ) : (
  821. <Button
  822. variant="outlined"
  823. size="medium"
  824. onClick={() => {
  825. setSelectedStore("4/F"); // 或用專門標示 Truck X
  826. setSelectedTruck("車線-X");
  827. setIsDefaultTruck(true);
  828. setDefaultDateScope("before"); // 類似上一輪說的 dateScope
  829. setModalOpen(true);
  830. }}
  831. >
  832. {`${t("Truck X")} (${beforeTodayTruckXCount})`}
  833. </Button>
  834. )}
  835. </Box>
  836. </Stack>
  837. </Grid>
  838. <ReleasedDoPickOrderSelectModal
  839. open={modalOpen}
  840. storeId={selectedStore}
  841. truck={selectedTruck}
  842. isDefaultTruck={isDefaultTruck}
  843. defaultDateScope={defaultDateScope}
  844. onClose={() => setModalOpen(false)}
  845. onAssigned={() => {
  846. loadSummaries();
  847. const loadCounts = async () => {
  848. try {
  849. const [list2F, list4F] = await Promise.all([
  850. fetchReleasedDoPickOrdersForSelection(undefined, "2/F"),
  851. fetchReleasedDoPickOrdersForSelection(undefined, "4/F"),
  852. ]);
  853. const groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
  854. const map: Record<string, number> = {};
  855. list.forEach((item) => {
  856. const t = item.truckLanceCode || "-";
  857. map[t] = (map[t] || 0) + 1;
  858. });
  859. return Object.entries(map)
  860. .map(([truck, count]) => ({ truck, count }))
  861. .sort((a, b) => a.truck.localeCompare(b.truck));
  862. };
  863. setTruckCounts2F(groupByTruck(list2F));
  864. setTruckCounts4F(groupByTruck(list4F));
  865. } catch (e) {
  866. setTruckCounts2F([]);
  867. setTruckCounts4F([]);
  868. }
  869. };
  870. loadCounts();
  871. onPickOrderAssigned?.();
  872. onSwitchToDetailTab?.();
  873. }}
  874. />
  875. </Grid>
  876. </Box>
  877. );
  878. };
  879. export default FinishedGoodFloorLanePanel;