FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

438 rivejä
14 KiB

  1. "use client";
  2. import {
  3. Autocomplete,
  4. Badge,
  5. Box,
  6. Button,
  7. CircularProgress,
  8. Tab,
  9. Tabs,
  10. TextField,
  11. Tooltip,
  12. Typography,
  13. } from "@mui/material";
  14. import React, { Suspense } from "react";
  15. import { usePathname, useRouter, useSearchParams } from "next/navigation";
  16. import DoWorkbenchPickShell from "./DoWorkbenchPickShell";
  17. import type { PrinterCombo } from "@/app/api/settings/printer";
  18. import GoodPickExecutionWorkbenchRecord from "./GoodPickExecutionWorkbenchRecord";
  19. import { useTranslation } from "react-i18next";
  20. import WorkbenchTicketReleaseTableTab from "./WorkbenchTicketReleaseTable";
  21. import { Stack } from "@mui/system";
  22. import Swal from "sweetalert2";
  23. import { printDNWorkbench } from "@/app/api/do/actions";
  24. import {
  25. fetchWorkbenchEtraLaneSummary,
  26. fetchWorkbenchReleasedDoPickOrdersForSelectionToday,
  27. type WorkbenchEtraShopLaneGroup,
  28. } from "@/app/api/doworkbench/actions";
  29. import FinishedGoodCartonDashboardTab from "../FinishedGoodSearch/FinishedGoodCartonDashboardTab";
  30. import TruckRoutingSummaryTabWorkbench from "./TruckRoutingSummaryTabWorkbench";
  31. import {
  32. DEFAULT_WORKBENCH_LANE_PANEL_PREFS,
  33. type WorkbenchLanePanelPrefs,
  34. } from "./workbenchLanePanelPrefs";
  35. const ALLOWED_WORKBENCH_TABS = new Set([0, 1, 2, 3, 4, 5, 6]);
  36. /** Backend Etra summary: each lane `total` = distinct incomplete (`pending`/`released`) `delivery_order_pick_order` rows for that day. */
  37. function sumIncompleteEtraDopoTickets(groups: WorkbenchEtraShopLaneGroup[]): number {
  38. let n = 0;
  39. for (const g of groups) {
  40. for (const lane of g.lanes) {
  41. n += Number(lane.total) || 0;
  42. }
  43. }
  44. return n;
  45. }
  46. type Props = {
  47. defaultTabIndex?: 0 | 1;
  48. printerCombo?: PrinterCombo[];
  49. };
  50. function TabPanel(props: { value: number; index: number; children: React.ReactNode }) {
  51. const { value, index, children } = props;
  52. if (value !== index) return null;
  53. return <Box sx={{ pt: 2 }}>{children}</Box>;
  54. }
  55. /** FP-MTMS Version Checklist | Functions Ref. No. 20 | v1.0.0 | 2026-07-19 */
  56. const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCombo = [] }) => {
  57. const searchParams = useSearchParams();
  58. const router = useRouter();
  59. const pathname = usePathname();
  60. const urlTabStr = searchParams.get("tab");
  61. const urlTicketRaw = searchParams.get("ticketNo");
  62. const urlTicketNo =
  63. urlTicketRaw && urlTicketRaw.trim() !== ""
  64. ? decodeURIComponent(urlTicketRaw.trim())
  65. : null;
  66. const urlTargetDateRaw = searchParams.get("targetDate");
  67. const urlTargetDate =
  68. urlTargetDateRaw && urlTargetDateRaw.trim() !== ""
  69. ? decodeURIComponent(urlTargetDateRaw.trim())
  70. : null;
  71. /** Item Tracing deep-link only; DO finish redirect must not set this. */
  72. const urlOpenDetail = searchParams.get("openDetail") === "1";
  73. const [tab, setTab] = React.useState<number>(defaultTabIndex);
  74. const [lanePanelPrefs, setLanePanelPrefs] = React.useState<WorkbenchLanePanelPrefs>(
  75. DEFAULT_WORKBENCH_LANE_PANEL_PREFS,
  76. );
  77. const [a4Printer, setA4Printer] = React.useState<PrinterCombo | null>(null);
  78. const [labelPrinter, setLabelPrinter] = React.useState<PrinterCombo | null>(null);
  79. const [releasedOrderCount, setReleasedOrderCount] = React.useState(0);
  80. const [etraIncompleteDopoCount, setEtraIncompleteDopoCount] = React.useState(0);
  81. const { t } = useTranslation( );
  82. const a4Printers = React.useMemo(
  83. () => (printerCombo || []).filter((printer) => printer.type === "A4"),
  84. [printerCombo],
  85. );
  86. const labelPrinters = React.useMemo(
  87. () => (printerCombo || []).filter((printer) => printer.type === "Label"),
  88. [printerCombo],
  89. );
  90. const refreshWorkbenchCounts = React.useCallback(async () => {
  91. const [releasedRes, etraRes] = await Promise.allSettled([
  92. fetchWorkbenchReleasedDoPickOrdersForSelectionToday(),
  93. fetchWorkbenchEtraLaneSummary(),
  94. ]);
  95. if (releasedRes.status === "fulfilled") {
  96. setReleasedOrderCount(releasedRes.value.length);
  97. } else {
  98. console.error("Error fetching workbench released order count:", releasedRes.reason);
  99. setReleasedOrderCount(0);
  100. }
  101. if (etraRes.status === "fulfilled") {
  102. setEtraIncompleteDopoCount(sumIncompleteEtraDopoTickets(etraRes.value));
  103. } else {
  104. console.error("Error fetching workbench Etra incomplete count:", etraRes.reason);
  105. setEtraIncompleteDopoCount(0);
  106. }
  107. }, []);
  108. React.useEffect(() => {
  109. void refreshWorkbenchCounts();
  110. }, [refreshWorkbenchCounts]);
  111. React.useEffect(() => {
  112. const onAssigned = () => {
  113. void refreshWorkbenchCounts();
  114. };
  115. window.addEventListener("pickOrderAssigned", onAssigned);
  116. return () => window.removeEventListener("pickOrderAssigned", onAssigned);
  117. }, [refreshWorkbenchCounts]);
  118. /** Opening Etra tab refreshes badge (completion does not always dispatch `pickOrderAssigned`). */
  119. const etraTabMountSkipRef = React.useRef(false);
  120. React.useEffect(() => {
  121. if (!etraTabMountSkipRef.current) {
  122. etraTabMountSkipRef.current = true;
  123. return;
  124. }
  125. if (tab === 1) void refreshWorkbenchCounts();
  126. }, [tab, refreshWorkbenchCounts]);
  127. React.useEffect(() => {
  128. if (urlTabStr == null || urlTabStr === "") return;
  129. const n = parseInt(urlTabStr, 10);
  130. if (!Number.isNaN(n) && ALLOWED_WORKBENCH_TABS.has(n)) {
  131. setTab(n);
  132. }
  133. }, [urlTabStr]);
  134. const handleTabChange = React.useCallback(
  135. (_: React.SyntheticEvent, newTab: number) => {
  136. setTab(newTab);
  137. const params = new URLSearchParams(searchParams.toString());
  138. params.set("tab", String(newTab));
  139. /* ticketNo / targetDate / openDetail deep-link for Finished Good Record tabs */
  140. if (newTab !== 2 && newTab !== 3) {
  141. params.delete("ticketNo");
  142. params.delete("targetDate");
  143. params.delete("openDetail");
  144. }
  145. const qs = params.toString();
  146. router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
  147. },
  148. [pathname, router, searchParams],
  149. );
  150. const handleAllDraft = React.useCallback(async () => {
  151. try {
  152. if (!a4Printer) {
  153. await Swal.fire({
  154. position: "bottom-end",
  155. icon: "warning",
  156. text: t("Please select a printer first"),
  157. showConfirmButton: false,
  158. timer: 1500,
  159. });
  160. return;
  161. }
  162. const releasedOrders = await fetchWorkbenchReleasedDoPickOrdersForSelectionToday();
  163. if (releasedOrders.length === 0) {
  164. await Swal.fire({
  165. title: "",
  166. text: t("No released pick order records found."),
  167. icon: "info",
  168. });
  169. return;
  170. }
  171. const confirmResult = await Swal.fire({
  172. title: t("Batch Print"),
  173. text: `${t("Confirm print: (")}${releasedOrders.length}${t("piece(s))")}`,
  174. icon: "question",
  175. showCancelButton: true,
  176. confirmButtonText: t("Confirm"),
  177. cancelButtonText: t("Cancel"),
  178. confirmButtonColor: "#8dba00",
  179. cancelButtonColor: "#F04438",
  180. });
  181. if (!confirmResult.isConfirmed) return;
  182. Swal.fire({
  183. title: t("Printing..."),
  184. text: t("Please wait..."),
  185. allowOutsideClick: false,
  186. allowEscapeKey: false,
  187. didOpen: () => Swal.showLoading(),
  188. });
  189. for (const order of releasedOrders) {
  190. const printRequest = {
  191. printerId: a4Printer.id,
  192. printQty: 1,
  193. isDraft: true,
  194. numOfCarton: 0,
  195. deliveryOrderPickOrderId: order.id,
  196. };
  197. const response = await printDNWorkbench(printRequest);
  198. if (!response.success) {
  199. console.error(`Workbench print draft failed for deliveryOrderPickOrderId ${order.id}:`, response.message);
  200. }
  201. }
  202. Swal.close();
  203. await Swal.fire({
  204. position: "bottom-end",
  205. icon: "success",
  206. text: t("Printed Successfully."),
  207. showConfirmButton: false,
  208. timer: 1500,
  209. });
  210. await refreshWorkbenchCounts();
  211. } catch (error) {
  212. Swal.close();
  213. console.error("Error in workbench handleAllDraft:", error);
  214. await Swal.fire({
  215. icon: "error",
  216. text: t("An error occurred during batch print"),
  217. });
  218. }
  219. }, [a4Printer, t, refreshWorkbenchCounts]);
  220. return (
  221. <Box>
  222. <Stack
  223. direction="row"
  224. spacing={2}
  225. sx={{
  226. alignItems: "center",
  227. justifyContent: "flex-end",
  228. flexWrap: "wrap",
  229. rowGap: 1,
  230. mb: 1,
  231. }}
  232. >
  233. <Typography variant="body2" sx={{ minWidth: "fit-content" }}>
  234. {t("A4 Printer")}:
  235. </Typography>
  236. <Autocomplete
  237. options={a4Printers}
  238. getOptionLabel={(option) => option.name || option.label || option.code || `Printer ${option.id}`}
  239. value={a4Printer}
  240. onChange={(_, newValue) => setA4Printer(newValue)}
  241. sx={{ minWidth: 200 }}
  242. size="small"
  243. renderInput={(params) => (
  244. <TextField
  245. {...params}
  246. placeholder={t("A4 Printer")}
  247. inputProps={{ ...params.inputProps, readOnly: true }}
  248. />
  249. )}
  250. />
  251. <Typography variant="body2" sx={{ minWidth: "fit-content" }}>
  252. {t("Label Printer")}:
  253. </Typography>
  254. <Autocomplete
  255. options={labelPrinters}
  256. getOptionLabel={(option) => option.name || option.label || option.code || `Printer ${option.id}`}
  257. value={labelPrinter}
  258. onChange={(_, newValue) => setLabelPrinter(newValue)}
  259. sx={{ minWidth: 200 }}
  260. size="small"
  261. renderInput={(params) => (
  262. <TextField
  263. {...params}
  264. placeholder={t("Label Printer")}
  265. inputProps={{ ...params.inputProps, readOnly: true }}
  266. />
  267. )}
  268. />
  269. <Button
  270. variant="contained"
  271. onClick={() => void handleAllDraft()}
  272. >
  273. {`${t("Print All Draft")} (${releasedOrderCount})`}
  274. </Button>
  275. </Stack>
  276. <Tabs
  277. value={tab}
  278. onChange={handleTabChange}
  279. sx={{
  280. borderBottom: 1,
  281. borderColor: "divider",
  282. "& .MuiTabs-flexContainer": {
  283. columnGap: 2,
  284. rowGap: 1,
  285. },
  286. /* 否則 Tab 內 overflow:hidden 會把 Badge 數字裁成紅點 */
  287. "& .MuiTab-root": {
  288. overflow: "visible",
  289. minWidth: "auto",
  290. px: 2,
  291. },
  292. }}
  293. >
  294. <Tab label={t("Pick Order Detail")} value={0} />
  295. <Tab
  296. value={1}
  297. sx={{
  298. overflow: "visible",
  299. /* 徽章在標籤右側外凸,預留空間避免與下一個 Tab 貼死 */
  300. pr: etraIncompleteDopoCount > 99 ? 5 : etraIncompleteDopoCount > 0 ? 4 : 2,
  301. }}
  302. label={
  303. <Tooltip
  304. title={
  305. etraIncompleteDopoCount > 0
  306. ? t("Etra incomplete badge tooltip", { count: etraIncompleteDopoCount })
  307. : t("Etra incomplete badge tooltip none")
  308. }
  309. >
  310. <Box component="span" sx={{ display: "inline-flex", alignItems: "center" }}>
  311. <Badge
  312. color="error"
  313. variant="standard"
  314. badgeContent={etraIncompleteDopoCount > 99 ? "99+" : etraIncompleteDopoCount}
  315. invisible={etraIncompleteDopoCount === 0}
  316. sx={{
  317. "& .MuiBadge-badge": {
  318. fontWeight: 800,
  319. fontSize: "0.7rem",
  320. minWidth: 18,
  321. height: 18,
  322. lineHeight: "18px",
  323. px: 0.5,
  324. right: -8,
  325. top: 2,
  326. },
  327. }}
  328. >
  329. <Typography
  330. component="span"
  331. variant="inherit"
  332. sx={{ pr: etraIncompleteDopoCount > 0 ? 1 : 0 }}
  333. >
  334. {t("Etra Pick Order Detail")}
  335. </Typography>
  336. </Badge>
  337. </Box>
  338. </Tooltip>
  339. }
  340. />
  341. <Tab label={t("Finished Good Record")} value={2} />
  342. <Tab label={t("Finished Good Record (All)")} value={3} />
  343. <Tab label={t("Ticket Release Table")} value={4} />
  344. <Tab label={t("成品出倉出箱數量")} value={5} />
  345. <Tab label={t("送貨路線摘要")} value={6} />
  346. </Tabs>
  347. <TabPanel value={tab} index={0}>
  348. <DoWorkbenchPickShell
  349. laneMode="normal"
  350. lanePanelPrefs={lanePanelPrefs}
  351. onLanePanelPrefsChange={setLanePanelPrefs}
  352. labelPrinter={labelPrinter}
  353. />
  354. </TabPanel>
  355. <TabPanel value={tab} index={1}>
  356. <DoWorkbenchPickShell
  357. laneMode="etra"
  358. lanePanelPrefs={lanePanelPrefs}
  359. onLanePanelPrefsChange={setLanePanelPrefs}
  360. labelPrinter={labelPrinter}
  361. />
  362. </TabPanel>
  363. <TabPanel value={tab} index={2}>
  364. <GoodPickExecutionWorkbenchRecord
  365. key={`workbench-record-mine-${urlTicketNo ?? ""}-${urlTargetDate ?? ""}-${urlOpenDetail ? "1" : "0"}`}
  366. printerCombo={printerCombo}
  367. listScope="mine"
  368. a4Printer={a4Printer}
  369. labelPrinter={labelPrinter}
  370. initialTicketNo={urlTicketNo}
  371. initialTargetDate={urlTargetDate}
  372. openDetail={urlOpenDetail}
  373. />
  374. </TabPanel>
  375. <TabPanel value={tab} index={3}>
  376. <GoodPickExecutionWorkbenchRecord
  377. key={`workbench-record-all-${urlTicketNo ?? ""}-${urlTargetDate ?? ""}-${urlOpenDetail ? "1" : "0"}`}
  378. printerCombo={printerCombo}
  379. listScope="all"
  380. a4Printer={a4Printer}
  381. labelPrinter={labelPrinter}
  382. initialTicketNo={urlTicketNo}
  383. initialTargetDate={urlTargetDate}
  384. openDetail={urlOpenDetail}
  385. />
  386. </TabPanel>
  387. <TabPanel value={tab} index={4}>
  388. <WorkbenchTicketReleaseTableTab />
  389. </TabPanel>
  390. <TabPanel value={tab} index={5}>
  391. <FinishedGoodCartonDashboardTab mode="workbench" />
  392. </TabPanel>
  393. <TabPanel value={tab} index={6}>
  394. <TruckRoutingSummaryTabWorkbench />
  395. </TabPanel>
  396. </Box>
  397. );
  398. };
  399. /** FP-MTMS Version Checklist | Functions Ref. No. 20 | v1.0.0 | 2026-07-19 */
  400. const DoWorkbenchTabs: React.FC<Props> = (props) => (
  401. <Suspense
  402. fallback={
  403. <Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
  404. <CircularProgress />
  405. </Box>
  406. }
  407. >
  408. <DoWorkbenchTabsInner {...props} />
  409. </Suspense>
  410. );
  411. export default DoWorkbenchTabs;