Browse Source

update new escalation flow

master
kelvinsuen 5 days ago
parent
commit
28a23973c0
8 changed files with 293 additions and 195 deletions
  1. +4
    -5
      src/app/api/po/actions.ts
  2. +7
    -0
      src/app/api/po/index.ts
  3. +3
    -2
      src/components/PoDetail/PoInputGrid.tsx
  4. +2
    -1
      src/components/PoDetail/PutAwayForm.tsx
  5. +209
    -145
      src/components/PoDetail/QcComponent.tsx
  6. +50
    -36
      src/components/PoDetail/QcStockInModalVer2.tsx
  7. +12
    -3
      src/components/PoDetail/StockInFormVer2.tsx
  8. +6
    -3
      src/i18n/zh/purchaseOrder.json

+ 4
- 5
src/app/api/po/actions.ts View File

@@ -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[];


+ 7
- 0
src/app/api/po/index.ts View File

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


+ 3
- 2
src/components/PoDetail/PoInputGrid.tsx View File

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


+ 2
- 1
src/components/PoDetail/PutAwayForm.tsx View File

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


+ 209
- 145
src/components/PoDetail/QcComponent.tsx View File

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


+ 50
- 36
src/components/PoDetail/QcStockInModalVer2.tsx View File

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


+ 12
- 3
src/components/PoDetail/StockInFormVer2.tsx View File

@@ -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]);


+ 6
- 3
src/i18n/zh/purchaseOrder.json View File

@@ -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": "請決定品檢結果"
} }

Loading…
Cancel
Save