diff --git a/backend/api/services/model_year_report.py b/backend/api/services/model_year_report.py index 9036f57b0..70b7e7acd 100644 --- a/backend/api/services/model_year_report.py +++ b/backend/api/services/model_year_report.py @@ -21,6 +21,8 @@ def get_model_year_report_statuses(report): compliance_obligation_confirmed_by = None summary_status = 'UNSAVED' summary_confirmed_by = None + assessment_status = 'UNSAVED' + assessment_confirmed_by = None confirmations = ModelYearReportConfirmation.objects.filter( model_year_report_id=report.id, @@ -107,5 +109,9 @@ def get_model_year_report_statuses(report): 'report_summary': { 'status': summary_status, 'confirmed_by': summary_confirmed_by + }, + 'assessment': { + 'status': assessment_status, + 'confirmed_by': assessment_confirmed_by } } diff --git a/frontend/src/app/css/ComplianceReport.scss b/frontend/src/app/css/ComplianceReport.scss index 7d0177e85..4f2b14612 100644 --- a/frontend/src/app/css/ComplianceReport.scss +++ b/frontend/src/app/css/ComplianceReport.scss @@ -1,3 +1,50 @@ +#assessment-details { + .text-grey { + color: grey; + } + table { + width: 100%; + td { + padding: 0 1rem 0 1rem; + line-height: 35px; + } + tr { + margin-top: 1rem; + } + .subclass { + font-weight: bold; + background-color: $default-background-grey; + } + .small-column { + width: 15%; + } + .large-column { + color: $default-text-blue; + padding-left: 1rem; + border-right: 4px solid white; + } + } + .grey-border-area { + border: 1px solid $border-grey; + padding: 1rem 1rem; + } + .ql-container { + height: auto !important; + } + + .ql-editor { + min-height: 150px !important; + max-height: 300px; + overflow: hidden; + overflow-y: scroll; + overflow-x: scroll; + } + .comment-box { + border: 1px solid $border-grey; + width:100%; + padding: 0.5rem 0.5rem; + } +} #compliance-report-list { .btn-group a { color: $default-text-black; diff --git a/frontend/src/app/router.js b/frontend/src/app/router.js index 84c99e887..dec508048 100644 --- a/frontend/src/app/router.js +++ b/frontend/src/app/router.js @@ -32,6 +32,7 @@ import ComplianceCalculatorContainer from '../compliance/ComplianceCalculatorCon import ComplianceReportsContainer from '../compliance/ComplianceReportsContainer'; import ComplianceReportSummaryContainer from '../compliance/ComplianceReportSummaryContainer'; import ComplianceRatiosContainer from '../compliance/ComplianceRatiosContainer'; +import AssessmentContainer from '../compliance/AssessmentContainer'; import SupplierInformationContainer from '../compliance/SupplierInformationContainer'; import ComplianceObligationContainer from '../compliance/ComplianceObligationContainer'; import ConsumerSalesContainer from '../compliance/ConsumerSalesContainer'; @@ -131,6 +132,10 @@ class Router extends Component { + } + /> } diff --git a/frontend/src/compliance/AssessmentContainer.js b/frontend/src/compliance/AssessmentContainer.js new file mode 100644 index 000000000..d5935cf3c --- /dev/null +++ b/frontend/src/compliance/AssessmentContainer.js @@ -0,0 +1,232 @@ +import axios from 'axios'; +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useParams } from 'react-router-dom'; +import { withRouter } from 'react-router'; +import Loading from '../app/components/Loading'; +import CONFIG from '../app/config'; +import history from '../app/History'; +import ROUTES_COMPLIANCE from '../app/routes/Compliance'; +import ROUTES_VEHICLES from '../app/routes/Vehicles'; +import CustomPropTypes from '../app/utilities/props'; +import ComplianceReportTabs from './components/ComplianceReportTabs'; +import AssessmentDetailsPage from './components/AssessmentDetailsPage'; +import ROUTES_SIGNING_AUTHORITY_ASSERTIONS from '../app/routes/SigningAuthorityAssertions'; + +const qs = require('qs'); + +const AssessmentContainer = (props) => { + const { location, keycloak, user } = props; + const { id } = useParams(); + const [ratios, setRatios] = useState({}); + const [details, setDetails] = useState({}); + const [offsetNumbers, setOffsetNumbers] = useState({}); + const [modelYear, setModelYear] = useState(CONFIG.FEATURES.MODEL_YEAR_REPORT.DEFAULT_YEAR); + const [loading, setLoading] = useState(true); + const [makes, setMakes] = useState([]); + const [make, setMake] = useState(''); + const [pendingBalanceExist, setPendingBalanceExist] = useState(false); + const [creditActivityDetails, setCreditActivityDetails] = useState({}); + const [supplierClassInfo, setSupplierClassInfo] = useState({ ldvSales: 0, class: '' }); + const [statuses, setStatuses] = useState({ + assessment: { + status: 'UNSAVED', + confirmedBy: null, + }, + }); + const handleAddComment = () => { + console.log('add logic here!'); + }; + const refreshDetails = () => { + if (id) { + axios.all([ + axios.get(ROUTES_COMPLIANCE.REPORT_DETAILS.replace(/:id/g, id)), + axios.get(ROUTES_COMPLIANCE.RATIOS), + axios.get(ROUTES_COMPLIANCE.REPORT_COMPLIANCE_DETAILS_BY_ID.replace(':id', id)), + + ]) + .then(axios.spread((response, ratioResponse, creditActivityResponse) => { + let supplierClass; + if (response.data.supplierClass === 'L') { + supplierClass = 'Large'; + } else if (response.data.supplierClass === 'M') { + supplierClass = 'Medium'; + } else if (response.data.supplierClass === 'S') { + supplierClass = 'Small'; + } + const { + makes: modelYearReportMakes, + modelYearReportAddresses, + modelYearReportHistory, + organizationName, + validationStatus, + modelYear: reportModelYear, + confirmations, + statuses: reportStatuses, + ldvSales, + } = response.data; + + const filteredRatio = ratioResponse.data.filter((data) => data.modelYear === modelYear.toString())[0]; + setRatios(filteredRatio); + if (modelYearReportMakes) { + const currentMakes = modelYearReportMakes.map((each) => (each.make)); + setMakes(currentMakes); + } + setDetails({ + ldvSales, + class: supplierClass, + assessment: { + history: modelYearReportHistory, + validationStatus, + }, + organization: { + name: organizationName, + organizationAddress: modelYearReportAddresses, + }, + supplierInformation: { + history: modelYearReportHistory, + validationStatus, + }, + }); + // CREDIT ACTIVITY + const complianceResponseDetails = creditActivityResponse.data.complianceObligation; + const { complianceOffset } = creditActivityResponse.data; + const creditBalanceStart = {}; + const creditBalanceEnd = {}; + const provisionalBalance = []; + const pendingBalance = []; + const transfersIn = []; + const transfersOut = []; + const creditsIssuedSales = []; + const complianceOffsetNumbers = []; + if (complianceOffset) { + complianceOffset.forEach((item) => { + complianceOffsetNumbers.push({ + modelYear: item.modelYear.name, + A: parseFloat(item.creditAOffsetValue), + B: parseFloat(item.creditAOffsetValue), + }); + }); + setOffsetNumbers(complianceOffsetNumbers); + } + complianceResponseDetails.forEach((item) => { + if (item.category === 'creditBalanceStart') { + creditBalanceStart[item.modelYear.name] = { + A: item.creditAValue, + B: item.creditBValue, + }; + } + if (item.category === 'creditBalanceEnd') { + creditBalanceEnd[item.modelYear.name] = { + A: item.creditAValue, + B: item.creditBValue, + }; + } + if (item.category === 'transfersIn') { + transfersIn.push({ + modelYear: item.modelYear.name, + A: item.creditAValue, + B: item.creditBValue, + }); + } + if (item.category === 'transfersOut') { + transfersOut.push({ + modelYear: item.modelYear.name, + A: item.creditAValue, + B: item.creditBValue, + }); + } + if (item.category === 'creditsIssuedSales') { + creditsIssuedSales.push({ + modelYear: item.modelYear.name, + A: item.creditAValue, + B: item.creditBValue, + }); + } + if (item.category === 'pendingBalance') { + pendingBalance.push({ + modelYear: item.modelYear.name, + A: item.creditAValue, + B: item.creditBValue, + }); + } + }); + + // go through every year in end balance and push to provisional + Object.keys(creditBalanceEnd).forEach((item) => { + provisionalBalance[item] = { + A: creditBalanceEnd[item].A, + B: creditBalanceEnd[item].B, + }; + }); + + // go through every item in pending and add to total if year already there or create new + pendingBalance.forEach((item) => { + if (provisionalBalance[item.modelYear]) { + provisionalBalance[item.modelYear].A += item.A; + provisionalBalance[item.modelYear].B += item.B; + } else { + provisionalBalance[item.modelYear] = { + A: item.A, + B: item.B, + }; + } + }); + + setCreditActivityDetails({ + creditBalanceStart, + creditBalanceEnd, + pendingBalance, + provisionalBalance, + transactions: { + creditsIssuedSales, + transfersIn, + transfersOut, + }, + }); + setLoading(false); + })); + } + }; + + useEffect(() => { + refreshDetails(); + }, [keycloak.authenticated]); + if (loading) { + return ; + } + return ( + <> + + + + ); +}; + +AssessmentContainer.propTypes = { + keycloak: CustomPropTypes.keycloak.isRequired, + location: PropTypes.shape().isRequired, + user: CustomPropTypes.user.isRequired, +}; + +export default withRouter(AssessmentContainer); diff --git a/frontend/src/compliance/components/AssessmentDetailsPage.js b/frontend/src/compliance/components/AssessmentDetailsPage.js new file mode 100644 index 000000000..e78b38268 --- /dev/null +++ b/frontend/src/compliance/components/AssessmentDetailsPage.js @@ -0,0 +1,551 @@ +/* eslint-disable react/no-array-index-key */ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import ReactQuill from 'react-quill'; +import Button from '../../app/components/Button'; +import Loading from '../../app/components/Loading'; +import Modal from '../../app/components/Modal'; +import history from '../../app/History'; +import CustomPropTypes from '../../app/utilities/props'; +import ROUTES_COMPLIANCE from '../../app/routes/Compliance'; +import ComplianceObligationAmountsTable from './ComplianceObligationAmountsTable'; +import ComplianceReportAlert from './ComplianceReportAlert'; +import formatNumeric from '../../app/utilities/formatNumeric'; +import TableSection from './TableSection'; +import ComplianceObligationReductionOffsetTable from './ComplianceObligationReductionOffsetTable'; + +const AssessmentDetailsPage = (props) => { + const { + details, + loading, + make, + makes, + user, + modelYear, + statuses, + id, + handleAddComment, + handleCommentChange, + ratios, + creditActivityDetails, + offsetNumbers, + } = props; + + const { + creditBalanceStart, pendingBalance, transactions, provisionalBalance, + } = creditActivityDetails; + const { + creditsIssuedSales, transfersIn, transfersOut, + } = transactions; + const [showModal, setShowModal] = useState(false); + const disabledInputs = false; + + if (loading) { + return ; + } + const totalReduction = ((ratios.complianceRatio / 100) * details.ldvSales); + const classAReduction = formatNumeric( + ((ratios.zevClassA / 100) * details.ldvSales), + 2, + ); + const leftoverReduction = ((ratios.complianceRatio / 100) * details.ldvSales) + - ((ratios.zevClassA / 100) * details.ldvSales); + + const modal = ( + { setShowModal(false); }} + handleSubmit={() => { setShowModal(false); handleCancelConfirmation(); }} + modalClass="w-75" + showModal={showModal} + confirmClass="button primary" + > +
+

