FPSMS-frontend
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

683 行
22 KiB

  1. "use client";
  2. import { GetPickOrderLineInfo, updateStockOutLineStatus } from "@/app/api/pickOrder/actions";
  3. import { QcItemWithChecks } from "@/app/api/qc";
  4. import { PurchaseQcResult } from "@/app/api/po/actions";
  5. import {
  6. Box,
  7. Button,
  8. Grid,
  9. Modal,
  10. ModalProps,
  11. Stack,
  12. Typography,
  13. TextField,
  14. Radio,
  15. RadioGroup,
  16. FormControlLabel,
  17. FormControl,
  18. Tab,
  19. Tabs,
  20. TabsProps,
  21. Paper,
  22. } from "@mui/material";
  23. import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
  24. import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
  25. import { useTranslation } from "react-i18next";
  26. import { dummyQCData } from "../PoDetail/dummyQcTemplate";
  27. import StyledDataGrid from "../StyledDataGrid";
  28. import { GridColDef } from "@mui/x-data-grid";
  29. import { submitDialogWithWarning } from "../Swal/CustomAlerts";
  30. import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable";
  31. import EscalationComponent from "../PoDetail/EscalationComponent";
  32. import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions";
  33. import {
  34. updateInventoryLotLineStatus
  35. } from "@/app/api/inventory/actions"; // 导入新的 API
  36. import { dayjsToDateTimeString } from "@/app/utils/formatUtil";
  37. import dayjs from "dayjs";
  38. // Define QcData interface locally
  39. interface ExtendedQcItem extends QcItemWithChecks {
  40. qcPassed?: boolean;
  41. failQty?: number;
  42. remarks?: string;
  43. order?: number; // Add order property
  44. stableId?: string; // Also add stableId for better row identification
  45. }
  46. interface Props extends CommonProps {
  47. itemDetail: GetPickOrderLineInfo & {
  48. pickOrderCode: string;
  49. qcResult?: PurchaseQcResult[]
  50. };
  51. qcItems: ExtendedQcItem[];
  52. setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>;
  53. selectedLotId?: number;
  54. onStockOutLineUpdate?: () => void;
  55. lotData: LotPickData[];
  56. // Add missing props
  57. pickQtyData?: PickQtyData;
  58. selectedRowId?: number;
  59. }
  60. const style = {
  61. position: "absolute",
  62. top: "50%",
  63. left: "50%",
  64. transform: "translate(-50%, -50%)",
  65. bgcolor: "background.paper",
  66. pt: 5,
  67. px: 5,
  68. pb: 10,
  69. display: "block",
  70. width: { xs: "80%", sm: "80%", md: "80%" },
  71. maxHeight: "90vh",
  72. overflowY: "auto",
  73. };
  74. interface PickQtyData {
  75. [lineId: number]: {
  76. [lotId: number]: number;
  77. };
  78. }
  79. interface CommonProps extends Omit<ModalProps, "children"> {
  80. itemDetail: GetPickOrderLineInfo & {
  81. pickOrderCode: string;
  82. qcResult?: PurchaseQcResult[]
  83. };
  84. setItemDetail: Dispatch<
  85. SetStateAction<
  86. | (GetPickOrderLineInfo & {
  87. pickOrderCode: string;
  88. warehouseId?: number;
  89. })
  90. | undefined
  91. >
  92. >;
  93. qc?: QcItemWithChecks[];
  94. warehouse?: any[];
  95. }
  96. interface Props extends CommonProps {
  97. itemDetail: GetPickOrderLineInfo & {
  98. pickOrderCode: string;
  99. qcResult?: PurchaseQcResult[]
  100. };
  101. qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem
  102. setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem
  103. // Add props for stock out line update
  104. selectedLotId?: number;
  105. onStockOutLineUpdate?: () => void;
  106. lotData: LotPickData[];
  107. }
  108. interface LotPickData {
  109. id: number;
  110. lotId: number;
  111. lotNo: string;
  112. expiryDate: string;
  113. location: string;
  114. stockUnit: string;
  115. availableQty: number;
  116. requiredQty: number;
  117. actualPickQty: number;
  118. lotStatus: string;
  119. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
  120. stockOutLineId?: number;
  121. stockOutLineStatus?: string;
  122. stockOutLineQty?: number;
  123. }
  124. const PickQcStockInModalVer3: React.FC<Props> = ({
  125. open,
  126. onClose,
  127. itemDetail,
  128. setItemDetail,
  129. qc,
  130. warehouse,
  131. qcItems,
  132. setQcItems,
  133. selectedLotId,
  134. onStockOutLineUpdate,
  135. lotData,
  136. pickQtyData,
  137. selectedRowId,
  138. }) => {
  139. const {
  140. t,
  141. i18n: { language },
  142. } = useTranslation("pickOrder");
  143. const [tabIndex, setTabIndex] = useState(0);
  144. //const [qcItems, setQcItems] = useState<QcData[]>(dummyQCData);
  145. const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
  146. const [isSubmitting, setIsSubmitting] = useState(false);
  147. const [feedbackMessage, setFeedbackMessage] = useState<string>("");
  148. // Add state to store submitted data
  149. const [submittedData, setSubmittedData] = useState<any[]>([]);
  150. const formProps = useForm<any>({
  151. defaultValues: {
  152. qcAccept: true,
  153. acceptQty: null,
  154. qcDecision: "1", // Default to accept
  155. ...itemDetail,
  156. },
  157. });
  158. const { control, register, formState: { errors }, watch, setValue } = formProps;
  159. const qcDecision = watch("qcDecision");
  160. const accQty = watch("acceptQty");
  161. const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
  162. (...args) => {
  163. onClose?.(...args);
  164. },
  165. [onClose],
  166. );
  167. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  168. (_e, newValue) => {
  169. setTabIndex(newValue);
  170. },
  171. [],
  172. );
  173. // Save failed QC results only
  174. const saveQcResults = async (qcData: any) => {
  175. try {
  176. const qcResults = qcData.qcItems
  177. .map((item: any) => ({
  178. qcItemId: item.id,
  179. itemId: itemDetail.itemId,
  180. stockInLineId: null,
  181. stockOutLineId: 1, // Fixed to 1 as requested
  182. failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed
  183. type: "pick_order_qc",
  184. remarks: item.remarks || "",
  185. qcPassed: item.isPassed, // This will now be included
  186. }));
  187. // Store the submitted data for debug display
  188. setSubmittedData(qcResults);
  189. console.log("Saving QC results:", qcResults);
  190. // Use the corrected API function instead of manual fetch
  191. for (const qcResult of qcResults) {
  192. const response = await savePickOrderQcResult(qcResult);
  193. console.log("QC Result save success:", response);
  194. // Check if the response indicates success
  195. if (!response.id) {
  196. throw new Error(`Failed to save QC result: ${response.message || 'Unknown error'}`);
  197. }
  198. }
  199. return true;
  200. } catch (error) {
  201. console.error("Error saving QC results:", error);
  202. return false;
  203. }
  204. };
  205. // 修改:在组件开始时自动设置失败数量
  206. useEffect(() => {
  207. if (itemDetail && qcItems.length > 0 && selectedLotId) {
  208. // 获取选中的批次数据
  209. const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId);
  210. if (selectedLot) {
  211. // 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty
  212. const updatedQcItems = qcItems.map((item, index) => ({
  213. ...item,
  214. failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty
  215. // Add stable order and ID fields
  216. order: index,
  217. stableId: `qc-${item.id}-${index}`
  218. }));
  219. setQcItems(updatedQcItems);
  220. }
  221. }
  222. }, [itemDetail, qcItems.length, selectedLotId, lotData]);
  223. // Add this helper function at the top of the component
  224. const safeClose = useCallback(() => {
  225. if (onClose) {
  226. // Create a mock event object that satisfies the Modal onClose signature
  227. const mockEvent = {
  228. target: null,
  229. currentTarget: null,
  230. type: 'close',
  231. preventDefault: () => {},
  232. stopPropagation: () => {},
  233. bubbles: false,
  234. cancelable: false,
  235. defaultPrevented: false,
  236. isTrusted: false,
  237. timeStamp: Date.now(),
  238. nativeEvent: null,
  239. isDefaultPrevented: () => false,
  240. isPropagationStopped: () => false,
  241. persist: () => {},
  242. eventPhase: 0,
  243. isPersistent: () => false
  244. } as any;
  245. // Fixed: Pass both event and reason parameters
  246. onClose(mockEvent, 'escapeKeyDown'); // 'escapeKeyDown' is a valid reason
  247. }
  248. }, [onClose]);
  249. // 修改:移除 alert 弹窗,改为控制台日志
  250. const onSubmitQc = useCallback<SubmitHandler<any>>(
  251. async (data, event) => {
  252. setIsSubmitting(true);
  253. try {
  254. const qcAccept = qcDecision === "1";
  255. const acceptQty = Number(accQty) || null;
  256. const validationErrors : string[] = [];
  257. const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId);
  258. // Add safety check for selectedLot
  259. if (!selectedLot) {
  260. console.error("Selected lot not found");
  261. return;
  262. }
  263. const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined);
  264. if (itemsWithoutResult.length > 0) {
  265. validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`);
  266. }
  267. if (validationErrors.length > 0) {
  268. console.error(`QC validation failed: ${validationErrors.join(", ")}`);
  269. return;
  270. }
  271. const qcData = {
  272. qcAccept,
  273. acceptQty,
  274. qcItems: qcItems.map(item => ({
  275. id: item.id,
  276. qcItem: item.code,
  277. qcDescription: item.description || "",
  278. isPassed: item.qcPassed,
  279. failQty: item.qcPassed ? 0 : (selectedLot.requiredQty || 0),
  280. remarks: item.remarks || "",
  281. })),
  282. };
  283. console.log("Submitting QC data:", qcData);
  284. const saveSuccess = await saveQcResults(qcData);
  285. if (!saveSuccess) {
  286. console.error("Failed to save QC results");
  287. return;
  288. }
  289. // Handle different QC decisions
  290. if (selectedLotId) {
  291. try {
  292. const allPassed = qcData.qcItems.every(item => item.isPassed);
  293. if (qcDecision === "2") {
  294. // QC Decision 2: Report and Re-pick
  295. console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable");
  296. // Inventory lot line status: unavailable
  297. if (selectedLot) {
  298. try {
  299. console.log("=== DEBUG: Updating inventory lot line status ===");
  300. console.log("Selected lot:", selectedLot);
  301. console.log("Selected lot ID:", selectedLotId);
  302. // FIX: Only send the fields that the backend expects
  303. const updateData = {
  304. inventoryLotLineId: selectedLot.lotId,
  305. status: 'unavailable'
  306. // ❌ Remove qty and operation - backend doesn't expect these
  307. };
  308. console.log("Update data:", updateData);
  309. const result = await updateInventoryLotLineStatus(updateData);
  310. console.log(" Inventory lot line status updated successfully:", result);
  311. } catch (error) {
  312. console.error("❌ Error updating inventory lot line status:", error);
  313. console.error("Error details:", {
  314. selectedLot,
  315. selectedLotId,
  316. acceptQty
  317. });
  318. // Show user-friendly error message
  319. return; // Stop execution if this fails
  320. }
  321. } else {
  322. console.error("❌ Selected lot not found for inventory update");
  323. alert("Selected lot not found. Cannot update inventory status.");
  324. return;
  325. }
  326. // Close modal and refresh data
  327. safeClose(); // Fixed: Use safe close function with both parameters
  328. if (onStockOutLineUpdate) {
  329. onStockOutLineUpdate();
  330. }
  331. } else if (qcDecision === "1") {
  332. // QC Decision 1: Accept
  333. console.log("QC Decision 1 - Accept: QC passed");
  334. // Stock out line status: checked (QC completed)
  335. await updateStockOutLineStatus({
  336. id: selectedLotId,
  337. status: 'checked',
  338. qty: acceptQty || 0
  339. });
  340. // Inventory lot line status: NO CHANGE needed
  341. // Keep the existing status from handleSubmitPickQty
  342. // Close modal and refresh data
  343. safeClose(); // Fixed: Use safe close function with both parameters
  344. if (onStockOutLineUpdate) {
  345. onStockOutLineUpdate();
  346. }
  347. }
  348. console.log("Stock out line status updated successfully after QC");
  349. // Call callback to refresh data
  350. if (onStockOutLineUpdate) {
  351. onStockOutLineUpdate();
  352. }
  353. } catch (error) {
  354. console.error("Error updating stock out line status after QC:", error);
  355. }
  356. }
  357. console.log("QC results saved successfully!");
  358. // Show warning dialog for failed QC items when accepting
  359. if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) {
  360. submitDialogWithWarning(() => {
  361. closeHandler?.({}, 'escapeKeyDown');
  362. }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""});
  363. return;
  364. }
  365. closeHandler?.({}, 'escapeKeyDown');
  366. } catch (error) {
  367. console.error("Error in QC submission:", error);
  368. if (error instanceof Error) {
  369. console.error("Error details:", error.message);
  370. console.error("Error stack:", error.stack);
  371. }
  372. } finally {
  373. setIsSubmitting(false);
  374. }
  375. },
  376. [qcItems, closeHandler, t, itemDetail, qcDecision, accQty, selectedLotId, onStockOutLineUpdate, lotData, pickQtyData, selectedRowId],
  377. );
  378. // DataGrid columns (QcComponent style)
  379. const qcColumns: GridColDef[] = useMemo(
  380. () => [
  381. {
  382. field: "code",
  383. headerName: t("qcItem"),
  384. flex: 2,
  385. renderCell: (params) => (
  386. <Box>
  387. <b>{`${params.api.getRowIndexRelativeToVisibleRows(params.id) + 1}. ${params.value}`}</b><br/>
  388. {params.row.name}<br/>
  389. </Box>
  390. ),
  391. },
  392. {
  393. field: "qcPassed",
  394. headerName: t("qcResult"),
  395. flex: 1.5,
  396. renderCell: (params) => {
  397. const current = params.row;
  398. return (
  399. <FormControl>
  400. <RadioGroup
  401. row
  402. aria-labelledby="qc-result"
  403. value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")}
  404. onChange={(e) => {
  405. const value = e.target.value === "true";
  406. // Simple state update
  407. setQcItems(prev =>
  408. prev.map(item =>
  409. item.id === params.id
  410. ? { ...item, qcPassed: value }
  411. : item
  412. )
  413. );
  414. }}
  415. name={`qcPassed-${params.id}`}
  416. >
  417. <FormControlLabel
  418. value="true"
  419. control={<Radio />}
  420. label="合格"
  421. sx={{
  422. color: current.qcPassed === true ? "green" : "inherit",
  423. "& .Mui-checked": {color: "green"}
  424. }}
  425. />
  426. <FormControlLabel
  427. value="false"
  428. control={<Radio />}
  429. label="不合格"
  430. sx={{
  431. color: current.qcPassed === false ? "red" : "inherit",
  432. "& .Mui-checked": {color: "red"}
  433. }}
  434. />
  435. </RadioGroup>
  436. </FormControl>
  437. );
  438. },
  439. },
  440. {
  441. field: "failQty",
  442. headerName: t("failedQty"),
  443. flex: 1,
  444. renderCell: (params) => (
  445. <TextField
  446. type="number"
  447. size="small"
  448. // 修改:失败项目自动显示 Lot Required Pick Qty
  449. value={!params.row.qcPassed ? (0) : 0}
  450. disabled={params.row.qcPassed}
  451. // 移除 onChange,因为数量是固定的
  452. // onChange={(e) => {
  453. // const v = e.target.value;
  454. // const next = v === "" ? undefined : Number(v);
  455. // if (Number.isNaN(next)) return;
  456. // setQcItems((prev) =>
  457. // prev.map((r) => (r.id === params.id ? { ...r, failQty: next } : r))
  458. // );
  459. // }}
  460. onClick={(e) => e.stopPropagation()}
  461. onMouseDown={(e) => e.stopPropagation()}
  462. onKeyDown={(e) => e.stopPropagation()}
  463. inputProps={{ min: 0, max: itemDetail?.requiredQty || 0 }}
  464. sx={{ width: "100%" }}
  465. />
  466. ),
  467. },
  468. {
  469. field: "remarks",
  470. headerName: t("remarks"),
  471. flex: 2,
  472. renderCell: (params) => (
  473. <TextField
  474. size="small"
  475. value={params.value ?? ""}
  476. onChange={(e) => {
  477. const remarks = e.target.value;
  478. setQcItems((prev) =>
  479. prev.map((r) => (r.id === params.id ? { ...r, remarks } : r))
  480. );
  481. }}
  482. onClick={(e) => e.stopPropagation()}
  483. onMouseDown={(e) => e.stopPropagation()}
  484. onKeyDown={(e) => e.stopPropagation()}
  485. sx={{ width: "100%" }}
  486. />
  487. ),
  488. },
  489. ],
  490. [t],
  491. );
  492. // Add stable update function
  493. const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => {
  494. setQcItems(prevItems =>
  495. prevItems.map(item =>
  496. item.id === itemId
  497. ? { ...item, qcPassed }
  498. : item
  499. )
  500. );
  501. }, []);
  502. // Remove duplicate functions
  503. const getRowId = useCallback((row: any) => {
  504. return row.id; // Just use the original ID
  505. }, []);
  506. // Remove complex sorting logic
  507. // const stableQcItems = useMemo(() => { ... }); // Remove
  508. // const sortedQcItems = useMemo(() => { ... }); // Remove
  509. // Use qcItems directly in DataGrid
  510. return (
  511. <>
  512. <FormProvider {...formProps}>
  513. <Modal open={open} onClose={closeHandler}>
  514. <Box sx={style}>
  515. <Grid container justifyContent="flex-start" alignItems="flex-start" spacing={2}>
  516. <Grid item xs={12}>
  517. <Tabs
  518. value={tabIndex}
  519. onChange={handleTabChange}
  520. variant="scrollable"
  521. >
  522. <Tab label={t("QC Info")} iconPosition="end" />
  523. <Tab label={t("Escalation History")} iconPosition="end" />
  524. </Tabs>
  525. </Grid>
  526. {tabIndex == 0 && (
  527. <>
  528. <Grid item xs={12}>
  529. <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
  530. <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}>
  531. Group A - 急凍貨類 (QCA1-MEAT01)
  532. </Typography>
  533. <Typography variant="subtitle1" sx={{ color: '#666' }}>
  534. <b>品檢類型</b>:OQC
  535. </Typography>
  536. <Typography variant="subtitle2" sx={{ color: '#666' }}>
  537. 記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全<br/>
  538. 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標
  539. </Typography>
  540. </Box>
  541. <StyledDataGrid
  542. columns={qcColumns}
  543. rows={qcItems} // Use qcItems directly
  544. autoHeight
  545. getRowId={getRowId} // Simple row ID function
  546. />
  547. </Grid>
  548. </>
  549. )}
  550. {tabIndex == 1 && (
  551. <>
  552. <Grid item xs={12}>
  553. <EscalationLogTable items={[]}/>
  554. </Grid>
  555. </>
  556. )}
  557. <Grid item xs={12}>
  558. <FormControl>
  559. <Controller
  560. name="qcDecision"
  561. control={control}
  562. defaultValue="1"
  563. render={({ field }) => (
  564. <RadioGroup
  565. row
  566. aria-labelledby="demo-radio-buttons-group-label"
  567. {...field}
  568. value={field.value}
  569. onChange={(e) => {
  570. const value = e.target.value.toString();
  571. if (value != "1" && Boolean(errors.acceptQty)) {
  572. setValue("acceptQty", itemDetail.requiredQty ?? 0);
  573. }
  574. field.onChange(value);
  575. }}
  576. >
  577. <FormControlLabel
  578. value="1"
  579. control={<Radio />}
  580. label={t("Accept Stock Out")}
  581. />
  582. {/* Combirne options 2 & 3 into one */}
  583. <FormControlLabel
  584. value="2"
  585. control={<Radio />}
  586. sx={{"& .Mui-checked": {color: "blue"}}}
  587. label={t("Report and Pick another lot")}
  588. />
  589. </RadioGroup>
  590. )}
  591. />
  592. </FormControl>
  593. </Grid>
  594. {/* Show escalation component when QC Decision = 2 (Report and Re-pick) */}
  595. <Grid item xs={12} sx={{ mt: 2 }}>
  596. <Stack direction="row" justifyContent="flex-start" gap={1}>
  597. <Button
  598. variant="contained"
  599. onClick={formProps.handleSubmit(onSubmitQc)}
  600. disabled={isSubmitting}
  601. sx={{ whiteSpace: 'nowrap' }}
  602. >
  603. {isSubmitting ? "Submitting..." : "Submit QC"}
  604. </Button>
  605. <Button
  606. variant="outlined"
  607. onClick={() => {
  608. closeHandler?.({}, 'escapeKeyDown');
  609. }}
  610. >
  611. Cancel
  612. </Button>
  613. </Stack>
  614. </Grid>
  615. </Grid>
  616. </Box>
  617. </Modal>
  618. </FormProvider>
  619. </>
  620. );
  621. };
  622. export default PickQcStockInModalVer3;