src/applications/appeals/995/components/EvidencePrivateRecords.jsx
import React, { useState, useEffect } from 'react';
import { VaTextInput } from '@department-of-veterans-affairs/component-library/dist/react-bindings';
import { EVIDENCE_PRIVATE_PATH } from '../constants';
import { content } from '../content/evidencePrivateRecords';
import { getIndex, hasErrors } from '../utils/evidence';
import {
validatePrivateName,
validateCountry,
validateStreet,
validateCity,
validateState,
validatePostal,
validatePrivateIssues,
validatePrivateFromDate,
validatePrivateToDate,
validatePrivateUnique,
isEmptyPrivateEntry,
} from '../validations/evidence';
import { focusEvidence } from '../../shared/utils/focus';
import {
HeaderAndModal,
FacilityAddress,
IssueAndDates,
PageNavigation,
} from './EvidenceRecords';
import { getIssueName, getSelected } from '../../shared/utils/issues';
import { checkValidations } from '../../shared/validations';
import { customPageProps995 } from '../../shared/props';
const PRIVATE_PATH = `/${EVIDENCE_PRIVATE_PATH}`;
// const REVIEW_AND_SUBMIT = '/review-and-submit';
const defaultData = {
providerFacilityName: '',
issues: [],
providerFacilityAddress: {
country: 'USA',
street: '',
street2: '',
city: '',
state: '',
postalCode: '',
},
treatmentDateRange: { from: '', to: '' },
};
const defaultState = {
dirty: {
name: false,
country: false,
street: false,
city: false,
state: false,
postal: false,
issues: false,
from: false,
to: false,
},
showModal: false,
submitted: false,
};
const EvidencePrivateRecords = ({
data,
goBack,
goForward,
goToPath,
setFormData,
testingIndex,
contentBeforeButtons,
contentAfterButtons,
}) => {
const { providerFacility = [] } = data || {};
// *** state ***
// currentIndex is zero-based
const [currentIndex, setCurrentIndex] = useState(
getIndex(providerFacility, testingIndex),
);
const [currentData, setCurrentData] = useState(
providerFacility?.[currentIndex] || defaultData,
);
// force a useEffect call when currentIndex doesn't change
const [forceReload, setForceReload] = useState(false);
const [isBusy, setIsBusy] = useState(false);
const [currentState, setCurrentState] = useState(defaultState);
const availableIssues = getSelected(data).map(getIssueName);
const getPageType = entry => (isEmptyPrivateEntry(entry) ? 'add' : 'edit');
const [addOrEdit, setAddorEdit] = useState(getPageType(currentData));
// *** validations ***
const errors = {
unique: checkValidations(
[validatePrivateUnique],
currentData,
data,
currentIndex,
)[0],
name: checkValidations([validatePrivateName], currentData, data)[0],
country: checkValidations([validateCountry], currentData)[0],
street: checkValidations([validateStreet], currentData)[0],
city: checkValidations([validateCity], currentData)[0],
state: checkValidations([validateState], currentData)[0],
postal: checkValidations([validatePostal], currentData)[0],
issues: checkValidations(
[validatePrivateIssues],
currentData,
data,
currentIndex,
)[0],
from: checkValidations([validatePrivateFromDate], currentData),
to: checkValidations([validatePrivateToDate], currentData),
};
useEffect(
() => {
const entry = providerFacility?.[currentIndex] || defaultData;
setCurrentData(entry);
setAddorEdit(getPageType(entry));
setCurrentState(defaultState);
focusEvidence();
setForceReload(false);
setTimeout(() => setIsBusy(false));
},
// don't include providerFacility or we clear state & move focus every time
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentIndex, forceReload],
);
const updateCurrentFacility = ({
name = currentData.providerFacilityName,
country = currentData.providerFacilityAddress?.country,
street = currentData.providerFacilityAddress?.street,
street2 = currentData.providerFacilityAddress?.street2,
city = currentData.providerFacilityAddress?.city,
state = currentData.providerFacilityAddress?.state,
postal = currentData.providerFacilityAddress?.postalCode,
issues = currentData.issues,
from = currentData.treatmentDateRange?.from,
to = currentData.treatmentDateRange?.to,
remove = false,
} = {}) => {
const newData = {
providerFacilityName: name,
providerFacilityAddress: {
country,
street,
street2,
city,
state,
postalCode: postal,
},
issues,
treatmentDateRange: {
from,
to,
},
};
const newProviderFacility = [...providerFacility];
if (remove) {
newProviderFacility.splice(currentIndex, 1);
} else {
newProviderFacility[currentIndex] = newData;
}
setCurrentData(newData);
setFormData({ ...data, providerFacility: newProviderFacility });
return newProviderFacility;
};
const updateState = ({
dirty = currentState.dirty,
showModal = currentState.showModal,
submitted = currentState.submitted,
} = {}) => {
setCurrentState({ dirty, showModal, submitted });
};
const goToPageIndex = index => {
setCurrentIndex(index);
setForceReload(true);
goToPath(`${PRIVATE_PATH}?index=${index}`);
};
const addAndGoToPageIndex = index => {
const newProviderFacility = [...providerFacility];
if (!isEmptyPrivateEntry(providerFacility[index])) {
// only insert a new entry if the existing entry isn't empty
newProviderFacility.splice(index, 0, defaultData);
}
setFormData({ ...data, providerFacility: newProviderFacility });
goToPageIndex(index);
};
const handlers = {
onBlur: event => {
// we're switching pages, don't set a field to dirty otherwise the next
// page may set this and focus on an error without blurring a field
if (!isBusy) {
// event.detail from testing
const fieldName = event.target?.getAttribute('name') || event.detail;
updateState({ dirty: { ...currentState.dirty, [fieldName]: true } });
}
},
onChange: event => {
const { target = {} } = event;
const fieldName = target.name;
// detail.value from va-select &
// target.value from va-text-input & va-memorable-date
const value = event.detail?.value || target.value || '';
// empty va-memorable-date may return '--'
updateCurrentFacility({ [fieldName]: value });
},
onIssueChange: event => {
updateState({ dirty: { ...currentState.dirty, issues: true } });
const { target } = event;
// Clean up issues list
const newIssues = new Set(
(currentData?.issues || []).filter(issue =>
availableIssues.includes(issue),
),
);
// remove issues that aren't selected any more
if (target.checked) {
newIssues.add(target.label);
} else {
newIssues.delete(target.label);
}
updateCurrentFacility({ issues: [...newIssues] });
},
onAddAnother: event => {
event.preventDefault();
if (hasErrors(errors)) {
// don't show modal
updateState({ submitted: true });
focusEvidence();
return;
}
// clear state and insert a new entry after the current index (previously
// added new entry to the end). This change prevents the situation where
// an invalid entry in the middle of the array can get bypassed by adding
// a new entry
addAndGoToPageIndex(currentIndex + 1);
},
onGoForward: event => {
event.preventDefault();
updateState({ submitted: true });
// non-empty entry, focus on error
if (hasErrors(errors)) {
focusEvidence();
return;
}
setIsBusy(true);
const nextIndex = currentIndex + 1;
if (currentIndex < providerFacility.length - 1) {
goToPageIndex(nextIndex);
} else {
// passing data is needed, including nextIndex for unit testing
goForward(data, nextIndex);
}
},
onGoBack: () => {
// show modal if there are errors; don't show _immediately after_ adding
// a new empty entry
if (isEmptyPrivateEntry(currentData)) {
updateCurrentFacility({ remove: true });
} else if (hasErrors(errors)) {
// focus on first error
updateState({ submitted: true, showModal: true });
return;
}
setIsBusy(true);
const prevIndex = currentIndex - 1;
if (currentIndex > 0) {
goToPageIndex(prevIndex);
} else {
// index only passed here for testing purposes
goBack(prevIndex);
}
},
onModalClose: event => {
// For unit testing only
event.stopPropagation();
updateState({ submitted: true, showModal: false });
focusEvidence();
},
onModalYes: () => {
// Yes, keep providerFacility
updateState({ submitted: true, showModal: false });
const prevIndex = currentIndex - 1;
// index only passed here for testing purposes
if (prevIndex < 0) {
goBack(prevIndex);
} else {
goToPageIndex(prevIndex);
}
focusEvidence();
},
onModalNo: () => {
// No, clear current data and navigate
setCurrentData(defaultData);
updateCurrentFacility({ remove: true });
updateState({ submitted: true, showModal: false });
const prevIndex = currentIndex - 1;
if (prevIndex >= 0) {
goToPageIndex(prevIndex);
} else {
// index only passed here for testing purposes
goBack(prevIndex);
}
},
};
const showError = name =>
((currentState.submitted || currentState.dirty[name]) &&
(Array.isArray(errors[name]) ? errors[name][0] : errors[name])) ||
null;
const isInvalid = (name, part) => {
const message = errors[name]?.[1] || '';
return message.includes(part) || message.includes('other');
};
return (
<form onSubmit={handlers.onGoForward}>
<fieldset>
<HeaderAndModal
currentData={currentData}
currentState={currentState}
currentIndex={currentIndex}
addOrEdit={addOrEdit}
content={content}
handlers={handlers}
/>
<VaTextInput
id="add-facility-name"
name="name"
type="text"
label={content.nameLabel}
required
value={currentData.providerFacilityName}
onInput={handlers.onChange}
onBlur={handlers.onBlur}
// ignore submitted & dirty state when showing unique error
error={showError('name') || errors.unique || null}
autocomplete="section-provider name"
uswds
/>
<FacilityAddress
currentData={currentData}
content={content}
handlers={handlers}
showError={showError}
/>
<IssueAndDates
currentData={currentData}
availableIssues={availableIssues}
content={content}
handlers={handlers}
showError={showError}
isInvalid={isInvalid}
dateRangeKey="treatmentDateRange"
/>
<PageNavigation
path={`${PRIVATE_PATH}?index=${currentIndex + 1}`}
content={{
...content,
contentBeforeButtons,
contentAfterButtons,
}}
handlers={handlers}
/>
</fieldset>
</form>
);
};
EvidencePrivateRecords.propTypes = customPageProps995;
export default EvidencePrivateRecords;