您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

ExpenseTable.tsx 12 KiB

1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
1年前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. import React, { useCallback, useMemo, useState, useEffect } from "react";
  2. import SearchBox, { Criterion } from "../SearchBox";
  3. import { useTranslation } from "react-i18next";
  4. import SearchResults, { Column } from "../SearchResults";
  5. import EditNote from "@mui/icons-material/EditNote";
  6. import { moneyFormatter } from "@/app/utils/formatUtil"
  7. import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, TextField, CardContent, Typography, Divider, Card, Box, Autocomplete, MenuItem } from "@mui/material";
  8. import FileUploadIcon from '@mui/icons-material/FileUpload';
  9. import { Add, Check, Close, Delete } from "@mui/icons-material";
  10. import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices";
  11. import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
  12. import {
  13. GridCellParams,
  14. GridColDef,
  15. GridEventListener,
  16. GridRowId,
  17. GridRowModel,
  18. GridRowModes,
  19. GridRowModesModel,
  20. GridRenderEditCellParams,
  21. useGridApiContext,
  22. } from "@mui/x-data-grid";
  23. import { useGridApiRef } from "@mui/x-data-grid";
  24. import StyledDataGrid from "../StyledDataGrid";
  25. import { uniq } from "lodash";
  26. import CreateInvoiceModal from "./CreateExpenseModal";
  27. import { GridToolbarContainer } from "@mui/x-data-grid";
  28. import { FooterPropsOverrides } from "@mui/x-data-grid";
  29. import { th } from "@faker-js/faker";
  30. import { GridRowIdGetter } from "@mui/x-data-grid";
  31. import { useFormContext } from "react-hook-form";
  32. import { ProjectResult } from "@/app/api/projects";
  33. import { ProjectExpensesResultFormatted } from "@/app/api/projectExpenses";
  34. import { GridRenderCellParams } from "@mui/x-data-grid";
  35. type ExpenseListError = {
  36. [field in keyof ProjectExpensesResultFormatted]?: string;
  37. }& {
  38. message?: string;
  39. };
  40. type ExpenseListRow = Partial<
  41. ProjectExpensesResultFormatted & {
  42. _isNew: boolean;
  43. _error: ExpenseListError;
  44. }
  45. >;
  46. interface Props {
  47. projects: ProjectResult[];
  48. }
  49. class ProcessRowUpdateError extends Error {
  50. public readonly row: ExpenseListRow;
  51. public readonly errors: ExpenseListError | undefined;
  52. constructor(
  53. row: ExpenseListRow,
  54. message?: string,
  55. errors?: ExpenseListError,
  56. ) {
  57. super(message);
  58. this.row = row;
  59. this.errors = errors;
  60. Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
  61. }
  62. }
  63. type project = {
  64. label: string;
  65. value: number;
  66. }
  67. const ExpenseTable: React.FC<Props> = ({ projects }) => {
  68. console.log(projects)
  69. const projectCombos = projects.map(item => item.code)
  70. const { t } = useTranslation()
  71. const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
  72. const [selectedRow, setSelectedRow] = useState<ExpenseListRow[] | []>([]);
  73. const { getValues, setValue, clearErrors, setError } =
  74. useFormContext<any>();
  75. const apiRef = useGridApiRef();
  76. const validateExpenseEntry = (
  77. entry: Partial<ProjectExpensesResultFormatted>,
  78. ): ExpenseListError | undefined => {
  79. // Test for errors
  80. const error: ExpenseListError = {};
  81. // if (!entry.issueDate) {
  82. // error.issueDate = "Please input issued date";
  83. // error.message = "Please input issued date";
  84. // }
  85. if (!entry.amount) {
  86. error.amount = "Please input amount";
  87. error.message = "Please input amount"
  88. }
  89. if (!entry.projectCode) {
  90. error.projectCode = "Please input project code";
  91. error.message = "Please input project code";
  92. }
  93. console.log(error)
  94. return Object.keys(error).length > 0 ? error : undefined;
  95. }
  96. const validateRow = useCallback(
  97. (id: GridRowId) => {
  98. const row = apiRef.current.getRowWithUpdatedValues(
  99. id,
  100. "",
  101. )
  102. const error = validateExpenseEntry(row);
  103. console.log(error)
  104. // Test for warnings
  105. // apiRef.current.updateRows([{ id, _error: error }]);
  106. return error;
  107. },
  108. [],
  109. );
  110. const handleEditStop = useCallback<GridEventListener<"rowEditStop">>(
  111. (params, event) => {
  112. const row = apiRef.current.getRowWithUpdatedValues(
  113. params.id,
  114. "",
  115. )
  116. console.log(validateRow(params.id) !== undefined)
  117. console.log(!validateRow(params.id))
  118. if (validateRow(params.id) !== undefined && !validateRow(params.id)) {
  119. setRowModesModel((model) => ({
  120. ...model,
  121. [params.id]: { mode: GridRowModes.View},
  122. }));
  123. console.log(row)
  124. setSelectedRow((row) => [...row] as any[])
  125. event.defaultMuiPrevented = true;
  126. }else{
  127. console.log(row)
  128. const error = validateRow(params.id)
  129. setSelectedRow((row) => {
  130. const updatedRow = row.map(r => r.id === params.id ? { ...r, _error: error } : r);
  131. return updatedRow;
  132. })
  133. }
  134. // console.log(row)
  135. },
  136. [validateRow],
  137. );
  138. const addRow = useCallback(() => {
  139. const id = Date.now();
  140. setSelectedRow((e) => [...e, { id, _isNew: true }]);
  141. setRowModesModel((model) => ({
  142. ...model,
  143. [id]: { mode: GridRowModes.Edit },
  144. }));
  145. }, []);
  146. const processRowUpdate = useCallback(
  147. (
  148. newRow: GridRowModel<ExpenseListRow>,
  149. originalRow: GridRowModel<ExpenseListRow>,
  150. ) => {
  151. const errors = validateRow(newRow.id!!);
  152. if (errors) {
  153. // console.log(errors)
  154. // throw new error for error checking
  155. throw new ProcessRowUpdateError(
  156. originalRow,
  157. "validation error",
  158. errors,
  159. )
  160. }
  161. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  162. const { _isNew, _error, ...updatedRow } = newRow;
  163. const rowToSave = {
  164. ...updatedRow,
  165. } satisfies ExpenseListRow;
  166. console.log(newRow)
  167. setSelectedRow((es) =>
  168. es.map((e) => (e.id === originalRow.id ? rowToSave : e))
  169. );
  170. console.log(rowToSave)
  171. return rowToSave;
  172. },
  173. [validateRow],
  174. );
  175. const hasRowErrors = selectedRow.some(row => row._error !== undefined)
  176. /**
  177. * Add callback to check error
  178. */
  179. const onProcessRowUpdateError = useCallback(
  180. (updateError: ProcessRowUpdateError) => {
  181. const errors = updateError.errors;
  182. const oldRow = updateError.row;
  183. console.log(errors)
  184. apiRef.current.updateRows([{ ...oldRow, _error: errors }]);
  185. },
  186. [apiRef]
  187. )
  188. useEffect(() => {
  189. console.log(selectedRow)
  190. setValue("data", selectedRow)
  191. }, [selectedRow, setValue]);
  192. function renderAutocomplete(params: GridRenderCellParams<any, number>) {
  193. return(
  194. <Box sx={{ display: 'flex', alignItems: 'center', pr: 2 }}>
  195. <Autocomplete
  196. readOnly
  197. sx={{ width: 300 }}
  198. value={params.row.projectCode}
  199. options={projectCombos}
  200. renderInput={(params) => <TextField {...params} />}
  201. />
  202. </Box>
  203. )
  204. }
  205. function AutocompleteInput(props: GridRenderCellParams<any, number>) {
  206. const { id, value, field, hasFocus } = props;
  207. const apiRef = useGridApiContext();
  208. const ref = React.useRef<HTMLElement>(null);
  209. const handleValueChange = useCallback((newValue: any) => {
  210. console.log(newValue)
  211. apiRef.current.setEditCellValue({ id, field, value: newValue })
  212. }, []);
  213. return (
  214. <Box sx={{ display: 'flex', alignItems: 'center', pr: 2 }}>
  215. <Autocomplete
  216. disablePortal
  217. options={projectCombos}
  218. sx={{ width: 300 }}
  219. onChange={(event: React.SyntheticEvent<Element, Event>, value: string | null, ) => handleValueChange(value)}
  220. renderInput={(params) => <TextField {...params} />}
  221. />
  222. </Box>
  223. );
  224. }
  225. const renderAutocompleteInput: GridColDef['renderCell'] = (params) => {
  226. return <AutocompleteInput {...params} />;
  227. };
  228. const editCombinedColumns = useMemo<GridColDef[]>(
  229. () => [
  230. {
  231. field: "expenseNo",
  232. headerName: t("Expense No"),
  233. editable: true,
  234. flex: 0.5
  235. },
  236. {
  237. field: "projectCode",
  238. headerName: t("Project Code"),
  239. editable: true,
  240. flex: 0.3,
  241. renderCell: renderAutocomplete,
  242. renderEditCell: renderAutocompleteInput
  243. },
  244. // {
  245. // field: "issueDate",
  246. // headerName: t("Issue Date"),
  247. // editable: true,
  248. // flex: 0.4,
  249. // type: 'date',
  250. // },
  251. {
  252. field: "amount",
  253. headerName: t("Amount (HKD)"),
  254. editable: true,
  255. flex: 0.5,
  256. type: 'number'
  257. },
  258. // {
  259. // field: "receiptDate",
  260. // headerName: t("Settle Date"),
  261. // editable: true,
  262. // flex: 0.4,
  263. // type: 'date',
  264. // },
  265. {
  266. field: "remarks",
  267. headerName: t("Remarks"),
  268. editable: true,
  269. flex: 1,
  270. },
  271. ],
  272. [t]
  273. )
  274. const footer = (
  275. <Box display="flex" gap={2} alignItems="center">
  276. <Button
  277. disableRipple
  278. variant="outlined"
  279. startIcon={<Add />}
  280. onClick={addRow}
  281. size="small"
  282. >
  283. {t("Create Expense")}
  284. </Button>
  285. {
  286. hasRowErrors &&
  287. <Typography color="warning.main" variant="body2">
  288. {t("There are errors!")} {selectedRow.find(row => row._error !== null)?._error?.message}
  289. </Typography>
  290. }
  291. </Box>
  292. );
  293. return (
  294. <>
  295. <StyledDataGrid
  296. apiRef={apiRef}
  297. sx={{
  298. "--DataGrid-overlayHeight": "100px",
  299. ".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
  300. border: "1px solid",
  301. borderColor: "error.main",
  302. },
  303. ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
  304. border: "1px solid",
  305. borderColor: "warning.main",
  306. },
  307. height: 400, width: '95%'
  308. }}
  309. disableColumnMenu
  310. editMode="row"
  311. rows={selectedRow}
  312. rowModesModel={rowModesModel}
  313. onRowModesModelChange={setRowModesModel}
  314. onRowEditStop={handleEditStop}
  315. columns={editCombinedColumns}
  316. processRowUpdate={processRowUpdate}
  317. onProcessRowUpdateError={onProcessRowUpdateError}
  318. getCellClassName={(params: GridCellParams<ExpenseListRow>) => {
  319. let classname = "";
  320. if (params.row._error?.[params.field as keyof ProjectExpensesResultFormatted]) {
  321. classname = "hasError";
  322. }
  323. return classname;
  324. }}
  325. slots={{
  326. footer: FooterToolbar,
  327. noRowsOverlay: NoRowsOverlay,
  328. }}
  329. slotProps={{
  330. footer: { child: footer },
  331. }}
  332. />
  333. </>
  334. )
  335. }
  336. export default ExpenseTable
  337. const NoRowsOverlay: React.FC = () => {
  338. const { t } = useTranslation("home");
  339. return (
  340. <Box
  341. display="flex"
  342. justifyContent="center"
  343. alignItems="center"
  344. height="100%"
  345. >
  346. <Typography variant="caption">{t("Add some time entries!")}</Typography>
  347. </Box>
  348. );
  349. };
  350. const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  351. return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
  352. };