Bladeren bron

expense

tags/Baseline_180220205_Frontend
MSI\2Fi 11 maanden geleden
bovenliggende
commit
a23c71ec69
10 gewijzigde bestanden met toevoegingen van 1004 en 2 verwijderingen
  1. +30
    -0
      src/app/(main)/expense/page.tsx
  2. +27
    -0
      src/app/(main)/invoice/new/[projectId]/page.tsx
  3. +4
    -2
      src/app/(main)/invoice/page.tsx
  4. +87
    -0
      src/components/ExpenseSearch/CreateExpenseModal.tsx
  5. +443
    -0
      src/components/ExpenseSearch/ExpenseSearch.tsx
  6. +40
    -0
      src/components/ExpenseSearch/ExpenseSearchLoading.tsx
  7. +22
    -0
      src/components/ExpenseSearch/ExpenseSearchWrapper.tsx
  8. +341
    -0
      src/components/ExpenseSearch/ExpenseTable.tsx
  9. +1
    -0
      src/components/ExpenseSearch/index.ts
  10. +9
    -0
      src/components/NavigationContent/NavigationContent.tsx

+ 30
- 0
src/app/(main)/expense/page.tsx Bestand weergeven

@@ -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(

<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Expense")}
</Typography>
<Suspense fallback={<ExpenseSearch.Loading />}>
<I18nProvider namespaces={["expense", "common"]}>
<ExpenseSearch />
</I18nProvider>
</Suspense>
</Stack>

)
}

export default Expense

+ 27
- 0
src/app/(main)/invoice/new/[projectId]/page.tsx Bestand weergeven

@@ -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 (
<>
<Typography variant="h4">{t("Create Invoice")}</Typography>
<I18nProvider namespaces={["invoice"]}>
<CreateInvoice />
</I18nProvider>
</>
)
};

export default Invoice;

+ 4
- 2
src/app/(main)/invoice/page.tsx Bestand weergeven

@@ -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 () => {
</Button> */}
</Stack>
<Suspense fallback={<InvoiceSearch.Loading />}>
<InvoiceSearch />
<I18nProvider namespaces={["Invoice", "common"]}>
<InvoiceSearch />
</I18nProvider>
</Suspense>
</>
)


+ 87
- 0
src/components/ExpenseSearch/CreateExpenseModal.tsx Bestand weergeven

@@ -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<Props> = ({isOpen, onClose, projects}) => {
const { t } = useTranslation()
const formProps = useForm<any>();

const onSubmit = useCallback<SubmitHandler<any>>(
(data) => {
console.log(data)
}
, [])

return (
<FormProvider {...formProps}>
<Modal
open={isOpen}
onClose={onClose}
>
<Card sx={modalSx}>
<CardContent
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Invoice Creation")}
</Typography>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
marginBlock: 2,
}}
>
<InvoiceTable projects={projects}/>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={onClose}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Save")}
</Button>
</CardActions>
</CardContent>
</Card>
</Modal>
</FormProvider>
);
};

export default CreateInvoiceModal;

+ 443
- 0
src/components/ExpenseSearch/ExpenseSearch.tsx Bestand weergeven

@@ -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<Omit<issuedInvoiceSearchForm, "id">>;
type SearchParamNames = keyof SearchQuery;


const ExpenseSearch: React.FC<Props> = ({ invoices, projects }) => {
// console.log(invoices)
const { t } = useTranslation("expense");


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(() => {
setFilterInovices(invoices)
}, [invoices]);

const [modelOpen, setModelOpen] = useState<boolean>(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<Column<issuedInvoiceList>[]>(
() => [
{ 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<Column<receivedInvoiceList>[]>(
() => [
{ 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<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 () => {
await updateInvoice(selectedRow[0])
setDialogOpen(false);
successDialog(t("Update Success"), t).then(() => {
window.location.reload()
})
};

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],
);

return (
<>
<Stack
direction="row"
justifyContent="right"
flexWrap="wrap"
spacing={2}
>
<ButtonGroup variant="contained">
<Button
startIcon={<AddIcon />}
variant="contained"
component="label"
onClick={handleAddInvoiceClick}
>
{t("Create Expense")}
</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 }} />

<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>
{/* <SearchResults<invoiceList>
items={selectedRow ? [selectedRow] : []}
columns={editCombinedColumns}
/> */}
<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}
/>
</>
);
};

export default ExpenseSearch;

+ 40
- 0
src/components/ExpenseSearch/ExpenseSearchLoading.tsx Bestand weergeven

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>Salary
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default ExpenseSearchLoading;

+ 22
- 0
src/components/ExpenseSearch/ExpenseSearchWrapper.tsx Bestand weergeven

@@ -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 <ExpenseSearch
invoices={[]}
projects={[]}
/>
};

ExpenseSearchWrapper.Loading = ExpenseSearchLoading;

export default ExpenseSearchWrapper;

+ 341
- 0
src/components/ExpenseSearch/ExpenseTable.tsx Bestand weergeven

@@ -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<Props> = ({ projects }) => {
console.log(projects)
const { t } = useTranslation()
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
const [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]);
const { getValues, setValue, clearErrors, setError } =
useFormContext<any>();
const apiRef = useGridApiRef();
const [projectCode, setProjectCode] = useState<project>({label: "", value: 0})
const validateInvoiceEntry = (
entry: Partial<invoiceList>,
): 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<GridEventListener<"rowEditStop">>(
(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<invoiceListRow>,
originalRow: GridRowModel<invoiceListRow>,
) => {
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<GridColDef[]>(
() => [
{ 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<invoiceListRow, number>){
return(
<Autocomplete
disablePortal
options={[]}
sx={{width: '100%'}}
renderInput={(params) => <TextField {...params} />}
/>

)
}
},
{ 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 = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Create Invoice")}
</Button>
</Box>
);

return (
<>
<StyledDataGrid
apiRef={apiRef}
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",
},
height: 400, width: '95%'
}}
disableColumnMenu
editMode="row"
rows={selectedRow}
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleEditStop}
columns={editCombinedColumns}
processRowUpdate={processRowUpdate}
onProcessRowUpdateError={onProcessRowUpdateError}
getCellClassName={(params: GridCellParams<invoiceListRow>) => {
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 (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some time entries!")}</Typography>
</Box>
);
};
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};

+ 1
- 0
src/components/ExpenseSearch/index.ts Bestand weergeven

@@ -0,0 +1 @@
export { default } from "./ExpenseSearchWrapper";

+ 9
- 0
src/components/NavigationContent/NavigationContent.tsx Bestand weergeven

@@ -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<Props> = ({ abilities, username }) => {
abilities!.includes(ability),
),
},
{
icon: <RequestQuoteIcon />,
label: "Expense",
path: "/expense",
isHidden: ![IMPORT_INVOICE, IMPORT_RECEIPT].some((ability) =>
abilities!.includes(ability),
),
},
{
icon: <Analytics />,
label: "Analysis Report",


Laden…
Annuleren
Opslaan