import React, { useCallback, useMemo, useState, useEffect } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import EditNote from "@mui/icons-material/EditNote"; import { moneyFormatter } from "@/app/utils/formatUtil" import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, TextField, CardContent, Typography, Divider, Card, Box, Autocomplete, MenuItem } from "@mui/material"; import FileUploadIcon from '@mui/icons-material/FileUpload'; import { Add, Check, Close, Delete } from "@mui/icons-material"; import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices"; import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import { GridCellParams, GridColDef, GridEventListener, GridRowId, GridRowModel, GridRowModes, GridRowModesModel, GridRenderEditCellParams, useGridApiContext, } from "@mui/x-data-grid"; import { useGridApiRef } from "@mui/x-data-grid"; import StyledDataGrid from "../StyledDataGrid"; import { uniq } from "lodash"; import CreateInvoiceModal from "./CreateExpenseModal"; import { GridToolbarContainer } from "@mui/x-data-grid"; import { FooterPropsOverrides } from "@mui/x-data-grid"; import { th } from "@faker-js/faker"; import { GridRowIdGetter } from "@mui/x-data-grid"; import { useFormContext } from "react-hook-form"; import { ProjectResult } from "@/app/api/projects"; import { ProjectExpensesResultFormatted } from "@/app/api/projectExpenses"; import { GridRenderCellParams } from "@mui/x-data-grid"; type ExpenseListError = { [field in keyof ProjectExpensesResultFormatted]?: string; }& { message?: string; }; type ExpenseListRow = Partial< ProjectExpensesResultFormatted & { _isNew: boolean; _error: ExpenseListError; } >; interface Props { projects: ProjectResult[]; } class ProcessRowUpdateError extends Error { public readonly row: ExpenseListRow; public readonly errors: ExpenseListError | undefined; constructor( row: ExpenseListRow, message?: string, errors?: ExpenseListError, ) { super(message); this.row = row; this.errors = errors; Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); } } type project = { label: string; value: number; } const ExpenseTable: React.FC = ({ projects }) => { console.log(projects) const projectCombos = projects.map(item => item.code) const { t } = useTranslation() const [rowModesModel, setRowModesModel] = useState({}); const [selectedRow, setSelectedRow] = useState([]); const { getValues, setValue, clearErrors, setError } = useFormContext(); const apiRef = useGridApiRef(); const validateExpenseEntry = ( entry: Partial, ): ExpenseListError | undefined => { // Test for errors const error: ExpenseListError = {}; // if (!entry.issueDate) { // error.issueDate = "Please input issued date"; // error.message = "Please input issued date"; // } if (!entry.amount) { error.amount = "Please input amount"; error.message = "Please input amount" } if (!entry.projectCode) { error.projectCode = "Please input project code"; error.message = "Please input project code"; } console.log(error) return Object.keys(error).length > 0 ? error : undefined; } const validateRow = useCallback( (id: GridRowId) => { const row = apiRef.current.getRowWithUpdatedValues( id, "", ) const error = validateExpenseEntry(row); console.log(error) // Test for warnings // apiRef.current.updateRows([{ id, _error: error }]); return error; }, [], ); const handleEditStop = useCallback>( (params, event) => { const row = apiRef.current.getRowWithUpdatedValues( params.id, "", ) console.log(validateRow(params.id) !== undefined) console.log(!validateRow(params.id)) if (validateRow(params.id) !== undefined && !validateRow(params.id)) { setRowModesModel((model) => ({ ...model, [params.id]: { mode: GridRowModes.View}, })); console.log(row) setSelectedRow((row) => [...row] as any[]) event.defaultMuiPrevented = true; }else{ console.log(row) const error = validateRow(params.id) setSelectedRow((row) => { const updatedRow = row.map(r => r.id === params.id ? { ...r, _error: error } : r); return updatedRow; }) } // console.log(row) }, [validateRow], ); const addRow = useCallback(() => { const id = Date.now(); setSelectedRow((e) => [...e, { id, _isNew: true }]); setRowModesModel((model) => ({ ...model, [id]: { mode: GridRowModes.Edit }, })); }, []); const processRowUpdate = useCallback( ( newRow: GridRowModel, originalRow: GridRowModel, ) => { const errors = validateRow(newRow.id!!); if (errors) { // console.log(errors) // throw new error for error checking throw new ProcessRowUpdateError( originalRow, "validation error", errors, ) } // eslint-disable-next-line @typescript-eslint/no-unused-vars const { _isNew, _error, ...updatedRow } = newRow; const rowToSave = { ...updatedRow, } satisfies ExpenseListRow; console.log(newRow) setSelectedRow((es) => es.map((e) => (e.id === originalRow.id ? rowToSave : e)) ); console.log(rowToSave) return rowToSave; }, [validateRow], ); const hasRowErrors = selectedRow.some(row => row._error !== undefined) /** * Add callback to check error */ const onProcessRowUpdateError = useCallback( (updateError: ProcessRowUpdateError) => { const errors = updateError.errors; const oldRow = updateError.row; console.log(errors) apiRef.current.updateRows([{ ...oldRow, _error: errors }]); }, [apiRef] ) useEffect(() => { console.log(selectedRow) setValue("data", selectedRow) }, [selectedRow, setValue]); function renderAutocomplete(params: GridRenderCellParams) { return( } /> ) } function AutocompleteInput(props: GridRenderCellParams) { const { id, value, field, hasFocus } = props; const apiRef = useGridApiContext(); const ref = React.useRef(null); const handleValueChange = useCallback((newValue: any) => { console.log(newValue) apiRef.current.setEditCellValue({ id, field, value: newValue }) }, []); return ( , value: string | null, ) => handleValueChange(value)} renderInput={(params) => } /> ); } const renderAutocompleteInput: GridColDef['renderCell'] = (params) => { return ; }; const editCombinedColumns = useMemo( () => [ { field: "expenseNo", headerName: t("Expense No"), editable: true, flex: 0.5 }, { field: "projectCode", headerName: t("Project Code"), editable: true, flex: 0.3, renderCell: renderAutocomplete, renderEditCell: renderAutocompleteInput }, // { // field: "issueDate", // headerName: t("Issue Date"), // editable: true, // flex: 0.4, // type: 'date', // }, { field: "amount", headerName: t("Amount (HKD)"), editable: true, flex: 0.5, type: 'number' }, // { // field: "receiptDate", // headerName: t("Settle Date"), // editable: true, // flex: 0.4, // type: 'date', // }, { field: "remarks", headerName: t("Remarks"), editable: true, flex: 1, }, ], [t] ) const footer = ( { hasRowErrors && {t("There are errors!")} {selectedRow.find(row => row._error !== null)?._error?.message} } ); return ( <> ) => { let classname = ""; if (params.row._error?.[params.field as keyof ProjectExpensesResultFormatted]) { classname = "hasError"; } return classname; }} slots={{ footer: FooterToolbar, noRowsOverlay: NoRowsOverlay, }} slotProps={{ footer: { child: footer }, }} /> ) } export default ExpenseTable const NoRowsOverlay: React.FC = () => { const { t } = useTranslation("home"); return ( {t("Add some time entries!")} ); }; const FooterToolbar: React.FC = ({ child }) => { return {child}; };