FPSMS-frontend
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 

421 rader
16 KiB

  1. "use client";
  2. import React, { useCallback, useEffect, useState } from "react";
  3. import {
  4. Box,
  5. Button,
  6. Card,
  7. Stack,
  8. Typography,
  9. Chip,
  10. CircularProgress,
  11. Grid,
  12. } from "@mui/material";
  13. import ArrowBackIcon from "@mui/icons-material/ArrowBack";
  14. import { useTranslation } from "react-i18next";
  15. import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actions";
  16. import JobPickExecution from "./newJobPickExecution";
  17. import SearchBox, { Criterion } from "../SearchBox";
  18. import dayjs from "dayjs";
  19. import type { PrinterCombo } from "@/app/api/settings/printer";
  20. interface Props {
  21. /** Reserved for tabs parity with Jodetail; not used in workbench list yet. */
  22. onSwitchToRecordTab?: () => void;
  23. printerCombo: PrinterCombo[];
  24. }
  25. const chipSx = {
  26. flexShrink: 0,
  27. height: 28,
  28. borderRadius: "14px",
  29. "& .MuiChip-label": {
  30. typography: "body2",
  31. px: 1.25,
  32. lineHeight: 1.2,
  33. },
  34. } as const;
  35. /** Jo workbench: same list + detail flow as Jodetail `JoPickOrderList`, detail uses `JoWorkbench/newJobPickExecution`. */
  36. const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => {
  37. const { t } = useTranslation(["common", "jo"]);
  38. const today = dayjs().format("YYYY-MM-DD");
  39. const [loading, setLoading] = useState(false);
  40. const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]);
  41. const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined);
  42. const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined);
  43. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({ planStart: today });
  44. type FloorFilter = "ALL" | "2F" | "3F" | "4F" | "NO_LOT";
  45. const searchCriteria: Criterion<any>[] = [
  46. { label: t("Job Order Code"), paramName: "jobOrderCode", type: "text" },
  47. { label: t("Pick Order"), paramName: "pickOrderCode", type: "text" },
  48. { label: t("Item Name"), paramName: "itemName", type: "text" },
  49. {
  50. label: t("Job Order Type"),
  51. paramName: "bomDescription",
  52. type: "select-labelled",
  53. options: [
  54. { label: t("FG"), value: "FG" },
  55. { label: t("WIP"), value: "WIP" },
  56. ],
  57. },
  58. { label: t("Plan Start"), paramName: "planStart", type: "date" },
  59. {
  60. label: t("BOM Type"),
  61. paramName: "bomType",
  62. type: "select-labelled",
  63. options: [
  64. { label: t("Drink"), value: "drink" },
  65. { label: t("Powder Mixture"), value: "Powder_Mixture" },
  66. { label: t("Other"), value: "other" },
  67. ],
  68. },
  69. {
  70. label: t("Floor"),
  71. paramName: "floor",
  72. type: "select-labelled",
  73. options: [
  74. { label: "2F", value: "2F" },
  75. { label: "3F", value: "3F" },
  76. { label: "4F", value: "4F" },
  77. { label: t("No Lot"), value: "NO_LOT" },
  78. ],
  79. },
  80. ];
  81. const selectedFloor: FloorFilter = (() => {
  82. const floor = String(searchQuery.floor || "ALL");
  83. if (floor === "2F" || floor === "3F" || floor === "4F" || floor === "NO_LOT") {
  84. return floor;
  85. }
  86. return "ALL";
  87. })();
  88. const fetchPickOrders = useCallback(async (query?: Record<string, any>) => {
  89. setLoading(true);
  90. try {
  91. const currentQuery = query ?? { planStart: today };
  92. const bomTypeValue = String(currentQuery.bomType || "All");
  93. const floorValue = String(currentQuery.floor || "ALL");
  94. const isAllOption = (value: string) => {
  95. const normalized = value.trim().toLowerCase();
  96. return normalized === "" || normalized === "all";
  97. };
  98. const typeParam = isAllOption(bomTypeValue) ? undefined : bomTypeValue;
  99. const floorParam = isAllOption(floorValue) ? undefined : floorValue;
  100. const jobOrderCode = String(currentQuery.jobOrderCode || "").trim();
  101. const pickOrderCode = String(currentQuery.pickOrderCode || "").trim();
  102. const itemName = String(currentQuery.itemName || "").trim();
  103. const bomDescription = String(currentQuery.bomDescription || "").trim();
  104. const planStart = String(currentQuery.planStart || "").trim();
  105. const data = await fetchAllJoPickOrders(typeParam, floorParam, {
  106. jobOrderCode: jobOrderCode || undefined,
  107. pickOrderCode: pickOrderCode || undefined,
  108. itemName: itemName || undefined,
  109. bomDescription: isAllOption(bomDescription) ? undefined : bomDescription,
  110. planStart: planStart || undefined,
  111. });
  112. setPickOrders(Array.isArray(data) ? data : []);
  113. } catch (e) {
  114. console.error(e);
  115. setPickOrders([]);
  116. } finally {
  117. setLoading(false);
  118. }
  119. }, [today]);
  120. const handleSearch = useCallback((query: Record<string, any>) => {
  121. const nextQuery = { ...query };
  122. setSearchQuery(nextQuery);
  123. void fetchPickOrders(nextQuery);
  124. }, [fetchPickOrders]);
  125. const handleSearchReset = useCallback(() => {
  126. const resetQuery = {};
  127. setSearchQuery(resetQuery);
  128. void fetchPickOrders(resetQuery);
  129. }, [fetchPickOrders]);
  130. useEffect(() => {
  131. void fetchPickOrders({ planStart: today });
  132. }, [fetchPickOrders, today]);
  133. const handleBackToList = useCallback(() => {
  134. setSelectedPickOrderId(undefined);
  135. setSelectedJobOrderId(undefined);
  136. void fetchPickOrders(searchQuery);
  137. }, [fetchPickOrders, searchQuery]);
  138. if (selectedPickOrderId !== undefined) {
  139. return (
  140. <Box>
  141. <Box sx={{ mb: 2 }}>
  142. <Button
  143. variant="outlined"
  144. onClick={handleBackToList}
  145. startIcon={<ArrowBackIcon />}
  146. >
  147. {t("Back to List")}
  148. </Button>
  149. </Box>
  150. <JobPickExecution
  151. filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }}
  152. onBackToList={handleBackToList}
  153. printerCombo={printerCombo}
  154. />
  155. </Box>
  156. );
  157. }
  158. return (
  159. <Box>
  160. <Box sx={{ mb: 2 }}>
  161. <SearchBox
  162. criteria={searchCriteria}
  163. onSearch={handleSearch}
  164. onReset={handleSearchReset}
  165. />
  166. </Box>
  167. {loading ? (
  168. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  169. <CircularProgress />
  170. </Box>
  171. ) : (
  172. <Box>
  173. <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
  174. {t("Total pick orders")}: {pickOrders.length}
  175. </Typography>
  176. <Grid container spacing={2} alignItems="stretch">
  177. {pickOrders.map((pickOrder) => {
  178. const status = String(pickOrder.jobOrderStatus || "");
  179. const statusLower = status.toLowerCase();
  180. const statusColor =
  181. statusLower === "completed"
  182. ? "success"
  183. : statusLower === "pending" || statusLower === "processing"
  184. ? "primary"
  185. : "default";
  186. const finishedCount = pickOrder.finishedPickOLineCount ?? 0;
  187. const bomDescription = pickOrder.bomDescription
  188. ? String(pickOrder.bomDescription).trim()
  189. : "";
  190. const bomType = pickOrder.bomType
  191. ? String(pickOrder.bomType).trim()
  192. : "";
  193. return (
  194. <Grid
  195. key={pickOrder.id}
  196. item
  197. xs={12}
  198. sm={6}
  199. md={4}
  200. sx={{ display: "flex" }}
  201. >
  202. <Card
  203. sx={{
  204. width: "100%",
  205. height: "100%",
  206. display: "flex",
  207. flexDirection: "column",
  208. border: "1px solid",
  209. borderColor: "divider",
  210. borderRadius: 2,
  211. boxShadow: "none",
  212. }}
  213. >
  214. <Box
  215. sx={{
  216. p: 2,
  217. flexGrow: 1,
  218. display: "flex",
  219. flexDirection: "column",
  220. minHeight: 0,
  221. }}
  222. >
  223. <Typography
  224. variant="body1"
  225. color="text.primary"
  226. fontWeight={600}
  227. title={
  228. [pickOrder.itemCode, pickOrder.itemName]
  229. .filter(Boolean)
  230. .join(" ") || undefined
  231. }
  232. sx={{
  233. display: "-webkit-box",
  234. WebkitLineClamp: 2,
  235. WebkitBoxOrient: "vertical",
  236. overflow: "hidden",
  237. lineHeight: 1.35,
  238. }}
  239. >
  240. {[pickOrder.itemCode, pickOrder.itemName]
  241. .filter(Boolean)
  242. .join(" ") || "-"}
  243. </Typography>
  244. <Stack
  245. direction="row"
  246. alignItems="center"
  247. spacing={0.75}
  248. flexWrap="nowrap"
  249. useFlexGap
  250. sx={{
  251. mt: 0.75,
  252. minWidth: 0,
  253. overflowX: "auto",
  254. scrollbarWidth: "none",
  255. "&::-webkit-scrollbar": { display: "none" },
  256. }}
  257. >
  258. {bomDescription ? (
  259. <Chip
  260. size="small"
  261. label={t(bomDescription)}
  262. variant="outlined"
  263. sx={chipSx}
  264. />
  265. ) : null}
  266. {bomType ? (
  267. <Chip
  268. size="small"
  269. label={t(bomType)}
  270. color="info"
  271. variant="outlined"
  272. sx={chipSx}
  273. />
  274. ) : null}
  275. <Chip
  276. size="small"
  277. label={t(status)}
  278. color={statusColor as any}
  279. sx={chipSx}
  280. />
  281. </Stack>
  282. <Stack
  283. spacing={0.35}
  284. sx={{ mt: 0.75, color: "text.secondary" }}
  285. >
  286. <Typography variant="body2">
  287. {t("Pick Order")}: {pickOrder.pickOrderCode || "-"}
  288. </Typography>
  289. <Typography variant="body2">
  290. {t("Required Qty")}: {pickOrder.reqQty} ({pickOrder.uomName})
  291. </Typography>
  292. <Box>
  293. {selectedFloor === "ALL" ? (
  294. <>
  295. {pickOrder.floorPickCounts?.map(
  296. ({ floor, finishedCount, totalCount }) => (
  297. <Typography
  298. key={floor}
  299. variant="body2"
  300. color="text.secondary"
  301. component="span"
  302. sx={{ mr: 1 }}
  303. >
  304. {floor}: {finishedCount}/{totalCount}
  305. </Typography>
  306. ),
  307. )}
  308. {!!pickOrder.noLotPickCount && (
  309. <Typography
  310. key="NO_LOT"
  311. variant="body2"
  312. color="text.secondary"
  313. component="span"
  314. sx={{ mr: 1 }}
  315. >
  316. {t("No Lot")}:{" "}
  317. {pickOrder.noLotPickCount.finishedCount}/
  318. {pickOrder.noLotPickCount.totalCount}
  319. </Typography>
  320. )}
  321. </>
  322. ) : selectedFloor === "NO_LOT" ? (
  323. !!pickOrder.noLotPickCount && (
  324. <Typography
  325. key="NO_LOT"
  326. variant="body2"
  327. color="text.secondary"
  328. component="span"
  329. sx={{ mr: 1 }}
  330. >
  331. {t("No Lot")}:{" "}
  332. {pickOrder.noLotPickCount.finishedCount}/
  333. {pickOrder.noLotPickCount.totalCount}
  334. </Typography>
  335. )
  336. ) : (
  337. pickOrder.floorPickCounts
  338. ?.filter((c) => c.floor === selectedFloor)
  339. .map(({ floor, finishedCount, totalCount }) => (
  340. <Typography
  341. key={floor}
  342. variant="body2"
  343. color="text.secondary"
  344. component="span"
  345. sx={{ mr: 1 }}
  346. >
  347. {floor}: {finishedCount}/{totalCount}
  348. </Typography>
  349. ))
  350. )}
  351. </Box>
  352. {typeof pickOrder.suggestedFailCount === "number" &&
  353. pickOrder.suggestedFailCount > 0 && (
  354. <Typography variant="body2" color="error">
  355. {t("Suggested Fail")}: {pickOrder.suggestedFailCount}
  356. </Typography>
  357. )}
  358. {statusLower !== "pending" && finishedCount > 0 && (
  359. <Typography variant="body2" fontWeight={600} color="text.primary">
  360. {t("Finished lines")}: {finishedCount}
  361. </Typography>
  362. )}
  363. </Stack>
  364. <Stack sx={{ mt: "auto", pt: 1.5 }}>
  365. <Button
  366. variant="contained"
  367. size="small"
  368. sx={{ alignSelf: "flex-start" }}
  369. onClick={() => {
  370. setSelectedPickOrderId(pickOrder.pickOrderId ?? undefined);
  371. setSelectedJobOrderId(pickOrder.jobOrderId ?? undefined);
  372. }}
  373. >
  374. {t("View Details")}
  375. </Button>
  376. </Stack>
  377. <Typography
  378. variant="caption"
  379. color="text.secondary"
  380. sx={{ mt: 1.25 }}
  381. >
  382. {pickOrder.jobOrderCode || "-"}
  383. {" · "}
  384. {t("Lot No")}: {pickOrder.lotNo || "-"}
  385. </Typography>
  386. </Box>
  387. </Card>
  388. </Grid>
  389. );
  390. })}
  391. </Grid>
  392. </Box>
  393. )}
  394. </Box>
  395. );
  396. };
  397. export default JoPickOrderList;