diff --git a/compliance-web/src/components/App/CaseFiles/Profile/CaseFileActions.tsx b/compliance-web/src/components/App/CaseFiles/Profile/CaseFileActions.tsx new file mode 100644 index 00000000..30353406 --- /dev/null +++ b/compliance-web/src/components/App/CaseFiles/Profile/CaseFileActions.tsx @@ -0,0 +1,121 @@ +import React, { useCallback } from "react"; +import MenuActionDropdown from "@/components/Shared/MenuActionDropdown"; +import { + useDeleteCaseFile, + useUpdateCaseFileStatus, +} from "@/hooks/useCaseFiles"; +import { CaseFile } from "@/models/CaseFile"; +import { useQueryClient } from "@tanstack/react-query"; +import ConfirmationModal from "@/components/Shared/Popups/ConfirmationModal"; +import { useModal } from "@/store/modalStore"; +import { notify } from "@/store/snackbarStore"; +import { useRouter } from "@tanstack/react-router"; + +interface CaseFileActionsProps { + status: string; + fileNumber: string; +} + +const CaseFileActions: React.FC = ({ + status, + fileNumber, +}) => { + const router = useRouter(); + const queryClient = useQueryClient(); + const { setOpen, setClose } = useModal(); + + const caseFileData = queryClient.getQueryData([ + "case-file", + fileNumber, + ]); + + const onUpdateStatusSuccess = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: ["case-file", fileNumber], + }); + notify.success("Case File status updated!"); + setClose(); + }, [queryClient, fileNumber, setClose]); + + const onDeleteSuccess = useCallback(() => { + notify.success("Case File deleted!"); + setClose(); + router.navigate({ to: "/ce-database/case-files" }); + }, [setClose, router]); + + const { mutate: updateCaseFileStatus } = useUpdateCaseFileStatus( + onUpdateStatusSuccess + ); + const { mutate: deleteCaseFile } = useDeleteCaseFile(onDeleteSuccess); + + const actionsList = [ + { + text: "Link to Case File", + onClick: () => { + // Handle linking case file + }, + hidden: true, + }, + { + text: "Unlink from Case File", + onClick: () => { + // Handle unlinking case file + }, + hidden: true, + }, + { + text: "Close Case File", + onClick: () => { + // Handle closing case file + setOpen({ + content: ( + { + updateCaseFileStatus({ + id: caseFileData?.id ?? 0, + caseFileStatus: { status: "CLOSED" }, + }); + }} + /> + ), + }); + }, + hidden: status?.toLowerCase() === "closed", + }, + { + text: "Reopen Case File", + onClick: () => { + // Handle reopening case file + updateCaseFileStatus({ + id: caseFileData?.id ?? 0, + caseFileStatus: { status: "OPEN" }, + }); + }, + hidden: status?.toLowerCase() === "open", + }, + { + text: "Delete Case File", + onClick: () => { + // Handle deleting case file + setOpen({ + content: ( + deleteCaseFile(caseFileData?.id ?? 0)} + /> + ), + }); + }, + hidden: false, + }, + ]; + + return ; +}; + +export default CaseFileActions; diff --git a/compliance-web/src/components/App/CaseFiles/Profile/CaseFileCreateComplaint.tsx b/compliance-web/src/components/App/CaseFiles/Profile/CaseFileCreateComplaint.tsx index 18f98a7c..ff43b99d 100644 --- a/compliance-web/src/components/App/CaseFiles/Profile/CaseFileCreateComplaint.tsx +++ b/compliance-web/src/components/App/CaseFiles/Profile/CaseFileCreateComplaint.tsx @@ -6,16 +6,20 @@ import { useQueryClient } from "@tanstack/react-query"; import ComplaintDrawer from "@/components/App/Complaints/ComplaintDrawer"; import { useDrawer } from "@/store/drawerStore"; import { CaseFile } from "@/models/CaseFile"; -import { useParams } from "@tanstack/react-router"; -const CaseFileCreateComplaint = () => { +const CaseFileCreateComplaint = ({ + fileNumber, + disabled = false, +}: { + fileNumber: string; + disabled?: boolean; +}) => { const queryClient = useQueryClient(); const { setOpen, setClose } = useDrawer(); - const { caseFileNumber } = useParams({ strict: false }); const caseFileData = queryClient.getQueryData([ "case-file", - caseFileNumber, + fileNumber, ]); const handleOnSubmit = useCallback( @@ -47,6 +51,7 @@ const CaseFileCreateComplaint = () => { size="small" onClick={handleOpenComplaintDrawer} startIcon={} + disabled={disabled} > Complaint diff --git a/compliance-web/src/components/App/CaseFiles/Profile/CaseFileCreateInspection.tsx b/compliance-web/src/components/App/CaseFiles/Profile/CaseFileCreateInspection.tsx index 29fe1110..dbf602da 100644 --- a/compliance-web/src/components/App/CaseFiles/Profile/CaseFileCreateInspection.tsx +++ b/compliance-web/src/components/App/CaseFiles/Profile/CaseFileCreateInspection.tsx @@ -6,16 +6,20 @@ import { useQueryClient } from "@tanstack/react-query"; import InspectionDrawer from "@/components/App/Inspections/InspectionDrawer"; import { useDrawer } from "@/store/drawerStore"; import { CaseFile } from "@/models/CaseFile"; -import { useParams } from "@tanstack/react-router"; -const CaseFileCreateInspection = () => { +const CaseFileCreateInspection = ({ + fileNumber, + disabled = false, +}: { + fileNumber: string; + disabled?: boolean; +}) => { const queryClient = useQueryClient(); const { setOpen, setClose } = useDrawer(); - const { caseFileNumber } = useParams({ strict: false }); const caseFileData = queryClient.getQueryData([ "case-file", - caseFileNumber, + fileNumber, ]); const handleOnSubmit = useCallback( @@ -47,6 +51,7 @@ const CaseFileCreateInspection = () => { size="small" onClick={handleOpenInspectionDrawer} startIcon={} + disabled={disabled} > Inspection diff --git a/compliance-web/src/components/App/CaseFiles/Profile/CaseFileGeneralInformation.tsx b/compliance-web/src/components/App/CaseFiles/Profile/CaseFileGeneralInformation.tsx index f30efabe..336cdf84 100644 --- a/compliance-web/src/components/App/CaseFiles/Profile/CaseFileGeneralInformation.tsx +++ b/compliance-web/src/components/App/CaseFiles/Profile/CaseFileGeneralInformation.tsx @@ -91,6 +91,7 @@ const CaseFileGeneralInformation: React.FC = ({ propertyName="Project Description" propertyValue={caseFileData.project_description} size="small" + expandable={true} /> diff --git a/compliance-web/src/components/App/ContinuationReports/ContinuationReportTimelineEntry.tsx b/compliance-web/src/components/App/ContinuationReports/ContinuationReportTimelineEntry.tsx index 9b04647a..7210d63d 100644 --- a/compliance-web/src/components/App/ContinuationReports/ContinuationReportTimelineEntry.tsx +++ b/compliance-web/src/components/App/ContinuationReports/ContinuationReportTimelineEntry.tsx @@ -1,7 +1,7 @@ +import ParagraphWithReadMore from "@/components/Shared/ParagraphWithReadMore"; import TimelineContent from "@mui/lab/TimelineContent"; -import { Link, Stack, Typography } from "@mui/material"; +import { Typography } from "@mui/material"; import { BCDesignTokens } from "epic.theme"; -import { useEffect, useRef, useState } from "react"; export default function ContinuationReportTimelineEntry({ renderText, @@ -14,22 +14,6 @@ export default function ContinuationReportTimelineEntry({ isSystemGenerated: boolean; searchText?: string; }) { - const contentRef = useRef(null); - const [isExpanded, setIsExpanded] = useState(false); - const [showReadMore, setShowReadMore] = useState(false); - - useEffect(() => { - if (contentRef.current && contentRef.current.scrollHeight > 170) { - setShowReadMore(true); - } - setIsExpanded(!!searchText); // if searchText is there, default should be open - }, [searchText]); - - const handleReadMoreClick = (event: React.MouseEvent) => { - event.stopPropagation(); - setIsExpanded(!isExpanded); - }; - const getFormattedText = () => { if (!searchText) return renderText; @@ -87,38 +71,28 @@ export default function ContinuationReportTimelineEntry({ return ( - - - {!isSystemGenerated && createdByUser && ( - - Created by {createdByUser} - - )} - - {showReadMore && ( - - {isExpanded ? "Read Less" : "Read More"} - - )} + + + {!isSystemGenerated && createdByUser && ( + + Created by {createdByUser} + + )} + + } + /> ); } diff --git a/compliance-web/src/components/App/FileProfileHeader.tsx b/compliance-web/src/components/App/FileProfileHeader.tsx index 4d0f05ac..66548e3e 100644 --- a/compliance-web/src/components/App/FileProfileHeader.tsx +++ b/compliance-web/src/components/App/FileProfileHeader.tsx @@ -1,23 +1,27 @@ -import { ExpandMoreRounded } from "@mui/icons-material"; -import { Box, Typography, Chip, Button } from "@mui/material"; +import { Box, Typography, Chip } from "@mui/material"; import { BCDesignTokens } from "epic.theme"; import BreadcrumbsNav, { BreadcrumbItem, } from "@/components/Shared/BreadcrumbsNav"; import CaseFileCreateInspection from "@/components/App/CaseFiles/Profile/CaseFileCreateInspection"; import CaseFileCreateComplaint from "@/components/App/CaseFiles/Profile/CaseFileCreateComplaint"; +import React from "react"; +import { FILE_PROFILE_CONTEXT } from "@/utils/constants"; +import CaseFileActions from "@/components/App/CaseFiles/Profile/CaseFileActions"; +import MenuActionDropdown from "@/components/Shared/MenuActionDropdown"; + interface FileProfileHeaderProps { fileNumber: string; status: string; breadcrumbs: BreadcrumbItem[]; - showInspectionComplaintButton?: boolean; + profileContext: string; } const FileProfileHeader: React.FC = ({ fileNumber, status, breadcrumbs, - showInspectionComplaintButton = false, + profileContext, }) => { return ( = ({ - {showInspectionComplaintButton && ( + {profileContext === FILE_PROFILE_CONTEXT.CASEFILE && ( <> - - + + + )} - + {(profileContext === FILE_PROFILE_CONTEXT.INSPECTION || + profileContext === FILE_PROFILE_CONTEXT.COMPLAINT) && ( + + )} ); diff --git a/compliance-web/src/components/App/FileProfileProperty.tsx b/compliance-web/src/components/App/FileProfileProperty.tsx index fd75a98e..f86807b1 100644 --- a/compliance-web/src/components/App/FileProfileProperty.tsx +++ b/compliance-web/src/components/App/FileProfileProperty.tsx @@ -1,14 +1,17 @@ import { Box, Typography } from "@mui/material"; import { BCDesignTokens } from "epic.theme"; +import ParagraphWithReadMore from "../Shared/ParagraphWithReadMore"; export default function FileProfileProperty({ propertyName, propertyValue, size = "default", + expandable = false, }: { propertyName: string; propertyValue?: string; size?: "small" | "default"; + expandable?: boolean; }) { return ( @@ -19,7 +22,20 @@ export default function FileProfileProperty({ > {propertyName} - {propertyValue ?? ""} + {expandable ? ( + + {propertyValue ?? ""} + + } + /> + ) : ( + + {propertyValue ?? ""} + + )} ); } diff --git a/compliance-web/src/components/Shared/MenuActionDropdown.tsx b/compliance-web/src/components/Shared/MenuActionDropdown.tsx new file mode 100644 index 00000000..ba2c0113 --- /dev/null +++ b/compliance-web/src/components/Shared/MenuActionDropdown.tsx @@ -0,0 +1,83 @@ +import { ExpandMoreRounded } from "@mui/icons-material"; +import { Button, Menu, MenuItem } from "@mui/material"; +import { BCDesignTokens } from "epic.theme"; +import React from "react"; + +interface MenuAction { + text: string; + onClick: () => void; + hidden?: boolean; +} + +interface MenuActionDropdownProps { + buttonText?: string; + actions: MenuAction[]; +} + +const MenuActionDropdown: React.FC = ({ + buttonText = "Actions", + actions, +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + {actions + .filter((item) => !item.hidden) + .map((item) => ( + { + item.onClick(); + handleClose(); + }} + > + {item.text} + + ))} + + + ); +}; + +export default MenuActionDropdown; diff --git a/compliance-web/src/components/Shared/ParagraphWithReadMore.tsx b/compliance-web/src/components/Shared/ParagraphWithReadMore.tsx new file mode 100644 index 00000000..db1dfd7e --- /dev/null +++ b/compliance-web/src/components/Shared/ParagraphWithReadMore.tsx @@ -0,0 +1,60 @@ +import { Link, Stack } from "@mui/material"; +import { BCDesignTokens } from "epic.theme"; +import { useEffect, useRef, useState } from "react"; + +interface ParagraphWithReadMoreProps { + maxHeight?: number; + renderTypography?: React.ReactNode; + expand?: boolean; +} + +export default function ParagraphWithReadMore({ + maxHeight = 150, + renderTypography, + expand = false, +}: ParagraphWithReadMoreProps) { + const contentRef = useRef(null); + const [isExpanded, setIsExpanded] = useState(false); + const [showReadMore, setShowReadMore] = useState(false); + + useEffect(() => { + if ( + contentRef.current && + contentRef.current.scrollHeight > maxHeight + 20 + ) { + setShowReadMore(true); + } + if (expand) { + setIsExpanded(true); + } + }, [maxHeight, expand]); + + const handleReadMoreClick = (event: React.MouseEvent) => { + event.stopPropagation(); + setIsExpanded(!isExpanded); + }; + + return ( + + + {renderTypography} + + {showReadMore && ( + + {isExpanded ? "Read Less" : "Read More"} + + )} + + ); +} diff --git a/compliance-web/src/hooks/useCaseFiles.tsx b/compliance-web/src/hooks/useCaseFiles.tsx index fd0fc1c0..a7a33b62 100644 --- a/compliance-web/src/hooks/useCaseFiles.tsx +++ b/compliance-web/src/hooks/useCaseFiles.tsx @@ -1,4 +1,4 @@ -import { CaseFile, CaseFileAPIData } from "@/models/CaseFile"; +import { CaseFile, CaseFileAPIData, CaseFileStatusAPIData } from "@/models/CaseFile"; import { Initiation } from "@/models/Initiation"; import { StaffUser } from "@/models/Staff"; import { OnSuccessType, request } from "@/utils/axiosUtils"; @@ -35,6 +35,20 @@ const updateCaseFile = ({ return request({ url: `/case-files/${id}`, method: "patch", data: caseFile }); }; +const updateCaseFileStatus = ({ + id, + caseFileStatus, +}: { + id: number; + caseFileStatus: CaseFileStatusAPIData; +}) => { + return request({ url: `/case-files/${id}/status`, method: "patch", data: caseFileStatus }); +}; + +const deleteCaseFile = (id: number) => { + return request({ url: `/case-files/${id}`, method: "delete" }); +}; + export const useCaseFilesData = () => { return useQuery({ queryKey: ["case-files"], @@ -88,3 +102,11 @@ export const useCreateCaseFile = (onSuccess: OnSuccessType) => { export const useUpdateCaseFile = (onSuccess: OnSuccessType) => { return useMutation({ mutationFn: updateCaseFile, onSuccess }); }; + +export const useUpdateCaseFileStatus = (onSuccess: OnSuccessType) => { + return useMutation({ mutationFn: updateCaseFileStatus, onSuccess }); +}; + +export const useDeleteCaseFile = (onSuccess: OnSuccessType) => { + return useMutation({ mutationFn: deleteCaseFile, onSuccess }); +}; diff --git a/compliance-web/src/models/CaseFile.ts b/compliance-web/src/models/CaseFile.ts index 2abaa359..89cb4f97 100644 --- a/compliance-web/src/models/CaseFile.ts +++ b/compliance-web/src/models/CaseFile.ts @@ -49,3 +49,7 @@ export interface CaseFileAPIData { unapproved_project_type?: string; unapproved_project_sub_type?: string; } + +export interface CaseFileStatusAPIData { + status: string; +} diff --git a/compliance-web/src/routes/_authenticated/ce-database/case-files/$caseFileNumber.tsx b/compliance-web/src/routes/_authenticated/ce-database/case-files/$caseFileNumber.tsx index 6339d4c0..f24d2d93 100644 --- a/compliance-web/src/routes/_authenticated/ce-database/case-files/$caseFileNumber.tsx +++ b/compliance-web/src/routes/_authenticated/ce-database/case-files/$caseFileNumber.tsx @@ -11,7 +11,7 @@ import { notify } from "@/store/snackbarStore"; import { useQueryClient } from "@tanstack/react-query"; import ErrorPage from "@/components/Shared/ErrorPage"; import LoadingPage from "@/components/Shared/LoadingPage"; -import { CR_CONTEXT_TYPE } from "@/utils/constants"; +import { CR_CONTEXT_TYPE, FILE_PROFILE_CONTEXT } from "@/utils/constants"; import { useIsRolesAllowed, KC_USER_GROUPS } from "@/hooks/useAuthorization"; export const Route = createFileRoute( @@ -85,7 +85,7 @@ function CaseFileProfilePage() { { label: "Case Files", to: "/ce-database/case-files" }, { label: caseFileNumber }, ]} - showInspectionComplaintButton + profileContext={FILE_PROFILE_CONTEXT.CASEFILE} />