|
- 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<Props> = ({ projects }) => {
- console.log(projects)
- const projectCombos = projects.map(item => item.code)
- const { t } = useTranslation()
- const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
- const [selectedRow, setSelectedRow] = useState<ExpenseListRow[] | []>([]);
- const { getValues, setValue, clearErrors, setError } =
- useFormContext<any>();
- const apiRef = useGridApiRef();
- const validateExpenseEntry = (
- entry: Partial<ProjectExpensesResultFormatted>,
- ): 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<GridEventListener<"rowEditStop">>(
- (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<ExpenseListRow>,
- originalRow: GridRowModel<ExpenseListRow>,
- ) => {
- 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<any, number>) {
- return(
- <Box sx={{ display: 'flex', alignItems: 'center', pr: 2 }}>
- <Autocomplete
- readOnly
- sx={{ width: 300 }}
- value={params.row.projectCode}
- options={projectCombos}
- renderInput={(params) => <TextField {...params} />}
- />
- </Box>
- )
- }
-
- function AutocompleteInput(props: GridRenderCellParams<any, number>) {
- const { id, value, field, hasFocus } = props;
- const apiRef = useGridApiContext();
- const ref = React.useRef<HTMLElement>(null);
-
- const handleValueChange = useCallback((newValue: any) => {
- console.log(newValue)
- apiRef.current.setEditCellValue({ id, field, value: newValue })
- }, []);
-
- return (
- <Box sx={{ display: 'flex', alignItems: 'center', pr: 2 }}>
- <Autocomplete
- disablePortal
- options={projectCombos}
- sx={{ width: 300 }}
- onChange={(event: React.SyntheticEvent<Element, Event>, value: string | null, ) => handleValueChange(value)}
- renderInput={(params) => <TextField {...params} />}
- />
- </Box>
- );
- }
-
- const renderAutocompleteInput: GridColDef['renderCell'] = (params) => {
- return <AutocompleteInput {...params} />;
- };
-
- const editCombinedColumns = useMemo<GridColDef[]>(
- () => [
- {
- 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 = (
- <Box display="flex" gap={2} alignItems="center">
- <Button
- disableRipple
- variant="outlined"
- startIcon={<Add />}
- onClick={addRow}
- size="small"
- >
- {t("Create Expense")}
- </Button>
- {
- hasRowErrors &&
- <Typography color="warning.main" variant="body2">
- {t("There are errors!")} {selectedRow.find(row => row._error !== null)?._error?.message}
- </Typography>
- }
- </Box>
- );
-
- return (
- <>
- <StyledDataGrid
- apiRef={apiRef}
- 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",
- },
- height: 400, width: '95%'
- }}
- disableColumnMenu
- editMode="row"
- rows={selectedRow}
- rowModesModel={rowModesModel}
- onRowModesModelChange={setRowModesModel}
- onRowEditStop={handleEditStop}
- columns={editCombinedColumns}
- processRowUpdate={processRowUpdate}
- onProcessRowUpdateError={onProcessRowUpdateError}
- getCellClassName={(params: GridCellParams<ExpenseListRow>) => {
- 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 (
- <Box
- display="flex"
- justifyContent="center"
- alignItems="center"
- height="100%"
- >
- <Typography variant="caption">{t("Add some time entries!")}</Typography>
- </Box>
- );
- };
-
- const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
- return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
- };
|