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.
 
 

685 lines
22 KiB

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