|
- "use client";
-
- import {
- Box, Card, CardContent, Checkbox, Collapse, FormControl,
- FormControlLabel, Grid, Radio, RadioGroup, Stack, Tab,
- Tabs, TabsProps, TextField, Tooltip, Typography,
- } from "@mui/material";
- import { useFormContext, Controller, FieldPath } from "react-hook-form";
- import { useTranslation } from "react-i18next";
- import StyledDataGrid from "../StyledDataGrid";
- import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
- import {
- GridColDef,
- GridRowIdGetter,
- GridRowModel,
- useGridApiContext,
- GridRenderCellParams,
- GridRenderEditCellParams,
- useGridApiRef,
- GridRowSelectionModel,
- } from "@mui/x-data-grid";
- import InputDataGrid from "../InputDataGrid";
- import { TableRow } from "../InputDataGrid/InputDataGrid";
- import TwoLineCell from "../PoDetail/TwoLineCell";
- import QcSelect from "../PoDetail/QcSelect";
- import { GridEditInputCell } from "@mui/x-data-grid";
- import { ModalFormInput, StockInLine } from "@/app/api/stockIn";
- import { fetchQcCategory, fetchQcResult } from "@/app/api/qc/actions";
- import { QcCategory, QcData, QcInput, QcFormInput, QcResult } from "@/app/api/qc";
- import axios from "@/app/(main)/axios/axiosInstance";
- import { NEXT_PUBLIC_API_URL } from "@/config/api";
- import axiosInstance from "@/app/(main)/axios/axiosInstance";
- import EscalationComponent from "../PoDetail/EscalationComponent";
- import QcDataGrid from "../PoDetail/QCDatagrid";
- import { dummyEscalationHistory,
- dummyQcData_A1, dummyQcData_E1, dummyQcData_E2,
- dummyQcHeader_A1, dummyQcHeader_E1, dummyQcHeader_E2 } from "./dummyQcTemplate";
- import { escape, isNull, min, template } from "lodash";
- import { PanoramaSharp } from "@mui/icons-material";
- import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable";
- import { EscalationResult } from "@/app/api/escalation";
- import { EscalationCombo } from "@/app/api/user";
- import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions";
- import CollapsibleCard from "../CollapsibleCard/CollapsibleCard";
- import LoadingComponent from "../General/LoadingComponent";
- import QcForm from "./QcForm";
-
- interface Props {
- itemDetail: QcInput;
- // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] };
- // qc: QcItemWithChecks[];
- disabled: boolean;
- // qcItems: QcData[]
- // setQcItems: Dispatch<SetStateAction<QcData[]>>
- }
-
- type EntryError =
- | {
- [field in keyof QcData]?: string;
- }
- | undefined;
-
- type QcRow = TableRow<Partial<QcResult>, EntryError>;
- // fetchQcItemCheck
- const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {
- const { t } = useTranslation("purchaseOrder");
- const apiRef = useGridApiRef();
- const {
- register,
- formState: { errors, defaultValues, touchedFields },
- watch,
- control,
- setValue,
- getValues,
- reset,
- resetField,
- setError,
- clearErrors,
- } = useFormContext<QcFormInput>();
-
- const [tabIndex, setTabIndex] = useState(0);
- const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>();
- const [escalationHistory, setEscalationHistory] = useState(dummyEscalationHistory);
- const qcAccept = watch("qcAccept");
- const qcDecision = watch("qcDecision"); //WIP
- // const qcResult = useMemo(() => [...watch("qcResult")], [watch("qcResult")]);
-
- const [qcCategory, setQcCategory] = useState<QcCategory>();
-
- const qcRecord = useMemo(() => { // Need testing
- const value = watch('qcResult'); //console.log("%c QC update!", "color:green", value);
- return Array.isArray(value) ? [...value] : [];
- }, [watch('qcResult')]);
- const [qcHistory, setQcHistory] = useState<QcResult[]>([]);
- const [qcResult, setQcResult] = useState<QcResult[]>([]);
- const [escResult, setEscResult] = useState<EscalationResult[]>([]);
-
- // const [qcAccept, setQcAccept] = useState(true);
- // const [qcItems, setQcItems] = useState(dummyQCData)
-
- const qcDisabled = (row : QcResult) => {
- return disabled || isExist(row.escalationLogId);
- };
-
- const isExist = (data : string | number | undefined) => {
- return (data !== null && data !== undefined);
- }
-
- const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
- (_e, newValue) => {
- setTabIndex(newValue);
- },
- [],
- );
-
- const qcType = useMemo(() => {
- if (itemDetail) {
- const d = itemDetail;
- if (isExist(d.jobOrderId)) {
- return "EPQC";
- }
- }
- return "IQC"; // Default
- }, [itemDetail]);
-
- const detailMode = useMemo(() => {
- const isDetailMode = itemDetail.status == "escalated" || isExist(itemDetail.jobOrderId);
- return isDetailMode;
- }, [itemDetail]);
-
- // W I P //
- const validateFieldFail = (field : FieldPath<QcFormInput>, 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
- const accQty = watch("acceptQty");
- const validateForm = useCallback(() => {
- if (qcDecision == 1) {
- if (isNaN(accQty) || accQty === undefined || accQty === null || typeof(accQty) != "number") {
- setError("acceptQty", { message: t("value must be a number") });
- } else
- if (!Number.isInteger(accQty)) {
- setError("acceptQty", { message: t("value must be integer") });
- }
- if (accQty > itemDetail.acceptedQty) {
- setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${
- itemDetail.acceptedQty}` });
- } else
- if (accQty < 1) {
- setError("acceptQty", { message: t("minimal value is 1") });
- } else
- console.log("%c Validated accQty:", "color:yellow", accQty);
- }
-
- },[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(() => {
- clearErrors();
- validateForm();
- }, [clearErrors, validateForm]);
-
- /// validate datagrid
- const validation = useCallback(
- (newRow: GridRowModel<QcRow>): EntryError => {
- const error: EntryError = {};
- // const { qcItemId, failQty } = newRow;
- return Object.keys(error).length > 0 ? error : undefined;
- },
- [],
- );
-
-
- // Set initial value for acceptQty
- useEffect(() => {
- if (itemDetail?.demandQty > 0) { //!== undefined) {
- setValue("acceptQty", itemDetail.demandQty); // TODO: THIS NEED TO UPDATE TO NOT USE DEMAND QTY
- } else {
- setValue("acceptQty", itemDetail?.acceptedQty);
- }
- }, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]);
-
- // Fetch Qc Data
- useEffect(() => {
- // console.log("%c QC ItemDetail updated:", "color: gold", itemDetail);
- if (itemDetail) {
- const d = itemDetail;
- fetchNewQcData(d);
- if (d.status == "pending") {
- //
- } else {
- fetchQcResultData(d);
- }
- }
- }, [itemDetail]);
-
- const fetchNewQcData = useCallback(
- async (input: QcInput) => {
- try {
- const res = await fetchQcCategory(input.itemId, qcType);
- if (res.qcItems.length > 0) {
- console.log("%c Fetched Qc Template: ", "color:orange", res);
-
- setQcCategory(res);
- // setQcResult(res.qcItems);
- // setValue("qcResult", res.qcItems);
- } else throw("Result is undefined");
- } catch (e) {
- console.log("%c Error when fetching Qc Template: ", "color:red", e);
- alert(t("Missing QC Template, please contact administrator"));
- // closeHandler({}, "backdropClick");
- }
- },[fetchQcCategory, setValue]
- );
-
- const fetchQcResultData = useCallback(
- async (input: QcInput) => {
- try {
- const res = await fetchQcResult(input.id); // StockInLineId for now
- if (res.length > 0) {
- console.log("%c Fetched Qc Result: ", "color:orange", res);
- setValue("qcResult", res);
-
- fetchEscalationLogData(input.id);
- // } else {setStockInLineInfo((prev) => ({...prev, qcResult: []} as StockInLine));}
- } else throw("Result is undefined");
- } catch (e) {
- console.log("%c Error when fetching Qc Result: ", "color:red", e);
- // alert("Something went wrong, please retry");
- // closeHandler({}, "backdropClick");
- }
- },[fetchQcResult, setValue]
- );
-
- const fetchEscalationLogData = useCallback(
- async (stockInLineId: number) => {
- try {
- const res = await fetchEscalationLogsByStockInLines([stockInLineId]);
- if (res.length > 0) {
- console.log("%c Fetched Escalation Log: ", "color:orange", res[0]);
- setEscResult(res);
- // formProps.setValue("escalationLog", res[0]);
-
- }// else throw("Result is undefined");
- } catch (e) {
- console.log("%c Error when fetching EscalationLog: ", "color:red", e);
- // alert("Something went wrong, please retry");
- // closeHandler({}, "backdropClick");
- }
- },[fetchEscalationLogsByStockInLines]
- );
-
- // Set QC Data
- useEffect(() => {
- if (itemDetail) {
- const d = itemDetail;
- if (qcRecord.length < 1) { // No QC Data
- if (d.status == "pending") { // New QC
- if (qcCategory) {
- if (qcCategory.qcItems.length > 0) {
- const filledQcItems = fillQcResult(qcCategory.qcItems);
- setValue("qcResult", filledQcItems);
- console.log("%c New QC Record applied:", "color:green", filledQcItems);
- }
- }
- } else {
- console.log("%c No QC Record loaded:", "color:green");
- //
- }
- } else { // QC Result fetched
- if (qcRecord.some(qc => qc.order !== undefined)) { // If QC Result is filled with order
- if (d.status == "escalated") { // Copy the previous QC data for editing
-
- // If no editable Qc Data
- if (!qcRecord.some((qc) => !isExist(qc.escalationLogId))) {
- const mutableQcData = qcRecord.map(qc => ({ ...qc, escalationLogId: undefined }));
- const copiedQcData = [...mutableQcData, ...qcRecord];
- setValue("qcResult", copiedQcData);
- console.log("%c QC Record copied:", "color:green", copiedQcData);
- return;
- }
- }
-
- // Set QC Result
- // const filteredQcResult = qcRecord;
- const filteredQcResult = qcRecord.filter((qc) => !isExist(qc.escalationLogId));
- console.log("%c QC Result loaded:", "color:green", filteredQcResult);
- setQcResult(filteredQcResult);
-
- // Set QC History
- if (filteredQcResult.length < qcRecord.length) { // If there are Qc History
- if (qcHistory.length < 1) {
- const filteredQcHistory = qcRecord.filter((qc) => isExist(qc.escalationLogId));
- console.log("%c QC History loaded:", "color:green", filteredQcHistory);
- setQcHistory(filteredQcHistory);
- }
- }
-
- } else {
- if (qcCategory) {
- const filledQcData = fillQcResult(qcRecord, qcCategory?.qcItems);
- console.log("%c QC Result filled:", "color:green", filledQcData);
- setValue("qcResult", filledQcData);
- }
- }
- }
- }
- }, [qcRecord, qcCategory, setValue, itemDetail])
-
- const fillQcResult = (qcResults: QcResult[], qcItems: QcData[] = []) => {
- let result = [] as QcResult[];
- qcResults.forEach((r, index) => {
- const target = qcItems.find((t) => t.qcItemId === r.qcItemId);
- const n = { ...target, ...r }; //, id: index };
- result.push(n);
- });
- result.sort((a,b) => a.order! - b.order!);
- return result;
- };
-
- // const [openCollapse, setOpenCollapse] = useState(false)
- const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
-
- const onFailedOpenCollapse = useCallback((qcItems: QcResult[]) => {
- const isFailed = qcItems.some((qc) => !qc.qcPassed)
- // console.log(isFailed)
- if (isFailed) {
- setIsCollapsed(true)
- } else {
- setIsCollapsed(false)
- }
- }, [])
-
- // const handleRadioChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
- // const value = event.target.value === 'true';
- // setValue("qcAccept", value);
- // }, [setValue]);
-
-
- const setDefaultQcDecision = (status : string | undefined) => {
- const param = status?.toLowerCase();
- if (param !== undefined && param !== null) {
- if (param == "received" || param == "completed" || param == "partially_completed") {
- return 1;
- } else if (param == "rejected") {
- return 2;
- } else if (param == "escalated") {
- return 1; // For new flow
- // return 3;
- } else { return undefined; }
- } else {
- return undefined;
- }
- }
-
- // }, [watch("qcResult")]);
- // useEffect(() => {
- // // onFailedOpenCollapse(qcItems)
- // }, [qcItems]);
-
- const getRowId = (row :any) => {
- return qcRecord.findIndex(qc => qc == row);
- // return row.id || `${row.name}-${Math.random().toString(36).substr(2, 9)}`;
- };
-
- const getRowHeight = (row :any) => { // Not used?
- console.log("row", row);
- if (!row.model.name) {
- return (row.model.name.length ?? 10) * 1.2 + 30;
- } else { return 60}
- };
-
- const formattedDesc = (content: string = "") => {
- return (
- <>
- {content.split("\\n").map((line, index) => (
- <span key={index}> {line} <br/></span>
- ))}
- </>
- );
- }
-
- const QcHeader = useMemo(() => () => {
- if (qcCategory === undefined || qcCategory === null) {
- return (
- <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}>
- N/A
- </Typography>
- );
- } else
- return (
- <>
- <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
- <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}>
- {qcCategory?.name} ({qcCategory?.code})
- </Typography>
- <Typography variant="subtitle1" sx={{ color: '#666' }}>
- <b>品檢類型</b>:{qcType}
- </Typography>
- <Typography variant="subtitle2" sx={{ color: '#666' }}>
- {formattedDesc(qcCategory?.description)}
- </Typography>
- </Box>
- </>
- );
- }, [qcType, qcCategory]);
-
- return (
- <>
- <Grid container justifyContent="flex-start" alignItems="flex-start">
- {(qcRecord.length > 0) ? (
- // {(qcRecord.length > 0 && qcCategory) ? (
- <Grid
- container
- justifyContent="flex-start"
- alignItems="flex-start"
- spacing={2}
- sx={{ mt: 0.5 }}
- >
- <Grid item xs={12}>
- <Tabs
- value={tabIndex}
- onChange={handleTabChange}
- variant="scrollable"
- >
- <Tab label={t("QC Info")} iconPosition="end" />
- {(escResult && escResult?.length > 0) &&
- (<Tab label={t("Escalation History")} iconPosition="end" />)}
- </Tabs>
- </Grid>
- {tabIndex == 0 && (
- <>
- <Grid item xs={12}>
- <QcHeader/>
- {/* <QcDataGrid<ModalFormInput, QcData, EntryError>
- apiRef={apiRef}
- columns={qcColumns}
- _formKey="qcResult"
- validateRow={validation}
- /> */}
- <QcForm
- rows={qcResult}
- disabled={disabled}
- />
- </Grid>
- </>
- )}
- {tabIndex == 1 && (
- <>
- {/* <Grid item xs={12}>
- <StockInFormVer2
- itemDetail={itemDetail}
- disabled={false}
- />
- </Grid> */}
- {/* <Grid item xs={12}>
- <Typography variant="h6" display="block" marginBlockEnd={1}>
- {t("Escalation Info")}
- </Typography>
- </Grid> */}
- <Grid item xs={12}>
- <EscalationLogTable type="qc" items={escResult || []}/>
- <CollapsibleCard title={t("QC Record")}>
- <QcHeader/>
- <QcForm
- disabled={disabled}
- rows={qcHistory}
- />
- </CollapsibleCard>
- </Grid>
- </>
- )}
- <Grid item xs={12}>
- <Card sx={{p:2}}>
- <Typography variant="h6" display="block" marginBlockEnd={1}>
- {t("Qc Decision")}
- </Typography>
- <FormControl>
- <Controller
- name="qcDecision"
- // name="qcAccept"
- control={control}
- defaultValue={setDefaultQcDecision(itemDetail?.status)}
- // defaultValue={true}
- render={({ field }) => (
- <>
- {/* <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';
-
- const input = document.getElementById('accQty') as HTMLInputElement; //TODO improve
- // console.log("%c AccQty Error", "color:red", errors.acceptQty);
- if (input) { // Selected Reject in new flow with Error
- if (value == "1") { // Selected Accept
- input.value = Number(accQty).toString();
- } else {
- if (Boolean(errors.acceptQty)) {
- setValue("acceptQty", 0);
- }
- input.value = '0';
- }
- }
- // setValue("acceptQty", itemDetail.acceptedQty ?? 0);
- // clearErrors("acceptQty");
- // }
- field.onChange(value);
- }}
- >
- <FormControlLabel disabled={disabled}
- value="1" control={<Radio />} label="接受來貨" />
-
- {(detailMode || (disabled && accQty != itemDetail.acceptedQty && qcDecision == 1)) && ( //TODO Improve
- <Box sx={{mr:2}}>
- <TextField
- // type="number"
- id="accQty"
- label={t("acceptQty")}
- sx={{ width: '150px' }}
- // value={Number(accQty)}
- defaultValue={Number(accQty)}
- // defaultValue={(qcDecision == 1)? Number(accQty) : 0}
- // value={(qcDecision == 1)? Number(accQty) : undefined }
- // value={qcAccept? accQty : 0 }
- disabled={qcDecision != 1 || disabled}
- // disabled={!qcAccept || disabled}
- onBlur={(e) => {
- const value = e.target.value;
- const input = document.getElementById('accQty') as HTMLInputElement;
- input.value = Number(value).toString()
- setValue(`acceptQty`, Number(value));
- }}
- onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
- const input = e.target.value;
-
- const numReg = /^[0-9]+$/
- let r = '';
- if (!numReg.test(input)) {
- const result = input.replace(/\D/g, "");
- r = (result === '' ? result : Number(result)).toString();
- } else {
- r = Number(input).toString()
- }
- e.target.value = r;
- }}
- inputProps={{ min: 1, max:itemDetail.acceptedQty }}
- // onChange={(e) => {
- // const inputValue = e.target.value;
- // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) {
- // setValue("acceptQty", Number(inputValue === '' ? null : parseInt(inputValue, 10)));
- // }
- // }}
- // {...register("acceptQty", {
- // required: "acceptQty required!",
- // })}
- error={Boolean(errors.acceptQty)}
- helperText={errors.acceptQty?.message}
- />
- <TextField
- type="number"
- label={t("rejectQty")}
- sx={{ width: '150px' }}
- value={
- (!Boolean(errors.acceptQty) && qcDecision !== undefined) ?
- (qcDecision == 1 ? itemDetail.acceptedQty - accQty : itemDetail.acceptedQty)
- : ""
- }
- error={Boolean(errors.acceptQty)}
- disabled={true}
- />
- </Box>)}
-
- <FormControlLabel disabled={disabled}
- value="2" control={<Radio />}
- sx={{"& .Mui-checked": {color: "red"}}}
- label= {detailMode ? "全部拒絕並退貨" : "不接受並需要退貨"} />
-
- {(itemDetail.status == "pending" || disabled) && (<>
- <FormControlLabel disabled={disabled}
- value="3" control={<Radio />}
- sx={{"& .Mui-checked": {color: "blue"}}}
- label="暫時存放到置物區,並等待品檢結果" />
- </>)}
- </RadioGroup>
- </>
- )}
- />
- </FormControl>
- </Card>
- </Grid>
- {qcDecision == 3 && (
- // {!qcAccept && (
- <Grid item xs={12}>
- <EscalationComponent
- forSupervisor={false}
- isCollapsed={isCollapsed}
- setIsCollapsed={setIsCollapsed}
- />
- </Grid>)}
- </Grid>
- ) : <LoadingComponent/>}
- </Grid>
- </>
- );
- };
- export default QcComponent;
|