"use client"; import React, { useCallback, useMemo, useState } 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 } from "@mui/material"; import FileUploadIcon from '@mui/icons-material/FileUpload'; import AddIcon from '@mui/icons-material/Add'; 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, 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 { ProjectResult } from "@/app/api/projects"; import { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware"; interface Props { invoices: invoiceList[]; projects: ProjectResult[]; abilities: string[]; } type InvoiceListError = { [field in keyof invoiceList]?: string; }; type invoiceListRow = Partial< invoiceList & { _isNew: boolean; _error: InvoiceListError; } >; type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; type SearchQuery2 = Partial>; type SearchParamNames2 = keyof SearchQuery2; const InvoiceSearch: React.FC = ({ invoices, projects, abilities }) => { console.log(abilities) const { t } = useTranslation("Invoice"); const [filteredIvoices, setFilterInovices] = useState(invoices); const searchCriteria: Criterion[] = useMemo( () => [ { label: t("Invoice No"), paramName: "invoiceNo", type: "text" }, { label: t("Project Code"), paramName: "projectCode", type: "text" }, { label: t("Team"), paramName: "team", type: "select", options: uniq(invoices.map((invoice) => invoice.teamCodeName)), }, { label: t("Issue Date"), label2: t("Issue Date To"), paramName: "invoiceDate", type: "dateRange" }, { label: t("Settle Date"), label2: t("Settle Date To"), paramName: "dueDate", type: "dateRange" }, ], [t, invoices], ); const onReset = useCallback(() => { // setFilteredIssuedInvoices(issuedInvoice); setFilterInovices(invoices) }, [invoices]); function concatListOfObject(obj: any[]): string { return obj.map(obj => `Cannot find "${obj.paymentMilestone}" in ${obj.invoiceNo}`).join(", ") } function concatListOfObject2(obj: any[]): string { return obj.map(obj => `"${obj.projectCode}" does not match with ${obj.invoicesNo}`).join(", ") } const handleImportClick = useCallback(async (event: any) => { try { const file = event.target.files[0]; if (!file) { console.log('No file selected'); return; } if (file.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { console.log('Invalid file format. Only XLSX files are allowed.'); return; } const formData = new FormData(); formData.append('multipartFileList', file); const response = await importIssuedInovice(formData); // response: status, message, projectList, emptyRowList, invoiceList if (response.status) { successDialog(t("Import Success"), t).then(() => { window.location.reload(); }); } else { handleImportError(response); } } catch (err) { console.log(err); return false; } }, []); const [modelOpen, setModelOpen] = useState(false); const handleAddInvoiceClick = useCallback(() => { setModelOpen(true) },[]) const handleModalClose = useCallback(() => { setModelOpen(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 { const file = event.target.files[0]; if (!file) { console.log('No file selected'); return; } if (file.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { console.log('Invalid file format. Only XLSX files are allowed.'); return; } const formData = new FormData(); formData.append('multipartFileList', file); const response = await importReceivedInovice(formData) 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 has not yet issued.
`)+ `${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.
`)+ `${concatListOfObject2(response.paymentMilestones)}`, t) .then(() => { window.location.reload() }) } } }catch(error){ console.log(error) } }, []); 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); }; const handleDeleteInvoice = useCallback(() => { deleteDialog(async() => { //console.log(selectedRow[0]) await deleteInvoice(selectedRow[0].id!!) setDialogOpen(false); const result = await successDialog("Delete Success", t); if (result) { window.location.reload() } }, t) }, [selectedRow]); const handleSaveDialog = async () => { // setDialogOpen(false); 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: "issuedAmount", label: t("Amount (HKD)"), type: 'money', needTranslation: true }, { name: "receiptDate", label: t("Settle Date") }, { 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, // renderCell: (params) => { // console.log(params) // return ( // // // // ); // } }, { field: "receivedAmount", headerName: t("Actual Received Amount (HKD)"), editable: true, flex: 0.5, type: 'number' }, ], [t] ) function isDateInRange(dateToCheck: string, startDate: string, endDate: string): boolean { if ((!startDate || startDate === "Invalid Date") && (!endDate || endDate === "Invalid Date")) { return true; } const dateToCheckObj = new Date(dateToCheck); const startDateObj = new Date(startDate); const endDateObj = new Date(endDate); return ((!startDate || startDate === "Invalid Date") || dateToCheckObj >= startDateObj) && ((!endDate || endDate === "Invalid Date") || dateToCheckObj <= endDateObj); } 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], ); const isAddInvoiceRightExist = () => { const importRight = [IMPORT_INVOICE, IMPORT_RECEIPT].some((ability) => abilities.includes(ability)) return importRight } return ( <> { isAddInvoiceRightExist() && } { // tabIndex == 0 && { // console.log(query) setFilterInovices( invoices.filter( (s) => (s.invoiceNo.toLowerCase().includes(query.invoiceNo.toLowerCase())) && (s.projectCode.toLowerCase().includes(query.projectCode.toLowerCase())) && (query.team === "All" || query.team.toLowerCase().includes(s.team.toLowerCase())) && (isDateInRange(s.issuedDate, query.invoiceDate ?? undefined, query.invoiceDateTo ?? undefined)) && (isDateInRange(s.receiptDate, query.dueDate ?? undefined, query.dueDateTo ?? undefined)) ), ); }} onReset={onReset} /> } {t('Total Issued Amount (HKD)')}: {moneyFormatter.format(filteredIvoices.reduce((acc, current) => (acc + current.issuedAmount), 0))} {t('Total Received Amount (HKD)')}: {moneyFormatter.format(filteredIvoices.reduce((acc, current) => (acc + current.receivedAmount), 0))} { // tabIndex == 0 && items={filteredIvoices} columns={combinedColumns} autoRedirectToFirstPage /> } {t("Edit Invoice")} {t("You can edit the invoice details here.")} .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; }} /> ); }; export default InvoiceSearch;