From 99dfcd03d1f88a14639ebd6511e30cd288854339 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Thu, 29 Aug 2024 15:08:35 +0800 Subject: [PATCH] Create Invoice --- .../InvoiceSearch/CreateInvoiceModal.tsx | 84 +++++ src/components/InvoiceSearch/InvoiceTable.tsx | 295 ++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 src/components/InvoiceSearch/CreateInvoiceModal.tsx create mode 100644 src/components/InvoiceSearch/InvoiceTable.tsx diff --git a/src/components/InvoiceSearch/CreateInvoiceModal.tsx b/src/components/InvoiceSearch/CreateInvoiceModal.tsx new file mode 100644 index 0000000..d552d5f --- /dev/null +++ b/src/components/InvoiceSearch/CreateInvoiceModal.tsx @@ -0,0 +1,84 @@ +import React, { useCallback, useState } from 'react'; +import { + Modal, + Box, + Typography, + Button, + SxProps, + CardContent, + CardActions, + Card +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; +import { Check, Close } from "@mui/icons-material"; +import InvoiceTable from './InvoiceTable'; + +interface Props { + isOpen: boolean, + onClose: () => void; +} + +const modalSx: SxProps= { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: { xs: "calc(100% - 2rem)", sm: "90%" }, + maxHeight: "90%", + maxWidth: 1400, + bgcolor: 'background.paper', +}; + +const CreateInvoiceModal: React.FC = ({isOpen, onClose}) => { + const { t } = useTranslation() + const formProps = useForm(); + + const onSubmit = useCallback>( + (data) => { + console.log(data) + } + , []) + + return ( + + + + + + {t("Invoice Creation")} + + + + + + + + + + + + + ); +}; + +export default CreateInvoiceModal; \ No newline at end of file diff --git a/src/components/InvoiceSearch/InvoiceTable.tsx b/src/components/InvoiceSearch/InvoiceTable.tsx new file mode 100644 index 0000000..8294e50 --- /dev/null +++ b/src/components/InvoiceSearch/InvoiceTable.tsx @@ -0,0 +1,295 @@ +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 } from "@mui/material"; +import FileUploadIcon from '@mui/icons-material/FileUpload'; +import { Add, Check, Close, Delete } from "@mui/icons-material"; +import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoice } from "@/app/api/invoices/actions"; +import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts"; +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 } from "@mui/x-data-grid"; +import { useGridApiRef } from "@mui/x-data-grid"; +import StyledDataGrid from "../StyledDataGrid"; + +import { uniq } from "lodash"; +import CreateInvoiceModal from "./CreateInvoiceModal"; +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"; + +type InvoiceListError = { + [field in keyof invoiceList]?: string; +}; + +type invoiceListRow = Partial< + invoiceList & { + _isNew: boolean; + _error: InvoiceListError; + } +>; + +class ProcessRowUpdateError extends Error { + public readonly row: invoiceListRow; + public readonly errors: InvoiceListError | undefined; + constructor( + row: invoiceListRow, + message?: string, + errors?: InvoiceListError, + ) { + super(message); + this.row = row; + this.errors = errors; + + Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); + } +} + +const InvoiceTable: React.FC = () => { + const { t } = useTranslation() + const [rowModesModel, setRowModesModel] = useState({}); + const [selectedRow, setSelectedRow] = useState([]); + const apiRef = useGridApiRef(); + + const validateInvoiceEntry = ( + entry: Partial, + ): InvoiceListError | undefined => { + // Test for errors + const error: any = {}; + + console.log(entry) + if (!entry.issuedAmount) { + error.issuedAmount = "Please input issued amount "; + } else if (!entry.issuedAmount) { + error.receivedAmount = "Please input received amount"; + } else if (entry.invoiceNo === "") { + error.invoiceNo = "Please input invoice number"; + } else if (!entry.issuedDate) { + error.issuedDate = "Please input issue date"; + } else if (!entry.receiptDate){ + + } + + return Object.keys(error).length > 0 ? error : undefined; + } + + const validateRow = useCallback( + (id: GridRowId) => { + const row = apiRef.current.getRowWithUpdatedValues( + id, + "", + ) + + const error = validateInvoiceEntry(row); + // console.log(error) + // Test for warnings + + apiRef.current.updateRows([{ id, _error: error }]); + return error; + }, + [apiRef], + ); + + const handleEditStop = useCallback>( + (params, event) => { + // console.log(params.id) + if (validateRow(params.id) !== undefined || !validateRow(params.id)) { + + setRowModesModel((model) => ({ + ...model, + [params.id]: { mode: GridRowModes.View}, + })); + + const row = apiRef.current.getRowWithUpdatedValues( + params.id, + "", + ) + console.log(row) + setSelectedRow((row) => [...row] as any[]) + event.defaultMuiPrevented = true; + } + // 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 invoiceListRow; + + console.log(newRow) + + setSelectedRow((es) => + es.map((e) => (e.id === rowToSave.id ? rowToSave : e)) + ); + return rowToSave; + }, + [validateRow], + ); + + /** + * Add callback to check error + */ + const onProcessRowUpdateError = useCallback( + (updateError: ProcessRowUpdateError) => { + const errors = updateError.errors; + const oldRow = updateError.row; + + apiRef.current.updateRows([{ ...oldRow, _error: errors }]); + }, + [apiRef] + ) + + useEffect(() => { + console.log(selectedRow) + }, [selectedRow]); + + +const editCombinedColumns = useMemo( + () => [ + { field: "invoiceNo", headerName: t("Invoice No"), editable: true, flex: 0.5 }, + { field: "projectCode", headerName: t("Project Code"), editable: false, flex: 0.3 }, + // { field: "projectName", headerName: t("Project Name"), flex: 1 }, + // { field: "team", headerName: t("Team"), flex: 0.2 }, + { field: "issuedDate", + headerName: t("Issue Date"), + editable: true, + flex: 0.4, + // type: 'date', + // valueGetter: (params) => { + // // console.log(params.row.issuedDate) + // return new Date(params.row.issuedDate) + // }, + }, + { field: "issuedAmount", + headerName: t("Amount (HKD)"), + editable: true, + flex: 0.5, + type: 'number' + }, + { + field: "receiptDate", + headerName: t("Settle Date"), + editable: true, + flex: 0.4, + }, + { field: "receivedAmount", + headerName: t("Actual Received Amount (HKD)"), + editable: true, + flex: 0.5, + type: 'number' + }, + ], + [t] + ) + +const footer = ( + + + + ); + + return ( + <> +) => { + let classname = ""; + if (params.row._error?.[params.field as keyof invoiceList]) { + classname = "hasError"; + } + return classname; + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + slotProps={{ + footer: { child: footer }, + }} + /> + + ) +} + +export default InvoiceTable + +const NoRowsOverlay: React.FC = () => { + const { t } = useTranslation("home"); + return ( + + {t("Add some time entries!")} + + ); +}; + +const FooterToolbar: React.FC = ({ child }) => { + return {child}; +}; \ No newline at end of file