+ Do you want to edit this page? This action will allow you to make further changes to{' '} + this information, it will also query the database to retrieve any recent updates.{' '} + Your previous confirmation will be cleared. +

+
+
+ ); + + return ( +
+
+
+

{modelYear} Model Year Report

+
+
+
+
+
+ {details && details.supplierInformation && details.supplierInformation.history && ( + + )} +
+ +
+
+ + + +
+
+
+
+
+
+
+ {user.isGovernment && (statuses.assessment.status === 'SUBMITTED' || statuses.assessment.status === 'UNSAVED') && ( + + )} +

Notice of Assessment

+
+

{details.organization.name}

+
+
+
+

Service Address

+ {details.organization.organizationAddress + && details.organization.organizationAddress.map((address) => ( + address.addressType.addressType === 'Service' && ( +
+ {address.representativeName && ( +
{address.representativeName}
+ )} +
{address.addressLine1}
+
{address.city} {address.state} {address.country}
+
{address.postalCode}
+
+ ) + ))} +
+
+

Records Address

+ {details.organization.organizationAddress + && details.organization.organizationAddress.map((address) => ( + address.addressType.addressType === 'Records' && ( +
+ {address.representativeName && ( +
{address.representativeName}
+ )} +
{address.addressLine1}
+
{address.city} {address.state} {address.country}
+
{address.postalCode}
+
+ ) + ))} +
+
+
+

Light Duty Vehicle Makes:

+ {(makes.length > 0) && ( +
+
    + {makes.map((item, index) => ( +
    +
  • +
    {item}
    +
  • +
    + ))} +
+
+ )} +

Vehicle Supplier Class:

+

{details.class} Volume Supplier

+
+
+ +
+
+ + + + + + + + + + + + + +
+ BALANCE AT END OF SEPT. 30, {modelYear} + + A + + B +
+ + {creditBalanceStart.A} + + {creditBalanceStart.B} +
+
+

+ Credit Activity +

+
+ + + {Object.keys(creditsIssuedSales).length > 0 + && ( + + )} + {/* {Object.keys(creditsIssuedInitiative).length > 0 + && ( + + + )} + {Object.keys(creditsIssuedPurchase).length > 0 + && ( + + )} */} + {Object.keys(transfersIn).length > 0 + && ( + + + )} + {Object.keys(transfersOut).length > 0 + && ( + + )} + +
+
+
+ + + + + + + + {Object.keys(pendingBalance).length > 0 + && ( + + Object.keys(provisionalBalance).sort((a, b) => { + if (a.modelYear < b.modelYear) { + return 1; + } + if (a.modelYear > b.modelYear) { + return -1; + } + return 0; + }).map((each) => ( + + + + + + )) + )} + +
+ BALANCE BEFORE CREDIT REDUCTION +
+ •     {each} Credits + + {formatNumeric(provisionalBalance[each].A, 2)} + + {formatNumeric(provisionalBalance[each].B, 2)} +
+
+

+ Credit Reduction +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ZEV Class A Credit Reduction + + A + + B +
•     2019 Credits: + -567.43 + + 0 +
+ Unspecified ZEV Class Credit Reduction + + +
+ Do you want to use ZEV Class A or B credits first for your unspecified ZEV class reduction? + + + + +
•     2019 Credits: + 0 + + 147.86 +
+ {/* */} +
+
+ + + + + + + + + + + + + +
+ ASSESSED BALANCE AT END OF SEPT. 30, {modelYear + 1} + + A + + B +
•     2020 Credits: + 977.76 + + 0 +
+
+
+
+
+

Analyst Recommended Director Assessment

+
+
+
+
+ + { + console.log('radio checked'); + }} + name="complied" + /> + +
+
+ { + console.log('radio checked'); + }} + name="not-complied" + /> + +
+
+ + { + console.log('radio checked'); + }} + name="penalty-radio" + /> + + +
+ + + +
+ +
+
+
+
+ +
+
+
+ + {/*
+
+ {modal} +
+ +
+ ); +}; + +AssessmentDetailsPage.defaultProps = { +}; + +AssessmentDetailsPage.propTypes = { + details: PropTypes.shape({ + organization: PropTypes.shape(), + supplierInformation: PropTypes.shape(), + }).isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + loading: PropTypes.bool.isRequired, + make: PropTypes.string.isRequired, + makes: PropTypes.arrayOf(PropTypes.string).isRequired, + user: CustomPropTypes.user.isRequired, + modelYear: PropTypes.number.isRequired, + statuses: PropTypes.shape().isRequired, +}; +export default AssessmentDetailsPage; diff --git a/frontend/src/compliance/components/ComplianceObligationAmountsTable.js b/frontend/src/compliance/components/ComplianceObligationAmountsTable.js index a1b999a6f..14516dd2f 100644 --- a/frontend/src/compliance/components/ComplianceObligationAmountsTable.js +++ b/frontend/src/compliance/components/ComplianceObligationAmountsTable.js @@ -4,14 +4,13 @@ import formatNumeric from '../../app/utilities/formatNumeric'; const ComplianceObligationAmountsTable = (props) => { const { - reportYear, supplierClassInfo, totalReduction, ratios, classAReduction, leftoverReduction, + reportYear, supplierClassInfo, totalReduction, ratios, classAReduction, leftoverReduction, page, } = props; return (

Compliance Obligation

-
- {/*

{reportYear} Compliance Ratio Reduction and Credit Offset

*/} +
@@ -40,12 +39,12 @@ const ComplianceObligationAmountsTable = (props) => { Compliance Ratio Credit Reduction: )} - {supplierClassInfo.class === 'L' && ( + {(supplierClassInfo.class === 'L' || supplierClassInfo.class === 'Large') && ( <>
- {formatNumeric((totalReduction) , 2)} + {formatNumeric((totalReduction), 2)}
@@ -68,7 +67,7 @@ const ComplianceObligationAmountsTable = (props) => {
- {supplierClassInfo.class === 'L' && ( + {(supplierClassInfo.class === 'L' || supplierClassInfo.class === 'Large') && (
diff --git a/frontend/src/compliance/components/ComplianceReportAlert.js b/frontend/src/compliance/components/ComplianceReportAlert.js index 41538bd0b..443c278ed 100644 --- a/frontend/src/compliance/components/ComplianceReportAlert.js +++ b/frontend/src/compliance/components/ComplianceReportAlert.js @@ -76,7 +76,25 @@ const ComplianceReportAlert = (props) => { ); } - + if (type === 'Assessment') { + switch (status && status.status) { + case 'UNSAVED': + title = 'Submitted'; + message = ` Model year report signed and submitted ${date} by ${userName}. Pending analyst review and Director assessment.`; + classname = 'alert-warning'; + break; + case 'SUBMITTED': + title = 'Submitted'; + message = ` Model year report signed and submitted ${date} by ${userName}. Pending analyst review and Director assessment.`; + classname = 'alert-warning'; + break; + default: + title = ''; + } + return ( + + ); + } switch (status && status.status) { case 'UNSAVED': title = 'Model Year Report Draft'; diff --git a/frontend/src/compliance/components/ComplianceReportTabs.js b/frontend/src/compliance/components/ComplianceReportTabs.js index 6127f5514..03d0e8184 100644 --- a/frontend/src/compliance/components/ComplianceReportTabs.js +++ b/frontend/src/compliance/components/ComplianceReportTabs.js @@ -5,11 +5,12 @@ import { Link, useParams } from 'react-router-dom'; import ROUTES_COMPLIANCE from '../../app/routes/Compliance'; const ComplianceReportTabs = (props) => { - const { active, reportStatuses } = props; + const { active, reportStatuses, user } = props; const { id } = useParams(); const disableOtherTabs = reportStatuses.supplierInformation && reportStatuses.supplierInformation.status === 'UNSAVED'; - + const disableAssessment = (reportStatuses.reportSummary && reportStatuses.reportSummary.status !== 'SUBMITTED') + || (reportStatuses.assessment && reportStatuses.assessment.status === 'SUBMITTED' && !user.isGovernment); return (
    { } role="presentation" > - {disableOtherTabs && ( + {(disableOtherTabs || disableAssessment) && ( Assessment )} - {!disableOtherTabs && ( - Assessment + {!disableOtherTabs && !disableAssessment && ( + Assessment )}
diff --git a/frontend/src/compliance/components/TableSection.js b/frontend/src/compliance/components/TableSection.js new file mode 100644 index 000000000..e9eff6990 --- /dev/null +++ b/frontend/src/compliance/components/TableSection.js @@ -0,0 +1,46 @@ +import React from 'react'; +import formatNumeric from '../../app/utilities/formatNumeric'; + +const TableSection = (props) => { + const { input, title, negativeValue } = props; + let numberClassname = 'text-right'; + if (negativeValue) { + numberClassname += ' text-red'; + } + console.log(input) + return ( + <> +
+ + + + + + {input.sort((a, b) => { + if (a.modelYear < b.modelYear) { + return 1; + } + if (a.modelYear > b.modelYear) { + return -1; + } + return 0; + }).map((each) => ( + + + + + + ))} + + ); +}; + +export default TableSection;
+ {title} +
+ •     {each.modelYear} Credits + + {formatNumeric(each.A, 2)} + + {formatNumeric(each.B, 2)} +