|
|
@@ -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" |
|
|
|