diff --git a/src/applications/financial-status-report/components/employment/EmploymentWorkDates.jsx b/src/applications/financial-status-report/components/employment/EmploymentWorkDates.jsx index a28c942f6829..6ef4e5e76678 100644 --- a/src/applications/financial-status-report/components/employment/EmploymentWorkDates.jsx +++ b/src/applications/financial-status-report/components/employment/EmploymentWorkDates.jsx @@ -2,16 +2,17 @@ import React, { useState } from 'react'; import { connect } from 'react-redux'; import { setData } from 'platform/forms-system/src/js/actions'; import { VaDate } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; -import { parseISODate } from 'platform/forms-system/src/js/helpers'; import PropTypes from 'prop-types'; -import ButtonGroup from '../shared/ButtonGroup'; + +import { parseISODate } from 'platform/forms-system/src/js/helpers'; +import { isValidStartDate, isValidEndDate } from '../../utils/helpers'; import { getJobIndex, getJobButton, jobButtonConstants, } from '../../utils/session'; import { BASE_EMPLOYMENT_RECORD } from '../../constants/index'; -import { isValidStartDate, isValidEndDate } from '../../utils/helpers'; +import ButtonGroup from '../shared/ButtonGroup'; const EmploymentWorkDates = props => { const { goToPath, setFormData, data } = props; @@ -19,9 +20,7 @@ const EmploymentWorkDates = props => { const RETURN_PATH = '/enhanced-employment-records'; const editIndex = getJobIndex(); - const isEditing = editIndex && !Number.isNaN(editIndex); - const index = isEditing ? Number(editIndex) : 0; const userType = 'veteran'; @@ -35,65 +34,75 @@ const EmploymentWorkDates = props => { }, } = data; - const [employmentRecord, setEmploymentRecord] = useState({ - ...(isEditing ? employmentRecords[index] : newRecord), - }); + const [employmentRecord, setEmploymentRecord] = useState( + isEditing ? employmentRecords[index] : newRecord, + ); const { employerName = '', from, to } = employmentRecord; + // Parse the 'from' and 'to' into { month, year } for VaDate const { month: fromMonth, year: fromYear } = parseISODate(from); const { month: toMonth, year: toYear } = parseISODate(to); - const fromError = 'Please enter a valid employment start date.'; - const toError = 'Please enter a valid employment end date.'; + const fromErrorMessage = 'Please enter a valid employment start date.'; + const toErrorMessage = 'Please enter a valid employment end date.'; - const [toDateError, setToDateError] = useState(null); const [fromDateError, setFromDateError] = useState(null); + const [toDateError, setToDateError] = useState(null); + + const handleChange = (key, value) => { + setEmploymentRecord(prev => ({ + ...prev, + [key]: value, + })); + }; const updateFormData = () => { + const { from: startDate, to: endDate, isCurrent } = employmentRecord; + + // If start date or end date is invalid, set errors and stop if ( - !isValidStartDate(employmentRecord.from) || - (!isValidEndDate(employmentRecord.from, employmentRecord.to) && - !employmentRecord.isCurrent) + !isValidStartDate(startDate) || + (!isCurrent && !isValidEndDate(startDate, endDate)) ) { + setFromDateError(!isValidStartDate(startDate) ? fromErrorMessage : null); setToDateError( - isValidEndDate(employmentRecord.from, employmentRecord.to) - ? null - : toError, - ); - setFromDateError( - isValidStartDate(employmentRecord.from) ? null : fromError, + !isCurrent && !isValidEndDate(startDate, endDate) + ? toErrorMessage + : null, ); return null; } + // Past this point, we know the date(s) are valid if (isEditing) { - // find the one we are editing in the employeeRecords array - const updatedRecords = employmentRecords.map((item, arrayIndex) => { - return arrayIndex === index ? employmentRecord : item; - }); - // update form data + const updatedRecords = employmentRecords.map( + (item, idx) => (idx === index ? employmentRecord : item), + ); + setFormData({ ...data, personalData: { ...data.personalData, employmentHistory: { ...data.personalData.employmentHistory, - [`${userType}`]: { - ...data.personalData.employmentHistory[`${userType}`], + [userType]: { + ...data.personalData.employmentHistory[userType], employmentRecords: updatedRecords, }, }, }, }); - if (employmentRecord.isCurrent) { - return goToPath(`/gross-monthly-income`); - } - return goToPath(`/employment-history`); + + // If current job, go to gross monthly income; else go back + return employmentRecord.isCurrent + ? goToPath(`/gross-monthly-income`) + : goToPath(`/employment-history`); } - // we are not editing a record, so we are adding a new one + // Otherwise, adding a brand new record if (employmentRecord.isCurrent) { + // Store new record for “current job” setFormData({ ...data, personalData: { @@ -107,6 +116,7 @@ const EmploymentWorkDates = props => { return goToPath(`/gross-monthly-income`); } + // Store record in array + reset newRecord setFormData({ ...data, personalData: { @@ -114,9 +124,9 @@ const EmploymentWorkDates = props => { employmentHistory: { ...data.personalData.employmentHistory, newRecord: { ...BASE_EMPLOYMENT_RECORD }, - [`${userType}`]: { - ...data.personalData.employmentHistory[`${userType}`], - employmentRecords: [{ ...employmentRecord }, ...employmentRecords], + [userType]: { + ...data.personalData.employmentHistory[userType], + employmentRecords: [employmentRecord, ...employmentRecords], }, }, }, @@ -124,90 +134,75 @@ const EmploymentWorkDates = props => { return goToPath(`/employment-history`); }; - const handleChange = (key, value) => { - setEmploymentRecord({ - ...employmentRecord, - [key]: value, - }); - }; - const handlers = { onCancel: event => { event.preventDefault(); goToPath(RETURN_PATH); }, - handleDateChange: (key, monthYear) => { - const dateString = `${monthYear}-XX`; + handleDateChange: (key, rawMonthYear) => { + const dateString = `${rawMonthYear}-01`; handleChange(key, dateString); - }, - handleBack: event => { - event.preventDefault(); - goToPath(RETURN_PATH); + if (key === 'from') { + if (!isValidStartDate(dateString)) { + setFromDateError(fromErrorMessage); + } else { + setFromDateError(null); + } + } + if (key === 'to') { + if ( + !isValidEndDate(employmentRecord.from, dateString) && + !employmentRecord.isCurrent + ) { + setToDateError(toErrorMessage); + } else { + setToDateError(null); + } + } }, onUpdate: event => { - // Handle validation in update event.preventDefault(); updateFormData(); }, getContinueButtonText: () => { - if ( - employmentRecord.isCurrent || - getJobButton() === jobButtonConstants.FIRST_JOB - ) { + const btn = getJobButton(); + if (employmentRecord.isCurrent || btn === jobButtonConstants.FIRST_JOB) { return 'Continue'; } - - if (getJobButton() === jobButtonConstants.EDIT_JOB) { + if (btn === jobButtonConstants.EDIT_JOB) { return 'Update employment record'; } return 'Add employment record'; }, }; - const ShowWorkDates = () => { - return ( -
+ /** + * Render the date fields. Note how we pass `onDateChange` to do immediate checks. + */ + const ShowWorkDates = () => ( +
+ handlers.handleDateChange('from', e.target.value)} + error={fromDateError} + /> + {!employmentRecord.isCurrent && ( { - setFromDateError(null); - handlers.handleDateChange('from', e.target.value); - }} - onBlur={() => - setFromDateError( - isValidStartDate(employmentRecord.from) ? null : fromError, - ) - } + value={`${toYear}-${toMonth}`} + label="Date you stopped work at this job?" + name="to" required - error={fromDateError} + onDateChange={e => handlers.handleDateChange('to', e.target.value)} + error={toDateError} /> - {!employmentRecord.isCurrent ? ( - { - setToDateError(null); - handlers.handleDateChange('to', e.target.value); - }} - onBlur={() => - setToDateError( - isValidEndDate(employmentRecord.from, employmentRecord.to) - ? null - : toError, - ) - } - required - error={toDateError} - /> - ) : null} -
- ); - }; + )} +
+ ); return (
@@ -215,7 +210,7 @@ const EmploymentWorkDates = props => { Your job at {employerName} -
{ShowWorkDates()}
+ {ShowWorkDates()} { ); }; -const mapStateToProps = ({ form }) => { - return { - formData: form.data, - employmentHistory: form.data.personalData.employmentHistory, - }; -}; - -const mapDispatchToProps = { - setFormData: setData, -}; - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(EmploymentWorkDates); - EmploymentWorkDates.propTypes = { data: PropTypes.shape({ personalData: PropTypes.shape({ @@ -283,3 +262,17 @@ EmploymentWorkDates.propTypes = { goToPath: PropTypes.func.isRequired, setFormData: PropTypes.func.isRequired, }; + +const mapStateToProps = ({ form }) => ({ + formData: form.data, + employmentHistory: form.data.personalData.employmentHistory, +}); + +const mapDispatchToProps = { + setFormData: setData, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(EmploymentWorkDates); diff --git a/src/applications/financial-status-report/components/employment/SpouseEmploymentWorkDates.jsx b/src/applications/financial-status-report/components/employment/SpouseEmploymentWorkDates.jsx index 510615e229df..38a215a4b4a7 100644 --- a/src/applications/financial-status-report/components/employment/SpouseEmploymentWorkDates.jsx +++ b/src/applications/financial-status-report/components/employment/SpouseEmploymentWorkDates.jsx @@ -4,6 +4,7 @@ import { setData } from 'platform/forms-system/src/js/actions'; import { VaDate } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { parseISODate } from 'platform/forms-system/src/js/helpers'; import PropTypes from 'prop-types'; + import ButtonGroup from '../shared/ButtonGroup'; import { getJobIndex, @@ -17,11 +18,8 @@ const SpouseEmploymentWorkDates = props => { const { goToPath, setFormData, data } = props; const RETURN_PATH = '/enhanced-spouse-employment-records'; - const editIndex = getJobIndex(); - const isEditing = editIndex && !Number.isNaN(editIndex); - const index = isEditing ? Number(editIndex) : 0; const userType = 'spouse'; @@ -35,65 +33,75 @@ const SpouseEmploymentWorkDates = props => { }, } = data; - const [employmentRecord, setEmploymentRecord] = useState({ - ...(isEditing ? spEmploymentRecords[index] : newRecord), - }); + const [employmentRecord, setEmploymentRecord] = useState( + isEditing ? spEmploymentRecords[index] : newRecord, + ); const { employerName = '', from, to } = employmentRecord; - const { month: fromMonth, year: fromYear } = parseISODate(from); const { month: toMonth, year: toYear } = parseISODate(to); - const fromError = "Please enter your spouse's employment start date."; - const toError = "Please enter your spouse's employment end date."; + const fromErrorMessage = "Please enter your spouse's employment start date."; + const toErrorMessage = "Please enter your spouse's employment end date."; - const [toDateError, setToDateError] = useState(null); const [fromDateError, setFromDateError] = useState(null); + const [toDateError, setToDateError] = useState(null); + /** + * Update local record state + */ + const handleChange = (key, value) => { + setEmploymentRecord(prev => ({ + ...prev, + [key]: value, + })); + }; + + /** + * Final check before submission + */ const updateFormData = () => { + const { from: startDate, to: endDate, isCurrent } = employmentRecord; + + // Check validity if ( - !isValidStartDate(employmentRecord.from) || - (!isValidEndDate(employmentRecord.from, employmentRecord.to) && - !employmentRecord.isCurrent) + !isValidStartDate(startDate) || + (!isCurrent && !isValidEndDate(startDate, endDate)) ) { + setFromDateError(!isValidStartDate(startDate) ? fromErrorMessage : null); setToDateError( - isValidEndDate(employmentRecord.from, employmentRecord.to) - ? null - : toError, - ); - setFromDateError( - isValidStartDate(employmentRecord.from) ? null : fromError, + !isCurrent && !isValidEndDate(startDate, endDate) + ? toErrorMessage + : null, ); return null; } + // If editing existing record if (isEditing) { - // find the one we are editing in the employeeRecords array - const updatedRecords = spEmploymentRecords.map((item, arrayIndex) => { - return arrayIndex === index ? employmentRecord : item; - }); - // update form data + const updatedRecords = spEmploymentRecords.map( + (item, idx) => (idx === index ? employmentRecord : item), + ); setFormData({ ...data, personalData: { ...data.personalData, employmentHistory: { ...data.personalData.employmentHistory, - [`${userType}`]: { - ...data.personalData.employmentHistory[`${userType}`], + [userType]: { + ...data.personalData.employmentHistory[userType], spEmploymentRecords: updatedRecords, }, }, }, }); - if (employmentRecord.isCurrent) { - return goToPath(`/spouse-gross-monthly-income`); - } - return goToPath(`/spouse-employment-history`); + return isCurrent + ? goToPath(`/spouse-gross-monthly-income`) + : goToPath(`/spouse-employment-history`); } - // we are not editing a record, so we are adding a new one - if (employmentRecord.isCurrent) { + // Otherwise, adding a new record + if (isCurrent) { setFormData({ ...data, personalData: { @@ -107,6 +115,7 @@ const SpouseEmploymentWorkDates = props => { return goToPath(`/spouse-gross-monthly-income`); } + // Not current job => push new record + reset newRecord setFormData({ ...data, personalData: { @@ -114,12 +123,9 @@ const SpouseEmploymentWorkDates = props => { employmentHistory: { ...data.personalData.employmentHistory, newRecord: { ...BASE_EMPLOYMENT_RECORD }, - [`${userType}`]: { - ...data.personalData.employmentHistory[`${userType}`], - spEmploymentRecords: [ - { ...employmentRecord }, - ...spEmploymentRecords, - ], + [userType]: { + ...data.personalData.employmentHistory[userType], + spEmploymentRecords: [employmentRecord, ...spEmploymentRecords], }, }, }, @@ -127,28 +133,37 @@ const SpouseEmploymentWorkDates = props => { return goToPath(`/spouse-employment-history`); }; - const handleChange = (key, value) => { - setEmploymentRecord({ - ...employmentRecord, - [key]: value, - }); - }; - const handlers = { onCancel: event => { event.preventDefault(); goToPath(RETURN_PATH); }, - handleDateChange: (key, monthYear) => { - const dateString = `${monthYear}-XX`; + + handleDateChange: (key, rawMonthYear) => { + const dateString = `${rawMonthYear}-01`; handleChange(key, dateString); - }, - handleBack: event => { - event.preventDefault(); - goToPath(RETURN_PATH); + + if (key === 'from') { + if (!isValidStartDate(dateString)) { + setFromDateError(fromErrorMessage); + } else { + setFromDateError(null); + } + } + + if (key === 'to') { + // Validate end date (if spouse is NOT currently employed) + if ( + !employmentRecord.isCurrent && + !isValidEndDate(employmentRecord.from, dateString) + ) { + setToDateError(toErrorMessage); + } else { + setToDateError(null); + } + } }, onUpdate: event => { - // Handle validation in update event.preventDefault(); updateFormData(); }, @@ -159,7 +174,6 @@ const SpouseEmploymentWorkDates = props => { ) { return 'Continue'; } - if (getJobButton() === jobButtonConstants.EDIT_JOB) { return 'Update employment record'; } @@ -167,50 +181,30 @@ const SpouseEmploymentWorkDates = props => { }, }; - const ShowWorkDates = () => { - return ( -
+ const ShowWorkDates = () => ( +
+ handlers.handleDateChange('from', e.target.value)} + error={fromDateError} + /> + {!employmentRecord.isCurrent && ( { - setFromDateError(null); - handlers.handleDateChange('from', e.target.value); - }} - onBlur={() => - setFromDateError( - isValidStartDate(employmentRecord.from) ? null : fromError, - ) - } + value={`${toYear}-${toMonth}`} + label="Date your spouse stopped work at this job?" + name="to" required - error={fromDateError} + onDateChange={e => handlers.handleDateChange('to', e.target.value)} + error={toDateError} /> - {!employmentRecord.isCurrent ? ( - { - setToDateError(null); - handlers.handleDateChange('to', e.target.value); - }} - onBlur={() => - setToDateError( - isValidEndDate(employmentRecord.from, employmentRecord.to) - ? null - : toError, - ) - } - required - error={toDateError} - /> - ) : null} -
- ); - }; + )} +
+ ); return ( @@ -218,7 +212,7 @@ const SpouseEmploymentWorkDates = props => { Your spouse’s job at {employerName} -
{ShowWorkDates()}
+ {ShowWorkDates()} { ); }; -const mapStateToProps = ({ form }) => { - return { - formData: form.data, - employmentHistory: form.data.personalData.employmentHistory, - }; -}; - -const mapDispatchToProps = { - setFormData: setData, -}; - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(SpouseEmploymentWorkDates); - SpouseEmploymentWorkDates.propTypes = { data: PropTypes.shape({ personalData: PropTypes.shape({ @@ -287,3 +265,17 @@ SpouseEmploymentWorkDates.propTypes = { goToPath: PropTypes.func.isRequired, setFormData: PropTypes.func.isRequired, }; + +const mapStateToProps = ({ form }) => ({ + formData: form.data, + employmentHistory: form.data.personalData.employmentHistory, +}); + +const mapDispatchToProps = { + setFormData: setData, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(SpouseEmploymentWorkDates); diff --git a/src/applications/financial-status-report/components/householdExpenses/InstallmentContractSummary.jsx b/src/applications/financial-status-report/components/householdExpenses/InstallmentContractSummary.jsx index 95ed88c41441..f6a74bf923f0 100644 --- a/src/applications/financial-status-report/components/householdExpenses/InstallmentContractSummary.jsx +++ b/src/applications/financial-status-report/components/householdExpenses/InstallmentContractSummary.jsx @@ -67,7 +67,7 @@ const InstallmentContractSummary = ({ const dateYear = dateReceived.split('-')[0]; const dateMonth = dateReceived.split('-')[1]; - return `${dateMonth}/XX/${dateYear}`; + return `${dateMonth}/${dateYear}`; }; const billBody = ({ diff --git a/src/applications/financial-status-report/tests/e2e/fsr-5655-complete.cypress.spec.js b/src/applications/financial-status-report/tests/e2e/fsr-5655-complete.cypress.spec.js index 1d9fecf93bce..0fce90ec5b17 100644 --- a/src/applications/financial-status-report/tests/e2e/fsr-5655-complete.cypress.spec.js +++ b/src/applications/financial-status-report/tests/e2e/fsr-5655-complete.cypress.spec.js @@ -628,7 +628,7 @@ const testConfig = createTestConfig( .and('contain', 'Original Loan Amount: $10,000.00') .and('contain', 'Unpaid balance: $1,000.00') .and('contain', 'Minimum monthly payment amount: $100.00') - .and('contain', 'Date received: 01/XX/2010') + .and('contain', 'Date received: 01/2010') .and('contain', 'Amount overdue: $10.00'); cy.get('.usa-button-primary').click(); }); diff --git a/src/applications/financial-status-report/utils/helpers.js b/src/applications/financial-status-report/utils/helpers.js index 43416e7f5935..5ffb782073ad 100644 --- a/src/applications/financial-status-report/utils/helpers.js +++ b/src/applications/financial-status-report/utils/helpers.js @@ -1,4 +1,4 @@ -import { addDays, format, isAfter, isFuture, isValid } from 'date-fns'; +import { parse, addDays, format, isAfter, isFuture, isValid } from 'date-fns'; import { toggleValues } from '~/platform/site-wide/feature-toggles/selectors'; import FEATURE_FLAG_NAMES from '~/platform/utilities/feature-toggles/featureFlagNames'; import { formatDateLong } from 'platform/utilities/date'; @@ -38,17 +38,27 @@ export const isNumber = value => { * @returns formatted date string 'MM/yyyy'; example: 01/2021 * */ -export const monthYearFormatter = date => { - // Slicing off '-XX' from date string - // replacing - with / since date-fns will be off by 1 month if we don't - const newDate = new Date(date?.slice(0, -3).replace(/-/g, '/')); - return isValid(newDate) ? format(newDate, 'MM/yyyy') : undefined; + +export const monthYearFormatter = dateString => { + if (!dateString) return ''; + + // Replace any '-XX' legacy markers with '-01' + const safeDate = dateString.replace(/-XX$/, '-01'); + let parsedDate; + if (safeDate.length === 7) { + parsedDate = parse(safeDate, 'yyyy-MM', new Date()); + } else { + parsedDate = parse(safeDate, 'yyyy-MM-dd', new Date()); + } + + // If parsed successfully, format as "MM/yyyy" + return isValid(parsedDate) ? format(parsedDate, 'MM/yyyy') : ''; }; export const endDate = (date, days) => { - return isValid(new Date(date)) - ? formatDateLong(addDays(new Date(date), days)) - : ''; + if (!date) return ''; + const parsed = new Date(date); + return isValid(parsed) ? formatDateLong(addDays(parsed, days)) : ''; }; export const currency = amount => {