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

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