diff --git a/submit-api/src/submit_api/services/authorization.py b/submit-api/src/submit_api/services/authorization.py index c50677135..bf0e92c29 100644 --- a/submit-api/src/submit_api/services/authorization.py +++ b/submit-api/src/submit_api/services/authorization.py @@ -69,7 +69,11 @@ def has_access_to_package(package_id): account_user: AccountUserModel = user.account_user user_roles: list[UserRoleModel] = account_user.roles - sufficient_roles = {RoleEnum.PROJECT_ADMIN.value, RoleEnum.SUBMISSION_ADMIN.value} + sufficient_roles = { + RoleEnum.ACCOUNT_PRIMARY_ADMIN.value, + RoleEnum.PROJECT_ADMIN.value, + RoleEnum.SUBMISSION_ADMIN.value, + } for user_role in user_roles: if user_role.role.role_name in sufficient_roles: diff --git a/submit-web/src/components/App/DocumentUpload/GenericDocumentUploadSection.tsx b/submit-web/src/components/App/DocumentUpload/GenericDocumentUploadSection.tsx new file mode 100644 index 000000000..53e1befe9 --- /dev/null +++ b/submit-web/src/components/App/DocumentUpload/GenericDocumentUploadSection.tsx @@ -0,0 +1,185 @@ +import { useCallback, useEffect, useMemo } from "react"; +import { Box, Grid, Typography } from "@mui/material"; +import { BCDesignTokens, EAOColors } from "epic.theme"; +import { Navigate, useParams } from "@tanstack/react-router"; +import { notify } from "@/components/Shared/Snackbar/snackbarStore"; +import { SUBMISSION_TYPE } from "@/models/Submission"; +import { ControlledFileUpload } from "@/components/Shared/ControlledFormFields/ControlledFileUpload"; +import { useQueryClient } from "@tanstack/react-query"; +import { SubmissionItem } from "@/models/SubmissionItem"; +import DocumentTable from "@/components/App/DocumentUpload/DocumentTable"; +import { QUERY_KEY } from "@/hooks/api/constants"; +import { getAccountProjectQueryOptions } from "@/hooks/api/useProjects"; +import { AccountProject } from "@/models/Project"; +import { camelCase } from "lodash"; +import { useFileStore } from "@/store/fileStore"; +import { BarBlueTitle } from "@/components/Shared/Text/BarTitle"; +import { getSubmissionFolderName } from "@/components/Shared/Table/utils"; + +export interface UploadSectionConfig { + name: string; + label: string; + folder: string; + maxFiles?: number; + maxFilesErrorMessage?: string; + description?: string; + acceptedFileTypes?: string; +} + +interface GenericDocumentUploadSectionProps { + sections: UploadSectionConfig[]; + title?: string; +} + +export const GenericDocumentUploadSection: React.FC< + GenericDocumentUploadSectionProps +> = ({ sections, title = "Document(s) Upload" }) => { + const { submissionId: submissionItemId, projectId } = useParams({ + from: "/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout/submissions/$submissionId", + }); + + const queryClient = useQueryClient(); + const submissionItem = queryClient.getQueryData([ + QUERY_KEY.SUBMISSION_ITEM, + Number(submissionItemId), + ]); + + const getDocumentSubmissions = useCallback(() => { + if (!submissionItem) return []; + return submissionItem.submissions.filter( + (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT, + ); + }, [submissionItem]); + + const accountProject = queryClient.getQueryData( + getAccountProjectQueryOptions(Number(projectId)).queryKey, + ); + + const { reset, files, addPendingFile, pendingFiles, initializeFiles } = + useFileStore(); + + useEffect(() => { + return () => { + reset(); + }; + }, [reset]); + + useEffect(() => { + initializeFiles(getDocumentSubmissions()); + }, [submissionItem, getDocumentSubmissions, initializeFiles]); + + const handleOnDrop = (acceptedFiles: File[], folder: string) => { + acceptedFiles.forEach((file) => { + addPendingFile(file, folder); + }); + }; + + const projectName = useMemo( + () => camelCase(accountProject?.project.name ?? ""), + [accountProject], + ); + + if (!submissionItemId) { + notify.error("Failed to load submission item"); + return ; + } + + if (!accountProject) { + notify.error("Failed to load project"); + return null; + } + + return ( + + + + + {sections.map((section) => { + const sectionDocuments = files?.filter( + (submission) => + submission.submitted_document?.folder === section.folder, + ); + const pendingSectionDocuments = pendingFiles.filter( + (document) => document.folder === section.folder, + ); + + return ( + + + + Upload {section.label} + + {section.description ? ( + + {section.description} + + ) : ( + <> + + Must be unlocked PDF document (i.e., not password + protected). + + + Any proposed changes must be in tracked changes. + + + )} + + + handleOnDrop(acceptedFiles, section.folder) + } + maxFiles={section.maxFiles} + maxFilesErrorMessage={section.maxFilesErrorMessage} + /> + + Accepted file types:{" "} + {section.acceptedFileTypes || "pdf, doc, docx, xlsx"}. Max. file + size: 500 MB. + + + + + + + ); + })} + + ); +}; diff --git a/submit-web/src/components/App/NewManagementPlan/Conditions.tsx b/submit-web/src/components/App/NewManagementPlan/Conditions.tsx index 588a58789..342276ad4 100644 --- a/submit-web/src/components/App/NewManagementPlan/Conditions.tsx +++ b/submit-web/src/components/App/NewManagementPlan/Conditions.tsx @@ -130,42 +130,50 @@ export const Conditions = () => { - { - setMainCondition( - conditions?.find((c) => c.plan_name === e.target.value) || null, - ); - if (errorText) { - setErrorText(null); - } - }} - value={mainCondition?.plan_name || ""} - error={!mainCondition && Boolean(errorText)} - > - {conditions - ?.filter( - (condition) => - condition.condition_number !== null && // Ensure condition_number is not null - !supportingConditions.includes(condition.condition_number), - ) - .map((condition) => { - const conditionLabel = `Condition ${condition.condition_number} - ${condition.plan_name}`; - - return ( - - {conditionLabel} - + {isLoading && !conditions ? ( + + + + ) : ( + { + setMainCondition( + conditions?.find((c) => c.plan_name === e.target.value) || + null, ); - })} - + if (errorText) { + setErrorText(null); + } + }} + value={mainCondition?.plan_name || ""} + error={!mainCondition && Boolean(errorText)} + > + {conditions + ?.filter( + (condition) => + condition.condition_number !== null && + !supportingConditions.includes(condition.condition_number), + ) + .map((condition) => { + const conditionLabel = `Condition ${condition.condition_number} - ${condition.plan_name}`; + + return ( + + {conditionLabel} + + ); + })} + + )} + {errorText && ( diff --git a/submit-web/src/components/App/Submission/SubmissionItemTableRow/StaffSubmissionItemTableRow/index.tsx b/submit-web/src/components/App/Submission/SubmissionItemTableRow/StaffSubmissionItemTableRow/index.tsx index 7265f5b3d..eaef3df98 100644 --- a/submit-web/src/components/App/Submission/SubmissionItemTableRow/StaffSubmissionItemTableRow/index.tsx +++ b/submit-web/src/components/App/Submission/SubmissionItemTableRow/StaffSubmissionItemTableRow/index.tsx @@ -23,6 +23,7 @@ import DocumentRow from "@/components/App/Submission/DocumentRow"; import StaffStatusCell from "./StaffStatusCell"; import { useMemo } from "react"; import { getSubmissionItemLabel } from "@/utils"; +import { SubmissionPackageType } from "@/components/Shared/types"; export default function StaffSubmissionItemTableRow({ item, @@ -52,6 +53,11 @@ export default function StaffSubmissionItemTableRow({ const actionLabel = hasDocument ? "Review" : "View"; + const isNoDetailedViewType = [ + SubmissionPackageType.IPD, + SubmissionPackageType.ENGAGEMENT_PLAN, + ].includes(submissionPackage.type.name); + const handleClick = () => { navigate({ to: `/staff/projects/${projectId}/submission-packages/${submissionPackageId}/submissions/${id}`, @@ -92,7 +98,7 @@ export default function StaffSubmissionItemTableRow({ - + void; - onSubmit: () => void; -}>; -export default function ActionButtons({ - onSubmit, - saveAndClose, -}: ActionButtonsProps) { - return ( - - - - - - - - - - - - - ); -} diff --git a/submit-web/src/components/App/SubmissionItem/ConsultationRecord/ConsultationRecordProponentView/DocumentUploadSection.tsx b/submit-web/src/components/App/SubmissionItem/ConsultationRecord/ConsultationRecordProponentView/DocumentUploadSection.tsx deleted file mode 100644 index 4a7660583..000000000 --- a/submit-web/src/components/App/SubmissionItem/ConsultationRecord/ConsultationRecordProponentView/DocumentUploadSection.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useCallback, useEffect } from "react"; -import { Box, Grid, Typography } from "@mui/material"; -import { BCDesignTokens, EAOColors } from "epic.theme"; -import { Navigate, useParams } from "@tanstack/react-router"; -import { notify } from "@/components/Shared/Snackbar/snackbarStore"; -import { SUBMISSION_TYPE } from "@/models/Submission"; -import { ControlledFileUpload } from "@/components/Shared/ControlledFormFields/ControlledFileUpload"; -import { CONSULTATION_RECORD_DOCUMENT_FOLDERS } from "./constants"; -import { useQueryClient } from "@tanstack/react-query"; -import { SubmissionItem } from "@/models/SubmissionItem"; -import DocumentTable from "@/components/App/DocumentUpload/DocumentTable"; -import { getSubmissionItemQueryOptions } from "@/hooks/api/useItems"; -import { AccountProject } from "@/models/Project"; -import { getAccountProjectQueryOptions } from "@/hooks/api/useProjects"; -import { S3_FOLDER } from "@/hooks/api/useObjectStorage"; -import { camelCase } from "lodash"; -import { useFileStore } from "@/store/fileStore"; -import { BarBlueTitle } from "@/components/Shared/Text/BarTitle"; -import { getSubmissionFolderName } from "@/components/Shared/Table/utils"; - -export const DocumentUploadSection = () => { - const MAX_FILES = 10; - const { submissionId: submissionItemId, projectId } = useParams({ - from: "/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout/submissions/$submissionId", - }); - const queryClient = useQueryClient(); - const submissionItem = queryClient.getQueryData( - getSubmissionItemQueryOptions({ itemId: Number(submissionItemId) }) - .queryKey, - ); - const getDocumentSubmissions = useCallback(() => { - if (!submissionItem) return []; - return submissionItem.submissions.filter( - (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT, - ); - }, [submissionItem]); - - const accountProject = queryClient.getQueryData( - getAccountProjectQueryOptions(Number(projectId)).queryKey, - ); - - const { reset, addPendingFile, initializeFiles, files, pendingFiles } = - useFileStore(); - - useEffect(() => { - initializeFiles(getDocumentSubmissions()); - }, [submissionItem, initializeFiles, getDocumentSubmissions]); - - useEffect(() => { - return () => { - reset(); - }; - }, [reset]); - - const handleOnDrop = (acceptedFiles: File[]) => { - acceptedFiles.forEach((file) => { - addPendingFile( - file, - CONSULTATION_RECORD_DOCUMENT_FOLDERS.CONSULTATION_RECORDS, - ); - }); - }; - - if (!submissionItemId) { - notify.error("Failed to load submission item"); - return ; - } - - const projectName = camelCase(accountProject?.project.name ?? ""); - - if (!accountProject) { - notify.error("Failed to load project"); - return null; - } - - return ( - - - - - - - - Upload Consultation Record(s), Including Comment Tracker - - - Must be unlocked PDF document (i.e., not password protected). - - - Any proposed changes must be in tracked changes. - - - - - Accepted file types: pdf, doc, docx, xlsx. Max. file size: 500 MB. - - - - - - - ); -}; diff --git a/submit-web/src/components/App/SubmissionItem/ConsultationRecord/ConsultationRecordProponentView/index.tsx b/submit-web/src/components/App/SubmissionItem/ConsultationRecord/ConsultationRecordProponentView/index.tsx index fee772873..7f7eeeecc 100644 --- a/submit-web/src/components/App/SubmissionItem/ConsultationRecord/ConsultationRecordProponentView/index.tsx +++ b/submit-web/src/components/App/SubmissionItem/ConsultationRecord/ConsultationRecordProponentView/index.tsx @@ -7,7 +7,6 @@ import { notify } from "@/components/Shared/Snackbar/snackbarStore"; import { useMemo, useState } from "react"; import { Navigate, useNavigate, useParams } from "@tanstack/react-router"; import { useGetAccountProject } from "@/hooks/api/useProjects"; -import { DocumentUploadSection } from "./DocumentUploadSection"; import { SUBMISSION_ITEM_STATUS, SUBMISSION_TYPE, @@ -18,7 +17,6 @@ import Form from "@/components/Shared/Forms/common"; import { useQueryClient } from "@tanstack/react-query"; import { SubmissionItem } from "@/models/SubmissionItem"; import FormFieldSection from "./FormFieldSection"; -import ActionButtons from "./ActionButtons"; import { consultationRecordSchema, ConsultationRecordForm, @@ -32,6 +30,12 @@ import { SubmissionPackage } from "@/models/Package"; import UpdateRequestWidget from "@/components/App/Submission/UpdateRequestWidget"; import { isAxiosError } from "axios"; import { SubmitLoaderBackdrop } from "@/components/Shared/Overlays/SubmitLoaderBackdrop"; +import { S3_FOLDER } from "@/hooks/api/useObjectStorage"; +import { + GenericDocumentUploadSection, + UploadSectionConfig, +} from "@/components/App/DocumentUpload/GenericDocumentUploadSection"; +import SubmissionActionButtons from "@/components/App/SubmissionItem/SubmissionActionButtons"; export const ConsultationRecordProponentView = () => { const { @@ -61,6 +65,15 @@ export const ConsultationRecordProponentView = () => { }).queryKey, ); + const sections: UploadSectionConfig[] = [ + { + name: "consultationRecords", + label: "Consultation Record(s)", + folder: S3_FOLDER.CONSULTATION_RECORDS.value, + description: "Including Comment Tracker", + }, + ]; + const partiesList = useMemo(() => { const parties = submissionPackage?.meta?.main_condition?.condition_attributes @@ -223,7 +236,7 @@ export const ConsultationRecordProponentView = () => { partiesList={partiesList} /> - + {submissionPackage && submissionPackage?.update_requests?.length > 0 && ( @@ -231,7 +244,7 @@ export const ConsultationRecordProponentView = () => { )} - diff --git a/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/ActionButtons.tsx b/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/ActionButtons.tsx deleted file mode 100644 index 2fe879579..000000000 --- a/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/ActionButtons.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { UnfinishedUploadsCheck } from "@/components/Shared/UnfinishedUploadsCheck"; -import { Button, Grid } from "@mui/material"; - -export default function ActionButtons({ - saveAndClose, -}: Readonly<{ - saveAndClose: () => void; -}>) { - return ( - - - - - - - - - - - - - ); -} diff --git a/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/DocumentUploadSection.tsx b/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/DocumentUploadSection.tsx deleted file mode 100644 index 92f7bd4e2..000000000 --- a/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/DocumentUploadSection.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useCallback, useEffect } from "react"; -import { Box, Grid, Typography } from "@mui/material"; -import { BCDesignTokens, EAOColors } from "epic.theme"; -import { Navigate, useParams } from "@tanstack/react-router"; -import { notify } from "@/components/Shared/Snackbar/snackbarStore"; -import { SUBMISSION_TYPE } from "@/models/Submission"; -import { ControlledFileUpload } from "@/components/Shared/ControlledFormFields/ControlledFileUpload"; -import { useQueryClient } from "@tanstack/react-query"; -import { SubmissionItem } from "@/models/SubmissionItem"; -import DocumentTable from "@/components/App/DocumentUpload/DocumentTable"; -import { QUERY_KEY } from "@/hooks/api/constants"; -import { S3_FOLDER } from "@/hooks/api/useObjectStorage"; -import { getAccountProjectQueryOptions } from "@/hooks/api/useProjects"; -import { AccountProject } from "@/models/Project"; -import { camelCase } from "lodash"; -import { useFileStore } from "@/store/fileStore"; -import { BarBlueTitle } from "@/components/Shared/Text/BarTitle"; -import { getSubmissionFolderName } from "@/components/Shared/Table/utils"; - -export const DocumentUploadSection = () => { - const MAX_FILES = 10; - const { submissionId: submissionItemId, projectId } = useParams({ - from: "/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout/submissions/$submissionId", - }); - - const queryClient = useQueryClient(); - const submissionItem = queryClient.getQueryData([ - QUERY_KEY.SUBMISSION_ITEM, - Number(submissionItemId), - ]); - - const getDocumentSubmissions = useCallback(() => { - if (!submissionItem) return []; - return submissionItem.submissions.filter( - (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT, - ); - }, [submissionItem]); - - const accountProject = queryClient.getQueryData( - getAccountProjectQueryOptions(Number(projectId)).queryKey, - ); - - const { reset, files, addPendingFile, pendingFiles, initializeFiles } = - useFileStore(); - - useEffect(() => { - return () => { - reset(); - }; - }, [reset]); - - useEffect(() => { - initializeFiles(getDocumentSubmissions()); - }, [submissionItem, getDocumentSubmissions, initializeFiles]); - - const handleOnDrop = (acceptedFiles: File[], folder: string) => { - acceptedFiles.forEach((file) => { - addPendingFile(file, folder); - }); - }; - - if (!submissionItemId) { - notify.error("Failed to load submission item"); - return ; - } - - const managementPlanDocuments = files?.filter( - (submission) => - submission.submitted_document?.folder === S3_FOLDER.IEMS.value, - ); - - const supportingDocuments = files?.filter( - (submission) => - submission.submitted_document?.folder === - S3_FOLDER.SUPPORTING_DOCUMENTS.value, - ); - - const pendingManagementPlanDocuments = pendingFiles.filter( - (document) => document.folder === S3_FOLDER.IEMS.value, - ); - - const pendingSupportingDocuments = pendingFiles.filter( - (document) => document.folder === S3_FOLDER.SUPPORTING_DOCUMENTS.value, - ); - const projectName = camelCase(accountProject?.project.name ?? ""); - - if (!accountProject) { - notify.error("Failed to load project"); - return null; - } - - return ( - - - - - - - - Upload Independent Environmental Monitor Terms of Engagement - - - Must be unlocked PDF document (i.e., not password protected). - - - Any proposed changes must be in tracked changes. - - - - handleOnDrop(acceptedFiles, S3_FOLDER.IEMS.value) - } - maxFiles={1} - /> - - Accepted file types: pdf, doc, docx, xlsx. Max. file size: 500 MB. - - - - - - - - - - Upload Supporting Documents, as applicable - - - e.g. table of proposed changes, table of concordance - - - - handleOnDrop(acceptedFiles, S3_FOLDER.SUPPORTING_DOCUMENTS.value) - } - maxFiles={MAX_FILES} - /> - - Accepted file types: pdf, doc, docx, xlsx. Max. file size: 500 MB. - - - - - - - - ); -}; diff --git a/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/index.tsx b/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/index.tsx index af3c77212..e0b12d9fe 100644 --- a/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/index.tsx +++ b/submit-web/src/components/App/SubmissionItem/IEMSubmission/IEMProponentView/index.tsx @@ -12,7 +12,6 @@ import { SubmissionItemStatus, } from "@/models/Submission"; import { useGetAccountProject } from "@/hooks/api/useProjects"; -import { DocumentUploadSection } from "./DocumentUploadSection"; import { IemSubmissionForm, iemSubmissionSchema } from "./constants"; import { booleanToString, stringToBoolean } from "@/utils"; import Form from "@/components/Shared/Forms/common"; @@ -20,13 +19,17 @@ import { useQueryClient } from "@tanstack/react-query"; import { SubmissionItem } from "@/models/SubmissionItem"; import { QUERY_KEY } from "@/hooks/api/constants"; import FormFieldSection from "./FormFieldSection"; -import ActionButtons from "./ActionButtons"; import { SubmissionFormContainer } from "@/components/App/SubmissionItem/SubmissionFormContainer"; import { BarBlueTitle } from "@/components/Shared/Text/BarTitle"; import { S3_FOLDER } from "@/hooks/api/useObjectStorage"; import { useGetSubmissionPackage } from "@/hooks/api/usePackages"; import { isAxiosError } from "axios"; import { SubmitLoaderBackdrop } from "@/components/Shared/Overlays/SubmitLoaderBackdrop"; +import { + GenericDocumentUploadSection, + UploadSectionConfig, +} from "@/components/App/DocumentUpload/GenericDocumentUploadSection"; +import SubmissionActionButtons from "@/components/App/SubmissionItem/SubmissionActionButtons"; export const IemSubmissionProponentView = () => { const { @@ -51,6 +54,21 @@ export const IemSubmissionProponentView = () => { Number(submissionItemId), ]); + const documentUploadSections: UploadSectionConfig[] = [ + { + name: "iems", + label: "Independent Environmental Monitor Terms of Engagement", + folder: S3_FOLDER.IEMS.value, + maxFiles: 1, + }, + { + name: "supportingDocuments", + label: "Supporting Documents", + folder: S3_FOLDER.SUPPORTING_DOCUMENTS.value, + description: "e.g. table of proposed changes, table of concordance", + }, + ]; + const formSubmission = submissionItem?.submissions.find( (submission) => submission.type === SUBMISSION_TYPE.FORM, ); @@ -188,9 +206,9 @@ export const IemSubmissionProponentView = () => { - + - + diff --git a/submit-web/src/components/App/SubmissionItem/IPDSubmission/IPDProponentView/constants.ts b/submit-web/src/components/App/SubmissionItem/IPDSubmission/IPDProponentView/constants.ts new file mode 100644 index 000000000..2b1456879 --- /dev/null +++ b/submit-web/src/components/App/SubmissionItem/IPDSubmission/IPDProponentView/constants.ts @@ -0,0 +1,17 @@ +import * as yup from "yup"; + +export const ipdSubmissionSchema = yup.object().shape({ + ipd: yup + .array() + .of(yup.string()) + .required("Please upload at least one document.") + .min(1, "Please upload at least one document."), + supportingIpd: yup.array().of(yup.string()), +}); + +export type IPDSubmissionForm = yup.InferType; + +export const IPD_DOCUMENT_FOLDERS = Object.freeze({ + IPD: "ipd", + SUPPORTING_IPD: "supporting_ipd", +}); diff --git a/submit-web/src/components/App/SubmissionItem/IPDSubmission/IPDProponentView/index.tsx b/submit-web/src/components/App/SubmissionItem/IPDSubmission/IPDProponentView/index.tsx new file mode 100644 index 000000000..506de3712 --- /dev/null +++ b/submit-web/src/components/App/SubmissionItem/IPDSubmission/IPDProponentView/index.tsx @@ -0,0 +1,186 @@ +import { Navigate, useNavigate, useParams } from "@tanstack/react-router"; +import { useGetAccountProject } from "@/hooks/api/useProjects"; +import { SubmissionFormContainer } from "@/components/App/SubmissionItem/SubmissionFormContainer"; +import { BCDesignTokens } from "epic.theme"; +import { SubmitLoaderBackdrop } from "@/components/Shared/Overlays/SubmitLoaderBackdrop"; +import { Grid } from "@mui/material"; +import { + GenericDocumentUploadSection, + UploadSectionConfig, +} from "@/components/App/DocumentUpload/GenericDocumentUploadSection"; +import { useMemo, useState } from "react"; +import Form from "@/components/Shared/Forms/common"; +import { FormProvider, useForm } from "react-hook-form"; +import { IPDSubmissionForm, ipdSubmissionSchema } from "./constants"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useQueryClient } from "@tanstack/react-query"; +import { SubmissionItem } from "@/models/SubmissionItem"; +import { QUERY_KEY } from "@/hooks/api/constants"; +import { + SUBMISSION_ITEM_STATUS, + SUBMISSION_TYPE, + SubmissionItemStatus, +} from "@/models/Submission"; +import { S3_FOLDER } from "@/hooks/api/useObjectStorage"; +import { useSaveSubmission } from "@/hooks/api/useSubmissions"; +import { useGetSubmissionPackage } from "@/hooks/api/usePackages"; +import { notify } from "@/components/Shared/Snackbar/snackbarStore"; +import { isAxiosError } from "axios"; +import SubmissionActionButtons from "@/components/App/SubmissionItem/SubmissionActionButtons"; + +export const IPDSubmissionProponentView = () => { + const { + projectId: accountProjectIdParam, + submissionPackageId, + submissionId: submissionItemId, + } = useParams({ + from: "/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout/submissions/$submissionId", + }); + + const accountProjectId = Number(accountProjectIdParam); + const { data: accountProject } = useGetAccountProject({ + accountProjectId, + }); + + const queryClient = useQueryClient(); + const submissionItem = queryClient.getQueryData([ + QUERY_KEY.SUBMISSION_ITEM, + Number(submissionItemId), + ]); + + const navigate = useNavigate(); + + const [isBackdropOpen, setIsBackdropOpen] = useState(false); + + const documentSubmissions = submissionItem?.submissions?.filter( + (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT, + ); + + const defaultDocumentValues = useMemo(() => { + if (!documentSubmissions) return {}; + + return { + ipd: documentSubmissions + .filter( + (submission) => + submission.submitted_document?.folder === S3_FOLDER.IPDS.value, + ) + .map((submission) => submission.submitted_document?.url), + supportingIpd: documentSubmissions + .filter( + (submission) => + submission.submitted_document?.folder === + S3_FOLDER.IPD_SUPPORTING_DOCUMENTS.value, + ) + .map((submission) => submission.submitted_document?.url), + }; + }, [documentSubmissions]); + + const methods = useForm({ + resolver: yupResolver(ipdSubmissionSchema), + mode: "onSubmit", + defaultValues: { + ...defaultDocumentValues, + }, + }); + + const { + handleSubmit, + formState: { dirtyFields }, + } = methods; + + const { refetch } = useGetSubmissionPackage({ + packageId: Number(submissionPackageId), + }); + + const { mutateAsync: callSaveSubmission } = useSaveSubmission({ + accountProjectId, + submissionItem, + }); + + const handleCompleteForm = (formData: IPDSubmissionForm) => { + saveSubmission(formData, SUBMISSION_ITEM_STATUS.COMPLETED.value); + }; + + const saveSubmission = async ( + _formData: IPDSubmissionForm, + status: SubmissionItemStatus, + ) => { + try { + setIsBackdropOpen(true); + await callSaveSubmission({ + data: { + type: SUBMISSION_TYPE.FORM, + status, + item_id: submissionItemId, + data: {}, // No form fields for IPD + }, + }); + await refetch(); + notify.success("Submission saved successfully"); + navigate({ + to: `/proponent/projects/${accountProjectId}/submission-packages/${submissionPackageId}`, + }); + } catch (error) { + const errorMessage = + isAxiosError(error) && error.response?.data?.message + ? error.response.data.message + : "Failed to save submission"; + notify.error(errorMessage); + } finally { + setIsBackdropOpen(false); + } + }; + + const saveAndClose = () => { + if (!Object.keys(dirtyFields).length) { + navigate({ + to: `/proponent/projects/${accountProjectId}/submission-packages/${submissionPackageId}`, + }); + return; + } + const formData = { + ...methods.getValues(), + }; + + saveSubmission(formData, SUBMISSION_ITEM_STATUS.PARTIALLY_COMPLETED.value); + }; + + const documentUploadSections: UploadSectionConfig[] = useMemo( + () => [ + { + name: "ipd", + label: "Initial Project Description", + folder: S3_FOLDER.IPDS.value, + }, + { + name: "supportingIpd", + label: "Supporting Documents", + folder: S3_FOLDER.IPD_SUPPORTING_DOCUMENTS.value, + description: + "Must be unlocked PDF document (i.e., not password protected).", + }, + ], + [], + ); + + if (!accountProject) return ; + return ( + + + +
+ + + + + + +
+
+
+ ); +}; diff --git a/submit-web/src/components/App/SubmissionItem/ItemForm/ProponentItemForm.tsx b/submit-web/src/components/App/SubmissionItem/ItemForm/ProponentItemForm.tsx index ebf4ecab5..4ae142546 100644 --- a/submit-web/src/components/App/SubmissionItem/ItemForm/ProponentItemForm.tsx +++ b/submit-web/src/components/App/SubmissionItem/ItemForm/ProponentItemForm.tsx @@ -9,6 +9,7 @@ import { ManagementPlanUpdateForm } from "@/components/App/SubmissionItem/Manage import { ContactInformationEntityView } from "@/components/App/SubmissionItem/ContactInformation/ContactInformationEntityView"; import { IemSubmissionProponentView } from "@/components/App/SubmissionItem/IEMSubmission/IEMProponentView"; import { IEMUpdateForm } from "@/components/App/SubmissionItem/IEMSubmission/IEMUpdateForm"; +import { IPDSubmissionProponentView } from "@/components/App/SubmissionItem/IPDSubmission/IPDProponentView"; type ItemFormProps = { submissionItem: TypeSubmissionItem; @@ -19,11 +20,14 @@ const createFormMap = { [SUBMISSION_ITEM_TYPE.MANAGEMENT_PLAN]: ManagementPlanSubmissionProponentView, [SUBMISSION_ITEM_TYPE.CONSULTATION_RECORD]: ConsultationRecordProponentView, [SUBMISSION_ITEM_TYPE.IEM]: IemSubmissionProponentView, + [SUBMISSION_ITEM_TYPE.IPD]: IPDSubmissionProponentView, + [SUBMISSION_ITEM_TYPE.ENGAGEMENT_PLAN]: ContactInformationEntityView, // TODO: Replace with actual component + [SUBMISSION_ITEM_TYPE.GEOSPATIAL_INFORMATION]: ContactInformationEntityView, // TODO: Replace with actual component }; export const ProponentItemForm = ({ submissionItem }: ItemFormProps) => { const Component = createFormMap[submissionItem.type.name]; - return Component ? : null; + return Component ? : <>; }; const updateFormMap = { @@ -31,9 +35,12 @@ const updateFormMap = { [SUBMISSION_ITEM_TYPE.MANAGEMENT_PLAN]: ManagementPlanUpdateForm, [SUBMISSION_ITEM_TYPE.CONTACT_INFORMATION]: ContactInformationEntityView, [SUBMISSION_ITEM_TYPE.IEM]: IEMUpdateForm, + [SUBMISSION_ITEM_TYPE.IPD]: IPDSubmissionProponentView, + [SUBMISSION_ITEM_TYPE.ENGAGEMENT_PLAN]: ContactInformationEntityView, // TODO: Replace with actual component + [SUBMISSION_ITEM_TYPE.GEOSPATIAL_INFORMATION]: ContactInformationEntityView, // TODO: Replace with actual component }; export const ProponentItemUpdateForm = ({ submissionItem }: ItemFormProps) => { const Component = updateFormMap[submissionItem.type.name]; - return Component ? : null; + return Component ? : <>; }; diff --git a/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/DocumentUploadSection.tsx b/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/DocumentUploadSection.tsx deleted file mode 100644 index 70a3b07a2..000000000 --- a/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/DocumentUploadSection.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { useCallback, useEffect } from "react"; -import { Box, Grid, Typography } from "@mui/material"; -import { BCDesignTokens, EAOColors } from "epic.theme"; -import { Navigate, useParams } from "@tanstack/react-router"; -import { notify } from "@/components/Shared/Snackbar/snackbarStore"; -import { SUBMISSION_TYPE } from "@/models/Submission"; -import { ControlledFileUpload } from "@/components/Shared/ControlledFormFields/ControlledFileUpload"; -import { useQueryClient } from "@tanstack/react-query"; -import { SubmissionItem } from "@/models/SubmissionItem"; -import DocumentTable from "@/components/App/DocumentUpload/DocumentTable"; -import { QUERY_KEY } from "@/hooks/api/constants"; -import { S3_FOLDER } from "@/hooks/api/useObjectStorage"; -import { getAccountProjectQueryOptions } from "@/hooks/api/useProjects"; -import { AccountProject } from "@/models/Project"; -import { camelCase } from "lodash"; -import { useFileStore } from "@/store/fileStore"; -import { BarBlueTitle } from "@/components/Shared/Text/BarTitle"; -import { getSubmissionFolderName } from "@/components/Shared/Table/utils"; - -export const DocumentUploadSection = () => { - const MAX_FILES = 10; - const { submissionId: submissionItemId, projectId } = useParams({ - from: "/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout/submissions/$submissionId", - }); - - const queryClient = useQueryClient(); - const submissionItem = queryClient.getQueryData([ - QUERY_KEY.SUBMISSION_ITEM, - Number(submissionItemId), - ]); - - const getDocumentSubmissions = useCallback(() => { - if (!submissionItem) return []; - return submissionItem.submissions.filter( - (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT, - ); - }, [submissionItem]); - - const accountProject = queryClient.getQueryData( - getAccountProjectQueryOptions(Number(projectId)).queryKey, - ); - - const { reset, files, addPendingFile, pendingFiles, initializeFiles } = - useFileStore(); - - useEffect(() => { - return () => { - reset(); - }; - }, [reset]); - - useEffect(() => { - initializeFiles(getDocumentSubmissions()); - }, [submissionItem, getDocumentSubmissions, initializeFiles]); - - const handleOnDrop = (acceptedFiles: File[], folder: string) => { - acceptedFiles.forEach((file) => { - addPendingFile(file, folder); - }); - }; - - if (!submissionItemId) { - notify.error("Failed to load submission item"); - return ; - } - - const managementPlanDocuments = files?.filter( - (submission) => - submission.submitted_document?.folder === - S3_FOLDER.MANAGEMENT_PLANS.value, - ); - - const supportingDocuments = files?.filter( - (submission) => - submission.submitted_document?.folder === - S3_FOLDER.SUPPORTING_DOCUMENTS.value, - ); - - const pendingManagementPlanDocuments = pendingFiles.filter( - (document) => document.folder === S3_FOLDER.MANAGEMENT_PLANS.value, - ); - - const pendingSupportingDocuments = pendingFiles.filter( - (document) => document.folder === S3_FOLDER.SUPPORTING_DOCUMENTS.value, - ); - const projectName = camelCase(accountProject?.project.name ?? ""); - - if (!accountProject) { - notify.error("Failed to load project"); - return null; - } - - return ( - - - - - - - - Upload Management Plan - - - Must be unlocked PDF document (i.e., not password protected). - - - Any proposed changes must be in tracked changes. - - - - handleOnDrop(acceptedFiles, S3_FOLDER.MANAGEMENT_PLANS.value) - } - maxFiles={1} - maxFilesErrorMessage={ - "You can only submit one Management Plan/IEM Terms of Engagement per submission. Please add supporting documents in the section below." - } - /> - - Accepted file types: pdf, doc, docx, xlsx. Max. file size: 500 MB. - - - - - - - - - - Upload Supporting Documents, as applicable - - - e.g. table of proposed changes, table of concordance - - - - handleOnDrop(acceptedFiles, S3_FOLDER.SUPPORTING_DOCUMENTS.value) - } - maxFiles={MAX_FILES} - /> - - Accepted file types: pdf, doc, docx, xlsx. Max. file size: 500 MB. - - - - - - - - ); -}; diff --git a/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/index.tsx b/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/index.tsx index 4dcc82be2..58bd143b3 100644 --- a/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/index.tsx +++ b/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/index.tsx @@ -12,7 +12,6 @@ import { SubmissionItemStatus, } from "@/models/Submission"; import { useGetAccountProject } from "@/hooks/api/useProjects"; -import { DocumentUploadSection } from "./DocumentUploadSection"; import { ManagementPlanSubmissionForm, managementPlanSubmissionSchema, @@ -23,13 +22,17 @@ import { useQueryClient } from "@tanstack/react-query"; import { SubmissionItem } from "@/models/SubmissionItem"; import { QUERY_KEY } from "@/hooks/api/constants"; import FormFieldSection from "./FormFieldSection"; -import ActionButtons from "./ActionButtons"; +import SubmissionActionButtons from "@/components/App/SubmissionItem/SubmissionActionButtons"; import { SubmissionFormContainer } from "@/components/App/SubmissionItem/SubmissionFormContainer"; import { BarBlueTitle } from "@/components/Shared/Text/BarTitle"; import { S3_FOLDER } from "@/hooks/api/useObjectStorage"; import { useGetSubmissionPackage } from "@/hooks/api/usePackages"; import { isAxiosError } from "axios"; import { SubmitLoaderBackdrop } from "@/components/Shared/Overlays/SubmitLoaderBackdrop"; +import { + GenericDocumentUploadSection, + UploadSectionConfig, +} from "@/components/App/DocumentUpload/GenericDocumentUploadSection"; export const ManagementPlanSubmissionProponentView = () => { const { @@ -54,6 +57,23 @@ export const ManagementPlanSubmissionProponentView = () => { Number(submissionItemId), ]); + const documentUploadSections: UploadSectionConfig[] = [ + { + name: "managementPlans", + label: "Management Plan/IEM Terms of Engagement", + folder: S3_FOLDER.MANAGEMENT_PLANS.value, + maxFiles: 1, + maxFilesErrorMessage: + "You can only submit one Management Plan/IEM Terms of Engagement per submission. Please add supporting documents in the section below.", + }, + { + name: "supportingDocuments", + label: "Supporting Documents", + folder: S3_FOLDER.SUPPORTING_DOCUMENTS.value, + description: "e.g. table of proposed changes, table of concordance", + }, + ]; + const formSubmission = submissionItem?.submissions.find( (submission) => submission.type === SUBMISSION_TYPE.FORM, ); @@ -192,9 +212,9 @@ export const ManagementPlanSubmissionProponentView = () => { - + - diff --git a/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/ActionButtons.tsx b/submit-web/src/components/App/SubmissionItem/SubmissionActionButtons.tsx similarity index 63% rename from submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/ActionButtons.tsx rename to submit-web/src/components/App/SubmissionItem/SubmissionActionButtons.tsx index f127cc52c..ad48102df 100644 --- a/submit-web/src/components/App/SubmissionItem/ManagementPlanSubmission/ManagementPlanProponentView/ActionButtons.tsx +++ b/submit-web/src/components/App/SubmissionItem/SubmissionActionButtons.tsx @@ -1,12 +1,12 @@ import { UnfinishedUploadsCheck } from "@/components/Shared/UnfinishedUploadsCheck"; import { Button, Grid } from "@mui/material"; -export default function ActionButtons({ +export default function SubmissionActionButtons({ saveAndClose, onSubmit, }: Readonly<{ saveAndClose: () => void; - onSubmit: () => void; + onSubmit?: () => void; }>) { return ( @@ -17,11 +17,13 @@ export default function ActionButtons({ - - - - - + {onSubmit && ( + + + + + + )} ); } diff --git a/submit-web/src/components/Shared/types.ts b/submit-web/src/components/Shared/types.ts index 76b6ada95..37d85048d 100644 --- a/submit-web/src/components/Shared/types.ts +++ b/submit-web/src/components/Shared/types.ts @@ -1,4 +1,7 @@ export enum SubmissionPackageType { MANAGEMENT_PLAN = "Management Plan", IEM = "IEM", + IPD = "Initial Project Description", + ENGAGEMENT_PLAN = "Engagement Plan", + GEOSPATIAL_INFORMATION = "Geospatial Information", } diff --git a/submit-web/src/hooks/api/useObjectStorage.ts b/submit-web/src/hooks/api/useObjectStorage.ts index 24ae32fba..22455749a 100644 --- a/submit-web/src/hooks/api/useObjectStorage.ts +++ b/submit-web/src/hooks/api/useObjectStorage.ts @@ -18,6 +18,11 @@ export const S3_FOLDER = { }, SUBMISSIONS: { value: "submissions", label: "Submissions" }, IEMS: { value: "iems", label: "IEM" }, + IPDS: { value: "ipds", label: "Initial Project Description" }, + IPD_SUPPORTING_DOCUMENTS: { + value: "ipd_supporting_documents", + label: "IPD Supporting Documents", + }, }; export const NEW_PACKAGE_TYPE_S3_FOLDER_MAP = { diff --git a/submit-web/src/models/SubmissionItem.ts b/submit-web/src/models/SubmissionItem.ts index cf0f36102..3beee83f5 100644 --- a/submit-web/src/models/SubmissionItem.ts +++ b/submit-web/src/models/SubmissionItem.ts @@ -19,6 +19,9 @@ export enum SUBMISSION_ITEM_TYPE { MANAGEMENT_PLAN = "Management Plan", CONSULTATION_RECORD = "Consultation Record(s)", IEM = "IEM Terms of Engagement", + IPD = "Initial Project Description", + ENGAGEMENT_PLAN = "Engagement Plan", + GEOSPATIAL_INFORMATION = "Geospatial Information", } export const SubmissionItemTypeLabelMap = { @@ -27,6 +30,9 @@ export const SubmissionItemTypeLabelMap = { [SUBMISSION_ITEM_TYPE.CONSULTATION_RECORD]: "Consultation Record(s)", [SUBMISSION_ITEM_TYPE.IEM]: "Independent Environmental Monitor Terms of Engagement", + [SUBMISSION_ITEM_TYPE.IPD]: "Initial Project Description", + [SUBMISSION_ITEM_TYPE.ENGAGEMENT_PLAN]: "Engagement Plan", + [SUBMISSION_ITEM_TYPE.GEOSPATIAL_INFORMATION]: "Geospatial Information", }; export const SUBMISSION_ITEM_MODAL_CONTENT: Record< diff --git a/submit-web/src/routes/proponent/_proponentLayout/projects/$projectId/_projectLayout/new-submission.tsx b/submit-web/src/routes/proponent/_proponentLayout/projects/$projectId/_projectLayout/new-submission.tsx index f66b6a2c0..72bd9c969 100644 --- a/submit-web/src/routes/proponent/_proponentLayout/projects/$projectId/_projectLayout/new-submission.tsx +++ b/submit-web/src/routes/proponent/_proponentLayout/projects/$projectId/_projectLayout/new-submission.tsx @@ -27,10 +27,14 @@ export const Route = createFileRoute( return; } - if ( - account.userManagementRole?.role_name !== - USER_MANAGEMENT_ROLE.PROJECT_ADMIN - ) { + const allowedRoles = [ + USER_MANAGEMENT_ROLE.ACCOUNT_PRIMARY_ADMIN, + USER_MANAGEMENT_ROLE.PROJECT_ADMIN, + ]; + + const roleName = account.userManagementRole?.role_name; + + if (!roleName || !allowedRoles.includes(roleName)) { return redirect({ to: "/unauthorized", }); diff --git a/submit-web/src/routes/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout.tsx b/submit-web/src/routes/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout.tsx index 27278b4ec..60fbcb2d2 100644 --- a/submit-web/src/routes/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout.tsx +++ b/submit-web/src/routes/proponent/_proponentLayout/projects/$projectId/_projectLayout/submission-packages/$submissionPackageId/_submissionLayout.tsx @@ -55,6 +55,7 @@ export const Route = createFileRoute( if ( [ + USER_MANAGEMENT_ROLE.ACCOUNT_PRIMARY_ADMIN, USER_MANAGEMENT_ROLE.PROJECT_ADMIN, USER_MANAGEMENT_ROLE.SUBMISSION_ADMIN, USER_MANAGEMENT_ROLE.SPECIFIC_SUBMISSION_CONTRIBUTOR,