diff --git a/submit-api/src/submit_api/models/geo_data_upload.py b/submit-api/src/submit_api/models/geo_data_upload.py index dfafcb46..07f3958e 100644 --- a/submit-api/src/submit_api/models/geo_data_upload.py +++ b/submit-api/src/submit_api/models/geo_data_upload.py @@ -53,7 +53,7 @@ class GeoDataUpload(BaseModel): # pylint: disable=too-many-instance-attributes nullable=True ) - def __init__( # pylint: disable=too-many-arguments + def __init__( # pylint: disable=too-many-arguments,R0917 self, filename: str = '', file_type: str | None = None, diff --git a/submit-api/src/submit_api/models/staff_user_work.py b/submit-api/src/submit_api/models/staff_user_work.py index 73603420..798b35f4 100644 --- a/submit-api/src/submit_api/models/staff_user_work.py +++ b/submit-api/src/submit_api/models/staff_user_work.py @@ -46,7 +46,7 @@ class StaffUserWork(BaseModel): lazy='joined', back_populates='work_assignments') - work = db.relationship('TrackWork', foreign_keys=[work_id], lazy='joined') + work = db.relationship('TrackWork', foreign_keys=[work_id], lazy='joined', back_populates='staff_assignments') @classmethod def find_by_staff_user_id(cls, staff_user_id: int): diff --git a/submit-api/src/submit_api/models/track_work.py b/submit-api/src/submit_api/models/track_work.py index 6fca87d1..3f84bc51 100644 --- a/submit-api/src/submit_api/models/track_work.py +++ b/submit-api/src/submit_api/models/track_work.py @@ -36,7 +36,8 @@ class TrackWork(BaseModel): foreign_keys='StaffUserWork.work_id', lazy='select', cascade='all, delete', - passive_deletes=True + passive_deletes=True, + back_populates='work' ) __table_args__ = ( diff --git a/submit-api/src/submit_api/resources/submission.py b/submit-api/src/submit_api/resources/submission.py index c81f0b7a..3c831ad1 100644 --- a/submit-api/src/submit_api/resources/submission.py +++ b/submit-api/src/submit_api/resources/submission.py @@ -24,6 +24,8 @@ from submit_api.models import Submission as SubmissionModel from submit_api.resources.apihelper import Api as ApiHelper from submit_api.schemas.submission import CreateSubmissionRequestSchema, SubmissionSchema, UpdateSubmissionStatusSchema +from submit_api.services.authorization import has_gis_extended_edit_role +from submit_api.utils.constants import GIS_ITEM_TYPE_NAME from submit_api.services.package_access_control import PackageAccessControl from submit_api.services.submission import SubmissionService from submit_api.utils.util import allowedorigins, cors_preflight @@ -226,10 +228,25 @@ class SubmissionUpdateStatus(Resource): code=HTTPStatus.OK, model=submission_model, description="Submission" ) @API.response(HTTPStatus.BAD_REQUEST, "Bad Request") + @API.response(HTTPStatus.FORBIDDEN, "Insufficient permissions") @cross_origin(origins=allowedorigins()) @auth.require def patch(submission_id): """Edit a submission status.""" + # Get the submission to check if it's a GIS document + submission = SubmissionModel.find_by_id(submission_id) + if not submission: + abort(HTTPStatus.NOT_FOUND, "Submission not found") + # Check if this is a GIS document (item type name "Geospatial Information") and validate GIS role + if ( + submission.item + and submission.item.type + and submission.item.type.name == GIS_ITEM_TYPE_NAME + ): + # For GIS documents, user must have GIS extended edit role or full access + if not has_gis_extended_edit_role(): + abort(HTTPStatus.FORBIDDEN, "GIS extended edit role required for GIS documents") + edit_submission_data = UpdateSubmissionStatusSchema().load(API.payload) status = edit_submission_data.get("status") edited_submission = SubmissionService.update_submission_status(submission_id, status) diff --git a/submit-api/src/submit_api/services/activity_log_service.py b/submit-api/src/submit_api/services/activity_log_service.py index 270d4152..5e7bc294 100644 --- a/submit-api/src/submit_api/services/activity_log_service.py +++ b/submit-api/src/submit_api/services/activity_log_service.py @@ -18,7 +18,7 @@ class ActivityLogService: """Service class for handling activity log operations.""" @staticmethod - def log_activity( # pylint: disable=too-many-arguments + def log_activity( # pylint: disable=too-many-arguments,R0917 entity_id: int, action: str, actor_id: str = '', @@ -69,7 +69,7 @@ def get_activity_logs(entity_type: str, entity_id: int, for_staff: bool = True) return logs @staticmethod - def _create_activity_log_object( # pylint: disable=too-many-arguments + def _create_activity_log_object( # pylint: disable=too-many-arguments,R0917 entity_type, entity_id, entity_version, action, actor_id, actor_type, visibility ) -> ActivityLog: """Creates an ActivityLog object without adding it to a session.""" diff --git a/submit-api/src/submit_api/services/authorization.py b/submit-api/src/submit_api/services/authorization.py index 9e2e3177..67f3fd30 100644 --- a/submit-api/src/submit_api/services/authorization.py +++ b/submit-api/src/submit_api/services/authorization.py @@ -120,3 +120,32 @@ def require_team_lead_access(): if jwt.contains_role([EpicSubmitRole.FULL_ACCESS.value, EpicSubmitRole.W_EXTENDED_EDIT.value]): return abort(HTTPStatus.FORBIDDEN) + + +def has_gis_extended_edit_role(user_roles=None): + """Check if user has GIS extended edit role. + + Args: + user_roles: List of user roles to check. If None, checks current user's roles. + + Returns: + bool: True if user has GIS extended edit role or full access + """ + if user_roles is None: + # Check current user's roles via JWT + return jwt.contains_role([EpicSubmitRole.GIS_EXTENDED_EDIT.value, EpicSubmitRole.FULL_ACCESS.value]) + + # Check provided roles list + return EpicSubmitRole.GIS_EXTENDED_EDIT.value in user_roles or EpicSubmitRole.FULL_ACCESS.value in user_roles + + +def require_gis_extended_edit_access(): + """Check if current user has GIS extended edit permission. + + Raises: + HTTPStatus.FORBIDDEN: If user doesn't have GIS extended edit permission + """ + # Check for full_access or gis_extended_edit role + if jwt.contains_role([EpicSubmitRole.FULL_ACCESS.value, EpicSubmitRole.GIS_EXTENDED_EDIT.value]): + return + abort(HTTPStatus.FORBIDDEN) diff --git a/submit-api/src/submit_api/services/geo/processor.py b/submit-api/src/submit_api/services/geo/processor.py index 7f1f6434..3a9ba240 100644 --- a/submit-api/src/submit_api/services/geo/processor.py +++ b/submit-api/src/submit_api/services/geo/processor.py @@ -406,7 +406,7 @@ def _spawn_processing_thread(cls, app, upload_id: int) -> None: # -- CRUD ---------------------------------------------------------------- @classmethod - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments,R0917 def create_upload(cls, app, filename: str, file_type: str, file_size_kb: float, s3_key: str) -> GeoDataUpload: """Create a GeoDataUpload record and kick off background processing.""" if file_type not in ("shp", "zip"): diff --git a/submit-api/src/submit_api/services/package_service.py b/submit-api/src/submit_api/services/package_service.py index 5f5c9994..a50fb53a 100644 --- a/submit-api/src/submit_api/services/package_service.py +++ b/submit-api/src/submit_api/services/package_service.py @@ -31,10 +31,12 @@ from submit_api.models.update_request import UpdateRequestType, UpdateRequestStatus from submit_api.models.user import UserType from submit_api.services import authorization +from submit_api.services.authorization import has_gis_extended_edit_role from submit_api.services.activity_log_service import ActivityLogService from submit_api.utils.constants import ( MANAGEMENT_PLAN_SUBMISSION_CONFIRMATION_EMAIL_TEMPLATE, MANAGEMENT_PLAN_UPDATE_REQUEST_CREATED_EMAIL_TEMPLATE, - MANAGEMENT_PLAN_SUBMISSION_NOTIFY_STAFF_EMAIL_TEMPLATE, MANAGEMENT_PLAN_RESUBMISSION_REQUEST_EMAIL_TEMPLATE) + MANAGEMENT_PLAN_SUBMISSION_NOTIFY_STAFF_EMAIL_TEMPLATE, MANAGEMENT_PLAN_RESUBMISSION_REQUEST_EMAIL_TEMPLATE, + GIS_ITEM_TYPE_NAME) from submit_api.utils.token_info import TokenInfo from submit_api.services.package_version_service import PackageVersionService @@ -709,6 +711,10 @@ def accept_update_request(cls, package_id, update_request_id): package = cls.get_package_by_id(package_id) update_request = UpdateRequestModel.find_by_id(update_request_id) cls._validate_accept_update_request(package, update_request) + + # Validate GIS role if update request is for GIS documents + cls._validate_gis_role_for_existing_update_request(package, update_request) + update_request = UpdateRequestModel.find_by_id(update_request_id) update_request.status = UpdateRequestStatus.ACCEPTED.value update_request.active = False @@ -723,6 +729,10 @@ def withdraw_update_request(cls, package_id, update_request_id): package = cls.get_package_by_id(package_id) update_request = UpdateRequestModel.find_by_id(update_request_id) cls._validate_withdraw_update_request(package, update_request) + + # Validate GIS role if update request is for GIS documents + cls._validate_gis_role_for_existing_update_request(package, update_request) + update_request = UpdateRequestModel.find_by_id(update_request_id) update_request.status = UpdateRequestStatus.CLOSED.value update_request.active = False @@ -1114,6 +1124,10 @@ def update_package_state(cls, package_id, request_data): def create_update_request(cls, package_id, request_data): """Create an update request for the package.""" package = cls._get_and_validate_package_for_update_request(package_id) + + # Validate GIS role if update request is for GIS documents + cls._validate_gis_role_for_update_request(package, request_data) + cls._create_update_request(package, request_data) cls._log_activity_update_request(package) cls._update_request_creation_email_queue(package.id) @@ -1186,6 +1200,44 @@ def _get_and_validate_package_for_starting_review(cls, package_id): "Cannot create a review for a package that has been rejected") return package + @classmethod + def _validate_gis_role_for_update_request(cls, package, request_data): + """Validate GIS role if update request is for GIS documents.""" + submission_item_types = request_data.get("submission_item_types", []) + + # Check if any of the submission items contain GIS documents + is_gis_update_request = cls._is_gis_update_request(package, submission_item_types) + + if is_gis_update_request: + # For GIS documents, user must have GIS extended edit role or full access + if not has_gis_extended_edit_role(): + raise BadRequestError("GIS extended edit role required for GIS document update requests") + + @classmethod + def _is_gis_update_request(cls, package, submission_item_types): + """Check if update request is for GIS documents. + + GIS documents are identified by item type name 'Geospatial Information'. + """ + # Get all items for the specified item types + for item in package.items: + if item.type_id in submission_item_types: + # Check if this item type is Geospatial Information + if item.type and item.type.name == GIS_ITEM_TYPE_NAME: + return True + return False + + @classmethod + def _validate_gis_role_for_existing_update_request(cls, package, update_request): + """Validate GIS role for existing update request (accept/withdraw operations).""" + # Check if the update request is for GIS documents + is_gis_update_request = cls._is_gis_update_request(package, update_request.submission_item_types) + + if is_gis_update_request: + # For GIS documents, user must have GIS extended edit role or full access + if not has_gis_extended_edit_role(): + raise BadRequestError("GIS extended edit role required for GIS document update requests") + @classmethod def create_update_request_note(cls, package_id, update_request_id, request_data): """Create a note for an update request.""" diff --git a/submit-api/src/submit_api/utils/constants.py b/submit-api/src/submit_api/utils/constants.py index c95d9679..688be898 100644 --- a/submit-api/src/submit_api/utils/constants.py +++ b/submit-api/src/submit_api/utils/constants.py @@ -49,6 +49,9 @@ 'TEAM_MEMBER': 'SUBMIT/OPS_TEAM_MEMBER' } +# Item type name for GIS/Geospatial submissions +GIS_ITEM_TYPE_NAME = "Geospatial Information" + MANAGEMENT_PLAN_SUBMISSION_CONFIRMATION_EMAIL_TEMPLATE = 'management_plan_submission_verification.html' MANAGEMENT_PLAN_SUBMISSION_CONFIRMATION_EMAIL_SUBJECT = 'Management Plan Submission Confirmation' MANAGEMENT_PLAN_UPDATE_REQUEST_CREATED_EMAIL_TEMPLATE = 'management_plan_update_request_created.html' diff --git a/submit-api/src/submit_api/utils/roles.py b/submit-api/src/submit_api/utils/roles.py index 317461ea..57587252 100644 --- a/submit-api/src/submit_api/utils/roles.py +++ b/submit-api/src/submit_api/utils/roles.py @@ -35,3 +35,6 @@ class EpicSubmitRole(Enum): W_EDIT = "w_edit" W_CREATE = "w_create" W_EXTENDED_EDIT = "w_extended_edit" + + # GIS role + GIS_EXTENDED_EDIT = "gis_extended_edit" diff --git a/submit-web/src/components/App/Map/MapPreviewModal.tsx b/submit-web/src/components/App/Map/MapPreviewModal.tsx index 780f8447..43700a1d 100644 --- a/submit-web/src/components/App/Map/MapPreviewModal.tsx +++ b/submit-web/src/components/App/Map/MapPreviewModal.tsx @@ -38,8 +38,10 @@ interface MapPreviewModalProps { fileSizeKb?: number; status?: string; errorMessage?: string; - onApprove: () => void; - onReject: () => Promise; + onApprove?: () => void; + onReject?: () => Promise; + onClose?: () => void; + previewOnly?: boolean; } export const MapPreviewModal: React.FC = ({ @@ -51,6 +53,8 @@ export const MapPreviewModal: React.FC = ({ errorMessage, onApprove, onReject, + onClose, + previewOnly = false, }) => { const mapRef = useRef(null); const [loading, setLoading] = useState(false); @@ -158,6 +162,7 @@ export const MapPreviewModal: React.FC = ({ const isProcessing = status === "processing" || (!uploadId && open); const handleReject = async () => { + if (!onReject) return; setIsRejecting(true); try { await onReject(); @@ -551,23 +556,35 @@ export const MapPreviewModal: React.FC = ({ - - Reject - - + {previewOnly ? ( + + ) : ( + <> + + Reject + + + + )} ); diff --git a/submit-web/src/components/App/Submission/DocumentRow/index.tsx b/submit-web/src/components/App/Submission/DocumentRow/index.tsx index df4ae124..d8048925 100644 --- a/submit-web/src/components/App/Submission/DocumentRow/index.tsx +++ b/submit-web/src/components/App/Submission/DocumentRow/index.tsx @@ -1,4 +1,4 @@ -import { Box, IconButton, TableRow, Typography } from "@mui/material"; +import { Box, IconButton, TableRow, Typography, Button, Tooltip } from "@mui/material"; import { Submission, SUBMISSION_STATUS } from "@/models/Submission"; import { SubmissionItem } from "@/models/SubmissionItem"; import { @@ -22,6 +22,17 @@ import ActionSplitButton, { import { BCDesignTokens } from "epic.theme"; import { useDocumentRow } from "@/hooks/useDocumentRow"; import { usePackageRoles } from "@/hooks/usePackageRoles"; +import { useState } from "react"; +import { useAccount } from "@/store/accountStore"; +import { lazy, Suspense } from "react"; +import { GIS_ITEM_TYPE_NAME } from "@/utils/constants"; +import { useGetGeoUploads } from "@/hooks/api/useGeo"; + +const MapPreviewModal = lazy(() => + import("@/components/App/Map/MapPreviewModal").then((m) => ({ + default: m.MapPreviewModal, + })), +); type DocumentRowProps = Readonly<{ documentSubmission: Submission; @@ -42,9 +53,26 @@ export default function DocumentRow({ documentSubmission; const packageType = propsPackageType || submissionPackage?.type; const name = submitted_document?.name || ""; - - // Get the correct package-specific roles - const packageRoles = usePackageRoles(submissionPackage, packageType); + const { roles } = useAccount(); + + // Check if this is a GIS document (item type name "Geospatial Information") + const isGISDocument = submissionItem.type.name === GIS_ITEM_TYPE_NAME; + + // Get the correct package-specific roles (include document folder for GIS handling) + const packageRoles = usePackageRoles(submissionPackage, packageType, submitted_document?.folder); + + // Check if user has GIS permissions + const hasGISPermissions = roles?.includes('gis_extended_edit') || roles?.includes('full_access'); + + // State for GIS preview modal + const [showPreviewModal, setShowPreviewModal] = useState(false); + + // Get geo uploads for GIS preview + const { data: geoUploads } = useGetGeoUploads({ + itemId: submissionItem.id, + autoRefetch: false, + }); + const uploads = geoUploads as any[]; const { pendingGetObject, @@ -148,32 +176,60 @@ export default function DocumentRow({ ]} > - - {staff ? ( - + + {staff ? ( + + + + ) : ( + + )} + + {isGISDocument && staff && ( + )} - + {submitted_by || ""} @@ -210,42 +266,90 @@ export default function DocumentRow({ }} > {showUndoVerificationButton && ( - - - Undo Verification - - + isGISDocument && !hasGISPermissions ? ( + + + Undo Verification + + + ) : ( + + + Undo Verification + + + ) )} {showUndoAcknowledgementButton && ( - - - Undo Acknowledgement - - + isGISDocument && !hasGISPermissions ? ( + + + Undo Acknowledgement + + + ) : ( + + + Undo Acknowledgement + + + ) )} {splitButtonConfig ? ( - - - + isGISDocument && !hasGISPermissions ? ( + + + {} // Disabled click + }} + secondaryActions={splitButtonConfig.secondary.map(action => ({ + ...action, + onClick: () => {} // Disabled click + }))} + disabled + /> + + + ) : ( + + + + ) ) : showDefaultActionButton ? ( @@ -264,6 +368,22 @@ export default function DocumentRow({ )} + + {/* GIS Preview Modal */} + {isGISDocument && ( + + u.raw_s3_key === documentSubmission.submitted_document?.url)?.id ?? null} + documentItem={documentSubmission} + fileSizeKb={uploads?.find((u) => u.raw_s3_key === documentSubmission.submitted_document?.url)?.file_size_kb} + status={uploads?.find((u) => u.raw_s3_key === documentSubmission.submitted_document?.url)?.status} + errorMessage={uploads?.find((u) => u.raw_s3_key === documentSubmission.submitted_document?.url)?.error_message} + previewOnly={true} + onClose={() => setShowPreviewModal(false)} + /> + + )} ); } 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 efc62533..b4d6cb07 100644 --- a/submit-web/src/components/App/Submission/SubmissionItemTableRow/StaffSubmissionItemTableRow/index.tsx +++ b/submit-web/src/components/App/Submission/SubmissionItemTableRow/StaffSubmissionItemTableRow/index.tsx @@ -4,6 +4,7 @@ import { TableCell, TableRow, Typography, + Tooltip, } from "@mui/material"; import { BCDesignTokens } from "epic.theme"; import { SubmissionStatusChipStack } from "@/components/App/SubmissionStatusChip"; @@ -24,6 +25,9 @@ import SubmissionItemReviewConfirmation from "@/components/App/Submission/Submis import DocumentRow from "@/components/App/Submission/DocumentRow"; import { useMemo } from "react"; import { getSubmissionItemLabel } from "@/utils"; +import { useAccount } from "@/store/accountStore"; +import { usePackageRoles } from "@/hooks/usePackageRoles"; +import { GIS_ITEM_TYPE_NAME } from "@/utils/constants"; export default function StaffSubmissionItemTableRow({ item, @@ -46,6 +50,8 @@ export default function StaffSubmissionItemTableRow({ const { submissions, id } = item; const { submitted_on } = submissionPackage; + const { roles } = useAccount(); + const name = useMemo(() => { return getSubmissionItemLabel(item.type.name); }, [item.type.name]); @@ -76,6 +82,26 @@ export default function StaffSubmissionItemTableRow({ return endStatuses.some(status => submissionPackage.status?.includes(status as any)); }, [submissionPackage.status]); + // Check if this submission item is GIS (Geospatial Information) + const isGISItem = item.type.name === GIS_ITEM_TYPE_NAME; + + // Get the appropriate roles for this package type + const packageRoles = usePackageRoles(submissionPackage, packageType); + + // Check if user has the appropriate create role for this package type + const hasPackageCreateRole = roles?.includes(packageRoles.create); + + // Check if user has GIS permissions + const hasGISPermissions = roles?.includes('gis_extended_edit') || roles?.includes('full_access'); + + // Determine if Request Update should be shown + // For GIS items: show if user has GIS permissions + // For non-GIS items: show if user has the appropriate package create role (mp_create, w_create, etc.) + const shouldShowRequestUpdate = isGISItem ? hasGISPermissions : hasPackageCreateRole; + + // Determine if Request Update should be disabled (for GIS items without GIS permissions) + const isRequestUpdateDisabled = isGISItem && !hasGISPermissions; + const handleClick = () => { navigate({ to: `/staff/projects/${projectId}/submission-packages/${submissionPackageId}/submissions/${id}`, @@ -151,34 +177,57 @@ export default function StaffSubmissionItemTableRow({ - onRequestUpdate?.(item.type_id, item.type.name)} - sx={{ - display: "flex", - alignItems: "center", - gap: "4px", - color: BCDesignTokens.typographyColorLink, - fontSize: "14px", - textDecoration: "none", - textAlign: "left", - "&:hover": { - textDecoration: "underline", - cursor: "pointer", - }, - }} - > - + + + Request Update + + + ) : ( + onRequestUpdate?.(item.type_id, item.type.name)} sx={{ - fontSize: "16px", + display: "flex", + alignItems: "center", + gap: "4px", color: BCDesignTokens.typographyColorLink, + fontSize: "14px", + textDecoration: "none", + textAlign: "left", + "&:hover": { + textDecoration: "underline", + cursor: "pointer", + }, }} - /> - Request Update - + > + + Request Update + + )} diff --git a/submit-web/src/components/App/SubmissionItem/SectionUpdateRequestPanel/SentRequestCollapsible.tsx b/submit-web/src/components/App/SubmissionItem/SectionUpdateRequestPanel/SentRequestCollapsible.tsx index 21d2d3af..4f1addf7 100644 --- a/submit-web/src/components/App/SubmissionItem/SectionUpdateRequestPanel/SentRequestCollapsible.tsx +++ b/submit-web/src/components/App/SubmissionItem/SectionUpdateRequestPanel/SentRequestCollapsible.tsx @@ -12,7 +12,7 @@ * - No action buttons (onAcceptUpdate/onWithdrawUpdate not used) */ import React, { useState } from "react"; -import { Box, TextField, Button, Typography } from "@mui/material"; +import { Box, TextField, Button, Typography, Tooltip } from "@mui/material"; import { StatusChip } from "@/components/Shared/StatusChip"; import CloseIcon from "@mui/icons-material/Close"; import { SentRequest } from "./types"; @@ -21,6 +21,7 @@ import { UPDATE_REQUEST_STATUS } from "@/models/UpdateRequest"; import ActionSplitButton from "@/components/Shared/ActionSplitButton/ActionSplitButton"; import { UpdateRequestAccordion } from "./UpdateRequestAccordion"; import { useUpdatePackageUpdateRequestNote, useCreatePackageUpdateRequesNote } from "@/hooks/api/usePackages"; +import { useAccount } from "@/store/accountStore"; interface SentRequestCollapsibleProps { request: SentRequest; @@ -32,6 +33,7 @@ interface SentRequestCollapsibleProps { isLoading?: boolean; // Loading state for save operation packageId: number; // Required for API calls isProponentView?: boolean; // PROPONENT: Show notes even when status is OPEN + isGISRequest?: boolean; // Whether this update request is for GIS documents } export const SentRequestCollapsible: React.FC = ({ @@ -44,10 +46,18 @@ export const SentRequestCollapsible: React.FC = ({ isLoading = false, packageId, isProponentView = false, + isGISRequest = false, }) => { const [isEditingNote, setIsEditingNote] = useState(false); const [noteText, setNoteText] = useState(request.note || ""); const maxNoteLength = 500; + const { roles } = useAccount(); + + // Check if user has GIS permissions + const hasGISPermissions = roles?.includes('gis_extended_edit') || roles?.includes('full_access'); + + // Determine if actions should be disabled for GIS requests + const isActionDisabled = isGISRequest && !hasGISPermissions; // Hooks for note operations const { mutate: createUpdateRequestNote, isPending: isCreatingNote } = @@ -100,14 +110,29 @@ export const SentRequestCollapsible: React.FC = ({ }} /> {onWithdrawUpdate && ( - , - }} - secondaryActions={[]} - /> + isActionDisabled ? ( + + + {}, + icon: , + }} + secondaryActions={[]} + /> + + + ) : ( + , + }} + secondaryActions={[]} + /> + ) )} )} @@ -123,13 +148,27 @@ export const SentRequestCollapsible: React.FC = ({ }} /> {onAcceptUpdate && ( - + isActionDisabled ? ( + + + {}, + }} + secondaryActions={[]} + /> + + + ) : ( + + ) )} )} diff --git a/submit-web/src/components/Shared/PermissionGate/utils.ts b/submit-web/src/components/Shared/PermissionGate/utils.ts index 9cea3356..fdce2e6e 100644 --- a/submit-web/src/components/Shared/PermissionGate/utils.ts +++ b/submit-web/src/components/Shared/PermissionGate/utils.ts @@ -23,6 +23,16 @@ export const checkIfStaff = (roles?: string[]) => { ); }; +export const checkIfGISUser = (roles?: string[]) => { + if (!roles) { + return false; + } + return hasPermission({ + permissions: roles || [], + scopes: [EPIC_SUBMIT_ROLE.gis_extended_edit], + }); +}; + export const hasPermission = ({ permissions, scopes, diff --git a/submit-web/src/hooks/usePackageRoles.ts b/submit-web/src/hooks/usePackageRoles.ts index b9a1d972..5244541d 100644 --- a/submit-web/src/hooks/usePackageRoles.ts +++ b/submit-web/src/hooks/usePackageRoles.ts @@ -1,18 +1,32 @@ import { useMemo } from "react"; import { EPIC_SUBMIT_ROLE } from "@/models/Role"; import { SubmissionPackage, PackageType } from "@/models/Package"; +import { S3_FOLDER } from "@/hooks/api/useObjectStorage"; /** * Hook to determine the correct package-specific roles based on package type. * Returns the appropriate MP_* or W_* roles for the given package. + * For GIS documents, includes GIS role handling. */ export const usePackageRoles = ( submissionPackage?: SubmissionPackage, - packageType?: PackageType + packageType?: PackageType, + documentFolder?: string ) => { const actualPackageType = packageType || submissionPackage?.type; + const isGISDocument = documentFolder === S3_FOLDER.GEOSPATIAL.value; return useMemo(() => { + // For GIS documents, use GIS role + full_access + if (isGISDocument) { + return { + view: EPIC_SUBMIT_ROLE.gis_extended_edit, + edit: EPIC_SUBMIT_ROLE.gis_extended_edit, + create: EPIC_SUBMIT_ROLE.gis_extended_edit, + approve: EPIC_SUBMIT_ROLE.gis_extended_edit, + }; + } + // Determine if this is a work package or MP-type package const isWorkPackage = submissionPackage?.account_project_work != null; @@ -46,5 +60,5 @@ export const usePackageRoles = ( approve: EPIC_SUBMIT_ROLE.eao_edit, // No EAO approve role }; } - }, [submissionPackage?.account_project_work, actualPackageType?.name]); + }, [submissionPackage?.account_project_work, actualPackageType?.name, isGISDocument]); }; diff --git a/submit-web/src/hooks/useStaffSubmissionPage.ts b/submit-web/src/hooks/useStaffSubmissionPage.ts index f4fe2b17..ce673904 100644 --- a/submit-web/src/hooks/useStaffSubmissionPage.ts +++ b/submit-web/src/hooks/useStaffSubmissionPage.ts @@ -174,10 +174,11 @@ export function useStaffSubmissionPage({ ].includes(approval_type as SubmissionPackageApprovalType); // Only users with w_extended_edit permission can approve packages + // GIS-only users should not be able to approve packages const canApprove = hasPermission({ permissions: account.roles || [], scopes: [EPIC_SUBMIT_ROLE.w_extended_edit], - }); + }) && !account.roles?.includes(EPIC_SUBMIT_ROLE.gis_extended_edit); const showApproveButtons = isPackageAcknowledged && approval_type == SubmissionPackageApprovalType.C && diff --git a/submit-web/src/models/Role.tsx b/submit-web/src/models/Role.tsx index 8f3d5c76..e6375bf5 100644 --- a/submit-web/src/models/Role.tsx +++ b/submit-web/src/models/Role.tsx @@ -10,7 +10,8 @@ export type EpicSubmitRole = | "w_view" | "w_edit" | "w_create" - | "w_extended_edit"; + | "w_extended_edit" + | "gis_extended_edit"; export const EPIC_SUBMIT_ROLE = Object.freeze< Record @@ -27,6 +28,7 @@ export const EPIC_SUBMIT_ROLE = Object.freeze< w_edit: "w_edit", w_create: "w_create", w_extended_edit: "w_extended_edit", + gis_extended_edit: "gis_extended_edit", }); export enum USER_MANAGEMENT_ROLE { diff --git a/submit-web/src/utils/constants.ts b/submit-web/src/utils/constants.ts index 10ca17f4..e812fc67 100644 --- a/submit-web/src/utils/constants.ts +++ b/submit-web/src/utils/constants.ts @@ -27,3 +27,6 @@ export const EXTENSION_TO_MIME_TYPE_MAP: Record = { zip: ["application/zip", "application/x-zip-compressed"], shp: ["application/octet-stream"], }; + +// Item type name for GIS/Geospatial submissions +export const GIS_ITEM_TYPE_NAME = "Geospatial Information";