| @@ -1,71 +1,395 @@ | |||
| "use client" | |||
| import { MutableRefObject } from "react"; | |||
| import StyledDataGrid from "../StyledDataGrid" | |||
| import { GridApiCommunity } from "@mui/x-data-grid/internals"; | |||
| import { GridColDef } from "@mui/x-data-grid"; | |||
| "use client"; | |||
| import { | |||
| Dispatch, | |||
| MutableRefObject, | |||
| SetStateAction, | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| useState, | |||
| } from "react"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { | |||
| FooterPropsOverrides, | |||
| GridActionsCellItem, | |||
| GridCellParams, | |||
| GridColDef, | |||
| GridEventListener, | |||
| GridRowEditStopReasons, | |||
| GridRowId, | |||
| GridRowIdGetter, | |||
| GridRowModel, | |||
| GridRowModes, | |||
| GridRowModesModel, | |||
| GridRowSelectionModel, | |||
| GridToolbarContainer, | |||
| GridValidRowModel, | |||
| useGridApiRef, | |||
| } from "@mui/x-data-grid"; | |||
| import { set, useFormContext } from "react-hook-form"; | |||
| import SaveIcon from "@mui/icons-material/Save"; | |||
| import DeleteIcon from "@mui/icons-material/Delete"; | |||
| import CancelIcon from "@mui/icons-material/Cancel"; | |||
| import { Add } from "@mui/icons-material"; | |||
| import { Box, Button, Typography } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { dummyQCData, QcData } from "./dummyQcTemplate"; | |||
| import { Checkbox } from "@mui/material"; | |||
| import { | |||
| GridApiCommunity, | |||
| GridSlotsComponentsProps, | |||
| } from "@mui/x-data-grid/internals"; | |||
| import { dummyQCData } from "./dummyQcTemplate"; | |||
| // T == CreatexxxInputs map of the form's fields | |||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | |||
| // E == error | |||
| interface ResultWithId { | |||
| id: string | number; | |||
| } | |||
| // export type InputGridProps = { | |||
| // [key: string]: any | |||
| // } | |||
| interface DefaultResult<E> { | |||
| _isNew: boolean; | |||
| _error: E; | |||
| } | |||
| interface Props { | |||
| // apiRef: MutableRefObject<GridApiCommunity>; | |||
| }; | |||
| interface SelectionResult<E> { | |||
| active: boolean; | |||
| _isNew: boolean; | |||
| _error: E; | |||
| } | |||
| type Result<E> = DefaultResult<E> | SelectionResult<E>; | |||
| export type TableRow<V, E> = Partial< | |||
| V & { | |||
| isActive: boolean | undefined; | |||
| _isNew: boolean; | |||
| _error: E; | |||
| } & ResultWithId | |||
| >; | |||
| export interface InputDataGridProps<T, V, E> { | |||
| apiRef: MutableRefObject<GridApiCommunity>; | |||
| // checkboxSelection: false | undefined; | |||
| _formKey: keyof T; | |||
| columns: GridColDef[]; | |||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | |||
| needAdd?: boolean; | |||
| } | |||
| export interface SelectionInputDataGridProps<T, V, E> { | |||
| // thinking how do | |||
| apiRef: MutableRefObject<GridApiCommunity>; | |||
| // checkboxSelection: true; | |||
| _formKey: keyof T; | |||
| columns: GridColDef[]; | |||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | |||
| } | |||
| export type Props<T, V, E> = | |||
| | InputDataGridProps<T, V, E> | |||
| | SelectionInputDataGridProps<T, V, E>; | |||
| export class ProcessRowUpdateError<T, E> extends Error { | |||
| public readonly row: T; | |||
| public readonly errors: E | undefined; | |||
| constructor(row: T, message?: string, errors?: E) { | |||
| super(message); | |||
| this.row = row; | |||
| this.errors = errors; | |||
| Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); | |||
| } | |||
| } | |||
| // T == CreatexxxInputs map of the form's fields | |||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | |||
| // E == error | |||
| function InputDataGrid<T, V, E>({ | |||
| apiRef, | |||
| // checkboxSelection = false, | |||
| _formKey, | |||
| columns, | |||
| validateRow, | |||
| }: Props<T, V, E>) { | |||
| const { | |||
| t, | |||
| // i18n: { language }, | |||
| } = useTranslation("purchaseOrder"); | |||
| const formKey = _formKey.toString(); | |||
| const { setValue, getValues } = useFormContext(); | |||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
| // const apiRef = useGridApiRef(); | |||
| const getRowId = useCallback<GridRowIdGetter<TableRow<V, E>>>( | |||
| (row) => row.id! as number, | |||
| [], | |||
| ); | |||
| const formValue = getValues(formKey) | |||
| const list: TableRow<V, E>[] = !formValue || formValue.length == 0 ? dummyQCData : getValues(formKey); | |||
| console.log(list) | |||
| const [rows, setRows] = useState<TableRow<V, E>[]>(() => { | |||
| // const list: TableRow<V, E>[] = getValues(formKey); | |||
| console.log(list) | |||
| return list && list.length > 0 ? list : []; | |||
| }); | |||
| console.log(rows) | |||
| // const originalRows = list && list.length > 0 ? list : []; | |||
| const originalRows = useMemo(() => ( | |||
| list && list.length > 0 ? list : [] | |||
| ), [list]) | |||
| // const originalRowModel = originalRows.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel | |||
| const [rowSelectionModel, setRowSelectionModel] = | |||
| useState<GridRowSelectionModel>(() => { | |||
| // const rowModel = list.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel | |||
| const rowModel: GridRowSelectionModel = getValues( | |||
| `${formKey}_active`, | |||
| ) as GridRowSelectionModel; | |||
| console.log(rowModel); | |||
| return rowModel; | |||
| }); | |||
| useEffect(() => { | |||
| for (let i = 0; i < rows.length; i++) { | |||
| const currRow = rows[i] | |||
| setRowModesModel((prevRowModesModel) => ({ | |||
| ...prevRowModesModel, | |||
| [currRow.id as number]: { mode: GridRowModes.View }, | |||
| })); | |||
| } | |||
| }, [rows]) | |||
| const handleSave = useCallback( | |||
| (id: GridRowId) => () => { | |||
| setRowModesModel((prevRowModesModel) => ({ | |||
| ...prevRowModesModel, | |||
| [id]: { mode: GridRowModes.View }, | |||
| })); | |||
| }, | |||
| [], | |||
| ); | |||
| const onProcessRowUpdateError = useCallback( | |||
| (updateError: ProcessRowUpdateError<T, E>) => { | |||
| const errors = updateError.errors; | |||
| const row = updateError.row; | |||
| console.log(errors); | |||
| apiRef.current.updateRows([{ ...row, _error: errors }]); | |||
| }, | |||
| [apiRef], | |||
| ); | |||
| const processRowUpdate = useCallback( | |||
| ( | |||
| newRow: GridRowModel<TableRow<V, E>>, | |||
| originalRow: GridRowModel<TableRow<V, E>>, | |||
| ) => { | |||
| ///////////////// | |||
| // validation here | |||
| const errors = validateRow(newRow); | |||
| console.log(newRow); | |||
| if (errors) { | |||
| throw new ProcessRowUpdateError( | |||
| originalRow, | |||
| "validation error", | |||
| errors, | |||
| ); | |||
| } | |||
| ///////////////// | |||
| const { _isNew, _error, ...updatedRow } = newRow; | |||
| const rowToSave = { | |||
| ...updatedRow, | |||
| } as TableRow<V, E>; /// test | |||
| console.log(rowToSave); | |||
| setRows((rw) => | |||
| rw.map((r) => (getRowId(r) === getRowId(originalRow) ? rowToSave : r)), | |||
| ); | |||
| return rowToSave; | |||
| }, | |||
| [validateRow, getRowId], | |||
| ); | |||
| const addRow = useCallback(() => { | |||
| const newEntry = { id: Date.now(), _isNew: true } as TableRow<V, E>; | |||
| setRows((prev) => [...prev, newEntry]); | |||
| setRowModesModel((model) => ({ | |||
| ...model, | |||
| [getRowId(newEntry)]: { | |||
| mode: GridRowModes.Edit, | |||
| // fieldToFocus: "team", /// test | |||
| }, | |||
| })); | |||
| }, [getRowId]); | |||
| const reset = useCallback(() => { | |||
| setRowModesModel({}); | |||
| setRows(originalRows); | |||
| }, [originalRows]); | |||
| const QcDataGrid: React.FC<Props> = ({ | |||
| // apiRef | |||
| }) => { | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const columns: GridColDef[] = [ | |||
| { | |||
| field: "qcItem", | |||
| headerName: t("qcItem"), | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "isPassed", | |||
| headerName: t("passed"), | |||
| flex: 1, | |||
| renderCell: (params) => ( | |||
| <Checkbox | |||
| checked={params.value} | |||
| // onChange={() => handleCheckboxChange(params.id)} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| field: "isFailed", | |||
| headerName: t("failed"), | |||
| flex: 1, | |||
| renderCell: (params) => ( | |||
| <Checkbox | |||
| checked={params.value} | |||
| // onChange={() => handleCheckboxChange(params.id)} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| field: "failedQty", | |||
| headerName: t("failedQty"), | |||
| flex: 1, | |||
| editable: true, | |||
| }, | |||
| { | |||
| field: "remarks", | |||
| headerName: t("remarks"), | |||
| flex: 1, | |||
| editable: true, | |||
| }, | |||
| ] | |||
| return ( | |||
| <StyledDataGrid | |||
| // apiRef={apiRef} | |||
| autoHeight | |||
| editMode="row" | |||
| rows={dummyQCData} | |||
| columns={columns} | |||
| /> | |||
| ) | |||
| const handleCancel = useCallback( | |||
| (id: GridRowId) => () => { | |||
| setRowModesModel((model) => ({ | |||
| ...model, | |||
| [id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||
| })); | |||
| const editedRow = rows.find((row) => getRowId(row) === id); | |||
| if (editedRow?._isNew) { | |||
| setRows((rw) => rw.filter((r) => getRowId(r) !== id)); | |||
| } else { | |||
| setRows((rw) => | |||
| rw.map((r) => (getRowId(r) === id ? { ...r, _error: undefined } : r)), | |||
| ); | |||
| } | |||
| }, | |||
| [rows, getRowId], | |||
| ); | |||
| const handleDelete = useCallback( | |||
| (id: GridRowId) => () => { | |||
| setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id)); | |||
| }, | |||
| [getRowId], | |||
| ); | |||
| const _columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| ...columns, | |||
| { | |||
| field: "actions", | |||
| type: "actions", | |||
| headerName: "", | |||
| flex: 0.5, | |||
| cellClassName: "actions", | |||
| getActions: ({ id }: { id: GridRowId }) => { | |||
| const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
| if (isInEditMode) { | |||
| return [ | |||
| <GridActionsCellItem | |||
| icon={<SaveIcon />} | |||
| label="Save" | |||
| key="edit" | |||
| sx={{ | |||
| color: "primary.main", | |||
| }} | |||
| onClick={handleSave(id)} | |||
| />, | |||
| <GridActionsCellItem | |||
| icon={<CancelIcon />} | |||
| label="Cancel" | |||
| key="edit" | |||
| onClick={handleCancel(id)} | |||
| />, | |||
| ]; | |||
| } | |||
| return [ | |||
| <GridActionsCellItem | |||
| icon={<DeleteIcon />} | |||
| label="Delete" | |||
| sx={{ | |||
| color: "error.main", | |||
| }} | |||
| onClick={handleDelete(id)} | |||
| color="inherit" | |||
| key="edit" | |||
| />, | |||
| ]; | |||
| }, | |||
| }, | |||
| ], | |||
| [columns, rowModesModel, handleSave, handleCancel, handleDelete], | |||
| ); | |||
| // sync useForm | |||
| useEffect(() => { | |||
| // console.log(formKey) | |||
| // console.log(rows) | |||
| setValue(formKey, rows); | |||
| }, [formKey, rows, setValue]); | |||
| const footer = ( | |||
| <Box display="flex" gap={2} alignItems="center"> | |||
| <Button | |||
| disableRipple | |||
| variant="outlined" | |||
| startIcon={<Add />} | |||
| onClick={addRow} | |||
| size="small" | |||
| > | |||
| 新增 | |||
| {/* {t("Add Record")} */} | |||
| </Button> | |||
| <Button | |||
| disableRipple | |||
| variant="outlined" | |||
| startIcon={<Add />} | |||
| onClick={reset} | |||
| size="small" | |||
| > | |||
| {/* {t("Clean Record")} */} | |||
| 清除 | |||
| </Button> | |||
| </Box> | |||
| ); | |||
| // const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { | |||
| // if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
| // event.defaultMuiPrevented = true; | |||
| // } | |||
| // }; | |||
| return ( | |||
| <StyledDataGrid | |||
| // {...props} | |||
| // getRowId={getRowId as GridRowIdGetter<GridValidRowModel>} | |||
| rowSelectionModel={rowSelectionModel} | |||
| apiRef={apiRef} | |||
| rows={rows} | |||
| columns={columns} | |||
| editMode="row" | |||
| autoHeight | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
| border: "1px solid", | |||
| borderColor: "error.main", | |||
| }, | |||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
| border: "1px solid", | |||
| borderColor: "warning.main", | |||
| }, | |||
| }} | |||
| disableColumnMenu | |||
| processRowUpdate={processRowUpdate as any} | |||
| // onRowEditStop={handleRowEditStop} | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={setRowModesModel} | |||
| onProcessRowUpdateError={onProcessRowUpdateError} | |||
| getCellClassName={(params: GridCellParams<TableRow<T, E>>) => { | |||
| let classname = ""; | |||
| if (params.row._error) { | |||
| classname = "hasError"; | |||
| } | |||
| return classname; | |||
| }} | |||
| slots={{ | |||
| // footer: FooterToolbar, | |||
| noRowsOverlay: NoRowsOverlay, | |||
| }} | |||
| // slotProps={{ | |||
| // footer: { child: footer }, | |||
| // } | |||
| // } | |||
| /> | |||
| ); | |||
| } | |||
| export default QcDataGrid | |||
| const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
| return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
| }; | |||
| const NoRowsOverlay: React.FC = () => { | |||
| const { t } = useTranslation("home"); | |||
| return ( | |||
| <Box | |||
| display="flex" | |||
| justifyContent="center" | |||
| alignItems="center" | |||
| height="100%" | |||
| > | |||
| <Typography variant="caption">{t("Add some entries!")}</Typography> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default InputDataGrid; | |||
| @@ -5,6 +5,7 @@ import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| Checkbox, | |||
| Grid, | |||
| Stack, | |||
| Tab, | |||
| @@ -17,7 +18,7 @@ import { | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { | |||
| GridColDef, | |||
| GridRowIdGetter, | |||
| @@ -43,22 +44,25 @@ import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||
| import EscalationComponent from "./EscalationComponent"; | |||
| import QcDataGrid from "./QCDatagrid"; | |||
| import StockInFormVer2 from "./StockInFormVer2"; | |||
| import { dummyEscalationHistory } from "./dummyQcTemplate"; | |||
| import { dummyEscalationHistory, dummyQCData, QcData } from "./dummyQcTemplate"; | |||
| import { ModalFormInput } from "@/app/api/dashboard/actions"; | |||
| interface Props { | |||
| itemDetail: StockInLine; | |||
| qc: QcItemWithChecks[]; | |||
| disabled: boolean; | |||
| qcItems: QcData[] | |||
| setQcItems: Dispatch<SetStateAction<QcData[]>> | |||
| } | |||
| type EntryError = | |||
| | { | |||
| [field in keyof PurchaseQcResult]?: string; | |||
| [field in keyof QcData]?: string; | |||
| } | |||
| | undefined; | |||
| type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | |||
| type QcRow = TableRow<Partial<QcData>, EntryError>; | |||
| // fetchQcItemCheck | |||
| const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcItems }) => { | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| @@ -76,6 +80,8 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>() | |||
| const [qcResult, setQcResult] = useState(dummyEscalationHistory) | |||
| // const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const column = useMemo<GridColDef[]>( | |||
| () => [ | |||
| @@ -137,18 +143,18 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| // flex: 1, | |||
| // editable: !disabled, | |||
| // valueFormatter(params) { | |||
| // const row = params.id ? params.api.getRow<PoQcRow>(params.id) : null; | |||
| // const row = params.id ? params.api.getRow<QcRow>(params.id) : null; | |||
| // if (!row) { | |||
| // return null; | |||
| // } | |||
| // const Qc = qc.find((q) => q.id === row.qcItemId); | |||
| // return Qc ? `${Qc.code} - ${Qc.name}` : t("Please select QC"); | |||
| // }, | |||
| // renderCell(params: GridRenderCellParams<PoQcRow, number>) { | |||
| // renderCell(params: GridRenderCellParams<QcRow, number>) { | |||
| // console.log(params.value); | |||
| // return <TwoLineCell>{params.formattedValue}</TwoLineCell>; | |||
| // }, | |||
| // renderEditCell(params: GridRenderEditCellParams<PoQcRow, number>) { | |||
| // renderEditCell(params: GridRenderEditCellParams<QcRow, number>) { | |||
| // const errorMessage = | |||
| // params.row._error?.[params.field as keyof PurchaseQcResult]; | |||
| // console.log(errorMessage); | |||
| @@ -185,7 +191,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| // flex: 1, | |||
| // editable: !disabled, | |||
| // type: "number", | |||
| // renderEditCell(params: GridRenderEditCellParams<PoQcRow>) { | |||
| // renderEditCell(params: GridRenderEditCellParams<QcRow>) { | |||
| // // const recordQty = params.row.qty | |||
| // // if (recordQty !== undefined) { | |||
| // // setUnrecordQty((prev) => prev - recordQty) | |||
| @@ -217,23 +223,124 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| ); | |||
| /// validate datagrid | |||
| const validation = useCallback( | |||
| (newRow: GridRowModel<PoQcRow>): EntryError => { | |||
| (newRow: GridRowModel<QcRow>): EntryError => { | |||
| const error: EntryError = {}; | |||
| const { qcItemId, failQty } = newRow; | |||
| if (!qcItemId || qcItemId <= 0) { | |||
| error["qcItemId"] = t("select qc"); | |||
| } | |||
| if (!failQty || failQty <= 0) { | |||
| error["failQty"] = t("enter a failQty"); | |||
| } | |||
| if (failQty && failQty > itemDetail.acceptedQty) { | |||
| error["failQty"] = t("qty too big"); | |||
| } | |||
| // const { qcItemId, failQty } = newRow; | |||
| return Object.keys(error).length > 0 ? error : undefined; | |||
| }, | |||
| [], | |||
| ); | |||
| function BooleanEditCell(params: GridRenderEditCellParams) { | |||
| const apiRef = useGridApiContext(); | |||
| const { id, field, value } = params; | |||
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||
| apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); | |||
| apiRef.current.stopCellEditMode({ id, field }); // commit immediately | |||
| }; | |||
| return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | |||
| } | |||
| const qcColumns: GridColDef[] = [ | |||
| { | |||
| field: "qcItem", | |||
| headerName: t("qcItem"), | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: 'isPassed', | |||
| headerName: t("passed"), | |||
| flex: 1, | |||
| renderCell: (params) => ( | |||
| <Checkbox | |||
| checked={!!params.value} | |||
| onClick={(e) => e.stopPropagation()} // avoid row selection | |||
| onMouseDown={(e) => e.stopPropagation()} // extra guard | |||
| onChange={(e) => { | |||
| const checked = e.target.checked; | |||
| setQcItems((prev) => | |||
| prev.map((r) => (r.id === params.id ? { ...r, isPassed: checked } : r)) | |||
| ); | |||
| }} | |||
| size="small" | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| field: "isFailed", | |||
| headerName: t("failed"), | |||
| flex: 1, | |||
| editable: true, | |||
| type: "boolean", | |||
| renderCell: (params) => ( | |||
| <Checkbox | |||
| checked={!!params.value} | |||
| onClick={(e) => e.stopPropagation()} // avoid row selection | |||
| onMouseDown={(e) => e.stopPropagation()} // extra guard | |||
| onChange={(e) => { | |||
| const checked = e.target.checked; | |||
| setQcItems((prev) => | |||
| prev.map((r) => (r.id === params.id ? { ...r, isFailed: checked } : r)) | |||
| ); | |||
| }} | |||
| size="small" | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| field: "failedQty", | |||
| headerName: t("failedQty"), | |||
| flex: 1, | |||
| // editable: true, | |||
| renderCell: (params) => ( | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={params.value ?? ''} | |||
| 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, failedQty: next } : r)) | |||
| ); | |||
| }} | |||
| onClick={(e) => e.stopPropagation()} | |||
| onMouseDown={(e) => e.stopPropagation()} | |||
| onKeyDown={(e) => e.stopPropagation()} | |||
| inputProps={{ min: 0 }} | |||
| sx={{ width: '100%' }} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| field: "remarks", | |||
| headerName: t("remarks"), | |||
| flex: 1, | |||
| renderCell: (params) => ( | |||
| <TextField | |||
| size="small" | |||
| value={params.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)) | |||
| ); | |||
| }} | |||
| onClick={(e) => e.stopPropagation()} | |||
| onMouseDown={(e) => e.stopPropagation()} | |||
| onKeyDown={(e) => e.stopPropagation()} | |||
| inputProps={{ min: 0 }} | |||
| sx={{ width: '100%' }} | |||
| /> | |||
| ), | |||
| }, | |||
| ] | |||
| useEffect(() => { | |||
| console.log(itemDetail); | |||
| const status = "receiving"; | |||
| @@ -268,10 +375,21 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| {tabIndex == 0 && ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| <QcDataGrid/> | |||
| {/* <QcDataGrid<ModalFormInput, QcData, EntryError> | |||
| apiRef={apiRef} | |||
| columns={qcColumns} | |||
| _formKey="qcResult" | |||
| validateRow={validation} | |||
| /> */} | |||
| <StyledDataGrid | |||
| columns={qcColumns} | |||
| rows={qcItems} | |||
| autoHeight | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <TextField | |||
| type="number" | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| /> | |||
| @@ -296,7 +414,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <StyledDataGrid | |||
| rows={dummyEscalationHistory} | |||
| rows={qcResult} | |||
| columns={columns} | |||
| onRowSelectionModelChange={(newRowSelectionModel) => { | |||
| setRowSelectionModel(newRowSelectionModel); | |||
| @@ -1,9 +1,16 @@ | |||
| "use client"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { ModalFormInput, PurchaseQcResult } from "@/app/api/po/actions"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { Box, Button, Grid, Modal, ModalProps, Stack, Typography } from "@mui/material"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| Grid, | |||
| Modal, | |||
| ModalProps, | |||
| Stack, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { Dispatch, SetStateAction, useCallback, useState } from "react"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { StockInLineRow } from "./PoInputGrid"; | |||
| @@ -12,8 +19,8 @@ import StockInForm from "./StockInForm"; | |||
| import StockInFormVer2 from "./StockInFormVer2"; | |||
| import QcFormVer2 from "./QcFormVer2"; | |||
| import PutawayForm from "./PutawayForm"; | |||
| import { dummyPutawayLine } from "./dummyQcTemplate"; | |||
| import { dummyPutawayLine, dummyQCData } from "./dummyQcTemplate"; | |||
| import { useGridApiRef } from "@mui/x-data-grid"; | |||
| const style = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| @@ -25,9 +32,8 @@ const style = { | |||
| pb: 10, | |||
| display: "block", | |||
| width: { xs: "60%", sm: "60%", md: "60%" }, | |||
| // height: { xs: "60%", sm: "60%", md: "60%" }, | |||
| // height: { xs: "60%", sm: "60%", md: "60%" }, | |||
| }; | |||
| interface CommonProps extends Omit<ModalProps, "children"> { | |||
| // setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>; | |||
| setEntries?: Dispatch<SetStateAction<StockInLineRow[]>>; | |||
| @@ -43,12 +49,10 @@ interface CommonProps extends Omit<ModalProps, "children"> { | |||
| >; | |||
| qc?: QcItemWithChecks[]; | |||
| warehouse?: any[]; | |||
| // type: "qc" | "stockIn" | "escalation" | "putaway" | "reject"; | |||
| // type: "qc" | "stockIn" | "escalation" | "putaway" | "reject"; | |||
| } | |||
| interface Props extends CommonProps{ | |||
| interface Props extends CommonProps { | |||
| itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] }; | |||
| } | |||
| const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
| // type, | |||
| @@ -62,165 +66,243 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
| qc, | |||
| warehouse, | |||
| }) => { | |||
| console.log(warehouse) | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("purchaseOrder"); | |||
| console.log(warehouse); | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("purchaseOrder"); | |||
| const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const formProps = useForm<ModalFormInput>({ | |||
| defaultValues: { | |||
| ...itemDetail, | |||
| putawayLine: dummyPutawayLine | |||
| putawayLine: dummyPutawayLine, | |||
| // receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT), | |||
| // warehouseId: itemDetail.defaultWarehouseId || 0 | |||
| }, | |||
| }); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| onClose?.(...args); | |||
| // reset(); | |||
| }, | |||
| [onClose], | |||
| ); | |||
| const [openPutaway, setOpenPutaway] = useState(false) | |||
| (...args) => { | |||
| onClose?.(...args); | |||
| // reset(); | |||
| }, | |||
| [onClose], | |||
| ); | |||
| const [openPutaway, setOpenPutaway] = useState(false); | |||
| const onOpenPutaway = useCallback(() => { | |||
| setOpenPutaway(true); | |||
| }, []); | |||
| const onClosePutaway = useCallback(() => { | |||
| setOpenPutaway(false); | |||
| }, []); | |||
| const [submissionType, setSubmissionType] = useState<"stockIn" | "qc" | "escalate" | undefined>(undefined) | |||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput>>( | |||
| // Stock In submission handler | |||
| const onSubmitStockIn = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log(event!.nativeEvent) | |||
| // closeHandler({}, "backdropClick"); | |||
| // for now go to putaway form | |||
| onOpenPutaway() | |||
| // divide 3 section for this submition | |||
| // switch (submissionType) { | |||
| // submit stock in data | |||
| // submit qc data | |||
| // submit putaway | |||
| // } | |||
| console.log("Stock In Submission:", event!.nativeEvent); | |||
| // Extract only stock-in related fields | |||
| const stockInData = { | |||
| // quantity: data.quantity, | |||
| // receiptDate: data.receiptDate, | |||
| // batchNumber: data.batchNumber, | |||
| // expiryDate: data.expiryDate, | |||
| // warehouseId: data.warehouseId, | |||
| // location: data.location, | |||
| // unitCost: data.unitCost, | |||
| data: data, | |||
| // Add other stock-in specific fields from your form | |||
| }; | |||
| console.log("Stock In Data:", stockInData); | |||
| // Handle stock-in submission logic here | |||
| // e.g., call API, update state, etc. | |||
| }, | |||
| [], | |||
| ); | |||
| // QC submission handler | |||
| const onSubmitQc = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log("QC Submission:", event!.nativeEvent); | |||
| // Extract only QC related fields | |||
| const qcData = { | |||
| // qcStatus: data.qcStatus, | |||
| // qcComments: data.qcComments, | |||
| // qcResult: data.qcResult, | |||
| // approvedBy: data.approvedBy, | |||
| // qualityGrade: data.qualityGrade, | |||
| // defectNotes: data.defectNotes, | |||
| data: data, | |||
| // Add other QC specific fields from your form | |||
| }; | |||
| console.log(qcItems) | |||
| console.log("QC Data:", qcData); | |||
| // Handle QC submission logic here | |||
| // After QC approval, open putaway form | |||
| // onOpenPutaway(); | |||
| }, | |||
| [onOpenPutaway, qcItems], | |||
| ); | |||
| // Email supplier handler | |||
| const onSubmitEmailSupplier = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log("Email Supplier Submission:", event!.nativeEvent); | |||
| // Extract only email supplier related fields | |||
| const emailData = { | |||
| // supplierEmail: data.supplierEmail, | |||
| // issueDescription: data.issueDescription, | |||
| // qcComments: data.qcComments, | |||
| // defectNotes: data.defectNotes, | |||
| // attachments: data.attachments, | |||
| // escalationReason: data.escalationReason, | |||
| data: data, | |||
| }, [submissionType]) | |||
| // Add other email-specific fields | |||
| }; | |||
| console.log("Email Supplier Data:", emailData); | |||
| // Handle email supplier logic here | |||
| // e.g., send email to supplier, log escalation, etc. | |||
| }, | |||
| [], | |||
| ); | |||
| // Putaway submission handler | |||
| const onSubmitPutaway = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log("Putaway Submission:", event!.nativeEvent); | |||
| // Extract only putaway related fields | |||
| const putawayData = { | |||
| // putawayLine: data.putawayLine, | |||
| // putawayLocation: data.putawayLocation, | |||
| // binLocation: data.binLocation, | |||
| // putawayQuantity: data.putawayQuantity, | |||
| // putawayNotes: data.putawayNotes, | |||
| data: data, | |||
| return ( | |||
| <> | |||
| {/* {itemDetail !== undefined && ( | |||
| <PutawayForm | |||
| itemDetail={itemDetail} | |||
| warehouse={warehouse!} | |||
| disabled={false} | |||
| /> | |||
| )} */} | |||
| <FormProvider {...formProps}> | |||
| <Modal open={open} onClose={closeHandler}> | |||
| <Box | |||
| sx={{ | |||
| ...style, | |||
| padding: 2, // Add padding to the Box | |||
| maxHeight: '90vh', // Limit the height of the modal | |||
| overflowY: 'auto', // Enable scrolling if content overflows | |||
| marginLeft: 3, | |||
| marginRight: 3, | |||
| }} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||
| > | |||
| {openPutaway ? ( | |||
| <> | |||
| <PutawayForm | |||
| itemDetail={itemDetail} | |||
| warehouse={warehouse!} | |||
| disabled={false} | |||
| /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| id="qc" | |||
| type="button" | |||
| variant="contained" | |||
| color="secondary" | |||
| sx={{ mt: 1 }} | |||
| > | |||
| {t("print")} | |||
| </Button> | |||
| <Button | |||
| id="qc" | |||
| type="submit" | |||
| variant="contained" | |||
| color="secondary" | |||
| sx={{ mt: 1 }} | |||
| > | |||
| {t("confirm putaway")} | |||
| </Button> | |||
| </Stack> | |||
| </> | |||
| // Add other putaway specific fields | |||
| }; | |||
| console.log("Putaway Data:", putawayData); | |||
| // Handle putaway submission logic here | |||
| // Close modal after successful putaway | |||
| closeHandler({}, "backdropClick"); | |||
| }, | |||
| [closeHandler], | |||
| ); | |||
| // Print handler | |||
| const onPrint = useCallback(() => { | |||
| console.log("Print putaway documents"); | |||
| // Handle print logic here | |||
| window.print(); | |||
| }, []); | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Modal open={open} onClose={closeHandler}> | |||
| <Box | |||
| sx={{ | |||
| ...style, | |||
| padding: 2, | |||
| maxHeight: "90vh", | |||
| overflowY: "auto", | |||
| marginLeft: 3, | |||
| marginRight: 3, | |||
| }} | |||
| > | |||
| {openPutaway ? ( | |||
| <Box | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmitPutaway)} | |||
| > | |||
| <PutawayForm | |||
| itemDetail={itemDetail} | |||
| warehouse={warehouse!} | |||
| disabled={false} | |||
| /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| id="printButton" | |||
| type="button" | |||
| variant="contained" | |||
| color="secondary" | |||
| sx={{ mt: 1 }} | |||
| onClick={onPrint} | |||
| > | |||
| {t("print")} | |||
| </Button> | |||
| <Button | |||
| id="putawaySubmit" | |||
| type="submit" | |||
| variant="contained" | |||
| color="secondary" | |||
| sx={{ mt: 1 }} | |||
| > | |||
| {t("confirm putaway")} | |||
| </Button> | |||
| </Stack> | |||
| </Box> | |||
| ) : ( | |||
| <> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("qc processing")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <StockInFormVer2 | |||
| itemDetail={itemDetail} | |||
| disabled={false} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| id="stockIn" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| > | |||
| {t("submitStockIn")} | |||
| </Button> | |||
| </Stack> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <QcFormVer2 | |||
| qc={qc!} | |||
| itemDetail={itemDetail} | |||
| disabled={false} | |||
| /> | |||
| </Grid> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| id="qc" | |||
| type="button" | |||
| variant="contained" | |||
| color="secondary" | |||
| sx={{ mt: 1 }} | |||
| > | |||
| {t("email supplier")} | |||
| </Button> | |||
| <Button | |||
| id="qc" | |||
| type="submit" | |||
| variant="contained" | |||
| color="secondary" | |||
| sx={{ mt: 1 }} | |||
| > | |||
| {t("confirm putaway")} | |||
| </Button> | |||
| </Stack> | |||
| </> | |||
| ) | |||
| } | |||
| </Box> | |||
| </Modal> | |||
| </FormProvider> | |||
| </> | |||
| ) | |||
| } | |||
| export default PoQcStockInModalVer2 | |||
| <> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| > | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("qc processing")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <StockInFormVer2 itemDetail={itemDetail} disabled={false} /> | |||
| </Grid> | |||
| </Grid> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| id="stockInSubmit" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={formProps.handleSubmit(onSubmitStockIn)} | |||
| > | |||
| {t("submitStockIn")} | |||
| </Button> | |||
| </Stack> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| > | |||
| <QcFormVer2 | |||
| qc={qc!} | |||
| itemDetail={itemDetail} | |||
| disabled={false} | |||
| qcItems={qcItems} | |||
| setQcItems={setQcItems} | |||
| /> | |||
| </Grid> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| id="emailSupplier" | |||
| type="button" | |||
| variant="contained" | |||
| color="secondary" | |||
| sx={{ mt: 1 }} | |||
| onClick={formProps.handleSubmit(onSubmitEmailSupplier)} | |||
| > | |||
| {t("email supplier")} | |||
| </Button> | |||
| <Button | |||
| id="qcSubmit" | |||
| type="button" | |||
| variant="contained" | |||
| color="secondary" | |||
| sx={{ mt: 1 }} | |||
| onClick={formProps.handleSubmit(onSubmitQc)} | |||
| > | |||
| {t("confirm putaway")} | |||
| </Button> | |||
| </Stack> | |||
| </> | |||
| )} | |||
| </Box> | |||
| </Modal> | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoQcStockInModalVer2; | |||
| @@ -82,7 +82,7 @@ const StockInForm: React.FC<Props> = ({ | |||
| // receiptDate default tdy | |||
| setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT)); | |||
| setValue("status", "received"); | |||
| }, []); | |||
| }, [setValue]); | |||
| useEffect(() => { | |||
| console.log(errors); | |||
| @@ -97,7 +97,7 @@ const StockInForm: React.FC<Props> = ({ | |||
| if (expiryDate) clearErrors(); | |||
| if (productionDate) clearErrors(); | |||
| }, [productionDate, expiryDate, clearErrors]); | |||
| return ( | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Grid item xs={12}> | |||