You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

587 lines
18 KiB

  1. "use client";
  2. import Grid from "@mui/material/Grid";
  3. import Paper from "@mui/material/Paper";
  4. import { useState, useEffect } from "react";
  5. import { useTranslation } from "react-i18next";
  6. import PageTitle from "../PageTitle/PageTitle";
  7. import { Suspense } from "react";
  8. import Button from "@mui/material/Button";
  9. import Stack from "@mui/material/Stack";
  10. import Link from "next/link";
  11. import {
  12. Box, Card, Typography,
  13. } from "@mui/material";
  14. import AddIcon from "@mui/icons-material/Add";
  15. import EditIcon from "@mui/icons-material/Edit";
  16. import DeleteIcon from "@mui/icons-material/DeleteOutlined";
  17. import SaveIcon from "@mui/icons-material/Save";
  18. import CancelIcon from "@mui/icons-material/Close";
  19. import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
  20. import ImageNotSupportedOutlinedIcon from "@mui/icons-material/ImageNotSupportedOutlined";
  21. import React from "react";
  22. import {
  23. GridRowsProp,
  24. GridRowModesModel,
  25. GridRowModes,
  26. DataGrid,
  27. GridColDef,
  28. GridActionsCellItem,
  29. GridEventListener,
  30. GridRowId,
  31. GridRowModel,
  32. GridRowEditStopReasons,
  33. GridEditInputCell,
  34. GridTreeNodeWithRender,
  35. GridRenderCellParams,
  36. } from "@mui/x-data-grid";
  37. import dayjs from "dayjs";
  38. import { Props } from "react-intl/src/components/relative";
  39. import palette from "@/theme/devias-material-kit/palette";
  40. import { ProjectCombo } from "@/app/api/claims";
  41. import { ClaimDetailTable, ClaimInputFormByStaff } from "@/app/api/claims/actions";
  42. import { useFieldArray, useFormContext } from "react-hook-form";
  43. import { GridRenderEditCellParams } from "@mui/x-data-grid";
  44. import { convertDateToString, moneyFormatter } from "@/app/utils/formatUtil";
  45. interface BottomBarProps {
  46. getCostTotal: () => number;
  47. setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
  48. setRowModesModel: (
  49. newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
  50. ) => void;
  51. }
  52. interface EditFooterProps {
  53. setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
  54. setRowModesModel: (
  55. newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
  56. ) => void;
  57. }
  58. const BottomBar = (props: BottomBarProps) => {
  59. const { t } = useTranslation("claim")
  60. const { setRows, setRowModesModel, getCostTotal } = props;
  61. // const getCostTotal = props.getCostTotal;
  62. const [newId, setNewId] = useState(-1);
  63. const handleAddClick = () => {
  64. const id = newId;
  65. setNewId(newId - 1);
  66. setRows((oldRows) => [
  67. ...oldRows,
  68. { id, invoiceDate: new Date(), project: null, description: null, amount: null, newSupportingDocument: null, supportingDocumentName: null, isNew: true },
  69. ]);
  70. setRowModesModel((oldModel) => ({
  71. ...oldModel,
  72. [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" },
  73. }));
  74. };
  75. const TotalCell = ({ value }: Props) => {
  76. const [invalid, setInvalid] = useState(false);
  77. useEffect(() => {
  78. const newInvalid = (value ?? 0) < 0;
  79. setInvalid(newInvalid);
  80. }, [value]);
  81. return (
  82. <Box flex={1} style={{ color: invalid ? "red" : "black" }}>
  83. $ {value}
  84. </Box>
  85. );
  86. };
  87. return (
  88. <div>
  89. <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
  90. <Box flex={1.5} textAlign={"right"} marginRight="4rem">
  91. <b>{t("Total")}:</b>
  92. </Box>
  93. <TotalCell value={getCostTotal()} />
  94. </div>
  95. <Button
  96. variant="outlined"
  97. color="primary"
  98. startIcon={<AddIcon />}
  99. onClick={handleAddClick}
  100. sx={{ margin: "20px" }}
  101. >
  102. {t("Add Record")}
  103. </Button>
  104. </div>
  105. );
  106. };
  107. const EditFooter = (props: EditFooterProps) => {
  108. return (
  109. <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
  110. <Box flex={1}>
  111. <b>Total: </b>
  112. </Box>
  113. <Box flex={2}>test</Box>
  114. </div>
  115. );
  116. };
  117. interface ClaimFormInputGridProps {
  118. // onClose?: () => void;
  119. projectCombo: ProjectCombo[]
  120. }
  121. const initialRows: GridRowsProp = [
  122. {
  123. id: 1,
  124. invoiceDate: new Date(),
  125. description: "Taxi to client office",
  126. amount: 169.5,
  127. supportingDocumentName: "taxi_receipt.jpg",
  128. },
  129. {
  130. id: 2,
  131. invoiceDate: dayjs().add(-14, "days").toDate(),
  132. description: "MTR fee to Kowloon Bay Office",
  133. amount: 15.5,
  134. supportingDocumentName: "octopus_invoice.jpg",
  135. },
  136. {
  137. id: 3,
  138. invoiceDate: dayjs().add(-44, "days").toDate(),
  139. description: "Starbucks",
  140. amount: 504,
  141. },
  142. ];
  143. const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({
  144. // onClose,
  145. projectCombo,
  146. }) => {
  147. const { t } = useTranslation()
  148. const { control, setValue, getValues, formState: { errors }, clearErrors, setError } = useFormContext<ClaimInputFormByStaff>();
  149. const { fields } = useFieldArray({
  150. control,
  151. name: "addClaimDetails"
  152. })
  153. const [rows, setRows] = useState<GridRowsProp>([]);
  154. const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>(
  155. {},
  156. );
  157. // Row function
  158. const handleRowEditStop: GridEventListener<"rowEditStop"> = (
  159. params,
  160. event,
  161. ) => {
  162. if (params.reason === GridRowEditStopReasons.rowFocusOut) {
  163. event.defaultMuiPrevented = true;
  164. }
  165. };
  166. const handleEditClick = (id: GridRowId) => () => {
  167. setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
  168. };
  169. const handleSaveClick = (id: GridRowId) => () => {
  170. setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
  171. };
  172. const handleDeleteClick = (id: GridRowId) => () => {
  173. setRows(rows.filter((row) => row.id !== id));
  174. };
  175. const handleCancelClick = (id: GridRowId) => () => {
  176. setRowModesModel({
  177. ...rowModesModel,
  178. [id]: { mode: GridRowModes.View, ignoreModifications: true },
  179. });
  180. const editedRow = rows.find((row) => row.id === id);
  181. if (editedRow!.isNew) {
  182. setRows(rows.filter((row) => row.id !== id));
  183. }
  184. };
  185. const processRowUpdate = React.useCallback((newRow: GridRowModel) => {
  186. const updatedRow = { ...newRow };
  187. const updatedRows = rows.map((row) => (row.id === newRow.id ? { ...updatedRow, supportingDocumentName: row.supportingDocumentName } : row))
  188. setRows(updatedRows);
  189. setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
  190. return updatedRows.find((row) => row.id === newRow.id) as GridRowModel;
  191. }, [rows, rowModesModel, t]);
  192. const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
  193. setRowModesModel(newRowModesModel);
  194. };
  195. // File Upload function
  196. const fileInputRef: React.RefObject<Record<string, HTMLInputElement | null>> = React.useRef({})
  197. const setFileInputRefs = (ele: HTMLInputElement | null, key: string) => {
  198. if (fileInputRef.current !== null) {
  199. fileInputRef.current[key] = ele
  200. }
  201. }
  202. useEffect(() => {
  203. }, [])
  204. const handleFileSelect = (key: string) => {
  205. if (fileInputRef !== null && fileInputRef.current !== null && fileInputRef.current[key] !== null) {
  206. fileInputRef.current[key]?.click()
  207. }
  208. }
  209. const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => {
  210. const file = event.target.files?.[0] ?? null
  211. if (file !== null) {
  212. console.log(file)
  213. console.log(typeof file)
  214. const updatedRows = rows.map((row) => (row.id === params.row.id ? { ...row, supportingDocumentName: file.name, newSupportingDocument: file } : row))
  215. setRows(updatedRows);
  216. setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
  217. // const url = URL.createObjectURL(new Blob([file]));
  218. // const link = document.createElement("a");
  219. // link.href = url;
  220. // link.setAttribute("download", file.name);
  221. // link.click();
  222. }
  223. }
  224. const handleFileDelete = (id: number) => {
  225. const updatedRows = rows.map((row) => (row.id === id ? { ...row, supportingDocumentName: null, newSupportingDocument: null } : row))
  226. setRows(updatedRows);
  227. setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
  228. }
  229. const handleLinkClick = (params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => {
  230. const url = URL.createObjectURL(new Blob([params.row.newSupportingDocument]));
  231. const link = document.createElement("a");
  232. link.href = url;
  233. link.setAttribute("download", params.row.supportingDocumentName);
  234. link.click();
  235. // console.log(params)
  236. // console.log(rows)
  237. }
  238. // columns
  239. const getCostTotal = () => {
  240. let sum = 0;
  241. rows.forEach((row) => {
  242. sum += row["amount"] ?? 0;
  243. });
  244. return sum;
  245. };
  246. const commonGridColConfig: any = {
  247. type: "number",
  248. // sortable: false,
  249. //width: 100,
  250. flex: 1,
  251. align: "left",
  252. headerAlign: "left",
  253. // headerClassName: 'header',
  254. editable: true,
  255. renderEditCell: (value: any) => (
  256. <GridEditInputCell
  257. {...value}
  258. inputProps={{
  259. max: 24,
  260. min: 0,
  261. step: 0.25,
  262. }}
  263. />
  264. ),
  265. };
  266. const columns: GridColDef[] = React.useMemo(() => [
  267. {
  268. field: "actions",
  269. type: "actions",
  270. headerName: t("Actions"),
  271. width: 100,
  272. cellClassName: "actions",
  273. getActions: ({ id }) => {
  274. const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
  275. if (isInEditMode) {
  276. return [
  277. <GridActionsCellItem
  278. key={`actions-${id}-save`}
  279. icon={<SaveIcon />}
  280. title="Save"
  281. label="Save"
  282. sx={{
  283. color: "primary.main",
  284. }}
  285. onClick={handleSaveClick(id)}
  286. />,
  287. <GridActionsCellItem
  288. key={`actions-${id}-cancel`}
  289. icon={<CancelIcon />}
  290. title="Cancel"
  291. label="Cancel"
  292. className="textPrimary"
  293. onClick={handleCancelClick(id)}
  294. color="inherit"
  295. />,
  296. ];
  297. }
  298. return [
  299. <GridActionsCellItem
  300. key={`actions-${id}-edit`}
  301. icon={<EditIcon />}
  302. title="Edit"
  303. label="Edit"
  304. className="textPrimary"
  305. onClick={handleEditClick(id)}
  306. color="inherit"
  307. />,
  308. <GridActionsCellItem
  309. key={`actions-${id}-delete`}
  310. title="Delete"
  311. label="Delete"
  312. icon={<DeleteIcon />}
  313. onClick={handleDeleteClick(id)}
  314. sx={{ color: "red" }}
  315. />,
  316. ];
  317. },
  318. },
  319. {
  320. field: "invoiceDate",
  321. headerName: t("Invoice Date"),
  322. // width: 220,
  323. flex: 1,
  324. editable: true,
  325. type: "date",
  326. renderCell: (params: GridRenderCellParams<any, Date>) => {
  327. return convertDateToString(params.value!!)
  328. },
  329. },
  330. {
  331. field: "project",
  332. headerName: t("Project"),
  333. // width: 220,
  334. flex: 1,
  335. editable: true,
  336. type: "singleSelect",
  337. getOptionLabel: (value: any) => {
  338. return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`;
  339. },
  340. getOptionValue: (value: any) => value,
  341. valueOptions: () => {
  342. const options = projectCombo ?? []
  343. if (options.length === 0) {
  344. options.push({ id: -1, code: "", name: "No Projects" })
  345. }
  346. return options;
  347. },
  348. valueGetter: (params) => {
  349. return params.value ?? projectCombo[0].id ?? -1
  350. },
  351. },
  352. {
  353. field: "description",
  354. headerName: t("Description"),
  355. // width: 220,
  356. flex: 2,
  357. editable: true,
  358. type: "string",
  359. },
  360. {
  361. field: "amount",
  362. headerName: t("Amount"),
  363. editable: true,
  364. type: "number",
  365. align: "right",
  366. valueFormatter: (params) => {
  367. return moneyFormatter.format(params.value ?? 0);
  368. },
  369. },
  370. {
  371. field: "supportingDocumentName",
  372. headerName: t("Supporting Document"),
  373. // type: "string",
  374. editable: true,
  375. flex: 2,
  376. renderCell: (params) => {
  377. return params.value ? (
  378. <span>
  379. <Link onClick={() => handleLinkClick(params)} href="#">{params.value}</Link>
  380. {/* <a href="" target="_blank" rel="noopener noreferrer">
  381. {params.value}
  382. </a> */}
  383. </span>
  384. ) : (
  385. <span style={{ color: palette.text.disabled }}>No Documents</span>
  386. );
  387. },
  388. renderEditCell: (params) => {
  389. // const currentRow = rows.find(row => row.id === params.row.id);
  390. return params.formattedValue ? (
  391. <span>
  392. <Link onClick={() => handleLinkClick(params)} href="#">{params.formattedValue}</Link>
  393. {/* <a href="" target="_blank" rel="noopener noreferrer">
  394. {params.formattedValue}
  395. </a> */}
  396. <Button
  397. title="Remove Document"
  398. onClick={() => handleFileDelete(params.row.id)}
  399. >
  400. <ImageNotSupportedOutlinedIcon
  401. sx={{ fontSize: "25px", color: "red" }}
  402. />
  403. </Button>
  404. </span>
  405. ) : (
  406. <div>
  407. <input
  408. type="file"
  409. ref={ele => setFileInputRefs(ele, params.row.id)}
  410. accept="image/jpg, image/jpeg, image/png, .doc, .docx, .pdf"
  411. style={{ display: 'none' }}
  412. onChange={(event) => handleFileChange(event, params)}
  413. />
  414. <Button title="Add Document" onClick={() => handleFileSelect(params.row.id)}>
  415. <AddPhotoAlternateOutlinedIcon
  416. sx={{ fontSize: "25px", color: "green" }}
  417. />
  418. </Button>
  419. </div>
  420. );
  421. },
  422. },
  423. ], [rows, rowModesModel, t],);
  424. // check error
  425. useEffect(() => {
  426. if (getValues("addClaimDetails") === undefined || getValues("addClaimDetails") === null) {
  427. return;
  428. }
  429. if (getValues("addClaimDetails").length === 0) {
  430. clearErrors("addClaimDetails")
  431. } else {
  432. console.log(rows)
  433. if (rows.filter(row => String(row.description).trim().length === 0 || String(row.amount).trim().length === 0 || row.project === null || row.project === undefined || ((row.oldSupportingDocument === null || row.oldSupportingDocument === undefined) && (row.newSupportingDocument === null || row.newSupportingDocument === undefined))).length > 0) {
  434. setError("addClaimDetails", { message: "Claim details include empty fields", type: "required" })
  435. } else {
  436. let haveError = false
  437. if (rows.filter(row => row.invoiceDate.getTime() > new Date().getTime()).length > 0) {
  438. haveError = true
  439. setError("addClaimDetails", { message: "Claim details include invalid invoice date", type: "invalid_date" })
  440. }
  441. if (rows.filter(row => row.project === null || row.project === undefined).length > 0) {
  442. haveError = true
  443. setError("addClaimDetails", { message: "Claim details include empty project", type: "invalid_project" })
  444. }
  445. if (rows.filter(row => row.amount <= 0).length > 0) {
  446. haveError = true
  447. setError("addClaimDetails", { message: "Claim details include invalid amount", type: "invalid_amount" })
  448. }
  449. if (!haveError) {
  450. clearErrors("addClaimDetails")
  451. }
  452. }
  453. }
  454. }, [rows, rowModesModel])
  455. // check editing
  456. useEffect(() => {
  457. const filteredByKey = Object.fromEntries(
  458. Object.entries(rowModesModel).filter(([key, value]) => rowModesModel[key].mode === 'edit'))
  459. if (Object.keys(filteredByKey).length > 0) {
  460. setValue("isGridEditing", true)
  461. } else {
  462. setValue("isGridEditing", false)
  463. }
  464. }, [rowModesModel])
  465. return (
  466. <Box
  467. sx={{
  468. // marginBottom: '-5px',
  469. display: "flex",
  470. "flex-direction": "column",
  471. // 'justify-content': 'flex-end',
  472. height: "100%", //'25rem',
  473. width: "100%",
  474. "& .actions": {
  475. color: "text.secondary",
  476. },
  477. "& .header": {
  478. backgroundColor: "#F8F9FA",
  479. // border: 1,
  480. // 'border-width': '1px',
  481. // 'border-color': 'grey',
  482. },
  483. "& .textPrimary": {
  484. color: "text.primary",
  485. },
  486. }}
  487. >
  488. {Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
  489. {t("Please ensure at least one row is created, and all the fields are inputted and saved")}
  490. </Typography>}
  491. {Boolean(errors.addClaimDetails?.type === "invalid_date") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
  492. {t("Please ensure the date are correct")}
  493. </Typography>}
  494. {Boolean(errors.addClaimDetails?.type === "invalid_project") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
  495. {t("Please ensure the projects are selected")}
  496. </Typography>}
  497. {Boolean(errors.addClaimDetails?.type === "invalid_amount") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
  498. {t("Please ensure the amount are correct")}
  499. </Typography>}
  500. <div style={{ height: 400, width: "100%" }}>
  501. <DataGrid
  502. sx={{ flex: 1 }}
  503. rows={rows}
  504. columns={columns}
  505. editMode="row"
  506. rowModesModel={rowModesModel}
  507. onRowModesModelChange={handleRowModesModelChange}
  508. onRowEditStop={handleRowEditStop}
  509. processRowUpdate={processRowUpdate}
  510. disableRowSelectionOnClick={true}
  511. disableColumnMenu={true}
  512. // hideFooterPagination={true}
  513. slots={
  514. {
  515. // footer: EditFooter,
  516. }
  517. }
  518. slotProps={
  519. {
  520. // footer: { setDay, setRows, setRowModesModel },
  521. }
  522. }
  523. initialState={{
  524. pagination: { paginationModel: { pageSize: 5 } },
  525. }}
  526. />
  527. </div>
  528. <BottomBar
  529. getCostTotal={getCostTotal}
  530. setRows={setRows}
  531. setRowModesModel={setRowModesModel}
  532. // sx={{flex:2}}
  533. />
  534. </Box>
  535. );
  536. };
  537. export default ClaimFormInputGrid;