|
- "use client";
- import Grid from "@mui/material/Grid";
- import Paper from "@mui/material/Paper";
- import { useState, useEffect } from "react";
- import { useTranslation } from "react-i18next";
- import PageTitle from "../PageTitle/PageTitle";
- import { Suspense } from "react";
- import Button from "@mui/material/Button";
- import Stack from "@mui/material/Stack";
- import Link from "next/link";
- import {
- Box, Card, Typography,
- } from "@mui/material";
- import AddIcon from "@mui/icons-material/Add";
- import EditIcon from "@mui/icons-material/Edit";
- import DeleteIcon from "@mui/icons-material/DeleteOutlined";
- import SaveIcon from "@mui/icons-material/Save";
- import CancelIcon from "@mui/icons-material/Close";
- import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
- import ImageNotSupportedOutlinedIcon from "@mui/icons-material/ImageNotSupportedOutlined";
- import React from "react";
- import {
- GridRowsProp,
- GridRowModesModel,
- GridRowModes,
- DataGrid,
- GridColDef,
- GridActionsCellItem,
- GridEventListener,
- GridRowId,
- GridRowModel,
- GridRowEditStopReasons,
- GridEditInputCell,
- GridTreeNodeWithRender,
- GridRenderCellParams,
- } from "@mui/x-data-grid";
- import dayjs from "dayjs";
- import { Props } from "react-intl/src/components/relative";
- import palette from "@/theme/devias-material-kit/palette";
- import { ProjectCombo } from "@/app/api/claims";
- import { ClaimDetailTable, ClaimInputFormByStaff } from "@/app/api/claims/actions";
- import { useFieldArray, useFormContext } from "react-hook-form";
- import { GridRenderEditCellParams } from "@mui/x-data-grid";
- import { convertDateToString, moneyFormatter } from "@/app/utils/formatUtil";
-
- interface BottomBarProps {
- getCostTotal: () => number;
- setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
- setRowModesModel: (
- newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
- ) => void;
- }
-
- interface EditFooterProps {
- setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
- setRowModesModel: (
- newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
- ) => void;
- }
-
- const BottomBar = (props: BottomBarProps) => {
- const { t } = useTranslation("claim")
- const { setRows, setRowModesModel, getCostTotal } = props;
- // const getCostTotal = props.getCostTotal;
- const [newId, setNewId] = useState(-1);
-
- const handleAddClick = () => {
- const id = newId;
- setNewId(newId - 1);
- setRows((oldRows) => [
- ...oldRows,
- { id, invoiceDate: new Date(), project: null, description: null, amount: null, newSupportingDocument: null, supportingDocumentName: null, isNew: true },
- ]);
- setRowModesModel((oldModel) => ({
- ...oldModel,
- [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" },
- }));
- };
-
- const TotalCell = ({ value }: Props) => {
- const [invalid, setInvalid] = useState(false);
-
- useEffect(() => {
- const newInvalid = (value ?? 0) < 0;
- setInvalid(newInvalid);
- }, [value]);
-
- return (
- <Box flex={1} style={{ color: invalid ? "red" : "black" }}>
- $ {value}
- </Box>
- );
- };
-
- return (
- <div>
- <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
- <Box flex={1.5} textAlign={"right"} marginRight="4rem">
- <b>{t("Total")}:</b>
- </Box>
- <TotalCell value={getCostTotal()} />
- </div>
- <Button
- variant="outlined"
- color="primary"
- startIcon={<AddIcon />}
- onClick={handleAddClick}
- sx={{ margin: "20px" }}
- >
- {t("Add Record")}
- </Button>
- </div>
- );
- };
-
- const EditFooter = (props: EditFooterProps) => {
- return (
- <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
- <Box flex={1}>
- <b>Total: </b>
- </Box>
- <Box flex={2}>test</Box>
- </div>
- );
- };
-
- interface ClaimFormInputGridProps {
- // onClose?: () => void;
- projectCombo: ProjectCombo[]
- }
-
- const initialRows: GridRowsProp = [
- {
- id: 1,
- invoiceDate: new Date(),
- description: "Taxi to client office",
- amount: 169.5,
- supportingDocumentName: "taxi_receipt.jpg",
- },
- {
- id: 2,
- invoiceDate: dayjs().add(-14, "days").toDate(),
- description: "MTR fee to Kowloon Bay Office",
- amount: 15.5,
- supportingDocumentName: "octopus_invoice.jpg",
- },
- {
- id: 3,
- invoiceDate: dayjs().add(-44, "days").toDate(),
- description: "Starbucks",
- amount: 504,
- },
- ];
-
- const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({
- // onClose,
- projectCombo,
- }) => {
- const { t } = useTranslation()
- const { control, setValue, getValues, formState: { errors }, clearErrors, setError } = useFormContext<ClaimInputFormByStaff>();
- const { fields } = useFieldArray({
- control,
- name: "addClaimDetails"
- })
-
- const [rows, setRows] = useState<GridRowsProp>([]);
- const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>(
- {},
- );
-
- // Row function
- const handleRowEditStop: GridEventListener<"rowEditStop"> = (
- params,
- event,
- ) => {
- if (params.reason === GridRowEditStopReasons.rowFocusOut) {
- event.defaultMuiPrevented = true;
- }
- };
-
- const handleEditClick = (id: GridRowId) => () => {
- setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
- };
-
- const handleSaveClick = (id: GridRowId) => () => {
- setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
- };
-
- const handleDeleteClick = (id: GridRowId) => () => {
- setRows(rows.filter((row) => row.id !== id));
- };
-
- const handleCancelClick = (id: GridRowId) => () => {
- setRowModesModel({
- ...rowModesModel,
- [id]: { mode: GridRowModes.View, ignoreModifications: true },
- });
-
- const editedRow = rows.find((row) => row.id === id);
- if (editedRow!.isNew) {
- setRows(rows.filter((row) => row.id !== id));
- }
- };
-
- const processRowUpdate = React.useCallback((newRow: GridRowModel) => {
- const updatedRow = { ...newRow };
- const updatedRows = rows.map((row) => (row.id === newRow.id ? { ...updatedRow, supportingDocumentName: row.supportingDocumentName } : row))
- setRows(updatedRows);
- setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
- return updatedRows.find((row) => row.id === newRow.id) as GridRowModel;
- }, [rows, rowModesModel, t]);
-
- const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
- setRowModesModel(newRowModesModel);
- };
-
- // File Upload function
- const fileInputRef: React.RefObject<Record<string, HTMLInputElement | null>> = React.useRef({})
-
- const setFileInputRefs = (ele: HTMLInputElement | null, key: string) => {
- if (fileInputRef.current !== null) {
- fileInputRef.current[key] = ele
- }
- }
-
- useEffect(() => {
-
- }, [])
- const handleFileSelect = (key: string) => {
- if (fileInputRef !== null && fileInputRef.current !== null && fileInputRef.current[key] !== null) {
- fileInputRef.current[key]?.click()
- }
- }
-
- const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => {
-
- const file = event.target.files?.[0] ?? null
-
- if (file !== null) {
- console.log(file)
- console.log(typeof file)
- const updatedRows = rows.map((row) => (row.id === params.row.id ? { ...row, supportingDocumentName: file.name, newSupportingDocument: file } : row))
- setRows(updatedRows);
- setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
- // const url = URL.createObjectURL(new Blob([file]));
- // const link = document.createElement("a");
- // link.href = url;
- // link.setAttribute("download", file.name);
- // link.click();
- }
- }
-
- const handleFileDelete = (id: number) => {
- const updatedRows = rows.map((row) => (row.id === id ? { ...row, supportingDocumentName: null, newSupportingDocument: null } : row))
- setRows(updatedRows);
- setValue("addClaimDetails", updatedRows as ClaimDetailTable[])
- }
-
- const handleLinkClick = (params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => {
-
- const url = URL.createObjectURL(new Blob([params.row.newSupportingDocument]));
- const link = document.createElement("a");
- link.href = url;
- link.setAttribute("download", params.row.supportingDocumentName);
- link.click();
-
- // console.log(params)
- // console.log(rows)
- }
-
- // columns
- const getCostTotal = () => {
- let sum = 0;
- rows.forEach((row) => {
- sum += row["amount"] ?? 0;
- });
- return sum;
- };
-
- const commonGridColConfig: any = {
- type: "number",
- // sortable: false,
- //width: 100,
- flex: 1,
- align: "left",
- headerAlign: "left",
- // headerClassName: 'header',
- editable: true,
- renderEditCell: (value: any) => (
- <GridEditInputCell
- {...value}
- inputProps={{
- max: 24,
- min: 0,
- step: 0.25,
- }}
- />
- ),
- };
-
- const columns: GridColDef[] = React.useMemo(() => [
- {
- field: "actions",
- type: "actions",
- headerName: t("Actions"),
- width: 100,
- cellClassName: "actions",
- getActions: ({ id }) => {
- const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
-
- if (isInEditMode) {
- return [
- <GridActionsCellItem
- key={`actions-${id}-save`}
- icon={<SaveIcon />}
- title="Save"
- label="Save"
- sx={{
- color: "primary.main",
- }}
- onClick={handleSaveClick(id)}
- />,
- <GridActionsCellItem
- key={`actions-${id}-cancel`}
- icon={<CancelIcon />}
- title="Cancel"
- label="Cancel"
- className="textPrimary"
- onClick={handleCancelClick(id)}
- color="inherit"
- />,
- ];
- }
-
- return [
- <GridActionsCellItem
- key={`actions-${id}-edit`}
- icon={<EditIcon />}
- title="Edit"
- label="Edit"
- className="textPrimary"
- onClick={handleEditClick(id)}
- color="inherit"
- />,
- <GridActionsCellItem
- key={`actions-${id}-delete`}
- title="Delete"
- label="Delete"
- icon={<DeleteIcon />}
- onClick={handleDeleteClick(id)}
- sx={{ color: "red" }}
- />,
- ];
- },
- },
- {
- field: "invoiceDate",
- headerName: t("Invoice Date"),
- // width: 220,
- flex: 1,
- editable: true,
- type: "date",
- renderCell: (params: GridRenderCellParams<any, Date>) => {
- return convertDateToString(params.value!!)
- },
- },
- {
- field: "project",
- headerName: t("Project"),
- // width: 220,
- flex: 1,
- editable: true,
- type: "singleSelect",
- getOptionLabel: (value: any) => {
- return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`;
- },
- getOptionValue: (value: any) => value,
- valueOptions: () => {
- const options = projectCombo ?? []
-
- if (options.length === 0) {
- options.push({ id: -1, code: "", name: "No Projects" })
- }
- return options;
- },
- valueGetter: (params) => {
- return params.value ?? projectCombo[0].id ?? -1
- },
- },
- {
- field: "description",
- headerName: t("Description"),
- // width: 220,
- flex: 2,
- editable: true,
- type: "string",
- },
- {
- field: "amount",
- headerName: t("Amount"),
- editable: true,
- type: "number",
- align: "right",
- valueFormatter: (params) => {
- return moneyFormatter.format(params.value ?? 0);
- },
- },
- {
- field: "supportingDocumentName",
- headerName: t("Supporting Document"),
- // type: "string",
- editable: true,
- flex: 2,
- renderCell: (params) => {
- return params.value ? (
- <span>
- <Link onClick={() => handleLinkClick(params)} href="#">{params.value}</Link>
- {/* <a href="" target="_blank" rel="noopener noreferrer">
- {params.value}
- </a> */}
- </span>
- ) : (
- <span style={{ color: palette.text.disabled }}>No Documents</span>
- );
- },
- renderEditCell: (params) => {
- // const currentRow = rows.find(row => row.id === params.row.id);
- return params.formattedValue ? (
- <span>
- <Link onClick={() => handleLinkClick(params)} href="#">{params.formattedValue}</Link>
- {/* <a href="" target="_blank" rel="noopener noreferrer">
- {params.formattedValue}
- </a> */}
- <Button
- title="Remove Document"
- onClick={() => handleFileDelete(params.row.id)}
- >
- <ImageNotSupportedOutlinedIcon
- sx={{ fontSize: "25px", color: "red" }}
- />
- </Button>
- </span>
- ) : (
- <div>
- <input
- type="file"
- ref={ele => setFileInputRefs(ele, params.row.id)}
- accept="image/jpg, image/jpeg, image/png, .doc, .docx, .pdf"
- style={{ display: 'none' }}
- onChange={(event) => handleFileChange(event, params)}
- />
- <Button title="Add Document" onClick={() => handleFileSelect(params.row.id)}>
- <AddPhotoAlternateOutlinedIcon
- sx={{ fontSize: "25px", color: "green" }}
- />
- </Button>
- </div>
- );
- },
- },
- ], [rows, rowModesModel, t],);
-
- // check error
- useEffect(() => {
- if (getValues("addClaimDetails") === undefined || getValues("addClaimDetails") === null) {
- return;
- }
-
- if (getValues("addClaimDetails").length === 0) {
- clearErrors("addClaimDetails")
- } else {
-
- console.log(rows)
- 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) {
- setError("addClaimDetails", { message: "Claim details include empty fields", type: "required" })
- } else {
- let haveError = false
-
- if (rows.filter(row => row.invoiceDate.getTime() > new Date().getTime()).length > 0) {
- haveError = true
- setError("addClaimDetails", { message: "Claim details include invalid invoice date", type: "invalid_date" })
- }
-
- if (rows.filter(row => row.project === null || row.project === undefined).length > 0) {
- haveError = true
- setError("addClaimDetails", { message: "Claim details include empty project", type: "invalid_project" })
- }
-
- if (rows.filter(row => row.amount <= 0).length > 0) {
- haveError = true
- setError("addClaimDetails", { message: "Claim details include invalid amount", type: "invalid_amount" })
- }
-
- if (!haveError) {
- clearErrors("addClaimDetails")
- }
- }
- }
- }, [rows, rowModesModel])
-
- // check editing
- useEffect(() => {
- const filteredByKey = Object.fromEntries(
- Object.entries(rowModesModel).filter(([key, value]) => rowModesModel[key].mode === 'edit'))
-
- if (Object.keys(filteredByKey).length > 0) {
- setValue("isGridEditing", true)
- } else {
- setValue("isGridEditing", false)
- }
- }, [rowModesModel])
-
- return (
- <Box
- sx={{
- // marginBottom: '-5px',
- display: "flex",
- "flex-direction": "column",
- // 'justify-content': 'flex-end',
- height: "100%", //'25rem',
- width: "100%",
- "& .actions": {
- color: "text.secondary",
- },
- "& .header": {
- backgroundColor: "#F8F9FA",
- // border: 1,
- // 'border-width': '1px',
- // 'border-color': 'grey',
- },
- "& .textPrimary": {
- color: "text.primary",
- },
- }}
- >
- {Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
- {t("Please ensure at least one row is created, and all the fields are inputted and saved")}
- </Typography>}
- {Boolean(errors.addClaimDetails?.type === "invalid_date") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
- {t("Please ensure the date are correct")}
- </Typography>}
- {Boolean(errors.addClaimDetails?.type === "invalid_project") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
- {t("Please ensure the projects are selected")}
- </Typography>}
- {Boolean(errors.addClaimDetails?.type === "invalid_amount") && <Typography sx={(theme) => ({ color: theme.palette.error.main, ml: 3, mt: 1 })} variant="overline" display='inline-block' noWrap>
- {t("Please ensure the amount are correct")}
- </Typography>}
- <div style={{ height: 400, width: "100%" }}>
- <DataGrid
- sx={{ flex: 1 }}
- rows={rows}
- columns={columns}
- editMode="row"
- rowModesModel={rowModesModel}
- onRowModesModelChange={handleRowModesModelChange}
- onRowEditStop={handleRowEditStop}
- processRowUpdate={processRowUpdate}
- disableRowSelectionOnClick={true}
- disableColumnMenu={true}
- // hideFooterPagination={true}
- slots={
- {
- // footer: EditFooter,
- }
- }
- slotProps={
- {
- // footer: { setDay, setRows, setRowModesModel },
- }
- }
- initialState={{
- pagination: { paginationModel: { pageSize: 5 } },
- }}
- />
- </div>
- <BottomBar
- getCostTotal={getCostTotal}
- setRows={setRows}
- setRowModesModel={setRowModesModel}
- // sx={{flex:2}}
- />
- </Box>
- );
- };
-
- export default ClaimFormInputGrid;
|