From 0f9defaffd47e13c38ef118a614e2dd2b7e47d27 Mon Sep 17 00:00:00 2001 From: acatchpole Date: Wed, 15 Jan 2025 09:14:54 -0800 Subject: [PATCH] feat: review selected facilities page --- .../api/report_selected_facilties.py | 47 ++++ .../models/report_selected_facility.py | 36 +++ .../schema/report_selected_facility.py | 10 + .../service/report_facilities_service.py | 50 ++++ .../test_report_selected_facilities_api.py | 0 .../review-facilities-list/page.tsx | 11 +- .../review-facilities-list/page.tsx | 12 +- .../reviewFacilities/ReviewFacilitiesForm.tsx | 81 +++++++ .../reviewFacilities/ReviewFacilitiesPage.tsx | 228 ++++++++++++++++++ .../reviewFacilities/reviewFacilities.ts | 53 ++++ .../reviewFacilitiesInfoText.tsx | 80 ++++++ .../ReviewFacilities.test.tsx | 0 bciers/libs/actions/src/api/getFacilities.ts | 7 + 13 files changed, 605 insertions(+), 10 deletions(-) create mode 100644 bc_obps/reporting/api/report_selected_facilties.py create mode 100644 bc_obps/reporting/models/report_selected_facility.py create mode 100644 bc_obps/reporting/schema/report_selected_facility.py create mode 100644 bc_obps/reporting/tests/api/test_report_selected_facilities_api.py create mode 100644 bciers/apps/reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesForm.tsx create mode 100644 bciers/apps/reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesPage.tsx create mode 100644 bciers/apps/reporting/src/data/jsonSchema/reviewFacilities/reviewFacilities.ts create mode 100644 bciers/apps/reporting/src/data/jsonSchema/reviewFacilities/reviewFacilitiesInfoText.tsx create mode 100644 bciers/apps/reporting/src/tests/components/operations/reviewFacilities/ReviewFacilities.test.tsx create mode 100644 bciers/libs/actions/src/api/getFacilities.ts diff --git a/bc_obps/reporting/api/report_selected_facilties.py b/bc_obps/reporting/api/report_selected_facilties.py new file mode 100644 index 0000000000..46ff16fb88 --- /dev/null +++ b/bc_obps/reporting/api/report_selected_facilties.py @@ -0,0 +1,47 @@ +from typing import Literal +from uuid import UUID +from bc_obps.reporting.service.report_facilities_service import ReportFacilitiesService +from reporting.constants import EMISSIONS_REPORT_TAGS +from common.api.utils.current_user_utils import get_current_user_guid +from common.permissions import authorize +from registration.decorators import handle_http_errors +from service.error_service.custom_codes_4xx import custom_codes_4xx +from django.http import HttpRequest +from reporting.schema.generic import Message +from .router import router + +@router.get( + "report-version/{report_version_id}/selected-facilities", + response={200: list[UUID], custom_codes_4xx: Message}, + tags=EMISSIONS_REPORT_TAGS, + description="""Retrieves the list of selected facilities for a report version""", + exclude_none=True, + auyh=authorize("approved_industry_user"), +) +@handle_http_errors() +def get_selected_facilities( + request: HttpRequest, report_version_id: int +) -> tuple[Literal[200], list[UUID]]: + response_data = ReportFacilitiesService.get_selected_facilities(report_version_id) + return 200, response_data + +@router.post( + "report-version/{report_version_id}/selected-facilities", + response={200: int, custom_codes_4xx: Message}, + tags=EMISSIONS_REPORT_TAGS, + description="""Saves the list of selected facilities for a report version""", + auth=authorize("approved_industry_user"), +) +@handle_http_errors() +def save_selected_facilities( + request: HttpRequest, + report_version_id: int, + payload: list[UUID], +) -> Literal[200]: + ReportFacilitiesService.save_selected_facilities( + report_version_id, + payload, + get_current_user_guid(request), + ) + + return 200 diff --git a/bc_obps/reporting/models/report_selected_facility.py b/bc_obps/reporting/models/report_selected_facility.py new file mode 100644 index 0000000000..9fe4ab5262 --- /dev/null +++ b/bc_obps/reporting/models/report_selected_facility.py @@ -0,0 +1,36 @@ +from django.db import models +from registration.models.facility import Facility +from registration.models.time_stamped_model import TimeStampedModel +from reporting.models import ReportVersion + + +class ReportSelectedFacility(TimeStampedModel): + ''' + Model representing a the facilities selected for a report. + A report (each report version) may contain multiple facilities. + ''' + + facility = models.ForeignKey( + Facility, + on_delete=models.CASCADE, + db_comment="The facility selected to be included in the report", + related_name="report_selected_facilities", + ) + + report_version = models.ForeignKey( + ReportVersion, + on_delete=models.CASCADE, + db_comment="The report this facility is selected for", + related_name="reprot_selected_facilities", + ) + + class Meta: + db_table_comment = "A table to store the facilities selected for a report" + db_table = 'erc"."report_selected_facility' + app_label = 'reporting' + constraints = [ + models.UniqueConstraint( + fields=['report_version', 'facility_id'], + name="unique_selected_facility_per_facility_and_report_version", + ) + ] diff --git a/bc_obps/reporting/schema/report_selected_facility.py b/bc_obps/reporting/schema/report_selected_facility.py new file mode 100644 index 0000000000..feb10e9866 --- /dev/null +++ b/bc_obps/reporting/schema/report_selected_facility.py @@ -0,0 +1,10 @@ +from uuid import UUID + + +class ReportSelectedFacilitySchemaOut(Schema): + """ + Schema for the get selected facilities endpoint response + """ + facility_id: UUID + is_selected: bool + is_current: bool diff --git a/bc_obps/reporting/service/report_facilities_service.py b/bc_obps/reporting/service/report_facilities_service.py index b511946c8a..edfba601c8 100644 --- a/bc_obps/reporting/service/report_facilities_service.py +++ b/bc_obps/reporting/service/report_facilities_service.py @@ -1,7 +1,9 @@ from django.db import transaction +from reporting.models.report_selected_facility import ReportSelectedFacility from reporting.models import ReportVersion from registration.models import Facility from typing import List, Dict +from uuid import UUID class ReportFacilitiesService: @@ -27,3 +29,51 @@ def get_report_facility_list_by_version_id(version_id: int) -> Dict[str, List[st ) return {"facilities": facilities_list} + + @classmethod + @transaction.atomic + def save_selected_facilities( + cls, + version_id: int, + facility_list: List[UUID], + user_guid: UUID, + ) -> None: + """ + Save selected facility to report version. + + Args: + version_id: The report version ID + facility_list: The facility IDs of the selected facilities + user_guid: The user GUID of the user making the save request + + + """ + + # Delete existing selected facilities that are no longer selected + ReportSelectedFacility.objects.filter(report_version_id=version_id).exclude(facility_id__in=facility_list).delete() + + for facility_id in facility_list: + selected_facility_record, created = ReportSelectedFacility.objects.get_or_create( + report_version_id=version_id, + facility_id=facility_id, + ) + if created: + selected_facility_record.set_create_or_update(user_guid) + + @staticmethod + @transaction.atomic + def get_selected_facilities( + version_id: int, + ) -> List[UUID]: + """ + Get selected facilities for a report version. + + Args: + version_id: The report version ID + + Returns: + List of facility IDs + """ + return list(ReportSelectedFacility.objects.filter(report_version_id=version_id).values_list('facility_id', flat=True)) + + diff --git a/bc_obps/reporting/tests/api/test_report_selected_facilities_api.py b/bc_obps/reporting/tests/api/test_report_selected_facilities_api.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bciers/apps/reporting/src/app/bceidbusiness/industry_user/reports/[version_id]/review-facilities-list/page.tsx b/bciers/apps/reporting/src/app/bceidbusiness/industry_user/reports/[version_id]/review-facilities-list/page.tsx index 2fa306c12a..7d5f349eec 100644 --- a/bciers/apps/reporting/src/app/bceidbusiness/industry_user/reports/[version_id]/review-facilities-list/page.tsx +++ b/bciers/apps/reporting/src/app/bceidbusiness/industry_user/reports/[version_id]/review-facilities-list/page.tsx @@ -1,8 +1,15 @@ +import ReviewFacilities from "@reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesPage"; +import { Suspense } from "react"; +import Loading from "@bciers/components/loading/SkeletonForm"; + export default async function Page({ params, }: { params: { version_id: number }; }) { - console.log(params); - return <>Review Facilities List Page TBD; + return ( + }> + + + ); } diff --git a/bciers/apps/reporting/src/app/bceidbusiness/industry_user_admin/reports/[version_id]/review-facilities-list/page.tsx b/bciers/apps/reporting/src/app/bceidbusiness/industry_user_admin/reports/[version_id]/review-facilities-list/page.tsx index 2fa306c12a..0eb421dcba 100644 --- a/bciers/apps/reporting/src/app/bceidbusiness/industry_user_admin/reports/[version_id]/review-facilities-list/page.tsx +++ b/bciers/apps/reporting/src/app/bceidbusiness/industry_user_admin/reports/[version_id]/review-facilities-list/page.tsx @@ -1,8 +1,4 @@ -export default async function Page({ - params, -}: { - params: { version_id: number }; -}) { - console.log(params); - return <>Review Facilities List Page TBD; -} +import defaultPageFactory from "@bciers/components/nextPageFactory/defaultPageFactory"; +import Page from "@reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesPage"; + +export default defaultPageFactory(Page); diff --git a/bciers/apps/reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesForm.tsx b/bciers/apps/reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesForm.tsx new file mode 100644 index 0000000000..63a5ed968e --- /dev/null +++ b/bciers/apps/reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesForm.tsx @@ -0,0 +1,81 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import MultiStepFormWithTaskList from "@bciers/components/form/MultiStepFormWithTaskList"; +import SimpleModal from "@bciers/components/modal/SimpleModal"; +import { RJSFSchema } from "@rjsf/utils"; +import { + operationReviewSchema, + operationReviewUiSchema, + updateSchema, +} from "@reporting/src/data/jsonSchema/operations"; +import { actionHandler } from "@bciers/actions"; +import safeJsonParse from "@bciers/utils/src/safeJsonParse"; +import { + ActivePage, + getOperationInformationTaskList, +} from "@reporting/src/app/components/taskList/1_operationInformation"; +import { multiStepHeaderSteps } from "@reporting/src/app/components/taskList/multiStepHeaderConfig"; +import { Task } from "@nx/devkit"; +import { TaskListElement } from "@bciers/components/navigation/reportingTaskList/types"; + +interface Props { + Initialdata: any; + version_id: number; + taskListElements: TaskListElement[]; +} + +export default function ReviewFacilitiesForm({ + Initialdata, + version_id, + taskListElements, +}: Props) { + const [formData, setFormData] = useState(() => ({ + ...Initialdata, + })); + const [schema, setSchema] = useState(operationReviewSchema); + const [uiSchema, setUiSchema] = useState(operationReviewUiSchema); + const [errors, setErrors] = useState(); + const [submitButtonDisabled, setSubmitButtonDisabled] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + //const [modalMessage, setModalMessage] = useState(null); + //const [modalTitle, setModalTitle] = useState(null); + //const [modalType, setModalType] = useState<"error" | "success">("success"); + + const saveAndContinueUrl = `/reports/${version_id}/compliance-summary`; + + const handleChange = (e: any) => { + const updatedData = { ...e.formData }; + setFormData(updatedData); + }; + + const handleSubmit = async (data: any) => { + const endpoint = `reporting/report-version/${version_id}/selected-facilities`; + const method = "POST"; + const response = await actionHandler(endpoint, method, endpoint, { + body: JSON.stringify(data), + }); + if (response?.error) { + setErrors([response.error]); + return false; + } + + setErrors(undefined); + return true; + }; + + return ( + + ); +} diff --git a/bciers/apps/reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesPage.tsx b/bciers/apps/reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesPage.tsx new file mode 100644 index 0000000000..6f2afe353e --- /dev/null +++ b/bciers/apps/reporting/src/app/components/operations/reviewFacilities/ReviewFacilitiesPage.tsx @@ -0,0 +1,228 @@ +"use client"; +import { useState, useEffect } from "react"; +import { RJSFSchema } from "@rjsf/utils"; +import { getContacts } from "@bciers/actions/api"; +import { getContact } from "@bciers/actions/api"; +import { TaskListElement } from "@bciers/components/navigation/reportingTaskList/types"; +import debounce from "lodash.debounce"; +import { + reviewFacilitiesSchema, + reviewFacilitiesUiSchema, +} from "@reporting/src/data/jsonSchema/reviewFacilities/reviewFacilities"; +import MultiStepFormWithTaskList from "@bciers/components/form/MultiStepFormWithTaskList"; +import { + Contact, + ContactRow, +} from "@reporting/src/app/components/operations/types"; +import { actionHandler } from "@bciers/actions"; +import { createPersonResponsibleSchema } from "@reporting/src/app/components/operations/personResponsible/createPersonResponsibleSchema"; +import { getReportingPersonResponsible } from "@reporting/src/app/utils/getReportingPersonResponsible"; +import { getFacilityReport } from "@reporting/src/app/utils/getFacilityReport"; + +interface Props { + version_id: number; +} + +const ReviewFacilities = ({ version_id }: Props) => { + const [facilities, setFacilities] = useState<{ + items: ContactRow[]; + count: number; + } | null>(null); + const [selectedContactId, setSelectedContactId] = useState( + null, + ); + const [contactFormData, setContactFormData] = useState(null); + const [formData, setFormData] = useState({ + person_responsible: "", // Default to empty string + }); + const [operationType, setOperationType] = useState(null); + + const [schema, setSchema] = useState(reviewFacilitiesSchema); + + const continueUrl = '/reports/${version_id}/report-information'; + const backUrl = `/reports/${version_id}/person-responsible`; + + const taskListElements: TaskListElement[] = [ + { + type: "Section", + title: "Operation information", + isExpanded: true, + elements: [ + { + type: "Page", + title: "Review Operation information", + isChecked: true, + link: `/reports/${version_id}/review-operator-data`, + }, + { + type: "Page", + title: "Person responsible", + isActive: true, + link: `/reports/${version_id}/person-responsible`, + }, + { + type: "Page", + title: "Review Facilities", + link: `/reports/${version_id}/review-facilities`, + }, + ], + }, + ]; + + useEffect(() => { + const fetchData = async () => { + const contactData = await getContacts(); + setContacts(contactData); + + const personResponsibleData = + await getReportingPersonResponsible(version_id); + if (personResponsibleData && contactData) { + const matchingContact = contactData.items.find( + (contact: { first_name: string; last_name: string }) => + contact.first_name === personResponsibleData.first_name && + contact.last_name === personResponsibleData.last_name, + ); + + if (matchingContact) { + setSelectedContactId(matchingContact.id); + const newContactFormData: Contact = await getContact( + `${matchingContact.id}`, + ); + setContactFormData(newContactFormData); + setFormData({ + person_responsible: `${newContactFormData?.first_name} ${newContactFormData?.last_name}`, + }); + } + } + + const initialSchema = createPersonResponsibleSchema( + personResponsibleSchema, + contactData?.items, + selectedContactId, + ); + setSchema(initialSchema); + }; + + fetchData(); + }, [version_id]); + + // Update schema whenever selectedContactId or contactFormData changes + useEffect(() => { + if (selectedContactId !== null && contactFormData) { + const updatedSchema = createPersonResponsibleSchema( + personResponsibleSchema, + contacts?.items || [], + selectedContactId, + contactFormData, + ); + setSchema(updatedSchema); + } + }, [selectedContactId, contactFormData]); + + useEffect(() => { + const getFacilityId = async () => { + const facilityReport = await getFacilityReport(version_id); + if (facilityReport?.facility_id) { + setFacilityId(facilityReport.facility_id); + setOperationType(facilityReport.operation_type); + } else { + setFacilityId(null); + setOperationType(null); + } + }; + getFacilityId(); + }, []); + + const handleContactSelect = debounce(async (e: any) => { + const selectedFullName = e.formData?.person_responsible; + + const selectedContact = contacts?.items.find( + (contact) => + `${contact.first_name} ${contact.last_name}` === selectedFullName, + ); + + if (selectedContact) { + const newSelectedContactId = selectedContact.id; + const newContactFormData: Contact = await getContact( + `${selectedContact.id}`, + ); + + setSelectedContactId(newSelectedContactId); + setContactFormData(newContactFormData); + setFormData({ + person_responsible: `${newContactFormData?.first_name || ""} ${ + newContactFormData?.last_name || "" + }`.trim(), + }); + } else { + setSelectedContactId(null); + setContactFormData(null); + setFormData((prevFormData: any) => ({ + ...prevFormData, + person_responsible: "", // Reset to empty string if no contact is selected + })); + } + }, 300); + + const handleSave = async () => { + const endpoint = `reporting/report-version/${version_id}/report-contact`; + const method = "POST"; + const payload = { + report_version: version_id, + ...contactFormData, + }; + + await actionHandler(endpoint, method, endpoint, { + body: JSON.stringify(payload), + }); + }; + + const handleSync = async () => { + const updatedContacts = await getContacts(); + setContacts(updatedContacts); + setSelectedContactId(null); + setContactFormData(null); + setFormData({ person_responsible: "" }); + + const updatedSchema = createPersonResponsibleSchema( + personResponsibleSchema, + updatedContacts.items, + null, + ); + setSchema(updatedSchema); + }; + + return ( + <> + + + ); +}; + +export default ReviewFacilities; diff --git a/bciers/apps/reporting/src/data/jsonSchema/reviewFacilities/reviewFacilities.ts b/bciers/apps/reporting/src/data/jsonSchema/reviewFacilities/reviewFacilities.ts new file mode 100644 index 0000000000..0d7783b9ff --- /dev/null +++ b/bciers/apps/reporting/src/data/jsonSchema/reviewFacilities/reviewFacilities.ts @@ -0,0 +1,53 @@ +import { RJSFSchema } from "@rjsf/utils"; +import FieldTemplate from "@bciers/components/form/fields/FieldTemplate"; +import { infoNote, instructionNote, SyncFacilitiesButton } from "./reviewFacilitiesInfoText" +import { TitleOnlyFieldTemplate, SectionFieldTemplate } from "@bciers/components/form/fields"; + +export const reviewFacilitiesSchema: RJSFSchema = { + title: "Review Facilities", + type: "object", + properties: { + facilities_note: { + type: "object", + readOnly: true, + }, + select_info: { + type: "object", + readOnly: true, + }, + sync_button: { + type: "object", + }, + }, +}; + +export const reviewFacilitiesUiSchema = { + "ui:FieldTemplate": FieldTemplate, + "ui:classNames": "form-heading-label", + "ui:order": [ + "facilities_note", + "select_info", + "current_facilities", + "past_facilities", + "sync_button", + ], + facilities_note: { + "ui:FieldTemplate": TitleOnlyFieldTemplate, + "ui:title": infoNote, + }, + select_info: { + "ui:FieldTemplate": TitleOnlyFieldTemplate, + "ui:title": instructionNote, + }, + sync_button: { + "ui:FieldTemplate": SyncFacilitiesButton, + }, + current_facilities: { + "ui:FieldTemplate": SectionFieldTemplate, + "ui:title": "List of facilities currently assigned to this operation", + }, + past_facilities: { + "ui:FieldTemplate": SectionFieldTemplate, + "ui:title": "Past facilities that belonged to this operation", + }, +}; diff --git a/bciers/apps/reporting/src/data/jsonSchema/reviewFacilities/reviewFacilitiesInfoText.tsx b/bciers/apps/reporting/src/data/jsonSchema/reviewFacilities/reviewFacilitiesInfoText.tsx new file mode 100644 index 0000000000..cf2abc63e3 --- /dev/null +++ b/bciers/apps/reporting/src/data/jsonSchema/reviewFacilities/reviewFacilitiesInfoText.tsx @@ -0,0 +1,80 @@ +import InfoIcon from "@mui/icons-material/Info"; +import { Box, Paper, Typography, Link, Button } from "@mui/material"; +import { BC_GOV_TEXT, LIGHT_BLUE_BG_COLOR } from "@bciers/styles"; +import { FieldTemplateProps } from "@rjsf/utils"; +import React from "react"; +import LoopIcon from "@mui/icons-material/Loop"; + +export const infoNote = ( + + + + + Linear Facilities Operations must register and report for all{" "} + + large + + and{" "} + + medium facilities + + , as well as{" "} + + small aggregate + + , if applicable. Don't see a facility?{" "} + + Add it under Facilities Information + {" "} + and click on the 'Sync latest data from Administration' button + below to update the list. + + + +); + +export const instructionNote = ( + Select the facilities that apply to your operation, prior to Dec 31 of the current reporting year. +); + +export const SyncFacilitiesButton: React.FC = ({ + uiSchema, +}) => { + const onSync = uiSchema?.["ui:options"]?.onSync; + + const handleClick = () => { + if (typeof onSync === "function") { + onSync(); + } + }; + + return ( + + ); +}; diff --git a/bciers/apps/reporting/src/tests/components/operations/reviewFacilities/ReviewFacilities.test.tsx b/bciers/apps/reporting/src/tests/components/operations/reviewFacilities/ReviewFacilities.test.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bciers/libs/actions/src/api/getFacilities.ts b/bciers/libs/actions/src/api/getFacilities.ts new file mode 100644 index 0000000000..07f7c76fb5 --- /dev/null +++ b/bciers/libs/actions/src/api/getFacilities.ts @@ -0,0 +1,7 @@ +import { actionHandler } from "@bciers/actions"; + +async function getFacilities(id: string) { + return actionHandler(`registration/operations/${id}/facilities`, "GET", ""); +} + +export default getFacilities;