Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 

387 rader
12 KiB

  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. };