diff --git a/src/app/api/invoices/actions.ts b/src/app/api/invoices/actions.ts index e82ab1c..22bbd0f 100644 --- a/src/app/api/invoices/actions.ts +++ b/src/app/api/invoices/actions.ts @@ -2,6 +2,7 @@ import { serverFetchJson, serverFetchString } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; +import { revalidateTag } from "next/cache"; import { cache } from "react"; export interface InvoiceResult { @@ -104,4 +105,16 @@ export const importInvoices = async (data: FormData) => { ); return importInvoices; -}; \ No newline at end of file +}; + +export const updateInvoice = async (data: any) => { + console.log(data) + const updateInvoice = await serverFetchJson(`${BASE_API_URL}/invoices/update`, { + method: "Post", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + + revalidateTag("invoices") + return updateInvoice; +} \ No newline at end of file diff --git a/src/app/api/invoices/index.ts b/src/app/api/invoices/index.ts index 930557e..2c0a326 100644 --- a/src/app/api/invoices/index.ts +++ b/src/app/api/invoices/index.ts @@ -69,6 +69,20 @@ export interface invoiceList { // stage: string; issuedDate: string; receiptDate: string; + issuedAmount: number; + receivedAmount: number; + team: string; +} + +export interface invoiceColum { + id: number; + action: string; + invoiceNo: string; + projectCode: string; + projectName: string; + // stage: string; + issuedDate: string; + receiptDate: string; issuedAmount: string; receivedAmount: string; team: string; diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index d5edb6f..8ecc9b8 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -33,6 +33,9 @@ export const convertDateArrayToString = ( format: string = OUTPUT_DATE_FORMAT, needTime: boolean = false, ) => { + if (dateArray === null){ + return "-" + } if (dateArray.length === 6) { if (!needTime) { const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`; @@ -131,6 +134,9 @@ export function convertLocaleStringToNumber(numberString: string): number { } export function timestampToDateString(timestamp: string): string { + if (timestamp === "0" || timestamp === null) { + return "-"; + } const date = new Date(timestamp); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); diff --git a/src/components/InvoiceSearch/InvoiceSearch.tsx b/src/components/InvoiceSearch/InvoiceSearch.tsx index 0e1409a..74fba52 100644 --- a/src/components/InvoiceSearch/InvoiceSearch.tsx +++ b/src/components/InvoiceSearch/InvoiceSearch.tsx @@ -6,13 +6,17 @@ import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import EditNote from "@mui/icons-material/EditNote"; import { convertLocaleStringToNumber } from "@/app/utils/formatUtil" -import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps } from "@mui/material"; -import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions } from "@mui/material"; import FileUploadIcon from '@mui/icons-material/FileUpload'; import { dateInRange, downloadFile } from "@/app/utils/commonUtil"; -import { importInvoices, importIssuedInovice, importReceivedInovice } from "@/app/api/invoices/actions"; +import { importInvoices, importIssuedInovice, importReceivedInovice, updateInvoice } from "@/app/api/invoices/actions"; import { errorDialogWithContent, successDialog } from "../Swal/CustomAlerts"; import { invoiceList, issuedInvoiceList, issuedInvoiceResult, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices"; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import { GridCellEditStartReasons, GridCellParams, GridColDef, GridEventListener, GridRowId, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; +import { useGridApiRef } from "@mui/x-data-grid"; +import StyledDataGrid from "../StyledDataGrid"; +import { QrCodeScannerOutlined } from "@mui/icons-material"; interface Props { issuedInvoice: issuedInvoiceList[]; @@ -20,6 +24,17 @@ interface Props { invoices: invoiceList[]; } +type InvoiceListError = { + [field in keyof invoiceList]?: string; +}; + +type invoiceListRow = Partial< + invoiceList & { + _isNew: boolean; + _error: InvoiceListError; + } +>; + type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; @@ -67,10 +82,8 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic return obj.map(obj => `"${obj.projectCode}" does not match with ${obj.invoicesNo}`).join(", ") } - const handleImportClick = useCallback(async (event:any) => { - // console.log(event) + const handleImportClick = useCallback(async (event: any) => { try { - const file = event.target.files[0]; if (!file) { @@ -86,57 +99,57 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic const formData = new FormData(); formData.append('multipartFileList', file); - const response = await importInvoices(formData); - // response: status, message, projectList, emptyRowList, invoiceList + const response = await importIssuedInovice(formData); + console.log(response); - console.log(response) if (response.status) { successDialog(t("Import Success"), t).then(() => { - window.location.reload() - }) - }else{ - if (response.emptyRowList.length >= 1){ - errorDialogWithContent(t("Import Fail"), - t(`Please fill the mandatory field at Row
${response.emptyRowList.join(", ")}`), t) - .then(() => { - window.location.reload() - }) - } - else if (response.projectList.length >= 1){ - errorDialogWithContent(t("Import Fail"), - t(`Please check the corresponding project code
${response.projectList.join(", ")}`), t) - .then(() => { - // window.location.reload() - }) - } - else if (response.invoiceList.length >= 1){ - errorDialogWithContent(t("Import Fail"), - t(`Please check the corresponding Invoice No. The invoice is imported.
`)+ `${response.invoiceList.join(", ")}`, t) - .then(() => { - window.location.reload() - }) - } - else if (response.duplicateItem.length >= 1){ - errorDialogWithContent(t("Import Fail"), - t(`Please check the corresponding Invoice No. The below invoice has duplicated number.
`)+ `${response.duplicateItem.join(", ")}`, t) - .then(() => { - window.location.reload() - }) - }else if (response.paymentMilestones.length >= 1){ - errorDialogWithContent(t("Import Fail"), - t(`The payment milestone does not match with records. Please check the corresponding Invoice No.
`)+ `${concatListOfObject(response.paymentMilestones)}`, t) - .then(() => { - window.location.reload() - }) - } + window.location.reload(); + }); + } else { + handleImportError(response); } - - } catch (err) { - console.log(err) - return false - } + } catch (err) { + console.log(err); + return false; + } }, []); + const handleImportError = (response: any) => { + if (response.emptyRowList.length >= 1) { + showErrorDialog( + t("Import Fail"), + t(`Please fill the mandatory field at Row
${response.emptyRowList.join(", ")}`) + ); + } else if (response.projectList.length >= 1) { + showErrorDialog( + t("Import Fail"), + t(`Please check the corresponding project code
${response.projectList.join(", ")}`) + ); + } else if (response.invoiceList.length >= 1) { + showErrorDialog( + t("Import Fail"), + t(`Please check the corresponding Invoice No. The invoice is imported.
`) + `${response.invoiceList.join(", ")}` + ); + } else if (response.duplicateItem.length >= 1) { + showErrorDialog( + t("Import Fail"), + t(`Please check the corresponding Invoice No. The below invoice has duplicated number.
`)+ `${response.duplicateItem.join(", ")}` + ); + } else if (response.paymentMilestones.length >= 1) { + showErrorDialog( + t("Import Fail"), + t(`The payment milestone does not match with records. Please check the corresponding Invoice No.
`) + `${concatListOfObject(response.paymentMilestones)}` + ); + } + }; + + const showErrorDialog = (title: string, content: string) => { + errorDialogWithContent(title, content, t).then(() => { + window.location.reload(); + }); + }; + const handleRecImportClick = useCallback(async (event:any) => { try { @@ -229,16 +242,93 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic [t], ); + const [selectedRow, setSelectedRow] = useState([]); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleButtonClick = (row: invoiceList) => { + console.log(row) + setSelectedRow([row]); + setDialogOpen(true); + setRowModesModel((model) => ({ + ...model, + [row.id]: { mode: GridRowModes.Edit, fieldToFocus: "issuedAmount" }, + })); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + // console.log(selectedRow) + // setSelectedRow([]); + }; + + const handleSaveDialog = async () => { + // setDialogOpen(false); + const response = await updateInvoice(selectedRow[0]) + setDialogOpen(false); + successDialog(t("Update Success"), t).then(() => { + // window.location.reload() + }) + + // console.log(selectedRow[0]) + // setSelectedRow([]); + }; + const combinedColumns = useMemo[]>( () => [ + { + name: "invoiceNo", + label: t("Edit"), + onClick: (row: invoiceList) => ( + handleButtonClick(row) + ), + buttonIcon: + }, { name: "invoiceNo", label: t("Invoice No") }, { name: "projectCode", label: t("Project Code") }, { name: "projectName", label: t("Project Name") }, { name: "team", label: t("Team") }, { name: "issuedDate", label: t("Issue Date") }, - { name: "receivedAmount", label: t("Amount (HKD)") }, + { name: "issuedAmount", label: t("Amount (HKD)"), type: 'money', needTranslation: true }, { name: "receiptDate", label: t("Settle Date") }, - { name: "receivedAmount", label: t("Actual Received Amount (HKD)") }, + { name: "receivedAmount", label: t("Actual Received Amount (HKD)"), type: 'money', needTranslation: true }, + ], + [t] + ) + + 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] ) @@ -263,6 +353,72 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic [], ); + const [rowModesModel, setRowModesModel] = useState({}); + const apiRef = useGridApiRef(); + + const validateInvoiceEntry = ( + entry: Partial, + ): InvoiceListError | undefined => { + // Test for errors + const error: InvoiceListError = {}; + + 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){ + error.receiptDate = "Please input receipt date"; + } + + + 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}] as invoiceList[]) + event.defaultMuiPrevented = true; + } + // console.log(row) + }, + [validateRow], + ); + return ( <> = ({ issuedInvoice, receivedInvoice, invoic flexWrap="wrap" spacing={2} > - {/* */} - {/* */} - {/* */} - + + {/* + */} { // tabIndex == 0 && @@ -360,6 +516,74 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic columns={columns2} /> } */} + + + {t("Edit Invoice")} + + + {t("You can edit the invoice details here.")} + + {/* + items={selectedRow ? [selectedRow] : []} + columns={editCombinedColumns} + /> */} + .MuiDataGrid-cell': { + overflow: 'auto', + whiteSpace: 'nowrap', + textWrap: 'pretty', + }, + width: "100%", // Make the DataGrid wider + }} + disableColumnMenu + editMode="row" + rows={selectedRow} + rowModesModel={rowModesModel} + onRowModesModelChange={setRowModesModel} + onRowEditStop={handleEditStop} + columns={editCombinedColumns} + getCellClassName={(params: GridCellParams) => { + let classname = ""; + if (params.row._error?.[params.field as keyof invoiceList]) { + classname = "hasError"; + } + return classname; + }} + /> + + + + + + ); diff --git a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx index ad3c49c..9171f6e 100644 --- a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx +++ b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx @@ -56,9 +56,9 @@ const InvoiceSearchWrapper: React.FC & SubComponents = async () => { projectName: invoice.projectName, team: invoice.team, issuedDate: timestampToDateString(invoice.invoiceDate)!!, - receiptDate: timestampToDateString(invoice.receiptDate??0)!!, - issuedAmount: moneyFormatter.format(invoice.issueAmount), - receivedAmount: moneyFormatter.format(invoice.paidAmount) + receiptDate: timestampToDateString(invoice.receiptDate)!!, + issuedAmount: invoice.issueAmount, + receivedAmount: invoice.paidAmount } })