diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/CommentarySection/CommentarySection.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/CommentarySection/CommentarySection.tsx index 8f7ba714f..f9433b075 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/components/CommentarySection/CommentarySection.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/CommentarySection/CommentarySection.tsx @@ -5,6 +5,7 @@ import CommentaryBox from "@/components/elements/CommentaryBox/CommentaryBox"; import Text from "@/components/elements/Text/Text"; import Loader from "@/components/generic/Loading/Loader"; import { useGetAuthMe } from "@/generated/apiComponents"; +import { AuditStatusResponse } from "@/generated/apiSchemas"; const CommentarySection = ({ auditLogData, @@ -12,14 +13,14 @@ const CommentarySection = ({ record, entity, viewCommentsList = true, - attachmentRefetch + loading = false }: { - auditLogData?: any; - refresh?: any; + auditLogData?: AuditStatusResponse[]; + refresh?: () => void; record?: any; entity?: "Project" | "SitePolygon" | "Site"; viewCommentsList?: boolean; - attachmentRefetch?: any; + loading?: boolean; }) => { const { data: authMe } = useGetAuthMe({}) as { data: { @@ -40,25 +41,24 @@ const CommentarySection = ({ entity={entity} /> -
- {auditLogData ? ( - auditLogData.length > 0 ? ( - auditLogData - .filter((item: any) => item.type === "comment") - .map((item: any) => ( - - )) - ) : ( - <> - ) - ) : ( +
+ {loading ? ( + ) : auditLogData && auditLogData.length > 0 ? ( + auditLogData + .filter(item => item.type === "comment") + .map((item: any) => ( + + )) + ) : ( + <> )}
diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/PolygonDrawer.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/PolygonDrawer.tsx new file mode 100644 index 000000000..4785cb456 --- /dev/null +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/PolygonDrawer.tsx @@ -0,0 +1,255 @@ +import { Divider } from "@mui/material"; +import { useEffect, useState } from "react"; +import { Else, If, Then, When } from "react-if"; + +import Accordion from "@/components/elements/Accordion/Accordion"; +import Button from "@/components/elements/Button/Button"; +import Status, { StatusEnum } from "@/components/elements/Status/Status"; +import Text from "@/components/elements/Text/Text"; +import { useSitePolygonData } from "@/context/sitePolygon.provider"; +import { + fetchGetV2TerrafundValidationPolygon, + fetchPutV2SitePolygonUUID, + GetV2AuditStatusResponse, + useGetV2AuditStatus, + useGetV2TerrafundValidationCriteriaData +} from "@/generated/apiComponents"; +import { SitePolygon } from "@/generated/apiSchemas"; + +import CommentarySection from "../CommentarySection/CommentarySection"; +import StatusDisplay from "../PolygonStatus/StatusDisplay "; +import AttributeInformation from "./components/AttributeInformation"; +import PolygonValidation from "./components/PolygonValidation"; +import VersionHistory from "./components/VersionHistory"; + +const statusColor: Record = { + draft: "bg-pinkCustom", + submitted: "bg-blue", + approved: "bg-green", + "needs-more-information": "bg-tertiary-600" +}; + +const validationLabels: any = { + 3: "No Overlapping Polygon", + 4: "No Self-Intersection", + 6: "Inside Size Limit", + 7: "Within Country", + 8: "No Spike", + 10: "Polygon Type", + 12: "Within Total Area Expected", + 14: "Data Completed" +}; + +export interface ICriteriaCheckItem { + id: string; + status: boolean; + label: string; + date?: string; +} + +const ESTIMATED_AREA_CRITERIA_ID = 12; + +const PolygonDrawer = ({ + polygonSelected, + isPolygonStatusOpen, + refresh +}: { + polygonSelected: string; + isPolygonStatusOpen: any; + refresh?: () => void; +}) => { + const [buttonToogle, setButtonToogle] = useState(true); + const [selectedPolygonData, setSelectedPolygonData] = useState(); + const [statusSelectedPolygon, setStatusSelectedPolygon] = useState(""); + const [openAttributes, setOpenAttributes] = useState(true); + const [checkPolygonValidation, setCheckPolygonValidation] = useState(false); + const [validationStatus, setValidationStatus] = useState(false); + const [polygonValidationData, setPolygonValidationData] = useState(); + const [criteriaValidation, setCriteriaValidation] = useState(); + + const context = useSitePolygonData(); + const sitePolygonData = context?.sitePolygonData; + const openEditNewPolygon = context?.isUserDrawingEnabled; + const selectedPolygon = (sitePolygonData as any as Array)?.find( + (item: SitePolygon) => item?.poly_id === polygonSelected + ); + const mutateSitePolygons = fetchPutV2SitePolygonUUID; + const { data: criteriaData, refetch: reloadCriteriaValidation } = useGetV2TerrafundValidationCriteriaData( + { + queryParams: { + uuid: polygonSelected + } + }, + { + enabled: !!polygonSelected + } + ); + + const { + data: auditLogData, + refetch, + isLoading + } = useGetV2AuditStatus<{ data: GetV2AuditStatusResponse }>({ + queryParams: { + entity: "SitePolygon", + uuid: selectedPolygon?.uuid ?? "" + } + }); + + const validatePolygon = () => { + fetchGetV2TerrafundValidationPolygon({ + queryParams: { + uuid: polygonSelected + } + }).then(() => { + reloadCriteriaValidation(); + setCheckPolygonValidation(false); + }); + }; + + useEffect(() => { + if (checkPolygonValidation) { + validatePolygon(); + reloadCriteriaValidation(); + } + }, [checkPolygonValidation]); + + useEffect(() => { + setButtonToogle(!isPolygonStatusOpen); + }, [isPolygonStatusOpen]); + + useEffect(() => { + if (criteriaData && criteriaData.criteria_list) { + const transformedData: ICriteriaCheckItem[] = criteriaData.criteria_list.map((criteria: any) => ({ + id: criteria.criteria_id, + date: criteria.latest_created_at, + status: criteria.valid === 1, + label: validationLabels[criteria.criteria_id] + })); + setPolygonValidationData(transformedData); + setValidationStatus(true); + } else { + setValidationStatus(false); + } + }, [criteriaData]); + + useEffect(() => { + if (sitePolygonData && Array.isArray(sitePolygonData)) { + const PolygonData = sitePolygonData.find((data: SitePolygon) => data.poly_id === polygonSelected); + setSelectedPolygonData(PolygonData || {}); + setStatusSelectedPolygon(PolygonData?.status || ""); + } else { + setSelectedPolygonData({}); + setStatusSelectedPolygon(""); + } + }, [polygonSelected, sitePolygonData]); + useEffect(() => { + console.log("openEditNewPolygon", openEditNewPolygon); + if (openEditNewPolygon) { + setButtonToogle(true); + setOpenAttributes(true); + } + }, [openEditNewPolygon]); + + const isValidCriteriaData = (criteriaData: any) => { + if (!criteriaData?.criteria_list?.length) { + return true; + } + return criteriaData.criteria_list.some( + (criteria: any) => criteria.criteria_id !== ESTIMATED_AREA_CRITERIA_ID && criteria.valid !== 1 + ); + }; + + useEffect(() => { + const fetchCriteriaValidation = async () => { + if (!buttonToogle) { + const criteriaData = await fetchGetV2TerrafundValidationPolygon({ + queryParams: { + uuid: polygonSelected + } + }); + setCriteriaValidation(criteriaData); + } + }; + + fetchCriteriaValidation(); + }, [buttonToogle, selectedPolygonData]); + + return ( +
+
+ {`Polygon ID: ${selectedPolygonData?.id}`} + + {selectedPolygonData?.poly_name ? selectedPolygonData?.poly_name : "Unnamed Polygon"} +
+ +
+
+ + +
+ + +
+
+ + Status: + + + + +
+ + +
+
+ +
+ + + + + + {selectedPolygonData && } + + + + + + +
+
+
+
+ ); +}; + +export default PolygonDrawer; diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/AttributeInformation.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/AttributeInformation.tsx new file mode 100644 index 000000000..1136bf0ec --- /dev/null +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/AttributeInformation.tsx @@ -0,0 +1,254 @@ +import { useT } from "@transifex/react"; +import { useEffect, useState } from "react"; + +import Button from "@/components/elements/Button/Button"; +import Dropdown from "@/components/elements/Inputs/Dropdown/Dropdown"; +import Input from "@/components/elements/Inputs/Input/Input"; +import Text from "@/components/elements/Text/Text"; +import { useSitePolygonData } from "@/context/sitePolygon.provider"; +import { fetchPutV2TerrafundSitePolygonUuid } from "@/generated/apiComponents"; +import { SitePolygon } from "@/generated/apiSchemas"; + +const dropdownOptionsRestoration = [ + { + title: "Tree Planting", + value: "Tree Planting" + }, + { + title: "Direct Seeding", + value: "Direct Seeding" + }, + { + title: "Assisted Natural Regeneration", + value: "Assisted Natural Regeneration" + } +]; +const dropdownOptionsTarget = [ + { + title: "Agroforest", + value: "Agroforest" + }, + { + title: "Natural Forest", + value: "Natural Forest" + }, + { + title: "Mangrove", + value: "Mangrove" + }, + { + title: "Peatland", + value: "Peatland" + }, + { + title: "Riparian Area or Wetland", + value: "Riparian Area or Wetland" + }, + { + title: "Silvopasture", + value: "Silvopasture" + }, + { + title: "Woodlot or Plantation", + value: "Woodlot or Plantation" + }, + { + title: "Urban Forest", + value: "Urban Forest" + } +]; + +const dropdownOptionsTree = [ + { + title: "Single Line", + value: "Single Line" + }, + { + title: "Partial", + value: "Partial" + }, + { + title: "Full Coverage", + value: "Full Coverage" + } +]; +const AttributeInformation = ({ selectedPolygon }: { selectedPolygon: SitePolygon }) => { + const [polygonName, setPolygonName] = useState(); + const [plantStartDate, setPlantStartDate] = useState(); + const [plantEndDate, setPlantEndDate] = useState(); + const [restorationPractice, setRestorationPractice] = useState([]); + const [targetLandUseSystem, setTargetLandUseSystem] = useState([]); + const [treeDistribution, setTreeDistribution] = useState([]); + const [treesPlanted, setTreesPlanted] = useState(selectedPolygon?.num_trees); + const [calculatedArea, setCalculatedArea] = useState(selectedPolygon?.calc_area || 0); + const [formattedArea, setFormattedArea] = useState(); + const contextSite = useSitePolygonData(); + const reloadSiteData = contextSite?.reloadSiteData; + + const t = useT(); + + useEffect(() => { + setPolygonName(selectedPolygon?.poly_name || ""); + setPlantStartDate(selectedPolygon?.plantstart || ""); + setPlantEndDate(selectedPolygon?.plantend || ""); + setTreesPlanted(selectedPolygon?.num_trees || 0); + setCalculatedArea(selectedPolygon?.calc_area || 0); + const restorationPracticeArray = selectedPolygon?.practice + ? selectedPolygon?.practice.split(",").map(function (item) { + return item.trim(); + }) + : []; + setRestorationPractice(restorationPracticeArray); + + const targetLandUseSystemArray = selectedPolygon?.target_sys + ? selectedPolygon?.target_sys.split(",").map(function (item) { + return item.trim(); + }) + : []; + setTargetLandUseSystem(targetLandUseSystemArray); + + const treeDistributionArray = selectedPolygon?.distr + ? selectedPolygon?.distr.split(",").map(function (item) { + return item.trim(); + }) + : []; + setTreeDistribution(treeDistributionArray); + }, [selectedPolygon]); + + useEffect(() => { + const format = + calculatedArea && calculatedArea.toLocaleString("UTC", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + setFormattedArea(format || ""); + }, [calculatedArea]); + + const savePolygonData = async () => { + if (selectedPolygon?.uuid) { + const restorationPracticeToSend = restorationPractice.join(", "); + const landUseSystemToSend = targetLandUseSystem.join(", "); + const treeDistributionToSend = treeDistribution.join(", "); + const updatedPolygonData = { + poly_name: polygonName, + plantstart: plantStartDate, + plantend: plantEndDate, + practice: restorationPracticeToSend, + target_sys: landUseSystemToSend, + distr: treeDistributionToSend, + num_trees: treesPlanted + }; + try { + await fetchPutV2TerrafundSitePolygonUuid({ + body: updatedPolygonData, + pathParams: { uuid: selectedPolygon.uuid } + }); + if (reloadSiteData) { + reloadSiteData(); + } + } catch (error) { + console.error("Error updating polygon data:", error); + } + } + }; + + return ( +
+ setPolygonName((e.target as HTMLInputElement).value)} + /> + + + setRestorationPractice(e as string[])} + options={dropdownOptionsRestoration} + /> + setTargetLandUseSystem(e as string[])} + /> + setTreeDistribution(e as string[])} + /> + ) => setTreesPlanted(Number(e.target.value))} + /> + +
+ + +
+
+ ); +}; + +export default AttributeInformation; diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/PolygonReviewButtons.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/PolygonReviewButtons.tsx new file mode 100644 index 000000000..06f89ca0f --- /dev/null +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/PolygonReviewButtons.tsx @@ -0,0 +1,109 @@ +import React from "react"; + +import Button from "@/components/elements/Button/Button"; +import Menu from "@/components/elements/Menu/Menu"; +import StepProgressbar from "@/components/elements/ProgressBar/StepProgressbar/StepProgressbar"; +import Text from "@/components/elements/Text/Text"; +import { IconNames } from "@/components/extensive/Icon/Icon"; +import { useSitePolygonData } from "@/context/sitePolygon.provider"; + +const PolygonReviewButtons = ({ + openFormModalHandlerAddPolygon, + downloadSiteGeoJsonPolygons, + openFormModalHandlerSubmitPolygon, + record, + openFormModalHandlerUploadImages +}: { + openFormModalHandlerAddPolygon: () => void; + downloadSiteGeoJsonPolygons: (uuid: string) => void; + openFormModalHandlerSubmitPolygon: () => void; + record: { uuid: string }; + openFormModalHandlerUploadImages: () => void; +}) => { + const context = useSitePolygonData(); + const { toggleUserDrawing } = context || {}; + + const addMenuItems = [ + { + id: "1", + render: () => Create Polygons, + onClick: () => toggleUserDrawing?.(true) + }, + { + id: "2", + render: () => Add Polygon Data, + onClick: openFormModalHandlerAddPolygon + }, + { + id: "3", + render: () => Upload Images, + onClick: openFormModalHandlerUploadImages + } + ]; + + const polygonStatusLabels = [ + { id: "1", label: "Draft" }, + { id: "2", label: "Awaiting Approval" }, + { id: "3", label: "Needs More Information" }, + { id: "4", label: "Planting In Progress" }, + { id: "5", label: "Approved" } + ]; + + return ( +
+
+
+ + Polygon Review + + + Add, remove or edit polygons that are associated to a site. Polygons may be edited in the map below; + exported, modified in QGIS or ArcGIS and imported again; or fed through the mobile application. + +
+
+ + + + + +
+
+
+ + Site Status + +
+ +
+
+
+ ); +}; + +export default PolygonReviewButtons; diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonReviewAside/index.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonReviewAside/index.tsx new file mode 100644 index 000000000..2e34ad885 --- /dev/null +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonReviewAside/index.tsx @@ -0,0 +1,27 @@ +import { Stack } from "@mui/material"; + +import Polygons, { IpolygonFromMap, IPolygonItem } from "../Polygons"; + +interface SitePolygonReviewAsideProps { + data: IPolygonItem[]; + polygonFromMap?: IpolygonFromMap; + setPolygonFromMap?: any; + refresh?: () => void; + mapFunctions: any; +} + +const SitePolygonReviewAside = (props: SitePolygonReviewAsideProps) => { + return ( + + + + ); +}; + +export default SitePolygonReviewAside; diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonStatus/StatusDisplay .tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonStatus/StatusDisplay .tsx new file mode 100644 index 000000000..34912298e --- /dev/null +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonStatus/StatusDisplay .tsx @@ -0,0 +1,304 @@ +import { useState } from "react"; +import { useShowContext } from "react-admin"; + +import Button from "@/components/elements/Button/Button"; +import Notification from "@/components/elements/Notification/Notification"; +import Text from "@/components/elements/Text/Text"; +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: 2, + viewPd: true + }, + { + title: "Needs More Information", + status: "needs-more-information", + value: 3, + viewPd: false + }, + { + title: "Approved", + status: "approved", + value: 4, + viewPd: false + } +]; +const menuSiteOptions = [ + { + title: "Draft", + status: "draft", + value: 1, + viewPd: true + }, + { + title: "Awaiting Approval", + status: "awaiting-approval", + value: 2, + viewPd: true + }, + { + title: "Needs More Information", + status: "needs-more-information", + value: 3, + viewPd: false + }, + { + title: "Planting in Progress", + status: "planting-in-progress", + value: 4, + viewPd: false + }, + { + title: "Approved", + status: "approved", + value: 5, + viewPd: false + } +]; +const menuProjectOptions = [ + { + title: "Draft", + status: "draft", + value: 1, + viewPd: true + }, + { + title: "Awaiting Approval", + status: "awaiting-approval", + value: 2, + viewPd: true + }, + { + title: "Needs More Information", + status: "needs-more-information", + value: 3, + viewPd: false + }, + { + title: "Approved", + status: "approved", + value: 4, + viewPd: false + } +]; + +export interface StatusProps { + titleStatus: "Site" | "Project" | "Polygon"; + mutate?: any; + record?: any; + refresh?: () => void; + name: any; + refetchPolygon?: () => void; + tab?: string; + checkPolygonsSite?: boolean | undefined; + viewPD?: boolean; +} + +const menuOptionsMap = { + Polygon: menuPolygonOptions, + Site: menuSiteOptions, + Project: menuProjectOptions +}; + +const DescriptionStatusMap = { + Polygon: "Are you sure you want to change the polygon status to", + Site: "Are you sure you want to change the site status to", + Project: "Are you sure you want to change the project status to" +}; + +const DescriptionRequestMap = { + Polygon: "Provide an explanation for your change request for the polygon", + Site: "Provide an explanation for your change request for the site", + Project: "Provide an explanation for your change request for the project" +}; + +const StatusDisplay = ({ + titleStatus = "Polygon", + mutate, + refresh, + name, + record, + checkPolygonsSite, + tab, + viewPD +}: StatusProps) => { + const { refetch: reloadEntity } = useShowContext(); + const [notificationStatus, setNotificationStatus] = useState<{ + open: boolean; + message: string; + type: "success" | "error" | "warning"; + title: string; + }>({ + open: false, + message: "", + type: "success", + title: "Success!" + }); + + const { openModal, closeModal } = useModalContext(); + const contentStatus = ( + + {DescriptionStatusMap[titleStatus]} {name}? + + ); + const contentRequest = ( + + {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 { + await mutate({ + pathParams: { uuid: record?.uuid }, + body: { + status: option?.status, + comment: text, + type: "status" + } + }); + setNotificationStatus({ + open: true, + message: "Your Status Update was just saved!", + type: "success", + title: "Success!" + }); + setTimeout(() => { + setNotificationStatus({ + open: false, + message: "", + type: "success", + title: "Success!" + }); + }, 3000); + } catch (e) { + setNotificationStatus({ + open: true, + message: "The request encountered an issue, or the comment exceeds 255 characters.", + type: "error", + title: "Error!" + }); + setTimeout(() => { + setNotificationStatus({ + open: false, + message: "", + type: "error", + title: "Error!" + }); + }, 3000); + console.error(e); + } finally { + refresh && refresh(); + reloadEntity && reloadEntity(); + closeModal; + } + }} + /> + ); + }; + + const openFormModalHandlerRequest = () => { + openModal( + { + const option = menuOptionsMap[titleStatus].find(option => option.value === opt[0]); + try { + await mutate({ + pathParams: { uuid: record?.uuid }, + body: { + status: option?.status, + comment: text, + type: "change-request", + is_active: true, + request_removed: false + } + }); + setNotificationStatus({ + open: true, + message: "Your Change Request was just added!", + type: "success", + title: "Success!" + }); + setTimeout(() => { + setNotificationStatus({ + open: false, + message: "", + type: "success", + title: "Success!" + }); + }, 3000); + } catch (e) { + setNotificationStatus({ + open: true, + message: "The request encountered an issue, or the comment exceeds 255 characters.", + type: "error", + title: "Error!" + }); + setTimeout(() => { + setNotificationStatus({ + open: false, + message: "", + type: "error", + title: "Error!" + }); + }, 3000); + console.error(e); + } finally { + refresh && refresh(); + reloadEntity && reloadEntity(); + closeModal; + } + }} + /> + ); + }; + return ( + <> +
+
+ + +
+
+ + + ); +}; + +export default StatusDisplay; diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/Polygons.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/Polygons.tsx new file mode 100644 index 000000000..319e833fc --- /dev/null +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/Polygons.tsx @@ -0,0 +1,251 @@ +import React, { useEffect, useRef, useState } from "react"; + +import Button from "@/components/elements/Button/Button"; +import Drawer from "@/components/elements/Drawer/Drawer"; +import Menu from "@/components/elements/Menu/Menu"; +import { MENU_PLACEMENT_LEFT_BOTTOM } from "@/components/elements/Menu/MenuVariant"; +import { MENU_ITEM_VARIANT_DIVIDER } from "@/components/elements/MenuItem/MenuItemVariant"; +import Text from "@/components/elements/Text/Text"; +import Icon from "@/components/extensive/Icon/Icon"; +import { IconNames } from "@/components/extensive/Icon/Icon"; +import ModalConfirm from "@/components/extensive/Modal/ModalConfirm"; +import { useModalContext } from "@/context/modal.provider"; +import { useSitePolygonData } from "@/context/sitePolygon.provider"; +import { + fetchDeleteV2TerrafundPolygonUuid, + fetchGetV2TerrafundGeojsonComplete, + fetchGetV2TerrafundPolygonBboxUuid +} from "@/generated/apiComponents"; + +import PolygonDrawer from "./PolygonDrawer/PolygonDrawer"; + +export interface IPolygonItem { + id: string; + status: "draft" | "submitted" | "approved" | "needs-more-information"; + label: string; + uuid: string; +} + +export interface IpolygonFromMap { + isOpen: boolean; + uuid: string; +} +export interface IPolygonProps { + menu: IPolygonItem[]; + polygonFromMap?: IpolygonFromMap; + setPolygonFromMap?: any; + refresh?: () => void; + mapFunctions: any; +} +const statusColor = { + draft: "bg-pinkCustom", + submitted: "bg-blue", + approved: "bg-green", + "needs-more-information": "bg-tertiary-600" +}; + +export const polygonData = [ + { id: "1", name: "Site-polygon001.geojson", status: "We are processing your polygon", isUploaded: false }, + { id: "2", name: "Site-polygon002.geojson", status: "We are processing your polygon", isUploaded: false }, + { id: "3", name: "Site-polygon003.geojson", status: "We are processing your polygon", isUploaded: true }, + { id: "4", name: "Site-polygon004.geojson", status: "We are processing your polygon", isUploaded: true }, + { id: "5", name: "Site-polygon005.geojson", status: "We are processing your polygon", isUploaded: true } +]; + +const Polygons = (props: IPolygonProps) => { + const [isOpenPolygonDrawer, setIsOpenPolygonDrawer] = useState(false); + const [polygonMenu, setPolygonMenu] = useState(props.menu); + const { polygonFromMap, setPolygonFromMap, mapFunctions } = props; + const { map } = mapFunctions; + const containerRef = useRef(null); + const { openModal, closeModal } = useModalContext(); + const [selectedPolygon, setSelectedPolygon] = useState(); + const [isPolygonStatusOpen, setIsPolygonStatusOpen] = useState(false); + const context = useSitePolygonData(); + const reloadSiteData = context?.reloadSiteData; + const { toggleUserDrawing } = context || {}; + + useEffect(() => { + setPolygonMenu(props.menu); + }, [props.menu]); + useEffect(() => { + if (!isOpenPolygonDrawer) { + setSelectedPolygon(undefined); + } + }, [isOpenPolygonDrawer]); + + useEffect(() => { + if (polygonFromMap?.isOpen) { + const newSelectedPolygon = polygonMenu.find(polygon => polygon.uuid === polygonFromMap.uuid); + setSelectedPolygon(newSelectedPolygon); + setIsOpenPolygonDrawer(true); + } else { + setIsOpenPolygonDrawer(false); + } + }, [polygonFromMap, polygonMenu]); + + const downloadGeoJsonPolygon = async (polygon: IPolygonItem) => { + const polygonGeojson = await fetchGetV2TerrafundGeojsonComplete({ + queryParams: { uuid: polygon.uuid } + }); + const blob = new Blob([JSON.stringify(polygonGeojson)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `polygon.geojson`; + link.click(); + URL.revokeObjectURL(url); + }; + + const flyToPolygonBounds = async (polygon: IPolygonItem) => { + const bbox = await fetchGetV2TerrafundPolygonBboxUuid({ pathParams: { uuid: polygon.uuid } }); + const bounds: any = bbox.bbox; + if (!map.current) { + return; + } + map.current.fitBounds(bounds, { + padding: 100, + linear: false + }); + }; + + const deletePolygon = async (polygon: IPolygonItem) => { + const response: any = await fetchDeleteV2TerrafundPolygonUuid({ pathParams: { uuid: polygon.uuid } }); + if (response && response?.uuid) { + if (reloadSiteData) { + reloadSiteData(); + } + closeModal(); + } + }; + + const openFormModalHandlerConfirm = (item: any) => { + openModal( + { + deletePolygon(item); + }} + /> + ); + }; + + const polygonMenuItems = (item: any) => [ + { + id: "1", + render: () => ( +
+ + Edit Polygon +
+ ), + onClick: () => { + setSelectedPolygon(item); + setPolygonFromMap({ isOpen: true, uuid: item.uuid }); + setIsOpenPolygonDrawer(true); + setIsPolygonStatusOpen(false); + } + }, + { + id: "2", + render: () => ( +
+ + Zoom to +
+ ), + onClick: () => { + flyToPolygonBounds(item); + } + }, + { + id: "3", + render: () => ( +
+ + Download +
+ ), + onClick: () => { + downloadGeoJsonPolygon(item); + } + }, + { + id: "4", + render: () => ( +
+ + Comment +
+ ), + onClick: () => { + setSelectedPolygon(item); + setIsOpenPolygonDrawer(true); + setIsPolygonStatusOpen(true); + } + }, + { + id: "5", + render: () =>
, + MenuItemVariant: MENU_ITEM_VARIANT_DIVIDER + }, + { + id: "6", + render: () => ( +
+ + Delete Polygon +
+ ), + onClick: () => { + openFormModalHandlerConfirm(item); + } + } + ]; + + return ( +
+ + + +
+ + Polygons + + +
+
+ {polygonMenu.map(item => { + return ( +
+
+
+ {item.label} +
+ + + +
+ ); + })} +
+
+ ); +}; + +export default Polygons; diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx new file mode 100644 index 000000000..cb4df5d37 --- /dev/null +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx @@ -0,0 +1,625 @@ +import { Grid, Stack } from "@mui/material"; +import classNames from "classnames"; +import { LngLatBoundsLike } from "mapbox-gl"; +import { FC, useEffect, useState } from "react"; +import { TabbedShowLayout, TabProps, useShowContext } from "react-admin"; + +import Button from "@/components/elements/Button/Button"; +import { VARIANT_FILE_INPUT_MODAL_ADD_IMAGES } from "@/components/elements/Inputs/FileInput/FileInputVariants"; +import { BBox } from "@/components/elements/Map-mapbox/GeoJSON"; +import { useMap } from "@/components/elements/Map-mapbox/hooks/useMap"; +import { MapContainer } from "@/components/elements/Map-mapbox/Map"; +import { addSourcesToLayers, mapPolygonData } from "@/components/elements/Map-mapbox/utils"; +import Menu from "@/components/elements/Menu/Menu"; +import { MENU_PLACEMENT_RIGHT_BOTTOM, MENU_PLACEMENT_RIGHT_TOP } from "@/components/elements/Menu/MenuVariant"; +import Table from "@/components/elements/Table/Table"; +import { VARIANT_TABLE_SITE_POLYGON_REVIEW } from "@/components/elements/Table/TableVariants"; +import Text from "@/components/elements/Text/Text"; +import Icon from "@/components/extensive/Icon/Icon"; +import { IconNames } from "@/components/extensive/Icon/Icon"; +import ModalAdd from "@/components/extensive/Modal/ModalAdd"; +import ModalApprove from "@/components/extensive/Modal/ModalApprove"; +import ModalConfirm from "@/components/extensive/Modal/ModalConfirm"; +import { useModalContext } from "@/context/modal.provider"; +import { SitePolygonDataProvider } from "@/context/sitePolygon.provider"; +import { + fetchDeleteV2TerrafundPolygonUuid, + fetchGetV2TerrafundGeojsonSite, + fetchGetV2TerrafundPolygonBboxUuid, + fetchPostV2TerrafundPolygon, + fetchPostV2TerrafundSitePolygonUuidSiteUuid, + fetchPostV2TerrafundUploadGeojson, + fetchPostV2TerrafundUploadKml, + fetchPostV2TerrafundUploadShapefile, + PostV2TerrafundUploadGeojsonRequestBody, + PostV2TerrafundUploadKmlRequestBody, + PostV2TerrafundUploadShapefileRequestBody, + useGetV2SitesSiteBbox, + useGetV2SitesSitePolygon +} from "@/generated/apiComponents"; +import { PolygonBboxResponse, SitePolygon, SitePolygonsDataResponse } from "@/generated/apiSchemas"; +import { uploadImageData } from "@/pages/site/[uuid]/components/MockecData"; +import { EntityName, FileType, UploadedFile } from "@/types/common"; + +import SitePolygonReviewAside from "./components/PolygonReviewAside"; +import { IpolygonFromMap } from "./components/Polygons"; +import SitePolygonStatus from "./components/SitePolygonStatus/SitePolygonStatus"; + +interface IProps extends Omit { + type: EntityName; + label: string; +} +export interface IPolygonItem { + id: string; + status: "draft" | "submitted" | "approved" | "needs-more-information"; + label: string; + uuid: string; +} + +interface TableItemMenuProps { + ellipse: boolean; + "planting-start-date": string | null; + "polygon-name": string; + "restoration-practice": string; + source?: string; + "target-land-use-system": string | null; + "tree-distribution": string | null; + uuid: string; +} + +interface DeletePolygonProps { + uuid: string; + message: string; +} + +const PolygonReviewAside: FC<{ + type: EntityName; + data: IPolygonItem[]; + polygonFromMap: IpolygonFromMap; + setPolygonFromMap: any; + refresh?: () => void; + mapFunctions: any; +}> = ({ type, data, polygonFromMap, setPolygonFromMap, refresh, mapFunctions }) => { + switch (type) { + case "sites": + return ( + + ); + default: + return null; + } +}; + +const PolygonReviewTab: FC = props => { + const { isLoading: ctxLoading, record } = useShowContext(); + const [files, setFiles] = useState([]); + const [saveFlags, setSaveFlags] = useState(false); + const [isUserDrawing, setIsUserDrawing] = useState(false); + + const [polygonFromMap, setPolygonFromMap] = useState({ isOpen: false, uuid: "" }); + + async function storePolygon(geojson: any, record: any) { + if (geojson?.length) { + const response = await fetchPostV2TerrafundPolygon({ + body: { geometry: JSON.stringify(geojson[0].geometry) } + }); + const polygonUUID = response.uuid; + if (polygonUUID) { + const site_id = record.uuid; + await fetchPostV2TerrafundSitePolygonUuidSiteUuid({ + body: {}, + pathParams: { uuid: polygonUUID, siteUuid: site_id } + }).then(() => { + refetch(); + setPolygonFromMap({ uuid: polygonUUID, isOpen: true }); + }); + } + } + } + + const mapFunctions = useMap(storePolygon); + + const { data: sitePolygonData, refetch } = useGetV2SitesSitePolygon({ + pathParams: { + site: record.uuid + } + }); + + const { data: sitePolygonBbox } = useGetV2SitesSiteBbox({ + pathParams: { + site: record.uuid + } + }); + + const siteBbox = sitePolygonBbox?.bbox as BBox; + const sitePolygonDataTable = (sitePolygonData ?? []).map((data: SitePolygon, index) => ({ + "polygon-name": data.poly_name || `Unnamed Polygon`, + "restoration-practice": data.practice, + "target-land-use-system": data.target_sys, + "tree-distribution": data.distr, + "planting-start-date": data.plantstart, + source: data.org_name, + uuid: data.poly_id, + ellipse: index === ((sitePolygonData ?? []) as SitePolygon[]).length - 1 + })); + + const transformedSiteDataForList = (sitePolygonData ?? []).map((data: SitePolygon, index: number) => ({ + id: (index + 1).toString(), + status: data.status, + label: data.poly_name || `Unnamed Polygon`, + uuid: data.poly_id + })); + + const polygonDataMap = mapPolygonData(sitePolygonData); + + const { openModal, closeModal } = useModalContext(); + + const flyToPolygonBounds = async (uuid: string) => { + const bbox: PolygonBboxResponse = await fetchGetV2TerrafundPolygonBboxUuid({ pathParams: { uuid } }); + const bboxArray = bbox?.bbox; + const { map } = mapFunctions; + if (bboxArray && map?.current) { + const bounds: LngLatBoundsLike = [ + [bboxArray[0], bboxArray[1]], + [bboxArray[2], bboxArray[3]] + ]; + map.current.fitBounds(bounds, { + padding: 100, + linear: false + }); + } else { + console.error("Bounding box is not in the expected format"); + } + }; + + const deletePolygon = (uuid: string) => { + fetchDeleteV2TerrafundPolygonUuid({ pathParams: { uuid } }) + .then((response: DeletePolygonProps | undefined) => { + if (response && response?.uuid) { + if (reloadSiteData) { + reloadSiteData(); + } + const { map } = mapFunctions; + if (map?.current) { + addSourcesToLayers(map.current, polygonDataMap); + } + closeModal(); + } + }) + .catch(error => { + console.error("Error deleting polygon:", error); + }); + }; + + const openFormModalHandlerConfirmDeletion = (uuid: string) => { + openModal( + { + deletePolygon(uuid); + }} + /> + ); + }; + + const downloadSiteGeoJsonPolygons = async (siteUuid: string) => { + const polygonGeojson = await fetchGetV2TerrafundGeojsonSite({ + queryParams: { uuid: siteUuid } + }); + const blob = new Blob([JSON.stringify(polygonGeojson)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `SitePolygons.geojson`; + link.click(); + URL.revokeObjectURL(url); + }; + + useEffect(() => { + if (files && files.length > 0 && saveFlags) { + uploadFiles(); + setSaveFlags(false); + } + }, [files, saveFlags]); + + const uploadFiles = async () => { + const uploadPromises = []; + + for (const file of files) { + const fileToUpload = file.rawFile as File; + const site_uuid = record.uuid; + const formData = new FormData(); + const fileType = getFileType(file); + formData.append("file", fileToUpload); + formData.append("uuid", site_uuid); + let newRequest: any; + + switch (fileType) { + case "geojson": + newRequest = formData as PostV2TerrafundUploadGeojsonRequestBody; + uploadPromises.push(fetchPostV2TerrafundUploadGeojson({ body: newRequest })); + break; + case "shapefile": + newRequest = formData as PostV2TerrafundUploadShapefileRequestBody; + uploadPromises.push(fetchPostV2TerrafundUploadShapefile({ body: newRequest })); + break; + case "kml": + newRequest = formData as PostV2TerrafundUploadKmlRequestBody; + uploadPromises.push(fetchPostV2TerrafundUploadKml({ body: newRequest })); + break; + default: + break; + } + } + + await Promise.all(uploadPromises); + + refetch(); + closeModal(); + }; + + const getFileType = (file: UploadedFile) => { + const fileType = file?.file_name.split(".").pop()?.toLowerCase(); + if (fileType === "geojson") return "geojson"; + if (fileType === "zip") return "shapefile"; + if (fileType === "kml") return "kml"; + return null; + }; + const openFormModalHandlerAddPolygon = () => { + openModal( + + TerraMatch upload limits:  + 50 MB per upload +
+ } + onClose={closeModal} + content="Start by adding polygons to your site." + primaryButtonText="Save" + primaryButtonProps={{ className: "px-8 py-3", variant: "primary", onClick: () => setSaveFlags(true) }} + acceptedTYpes={FileType.ShapeFiles.split(",") as FileType[]} + setFile={setFiles} + > + {/* Next div is only Mocked data delete this children later*/} + {/*
+ {polygonData.map(polygon => ( +
+
+
+ +
+
+ {polygon.name} + + {polygon.status} + +
+
+ +
+ ))} +
*/} + + ); + }; + const reloadSiteData = () => { + refetch(); + }; + const openFormModalHandlerConfirm = () => { + openModal( + {}} + /> + ); + }; + + const openFormModalHandlerUploadImages = () => { + openModal( + + Uploaded Files + + } + onClose={closeModal} + content="Start by adding images for processing." + primaryButtonText="Save" + primaryButtonProps={{ className: "px-8 py-3", variant: "primary", onClick: closeModal }} + > + {/* Next div is only Mocked data delete this children later*/} +
+ {uploadImageData.map(image => ( +
+
+
+ +
+
+ {image.name} + + {image.status} + +
+
+
+ + {image.isVerified ? "GeoTagged Verified" : "Not Verified"} + +
+
+ ))} +
+
+ ); + }; + + const openFormModalHandlerSubmitPolygon = () => { + openModal( + { + closeModal(); + openFormModalHandlerConfirm(); + } + }} + secondaryButtonText="Cancel" + secondaryButtonProps={{ className: "px-8 py-3", variant: "white-page-admin", onClick: closeModal }} + > + ); + }; + + const isLoading = ctxLoading; + + if (isLoading) return null; + + const addMenuItems = [ + { + id: "1", + render: () => Create Polygons, + onClick: () => setIsUserDrawing(true) + }, + { + id: "2", + render: () => Add Polygon Data, + onClick: openFormModalHandlerAddPolygon + }, + { + id: "3", + render: () => Upload Images, + onClick: openFormModalHandlerUploadImages + } + ]; + + const tableItemMenu = (props: TableItemMenuProps) => [ + { + id: "1", + render: () => ( +
setPolygonFromMap({ isOpen: true, uuid: props.uuid })}> + + Open Polygon +
+ ) + }, + { + id: "2", + render: () => ( +
flyToPolygonBounds(props.uuid)}> + + Zoom to +
+ ) + }, + { + id: "3", + render: () => ( +
openFormModalHandlerConfirmDeletion(props.uuid)}> + + Delete Polygon +
+ ) + } + ]; + + const contentForApproval = ( + + Are you sure you want to approve the polygons for  + Tannous/Brayrton Road? + + ); + + return ( + + + + + +
+
+
+ + Polygon Review + + + Add, remove or edit polygons that are associated to a site. Polygons may be edited in the map + below; exported, modified in QGIS or ArcGIS and imported again; or fed through the mobile + application. + +
+
+ + + + + +
+
+
+ + Site Status + +
+ +
+
+
+ +
+
+ + Site Attribute Table + + + Edit attribute table for all polygons quickly through the table below. Alternatively, open a polygon + and edit the attributes in the map above. + +
+ { + const placeholder = props.getValue() as string; + return ( + + ); + } + }, + { header: "Target Land Use System", accessorKey: "target-land-use-system" }, + { header: "Tree Distribution", accessorKey: "tree-distribution" }, + { header: "Planting Start Date", accessorKey: "planting-start-date" }, + { header: "Source", accessorKey: "source" }, + { + header: "", + accessorKey: "ellipse", + enableSorting: false, + cell: props => ( + +
+ +
+
+ ) + } + ]} + data={sitePolygonDataTable} + >
+
+
+
+ + + +
+
+
+ ); +}; + +export default PolygonReviewTab; diff --git a/src/admin/modules/sites/components/SiteShow.tsx b/src/admin/modules/sites/components/SiteShow.tsx index b6a76ce08..e621dbad9 100644 --- a/src/admin/modules/sites/components/SiteShow.tsx +++ b/src/admin/modules/sites/components/SiteShow.tsx @@ -7,6 +7,7 @@ import ChangeRequestsTab from "@/admin/components/ResourceTabs/ChangeRequestsTab import DocumentTab from "@/admin/components/ResourceTabs/DocumentTab/DocumentTab"; import GalleryTab from "@/admin/components/ResourceTabs/GalleryTab/GalleryTab"; import InformationTab from "@/admin/components/ResourceTabs/InformationTab"; +import PolygonReviewTab from "@/admin/components/ResourceTabs/PolygonReviewTab"; import ShowTitle from "@/admin/components/ShowTitle"; const SiteShow: FC = () => { @@ -17,6 +18,9 @@ const SiteShow: FC = () => { > + + + diff --git a/src/assets/icons/ic-government.svg b/src/assets/icons/ic-government.svg new file mode 100644 index 000000000..f38db0292 --- /dev/null +++ b/src/assets/icons/ic-government.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/ic-investor.svg b/src/assets/icons/ic-investor.svg new file mode 100644 index 000000000..01e0feffc --- /dev/null +++ b/src/assets/icons/ic-investor.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/ic-projectdeveloper.svg b/src/assets/icons/ic-projectdeveloper.svg new file mode 100644 index 000000000..bfab33728 --- /dev/null +++ b/src/assets/icons/ic-projectdeveloper.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/icons/ic-public.svg b/src/assets/icons/ic-public.svg new file mode 100644 index 000000000..20d148185 --- /dev/null +++ b/src/assets/icons/ic-public.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/ic_approved.svg b/src/assets/icons/ic_approved.svg new file mode 100644 index 000000000..b5b7d251b --- /dev/null +++ b/src/assets/icons/ic_approved.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/ic_draft.svg b/src/assets/icons/ic_draft.svg new file mode 100644 index 000000000..46d8c72a8 --- /dev/null +++ b/src/assets/icons/ic_draft.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/ic_needs-more-information.svg b/src/assets/icons/ic_needs-more-information.svg new file mode 100644 index 000000000..f604957ed --- /dev/null +++ b/src/assets/icons/ic_needs-more-information.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/ic_submitted.svg b/src/assets/icons/ic_submitted.svg new file mode 100644 index 000000000..5504b7573 --- /dev/null +++ b/src/assets/icons/ic_submitted.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/elements/Commentary/Commentary.tsx b/src/components/elements/Commentary/Commentary.tsx index 9230e11a7..cd0eca3b5 100644 --- a/src/components/elements/Commentary/Commentary.tsx +++ b/src/components/elements/Commentary/Commentary.tsx @@ -10,7 +10,7 @@ export interface CommentaryProps { lastName: string; date: string; commentary: string; - status?: "draft" | "submitted"; + status?: string; files?: CommentaryFilesProps[]; } @@ -21,14 +21,15 @@ const statusStyle = { const Commentary = (props: CommentaryProps) => { const { name, lastName, date, commentary, files = [], status } = props; + const statusKey = status as keyof typeof statusStyle; return (
- {name[0]} - {lastName[0]} + {name?.[0]} + {lastName?.[0]}
@@ -43,16 +44,15 @@ const Commentary = (props: CommentaryProps) => {
- + {status}
- { {commentary}
- {files.map(file => ( + {files?.map((file: any) => (
- {file.file} + {file?.attachment}
))} diff --git a/src/components/elements/CommentaryBox/CommentaryBox.tsx b/src/components/elements/CommentaryBox/CommentaryBox.tsx index 8385e66fe..4527a4822 100644 --- a/src/components/elements/CommentaryBox/CommentaryBox.tsx +++ b/src/components/elements/CommentaryBox/CommentaryBox.tsx @@ -1,32 +1,115 @@ import { useT } from "@transifex/react"; +import { useState } from "react"; import { When } from "react-if"; import Button from "@/components/elements/Button/Button"; import TextArea from "@/components/elements/Inputs/textArea/TextArea"; import Text from "@/components/elements/Text/Text"; import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; +import { usePostV2AuditStatus } from "@/generated/apiComponents"; + +import Notification from "../Notification/Notification"; export interface CommentaryBoxProps { name: string; lastName: string; buttonSendOnBox?: boolean; mutate?: any; - refresh?: any; + refresh?: () => void; record?: any; entity?: string; } const CommentaryBox = (props: CommentaryBoxProps) => { - const { name, lastName, buttonSendOnBox } = props; + const { name, lastName, buttonSendOnBox, refresh, record, entity } = props; + const { mutate: sendCommentary } = usePostV2AuditStatus(); + const [files, setFiles] = useState([]); + const [comment, setComment] = useState(""); + const [error, setError] = useState(""); + const [charCount, setCharCount] = useState(0); + const [showNotification, setShowNotification] = useState(false); + const [loading, setLoading] = useState(false); + const [warning, setWarning] = useState(""); const t = useT(); + const validFileTypes = [ + "application/pdf", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "image/jpeg", + "image/png", + "image/tiff" + ]; + const maxFileSize = 10 * 1024 * 1024; + const maxFiles = 5; + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files) { + const file = e.target.files[0]; + if (files.length >= maxFiles) { + setError("You can upload a maximum of 5 files."); + return; + } + if (!validFileTypes.includes(file?.type)) { + setError("Invalid file type. Only PDF, XLS, DOC, XLSX, DOCX, JPG, PNG, and TIFF are allowed."); + return; + } + if (file.size > maxFileSize) { + setError("File size must be less than 10MB."); + return; + } + setFiles(prevFiles => [...prevFiles, file]); + setError(""); + } + }; + const submitComment = () => { + const body = new FormData(); + body.append("entity_uuid", record?.uuid); + body.append("status", record?.status); + body.append("entity", entity as string); + body.append("comment", comment); + body.append("type", "comment"); + files.forEach((element: File, index: number) => { + body.append(`file[${index}]`, element); + }); + setLoading(true); + sendCommentary?.( + { + //@ts-ignore swagger issue + body + }, + { + onSuccess: () => { + setShowNotification(true); + setTimeout(() => { + setShowNotification(false); + }, 3000); + setComment(""); + setError(""); + setFiles([]); + refresh && refresh(); + setLoading(false); + } + } + ); + }; + const handleCommentChange = (e: any) => { + setComment(e.target.value); + setCharCount(e.target.value.length); + if (charCount >= 255) { + setWarning("Your comment exceeds 255 characters."); + } else { + setWarning(""); + } + }; return (
- {name[0]} - {lastName[0]} + {name?.[0]} + {lastName?.[0]}