|
|
@@ -1,443 +1,144 @@ |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import React, { useCallback, useMemo, useState } from "react"; |
|
|
|
import SearchBox, { Criterion } from "../SearchBox"; |
|
|
|
import { ExpensesResult } from "@/app/api/expenses"; |
|
|
|
import { useCallback, useMemo, useState } from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import SearchBox, { Criterion } from "../SearchBox"; |
|
|
|
import SearchResults, { Column } from "../SearchResults"; |
|
|
|
import { moneyFormatter } from "@/app/utils/formatUtil" |
|
|
|
import { |
|
|
|
Button, ButtonGroup, Stack, |
|
|
|
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, |
|
|
|
CardContent, Typography, Divider, Card |
|
|
|
import { useRouter } from "next/navigation"; |
|
|
|
import { |
|
|
|
Button, |
|
|
|
ButtonGroup, |
|
|
|
Card, |
|
|
|
CardContent, |
|
|
|
Divider, |
|
|
|
Grid, |
|
|
|
Stack, |
|
|
|
Typography, |
|
|
|
} from "@mui/material"; |
|
|
|
import { moneyFormatter } from "@/app/utils/formatUtil"; |
|
|
|
import { EditNote } from "@mui/icons-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[]; |
|
|
|
expenses: ExpensesResult[] |
|
|
|
} |
|
|
|
|
|
|
|
type InvoiceListError = { |
|
|
|
[field in keyof invoiceList]?: string; |
|
|
|
}; |
|
|
|
|
|
|
|
type invoiceListRow = Partial< |
|
|
|
invoiceList & { |
|
|
|
_isNew: boolean; |
|
|
|
_error: InvoiceListError; |
|
|
|
} |
|
|
|
>; |
|
|
|
|
|
|
|
type SearchQuery = Partial<Omit<issuedInvoiceSearchForm, "id">>; |
|
|
|
type SearchQuery = Partial<Omit<ExpensesResult, "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 ExpenseSearch: React.FC<Props> = ({ expenses }) => { |
|
|
|
console.log(expenses) |
|
|
|
const router = useRouter(); |
|
|
|
const { t } = useTranslation("expenses"); |
|
|
|
const [filteredExpenses, setFilteredExpenses] = useState(expenses); |
|
|
|
|
|
|
|
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( |
|
|
|
() => [ |
|
|
|
{ label: t("Invoice No"), paramName: "invoiceNo", type: "text" }, |
|
|
|
// { label: t("Expense No"), paramName: "ExpenseNo", type: "text" }, |
|
|
|
{ label: t("Project Code"), paramName: "projectCode", type: "text" }, |
|
|
|
{ label: t("Project Name"), paramName: "projectName", type: "text" }, |
|
|
|
{ |
|
|
|
label: t("Team"), |
|
|
|
paramName: "team", |
|
|
|
type: "select", |
|
|
|
options: uniq(invoices.map((invoice) => invoice.teamCodeName)), |
|
|
|
label: t("Verified Date"), |
|
|
|
label2: t("Verified Date To"), |
|
|
|
paramName: "verifiedDatetime", |
|
|
|
type: "dateRange", |
|
|
|
}, |
|
|
|
{ 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 onExpenseClick = useCallback( |
|
|
|
(expenses?: ExpensesResult) => {}, |
|
|
|
[router] |
|
|
|
); |
|
|
|
|
|
|
|
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>[]>( |
|
|
|
const columns = useMemo<Column<ExpensesResult>[]>( |
|
|
|
() => [ |
|
|
|
{ |
|
|
|
name: "invoiceNo", |
|
|
|
label: t("Edit"), |
|
|
|
onClick: (row: invoiceList) => ( |
|
|
|
handleButtonClick(row) |
|
|
|
), |
|
|
|
buttonIcon: <EditOutlinedIcon /> |
|
|
|
{ |
|
|
|
name: "id", |
|
|
|
label: t("Details"), |
|
|
|
onClick: onExpenseClick, |
|
|
|
buttonIcon: <EditNote />, |
|
|
|
// disabled: !abilities.includes(MAINTAIN_PROJECT), |
|
|
|
}, |
|
|
|
{ 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' |
|
|
|
}, |
|
|
|
{ name: "verifiedDatetime", label: t("verifiedDatetime") }, |
|
|
|
], |
|
|
|
[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], |
|
|
|
[t, onExpenseClick] |
|
|
|
); |
|
|
|
const onReset = useCallback(() => { |
|
|
|
// setFilteredExpenses(); |
|
|
|
}, []); |
|
|
|
|
|
|
|
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} |
|
|
|
/> |
|
|
|
} |
|
|
|
|
|
|
|
<Stack |
|
|
|
direction="row" |
|
|
|
justifyContent="right" |
|
|
|
flexWrap="wrap" |
|
|
|
spacing={2} |
|
|
|
> |
|
|
|
<Grid xs={12} justifyContent="right"> |
|
|
|
<ButtonGroup variant="contained"> |
|
|
|
<Button |
|
|
|
startIcon={<AddIcon />} |
|
|
|
variant="outlined" |
|
|
|
component="label" |
|
|
|
onClick={() => console.log()} |
|
|
|
> |
|
|
|
{t("Create expense")} |
|
|
|
</Button> |
|
|
|
</ButtonGroup> |
|
|
|
</Grid> |
|
|
|
</Stack> |
|
|
|
<SearchBox |
|
|
|
criteria={searchCriteria} |
|
|
|
onSearch={(query) => { |
|
|
|
// setFilteredExpenses( |
|
|
|
// projects.filter( |
|
|
|
// (p) => |
|
|
|
// p.code.toLowerCase().includes(query.code.toLowerCase()) && |
|
|
|
// p.name.toLowerCase().includes(query.name.toLowerCase()) && |
|
|
|
// (query.client === "All" || p.client === query.client) && |
|
|
|
// (query.category === "All" || p.category === query.category) && |
|
|
|
// // (query.team === "All" || p.team === query.team) && |
|
|
|
// (query.team === "All" || query.team.toLowerCase().includes(p.team.toLowerCase())) && |
|
|
|
// (query.status === "All" || p.status === query.status), |
|
|
|
// ), |
|
|
|
// ); |
|
|
|
}} |
|
|
|
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> |
|
|
|
<Stack direction="row" justifyContent="space-between"> |
|
|
|
<Typography variant="h6"> |
|
|
|
{t("Total Issued Amount (HKD)")}: |
|
|
|
</Typography> |
|
|
|
<Typography variant="h6"> |
|
|
|
{/* {moneyFormatter.format(filteredExpenses.reduce((acc, curr) => (acc + curr.issuedAmount), 0))} */} |
|
|
|
</Typography> |
|
|
|
</Stack> |
|
|
|
<Stack direction="row" justifyContent="space-between"> |
|
|
|
<Typography variant="h6"> |
|
|
|
{t("Total Received Amount (HKD)")}: |
|
|
|
</Typography> |
|
|
|
<Typography variant="h6"> |
|
|
|
{/* {moneyFormatter.format(filteredExpenses.reduce((acc, curr) => (acc + curr.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; |
|
|
|
}} |
|
|
|
<SearchResults<ExpensesResult> |
|
|
|
items={filteredExpenses} |
|
|
|
columns={columns} |
|
|
|
/> |
|
|
|
</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} |
|
|
|
/> |
|
|
|
</> |
|
|
|
</Stack> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
export default ExpenseSearch; |