@@ -38,11 +38,10 @@ export interface StockInLineEntry { | |||||
} | } | ||||
export interface PurchaseQcResult{ | export interface PurchaseQcResult{ | ||||
qcItemId: number; | |||||
id: number; | |||||
qcPassed?: boolean; | qcPassed?: boolean; | ||||
failQty?: number; | failQty?: number; | ||||
remarks?: string; | remarks?: string; | ||||
} | } | ||||
export interface StockInInput { | export interface StockInInput { | ||||
status: string; | status: string; | ||||
@@ -66,9 +65,9 @@ export interface PurchaseQCInput { | |||||
status: string; | status: string; | ||||
acceptQty: number; | acceptQty: number; | ||||
passingQty: number; | passingQty: number; | ||||
sampleRate: number; | |||||
sampleWeight: number; | |||||
totalWeight: number; | |||||
sampleRate?: number; | |||||
sampleWeight?: number; | |||||
totalWeight?: number; | |||||
qcAccept: boolean; | qcAccept: boolean; | ||||
qcDecision?: number; | qcDecision?: number; | ||||
qcResult: PurchaseQcResult[]; | qcResult: PurchaseQcResult[]; | ||||
@@ -8,6 +8,13 @@ import { Uom } from "../settings/uom"; | |||||
import { RecordsRes } from "../utils"; | import { RecordsRes } from "../utils"; | ||||
import { PutAwayLine } from "./actions"; | import { PutAwayLine } from "./actions"; | ||||
export enum StockInStatus { | |||||
PENDING = "pending", | |||||
RECEIVED = "received", | |||||
APPROVED = "escalated", | |||||
REJECTED = "rejected", | |||||
COMPLETED = "completed", | |||||
} | |||||
export interface PoResult { | export interface PoResult { | ||||
id: number; | id: number; | ||||
code: string; | code: string; | ||||
@@ -124,7 +124,7 @@ function PoInputGrid({ | |||||
handleMailTemplateForStockInLine, | handleMailTemplateForStockInLine, | ||||
printerCombo | printerCombo | ||||
}: Props) { | }: Props) { | ||||
console.log(itemDetail); | |||||
const { t } = useTranslation("purchaseOrder"); | const { t } = useTranslation("purchaseOrder"); | ||||
const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | ||||
@@ -132,7 +132,7 @@ function PoInputGrid({ | |||||
(row) => row.id as number, | (row) => row.id as number, | ||||
[], | [], | ||||
); | ); | ||||
console.log(stockInLine); | |||||
const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []); | const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []); | ||||
useEffect(() => { | useEffect(() => { | ||||
setEntries(stockInLine) | setEntries(stockInLine) | ||||
@@ -448,6 +448,7 @@ const closeNewModal = useCallback(() => { | |||||
btnSx = {label: t("escalation processing"), color:"warning.main"}; | btnSx = {label: t("escalation processing"), color:"warning.main"}; | ||||
break;} | break;} | ||||
case "rejected": | case "rejected": | ||||
case "partially_completed": | |||||
case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break; | case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break; | ||||
default: btnSx = {label: t("qc processing"), color:"success.main"}; | default: btnSx = {label: t("qc processing"), color:"success.main"}; | ||||
} | } | ||||
@@ -338,7 +338,8 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, printer | |||||
); | ); | ||||
const addRowDefaultValue = useMemo(() => { | const addRowDefaultValue = useMemo(() => { | ||||
const defaultMaxQty = watch("acceptedQty") - watch("putAwayLines").reduce((acc, cur) => acc + cur.qty, 0) | |||||
const defaultMaxQty = Number(itemDetail.demandQty?? itemDetail.acceptedQty)//watch("acceptedQty") | |||||
- watch("putAwayLines").reduce((acc, cur) => acc + cur.qty, 0) | |||||
const defaultWarehouseId = itemDetail.defaultWarehouseId ?? 1 | const defaultWarehouseId = itemDetail.defaultWarehouseId ?? 1 | ||||
const defaultWarehouse = options.find((o) => o.value === defaultWarehouseId)?.label | const defaultWarehouse = options.find((o) => o.value === defaultWarehouseId)?.label | ||||
return {qty: defaultMaxQty, warehouseId: defaultWarehouseId, warehouse: defaultWarehouse, printQty: 1, _isNew: true } as Partial<PutAwayLine> | return {qty: defaultMaxQty, warehouseId: defaultWarehouseId, warehouse: defaultWarehouse, printQty: 1, _isNew: true } as Partial<PutAwayLine> | ||||
@@ -19,7 +19,7 @@ import { | |||||
Tooltip, | Tooltip, | ||||
Typography, | Typography, | ||||
} from "@mui/material"; | } from "@mui/material"; | ||||
import { useFormContext, Controller } from "react-hook-form"; | |||||
import { useFormContext, Controller, FieldPath, useFieldArray } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | ||||
@@ -59,8 +59,8 @@ interface Props { | |||||
itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | ||||
qc: QcItemWithChecks[]; | qc: QcItemWithChecks[]; | ||||
disabled: boolean; | disabled: boolean; | ||||
qcItems: QcData[] | |||||
setQcItems: Dispatch<SetStateAction<QcData[]>> | |||||
// qcItems: QcData[] | |||||
// setQcItems: Dispatch<SetStateAction<QcData[]>> | |||||
} | } | ||||
type EntryError = | type EntryError = | ||||
@@ -71,7 +71,7 @@ type EntryError = | |||||
type QcRow = TableRow<Partial<QcData>, EntryError>; | type QcRow = TableRow<Partial<QcData>, EntryError>; | ||||
// fetchQcItemCheck | // fetchQcItemCheck | ||||
const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcItems }) => { | |||||
const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled = false }) => { | |||||
const { t } = useTranslation("purchaseOrder"); | const { t } = useTranslation("purchaseOrder"); | ||||
const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
const { | const { | ||||
@@ -93,8 +93,9 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
// const [qcResult, setQcResult] = useState(); | // const [qcResult, setQcResult] = useState(); | ||||
const qcAccept = watch("qcAccept"); | const qcAccept = watch("qcAccept"); | ||||
const qcDecision = watch("qcDecision"); //WIP | const qcDecision = watch("qcDecision"); //WIP | ||||
const qcResult = watch("qcResult"); | |||||
console.log(qcResult); | |||||
// const qcResult = useMemo(() => [...watch("qcResult")], [watch("qcResult")]); | |||||
const qcResult = [...watch("qcResult")]; | |||||
// const [qcAccept, setQcAccept] = useState(true); | // const [qcAccept, setQcAccept] = useState(true); | ||||
// const [qcItems, setQcItems] = useState(dummyQCData) | // const [qcItems, setQcItems] = useState(dummyQCData) | ||||
@@ -119,32 +120,59 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
[], | [], | ||||
); | ); | ||||
// W I P // | |||||
const validateFieldFail = (field : FieldPath<PurchaseQCInput>, condition: boolean, message: string) : boolean => { | |||||
// console.log("Checking if " + message) | |||||
if (condition) { setError(field, { message: message}); return false; } | |||||
else { clearErrors(field); return true; } | |||||
} | |||||
//// validate form | //// validate form | ||||
const accQty = watch("acceptQty"); | const accQty = watch("acceptQty"); | ||||
const validateForm = useCallback(() => { | const validateForm = useCallback(() => { | ||||
if (qcDecision == 1) { | if (qcDecision == 1) { | ||||
if (accQty > itemDetail.acceptedQty) { | |||||
setError("acceptQty", { | |||||
message: `${t("acceptQty must not greater than")} ${ | |||||
itemDetail.acceptedQty | |||||
}`, | |||||
type: "required", | |||||
}); | |||||
if (accQty > itemDetail.acceptedQty){ | |||||
setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${ | |||||
itemDetail.acceptedQty}` }); | |||||
} | } | ||||
if (accQty < 1) { | |||||
setError("acceptQty", { | |||||
message: t("minimal value is 1"), | |||||
type: "required", | |||||
}); | |||||
if (accQty < 1){ | |||||
setError("acceptQty", { message: t("minimal value is 1") }); | |||||
} | } | ||||
if (isNaN(accQty)) { | |||||
setError("acceptQty", { | |||||
message: t("value must be a number"), | |||||
type: "required", | |||||
}); | |||||
if (isNaN(accQty)){ | |||||
setError("acceptQty", { message: t("value must be a number") }); | |||||
} | } | ||||
} | } | ||||
}, [accQty, qcDecision]); | |||||
},[setError, qcDecision, accQty, itemDetail]) | |||||
useEffect(() => { // W I P // ----- | |||||
if (qcDecision == 1) { | |||||
if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${ | |||||
itemDetail.acceptedQty}`)) return; | |||||
if (validateFieldFail("acceptQty", accQty < 1, t("minimal value is 1"))) return; | |||||
if (validateFieldFail("acceptQty", isNaN(accQty), t("value must be a number"))) return; | |||||
} | |||||
const qcResultItems = qcResult; console.log("Validating:", qcResultItems); | |||||
// Check if failed items have failed quantity | |||||
const failedItemsWithoutQty = qcResultItems.filter(item => | |||||
item.qcPassed === false && (!item.failQty || item.failQty <= 0) | |||||
); | |||||
if (validateFieldFail("qcResult", failedItemsWithoutQty.length > 0, `${t("Failed items must have failed quantity")}`)) return; | |||||
// Check if all QC items have results | |||||
const itemsWithoutResult = qcResultItems.filter(item => item.qcPassed === undefined); | |||||
if (validateFieldFail("qcDecision", (itemsWithoutResult.length > 0 && itemDetail.status != "escalated"), | |||||
`${t("QC items without result")}`)) return; | |||||
if (validateFieldFail("qcDecision", (!qcResultItems.every((qc) => qc.qcPassed) && qcDecision == 1 && itemDetail.status != "escalated"), | |||||
"有不合格檢查項目,無法收貨!")) return; // TODO: Fix it please | |||||
// submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?", | |||||
// confirmButtonText: t("confirm putaway"), html: ""}); | |||||
// return; | |||||
// console.log("Validated without errors"); | |||||
}, [accQty, qcDecision, watch("qcResult")]); | |||||
useEffect(() => { | useEffect(() => { | ||||
clearErrors(); | clearErrors(); | ||||
@@ -188,39 +216,41 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | ||||
} | } | ||||
const qcColumns: GridColDef[] = [ | |||||
const qcColumns: GridColDef[] = useMemo(() => [ | |||||
{ | { | ||||
field: "code", | field: "code", | ||||
headerName: t("qcItem"), | headerName: t("qcItem"), | ||||
flex: 2, | flex: 2, | ||||
renderCell: (params) => ( | |||||
renderCell: (params) => { | |||||
const index = params.api.getRowIndexRelativeToVisibleRows(params.id) + 1; | |||||
return ( | |||||
<Box> | <Box> | ||||
<b>{`${params.api.getRowIndexRelativeToVisibleRows(params.id) + 1}. ${params.value}`}</b><br/> | |||||
<b>{`${index}. ${params.value}`}</b><br/> | |||||
{params.row.name}<br/> | {params.row.name}<br/> | ||||
</Box> | </Box> | ||||
), | |||||
)}, | |||||
}, | }, | ||||
{ | { | ||||
field: 'qcPassed', | |||||
field: 'qcResult', | |||||
headerName: t("qcResult"), | headerName: t("qcResult"), | ||||
flex: 1.5, | flex: 1.5, | ||||
renderCell: (params) => { | renderCell: (params) => { | ||||
const currentValue = params.row; | |||||
const rowValue = params.row; | |||||
const index = params.api.getRowIndexRelativeToVisibleRows(params.id); | const index = params.api.getRowIndexRelativeToVisibleRows(params.id); | ||||
// console.log(currentValue.row); | |||||
// console.log(rowValue.row); | |||||
return ( | return ( | ||||
<FormControl> | <FormControl> | ||||
<RadioGroup | <RadioGroup | ||||
row | row | ||||
aria-labelledby="demo-radio-buttons-group-label" | aria-labelledby="demo-radio-buttons-group-label" | ||||
value={currentValue.qcPassed === undefined ? "" : (currentValue.qcPassed ? "true" : "false")} | |||||
// value={currentValue.qcPassed === undefined ? (currentValue.failQty!==undefined?(currentValue.failQty==0?"true":"false"):"") : (currentValue.qcPassed ? "true" : "false")} | |||||
// defaultValue={""} | |||||
value={rowValue.qcPassed === undefined ? "" : (rowValue.qcPassed ? "true" : "false")} | |||||
onChange={(e) => { | onChange={(e) => { | ||||
const value = e.target.value; | |||||
setQcItems((prev) => | |||||
prev.map((r): QcData => (r.id === params.id ? { ...r, qcPassed: value === "true" } : r)) | |||||
); | |||||
// setValue(`qcResult.${index}.qcPassed`, value == "true"); | |||||
const value = (e.target.value === "true"); | |||||
// setQcItems((prev) => | |||||
// prev.map((r): QcData => (r.id === params.id ? { ...r, qcPassed: value === "true" } : r)) | |||||
// ); | |||||
setValue(`qcResult.${index}.qcPassed`, value); | |||||
}} | }} | ||||
name={`qcPassed-${params.id}`} | name={`qcPassed-${params.id}`} | ||||
> | > | ||||
@@ -230,7 +260,7 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
label="合格" | label="合格" | ||||
disabled={disabled || itemDetail.status == "escalated"} | disabled={disabled || itemDetail.status == "escalated"} | ||||
sx={{ | sx={{ | ||||
color: currentValue.qcPassed === true ? "green" : "inherit", | |||||
color: rowValue.qcPassed === true ? "green" : "inherit", | |||||
"& .Mui-checked": {color: "green"} | "& .Mui-checked": {color: "green"} | ||||
}} | }} | ||||
/> | /> | ||||
@@ -240,7 +270,7 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
label="不合格" | label="不合格" | ||||
disabled={disabled || itemDetail.status == "escalated"} | disabled={disabled || itemDetail.status == "escalated"} | ||||
sx={{ | sx={{ | ||||
color: currentValue.qcPassed === false ? "red" : "inherit", | |||||
color: rowValue.qcPassed === false ? "red" : "inherit", | |||||
"& .Mui-checked": {color: "red"} | "& .Mui-checked": {color: "red"} | ||||
}} | }} | ||||
/> | /> | ||||
@@ -254,58 +284,68 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
headerName: t("failedQty"), | headerName: t("failedQty"), | ||||
flex: 1, | flex: 1, | ||||
// editable: true, | // editable: true, | ||||
renderCell: (params) => ( | |||||
<TextField | |||||
type="number" | |||||
size="small" | |||||
value={!params.row.qcPassed? (params.value ?? '') : '0'} | |||||
disabled={params.row.qcPassed || disabled || itemDetail.status == "escalated"} | |||||
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)) | |||||
); | |||||
// setValue(`failQty`,failQty); | |||||
}} | |||||
onClick={(e) => e.stopPropagation()} | |||||
onMouseDown={(e) => e.stopPropagation()} | |||||
onKeyDown={(e) => e.stopPropagation()} | |||||
inputProps={{ min: 0 }} | |||||
sx={{ width: '100%' }} | |||||
/> | |||||
), | |||||
renderCell: (params) => { | |||||
const index = params.api.getRowIndexRelativeToVisibleRows(params.id); | |||||
return ( | |||||
<TextField | |||||
type="number" | |||||
size="small" | |||||
value={!params.row.qcPassed? params.value : '0'} | |||||
disabled={params.row.qcPassed || disabled || itemDetail.status == "escalated"} | |||||
onBlur={(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)) | |||||
// ); | |||||
setValue(`qcResult.${index}.failQty`, next); | |||||
}} | |||||
onClick={(e) => e.stopPropagation()} | |||||
onMouseDown={(e) => e.stopPropagation()} | |||||
onKeyDown={(e) => e.stopPropagation()} | |||||
inputProps={{ min: 0 }} | |||||
sx={{ width: '100%' }} | |||||
/> | |||||
); | |||||
}, | |||||
}, | }, | ||||
{ | { | ||||
field: "remarks", | field: "remarks", | ||||
headerName: t("remarks"), | headerName: t("remarks"), | ||||
flex: 2, | flex: 2, | ||||
renderCell: (params) => ( | |||||
<TextField | |||||
size="small" | |||||
value={params.value ?? ''} | |||||
disabled={disabled || itemDetail.status == "escalated"} | |||||
onChange={(e) => { | |||||
const remarks = e.target.value; | |||||
// const next = v === '' ? undefined : Number(v); | |||||
// if (Number.isNaN(next)) return; | |||||
setQcItems((prev) => | |||||
prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r)) | |||||
); | |||||
}} | |||||
// {...register(`qcResult.${params.row.rowIndex}.remarks`, { | |||||
// required: "remarks required!", | |||||
// })} | |||||
onClick={(e) => e.stopPropagation()} | |||||
onMouseDown={(e) => e.stopPropagation()} | |||||
onKeyDown={(e) => e.stopPropagation()} | |||||
inputProps={{ min: 0 }} | |||||
sx={{ width: '100%' }} | |||||
/> | |||||
), | |||||
renderCell: (params) => { | |||||
const index = params.api.getRowIndexRelativeToVisibleRows(params.id); | |||||
return ( | |||||
<TextField | |||||
size="small" | |||||
defaultValue={params.value} | |||||
disabled={disabled || itemDetail.status == "escalated"} | |||||
onBlur={(e) => { | |||||
const value = e.target.value; | |||||
setValue(`qcResult.${index}.remarks`, value); | |||||
}} | |||||
// onChange={(e) => { | |||||
// const remarks = e.target.value; | |||||
// // const next = v === '' ? undefined : Number(v); | |||||
// // if (Number.isNaN(next)) return; | |||||
// // setQcItems((prev) => | |||||
// // prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r)) | |||||
// // ); | |||||
// }} | |||||
// {...register(`qcResult.${index}.remarks`, { | |||||
// required: "remarks required!", | |||||
// })} | |||||
onClick={(e) => e.stopPropagation()} | |||||
onMouseDown={(e) => e.stopPropagation()} | |||||
onKeyDown={(e) => e.stopPropagation()} | |||||
inputProps={{ min: 0 }} | |||||
sx={{ width: '100%' }} | |||||
/> | |||||
); | |||||
}, | |||||
}, | }, | ||||
] | |||||
], []) | |||||
// Set initial value for acceptQty | // Set initial value for acceptQty | ||||
useEffect(() => { | useEffect(() => { | ||||
@@ -316,12 +356,23 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
} | } | ||||
}, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]); | }, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]); | ||||
useEffect(() => { | |||||
// console.log("Qc Result updated:", qcResult); | |||||
if (qcResult.length < 1) { // New QC | |||||
const mutableQcData = dummyQCData; | |||||
// const mutableQcData = JSON.parse(JSON.stringify(dummyQCData)); | |||||
// replace([mutableQcData]); | |||||
setValue("qcResult", mutableQcData); | |||||
// setValue("qcResult.0.qcPassed", false); | |||||
} | |||||
}, [qcResult, setValue]) | |||||
// const [openCollapse, setOpenCollapse] = useState(false) | // const [openCollapse, setOpenCollapse] = useState(false) | ||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(true); | const [isCollapsed, setIsCollapsed] = useState<boolean>(true); | ||||
const onFailedOpenCollapse = useCallback((qcItems: PurchaseQcResult[]) => { | const onFailedOpenCollapse = useCallback((qcItems: PurchaseQcResult[]) => { | ||||
const isFailed = qcItems.some((qc) => !qc.qcPassed) | const isFailed = qcItems.some((qc) => !qc.qcPassed) | ||||
console.log(isFailed) | |||||
// console.log(isFailed) | |||||
if (isFailed) { | if (isFailed) { | ||||
setIsCollapsed(true) | setIsCollapsed(true) | ||||
} else { | } else { | ||||
@@ -336,30 +387,29 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
useEffect(() => { | useEffect(() => { | ||||
console.log("ItemDetail in QC:", itemDetail); | |||||
console.log("Qc Result in QC:", qcResult); | |||||
console.log("%c QC ItemDetail updated:", "color: gold", itemDetail); | |||||
}, [itemDetail]); | }, [itemDetail]); | ||||
const setQcDecision = (status : string | undefined) => { | |||||
const setDefaultQcDecision = (status : string | undefined) => { | |||||
const param = status?.toLowerCase(); | const param = status?.toLowerCase(); | ||||
if (param !== undefined && param !== null) { | if (param !== undefined && param !== null) { | ||||
if (param == "completed") { | |||||
if (param == "completed" || param == "partially_completed") { | |||||
return 1; | return 1; | ||||
} else if (param == "rejected") { | } else if (param == "rejected") { | ||||
return 2; | return 2; | ||||
} else if (param == "escalated") { | } else if (param == "escalated") { | ||||
return 3; | |||||
return 1; // For new flow | |||||
// return 3; | |||||
} else { return undefined; } | } else { return undefined; } | ||||
} else { | } else { | ||||
return undefined; | return undefined; | ||||
} | } | ||||
} | } | ||||
useEffect(() => { | |||||
// onFailedOpenCollapse(qcItems) // This function is no longer needed | |||||
}, [qcItems]); | |||||
// useEffect(() => { | |||||
// // onFailedOpenCollapse(qcItems) | |||||
// }, [qcItems]); | |||||
return ( | return ( | ||||
<> | <> | ||||
@@ -405,9 +455,11 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
/> */} | /> */} | ||||
<StyledDataGrid | <StyledDataGrid | ||||
columns={qcColumns} | columns={qcColumns} | ||||
rows={qcResult && qcResult.length > 0 ? qcResult : qcItems} | |||||
rows={qcResult} | |||||
// rows={qcResult && qcResult.length > 0 ? qcResult : qcItems} | |||||
// rows={disabled? qcResult:qcItems} | // rows={disabled? qcResult:qcItems} | ||||
autoHeight | autoHeight | ||||
sortModel={[]} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
</> | </> | ||||
@@ -443,57 +495,69 @@ const QcComponent: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQc | |||||
name="qcDecision" | name="qcDecision" | ||||
// name="qcAccept" | // name="qcAccept" | ||||
control={control} | control={control} | ||||
defaultValue={setQcDecision(itemDetail?.status)} | |||||
defaultValue={setDefaultQcDecision(itemDetail?.status)} | |||||
// defaultValue={true} | // defaultValue={true} | ||||
render={({ field }) => ( | render={({ field }) => ( | ||||
<RadioGroup | |||||
row | |||||
aria-labelledby="demo-radio-buttons-group-label" | |||||
{...field} | |||||
value={field.value} | |||||
// value={field.value?.toString() || "true"} | |||||
onChange={(e) => { | |||||
const value = e.target.value.toString();// === 'true'; | |||||
if (value != "1" && Boolean(errors.acceptQty)) { | |||||
// if (!value && Boolean(errors.acceptQty)) { | |||||
setValue("acceptQty", itemDetail.acceptedQty ?? 0); | |||||
} | |||||
field.onChange(value); | |||||
}} | |||||
> | |||||
<FormControlLabel disabled={disabled} | |||||
value="1" control={<Radio />} label="接受來貨" /> | |||||
{itemDetail.status == "escalated" && (<Box sx={{mr:2}}> | |||||
<TextField | |||||
type="number" | |||||
label={t("acceptQty")} | |||||
sx={{ width: '150px' }} | |||||
value={(qcDecision == 1)? accQty : 0 } | |||||
// value={qcAccept? accQty : 0 } | |||||
disabled={qcDecision != 1 || disabled} | |||||
// disabled={!qcAccept || disabled} | |||||
{...register("acceptQty", { | |||||
required: "acceptQty required!", | |||||
})} | |||||
error={Boolean(errors.acceptQty)} | |||||
helperText={errors.acceptQty?.message} | |||||
/> | |||||
</Box>)} | |||||
{itemDetail.status == "pending" && (<> | |||||
<FormControlLabel disabled={disabled} | |||||
value="2" control={<Radio />} | |||||
sx={{"& .Mui-checked": {color: "red"}}} | |||||
label="不接受並退貨" /> | |||||
<> | |||||
{/* <Typography sx={{color:"red"}}> | |||||
{errors.qcDecision?.message} | |||||
</Typography> */} | |||||
<RadioGroup | |||||
row | |||||
aria-labelledby="demo-radio-buttons-group-label" | |||||
{...field} | |||||
value={field.value} | |||||
// value={field.value?.toString() || "true"} | |||||
onChange={(e) => { | |||||
const value = e.target.value.toString();// === 'true'; | |||||
if (value != "1" && Boolean(errors.acceptQty)) { | |||||
// if (!value && Boolean(errors.acceptQty)) { | |||||
setValue("acceptQty", itemDetail.acceptedQty ?? 0); | |||||
} | |||||
field.onChange(value); | |||||
}} | |||||
> | |||||
<FormControlLabel disabled={disabled} | |||||
value="1" control={<Radio />} label="接受來貨" /> | |||||
<FormControlLabel disabled={disabled} | |||||
value="3" control={<Radio />} | |||||
sx={{"& .Mui-checked": {color: "blue"}}} | |||||
label="上報品檢結果" /> | |||||
</>)} | |||||
</RadioGroup> | |||||
{(itemDetail.status == "escalated" || (disabled && accQty != itemDetail.acceptedQty && qcDecision == 1)) && ( //TODO Improve | |||||
<Box sx={{mr:2}}> | |||||
<TextField | |||||
type="number" | |||||
label={t("acceptQty")} | |||||
sx={{ width: '150px' }} | |||||
value={(qcDecision == 1)? Number(accQty) : 0 } | |||||
// value={qcAccept? accQty : 0 } | |||||
disabled={qcDecision != 1 || disabled} | |||||
// disabled={!qcAccept || disabled} | |||||
{...register("acceptQty", { | |||||
required: "acceptQty required!", | |||||
})} | |||||
error={Boolean(errors.acceptQty)} | |||||
helperText={errors.acceptQty?.message} | |||||
/> | |||||
<TextField | |||||
type="number" | |||||
label={t("rejectQty")} | |||||
sx={{ width: '150px' }} | |||||
value={itemDetail.acceptedQty - accQty} | |||||
disabled={true} | |||||
/> | |||||
</Box>)} | |||||
<FormControlLabel disabled={disabled} | |||||
value="2" control={<Radio />} | |||||
sx={{"& .Mui-checked": {color: "red"}}} | |||||
label= {itemDetail.status == "escalated" ? "全部拒絕並退貨" : "不接受並退貨"} /> | |||||
{(itemDetail.status == "pending" || disabled) && (<> | |||||
<FormControlLabel disabled={disabled} | |||||
value="3" control={<Radio />} | |||||
sx={{"& .Mui-checked": {color: "blue"}}} | |||||
label="上報品檢結果" /> | |||||
</>)} | |||||
</RadioGroup> | |||||
</> | |||||
)} | )} | ||||
/> | /> | ||||
</FormControl> | </FormControl> | ||||
@@ -30,7 +30,6 @@ import dayjs from "dayjs"; | |||||
import { fetchPoQrcode } from "@/app/api/pdf/actions"; | import { fetchPoQrcode } from "@/app/api/pdf/actions"; | ||||
import { downloadFile } from "@/app/utils/commonUtil"; | import { downloadFile } from "@/app/utils/commonUtil"; | ||||
import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
import { watch } from "fs"; | |||||
import { EscalationResult } from "@/app/api/escalation"; | import { EscalationResult } from "@/app/api/escalation"; | ||||
import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
@@ -93,7 +92,7 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||||
} = useTranslation("purchaseOrder"); | } = useTranslation("purchaseOrder"); | ||||
// Select Printer | // Select Printer | ||||
const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]) | |||||
const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]); | |||||
const defaultNewValue = useMemo(() => { | const defaultNewValue = useMemo(() => { | ||||
return ( | return ( | ||||
@@ -104,7 +103,7 @@ const defaultNewValue = useMemo(() => { | |||||
// putAwayLines: dummyPutAwayLine, | // putAwayLines: dummyPutAwayLine, | ||||
// putAwayLines: itemDetail.putAwayLines.map((line) => (return {...line, printQty: 1})) ?? [], | // putAwayLines: itemDetail.putAwayLines.map((line) => (return {...line, printQty: 1})) ?? [], | ||||
putAwayLines: itemDetail.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false})) ?? [], | putAwayLines: itemDetail.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false})) ?? [], | ||||
qcResult: (itemDetail.qcResult && itemDetail.qcResult?.length > 0) ? itemDetail.qcResult : [],//[...dummyQCData], | |||||
// qcResult: (itemDetail.qcResult && itemDetail.qcResult?.length > 0) ? itemDetail.qcResult : [],//[...dummyQCData], | |||||
escResult: (itemDetail.escResult && itemDetail.escResult?.length > 0) ? itemDetail.escResult : [], | escResult: (itemDetail.escResult && itemDetail.escResult?.length > 0) ? itemDetail.escResult : [], | ||||
receiptDate: itemDetail.receiptDate ?? dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | receiptDate: itemDetail.receiptDate ?? dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | ||||
acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, | acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, | ||||
@@ -135,27 +134,17 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
} else return false; | } else return false; | ||||
}; | }; | ||||
useEffect(() => { | |||||
formProps.reset({ | |||||
...itemDetail, | |||||
dnDate: dayjsToInputDateString(dayjs()), | |||||
// putAwayLines: dummyPutAwayLine, | |||||
putAwayLines: itemDetail.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false})) ?? [], | |||||
}) | |||||
setOpenPutaway(isPutaway); | |||||
}, [open, itemDetail]) | |||||
const [viewOnly, setViewOnly] = useState(false); | const [viewOnly, setViewOnly] = useState(false); | ||||
useEffect(() => { | useEffect(() => { | ||||
if (itemDetail && itemDetail.status) { | if (itemDetail && itemDetail.status) { | ||||
const isViewOnly = itemDetail.status.toLowerCase() == "completed" | const isViewOnly = itemDetail.status.toLowerCase() == "completed" | ||||
|| itemDetail.status.toLowerCase() == "partially_completed" // TODO update DB | |||||
|| itemDetail.status.toLowerCase() == "rejected" | || itemDetail.status.toLowerCase() == "rejected" | ||||
|| (itemDetail.status.toLowerCase() == "escalated" && session?.id != itemDetail.handlerId) | || (itemDetail.status.toLowerCase() == "escalated" && session?.id != itemDetail.handlerId) | ||||
setViewOnly(isViewOnly) | setViewOnly(isViewOnly) | ||||
} | } | ||||
console.log("ITEM", itemDetail); | |||||
console.log("Modal ItemDetail updated:", itemDetail); | |||||
}, [itemDetail]); | }, [itemDetail]); | ||||
useEffect(() => { | useEffect(() => { | ||||
@@ -170,7 +159,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
setQcItems(dummyQCData); | setQcItems(dummyQCData); | ||||
setOpenPutaway(isPutaway); | setOpenPutaway(isPutaway); | ||||
}, [open, defaultNewValue]) | |||||
}, [open]) | |||||
const [openPutaway, setOpenPutaway] = useState(false); | const [openPutaway, setOpenPutaway] = useState(false); | ||||
const onOpenPutaway = useCallback(() => { | const onOpenPutaway = useCallback(() => { | ||||
@@ -216,12 +205,17 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
async (data, event) => { | async (data, event) => { | ||||
console.log("QC Submission:", event!.nativeEvent); | console.log("QC Submission:", event!.nativeEvent); | ||||
// TODO: Move validation into QC page | // TODO: Move validation into QC page | ||||
// if (errors.length > 0) { | |||||
// alert(`未完成品檢: ${errors.map((err) => err[1].message)}`); | |||||
// return; | |||||
// } | |||||
// Get QC data from the shared form context | // Get QC data from the shared form context | ||||
const qcAccept = data.qcDecision == 1; | const qcAccept = data.qcDecision == 1; | ||||
// const qcAccept = data.qcAccept; | // const qcAccept = data.qcAccept; | ||||
const acceptQty = data.acceptQty as number; | |||||
const qcResults = qcItems; // qcItems; | |||||
const acceptQty = Number(data.acceptQty); | |||||
const qcResults = data.qcResult || dummyQCData; // qcItems; | |||||
// const qcResults = data.qcResult as PurchaseQcResult[]; // qcItems; | // const qcResults = data.qcResult as PurchaseQcResult[]; // qcItems; | ||||
// const qcResults = viewOnly? data.qcResult as PurchaseQcResult[] : qcItems; | // const qcResults = viewOnly? data.qcResult as PurchaseQcResult[] : qcItems; | ||||
@@ -239,9 +233,10 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
} | } | ||||
// Check if QC accept decision is made | // Check if QC accept decision is made | ||||
if (data.qcDecision === undefined) { | |||||
// if (qcAccept === undefined) { | // if (qcAccept === undefined) { | ||||
// validationErrors.push("QC accept/reject decision is required"); | |||||
// } | |||||
validationErrors.push(t("QC decision is required")); | |||||
} | |||||
// Check if accept quantity is valid | // Check if accept quantity is valid | ||||
if (acceptQty === undefined || acceptQty <= 0) { | if (acceptQty === undefined || acceptQty <= 0) { | ||||
@@ -250,10 +245,12 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
// Check if dates are input | // Check if dates are input | ||||
if (data.productionDate === undefined || data.productionDate == null) { | if (data.productionDate === undefined || data.productionDate == null) { | ||||
validationErrors.push("請輸入生產日期!"); | |||||
alert("請輸入生產日期!"); | |||||
return; | |||||
} | } | ||||
if (data.expiryDate === undefined || data.expiryDate == null) { | if (data.expiryDate === undefined || data.expiryDate == null) { | ||||
validationErrors.push("請輸入到期日!"); | |||||
alert("請輸入到期日!"); | |||||
return; | |||||
} | } | ||||
if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && itemDetail.status != "escalated") { //TODO: fix it please! | if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && itemDetail.status != "escalated") { //TODO: fix it please! | ||||
validationErrors.push("有不合格檢查項目,無法收貨!"); | validationErrors.push("有不合格檢查項目,無法收貨!"); | ||||
@@ -265,10 +262,10 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
// Check if all QC items have results | // Check if all QC items have results | ||||
const itemsWithoutResult = qcResults.filter(item => item.qcPassed === undefined); | const itemsWithoutResult = qcResults.filter(item => item.qcPassed === undefined); | ||||
// if (itemsWithoutResult.length > 0 && itemDetail.status != "escalated") { //TODO: fix it please! | |||||
// validationErrors.push(`${t("QC items without result")}`); | |||||
// // validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`); | |||||
// } | |||||
if (itemsWithoutResult.length > 0 && itemDetail.status != "escalated") { //TODO: fix it please! | |||||
validationErrors.push(`${t("QC items without result")}`); | |||||
// validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`); | |||||
} | |||||
if (validationErrors.length > 0) { | if (validationErrors.length > 0) { | ||||
console.error("QC Validation failed:", validationErrors); | console.error("QC Validation failed:", validationErrors); | ||||
@@ -286,7 +283,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
qcAccept: qcAccept? qcAccept : false, | qcAccept: qcAccept? qcAccept : false, | ||||
acceptQty: acceptQty? acceptQty : 0, | acceptQty: acceptQty? acceptQty : 0, | ||||
qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({ | qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({ | ||||
// id: item.id, | |||||
id: item.id, | |||||
qcItemId: item.id, | qcItemId: item.id, | ||||
// code: item.code, | // code: item.code, | ||||
// qcDescription: item.qcDescription, | // qcDescription: item.qcDescription, | ||||
@@ -307,7 +304,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
recordDate : dayjsToInputDateString(dayjs()), | recordDate : dayjsToInputDateString(dayjs()), | ||||
handlerId : Number(session?.id), | handlerId : Number(session?.id), | ||||
} | } | ||||
console.log("ESCALATION RESULT", escalationLog); | |||||
console.log("Escalation Data for submission", escalationLog); | |||||
await postStockInLine({...qcData, escalationLog}); | await postStockInLine({...qcData, escalationLog}); | ||||
} else { | } else { | ||||
await postStockInLine(qcData); | await postStockInLine(qcData); | ||||
@@ -323,7 +320,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
return ; | return ; | ||||
}, | }, | ||||
[onOpenPutaway, qcItems], | |||||
[onOpenPutaway, qcItems, formProps.formState.errors], | |||||
); | ); | ||||
const postStockInLine = useCallback(async (args: ModalFormInput) => { | const postStockInLine = useCallback(async (args: ModalFormInput) => { | ||||
@@ -371,8 +368,10 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
// binLocation: data.binLocation, | // binLocation: data.binLocation, | ||||
// putawayQuantity: data.putawayQuantity, | // putawayQuantity: data.putawayQuantity, | ||||
// putawayNotes: data.putawayNotes, | // putawayNotes: data.putawayNotes, | ||||
acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, | |||||
...data, | |||||
acceptQty: Number(data.acceptQty?? (itemDetail.demandQty?? (itemDetail.acceptedQty))), //TODO improve | |||||
warehouseId: data.warehouseId, | |||||
status: data.status, //TODO Fix it! | |||||
// ...data, | |||||
dnDate : data.dnDate? arrayToInputDateString(data.dnDate) : dayjsToInputDateString(dayjs()), | dnDate : data.dnDate? arrayToInputDateString(data.dnDate) : dayjsToInputDateString(dayjs()), | ||||
productionDate : arrayToInputDateString(data.productionDate), | productionDate : arrayToInputDateString(data.productionDate), | ||||
@@ -386,6 +385,21 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
} as ModalFormInput; | } as ModalFormInput; | ||||
console.log("Putaway Data:", putawayData); | console.log("Putaway Data:", putawayData); | ||||
console.log("DEBUG",data.putAwayLines); | |||||
if (data.putAwayLines!!.filter((line) => line._isNew !== false).length <= 0) { | |||||
alert("請新增上架資料!"); | |||||
return; | |||||
} | |||||
console.log(typeof data.putAwayLines!![0].qty + " = 'number'"); | |||||
console.log(typeof data.putAwayLines!![0].qty !== "number"); | |||||
if (data.putAwayLines!!.filter((line) => /[^0-9]/.test(String(line.qty))).length > 0) { //TODO Improve | |||||
alert("上架數量不正確!"); | |||||
return; | |||||
} | |||||
if (data.putAwayLines!!.reduce((acc, cur) => acc + Number(cur.qty), 0) > putawayData.acceptQty!!) { | |||||
alert(`上架數量不能大於 ${putawayData.acceptQty}!`); | |||||
return; | |||||
} | |||||
// Handle putaway submission logic here | // Handle putaway submission logic here | ||||
const res = await postStockInLine(putawayData); | const res = await postStockInLine(putawayData); | ||||
console.log("result ", res); | console.log("result ", res); | ||||
@@ -438,7 +452,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
useEffect(() => { | useEffect(() => { | ||||
// maybe check if submitted before | // maybe check if submitted before | ||||
console.log(qcItems) | |||||
console.log("Modal QC Items updated:", qcItems); | |||||
// checkQcIsPassed(qcItems) | // checkQcIsPassed(qcItems) | ||||
}, [qcItems, checkQcIsPassed]) | }, [qcItems, checkQcIsPassed]) | ||||
@@ -491,7 +505,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
color="primary" | color="primary" | ||||
sx={{ mt: 1 }} | sx={{ mt: 1 }} | ||||
onClick={handlePrint} | onClick={handlePrint} | ||||
disabled={isPrinting} | |||||
disabled={isPrinting || printerCombo.length <= 0} | |||||
> | > | ||||
{isPrinting ? t("Printing") : t("print")} | {isPrinting ? t("Printing") : t("print")} | ||||
</Button> | </Button> | ||||
@@ -543,8 +557,8 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||||
qc={qc!} | qc={qc!} | ||||
itemDetail={itemDetail} | itemDetail={itemDetail} | ||||
disabled={viewOnly} | disabled={viewOnly} | ||||
qcItems={qcItems} | |||||
setQcItems={setQcItems} | |||||
// qcItems={qcItems} | |||||
// setQcItems={setQcItems} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
@@ -92,10 +92,19 @@ const StockInFormVer2: React.FC<Props> = ({ | |||||
const expiryDate = watch("expiryDate"); | const expiryDate = watch("expiryDate"); | ||||
const uom = watch("uom"); | const uom = watch("uom"); | ||||
//// TODO : Add Checking //// | |||||
// Check if dates are input | |||||
// if (data.productionDate === undefined || data.productionDate == null) { | |||||
// validationErrors.push("請輸入生產日期!"); | |||||
// } | |||||
// if (data.expiryDate === undefined || data.expiryDate == null) { | |||||
// validationErrors.push("請輸入到期日!"); | |||||
// } | |||||
useEffect(() => { | useEffect(() => { | ||||
console.log(uom); | |||||
console.log(productionDate); | |||||
console.log(expiryDate); | |||||
// console.log(uom); | |||||
// console.log(productionDate); | |||||
// console.log(expiryDate); | |||||
if (expiryDate) clearErrors(); | if (expiryDate) clearErrors(); | ||||
if (productionDate) clearErrors(); | if (productionDate) clearErrors(); | ||||
}, [productionDate, expiryDate, clearErrors]); | }, [productionDate, expiryDate, clearErrors]); | ||||
@@ -58,7 +58,7 @@ | |||||
"putaway": "上架", | "putaway": "上架", | ||||
"delete": "刪除", | "delete": "刪除", | ||||
"Accept quantity must be greater than 0": "揀收數量不能少於1", | "Accept quantity must be greater than 0": "揀收數量不能少於1", | ||||
"QC items without result": "請完成品檢結果", | |||||
"QC items without result": "有未完成品檢項目", | |||||
"Failed items must have failed quantity": "請輸入不合格數量", | "Failed items must have failed quantity": "請輸入不合格數量", | ||||
"qty cannot be greater than remaining qty": "數量不能大於剩餘數量", | "qty cannot be greater than remaining qty": "數量不能大於剩餘數量", | ||||
"acceptQty must not greater than": "揀收數量不能大於", | "acceptQty must not greater than": "揀收數量不能大於", | ||||
@@ -70,8 +70,9 @@ | |||||
"determine2": "上報2", | "determine2": "上報2", | ||||
"determine3": "上報3", | "determine3": "上報3", | ||||
"receiving": "收貨中", | "receiving": "收貨中", | ||||
"received": "已檢收", | |||||
"received": "待上架", | |||||
"completed": "已上架", | "completed": "已上架", | ||||
"partially_completed": "已部分上架", | |||||
"rejected": "已拒絕", | "rejected": "已拒絕", | ||||
"escalated": "已上報", | "escalated": "已上報", | ||||
"status": "狀態", | "status": "狀態", | ||||
@@ -135,5 +136,7 @@ | |||||
"Found": "已找到", | "Found": "已找到", | ||||
"escalation processing": "處理上報記錄", | "escalation processing": "處理上報記錄", | ||||
"Printer": "列印機", | "Printer": "列印機", | ||||
"Printing": "列印中" | |||||
"Printing": "列印中", | |||||
"rejectQty": "拒絕數量", | |||||
"QC decision is required": "請決定品檢結果" | |||||
} | } |