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")}
+
+
+
+
+
+ }
+ onClick={onClose}
+ >
+ {t("Cancel")}
+
+ } type="submit">
+ {t("Save")}
+
+
+
+
+
+
+ );
+};
+
+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 (
+ <>
+
+
+ }
+ variant="contained"
+ component="label"
+ onClick={handleAddInvoiceClick}
+ >
+ {t("Create Expense")}
+
+
+
+ {
+ // 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
+ />
+
+
+
+ >
+ );
+};
+
+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 = (
+
+ }
+ onClick={addRow}
+ size="small"
+ >
+ {t("Create Invoice")}
+
+
+);
+
+ 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",