diff --git a/src/app/(main)/expense/page.tsx b/src/app/(main)/expense/page.tsx new file mode 100644 index 0000000..6cf1645 --- /dev/null +++ b/src/app/(main)/expense/page.tsx @@ -0,0 +1,30 @@ +import ExpenseSearch from "@/components/ExpenseSearch" +import { getServerI18n, I18nProvider } from "@/i18n" +import { Stack, Typography } from "@mui/material" +import { Suspense } from "react" + +const Expense: React.FC = async () => { + const { t } = await getServerI18n("expense") + + return( + + + + {t("Expense")} + + }> + + + + + + + ) +} + +export default Expense \ No newline at end of file diff --git a/src/app/(main)/invoice/new/[projectId]/page.tsx b/src/app/(main)/invoice/new/[projectId]/page.tsx new file mode 100644 index 0000000..da4701b --- /dev/null +++ b/src/app/(main)/invoice/new/[projectId]/page.tsx @@ -0,0 +1,27 @@ +import { Metadata } from "next"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Link from "next/link"; +import CreateInvoice from "@/components/CreateInvoice_forGen"; + +export const metadata: Metadata = { + title: "Create Invoice", +}; + +const Invoice: React.FC = async () => { + const { t } = await getServerI18n("Create Invoice"); + + return ( + <> + {t("Create Invoice")} + + + + + ) +}; + +export default Invoice; \ No newline at end of file diff --git a/src/app/(main)/invoice/page.tsx b/src/app/(main)/invoice/page.tsx index 237ca9c..2c19f4f 100644 --- a/src/app/(main)/invoice/page.tsx +++ b/src/app/(main)/invoice/page.tsx @@ -1,5 +1,5 @@ import { Metadata } from "next"; -import { getServerI18n } from "@/i18n"; +import { getServerI18n, I18nProvider } from "@/i18n"; import Add from "@mui/icons-material/Add"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; @@ -36,7 +36,9 @@ const Invoice: React.FC = async () => { */} }> - + + + ) diff --git a/src/components/ExpenseSearch/CreateExpenseModal.tsx b/src/components/ExpenseSearch/CreateExpenseModal.tsx new file mode 100644 index 0000000..d1a296c --- /dev/null +++ b/src/components/ExpenseSearch/CreateExpenseModal.tsx @@ -0,0 +1,87 @@ +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 './ExpenseTable'; +import { ProjectResult } from '@/app/api/projects'; + +interface Props { + isOpen: boolean, + onClose: () => void + projects: ProjectResult[] +} + +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, projects}) => { + 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/ExpenseSearch/ExpenseSearch.tsx b/src/components/ExpenseSearch/ExpenseSearch.tsx new file mode 100644 index 0000000..8a0dc1d --- /dev/null +++ b/src/components/ExpenseSearch/ExpenseSearch.tsx @@ -0,0 +1,443 @@ +"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 { moneyFormatter } from "@/app/utils/formatUtil" +import { + Button, ButtonGroup, Stack, + Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, + CardContent, Typography, Divider, Card +} from "@mui/material"; +import AddIcon from '@mui/icons-material/Add'; +import { deleteInvoice, updateInvoice } from "@/app/api/invoices/actions"; +import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts"; +import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList } 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 "./CreateExpenseModal"; +import { ProjectResult } from "@/app/api/projects"; + + + +interface Props { + invoices: invoiceList[]; + projects: ProjectResult[]; +} + +type InvoiceListError = { + [field in keyof invoiceList]?: string; +}; + +type invoiceListRow = Partial< + invoiceList & { + _isNew: boolean; + _error: InvoiceListError; + } +>; + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + + +const ExpenseSearch: React.FC = ({ invoices, projects }) => { + // console.log(invoices) + const { t } = useTranslation("expense"); + + + 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(() => { + setFilterInovices(invoices) + }, [invoices]); + + const [modelOpen, setModelOpen] = useState(false); + + const handleAddInvoiceClick = useCallback(() => { + setModelOpen(true) + },[]) + + const handleModalClose = useCallback(() => { + setModelOpen(false) + },[]) + + const showErrorDialog = (title: string, content: string) => { + errorDialogWithContent(title, content, t).then(() => { + window.location.reload(); + }); + }; + + const columns = useMemo[]>( + () => [ + { name: "invoiceNo", label: t("Invoice No") }, + { name: "projectCode", label: t("Project Code") }, + { name: "stage", label: t("Stage") }, + { name: "paymentMilestone", label: t("Payment Milestone") }, + { name: "invoiceDate", label: t("Invoice Date") }, + { name: "dueDate", label: t("Due Date") }, + { name: "issuedAmount", label: t("Amount (HKD)") }, + ], + [t], + ); + + const columns2 = useMemo[]>( + () => [ + { name: "invoiceNo", label: t("Invoice No") }, + { name: "projectCode", label: t("Project Code") }, + { name: "projectName", label: t("Project Name") }, + { name: "team", label: t("Team") }, + { name: "receiptDate", label: t("Receipt Date") }, + { name: "receivedAmount", label: t("Amount (HKD)") }, + ], + [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); + }; + + 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 () => { + await updateInvoice(selectedRow[0]) + setDialogOpen(false); + successDialog(t("Update Success"), t).then(() => { + window.location.reload() + }) + }; + + 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], + ); + + return ( + <> + + + + + + { + // 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))} + + + + + + + + items={filteredIvoices} + columns={combinedColumns} + autoRedirectToFirstPage + /> + + + {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; + }} + /> + + + + + + + + + + ); +}; + +export default ExpenseSearch; diff --git a/src/components/ExpenseSearch/ExpenseSearchLoading.tsx b/src/components/ExpenseSearch/ExpenseSearchLoading.tsx new file mode 100644 index 0000000..f52c41e --- /dev/null +++ b/src/components/ExpenseSearch/ExpenseSearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const ExpenseSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + Salary + + + + + + + + + + + ); +}; + +export default ExpenseSearchLoading; diff --git a/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx b/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx new file mode 100644 index 0000000..1cc2282 --- /dev/null +++ b/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import ExpenseSearch from "./ExpenseSearch"; +import ExpenseSearchLoading from "./ExpenseSearchLoading"; + + +interface SubComponents { + Loading: typeof ExpenseSearchLoading; +} + +const ExpenseSearchWrapper: React.FC & SubComponents = async () => { + + + return + +}; + +ExpenseSearchWrapper.Loading = ExpenseSearchLoading; + +export default ExpenseSearchWrapper; diff --git a/src/components/ExpenseSearch/ExpenseTable.tsx b/src/components/ExpenseSearch/ExpenseTable.tsx new file mode 100644 index 0000000..92cfae9 --- /dev/null +++ b/src/components/ExpenseSearch/ExpenseTable.tsx @@ -0,0 +1,341 @@ +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, Autocomplete, MenuItem } 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, + GridRenderEditCellParams, +} from "@mui/x-data-grid"; +import { useGridApiRef } from "@mui/x-data-grid"; +import StyledDataGrid from "../StyledDataGrid"; + +import { uniq } from "lodash"; +import CreateInvoiceModal from "./CreateExpenseModal"; +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"; +import { useFormContext } from "react-hook-form"; +import { ProjectResult } from "@/app/api/projects"; + +type InvoiceListError = { + [field in keyof invoiceList]?: string; +}; + +type invoiceListRow = Partial< + invoiceList & { + _isNew: boolean; + _error: InvoiceListError; + } +>; + +interface Props { + projects: ProjectResult[]; +} + +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); + } +} +type project = { + label: string; + value: number; +} +const InvoiceTable: React.FC = ({ projects }) => { + console.log(projects) + const { t } = useTranslation() + const [rowModesModel, setRowModesModel] = useState({}); + const [selectedRow, setSelectedRow] = useState([]); + const { getValues, setValue, clearErrors, setError } = + useFormContext(); + const apiRef = useGridApiRef(); + const [projectCode, setProjectCode] = useState({label: "", value: 0}) + 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; + }, + [], + ); + + const handleEditStop = useCallback>( + (params, event) => { + const row = apiRef.current.getRowWithUpdatedValues( + params.id, + "", + ) + console.log(validateRow(params.id) !== undefined) + console.log(!validateRow(params.id)) + if (validateRow(params.id) !== undefined && !validateRow(params.id)) { + + setRowModesModel((model) => ({ + ...model, + [params.id]: { mode: GridRowModes.View}, + })); + + console.log(row) + setSelectedRow((row) => [...row] as any[]) + event.defaultMuiPrevented = true; + }else{ + console.log(row) + const error = validateRow(params.id) + setSelectedRow((row) => { + const updatedRow = row.map(r => r.id === params.id ? { ...r, _error: error } : r); + return updatedRow; + }) + } + // 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 === originalRow.id ? rowToSave : e)) + ); + console.log(rowToSave) + return rowToSave; + }, + [validateRow], + ); + + /** + * Add callback to check error + */ + const onProcessRowUpdateError = useCallback( + (updateError: ProcessRowUpdateError) => { + const errors = updateError.errors; + const oldRow = updateError.row; + // console.log(errors) + apiRef.current.updateRows([{ ...oldRow, _error: errors }]); + }, + [apiRef] + ) + + useEffect(() => { + console.log(selectedRow) + setValue("data", selectedRow) + }, [selectedRow, setValue]); + + +const editCombinedColumns = useMemo( + () => [ + { field: "invoiceNo", headerName: t("Invoice No"), editable: true, flex: 0.5 }, + { field: "projectCode", + headerName: t("Project Code"), + editable: true, + flex: 0.3, + renderEditCell(params: GridRenderEditCellParams){ + return( + } + /> + + ) + + } + }, + { 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 diff --git a/src/components/ExpenseSearch/index.ts b/src/components/ExpenseSearch/index.ts new file mode 100644 index 0000000..eb5e82a --- /dev/null +++ b/src/components/ExpenseSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./ExpenseSearchWrapper"; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 8084eac..658dd02 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -36,6 +36,7 @@ import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; import FileUploadIcon from '@mui/icons-material/FileUpload'; import EmailIcon from "@mui/icons-material/Email"; +import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; import { IMPORT_INVOICE, @@ -199,6 +200,14 @@ const NavigationContent: React.FC = ({ abilities, username }) => { abilities!.includes(ability), ), }, + { + icon: , + label: "Expense", + path: "/expense", + isHidden: ![IMPORT_INVOICE, IMPORT_RECEIPT].some((ability) => + abilities!.includes(ability), + ), + }, { icon: , label: "Analysis Report",