From 09475a404a6a62e5758b7ae2b63c50833b63a655 Mon Sep 17 00:00:00 2001 From: Jose Carlos Laura Ramirez Date: Mon, 10 Jun 2024 01:56:21 -0400 Subject: [PATCH 01/29] add basic structure for audit log --- src/admin/apiProvider/utils/entryFormat.ts | 2 +- .../ResourceTabs/AuditLogTab/AuditLogTab.tsx | 157 +- .../components/AuditLogSiteTab.tsx | 111 + .../components/AuditLogSiteTabSelection.tsx | 37 + .../AuditLogTab/components/AuditLogTable.tsx | 80 + .../components/SiteAuditLogEntityStatus.tsx | 92 + .../SiteAuditLogEntityStatusSide.tsx | 131 + .../components/SiteAuditLogProjectStatus.tsx | 83 + .../PolygonStatus/StatusDisplay.tsx | 108 +- .../elements/CommentaryBox/CommentaryBox.tsx | 4 +- .../elements/Inputs/Dropdown/Dropdown.tsx | 34 +- .../elements/Notification/Notification.tsx | 34 +- src/components/extensive/Modal/Modal.tsx | 15 +- .../extensive/Modal/ModalConfirm.tsx | 13 +- src/constants/entities.ts | 4 + src/generated/apiComponents.ts | 2204 ++++++++++++++++- src/generated/apiRequestBodies.ts | 58 +- src/generated/apiSchemas.ts | 496 +++- src/hooks/AuditStatus/useAuditLogActions.ts | 173 ++ src/hooks/AuditStatus/useLoadEntityList.ts | 114 + src/utils/statusUtils.ts | 76 + 21 files changed, 3827 insertions(+), 199 deletions(-) create mode 100644 src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogSiteTab.tsx create mode 100644 src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogSiteTabSelection.tsx create mode 100644 src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogTable.tsx create mode 100644 src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogEntityStatus.tsx create mode 100644 src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogEntityStatusSide.tsx create mode 100644 src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogProjectStatus.tsx create mode 100644 src/constants/entities.ts create mode 100644 src/hooks/AuditStatus/useAuditLogActions.ts create mode 100644 src/hooks/AuditStatus/useLoadEntityList.ts create mode 100644 src/utils/statusUtils.ts diff --git a/src/admin/apiProvider/utils/entryFormat.ts b/src/admin/apiProvider/utils/entryFormat.ts index 929eef6a5..e02829c6e 100644 --- a/src/admin/apiProvider/utils/entryFormat.ts +++ b/src/admin/apiProvider/utils/entryFormat.ts @@ -26,7 +26,7 @@ const isDateType = (value: any) => { return isValid(parseISO(value)); }; -const convertDateFormat = (value: any) => { +export const convertDateFormat = (value: any) => { if (typeof value === "string") { const dateObject = new Date(value); const formattedDay = dateObject.getUTCDate().toString().padStart(2, "0"); diff --git a/src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx b/src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx index 691a02bd9..bc9c9c536 100644 --- a/src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx +++ b/src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx @@ -1,94 +1,105 @@ -import { Typography } from "@mui/material"; -import { FC } from "react"; -import { - Datagrid, - DateField, - FunctionField, - Pagination, - ReferenceField, - ReferenceManyField, - TabbedShowLayout, - TabProps, - useShowContext -} from "react-admin"; +import { Grid, Stack } from "@mui/material"; +import { FC, useEffect, useState } from "react"; +import { TabbedShowLayout, TabProps, useShowContext } from "react-admin"; import { When } from "react-if"; -import modules from "@/admin/modules"; -import { V2AdminUserRead } from "@/generated/apiSchemas"; +import { SITE } from "@/constants/entities"; +import useAuditLogActions from "@/hooks/AuditStatus/useAuditLogActions"; import { Entity } from "@/types/common"; +import AuditLogSiteTabSelection from "./components/AuditLogSiteTabSelection"; +import SiteAuditLogEntityStatus from "./components/SiteAuditLogEntityStatus"; +import SiteAuditLogEntityStatusSide from "./components/SiteAuditLogEntityStatusSide"; +import SiteAuditLogProjectStatus from "./components/SiteAuditLogProjectStatus"; + interface IProps extends Omit { label?: string; entity?: Entity["entityName"]; } -interface FeedbackProps { - comment: string | undefined; +export const ButtonStates = { + PROJECTS: 0, + SITE: 1, + POLYGON: 2 +}; + +export interface EntityList { + poly_name?: string | undefined; + name?: string | undefined; + uuid?: string | undefined; + value?: string | undefined; + meta?: string | undefined; + status?: string | undefined; } -const Feedback: FC = ({ comment }) => { - if (comment == null) { - return <>-; - } +const AuditLogTab: FC = ({ label, entity, ...rest }) => { + const [buttonToogle, setButtonToogle] = useState(ButtonStates.PROJECTS); + const { record, isLoading } = useShowContext(); - return ( - <> - {comment.split("\n").map(fragment => ( - <> - {fragment} -
- - ))} - - ); -}; + const { + mutateEntity, + valuesForStatus, + statusLabels, + entityType, + entityListItem, + loadEntityList, + selected, + setSelected, + auditLogData, + refetch, + checkPolygonsSite + } = useAuditLogActions({ + record, + buttonToogle, + entityLevel: SITE + }); -const AuditLogTab: FC = ({ label, entity, ...rest }) => { - const ctx = useShowContext(); - const resource = entity ?? ctx.resource; + useEffect(() => { + refetch(); + loadEntityList(); + }, [buttonToogle]); return ( - + - - Audit Log - - } - reference={modules.audit.ResourceName} - filter={{ entity: resource }} - target="uuid" - label="" - > - - - - `${record?.first_name || ""} ${record?.last_name || ""}`} - /> - - { - const str: string = record?.new_values?.status ?? record?.event ?? ""; - - return str.replaceAll("-", " "); + + + + + + + + + + + + + + { + loadEntityList(); + refetch(); }} + record={selected} + polygonList={entityListItem} + selectedPolygon={selected} + setSelectedPolygon={setSelected} + auditLogData={auditLogData?.data} + checkPolygonsSite={checkPolygonsSite} /> - } - /> - - + + ); diff --git a/src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogSiteTab.tsx b/src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogSiteTab.tsx new file mode 100644 index 000000000..a67b150be --- /dev/null +++ b/src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogSiteTab.tsx @@ -0,0 +1,111 @@ +import { Grid, Stack } from "@mui/material"; +import { FC, useEffect, useState } from "react"; +import { Button, Link, TabbedShowLayout, TabProps, useBasename, useShowContext } from "react-admin"; +import { When } from "react-if"; + +import modules from "@/admin/modules"; +import Text from "@/components/elements/Text/Text"; +import { SITE } from "@/constants/entities"; +import useAuditLogActions from "@/hooks/AuditStatus/useAuditLogActions"; +import { Entity } from "@/types/common"; + +import SiteAuditLogEntityStatus from "../components/SiteAuditLogEntityStatus"; +import SiteAuditLogEntityStatusSide from "../components/SiteAuditLogEntityStatusSide"; +import AuditLogSiteTabSelection from "./AuditLogSiteTabSelection"; + +interface IProps extends Omit { + label?: string; + entity?: Entity["entityName"]; +} + +export const ButtonStates = { + PROJECTS: 0, + SITE: 1, + POLYGON: 2 +}; + +const AuditLogSiteTab: FC = ({ label, entity, ...rest }) => { + const { record, isLoading } = useShowContext(); + const basename = useBasename(); + const [buttonToogle, setButtonToogle] = useState(ButtonStates.PROJECTS); + + const { + mutateEntity, + valuesForStatus, + statusLabels, + entityType, + entityListItem, + loadEntityList, + selected, + setSelected, + auditLogData, + refetch, + checkPolygonsSite + } = useAuditLogActions({ + record, + buttonToogle, + entityLevel: SITE + }); + + useEffect(() => { + refetch(); + loadEntityList(); + }, [buttonToogle]); + + return ( + + + + + + + + Project Status + + Update the site status, view updates, or add comments + + + + + + ); +}; + +export default AuditLogSiteTabSelection; diff --git a/src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogTable.tsx b/src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogTable.tsx new file mode 100644 index 000000000..7b7a46d31 --- /dev/null +++ b/src/admin/components/ResourceTabs/AuditLogTab/components/AuditLogTable.tsx @@ -0,0 +1,80 @@ +import { FC, Fragment } from "react"; + +import { convertDateFormat } from "@/admin/apiProvider/utils/entryFormat"; +import Text from "@/components/elements/Text/Text"; +import { AuditStatusResponse, V2FileRead } from "@/generated/apiSchemas"; + +const formattedTextStatus = (text: string) => { + return text.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase()); +}; + +const getTextForActionTable = (item: { type: string; status: string; request_removed: boolean }): string => { + if (item.type === "comment") { + return "New Comment"; + } else if (item.type === "status") { + return `New Status: ${formattedTextStatus(item.status)}`; + } else if (item.request_removed) { + return "Change Request Removed"; + } else { + return "Change Requested Added"; + } +}; + +const AuditLogTable: FC<{ auditLogData: { data: AuditStatusResponse[] } }> = ({ auditLogData }) => { + return ( + +
+ + Date + + + User + + + Action + + + Comments + + + Attachments + +
+
+ {auditLogData?.data?.map((item: AuditStatusResponse, index: number) => ( + + + {convertDateFormat(item?.date_created)} + + + {`${item.first_name} ${item.last_name}`} + + + {getTextForActionTable(item as { type: string; status: string; request_removed: boolean })} + + + {item.comment || "-"} + +
+ {item?.attachments?.map((attachmentItem: V2FileRead) => ( + { + attachmentItem.url && window.open(attachmentItem.url, "_blank"); + }} + > + {attachmentItem.file_name} + + ))} +
+
+ ))} +
+
+ ); +}; + +export default AuditLogTable; diff --git a/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogEntityStatus.tsx b/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogEntityStatus.tsx new file mode 100644 index 000000000..258deba30 --- /dev/null +++ b/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogEntityStatus.tsx @@ -0,0 +1,92 @@ +import Link from "next/link"; +import { FC } from "react"; +import { Link as RaLink, useBasename } from "react-admin"; + +import modules from "@/admin/modules"; +import Text from "@/components/elements/Text/Text"; +import { AuditStatusResponse } from "@/generated/apiSchemas"; + +import CommentarySection from "../../PolygonReviewTab/components/CommentarySection/CommentarySection"; +import AuditLogTable from "./AuditLogTable"; + +export interface SiteAuditLogEntityStatusProps { + record?: SelectedItem | null; + auditLogData?: { data: AuditStatusResponse[] }; + refresh?: () => void; + entityType?: number; + entityName?: string; + buttonToogle?: number; + buttonStates?: { PROJECTS: number; SITE: number; POLYGON: number }; + viewPD?: boolean; +} + +interface SelectedItem { + title?: string | undefined; + name?: string | undefined; + uuid?: string | undefined; + value?: string | undefined; + meta?: string | undefined; + status?: string | undefined; +} + +const SiteAuditLogEntityStatus: FC = ({ + record, + auditLogData, + refresh, + buttonToogle, + buttonStates, + viewPD = true +}) => { + const entityType = buttonToogle === buttonStates?.POLYGON; + const isSite = buttonToogle === buttonStates?.SITE; + const basename = useBasename(); + const title = () => { + if (!record?.title) { + return record?.name; + } else { + return record?.title; + } + }; + const redirectTo = viewPD + ? `/site/${record?.uuid}?tab=audit-log` + : `${basename}/${modules.site.ResourceName}/${record?.uuid}/show/6`; + return ( +
+
+ + {entityType ? "Polygon" : "Site"} Status and Comments + + + Update the {entityType ? "polygon" : "site"} status, view updates, or add comments + + +
+
+ {!isSite && History and Discussion for {title()}} + {isSite && ( + + History and Discussion for{" "} + {viewPD ? ( + + {title()} + + ) : ( + + {title()} + + )} + + )} +
+ {auditLogData && } +
+ ); +}; + +export default SiteAuditLogEntityStatus; diff --git a/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogEntityStatusSide.tsx b/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogEntityStatusSide.tsx new file mode 100644 index 000000000..9a3822c5c --- /dev/null +++ b/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogEntityStatusSide.tsx @@ -0,0 +1,131 @@ +import classNames from "classnames"; +import { Dispatch, SetStateAction, useState } from "react"; + +import Dropdown from "@/components/elements/Inputs/Dropdown/Dropdown"; +import Notification from "@/components/elements/Notification/Notification"; +import StepProgressbar from "@/components/elements/ProgressBar/StepProgressbar/StepProgressbar"; +import Text from "@/components/elements/Text/Text"; +import { usePostV2AuditStatus } from "@/generated/apiComponents"; +import { AuditStatusResponse } from "@/generated/apiSchemas"; +import { SelectedItem } from "@/hooks/AuditStatus/useLoadEntityList"; +import { recentRequestData } from "@/utils/statusUtils"; + +import StatusDisplay from "../../PolygonReviewTab/components/PolygonStatus/StatusDisplay"; + +const SiteAuditLogEntityStatusSide = ({ + refresh, + record, + polygonList, + selectedPolygon, + setSelectedPolygon, + auditLogData, + recordType = "Polygon", + mutate, + getValueForStatus, + progressBarLabels, + tab, + checkPolygonsSite, + viewPD = false +}: { + recordType?: "Polygon" | "Site" | "Project"; + refresh?: () => void; + record?: any; + polygonList?: any[]; + selectedPolygon?: SelectedItem | null; + setSelectedPolygon?: Dispatch> | null; + auditLogData?: AuditStatusResponse[]; + mutate?: any; + getValueForStatus?: (status: string) => number; + progressBarLabels?: Array<{ id: string; label: string }>; + tab?: string; + checkPolygonsSite?: boolean | undefined; + viewPD?: boolean; +}) => { + const [open, setOpen] = useState(false); + const recentRequest = auditLogData?.find( + (item: AuditStatusResponse) => item.type == "change-request" && item.is_active + ); + const mutateUpload = recordType === "Project" ? usePostV2AuditStatus : usePostV2AuditStatus; + const { mutate: upload } = mutateUpload(); + + const deactivateRecentRequest = async () => { + upload?.( + { + //@ts-ignore swagger issue + body: { + // entity_uuid: record?.uuid, + status: record?.status, + // entity: recordType === "Polygon" ? "SitePolygon" : recordType, + comment: "", + type: "change-request", + // is_active: false, + request_removed: true + } + }, + { + onSuccess: () => { + setOpen(true); + setTimeout(() => { + setOpen(false); + }, 3000); + refresh && refresh(); + } + } + ); + }; + + return ( +
+ {polygonList && polygonList?.length > 0 && ( + { + console.log("onChange", e); + setSelectedPolygon && setSelectedPolygon(polygonList?.find(item => item?.uuid === e[0])); + }} + /> + )} + {`${recordType} Status`} + + {recentRequest && ( +
+
+
+ Change Requested + +
+ {recentRequestData(recentRequest)} +
+ {recentRequest?.comment} +
+ )} + + +
+ ); +}; + +export default SiteAuditLogEntityStatusSide; diff --git a/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogProjectStatus.tsx b/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogProjectStatus.tsx new file mode 100644 index 000000000..cb391423b --- /dev/null +++ b/src/admin/components/ResourceTabs/AuditLogTab/components/SiteAuditLogProjectStatus.tsx @@ -0,0 +1,83 @@ +import { FC } from "react"; + +import Text from "@/components/elements/Text/Text"; +import { AuditStatusResponse, ProjectLiteRead } from "@/generated/apiSchemas"; + +import CommentarySection from "../../PolygonReviewTab/components/CommentarySection/CommentarySection"; +import AuditLogTable from "./AuditLogTable"; + +export interface SiteAuditLogProjectStatusProps { + record?: ProjectLiteRead | null; + refresh?: () => void; + auditLogData?: { data: AuditStatusResponse[] }; +} + +export const gridData = [ + { + id: "1", + date: "28/11/2023 09.39", + user: "Jessica Chaimers", + site: null, + status: "Need More Information", + comentary: null + }, + { + id: "2", + date: "28/11/2023 09.39", + user: "Teresa Muthoni", + site: null, + status: "Need More Information", + comentary: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + }, + { + id: "3", + date: "28/11/2023 09.39", + user: "Jessica Chaimers", + site: null, + status: "Awaiting Approval", + comentary: null + }, + { + id: "4", + date: "28/11/2023 09.39", + user: "Jessica Chaimers", + site: null, + status: "Awaiting Approval", + comentary: null + }, + { + id: "5", + date: "28/11/2023 09.39", + user: "Jessica Chaimers", + site: null, + status: "Awaiting Approval", + comentary: null + } +]; + +const SiteAuditLogProjectStatus: FC = ({ record, auditLogData, refresh }) => { + return ( +
+
+ + Project Status and Comments + + + Update the project status, view updates, or add comments + + +
+ History and Discussion for {record && record?.name} + {auditLogData && } +
+ ); +}; + +export default SiteAuditLogProjectStatus; diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonStatus/StatusDisplay.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonStatus/StatusDisplay.tsx index a5aae6936..34912298e 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonStatus/StatusDisplay.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonStatus/StatusDisplay.tsx @@ -8,69 +8,87 @@ import ModalConfirm from "@/components/extensive/Modal/ModalConfirm"; import { useModalContext } from "@/context/modal.provider"; const menuPolygonOptions = [ + { + title: "Draft", + status: "draft", + value: 1, + viewPd: false + }, { title: "Submitted", status: "submitted", - value: 1 + value: 2, + viewPd: true }, { title: "Needs More Information", status: "needs-more-information", - value: 2 + value: 3, + viewPd: false }, { title: "Approved", status: "approved", - value: 3 + value: 4, + viewPd: false } ]; const menuSiteOptions = [ { title: "Draft", status: "draft", - value: 1 + value: 1, + viewPd: true }, { title: "Awaiting Approval", status: "awaiting-approval", - value: 2 + value: 2, + viewPd: true }, { title: "Needs More Information", status: "needs-more-information", - value: 3 + value: 3, + viewPd: false }, { title: "Planting in Progress", status: "planting-in-progress", - value: 4 + value: 4, + viewPd: false }, { title: "Approved", status: "approved", - value: 5 + value: 5, + viewPd: false } ]; const menuProjectOptions = [ { title: "Draft", status: "draft", - value: 1 + value: 1, + viewPd: true }, { title: "Awaiting Approval", status: "awaiting-approval", - value: 2 + value: 2, + viewPd: true }, { title: "Needs More Information", status: "needs-more-information", - value: 3 + value: 3, + viewPd: false }, { title: "Approved", status: "approved", - value: 4 + value: 4, + viewPd: false } ]; @@ -78,10 +96,12 @@ export interface StatusProps { titleStatus: "Site" | "Project" | "Polygon"; mutate?: any; record?: any; - refresh?: any; + refresh?: () => void; name: any; - refetchPolygon?: any; - setSelectedPolygon?: any; + refetchPolygon?: () => void; + tab?: string; + checkPolygonsSite?: boolean | undefined; + viewPD?: boolean; } const menuOptionsMap = { @@ -102,7 +122,16 @@ const DescriptionRequestMap = { Project: "Provide an explanation for your change request for the project" }; -const StatusDisplay = ({ titleStatus = "Polygon", mutate, refresh, name, record, setSelectedPolygon }: StatusProps) => { +const StatusDisplay = ({ + titleStatus = "Polygon", + mutate, + refresh, + name, + record, + checkPolygonsSite, + tab, + viewPD +}: StatusProps) => { const { refetch: reloadEntity } = useShowContext(); const [notificationStatus, setNotificationStatus] = useState<{ open: boolean; @@ -127,20 +156,23 @@ const StatusDisplay = ({ titleStatus = "Polygon", mutate, refresh, name, record, {DescriptionRequestMap[titleStatus]} {name}? ); - + const filterViewPd = viewPD + ? menuOptionsMap[titleStatus].filter(option => option.viewPd === true) + : menuOptionsMap[titleStatus]; const openFormModalHandlerStatus = () => { openModal( { const option = menuOptionsMap[titleStatus].find(option => option.value === opt[0]); try { - const response = await mutate({ + await mutate({ pathParams: { uuid: record?.uuid }, body: { status: option?.status, @@ -148,9 +180,6 @@ const StatusDisplay = ({ titleStatus = "Polygon", mutate, refresh, name, record, type: "status" } }); - if (response.poly_id) { - setSelectedPolygon(response?.poly_id); - } setNotificationStatus({ open: true, message: "Your Status Update was just saved!", @@ -172,10 +201,19 @@ const StatusDisplay = ({ titleStatus = "Polygon", mutate, refresh, name, record, type: "error", title: "Error!" }); + setTimeout(() => { + setNotificationStatus({ + open: false, + message: "", + type: "error", + title: "Error!" + }); + }, 3000); + console.error(e); } finally { - refresh(); - reloadEntity(); - closeModal(); + refresh && refresh(); + reloadEntity && reloadEntity(); + closeModal; } }} /> @@ -223,10 +261,19 @@ const StatusDisplay = ({ titleStatus = "Polygon", mutate, refresh, name, record, type: "error", title: "Error!" }); + setTimeout(() => { + setNotificationStatus({ + open: false, + message: "", + type: "error", + title: "Error!" + }); + }, 3000); + console.error(e); } finally { - refresh(); - reloadEntity(); - closeModal(); + refresh && refresh(); + reloadEntity && reloadEntity(); + closeModal; } }} /> @@ -237,11 +284,12 @@ const StatusDisplay = ({ titleStatus = "Polygon", mutate, refresh, name, record,