|
|
@@ -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<GridRowModesModel>({}); |
|
|
|
const [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]); |
|
|
|
const apiRef = useGridApiRef(); |
|
|
|
|
|
|
|
const validateInvoiceEntry = ( |
|
|
|
entry: Partial<invoiceList>, |
|
|
|
): 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<GridEventListener<"rowEditStop">>( |
|
|
|
(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<invoiceListRow>, |
|
|
|
originalRow: GridRowModel<invoiceListRow>, |
|
|
|
) => { |
|
|
|
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<GridColDef[]>( |
|
|
|
() => [ |
|
|
|
{ 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 = ( |
|
|
|
<Box display="flex" gap={2} alignItems="center"> |
|
|
|
<Button |
|
|
|
disableRipple |
|
|
|
variant="outlined" |
|
|
|
startIcon={<Add />} |
|
|
|
onClick={addRow} |
|
|
|
size="small" |
|
|
|
> |
|
|
|
{t("Create Invoice")} |
|
|
|
</Button> |
|
|
|
</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", |
|
|
|
}, |
|
|
|
}} |
|
|
|
disableColumnMenu |
|
|
|
editMode="row" |
|
|
|
rows={selectedRow} |
|
|
|
rowModesModel={rowModesModel} |
|
|
|
onRowModesModelChange={setRowModesModel} |
|
|
|
onRowEditStop={handleEditStop} |
|
|
|
columns={editCombinedColumns} |
|
|
|
processRowUpdate={processRowUpdate} |
|
|
|
onProcessRowUpdateError={onProcessRowUpdateError} |
|
|
|
getCellClassName={(params: GridCellParams<invoiceListRow>) => { |
|
|
|
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 ( |
|
|
|
<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>; |
|
|
|
}; |