|
- "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<Omit<issuedInvoiceSearchForm, "id">>;
- type SearchParamNames = keyof SearchQuery;
-
- type SearchQuery2 = Partial<Omit<receivedInvoiceSearchForm, "id">>;
- type SearchParamNames2 = keyof SearchQuery2;
-
- const InvoiceSearch: React.FC<Props> = ({ invoices, projects, abilities }) => {
- console.log(abilities)
- const { t } = useTranslation("Invoice");
-
- const [filteredIvoices, setFilterInovices] = useState(invoices);
-
- const searchCriteria: Criterion<SearchParamNames>[] = 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<boolean>(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 <br> ${response.emptyRowList.join(", ")}`)
- );
- } else if (response.projectList.length >= 1) {
- showErrorDialog(
- t("Import Fail"),
- t(`Please check the corresponding project code <br> ${response.projectList.join(", ")}`)
- );
- } else if (response.invoiceList.length >= 1) {
- showErrorDialog(
- t("Import Fail"),
- t(`Please check the corresponding Invoice No. The invoice is imported. <br>`) + `${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. <br>`)+ `${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. <br>`) + `${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 <br> ${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 <br> ${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. <br>`)+ `${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. <br>`)+ `${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. <br>`)+ `${concatListOfObject2(response.paymentMilestones)}`, t)
- .then(() => {
- window.location.reload()
- })
- }
- }
- }catch(error){
- console.log(error)
- }
-
- }, []);
-
- const [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]);
- 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<Column<invoiceList>[]>(
- () => [
- {
- name: "invoiceNo",
- label: t("Edit"),
- onClick: (row: invoiceList) => (
- handleButtonClick(row)
- ),
- buttonIcon: <EditOutlinedIcon />
- },
- { 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<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,
- // renderCell: (params) => {
- // console.log(params)
- // return (
- // <LocalizationProvider dateAdapter={AdapterDayjs}>
- // <DatePicker
- // value={dayjs(params.value)}
- // />
- // </LocalizationProvider>
- // );
- // }
- },
- { 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<GridRowModesModel>({});
- const apiRef = useGridApiRef();
-
- const validateInvoiceEntry = (
- entry: Partial<invoiceList>,
- ): 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<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}] 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() &&
- <Stack
- direction="row"
- justifyContent="right"
- flexWrap="wrap"
- spacing={2}
- >
- <ButtonGroup variant="contained">
- <Button
- startIcon={<AddIcon />}
- variant="outlined"
- component="label"
- onClick={handleAddInvoiceClick}
- >
- {t("Create Invoice")}
- </Button>
- <Button startIcon={<FileUploadIcon />} variant="contained" component="label">
- <input
- id='importExcel'
- type='file'
- accept='.xlsx, .csv'
- hidden
- onChange={(event) => {handleImportClick(event)}}
- />
- {t("Import Invoice Issue Summary")}
- </Button>
- <Button startIcon={<FileUploadIcon />} component="label" variant="contained">
- <input
- id='importExcel'
- type='file'
- accept='.xlsx, .csv'
- hidden
- onChange={(event) => {handleRecImportClick(event)}}
- />
- {t("Import Invoice Amount Receive Summary")}
- </Button>
- </ButtonGroup>
- </Stack>
- }
-
- {
- // tabIndex == 0 &&
- <SearchBox
- criteria={searchCriteria}
- onSearch={(query) => {
- // 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}
- />
- }
-
- <Divider sx={{ paddingBlockStart: 2 }} />
- <Card sx={{ display: "block" }}>
- <CardContent>
- <Stack direction="row" justifyContent="space-between">
- <Typography variant="h6">{t('Total Issued Amount (HKD)')}:</Typography>
- <Typography variant="h6">{moneyFormatter.format(filteredIvoices.reduce((acc, current) => (acc + current.issuedAmount), 0))}</Typography>
- </Stack>
- <Stack direction="row" justifyContent="space-between">
- <Typography variant="h6">{t('Total Received Amount (HKD)')}:</Typography>
- <Typography variant="h6">{moneyFormatter.format(filteredIvoices.reduce((acc, current) => (acc + current.receivedAmount), 0))}</Typography>
- </Stack>
- </CardContent>
- </Card>
- <Divider sx={{ paddingBlockEnd: 2 }} />
-
- {
- // tabIndex == 0 &&
- <SearchResults<invoiceList>
- items={filteredIvoices}
- columns={combinedColumns}
- autoRedirectToFirstPage
- />
- }
- <Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="lg" fullWidth>
- <DialogTitle>{t("Edit Invoice")}</DialogTitle>
- <DialogContent>
- <DialogContentText>
- {t("You can edit the invoice details here.")}
- </DialogContentText>
- <StyledDataGrid
- apiRef={apiRef}
- autoHeight
- 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",
- },
- '& .MuiDataGrid-columnHeaderTitle': {
- whiteSpace: 'normal',
- textWrap: 'pretty',
- textAlign: 'center',
- },
- '.MuiDataGrid-row:not(.MuiDataGrid-row--dynamicHeight)>.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<invoiceListRow>) => {
- let classname = "";
- if (params.row._error?.[params.field as keyof invoiceList]) {
- classname = "hasError";
- }
- return classname;
- }}
- />
- </DialogContent>
- <DialogActions>
- <Button onClick={handleDeleteInvoice} color="error">
- {t("Delete")}
- </Button>
- <Button onClick={handleCloseDialog} color="primary">
- {t("Cancel")}
- </Button>
- <Button
- onClick={handleSaveDialog}
- color="primary"
- disabled={
- Object.values(rowModesModel).some((mode) => mode.mode === GridRowModes.Edit) ||
- selectedRow.some((row) => row._error)
- }
- >
- {t("Save")}
- </Button>
- </DialogActions>
- </Dialog>
- <CreateInvoiceModal
- isOpen={modelOpen}
- onClose={handleModalClose}
- projects={projects}
- invoices={invoices}
- />
- </>
- );
- };
-
- export default InvoiceSearch;
|