Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

683 contact information #753

Merged
merged 10 commits into from
Sep 6, 2024
47 changes: 45 additions & 2 deletions app/src/components/intake-profile/IntakeProfileGroups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import {useOutletContext} from 'react-router-dom';
import {InitialValues} from 'src/views/IntakeProfile';
import {AdditionalGuestsField} from './fields/AdditionaGuestsField';
import {FieldGroup, Fields, Guest, Pet} from 'src/services/profile';

import {AdditionalPetsField} from './fields/AdditionalPetsField';
import {AdditionalPetsField} from './AdditionalPetsField';
import {phoneRegExp} from '../../views/IntakeProfile/constants/index';
import {DatePickerField} from './fields/DatePickerField';


export interface OutletContext {
groupId: string;
fieldGroups: FieldGroup[];
Expand Down Expand Up @@ -79,6 +80,7 @@ export const RenderFields = ({groupId, field}: RenderFieldProps) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const helperText = errors[groupId]?.[field.id];

switch (field.type) {
case 'additional_guests':
return (
Expand Down Expand Up @@ -175,6 +177,7 @@ export const RenderFields = ({groupId, field}: RenderFieldProps) => {
{...props}
error={error}
helperText={helperText}
type="tel"
id="outlined"
placeholder="(909)555-1234"
variant="outlined"
Expand Down Expand Up @@ -204,13 +207,53 @@ export const RenderFields = ({groupId, field}: RenderFieldProps) => {
variant="outlined"
placeholder="Type you answer here"
label={field.title}
type="email"
error={error}
helperText={helperText}
InputLabelProps={{
shrink: true,
}}
/>
);
case 'contact_method':
// eslint-disable-next-line no-case-declarations
const {emailFieldId, phoneFieldId} = field.linkedFields;
// eslint-disable-next-line no-case-declarations
let isEmailFilled = false;
// eslint-disable-next-line no-case-declarations
let isPhoneFilled = false;
if (emailFieldId) {
// This isn't best practice and can be replaced with validator library to verify email
isEmailFilled = Boolean(
values[emailFieldId] && /\S+@\S+\.\S+/.test(values[emailFieldId]),
);
}
// eslint-disable-next-line no-case-declarations
if (phoneFieldId) {
isPhoneFilled = phoneRegExp.test(values[phoneFieldId]);
}

return (
<FormControl error={error} required={field.validations.required}>
<FormLabel sx={{color: 'black'}}>{field.title}</FormLabel>
<RadioGroup {...props} aria-labelledby="contact-method-field">
<FormControlLabel
value="phone"
control={<Radio />}
label="Phone"
disabled={!isPhoneFilled}
/>
<FormControlLabel
value="email"
control={<Radio />}
label="Email"
disabled={!isEmailFilled}
/>
</RadioGroup>
<FormHelperText>{helperText}</FormHelperText>
</FormControl>
);

case 'yes_no':
return (
<FormControl error={error} required={field.validations.required}>
Expand Down
6 changes: 6 additions & 0 deletions app/src/services/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const fieldTypes = [
'yes_no',
'additional_guests',
'pets',
'contact_method',
] as const;

type FieldTypeTuple = typeof fieldTypes;
Expand Down Expand Up @@ -43,6 +44,10 @@ export interface Fields {
max_characters?: number;
required_if?: ReduiredIf;
};
linkedFields?: {
emailFieldId?: string;
phoneFieldId?: string;
};
}

export interface FieldGroup {
Expand All @@ -66,6 +71,7 @@ export interface Response {

export interface Pet {
type: string;
id: string;
}

const injectedRtkApi = api.injectEndpoints({
Expand Down
44 changes: 18 additions & 26 deletions app/src/utils/test/db/profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {faker} from '@faker-js/faker';
import {GetProfileApiResponse} from 'src/services/profile';

const emailFieldId = faker.string.numeric(4);
const phoneFieldId = faker.string.numeric(4);

export const intakeProfiles: GetProfileApiResponse[] = [
{
id: '1',
Expand Down Expand Up @@ -64,7 +67,7 @@ export const intakeProfiles: GetProfileApiResponse[] = [
title: 'Contact Information',
fields: [
{
id: faker.string.numeric(4),
id: emailFieldId,
title: 'Email',
type: 'email',
properties: {},
Expand All @@ -73,14 +76,27 @@ export const intakeProfiles: GetProfileApiResponse[] = [
},
},
{
id: faker.string.numeric(4),
id: phoneFieldId,
title: 'Phone Number',
type: 'number',
properties: {},
validations: {
required: true,
},
},
{
id: faker.string.numeric(4),
title: 'What is the best way to contact you?',
type: 'contact_method',
properties: {},
validations: {
required: true,
},
linkedFields: {
emailFieldId: emailFieldId,
phoneFieldId: phoneFieldId,
},
},
],
},
{
Expand Down Expand Up @@ -359,30 +375,6 @@ export const intakeProfiles: GetProfileApiResponse[] = [
},
],
},
{
id: faker.string.numeric(4),
title: 'Contact Information',
fields: [
{
id: faker.string.numeric(4),
title: 'Email',
type: 'email',
properties: {},
validations: {
required: true,
},
},
{
id: faker.string.numeric(4),
title: 'Phone Number',
type: 'number',
properties: {},
validations: {
required: true,
},
},
],
},
{
id: faker.string.numeric(4),
title: 'Home Information',
Expand Down
194 changes: 192 additions & 2 deletions app/src/views/IntakeProfile/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,192 @@
export {buildValidationSchema} from './buildValidationSchema';
export {createInitialValues} from './createInitialValues';

import {faker} from '@faker-js/faker';
import {array, object, string} from 'yup';
import {InitialValues} from '..';
import {
FieldGroup,
Fields,
fieldTypes,
FieldTypes,
Response,
} from '../../../services/profile';

export const fieldGroupBuilder = (
options: Partial<FieldGroup> = {},
): FieldGroup => ({
id: faker.string.numeric(4),
title: faker.lorem.sentence({min: 2, max: 4}),
fields: [],
...options,
});

export const fieldBuilder = (options: Partial<Fields> = {}): Fields => ({
id: faker.string.numeric(4),
title: faker.lorem.sentence({min: 5, max: 10}),
type: faker.helpers.arrayElement(fieldTypes),
...options,
properties: {
...options.properties,
},
validations: {
...options.validations,
},
});

/**
* Creates an object used for the initial Formik valiues
* It takes the form of:
* {
* fieldGroupId: {
* fieldId: responseValue
* }
* }
*/
const fieldDefaultValue = (fieldType: FieldTypes) => {
switch (fieldType) {
case 'short_text':
return '';
case 'long_text':
return '';
case 'dropdown':
return '';
case 'number':
return '';
case 'additional_guests':
return [];
case 'email':
return '';
case 'multiple_choice':
return '';
case 'yes_no':
return '';
case 'pets':
return [];
default:
return '';
}
};

export const createInitialValues = (
fieldGroups: FieldGroup[],
responses: Response[],
): InitialValues => {
return fieldGroups.reduce((acc: InitialValues, fieldGroup) => {
const fields = fieldGroup.fields.reduce((acc, field) => {
return {
...acc,
[field.id]:
responses.find(response => response.fieldId === field.id)?.value ||
fieldDefaultValue(field.type),
};
}, {});

return {
...acc,
[fieldGroup.id]: {...fields},
};
}, {});
};

/**
* Creates a validation schema for Formik based on field type
* It takes the form of:
* {
* fieldGroupId: {
* fieldId: validationSchema
* }
* }
*/

export const phoneRegExp =
/^((\\+[1-9]{1,4}[ \\-]*)|(\\([0-9]{2,3}\\)[ \\-]*)|([0-9]{2,4})[ \\-]*)*?[0-9]{3,4}?[ \\-]*[0-9]{3,4}?$/;

export const typeValidations = {
short_text: string(),
long_text: string(),
number: string().matches(phoneRegExp, 'Phone number is not valid'),
email: string().email('Must be a valid email address'),
yes_no: string(),
dropdown: string(),
multiple_choice: string(),
additional_guests: array().of(
object().shape({
name: string().required('Name is required'),
dob: string().required('DOB is required'),
relationship: string().required('Relationship is required'),
}),
),
// array of strings of the type of pets
pets: array().of(
object().shape({
type: string().required('Type of pet is required'),
}),
),
contact_method: string(),
};

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const merge = (...schemas) => {
const [first, ...rest] = schemas;

const merged = rest.reduce(
(mergedSchemas, schema) => mergedSchemas.concat(schema),
first,
);

return merged;
};

const createFieldValidationSchema = ({type, validations}: Fields) => {
if (!typeValidations[type]) {
console.error(
`No schema exists in typeValidations hashmap in IntakeProfile/constants/index.ts for type: ${type}`,
);
}

let schema = typeValidations[type];

if (validations.required) {
// only works for string types at the moment
schema = merge(schema, string().required('This field is required'));
}

if (validations.required_if) {
const {field_id, value} = validations.required_if;
// only works for string types at the moment
const requiedIfSchema = string().when(field_id, {
is: value,
then: schema => schema.required('This field is required'),
otherwise: schema => schema,
});
schema = merge(schema, requiedIfSchema);
}

return schema;
};

export const buildValidationSchema = (
fieldGroup: FieldGroup[],
groupId: string | undefined,
) => {
if (groupId === undefined) {
console.error('Invalid groupId');
return object().shape({});
}

const fields = fieldGroup.find(group => group.id === groupId)?.fields || [];

const schema = object().shape(
fields.reduce((acc, field) => {
return {
...acc,
[field.id]: createFieldValidationSchema(field),
};
}, {}),
);

return object().shape({
[groupId]: object().shape({...schema.fields}),
});
};

Loading