| @@ -34,6 +34,23 @@ import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/acti | |||
| import { | |||
| updateInventoryLotLineStatus | |||
| } 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 | |||
| interface ExtendedQcItem extends QcItemWithChecks { | |||
| @@ -44,6 +61,7 @@ interface ExtendedQcItem extends QcItemWithChecks { | |||
| stableId?: string; // ✅ Also add stableId for better row identification | |||
| } | |||
| const style = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| @@ -58,7 +76,11 @@ const style = { | |||
| maxHeight: "90vh", | |||
| overflowY: "auto", | |||
| }; | |||
| interface PickQtyData { | |||
| [lineId: number]: { | |||
| [lotId: number]: number; | |||
| }; | |||
| } | |||
| interface CommonProps extends Omit<ModalProps, "children"> { | |||
| itemDetail: GetPickOrderLineInfo & { | |||
| pickOrderCode: string; | |||
| @@ -117,6 +139,8 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| selectedLotId, | |||
| onStockOutLineUpdate, | |||
| lotData, | |||
| pickQtyData, | |||
| selectedRowId, | |||
| }) => { | |||
| const { | |||
| t, | |||
| @@ -222,10 +246,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| 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(", ")}`); | |||
| @@ -244,7 +274,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| qcItem: item.code, | |||
| qcDescription: item.description || "", | |||
| isPassed: item.qcPassed, | |||
| failQty: item.qcPassed ? 0 : (selectedLot?.requiredQty || 0), | |||
| failQty: item.qcPassed ? 0 : (selectedLot.requiredQty || 0), | |||
| remarks: item.remarks || "", | |||
| })), | |||
| }; | |||
| @@ -257,35 +287,64 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| return; | |||
| } | |||
| // ✅ Fix: Update stock out line status based on QC decision | |||
| if (selectedLotId) { // ✅ Remove qcData.qcAccept condition | |||
| // ✅ Handle different QC decisions | |||
| if (selectedLotId) { | |||
| try { | |||
| 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({ | |||
| id: selectedLotId, | |||
| status: newStockOutLineStatus, | |||
| 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 { | |||
| 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, | |||
| status: 'unavailable' | |||
| }); | |||
| @@ -294,18 +353,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| inventoryLotLineId: selectedLot.lotId, | |||
| 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"); | |||
| // ✅ Call callback to refresh data | |||
| // Call callback to refresh data | |||
| if (onStockOutLineUpdate) { | |||
| onStockOutLineUpdate(); | |||
| } | |||
| @@ -316,8 +373,8 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| 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(() => { | |||
| closeHandler?.({}, 'escapeKeyDown'); | |||
| }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); | |||
| @@ -327,7 +384,6 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| closeHandler?.({}, 'escapeKeyDown'); | |||
| } catch (error) { | |||
| console.error("Error in QC submission:", error); | |||
| // ✅ Enhanced error logging | |||
| if (error instanceof Error) { | |||
| console.error("Error details:", error.message); | |||
| console.error("Error stack:", error.stack); | |||
| @@ -336,9 +392,8 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| 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) | |||
| const qcColumns: GridColDef[] = useMemo( | |||
| () => [ | |||
| @@ -529,77 +584,46 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| )} | |||
| <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}> | |||
| <Button | |||
| variant="contained" | |||