|
- "use client";
-
- import { GetPickOrderLineInfo, updateStockOutLineStatus } from "@/app/api/pickOrder/actions";
- import { QcItemWithChecks } from "@/app/api/qc";
- import { PurchaseQcResult } from "@/app/api/po/actions";
- import {
- Box,
- Button,
- Grid,
- Modal,
- ModalProps,
- Stack,
- Typography,
- TextField,
- Radio,
- RadioGroup,
- FormControlLabel,
- FormControl,
- Tab,
- Tabs,
- TabsProps,
- Paper,
- } from "@mui/material";
- import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
- import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
- import { useTranslation } from "react-i18next";
- import { dummyQCData } from "../PoDetail/dummyQcTemplate";
- import StyledDataGrid from "../StyledDataGrid";
- import { GridColDef } from "@mui/x-data-grid";
- import { submitDialogWithWarning } from "../Swal/CustomAlerts";
- import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable";
- import EscalationComponent from "../PoDetail/EscalationComponent";
- import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions";
- import {
- updateInventoryLotLineStatus
- } from "@/app/api/inventory/actions"; // 导入新的 API
- import { dayjsToDateTimeString } from "@/app/utils/formatUtil";
- import dayjs from "dayjs";
-
- // Define QcData interface locally
- interface ExtendedQcItem extends QcItemWithChecks {
- qcPassed?: boolean;
- failQty?: number;
- remarks?: string;
- order?: number; // Add order property
- stableId?: string; // Also add stableId for better row identification
- }
- interface Props extends CommonProps {
- itemDetail: GetPickOrderLineInfo & {
- pickOrderCode: string;
- qcResult?: PurchaseQcResult[]
- };
- qcItems: ExtendedQcItem[];
- setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>;
- selectedLotId?: number;
- onStockOutLineUpdate?: () => void;
- lotData: LotPickData[];
- // Add missing props
- pickQtyData?: PickQtyData;
- selectedRowId?: number;
- }
-
- const style = {
- position: "absolute",
- top: "50%",
- left: "50%",
- transform: "translate(-50%, -50%)",
- bgcolor: "background.paper",
- pt: 5,
- px: 5,
- pb: 10,
- display: "block",
- width: { xs: "80%", sm: "80%", md: "80%" },
- maxHeight: "90vh",
- overflowY: "auto",
- };
- interface PickQtyData {
- [lineId: number]: {
- [lotId: number]: number;
- };
- }
- interface CommonProps extends Omit<ModalProps, "children"> {
- itemDetail: GetPickOrderLineInfo & {
- pickOrderCode: string;
- qcResult?: PurchaseQcResult[]
- };
- setItemDetail: Dispatch<
- SetStateAction<
- | (GetPickOrderLineInfo & {
- pickOrderCode: string;
- warehouseId?: number;
- })
- | undefined
- >
- >;
- qc?: QcItemWithChecks[];
- warehouse?: any[];
- }
-
- interface Props extends CommonProps {
- itemDetail: GetPickOrderLineInfo & {
- pickOrderCode: string;
- qcResult?: PurchaseQcResult[]
- };
- qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem
- setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem
- // Add props for stock out line update
- selectedLotId?: number;
- onStockOutLineUpdate?: () => void;
- lotData: LotPickData[];
- }
- interface LotPickData {
- id: number;
- lotId: number;
- lotNo: string;
- expiryDate: string;
- location: string;
- stockUnit: string;
- availableQty: number;
- requiredQty: number;
- actualPickQty: number;
- lotStatus: string;
- lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
- stockOutLineId?: number;
- stockOutLineStatus?: string;
- stockOutLineQty?: number;
- }
- const PickQcStockInModalVer3: React.FC<Props> = ({
- open,
- onClose,
- itemDetail,
- setItemDetail,
- qc,
- warehouse,
- qcItems,
- setQcItems,
- selectedLotId,
- onStockOutLineUpdate,
- lotData,
- pickQtyData,
- selectedRowId,
- }) => {
- const {
- t,
- i18n: { language },
- } = useTranslation("pickOrder");
-
- const [tabIndex, setTabIndex] = useState(0);
- //const [qcItems, setQcItems] = useState<QcData[]>(dummyQCData);
- const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [feedbackMessage, setFeedbackMessage] = useState<string>("");
-
- // Add state to store submitted data
- const [submittedData, setSubmittedData] = useState<any[]>([]);
-
- const formProps = useForm<any>({
- defaultValues: {
- qcAccept: true,
- acceptQty: null,
- qcDecision: "1", // Default to accept
- ...itemDetail,
- },
- });
- const { control, register, formState: { errors }, watch, setValue } = formProps;
-
- const qcDecision = watch("qcDecision");
- const accQty = watch("acceptQty");
-
- const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
- (...args) => {
- onClose?.(...args);
- },
- [onClose],
- );
-
- const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
- (_e, newValue) => {
- setTabIndex(newValue);
- },
- [],
- );
-
- // Save failed QC results only
- const saveQcResults = async (qcData: any) => {
- try {
- const qcResults = qcData.qcItems
- .map((item: any) => ({
- qcItemId: item.id,
- itemId: itemDetail.itemId,
- stockInLineId: null,
- stockOutLineId: 1, // Fixed to 1 as requested
- failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed
- type: "pick_order_qc",
- remarks: item.remarks || "",
- qcPassed: item.isPassed, // This will now be included
- }));
-
- // Store the submitted data for debug display
- setSubmittedData(qcResults);
- console.log("Saving QC results:", qcResults);
-
- // Use the corrected API function instead of manual fetch
- for (const qcResult of qcResults) {
- const response = await savePickOrderQcResult(qcResult);
- console.log("QC Result save success:", response);
-
- // Check if the response indicates success
- if (!response.id) {
- throw new Error(`Failed to save QC result: ${response.message || 'Unknown error'}`);
- }
- }
- return true;
- } catch (error) {
- console.error("Error saving QC results:", error);
- return false;
- }
- };
-
- // 修改:在组件开始时自动设置失败数量
- useEffect(() => {
- if (itemDetail && qcItems.length > 0 && selectedLotId) {
- // 获取选中的批次数据
- const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId);
- if (selectedLot) {
- // 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty
- const updatedQcItems = qcItems.map((item, index) => ({
- ...item,
- failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty
- // Add stable order and ID fields
- order: index,
- stableId: `qc-${item.id}-${index}`
- }));
- setQcItems(updatedQcItems);
- }
- }
- }, [itemDetail, qcItems.length, selectedLotId, lotData]);
-
- // Add this helper function at the top of the component
- const safeClose = useCallback(() => {
- if (onClose) {
- // Create a mock event object that satisfies the Modal onClose signature
- const mockEvent = {
- target: null,
- currentTarget: null,
- type: 'close',
- preventDefault: () => {},
- stopPropagation: () => {},
- bubbles: false,
- cancelable: false,
- defaultPrevented: false,
- isTrusted: false,
- timeStamp: Date.now(),
- nativeEvent: null,
- isDefaultPrevented: () => false,
- isPropagationStopped: () => false,
- persist: () => {},
- eventPhase: 0,
- isPersistent: () => false
- } as any;
-
- // Fixed: Pass both event and reason parameters
- onClose(mockEvent, 'escapeKeyDown'); // 'escapeKeyDown' is a valid reason
- }
- }, [onClose]);
-
- // 修改:移除 alert 弹窗,改为控制台日志
- const onSubmitQc = useCallback<SubmitHandler<any>>(
- async (data, event) => {
- setIsSubmitting(true);
-
- try {
- const qcAccept = qcDecision === "1";
- const acceptQty = Number(accQty) || null;
-
- const validationErrors : string[] = [];
- const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId);
-
- // Add safety check for selectedLot
- if (!selectedLot) {
- console.error("Selected lot not found");
- return;
- }
-
- const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined);
- if (itemsWithoutResult.length > 0) {
- validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`);
- }
-
- if (validationErrors.length > 0) {
- console.error(`QC validation failed: ${validationErrors.join(", ")}`);
- return;
- }
-
- const qcData = {
- qcAccept,
- acceptQty,
- qcItems: qcItems.map(item => ({
- id: item.id,
- qcItem: item.code,
- qcDescription: item.description || "",
- isPassed: item.qcPassed,
- failQty: item.qcPassed ? 0 : (selectedLot.requiredQty || 0),
- remarks: item.remarks || "",
- })),
- };
-
- console.log("Submitting QC data:", qcData);
-
- const saveSuccess = await saveQcResults(qcData);
- if (!saveSuccess) {
- console.error("Failed to save QC results");
- return;
- }
-
- // Handle different QC decisions
- if (selectedLotId) {
- try {
- const allPassed = qcData.qcItems.every(item => item.isPassed);
-
- if (qcDecision === "2") {
- // QC Decision 2: Report and Re-pick
- console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable");
-
- // Inventory lot line status: unavailable
- if (selectedLot) {
- try {
- console.log("=== DEBUG: Updating inventory lot line status ===");
- console.log("Selected lot:", selectedLot);
- console.log("Selected lot ID:", selectedLotId);
-
- // FIX: Only send the fields that the backend expects
- const updateData = {
- inventoryLotLineId: selectedLot.lotId,
- status: 'unavailable'
- // ❌ Remove qty and operation - backend doesn't expect these
- };
-
- console.log("Update data:", updateData);
-
- const result = await updateInventoryLotLineStatus(updateData);
- console.log(" Inventory lot line status updated successfully:", result);
-
- } catch (error) {
- console.error("❌ Error updating inventory lot line status:", error);
- console.error("Error details:", {
- selectedLot,
- selectedLotId,
- acceptQty
- });
-
- // Show user-friendly error message
-
- return; // Stop execution if this fails
- }
- } else {
- console.error("❌ Selected lot not found for inventory update");
- alert("Selected lot not found. Cannot update inventory status.");
- return;
- }
-
- // Close modal and refresh data
- safeClose(); // Fixed: Use safe close function with both parameters
- if (onStockOutLineUpdate) {
- onStockOutLineUpdate();
- }
-
- } else if (qcDecision === "1") {
- // QC Decision 1: Accept
- console.log("QC Decision 1 - Accept: QC passed");
-
- // Stock out line status: checked (QC completed)
- await updateStockOutLineStatus({
- id: selectedLotId,
- status: 'checked',
- qty: acceptQty || 0
- });
-
- // Inventory lot line status: NO CHANGE needed
- // Keep the existing status from handleSubmitPickQty
-
- // Close modal and refresh data
- safeClose(); // Fixed: Use safe close function with both parameters
- if (onStockOutLineUpdate) {
- onStockOutLineUpdate();
- }
- }
-
- console.log("Stock out line status updated successfully after QC");
-
- // Call callback to refresh data
- if (onStockOutLineUpdate) {
- onStockOutLineUpdate();
- }
- } catch (error) {
- console.error("Error updating stock out line status after QC:", error);
- }
- }
-
- console.log("QC results saved successfully!");
-
- // Show warning dialog for failed QC items when accepting
- if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) {
- submitDialogWithWarning(() => {
- closeHandler?.({}, 'escapeKeyDown');
- }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""});
- return;
- }
-
- closeHandler?.({}, 'escapeKeyDown');
- } catch (error) {
- console.error("Error in QC submission:", error);
- if (error instanceof Error) {
- console.error("Error details:", error.message);
- console.error("Error stack:", error.stack);
- }
- } finally {
- setIsSubmitting(false);
- }
- },
- [qcItems, closeHandler, t, itemDetail, qcDecision, accQty, selectedLotId, onStockOutLineUpdate, lotData, pickQtyData, selectedRowId],
- );
- // DataGrid columns (QcComponent style)
- const qcColumns: GridColDef[] = useMemo(
- () => [
- {
- field: "code",
- headerName: t("qcItem"),
- flex: 2,
- renderCell: (params) => (
- <Box>
- <b>{`${params.api.getRowIndexRelativeToVisibleRows(params.id) + 1}. ${params.value}`}</b><br/>
- {params.row.name}<br/>
- </Box>
- ),
- },
- {
- field: "qcPassed",
- headerName: t("qcResult"),
- flex: 1.5,
- renderCell: (params) => {
- const current = params.row;
- return (
- <FormControl>
- <RadioGroup
- row
- aria-labelledby="qc-result"
- value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")}
- onChange={(e) => {
- const value = e.target.value === "true";
- // Simple state update
- setQcItems(prev =>
- prev.map(item =>
- item.id === params.id
- ? { ...item, qcPassed: value }
- : item
- )
- );
- }}
- name={`qcPassed-${params.id}`}
- >
- <FormControlLabel
- value="true"
- control={<Radio />}
- label="合格"
- sx={{
- color: current.qcPassed === true ? "green" : "inherit",
- "& .Mui-checked": {color: "green"}
- }}
- />
- <FormControlLabel
- value="false"
- control={<Radio />}
- label="不合格"
- sx={{
- color: current.qcPassed === false ? "red" : "inherit",
- "& .Mui-checked": {color: "red"}
- }}
- />
- </RadioGroup>
- </FormControl>
- );
- },
- },
- {
- field: "failQty",
- headerName: t("failedQty"),
- flex: 1,
- renderCell: (params) => (
- <TextField
- type="number"
- size="small"
- // 修改:失败项目自动显示 Lot Required Pick Qty
- value={!params.row.qcPassed ? (0) : 0}
- disabled={params.row.qcPassed}
- // 移除 onChange,因为数量是固定的
- // onChange={(e) => {
- // const v = e.target.value;
- // const next = v === "" ? undefined : Number(v);
- // if (Number.isNaN(next)) return;
- // setQcItems((prev) =>
- // prev.map((r) => (r.id === params.id ? { ...r, failQty: next } : r))
- // );
- // }}
- onClick={(e) => e.stopPropagation()}
- onMouseDown={(e) => e.stopPropagation()}
- onKeyDown={(e) => e.stopPropagation()}
- inputProps={{ min: 0, max: itemDetail?.requiredQty || 0 }}
- sx={{ width: "100%" }}
- />
- ),
- },
- {
- field: "remarks",
- headerName: t("remarks"),
- flex: 2,
- renderCell: (params) => (
- <TextField
- size="small"
- value={params.value ?? ""}
- onChange={(e) => {
- const remarks = e.target.value;
- setQcItems((prev) =>
- prev.map((r) => (r.id === params.id ? { ...r, remarks } : r))
- );
- }}
- onClick={(e) => e.stopPropagation()}
- onMouseDown={(e) => e.stopPropagation()}
- onKeyDown={(e) => e.stopPropagation()}
- sx={{ width: "100%" }}
- />
- ),
- },
- ],
- [t],
- );
-
- // Add stable update function
- const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => {
- setQcItems(prevItems =>
- prevItems.map(item =>
- item.id === itemId
- ? { ...item, qcPassed }
- : item
- )
- );
- }, []);
-
- // Remove duplicate functions
- const getRowId = useCallback((row: any) => {
- return row.id; // Just use the original ID
- }, []);
-
- // Remove complex sorting logic
- // const stableQcItems = useMemo(() => { ... }); // Remove
- // const sortedQcItems = useMemo(() => { ... }); // Remove
-
- // Use qcItems directly in DataGrid
- return (
- <>
- <FormProvider {...formProps}>
- <Modal open={open} onClose={closeHandler}>
- <Box sx={style}>
- <Grid container justifyContent="flex-start" alignItems="flex-start" spacing={2}>
- <Grid item xs={12}>
- <Tabs
- value={tabIndex}
- onChange={handleTabChange}
- variant="scrollable"
- >
- <Tab label={t("QC Info")} iconPosition="end" />
- <Tab label={t("Escalation History")} iconPosition="end" />
- </Tabs>
- </Grid>
-
- {tabIndex == 0 && (
- <>
- <Grid item xs={12}>
- <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
- <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}>
- Group A - 急凍貨類 (QCA1-MEAT01)
- </Typography>
-
- <Typography variant="subtitle1" sx={{ color: '#666' }}>
- <b>品檢類型</b>:OQC
- </Typography>
- <Typography variant="subtitle2" sx={{ color: '#666' }}>
- 記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全<br/>
- 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標
- </Typography>
-
- </Box>
-
- <StyledDataGrid
- columns={qcColumns}
- rows={qcItems} // Use qcItems directly
- autoHeight
- getRowId={getRowId} // Simple row ID function
- />
- </Grid>
- </>
- )}
-
- {tabIndex == 1 && (
- <>
- <Grid item xs={12}>
- <EscalationLogTable items={[]}/>
- </Grid>
- </>
- )}
-
- <Grid item xs={12}>
- <FormControl>
- <Controller
- name="qcDecision"
- control={control}
- defaultValue="1"
- render={({ field }) => (
- <RadioGroup
- row
- aria-labelledby="demo-radio-buttons-group-label"
- {...field}
- value={field.value}
- onChange={(e) => {
- const value = e.target.value.toString();
- if (value != "1" && Boolean(errors.acceptQty)) {
- setValue("acceptQty", itemDetail.requiredQty ?? 0);
- }
- field.onChange(value);
- }}
- >
- <FormControlLabel
- value="1"
- control={<Radio />}
- label={t("Accept Stock Out")}
- />
-
-
- {/* Combirne options 2 & 3 into one */}
- <FormControlLabel
- value="2"
- control={<Radio />}
- sx={{"& .Mui-checked": {color: "blue"}}}
- label={t("Report and Pick another lot")}
- />
- </RadioGroup>
- )}
- />
- </FormControl>
- </Grid>
-
- {/* Show escalation component when QC Decision = 2 (Report and Re-pick) */}
-
-
- <Grid item xs={12} sx={{ mt: 2 }}>
- <Stack direction="row" justifyContent="flex-start" gap={1}>
- <Button
- variant="contained"
- onClick={formProps.handleSubmit(onSubmitQc)}
- disabled={isSubmitting}
- sx={{ whiteSpace: 'nowrap' }}
- >
- {isSubmitting ? "Submitting..." : "Submit QC"}
- </Button>
- <Button
- variant="outlined"
- onClick={() => {
- closeHandler?.({}, 'escapeKeyDown');
- }}
- >
- Cancel
- </Button>
- </Stack>
- </Grid>
- </Grid>
- </Box>
- </Modal>
- </FormProvider>
- </>
- );
- };
-
- export default PickQcStockInModalVer3;
|