diff --git a/submit-api/migrations/versions/67a4a5ef2087_.py b/submit-api/migrations/versions/67a4a5ef2087_.py new file mode 100644 index 00000000..876a86d9 --- /dev/null +++ b/submit-api/migrations/versions/67a4a5ef2087_.py @@ -0,0 +1,58 @@ +from alembic import op +import sqlalchemy as sa + +# Revision identifiers, used by Alembic +revision = '67a4a5ef2087' +down_revision = '9655618a231b' +branch_labels = None +depends_on = None + +# Define the enums +itemstatus = sa.Enum('NEW_SUBMISSION', 'PARTIALLY_COMPLETED', 'COMPLETED', name='itemstatus') +temp_itemstatus = sa.Enum('PENDING', 'NEW_SUBMISSION', 'PARTIALLY_COMPLETED', 'COMPLETED', name='temp_itemstatus') +old_itemstatus = sa.Enum('PENDING', 'COMPLETED', name='itemstatus') + +def upgrade(): + # Step 1: Create the temporary enum type with 'PENDING' included + temp_itemstatus.create(op.get_bind(), checkfirst=False) + + # Step 2: Alter the column to use the temporary enum type, allowing both 'PENDING' and 'NEW_SUBMISSION' + op.alter_column('items', 'status', type_=temp_itemstatus, postgresql_using='status::text::temp_itemstatus') + + # Step 3: Update all instances of 'PENDING' to 'NEW_SUBMISSION' in the items table + op.execute("UPDATE items SET status = 'NEW_SUBMISSION' WHERE status = 'PENDING'") + + # Step 4: Drop the old enum type + itemstatus.drop(op.get_bind(), checkfirst=False) + + # Step 5: Recreate the original enum with the new values (excluding 'PENDING') + itemstatus.create(op.get_bind(), checkfirst=False) + + # Step 6: Alter the column to use the new version of the original enum + op.alter_column('items', 'status', type_=itemstatus, postgresql_using='status::text::itemstatus') + + # Step 7: Drop the temporary enum type after migration + temp_itemstatus.drop(op.get_bind(), checkfirst=False) + + +def downgrade(): + # Step 1: Create the temporary enum type with the values needed for conversion, including 'PENDING' + temp_itemstatus.create(op.get_bind(), checkfirst=False) + + # Step 2: Alter the column to use the temporary enum type (with 'PENDING') to allow conversion + op.alter_column('items', 'status', type_=temp_itemstatus, postgresql_using='status::text::temp_itemstatus') + + # Step 3: Update any instances of 'NEW_SUBMISSION' back to 'PENDING' after confirming the column type allows it + op.execute("UPDATE items SET status = 'PENDING' WHERE status = 'NEW_SUBMISSION'") + op.execute("UPDATE items SET status = 'PENDING' WHERE status = 'PARTIALLY_COMPLETED'") + # Step 4: Drop the current itemstatus enum + itemstatus.drop(op.get_bind(), checkfirst=False) + + # Step 5: Recreate the old enum type (including 'PENDING' and 'COMPLETED') + old_itemstatus.create(op.get_bind(), checkfirst=False) + + # Step 6: Alter the column to use the old enum type + op.alter_column('items', 'status', type_=old_itemstatus, postgresql_using='status::text::itemstatus') + + # Step 7: Drop the temporary enum type after conversion + temp_itemstatus.drop(op.get_bind(), checkfirst=False) \ No newline at end of file diff --git a/submit-api/src/submit_api/models/item.py b/submit-api/src/submit_api/models/item.py index 35708d24..7750791f 100644 --- a/submit-api/src/submit_api/models/item.py +++ b/submit-api/src/submit_api/models/item.py @@ -15,7 +15,8 @@ class ItemStatus(enum.Enum): """Enum for item statuses.""" - PENDING = 'PENDING' + NEW_SUBMISSION = 'NEW_SUBMISSION' + PARTIALLY_COMPLETED = 'PARTIALLY_COMPLETED' COMPLETED = 'COMPLETED' @@ -30,7 +31,7 @@ class Item(BaseModel): sort_order = Column(db.Integer, nullable=True, default=0) type = db.relationship('ItemType', foreign_keys=[type_id], lazy='joined') status = Column(Enum(ItemStatus), nullable=False, - default=ItemStatus.PENDING) + default=ItemStatus.NEW_SUBMISSION) submitted_on = Column(db.DateTime, nullable=True) submitted_by = Column(db.String(255), nullable=True) version = Column(db.Integer, nullable=False, default=1) diff --git a/submit-api/src/submit_api/resources/submission.py b/submit-api/src/submit_api/resources/submission.py index 49e685d1..47b801ce 100644 --- a/submit-api/src/submit_api/resources/submission.py +++ b/submit-api/src/submit_api/resources/submission.py @@ -20,7 +20,6 @@ from submit_api.schemas.submission import CreateSubmissionRequestSchema, SubmissionSchema from submit_api.services.submission import SubmissionService from submit_api.utils.util import cors_preflight - from ..auth import auth from .apihelper import Api as ApiHelper @@ -55,6 +54,10 @@ def post(submission_item_id): """Create a submission.""" create_submission_data = CreateSubmissionRequestSchema().load(API.payload) created_submission = SubmissionService.create_submission(submission_item_id, create_submission_data) + item_id = create_submission_data.get("item_id") + status = create_submission_data.get("status") + if status: + SubmissionService.update_submission_item_status(item_id, status) return SubmissionSchema().dump(created_submission), HTTPStatus.CREATED @@ -76,4 +79,8 @@ def patch(submission_id): """Edit a submission.""" edit_submission_data = CreateSubmissionRequestSchema().load(API.payload) edited_submission = SubmissionService.edit_submission_form(submission_id, edit_submission_data) + item_id = edit_submission_data.get("item_id") + status = edit_submission_data.get("status") + if status: + SubmissionService.update_submission_item_status(item_id, status) return SubmissionSchema().dump(edited_submission), HTTPStatus.OK diff --git a/submit-api/src/submit_api/schemas/submission.py b/submit-api/src/submit_api/schemas/submission.py index ec659652..209de387 100644 --- a/submit-api/src/submit_api/schemas/submission.py +++ b/submit-api/src/submit_api/schemas/submission.py @@ -67,4 +67,6 @@ class Meta: # pylint: disable=too-few-public-methods unknown = EXCLUDE type = fields.Str(data_key="type") + status = fields.Str(data_key="status") data = fields.Dict(data_key="data") + item_id = fields.Int(data_key="item_id") diff --git a/submit-api/src/submit_api/services/item.py b/submit-api/src/submit_api/services/item.py index 2e1c0ee2..a00e731d 100644 --- a/submit-api/src/submit_api/services/item.py +++ b/submit-api/src/submit_api/services/item.py @@ -11,3 +11,16 @@ def get_item_by_id(cls, item_id): """Get item by id.""" item = ItemModel.find_by_id(item_id) return item + + @classmethod + def update_submission_item(cls, item_id, update_data): + """Update submission item by id.""" + submission_item = cls.get_item_by_id(item_id) + if not submission_item: + raise ValueError(f"Item with id {item_id} not found.") + + for key, value in update_data.items(): + setattr(submission_item, key, value) + + submission_item.save() + return submission_item diff --git a/submit-api/src/submit_api/services/submission/__init__.py b/submit-api/src/submit_api/services/submission/__init__.py index b0691ccd..fe4a9b8f 100644 --- a/submit-api/src/submit_api/services/submission/__init__.py +++ b/submit-api/src/submit_api/services/submission/__init__.py @@ -4,6 +4,7 @@ from submit_api.models.submission import SubmissionTypeStatus from submit_api.services.submission.submission_creator_factory import ( DocumentSubmissionCreator, FormSubmissionCreator, SubmissionCreatorFactory) +from submit_api.services.item import ItemService class SubmissionService: @@ -36,10 +37,10 @@ def create_submission(cls, item_id, request_data): submission_type = request_data.get("type") if not submission_type: raise ValueError("Submission type is required.") - submission_creator = cls.make_submission_creator(submission_type) submission_data = request_data.get("data") - return submission_creator.create(item_id, submission_data) + submission = submission_creator.create(item_id, submission_data) + return submission @classmethod def get_submission_by_id_and_validate_edit(cls, submission_id): @@ -61,3 +62,11 @@ def edit_submission_form(cls, submission_id, request): submission.submitted_form.submission_json = request.get('data') submission.submitted_form.save() return submission + + @classmethod + def update_submission_item_status(cls, item_id, status): + """Update the status of the submission item.""" + if status is None: + raise ValueError("Status is required.") + update_data = {"status": status} + ItemService.update_submission_item(item_id, update_data) diff --git a/submit-web/src/components/Submission/SubmissionItemTableRow.tsx b/submit-web/src/components/Submission/SubmissionItemTableRow.tsx index 6cc68946..0b851b03 100644 --- a/submit-web/src/components/Submission/SubmissionItemTableRow.tsx +++ b/submit-web/src/components/Submission/SubmissionItemTableRow.tsx @@ -116,9 +116,7 @@ export default function SubmissionItemTableRow({ - + diff --git a/submit-web/src/components/Submission/SubmissionStatusChip.tsx b/submit-web/src/components/Submission/SubmissionStatusChip.tsx index a052ea7f..c3b10c54 100644 --- a/submit-web/src/components/Submission/SubmissionStatusChip.tsx +++ b/submit-web/src/components/Submission/SubmissionStatusChip.tsx @@ -7,12 +7,14 @@ type StyleProps = { sx: Record; label: string; }; + const statusStyles: Record = { NEW_SUBMISSION: { sx: { borderRadius: 1, border: `2px solid ${EAOColors.DecisionDark}`, background: EAOColors.DecisionLight, + height: "24px", }, label: "New Submission", }, @@ -21,15 +23,17 @@ const statusStyles: Record = { borderRadius: 1, border: `2px solid ${BCDesignTokens.supportBorderColorSuccess}`, background: BCDesignTokens.supportSurfaceColorSuccess, + height: "24px", }, label: "Completed", }, - PARTIALLY_COMPLETE: { - label: "Partially Complete", + PARTIALLY_COMPLETED: { + label: "Partially Completed", sx: { borderRadius: 1, border: `2px solid ${BCDesignTokens.supportBorderColorWarning}`, background: BCDesignTokens.supportSurfaceColorWarning, + height: "24px", }, }, SUBMITTED: { @@ -38,6 +42,7 @@ const statusStyles: Record = { borderRadius: 1, border: `2px solid ${BCDesignTokens.themeBlue100}`, background: BCDesignTokens.themeBlue20, + height: "24px", }, }, }; @@ -47,7 +52,7 @@ export default function SubmissionStatusChip({ }: { status: SubmissionStatus; }) { - const style = statusStyles[status]; + const style = statusStyles[status] || statusStyles.NEW_SUBMISSION; return ( { }); const formSubmission = submissionItem?.submissions?.find( - (submission) => submission.type === SUBMISSION_TYPE.FORM, + (submission) => submission.type === SUBMISSION_TYPE.FORM ); const defaultFormValues = useMemo(() => { if (!formSubmission?.submitted_form?.submission_json) return {}; @@ -89,31 +93,31 @@ export const ConsultationRecord = () => { return { ...formSubmission.submitted_form.submission_json, allPartiesConsulted: booleanToString( - formSubmission.submitted_form.submission_json.allPartiesConsulted, + formSubmission.submitted_form.submission_json.allPartiesConsulted ), planWasReviewed: booleanToString( - formSubmission.submitted_form.submission_json.planWasReviewed, + formSubmission.submitted_form.submission_json.planWasReviewed ), writtenExplanationsProvidedToParties: booleanToString( formSubmission.submitted_form.submission_json - .writtenExplanationsProvidedToParties, + .writtenExplanationsProvidedToParties ), writtenExplanationsProvidedToCommenters: booleanToString( formSubmission.submitted_form.submission_json - .writtenExplanationsProvidedToCommenters, + .writtenExplanationsProvidedToCommenters ), }; }, [formSubmission]); const documentSubmissions = submissionItem?.submissions?.filter( - (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT, + (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT ); const defaultDocumentValues = useMemo(() => { if (!documentSubmissions) return {}; return { consultationRecords: documentSubmissions.map( - (submission) => submission.submitted_document.url, + (submission) => submission.submitted_document.url ), }; }, [documentSubmissions]); @@ -162,7 +166,14 @@ export const ConsultationRecord = () => { formState: { errors, dirtyFields }, } = methods; - const saveSubmission = async (formData: ConsultationRecordForm) => { + const handleCompleteForm = (formData: ConsultationRecordForm) => { + saveSubmission(formData, SUBMISSION_STATUS.COMPLETED.value); // Add default status here + }; + + const saveSubmission = async ( + formData: ConsultationRecordForm, + status: SubmissionStatus + ) => { const { consultedParties, allPartiesConsulted, @@ -173,15 +184,17 @@ export const ConsultationRecord = () => { callSaveSubmission({ data: { type: SUBMISSION_TYPE.FORM, + status, + item_id: submissionItemId, data: { consultedParties, allPartiesConsulted: stringToBoolean(allPartiesConsulted), planWasReviewed: stringToBoolean(planWasReviewed), writtenExplanationsProvidedToParties: stringToBoolean( - writtenExplanationsProvidedToParties, + writtenExplanationsProvidedToParties ), writtenExplanationsProvidedToCommenters: stringToBoolean( - writtenExplanationsProvidedToCommenters, + writtenExplanationsProvidedToCommenters ), }, }, @@ -199,7 +212,7 @@ export const ConsultationRecord = () => { ...methods.getValues(), }; - saveSubmission(formData); + saveSubmission(formData, SUBMISSION_STATUS.PARTIALLY_COMPLETED.value); }; useEffect(() => { @@ -243,7 +256,7 @@ export const ConsultationRecord = () => { title={accountProject.project.name + " Management Plan"} /> -
+ { @@ -401,7 +414,7 @@ export const ConsultationRecord = () => { diff --git a/submit-web/src/components/SubmissionItem/ContactInformation/index.tsx b/submit-web/src/components/SubmissionItem/ContactInformation/index.tsx index 601abf49..93e5bfbf 100644 --- a/submit-web/src/components/SubmissionItem/ContactInformation/index.tsx +++ b/submit-web/src/components/SubmissionItem/ContactInformation/index.tsx @@ -10,7 +10,7 @@ import { notify } from "@/components/Shared/Snackbar/snackbarStore"; import { useEffect, useMemo } from "react"; import { useLoaderBackdrop } from "@/components/Shared/Overlays/loaderBackdropStore"; import { Navigate, useNavigate, useParams } from "@tanstack/react-router"; -import { SUBMISSION_TYPE } from "@/models/Submission"; +import { SUBMISSION_STATUS, SUBMISSION_TYPE } from "@/models/Submission"; import ControlledInputMask from "@/components/Shared/controlled/ControlledInputMask"; import BarTitle from "@/components/Shared/Text/BarTitle"; import { CardInnerBox } from "@/components/Projects/Project"; @@ -65,7 +65,7 @@ export const ContactInformation = () => { const navigate = useNavigate(); const formSubmission = submissionItem?.submissions.find( - (submission) => submission.type === SUBMISSION_TYPE.FORM, + (submission) => submission.type === SUBMISSION_TYPE.FORM ); const defaultValues = useMemo(() => { if (!formSubmission?.submitted_form?.submission_json) return {}; @@ -103,6 +103,8 @@ export const ContactInformation = () => { const request = { type: SUBMISSION_TYPE.FORM, data: formData, + status: SUBMISSION_STATUS.COMPLETED.value, + item_id: submissionItem.id, }; saveSubmission({ data: request, diff --git a/submit-web/src/components/SubmissionItem/ManagementPlanSubmission/index.tsx b/submit-web/src/components/SubmissionItem/ManagementPlanSubmission/index.tsx index 8762d46a..6c461559 100644 --- a/submit-web/src/components/SubmissionItem/ManagementPlanSubmission/index.tsx +++ b/submit-web/src/components/SubmissionItem/ManagementPlanSubmission/index.tsx @@ -9,7 +9,11 @@ import { notify } from "@/components/Shared/Snackbar/snackbarStore"; import { useEffect, useMemo } from "react"; import { useLoaderBackdrop } from "@/components/Shared/Overlays/loaderBackdropStore"; import { Navigate, useNavigate, useParams } from "@tanstack/react-router"; -import { SUBMISSION_TYPE } from "@/models/Submission"; +import { + SUBMISSION_STATUS, + SUBMISSION_TYPE, + SubmissionStatus, +} from "@/models/Submission"; import { useGetProject } from "@/hooks/api/useProjects"; import { CardInnerBox } from "@/components/Projects/Project"; import { PROJECT_STATUS } from "@/components/registration/addProjects/ProjectCard/constants"; @@ -61,7 +65,7 @@ export const ManagementPlanSubmission = () => { }); const formSubmission = submissionItem?.submissions.find( - (submission) => submission.type === SUBMISSION_TYPE.FORM, + (submission) => submission.type === SUBMISSION_TYPE.FORM ); const defaultFormValues = useMemo(() => { if (!formSubmission?.submitted_form?.submission_json) return {}; @@ -69,22 +73,22 @@ export const ManagementPlanSubmission = () => { return { ...formSubmission.submitted_form.submission_json, conditionSatisfied: booleanToString( - formSubmission.submitted_form.submission_json.conditionSatisfied, + formSubmission.submitted_form.submission_json.conditionSatisfied ), allRequirementsAddressed: booleanToString( - formSubmission.submitted_form.submission_json.allRequirementsAddressed, + formSubmission.submitted_form.submission_json.allRequirementsAddressed ), requirementsClear: booleanToString( - formSubmission.submitted_form.submission_json.requirementsClear, + formSubmission.submitted_form.submission_json.requirementsClear ), informationAccurate: booleanToString( - formSubmission.submitted_form.submission_json.informationAccurate, + formSubmission.submitted_form.submission_json.informationAccurate ), }; }, [formSubmission]); const documentSubmissions = submissionItem?.submissions?.filter( - (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT, + (submission) => submission.type === SUBMISSION_TYPE.DOCUMENT ); const defaultDocumentValues = useMemo(() => { if (!documentSubmissions) return {}; @@ -94,14 +98,14 @@ export const ManagementPlanSubmission = () => { .filter( (submission) => submission.submitted_document.folder === - MANAGEMENT_PLAN_DOCUMENT_FOLDERS.MANAGEMENT_PLAN, + MANAGEMENT_PLAN_DOCUMENT_FOLDERS.MANAGEMENT_PLAN ) .map((submission) => submission.submitted_document.url), supportingDocuments: documentSubmissions .filter( (submission) => submission.submitted_document.folder === - MANAGEMENT_PLAN_DOCUMENT_FOLDERS.SUPPORTING, + MANAGEMENT_PLAN_DOCUMENT_FOLDERS.SUPPORTING ) .map((submission) => submission.submitted_document.url), }; @@ -142,7 +146,14 @@ export const ManagementPlanSubmission = () => { return () => setIsOpen(false); }, [isCreatingSubmissionPending, setIsOpen]); - const saveSubmission = async (formData: ManagementPlanSubmissionForm) => { + const handleCompleteForm = (formData: ManagementPlanSubmissionForm) => { + saveSubmission(formData, SUBMISSION_STATUS.COMPLETED.value); // Add default status here + }; + + const saveSubmission = async ( + formData: ManagementPlanSubmissionForm, + status: SubmissionStatus + ) => { const { conditionSatisfied, allRequirementsAddressed, @@ -152,6 +163,8 @@ export const ManagementPlanSubmission = () => { callSaveSubmission({ data: { type: SUBMISSION_TYPE.FORM, + status, + item_id: submissionItemId, data: { conditionSatisfied: stringToBoolean(conditionSatisfied), allRequirementsAddressed: stringToBoolean(allRequirementsAddressed), @@ -173,7 +186,7 @@ export const ManagementPlanSubmission = () => { ...methods.getValues(), }; - saveSubmission(formData); + saveSubmission(formData, SUBMISSION_STATUS.PARTIALLY_COMPLETED.value); }; if (!accountProject) return ; @@ -212,7 +225,7 @@ export const ManagementPlanSubmission = () => { title={accountProject.project.name + " Management Plan"} /> - +