FPSMS-frontend
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 

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