diff --git a/services/common/src/components/reports/ReportDefinitionFieldSelect.tsx b/services/common/src/components/reports/ReportDefinitionFieldSelect.tsx new file mode 100644 index 0000000000..4e0dcf3f4d --- /dev/null +++ b/services/common/src/components/reports/ReportDefinitionFieldSelect.tsx @@ -0,0 +1,60 @@ +import { formatComplianceCodeReportName } from "@mds/common/redux/utils/helpers"; +import React, { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { Field } from "redux-form"; + +import { getMineReportDefinitionOptions } from "@mds/common/redux/selectors/staticContentSelectors"; + +import RenderSelect from "../forms/RenderSelect"; +import { uniqBy } from "lodash"; +import moment from "moment"; + +export interface ReportDefinitionFieldSelectProps { + id: string; + name: string; + label?: string; + disabled?: boolean; + required?: boolean; + placeholder?: string; + validate?: any[]; +} + +export const ReportDefinitionFieldSelect = (props: ReportDefinitionFieldSelectProps) => { + const mineReportDefinitionOptions = useSelector(getMineReportDefinitionOptions); + + const [formattedMineReportDefinitionOptions, setFormatMineReportDefinitionOptions] = useState([]); + + useEffect(() => { + // Format the mine report definition options for the search bar + const newFormattedMineReportDefinitionOptions = mineReportDefinitionOptions + .filter((m) => { + // Only include reports that are linked to a compliance code that have expired + // Reason: A report definition can only be linked to a single compliance code as it currently stands + return !m.compliance_articles.find((c) => moment().isBefore(moment(c.expiry_date))); + }) + .map((report) => { + return { + label: formatComplianceCodeReportName(report), + value: report.mine_report_definition_guid, + }; + }) + .sort((a, b) => a.label.localeCompare(b.label)); + setFormatMineReportDefinitionOptions(uniqBy(newFormattedMineReportDefinitionOptions, "value")); + }, [mineReportDefinitionOptions]); + + return ( + + ); +}; diff --git a/services/common/src/interfaces/complianceArticle.interface.ts b/services/common/src/interfaces/complianceArticle.interface.ts index d9ba6d7a0b..fa90064812 100644 --- a/services/common/src/interfaces/complianceArticle.interface.ts +++ b/services/common/src/interfaces/complianceArticle.interface.ts @@ -1,3 +1,5 @@ +import { IMineReportDefinition } from "./reports"; + export interface IComplianceArticle { compliance_article_id: number; articleNumber?: string; @@ -13,4 +15,5 @@ export interface IComplianceArticle { expiry_date: Date; help_reference_link: string; cim_or_cpo: string; + reports: IMineReportDefinition[]; } diff --git a/services/core-api/app/api/compliance/resources/compliance_article_create_resource.py b/services/core-api/app/api/compliance/resources/compliance_article_create_resource.py index 8b35eb925e..e2e2881945 100644 --- a/services/core-api/app/api/compliance/resources/compliance_article_create_resource.py +++ b/services/core-api/app/api/compliance/resources/compliance_article_create_resource.py @@ -1,15 +1,17 @@ from flask_restx import Resource, inputs, reqparse from datetime import datetime from app.api.compliance.models.compliance_article import ComplianceArticle +from app.api.mines.reports.models.mine_report_definition import MineReportDefinition from app.api.compliance.response_models import COMPLIANCE_ARTICLE_MODEL from app.api.utils.resources_mixins import UserMixin from app.extensions import api from app.api.utils.access_decorators import EDIT_CODE, requires_any_of from werkzeug.exceptions import BadRequest - +from app.api.exports.static_content.cache_service import reset_static_content_cache class ComplianceArticleCreateResource(Resource, UserMixin): parser = reqparse.RequestParser() + reports_parser = reqparse.RequestParser() parser.add_argument( 'article_act_code', @@ -89,6 +91,14 @@ class ComplianceArticleCreateResource(Resource, UserMixin): location='json', ) + parser.add_argument( + 'reports', + type=list, + location='json', + store_missing=False, + required=False + ) + @api.doc(description='Create a new Compliance Article.') @api.expect(parser) @api.marshal_with(COMPLIANCE_ARTICLE_MODEL, code=201) @@ -108,6 +118,10 @@ def post(self): effective_date = data.get('effective_date') expiry_date = data.get('expiry_date') + + reports = data.get('reports') + + compliance_article = ComplianceArticle.find_existing_compliance_article(article_act_code, section, sub_section, paragraph, sub_paragraph, @@ -126,5 +140,17 @@ def post(self): expiry_date, help_reference_link, cim_or_cpo) + + + if reports is not None and len(reports): + report_guids = [r.get('mine_report_definition_guid') for r in reports] + reports = MineReportDefinition.find_by_mine_report_definition_many(report_guids) + + new_compliance_article.reports = reports new_compliance_article.save() + + # Mine report definitions are cached for 60min by the API as they're rarely updated + # Manually clear the cache to apply the changes immediately + reset_static_content_cache() + return new_compliance_article, 201 diff --git a/services/core-api/app/api/compliance/response_models.py b/services/core-api/app/api/compliance/response_models.py index 99e78337f8..b57b623c0a 100644 --- a/services/core-api/app/api/compliance/response_models.py +++ b/services/core-api/app/api/compliance/response_models.py @@ -1,6 +1,20 @@ from app.extensions import api from flask_restx import fields +MINE_REPORT_DEFINITION_BASE_MODEL = api.model( + 'MineReportDefinitionBase', { + 'mine_report_definition_guid': fields.String, + 'report_name': fields.String, + 'description': fields.String, + 'due_date_period_months': fields.Integer, + 'mine_report_due_date_type': fields.String, + 'default_due_date': fields.Date, + 'active_ind': fields.Boolean, + 'is_common': fields.Boolean, + 'is_prr_only': fields.Boolean, + }) + + COMPLIANCE_ARTICLE_MODEL = api.model( 'ComplianceArticle', { 'compliance_article_id': fields.Integer, @@ -14,7 +28,8 @@ 'effective_date': fields.Date, 'expiry_date': fields.Date, 'help_reference_link': fields.String, - 'cim_or_cpo': fields.String + 'cim_or_cpo': fields.String, + 'reports': fields.List(fields.Nested(MINE_REPORT_DEFINITION_BASE_MODEL)) }) COMPLIANCE_ARTICLE_UPDATE_MODEL = api.model( diff --git a/services/core-api/app/api/exports/static_content/__init__.py b/services/core-api/app/api/exports/static_content/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/core-api/app/api/exports/static_content/cache_service.py b/services/core-api/app/api/exports/static_content/cache_service.py new file mode 100644 index 0000000000..97e916b176 --- /dev/null +++ b/services/core-api/app/api/exports/static_content/cache_service.py @@ -0,0 +1,6 @@ + +from app.extensions import cache +from app.api.constants import STATIC_CONTENT_KEY + +def reset_static_content_cache(): + cache.delete(STATIC_CONTENT_KEY) diff --git a/services/core-api/app/api/mines/reports/models/mine_report_definition.py b/services/core-api/app/api/mines/reports/models/mine_report_definition.py index 97587de48d..a17e020738 100644 --- a/services/core-api/app/api/mines/reports/models/mine_report_definition.py +++ b/services/core-api/app/api/mines/reports/models/mine_report_definition.py @@ -32,7 +32,9 @@ class MineReportDefinition(Base, AuditMixin): compliance_articles = db.relationship( 'ComplianceArticle', lazy='selectin', - secondary='mine_report_definition_compliance_article_xref') + secondary='mine_report_definition_compliance_article_xref', + backref='reports' + ) def __repr__(self): return '' % self.mine_report_definition_guid @@ -52,6 +54,13 @@ def find_by_mine_report_definition_id(cls, _id): except ValueError: return None + @classmethod + def find_by_mine_report_definition_many(cls, _guids): + try: + return cls.query.filter(cls.mine_report_definition_guid.in_(_guids)).all() + except ValueError: + return None + @classmethod def find_by_mine_report_definition_guid(cls, _id): try: diff --git a/services/core-api/app/api/mines/response_models.py b/services/core-api/app/api/mines/response_models.py index e99198d30d..c1e224bc06 100644 --- a/services/core-api/app/api/mines/response_models.py +++ b/services/core-api/app/api/mines/response_models.py @@ -762,8 +762,8 @@ def format(self, value): 'active_ind': fields.Boolean }) -MINE_REPORT_DEFINITION_MODEL = api.model( - 'MineReportDefinition', { +MINE_REPORT_DEFINITION_BASE_MODEL = api.model( + 'MineReportDefinitionBase', { 'mine_report_definition_guid': fields.String, 'report_name': fields.String, 'description': fields.String, @@ -772,11 +772,14 @@ def format(self, value): 'default_due_date': fields.Date, 'active_ind': fields.Boolean, 'categories': fields.List(fields.Nested(MINE_REPORT_DEFINITION_CATEGORIES)), - 'compliance_articles': fields.List(fields.Nested(COMPLIANCE_ARTICLE_MODEL)), 'is_common': fields.Boolean, 'is_prr_only': fields.Boolean, }) +MINE_REPORT_DEFINITION_MODEL = api.inherit('MineReportDefinition', MINE_REPORT_DEFINITION_BASE_MODEL, { + 'compliance_articles': fields.List(fields.Nested(COMPLIANCE_ARTICLE_MODEL)), +}) + PAGINATED_LIST = api.model( 'List', { 'current_page': fields.Integer, diff --git a/services/core-web/src/components/admin/complianceCodes/ComplianceCodeManagement.tsx b/services/core-web/src/components/admin/complianceCodes/ComplianceCodeManagement.tsx index 03cc794ca4..113481c9e9 100644 --- a/services/core-web/src/components/admin/complianceCodes/ComplianceCodeManagement.tsx +++ b/services/core-web/src/components/admin/complianceCodes/ComplianceCodeManagement.tsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from "react-redux"; import { change, Field, initialize, reset } from "redux-form"; import SearchOutlined from "@ant-design/icons/SearchOutlined"; import PlusOutlined from "@ant-design/icons/PlusOutlined"; -import { Button, Input, Row, Table, Typography } from "antd"; +import { Button, Input, Row, Table, Tag, Typography } from "antd"; import CoreTable from "@mds/common/components/common/CoreTable"; import { @@ -28,6 +28,9 @@ import { } from "@mds/common/redux/slices/complianceCodesSlice"; import AuthorizationGuard from "@/HOC/AuthorizationGuard"; import * as Permission from "@/constants/permissions"; +import { faLink } from "@fortawesome/pro-light-svg-icons"; +import { EMPTY_FIELD } from "@mds/common"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const ComplianceCodeManagement: FC = () => { const dispatch = useDispatch(); @@ -214,6 +217,23 @@ const ComplianceCodeManagement: FC = () => { ); }, }, + { + title: "Description", + dataIndex: "description", + key: "description", + render: (text, record) => { + return ( + + {text ?? EMPTY_FIELD}{" "} + {!!record.reports?.length && ( + + Report + + )} + + ); + }, + }, renderTextColumn("description", "Description"), { ...renderDateColumn("effective_date", "Date Active"), width: 150 }, { diff --git a/services/core-web/src/components/admin/complianceCodes/ComplianceCodeViewEditForm.tsx b/services/core-web/src/components/admin/complianceCodes/ComplianceCodeViewEditForm.tsx index 6dec01bfe1..3df50cac60 100644 --- a/services/core-web/src/components/admin/complianceCodes/ComplianceCodeViewEditForm.tsx +++ b/services/core-web/src/components/admin/complianceCodes/ComplianceCodeViewEditForm.tsx @@ -1,51 +1,31 @@ -import React, { FC, useEffect } from "react"; +import React, { FC, useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { Field, getFormValues, change, touch } from "redux-form"; -import { Row, Col, Button, Typography } from "antd"; +import { getFormValues, change, touch } from "redux-form"; +import { Row, Button, Steps } from "antd"; import * as FORM from "@/constants/forms"; import FormWrapper from "@mds/common/components/forms/FormWrapper"; -import { - IComplianceArticle, - REPORT_REGULATORY_AUTHORITY_CODES, - REPORT_REGULATORY_AUTHORITY_ENUM, -} from "@mds/common"; -import { - required, - maxLength, - digitCharactersOnly, - requiredRadioButton, - protocol, -} from "@mds/common/redux/utils/Validate"; -import RenderField from "@mds/common/components/forms/RenderField"; -import RenderDate from "@mds/common/components/forms/RenderDate"; -import RenderRadioButtons from "@mds/common/components/forms/RenderRadioButtons"; -import RenderAutoSizeField from "@mds/common/components/forms/RenderAutoSizeField"; +import { IComplianceArticle, REPORT_REGULATORY_AUTHORITY_CODES } from "@mds/common"; import RenderCancelButton from "@mds/common/components/forms/RenderCancelButton"; import RenderSubmitButton from "@mds/common/components/forms/RenderSubmitButton"; -import { - formatComplianceCodeArticleNumber, - stripParentheses, -} from "@mds/common/redux/utils/helpers"; import { createComplianceCode, formatCode, getActiveComplianceCodesList, } from "@mds/common/redux/slices/complianceCodesSlice"; import { closeModal } from "@mds/common/redux/actions/modalActions"; +import { HSRCEditForm } from "./HSRCEditForm"; +import { ReportEditForm } from "./ReportEditForm"; const ComplianceCodeViewEditForm: FC<{ initialValues: IComplianceArticle; isEditMode: boolean; onSave: (values: IComplianceArticle) => void | Promise; }> = ({ initialValues = {}, isEditMode = true, onSave = null }) => { + const [currentStep, setCurrentStep] = useState(0); const dispatch = useDispatch(); const complianceCodes = useSelector(getActiveComplianceCodesList); const formValues = useSelector(getFormValues(FORM.ADD_COMPLIANCE_CODE)) ?? {}; const { section, sub_section, paragraph, sub_paragraph } = formValues; - const uniqueArticleNumbers = complianceCodes.map((code) => { - const articleNumber = formatComplianceCodeArticleNumber(code); - return stripParentheses(articleNumber); - }); const generateArticleNumber = () => { const articleNumber = [section, sub_section, paragraph, sub_paragraph] @@ -55,10 +35,12 @@ const ComplianceCodeViewEditForm: FC<{ dispatch(touch(FORM.ADD_COMPLIANCE_CODE, "articleNumber")); }; - const validateUniqueArticleNumber = (value) => { - return value && uniqueArticleNumbers.includes(stripParentheses(value)) - ? "Must select a unique article number" - : undefined; + const next = () => { + setCurrentStep(currentStep + 1); + }; + + const previous = () => { + setCurrentStep(currentStep - 1); }; useEffect(() => { @@ -67,18 +49,35 @@ const ComplianceCodeViewEditForm: FC<{ } }, [section, sub_section, paragraph, sub_paragraph]); + const steps = [ + { + title: "HSRC Details", + content: , + }, + { + title: "Report Details", + content: ( + + ), + }, + ]; + const handleSubmit = async (values: IComplianceArticle) => { - const cim_or_cpo = - values.cim_or_cpo !== REPORT_REGULATORY_AUTHORITY_CODES.NONE ? values.cim_or_cpo : null; - const payload = { ...values, article_act_code: "HSRCM", cim_or_cpo }; - dispatch(createComplianceCode(payload)).then((resp) => { - if (resp.payload) { - if (onSave) { - onSave(formatCode(resp.payload)); + if (currentStep === steps.length - 1) { + const cim_or_cpo = + values.cim_or_cpo !== REPORT_REGULATORY_AUTHORITY_CODES.NONE ? values.cim_or_cpo : null; + const payload = { ...values, article_act_code: "HSRCM", cim_or_cpo }; + dispatch(createComplianceCode(payload)).then((resp) => { + if (resp.payload) { + if (onSave) { + onSave(formatCode(resp.payload)); + } + dispatch(closeModal()); } - dispatch(closeModal()); - } - }); + }); + } else { + next(); + } }; return ( @@ -90,139 +89,37 @@ const ComplianceCodeViewEditForm: FC<{ isEditMode={isEditMode} isModal={true} > - - - HSRC Details - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Regulatory Authority - - - - - - - - - - - + + + {steps.map((step) => ( + + ))} + + + {steps[currentStep].content} + + + {currentStep > 0 && ( + + Back + + )} + + {isEditMode ? ( + + ) : ( + "" + )} + {!isEditMode && currentStep < steps.length - 1 ? ( + + Continue + + ) : ( + "" + )} + diff --git a/services/core-web/src/components/admin/complianceCodes/HSRCEditForm.tsx b/services/core-web/src/components/admin/complianceCodes/HSRCEditForm.tsx new file mode 100644 index 0000000000..1290eeb048 --- /dev/null +++ b/services/core-web/src/components/admin/complianceCodes/HSRCEditForm.tsx @@ -0,0 +1,175 @@ +import React from "react"; +import { Field } from "redux-form"; +import { Row, Col, Typography } from "antd"; +import { + IComplianceArticle, + REPORT_REGULATORY_AUTHORITY_CODES, + REPORT_REGULATORY_AUTHORITY_ENUM, +} from "@mds/common"; +import { + required, + maxLength, + digitCharactersOnly, + requiredRadioButton, + protocol, +} from "@mds/common/redux/utils/Validate"; +import RenderField from "@mds/common/components/forms/RenderField"; +import RenderDate from "@mds/common/components/forms/RenderDate"; +import RenderRadioButtons from "@mds/common/components/forms/RenderRadioButtons"; +import RenderAutoSizeField from "@mds/common/components/forms/RenderAutoSizeField"; +import { + formatComplianceCodeArticleNumber, + stripParentheses, +} from "@mds/common/redux/utils/helpers"; + +export interface HSRCEditFormProps { + complianceCodes: IComplianceArticle[]; +} + +export const HSRCEditForm = (props: HSRCEditFormProps) => { + const uniqueArticleNumbers = props.complianceCodes.map((code) => { + const articleNumber = formatComplianceCodeArticleNumber(code); + return stripParentheses(articleNumber); + }); + + const validateUniqueArticleNumber = (value) => { + return value && uniqueArticleNumbers.includes(stripParentheses(value)) + ? "Must select a unique article number" + : undefined; + }; + + return ( + <> + + + HSRC Details + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Regulatory Authority + + + + + + + + + > + ); +}; diff --git a/services/core-web/src/components/admin/complianceCodes/ReportEditForm.tsx b/services/core-web/src/components/admin/complianceCodes/ReportEditForm.tsx new file mode 100644 index 0000000000..992d3dc118 --- /dev/null +++ b/services/core-web/src/components/admin/complianceCodes/ReportEditForm.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import { FieldArray, getFormValues } from "redux-form"; +import { useSelector } from "react-redux"; +import { Row, Col, Typography, Button, Collapse, Popconfirm } from "antd"; +import { IComplianceArticle } from "@mds/common"; +import { ReportDefinitionFieldSelect } from "@mds/common/components/reports/ReportDefinitionFieldSelect"; +import { ReportInfoBox } from "@mds/common/components/reports/ReportGetStarted"; +import * as FORM from "@/constants/forms"; +import { TRASHCAN } from "@/constants/assets"; +import { getMineReportDefinitionOptions } from "@mds/common/redux/selectors/staticContentSelectors"; + +export interface ReportEditProps { + complianceCodes: IComplianceArticle[]; + isEditMode: boolean; +} + +export const ReportEditForm = (props: ReportEditProps) => { + const formValues = useSelector(getFormValues(FORM.ADD_COMPLIANCE_CODE)); + const mineReportDefinitionOptions = useSelector(getMineReportDefinitionOptions); + + const renderPanelHeader = (fields, index, isEditMode) => { + return ( + + + Code Required Report + + + {isEditMode ? ( + event.stopPropagation()}> + fields.remove(index)} + okText="Yes" + cancelText="No" + > + + + + + + ) : ( + "" + )} + + ); + }; + + const renderReports = ({ fields }) => ( + + {fields.map((report, index) => { + const reportData = formValues.reports && formValues.reports[index]; + const reportDefinition = + mineReportDefinitionOptions?.find( + (d) => d?.mine_report_definition_guid === reportData.mine_report_definition_guid + ) || {}; + const reportWCompliance = { + ...reportDefinition, + compliance_articles: [formValues], + }; + + return ( + + + + + <> + + > + + + {reportData?.mine_report_definition_guid ? ( + + ) : ( + "" + )} + + + + + ); + })} + + {props.isEditMode ? ( + + fields.unshift({})}> + + Add Report + + + ) : ( + "" + )} + + + ); + + return ( + + + + Add Report + + + Select the Code Required Report(s) corresponding to the code. Once saved, they will be + accessible to MineSpace users for Selection in Incidents, Variances, Reports. + + + + + + + ); +}; diff --git a/services/core-web/src/styles/components/Reports.scss b/services/core-web/src/styles/components/Reports.scss index bfea99518b..e3c9201155 100644 --- a/services/core-web/src/styles/components/Reports.scss +++ b/services/core-web/src/styles/components/Reports.scss @@ -60,6 +60,8 @@ padding: 16px; border: 1px solid $light-violet; + background: #fff; + h5 { text-transform: none; font-weight: bold; diff --git a/services/core-web/src/styles/components/Tags.scss b/services/core-web/src/styles/components/Tags.scss index d4901255dc..ef18524718 100644 --- a/services/core-web/src/styles/components/Tags.scss +++ b/services/core-web/src/styles/components/Tags.scss @@ -19,4 +19,9 @@ color: $violet; border-color: $violet; background-color: $lightest-violet; +} + +.tag-secondary { + color: $white; + background-color: $violet; } \ No newline at end of file diff --git a/services/core-web/src/tests/components/admin/__snapshots__/ComplianceCodeManagement.spec.tsx.snap b/services/core-web/src/tests/components/admin/__snapshots__/ComplianceCodeManagement.spec.tsx.snap index dc924a0f89..fd01b6abf1 100644 --- a/services/core-web/src/tests/components/admin/__snapshots__/ComplianceCodeManagement.spec.tsx.snap +++ b/services/core-web/src/tests/components/admin/__snapshots__/ComplianceCodeManagement.spec.tsx.snap @@ -278,6 +278,11 @@ exports[`PermitConditionsNavigation renders properly 1`] = ` > Description + + Description + @@ -307,6 +312,7 @@ exports[`PermitConditionsNavigation renders properly 1`] = ` style="width: 150px;" /> + @@ -367,6 +373,15 @@ exports[`PermitConditionsNavigation renders properly 1`] = ` + + + + + 2.3.7 + + + Spills + + + @@ -452,6 +477,16 @@ exports[`PermitConditionsNavigation renders properly 1`] = ` > 2.3.8 + + + Flammable Waste Storage + + + @@ -535,7 +570,7 @@ exports[`PermitConditionsNavigation renders properly 1`] = `