FPSMS-frontend
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

FinishedGoodFloorLanePanel.tsx 33 KiB

vor 6 Monaten
vor 4 Monaten
vor 6 Monaten
vor 6 Monaten
vor 1 Monat
vor 6 Monaten
vor 5 Monaten
vor 6 Monaten
vor 5 Monaten
vor 6 Monaten
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 6 Monaten
vor 1 Monat
vor 6 Monaten
vor 1 Monat
vor 5 Monaten
vor 1 Monat
vor 6 Monaten
vor 1 Monat
vor 5 Monaten
vor 6 Monaten
vor 1 Monat
vor 6 Monaten
vor 5 Monaten
vor 6 Monaten
vor 6 Monaten
vor 5 Monaten
vor 6 Monaten
vor 1 Monat
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 5 Monaten
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 2 Monaten
vor 2 Monaten
vor 5 Monaten
vor 2 Monaten
vor 2 Monaten
vor 6 Monaten
vor 2 Monaten
vor 6 Monaten
vor 2 Monaten
vor 6 Monaten
vor 2 Monaten
vor 6 Monaten
vor 2 Monaten
vor 2 Monaten
vor 2 Monaten
vor 2 Monaten
vor 6 Monaten
vor 6 Monaten
vor 5 Monaten
vor 6 Monaten
vor 1 Monat
vor 6 Monaten
vor 6 Monaten
vor 4 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 6 Monaten
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 1 Monat
vor 6 Monaten
vor 6 Monaten
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;