Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

726 lignes
24 KiB

  1. "use client";
  2. import React, { useCallback, useMemo, useState } from "react";
  3. import SearchBox, { Criterion } from "../SearchBox";
  4. import { useTranslation } from "react-i18next";
  5. import SearchResults, { Column } from "../SearchResults";
  6. import EditNote from "@mui/icons-material/EditNote";
  7. import { moneyFormatter, timestampToDateStringV2 } from "@/app/utils/formatUtil"
  8. import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, TextField, CardContent, Typography, Divider, Card, ThemeProvider } from "@mui/material";
  9. import FileUploadIcon from '@mui/icons-material/FileUpload';
  10. import AddIcon from '@mui/icons-material/Add';
  11. import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoice } from "@/app/api/invoices/actions";
  12. import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts";
  13. import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices";
  14. import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
  15. import { GridCellParams, GridColDef, GridEventListener, GridRenderEditCellParams, GridRowId, GridRowModes, GridRowModesModel, GridValueFormatterParams } from "@mui/x-data-grid";
  16. import { useGridApiRef } from "@mui/x-data-grid";
  17. import StyledDataGrid from "../StyledDataGrid";
  18. import { uniq } from "lodash";
  19. import CreateInvoiceModal from "./CreateInvoiceModal";
  20. import { ProjectResult } from "@/app/api/projects";
  21. import { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware";
  22. import InvoiceSearchLoading from "./InvoiceSearchLoading";
  23. import { NumberFormatValues, NumericFormat } from "react-number-format";
  24. import theme from "@/theme";
  25. import { SMALL_ROW_THEME } from "@/theme/colorConst";
  26. interface CustomMoneyComponentProps {
  27. params: GridRenderEditCellParams;
  28. }
  29. interface Props {
  30. invoices: invoiceList[];
  31. projects: ProjectResult[];
  32. abilities: string[];
  33. }
  34. type InvoiceListError = {
  35. [field in keyof invoiceList]?: string;
  36. };
  37. type invoiceListRow = Partial<
  38. invoiceList & {
  39. _isNew: boolean;
  40. _error: InvoiceListError;
  41. }
  42. >;
  43. interface SubComponents {
  44. Loading: typeof InvoiceSearchLoading;
  45. }
  46. type SearchQuery = Partial<Omit<issuedInvoiceSearchForm, "id">>;
  47. type SearchParamNames = keyof SearchQuery;
  48. type SearchQuery2 = Partial<Omit<receivedInvoiceSearchForm, "id">>;
  49. type SearchParamNames2 = keyof SearchQuery2;
  50. const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, abilities }) => {
  51. console.log(uniq(invoices.map((invoice) => invoice.teamCodeName)))
  52. const { t } = useTranslation("Invoice");
  53. const [filteredIvoices, setFilterInovices] = useState(invoices);
  54. const apiRef = useGridApiRef();
  55. const [autoRedirectToFirstPage, setAutoRedirectToFirstPage] = useState(true);
  56. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  57. () => [
  58. { label: t("Invoice No"), paramName: "invoiceNo", type: "text" },
  59. { label: t("Project Code"), paramName: "projectCode", type: "text" },
  60. {
  61. label: t("Team"),
  62. paramName: "team",
  63. type: "select",
  64. options: uniq(invoices.map((invoice) => invoice.teamCodeName)),
  65. },
  66. { label: t("Issue Date"), label2: t("Issue Date To"), paramName: "invoiceDate", type: "dateRange" },
  67. { label: t("Settle Date"), label2: t("Settle Date To"), paramName: "dueDate", type: "dateRange" },
  68. {
  69. label: t("Settled"),
  70. paramName: "settled",
  71. type: "select",
  72. options: [t("Settled"), t("Unsettled")],
  73. },
  74. ],
  75. [t, invoices],
  76. );
  77. const onReset = useCallback(() => {
  78. // setFilteredIssuedInvoices(issuedInvoice);
  79. setAutoRedirectToFirstPage(() => true)
  80. setFilterInovices(invoices)
  81. }, [invoices]);
  82. function concatListOfObject(obj: any[]): string {
  83. return obj.map(obj => `Cannot find "${obj.paymentMilestone}" in ${obj.invoiceNo}`).join(", ")
  84. }
  85. function concatListOfObject2(obj: any[]): string {
  86. return obj.map(obj => `"${obj.projectCode}" does not match with ${obj.invoicesNo}`).join(", ")
  87. }
  88. const handleImportClick = useCallback(async (event: any) => {
  89. try {
  90. const file = event.target.files[0];
  91. if (!file) {
  92. console.log('No file selected');
  93. return;
  94. }
  95. if (file.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
  96. console.log('Invalid file format. Only XLSX files are allowed.');
  97. return;
  98. }
  99. setLoading(true)
  100. const formData = new FormData();
  101. formData.append('multipartFileList', file);
  102. const response = await importIssuedInovice(formData);
  103. // response: status, message, projectList, emptyRowList, invoiceList
  104. if (response.status) {
  105. successDialog(t("Import Success"), t).then(() => {
  106. setLoading(false)
  107. window.location.reload();
  108. });
  109. } else {
  110. handleImportError(response);
  111. setLoading(false)
  112. }
  113. } catch (err) {
  114. console.log(err);
  115. return false;
  116. }
  117. }, []);
  118. const [modelOpen, setModelOpen] = useState<boolean>(false);
  119. const handleAddInvoiceClick = useCallback(() => {
  120. setModelOpen(true)
  121. },[])
  122. const handleModalClose = useCallback(() => {
  123. setModelOpen(false)
  124. },[])
  125. const handleImportError = (response: any) => {
  126. if (response.emptyRowList.length >= 1) {
  127. showErrorDialog(
  128. t("Import Fail"),
  129. t(`Please fill the mandatory field at Row <br> ${response.emptyRowList.join(", ")}`)
  130. );
  131. } else if (response.projectList.length >= 1) {
  132. showErrorDialog(
  133. t("Import Fail"),
  134. t(`Please check the corresponding project code <br> ${response.projectList.join(", ")}`)
  135. );
  136. } else if (response.invoiceList.length >= 1) {
  137. showErrorDialog(
  138. t("Import Fail"),
  139. t(`Please check the corresponding Invoice No. The invoice is imported. <br>`) + `${response.invoiceList.join(", ")}`
  140. );
  141. } else if (response.duplicateItem.length >= 1) {
  142. showErrorDialog(
  143. t("Import Fail"),
  144. t(`Please check the corresponding Invoice No. The below invoice has duplicated number. <br>`)+ `${response.duplicateItem.join(", ")}`
  145. );
  146. } else if (response.paymentMilestones.length >= 1) {
  147. showErrorDialog(
  148. t("Import Fail"),
  149. t(`The payment milestone does not match with records. Please check the corresponding Invoice No. <br>`) + `${concatListOfObject(response.paymentMilestones)}`
  150. );
  151. }
  152. };
  153. const showErrorDialog = (title: string, content: string) => {
  154. errorDialogWithContent(title, content, t).then(() => {
  155. window.location.reload();
  156. });
  157. };
  158. const handleRecImportClick = useCallback(async (event:any) => {
  159. try {
  160. const file = event.target.files[0];
  161. if (!file) {
  162. console.log('No file selected');
  163. return;
  164. }
  165. if (file.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
  166. console.log('Invalid file format. Only XLSX files are allowed.');
  167. return;
  168. }
  169. const formData = new FormData();
  170. formData.append('multipartFileList', file);
  171. setLoading(true)
  172. const response = await importReceivedInovice(formData)
  173. console.log(response)
  174. if (response.status) {
  175. successDialog(t("Import Success"), t).then(() => {
  176. window.location.reload()
  177. })
  178. setLoading(false)
  179. }else{
  180. if (response.emptyRowList.length >= 1){
  181. errorDialogWithContent(t("Import Fail"),
  182. t(`Please fill the mandatory field at Row <br> ${response.emptyRowList.join(", ")}`), t)
  183. .then(() => {
  184. window.location.reload()
  185. })
  186. }
  187. else if (response.projectList.length >= 1){
  188. errorDialogWithContent(t("Import Fail"),
  189. t(`Please check the corresponding project code <br> ${response.projectList.join(", ")}`), t)
  190. .then(() => {
  191. // window.location.reload()
  192. })
  193. }
  194. else if (response.invoiceList.length >= 1){
  195. errorDialogWithContent(t("Import Fail"),
  196. t(`Please check the corresponding Invoice No. The invoice has not yet issued. <br>`)+ `${response.invoiceList.join(", ")}`, t)
  197. .then(() => {
  198. window.location.reload()
  199. })
  200. }
  201. else if (response.duplicateItem.length >= 1){
  202. errorDialogWithContent(t("Import Fail"),
  203. t(`Please check the corresponding Invoice No. The below invoice has duplicated number. <br>`)+ `${response.duplicateItem.join(", ")}`, t)
  204. .then(() => {
  205. window.location.reload()
  206. })
  207. }else if (response.paymentMilestones.length >= 1){
  208. errorDialogWithContent(t("Import Fail"),
  209. t(`The payment milestone does not match with records. Please check the corresponding Invoice No. <br>`)+ `${concatListOfObject2(response.paymentMilestones)}`, t)
  210. .then(() => {
  211. window.location.reload()
  212. })
  213. }
  214. setLoading(false)
  215. }
  216. }catch(error){
  217. console.log(error)
  218. }
  219. }, []);
  220. const [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]);
  221. const [dialogOpen, setDialogOpen] = useState(false);
  222. const handleButtonClick = (row: invoiceList) => {
  223. console.log(row)
  224. setSelectedRow([row]);
  225. setDialogOpen(true);
  226. setRowModesModel((model) => ({
  227. ...model,
  228. [row.id]: { mode: GridRowModes.Edit, fieldToFocus: "issuedAmount" },
  229. }));
  230. };
  231. const handleCloseDialog = () => {
  232. setDialogOpen(false);
  233. };
  234. const handleDeleteInvoice = useCallback(() => {
  235. deleteDialog(async() => {
  236. //console.log(selectedRow[0])
  237. await deleteInvoice(selectedRow[0].id!!)
  238. setDialogOpen(false);
  239. const result = await successDialog("Delete Success", t);
  240. if (result) {
  241. window.location.reload()
  242. }
  243. }, t)
  244. }, [selectedRow]);
  245. const handleSaveDialog = async () => {
  246. // setDialogOpen(false);
  247. const response = await updateInvoice(selectedRow[0])
  248. setDialogOpen(false);
  249. setAutoRedirectToFirstPage(() => false)
  250. successDialog(t("Update Success"), t).then(() => {
  251. // window.location.reload()
  252. setFilterInovices((prev) => {
  253. const tempInvoices = [...prev]
  254. const invoiceId = tempInvoices.findIndex((invoice) => invoice.id === response.id)
  255. if (invoiceId >= 0) {
  256. const updatedInvoice = tempInvoices[invoiceId]
  257. updatedInvoice.invoiceNo = response?.invoiceNo
  258. updatedInvoice.issuedAmount = response?.issuedAmount
  259. updatedInvoice.issuedDate = timestampToDateStringV2(response.issuedDate)!!
  260. updatedInvoice.receiptDate = timestampToDateStringV2(response.receiptDate)!!
  261. updatedInvoice.receivedAmount = response?.receivedAmount
  262. tempInvoices[invoiceId] = updatedInvoice
  263. }
  264. return tempInvoices
  265. })
  266. })
  267. // console.log(selectedRow[0])
  268. // setSelectedRow([]);
  269. };
  270. // Money format : 000,000,000.00
  271. const CustomMoneyFormat = useCallback((value: number) => {
  272. if (value) {
  273. return moneyFormatter.format(value);
  274. } else {
  275. return ""
  276. }
  277. }, [])
  278. const CustomMoneyComponent: React.FC<CustomMoneyComponentProps> = ({ params }) => {
  279. const { id, value, field } = params;
  280. const ref = React.useRef();
  281. const handleValueChange = (newValue: NumberFormatValues) => {
  282. apiRef.current.setEditCellValue({ id, field, value: newValue.value });
  283. };
  284. return <NumericFormat
  285. fullWidth
  286. prefix="HK$"
  287. onValueChange={(values) => {
  288. console.log(values)
  289. handleValueChange(values)
  290. }}
  291. customInput={TextField}
  292. thousandSeparator
  293. valueIsNumericString
  294. decimalScale={2}
  295. fixedDecimalScale
  296. value={value}
  297. inputRef={ref}
  298. InputProps={{
  299. sx: {
  300. '& .MuiInputBase-input': {
  301. textAlign: 'right',
  302. mb: 2
  303. }
  304. }
  305. }}
  306. />;
  307. }
  308. const combinedColumns = useMemo<Column<invoiceList>[]>(
  309. () => [
  310. {
  311. name: "invoiceNo",
  312. label: t("Edit"),
  313. onClick: (row: invoiceList) => (
  314. handleButtonClick(row)
  315. ),
  316. buttonIcon: <EditOutlinedIcon />
  317. },
  318. { name: "invoiceNo", label: t("Invoice No") },
  319. { name: "projectCode", label: t("Project Code") },
  320. { name: "projectName", label: t("Project Name") },
  321. { name: "team", label: t("Team") },
  322. { name: "issuedDate", label: t("Issue Date") },
  323. { name: "issuedAmount", label: t("Amount (HKD)"), type: 'money', needTranslation: true },
  324. { name: "receiptDate", label: t("Settle Date") },
  325. { name: "receivedAmount", label: t("Actual Received Amount (HKD)"), type: 'money', needTranslation: true },
  326. ],
  327. [t]
  328. )
  329. const editCombinedColumns = useMemo<GridColDef[]>(
  330. () => [
  331. { field: "invoiceNo", headerName: t("Invoice No"), editable: true, flex: 0.5 },
  332. { field: "projectCode", headerName: t("Project Code"), editable: false, flex: 0.3 },
  333. { field: "projectName", headerName: t("Project Name"), flex: 1 },
  334. { field: "team", headerName: t("Team"), flex: 0.4 },
  335. { field: "issuedDate",
  336. headerName: t("Issue Date"),
  337. editable: true,
  338. flex: 0.4,
  339. // type: 'date',
  340. // valueGetter: (params) => {
  341. // // console.log(params.row.issuedDate)
  342. // return new Date(params.row.issuedDate)
  343. // },
  344. },
  345. { field: "issuedAmount",
  346. headerName: t("Amount (HKD)"),
  347. editable: true,
  348. flex: 0.5,
  349. type: 'number',
  350. renderEditCell: (params: GridRenderEditCellParams) => {
  351. return <CustomMoneyComponent params={params} />
  352. },
  353. valueFormatter: (params: GridValueFormatterParams) => {
  354. return CustomMoneyFormat(params.value as number)
  355. }
  356. },
  357. {
  358. field: "receiptDate",
  359. headerName: t("Settle Date"),
  360. editable: true,
  361. flex: 0.4,
  362. // renderCell: (params) => {
  363. // console.log(params)
  364. // return (
  365. // <LocalizationProvider dateAdapter={AdapterDayjs}>
  366. // <DatePicker
  367. // value={dayjs(params.value)}
  368. // />
  369. // </LocalizationProvider>
  370. // );
  371. // }
  372. },
  373. { field: "receivedAmount",
  374. headerName: t("Actual Received Amount (HKD)"),
  375. editable: true,
  376. flex: 0.5,
  377. type: 'number',
  378. renderEditCell: (params: GridRenderEditCellParams) => {
  379. return <CustomMoneyComponent params={params} />
  380. },
  381. valueFormatter: (params: GridValueFormatterParams) => {
  382. return CustomMoneyFormat(params.value as number)
  383. }
  384. },
  385. ],
  386. [t]
  387. )
  388. function convertDateFormat(dateString: string) {
  389. // Split the input date string by '/'
  390. const parts = dateString.split('/');
  391. // Extract day, month, and year
  392. const day = parts[0];
  393. const month = parts[1];
  394. const year = parts[2];
  395. // Return the formatted date string
  396. return `${year}-${month}-${day}`;
  397. }
  398. function isDateInRange(dateToCheck: string, startDate: string, endDate: string, dash: boolean): boolean {
  399. if ((!startDate || startDate === "Invalid Date") && (!endDate || endDate === "Invalid Date")) {
  400. return true;
  401. }
  402. let tempDateToCheckObj
  403. if(dash){
  404. tempDateToCheckObj = new Date(dateToCheck)
  405. }else{
  406. tempDateToCheckObj = new Date(convertDateFormat(dateToCheck))
  407. }
  408. const dateToCheckObj = tempDateToCheckObj;
  409. const startDateObj = new Date(startDate);
  410. const endDateObj = new Date(endDate);
  411. return ((!startDate || startDate === "Invalid Date") || dateToCheckObj >= startDateObj) && ((!endDate || endDate === "Invalid Date") || dateToCheckObj <= endDateObj);
  412. }
  413. const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
  414. const validateInvoiceEntry = (
  415. entry: Partial<invoiceList>,
  416. ): InvoiceListError | undefined => {
  417. // Test for errors
  418. const error: InvoiceListError = {};
  419. console.log(entry)
  420. if (!entry.issuedAmount) {
  421. error.issuedAmount = "Please input issued amount ";
  422. } else if (!entry.issuedAmount) {
  423. error.receivedAmount = "Please input received amount";
  424. } else if (entry.invoiceNo === "") {
  425. error.invoiceNo = "Please input invoice number";
  426. } else if (!entry.issuedDate) {
  427. error.issuedDate = "Please input issue date";
  428. } else if (!entry.receiptDate){
  429. error.receiptDate = "Please input receipt date";
  430. }
  431. return Object.keys(error).length > 0 ? error : undefined;
  432. }
  433. const validateRow = useCallback(
  434. (id: GridRowId) => {
  435. const row = apiRef.current.getRowWithUpdatedValues(
  436. id,
  437. "",
  438. )
  439. const error = validateInvoiceEntry(row);
  440. console.log(error)
  441. // Test for warnings
  442. apiRef.current.updateRows([{ id, _error: error }]);
  443. return !error;
  444. },
  445. [apiRef],
  446. );
  447. const handleEditStop = useCallback<GridEventListener<"rowEditStop">>(
  448. (params, event) => {
  449. // console.log(params.id)
  450. if (validateRow(params.id) !== undefined || !validateRow(params.id)) {
  451. const row = apiRef.current.getRowWithUpdatedValues(
  452. params.id,
  453. "",
  454. )
  455. console.log(row)
  456. setSelectedRow(() => [{...row}] as invoiceList[])
  457. setRowModesModel((model) => ({
  458. ...model,
  459. [params.id]: { mode: GridRowModes.View},
  460. }));
  461. event.defaultMuiPrevented = true;
  462. }
  463. // console.log(row)
  464. },
  465. [validateRow],
  466. );
  467. const isAddInvoiceRightExist = () => {
  468. const importRight = [IMPORT_INVOICE, IMPORT_RECEIPT].some((ability) => abilities.includes(ability))
  469. return importRight
  470. }
  471. const [loading, setLoading] = React.useState(false);
  472. return (
  473. <>
  474. {loading && <InvoiceSearch.Loading />}
  475. {
  476. !loading &&
  477. isAddInvoiceRightExist() &&
  478. <Stack
  479. direction="row"
  480. justifyContent="right"
  481. flexWrap="wrap"
  482. spacing={2}
  483. >
  484. <ButtonGroup variant="contained">
  485. <Button
  486. startIcon={<AddIcon />}
  487. variant="outlined"
  488. component="label"
  489. onClick={handleAddInvoiceClick}
  490. >
  491. {t("Create Invoice")}
  492. </Button>
  493. <Button startIcon={<FileUploadIcon />} variant="contained" component="label">
  494. <input
  495. id='importExcel'
  496. type='file'
  497. accept='.xlsx, .csv'
  498. hidden
  499. onChange={(event) => {handleImportClick(event)}}
  500. />
  501. {t("Import Invoice Issue Summary")}
  502. </Button>
  503. <Button startIcon={<FileUploadIcon />} component="label" variant="contained">
  504. <input
  505. id='importExcel'
  506. type='file'
  507. accept='.xlsx, .csv'
  508. hidden
  509. onChange={(event) => {handleRecImportClick(event)}}
  510. />
  511. {t("Import Invoice Amount Receive Summary")}
  512. </Button>
  513. </ButtonGroup>
  514. </Stack>
  515. }
  516. {
  517. !loading &&
  518. // tabIndex == 0 &&
  519. <SearchBox
  520. criteria={searchCriteria}
  521. onSearch={(query) => {
  522. // console.log(query.team)
  523. // console.log(invoices[0].team)
  524. setFilterInovices(
  525. invoices.filter(
  526. (s) => (s.invoiceNo.toLowerCase().includes(query.invoiceNo.toLowerCase()))
  527. && (s.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()))
  528. && (query.team === "All" || query.team.includes(s.team))
  529. && (isDateInRange(s.issuedDate, query.invoiceDate ?? undefined, query.invoiceDateTo ?? undefined, false))
  530. && (isDateInRange(s.receiptDate, query.dueDate ?? undefined, query.dueDateTo ?? undefined, false))
  531. && (query.settled === "All" || (query.settled === t("Settled")) === s.settled)
  532. ),
  533. );
  534. setAutoRedirectToFirstPage(() => true)
  535. }}
  536. onReset={onReset}
  537. />
  538. }
  539. {
  540. !loading &&
  541. <>
  542. <Divider sx={{ paddingBlockStart: 2 }} />
  543. <Card sx={{ display: "block" }}>
  544. <CardContent>
  545. <Stack direction="row" justifyContent="space-between">
  546. <Typography variant="h6">{t('Total Issued Amount (HKD)')}:</Typography>
  547. <Typography variant="h6">{moneyFormatter.format(filteredIvoices.reduce((acc, current) => (acc + current.issuedAmount), 0))}</Typography>
  548. </Stack>
  549. <Stack direction="row" justifyContent="space-between">
  550. <Typography variant="h6">{t('Total Received Amount (HKD)')}:</Typography>
  551. <Typography variant="h6">{moneyFormatter.format(filteredIvoices.reduce((acc, current) => (acc + current.receivedAmount), 0))}</Typography>
  552. </Stack>
  553. </CardContent>
  554. </Card>
  555. <Divider sx={{ paddingBlockEnd: 2 }} />
  556. </>
  557. }
  558. {
  559. !loading &&
  560. // tabIndex == 0 &&
  561. <ThemeProvider theme={SMALL_ROW_THEME}>
  562. <SearchResults<invoiceList>
  563. items={filteredIvoices}
  564. columns={combinedColumns}
  565. autoRedirectToFirstPage={autoRedirectToFirstPage}
  566. />
  567. </ThemeProvider>
  568. }
  569. <Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="lg" fullWidth>
  570. <DialogTitle>{t("Edit Invoice")}</DialogTitle>
  571. <DialogContent>
  572. <DialogContentText>
  573. {t("You can edit the invoice details here.")}
  574. </DialogContentText>
  575. <StyledDataGrid
  576. apiRef={apiRef}
  577. autoHeight
  578. sx={{
  579. "--DataGrid-overlayHeight": "100px",
  580. ".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
  581. border: "1px solid",
  582. borderColor: "error.main",
  583. },
  584. ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
  585. border: "1px solid",
  586. borderColor: "warning.main",
  587. },
  588. '& .MuiDataGrid-columnHeaderTitle': {
  589. whiteSpace: 'normal',
  590. textWrap: 'pretty',
  591. textAlign: 'center',
  592. },
  593. '.MuiDataGrid-row:not(.MuiDataGrid-row--dynamicHeight)>.MuiDataGrid-cell': {
  594. overflow: 'auto',
  595. whiteSpace: 'nowrap',
  596. textWrap: 'pretty',
  597. },
  598. width: "100%", // Make the DataGrid wider
  599. }}
  600. disableColumnMenu
  601. editMode="row"
  602. rows={selectedRow}
  603. rowModesModel={rowModesModel}
  604. onRowModesModelChange={setRowModesModel}
  605. onRowEditStop={handleEditStop}
  606. columns={editCombinedColumns}
  607. getCellClassName={(params: GridCellParams<invoiceListRow>) => {
  608. let classname = "";
  609. if (params.row._error?.[params.field as keyof invoiceList]) {
  610. classname = "hasError";
  611. }
  612. return classname;
  613. }}
  614. />
  615. </DialogContent>
  616. <DialogActions>
  617. <Button onClick={handleDeleteInvoice} color="error">
  618. {t("Delete")}
  619. </Button>
  620. <Button onClick={handleCloseDialog} color="primary">
  621. {t("Cancel")}
  622. </Button>
  623. <Button
  624. onClick={handleSaveDialog}
  625. color="primary"
  626. disabled={
  627. Object.values(rowModesModel).some((mode) => mode.mode === GridRowModes.Edit) ||
  628. selectedRow.some((row) => row._error)
  629. }
  630. >
  631. {t("Save")}
  632. </Button>
  633. </DialogActions>
  634. </Dialog>
  635. <CreateInvoiceModal
  636. isOpen={modelOpen}
  637. onClose={handleModalClose}
  638. projects={projects}
  639. invoices={invoices}
  640. />
  641. </>
  642. );
  643. };
  644. InvoiceSearch.Loading = InvoiceSearchLoading;
  645. export default InvoiceSearch;