| @@ -34,6 +34,23 @@ import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/acti | |||||
| import { | import { | ||||
| updateInventoryLotLineStatus | updateInventoryLotLineStatus | ||||
| } from "@/app/api/inventory/actions"; // ✅ 导入新的 API | } from "@/app/api/inventory/actions"; // ✅ 导入新的 API | ||||
| import { dayjsToInputDateString } from "@/app/utils/formatUtil"; | |||||
| import dayjs from "dayjs"; | |||||
| interface Props extends CommonProps { | |||||
| itemDetail: GetPickOrderLineInfo & { | |||||
| pickOrderCode: string; | |||||
| qcResult?: PurchaseQcResult[] | |||||
| }; | |||||
| qcItems: ExtendedQcItem[]; | |||||
| setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; | |||||
| selectedLotId?: number; | |||||
| onStockOutLineUpdate?: () => void; | |||||
| lotData: LotPickData[]; | |||||
| pickQtyData?: PickQtyData; | |||||
| selectedRowId?: number; | |||||
| // ✅ Add callback to update pickQtyData in parent | |||||
| onPickQtyUpdate?: (updatedPickQtyData: PickQtyData) => void; | |||||
| } | |||||
| // Define QcData interface locally | // Define QcData interface locally | ||||
| interface ExtendedQcItem extends QcItemWithChecks { | interface ExtendedQcItem extends QcItemWithChecks { | ||||
| @@ -44,6 +61,7 @@ interface ExtendedQcItem extends QcItemWithChecks { | |||||
| stableId?: string; // ✅ Also add stableId for better row identification | stableId?: string; // ✅ Also add stableId for better row identification | ||||
| } | } | ||||
| const style = { | const style = { | ||||
| position: "absolute", | position: "absolute", | ||||
| top: "50%", | top: "50%", | ||||
| @@ -58,7 +76,11 @@ const style = { | |||||
| maxHeight: "90vh", | maxHeight: "90vh", | ||||
| overflowY: "auto", | overflowY: "auto", | ||||
| }; | }; | ||||
| interface PickQtyData { | |||||
| [lineId: number]: { | |||||
| [lotId: number]: number; | |||||
| }; | |||||
| } | |||||
| interface CommonProps extends Omit<ModalProps, "children"> { | interface CommonProps extends Omit<ModalProps, "children"> { | ||||
| itemDetail: GetPickOrderLineInfo & { | itemDetail: GetPickOrderLineInfo & { | ||||
| pickOrderCode: string; | pickOrderCode: string; | ||||
| @@ -117,6 +139,8 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| selectedLotId, | selectedLotId, | ||||
| onStockOutLineUpdate, | onStockOutLineUpdate, | ||||
| lotData, | lotData, | ||||
| pickQtyData, | |||||
| selectedRowId, | |||||
| }) => { | }) => { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -222,10 +246,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| try { | try { | ||||
| const qcAccept = qcDecision === "1"; | const qcAccept = qcDecision === "1"; | ||||
| const acceptQty = Number(accQty) || null; | const acceptQty = Number(accQty) || null; | ||||
| const validationErrors : string[] = []; | const validationErrors : string[] = []; | ||||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | 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); | const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); | ||||
| if (itemsWithoutResult.length > 0) { | if (itemsWithoutResult.length > 0) { | ||||
| validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | ||||
| @@ -244,7 +274,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| qcItem: item.code, | qcItem: item.code, | ||||
| qcDescription: item.description || "", | qcDescription: item.description || "", | ||||
| isPassed: item.qcPassed, | isPassed: item.qcPassed, | ||||
| failQty: item.qcPassed ? 0 : (selectedLot?.requiredQty || 0), | |||||
| failQty: item.qcPassed ? 0 : (selectedLot.requiredQty || 0), | |||||
| remarks: item.remarks || "", | remarks: item.remarks || "", | ||||
| })), | })), | ||||
| }; | }; | ||||
| @@ -257,35 +287,64 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| return; | return; | ||||
| } | } | ||||
| // ✅ Fix: Update stock out line status based on QC decision | |||||
| if (selectedLotId) { // ✅ Remove qcData.qcAccept condition | |||||
| // ✅ Handle different QC decisions | |||||
| if (selectedLotId) { | |||||
| try { | try { | ||||
| const allPassed = qcData.qcItems.every(item => item.isPassed); | const allPassed = qcData.qcItems.every(item => item.isPassed); | ||||
| // ✅ Fix: Use correct backend enum values | |||||
| const newStockOutLineStatus = allPassed ? 'completed' : 'rejected'; | |||||
| console.log("Updating stock out line status after QC:", { | |||||
| stockOutLineId: selectedLotId, | |||||
| newStatus: newStockOutLineStatus | |||||
| }); | |||||
| // ✅ Fix: 1. Update stock out line status with required qty field | |||||
| if (selectedLot) { | |||||
| if (qcDecision === "1") { | |||||
| // ✅ QC Decision 1: Accept - Update lot's required pick qty to actual pick qty | |||||
| // ✅ Use selectedLotId to get the actual pick qty from pickQtyData | |||||
| const actualPickQty = pickQtyData?.[selectedRowId || 0]?.[selectedLot?.lotId || 0] || 0; | |||||
| console.log("QC Decision 1 - Accept: Updating lot required pick qty to actual pick qty:", { | |||||
| lotId: selectedLot.lotId, | |||||
| currentRequiredQty: selectedLot.requiredQty, | |||||
| newRequiredQty: actualPickQty | |||||
| }); | |||||
| // Update stock out line status to completed | |||||
| const newStockOutLineStatus = 'completed'; | |||||
| await updateStockOutLineStatus({ | |||||
| id: selectedLotId, | |||||
| status: newStockOutLineStatus, | |||||
| qty: actualPickQty // Use actual pick qty | |||||
| }); | |||||
| } else if (qcDecision === "2") { | |||||
| // ✅ QC Decision 2: Report and Re-pick - Return to no accept and pick another lot | |||||
| console.log("QC Decision 2 - Report and Re-pick: Returning to no accept and allowing another lot pick"); | |||||
| // Update stock out line status to rejected (for re-pick) | |||||
| const newStockOutLineStatus = 'rejected'; | |||||
| await updateStockOutLineStatus({ | await updateStockOutLineStatus({ | ||||
| id: selectedLotId, | id: selectedLotId, | ||||
| status: newStockOutLineStatus, | status: newStockOutLineStatus, | ||||
| qty: selectedLot.requiredQty || 0 | qty: selectedLot.requiredQty || 0 | ||||
| }); | }); | ||||
| } else { | |||||
| console.warn("Selected lot not found for stock out line status update"); | |||||
| } | |||||
| // ✅ Fix: 2. If QC failed, also update inventory lot line status | |||||
| if (!allPassed) { | |||||
| // ✅ Update inventory lot line status to unavailable so user can pick another lot | |||||
| try { | try { | ||||
| if (selectedLot) { | |||||
| console.log("Updating inventory lot line status for failed QC:", { | |||||
| console.log("Updating inventory lot line status to unavailable for re-pick:", { | |||||
| inventoryLotLineId: selectedLot.lotId, | |||||
| status: 'unavailable' | |||||
| }); | |||||
| await updateInventoryLotLineStatus({ | |||||
| inventoryLotLineId: selectedLot.lotId, | |||||
| status: 'unavailable' | |||||
| }); | |||||
| console.log("Inventory lot line status updated to unavailable - user can now pick another lot"); | |||||
| } catch (error) { | |||||
| console.error("Failed to update inventory lot line status:", error); | |||||
| } | |||||
| // ✅ Also update inventory lot line status for failed QC items | |||||
| if (!allPassed) { | |||||
| try { | |||||
| console.log("Updating inventory lot line status for failed QC items:", { | |||||
| inventoryLotLineId: selectedLot.lotId, | inventoryLotLineId: selectedLot.lotId, | ||||
| status: 'unavailable' | status: 'unavailable' | ||||
| }); | }); | ||||
| @@ -294,18 +353,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| inventoryLotLineId: selectedLot.lotId, | inventoryLotLineId: selectedLot.lotId, | ||||
| status: 'unavailable' | status: 'unavailable' | ||||
| }); | }); | ||||
| console.log("Inventory lot line status updated to unavailable"); | |||||
| } else { | |||||
| console.warn("Selected lot not found for inventory lot line status update"); | |||||
| console.log("Failed QC items inventory lot line status updated to unavailable"); | |||||
| } catch (error) { | |||||
| console.error("Failed to update failed QC items inventory lot line status:", error); | |||||
| } | |||||
| } | } | ||||
| } catch (error) { | |||||
| console.error("Failed to update inventory lot line status:", error); | |||||
| } | |||||
| } | } | ||||
| console.log("Stock out line status updated successfully after QC"); | console.log("Stock out line status updated successfully after QC"); | ||||
| // ✅ Call callback to refresh data | |||||
| // Call callback to refresh data | |||||
| if (onStockOutLineUpdate) { | if (onStockOutLineUpdate) { | ||||
| onStockOutLineUpdate(); | onStockOutLineUpdate(); | ||||
| } | } | ||||
| @@ -316,8 +373,8 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| console.log("QC results saved successfully!"); | console.log("QC results saved successfully!"); | ||||
| // ✅ Show warning dialog for failed QC items | |||||
| if (!qcData.qcItems.every((q) => q.isPassed) && qcData.qcAccept) { | |||||
| // ✅ Show warning dialog for failed QC items when accepting | |||||
| if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) { | |||||
| submitDialogWithWarning(() => { | submitDialogWithWarning(() => { | ||||
| closeHandler?.({}, 'escapeKeyDown'); | closeHandler?.({}, 'escapeKeyDown'); | ||||
| }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); | }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); | ||||
| @@ -327,7 +384,6 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| closeHandler?.({}, 'escapeKeyDown'); | closeHandler?.({}, 'escapeKeyDown'); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error in QC submission:", error); | console.error("Error in QC submission:", error); | ||||
| // ✅ Enhanced error logging | |||||
| if (error instanceof Error) { | if (error instanceof Error) { | ||||
| console.error("Error details:", error.message); | console.error("Error details:", error.message); | ||||
| console.error("Error stack:", error.stack); | console.error("Error stack:", error.stack); | ||||
| @@ -336,9 +392,8 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| setIsSubmitting(false); | setIsSubmitting(false); | ||||
| } | } | ||||
| }, | }, | ||||
| [qcItems, closeHandler, t, itemDetail, qcDecision, accQty, selectedLotId, onStockOutLineUpdate, lotData], | |||||
| [qcItems, closeHandler, t, itemDetail, qcDecision, accQty, selectedLotId, onStockOutLineUpdate, lotData, pickQtyData, selectedRowId], | |||||
| ); | ); | ||||
| // DataGrid columns (QcComponent style) | // DataGrid columns (QcComponent style) | ||||
| const qcColumns: GridColDef[] = useMemo( | const qcColumns: GridColDef[] = useMemo( | ||||
| () => [ | () => [ | ||||
| @@ -529,77 +584,46 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| )} | )} | ||||
| <Grid item xs={12}> | <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="接受出庫" | |||||
| /> | |||||
| <Box sx={{mr:2}}> | |||||
| <TextField | |||||
| type="number" | |||||
| label={t("acceptQty")} | |||||
| sx={{ width: '150px' }} | |||||
| value={(qcDecision == 1)? accQty : 0 } | |||||
| disabled={qcDecision != 1} | |||||
| {...register("acceptQty", { | |||||
| //required: "acceptQty required!", | |||||
| })} | |||||
| error={Boolean(errors.acceptQty)} | |||||
| helperText={errors.acceptQty?.message?.toString() || ""} | |||||
| /> | |||||
| </Box> | |||||
| <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="接受出庫" | |||||
| /> | |||||
| <FormControlLabel | |||||
| value="2" | |||||
| control={<Radio />} | |||||
| sx={{"& .Mui-checked": {color: "red"}}} | |||||
| label="不接受並重新揀貨" | |||||
| /> | |||||
| <FormControlLabel | |||||
| value="3" | |||||
| control={<Radio />} | |||||
| sx={{"& .Mui-checked": {color: "blue"}}} | |||||
| label="上報品檢結果" | |||||
| /> | |||||
| </RadioGroup> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| {qcDecision == 3 && ( | |||||
| <Grid item xs={12}> | |||||
| <EscalationComponent | |||||
| forSupervisor={false} | |||||
| isCollapsed={isCollapsed} | |||||
| setIsCollapsed={setIsCollapsed} | |||||
| //escalationCombo={[]} // ✅ Add missing prop | |||||
| /> | |||||
| </Grid> | |||||
| )} | |||||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||||
| {/* ✅ Combine options 2 & 3 into one */} | |||||
| <FormControlLabel | |||||
| value="2" | |||||
| control={<Radio />} | |||||
| sx={{"& .Mui-checked": {color: "blue"}}} | |||||
| label="上報品檢結果並重新揀貨" | |||||
| /> | |||||
| </RadioGroup> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| /* | |||||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | <Stack direction="row" justifyContent="flex-start" gap={1}> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||