Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type GroupSettings {
idValidationRegex String?
idValidationRegexErrorMessage ErrorMessage?
subjectIdDisplayLength Int?
minimumAge Int?
}

model Group {
Expand Down
23 changes: 17 additions & 6 deletions apps/web/src/components/StartSessionForm/StartSessionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ import { z } from 'zod/v4';

const currentDate = new Date();

const EIGHTEEN_YEARS = 568025136000; // milliseconds

const MIN_DATE_OF_BIRTH = new Date(currentDate.getTime() - EIGHTEEN_YEARS);

type StartSessionFormData = {
sessionDate: Date;
sessionType: 'IN_PERSON' | 'RETROSPECTIVE';
Expand Down Expand Up @@ -46,6 +42,10 @@ export const StartSessionForm = ({
onSubmit
}: StartSessionFormProps) => {
const { resolvedLanguage, t } = useTranslation();
const minDateOfBirth = currentGroup?.settings.minimumAge
? new Date(currentDate.getTime() - currentGroup.settings.minimumAge * 31556952000)
: undefined;

return (
<Form
preventResetValuesOnReset
Expand Down Expand Up @@ -177,8 +177,19 @@ export const StartSessionForm = ({
.optional(),
subjectDateOfBirth: z
.date()
.max(MIN_DATE_OF_BIRTH, { message: t('session.errors.mustBeAdult') })
.optional(),
.optional()
.refine(
(date) => {
if (!date || !minDateOfBirth) return true;
return date <= minDateOfBirth;
},
{
message: t({
en: `Subject must be above age of ${currentGroup?.settings.minimumAge}`,
fr: `Le sujet doit être âgé de plus de ${currentGroup?.settings.minimumAge}`
})
}
),
subjectSex: z.enum(['MALE', 'FEMALE']).optional(),
sessionType: $SessionType.exclude(['REMOTE']),
sessionDate: z
Expand Down
79 changes: 68 additions & 11 deletions apps/web/src/routes/_app/group/manage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ type ManageGroupFormProps = {
accessibleInteractiveInstrumentIds: Set<string>;
defaultIdentificationMethod?: SubjectIdentificationMethod;
idValidationRegex?: null | string;
subjectIdDisplayLength?: number;
minimumAge?: null | number;
minimumAgeApplied?: boolean | null;
subjectIdDisplayLength?: null | number;
};
};
onSubmit: (data: Partial<UpdateGroupData>) => Promisable<any>;
Expand Down Expand Up @@ -86,6 +88,40 @@ const ManageGroupForm = ({ data, onSubmit, readOnly }: ManageGroupFormProps) =>
fr: "Paramètres d'affichage"
})
},
{
fields: {
minimumAgeApplied: {
kind: 'boolean',
label: t({
en: 'Apply Minimum Age For Subjects',
fr: 'Appliquer un âge minimum aux sujets'
}),
variant: 'radio'
},
// eslint-disable-next-line perfectionist/sort-objects
minimumAge: {
deps: ['minimumAgeApplied'],
kind: 'dynamic',
render: (data) => {
if (data.minimumAgeApplied) {
return {
kind: 'number',
label: t({
en: 'Minimum Age',
fr: "L'âge minimum"
}),
variant: 'input'
};
}
return null;
}
}
},
title: t({
en: 'Age Limit Settings',
fr: "Paramètres de l'âge"
})
},
{
fields: {
defaultIdentificationMethod: {
Expand Down Expand Up @@ -150,15 +186,32 @@ const ManageGroupForm = ({ data, onSubmit, readOnly }: ManageGroupFormProps) =>
initialValues={initialValues}
preventResetValuesOnReset={true}
readOnly={readOnly}
validationSchema={z.object({
accessibleFormInstrumentIds: z.set(z.string()),
accessibleInteractiveInstrumentIds: z.set(z.string()),
defaultIdentificationMethod: $SubjectIdentificationMethod.optional(),
idValidationRegex: $RegexString.optional(),
idValidationRegexErrorMessageEn: z.string().optional(),
idValidationRegexErrorMessageFr: z.string().optional(),
subjectIdDisplayLength: z.number().int().min(1)
})}
validationSchema={z
.object({
accessibleFormInstrumentIds: z.set(z.string()),
accessibleInteractiveInstrumentIds: z.set(z.string()),
defaultIdentificationMethod: $SubjectIdentificationMethod.optional(),
idValidationRegex: $RegexString.optional(),
idValidationRegexErrorMessageEn: z.string().optional(),
idValidationRegexErrorMessageFr: z.string().optional(),
minimumAge: z.number().int().positive().optional(),
minimumAgeApplied: z.boolean().optional(),
subjectIdDisplayLength: z.number().int().min(1).optional()
})
.check((ctx) => {
if (ctx.value.minimumAgeApplied && !ctx.value.minimumAge) {
ctx.issues.push({
code: 'custom',
input: ctx.value.minimumAge,
message: t({
en: 'Please enter an age',
fr: "Entrez un âge s'il vous plait"
}),
path: ['minimumAge']
});
}
return;
})}
onSubmit={(data) => {
void onSubmit({
accessibleInstrumentIds: [...data.accessibleFormInstrumentIds, ...data.accessibleInteractiveInstrumentIds],
Expand All @@ -169,6 +222,7 @@ const ManageGroupForm = ({ data, onSubmit, readOnly }: ManageGroupFormProps) =>
en: data.idValidationRegexErrorMessageEn,
fr: data.idValidationRegexErrorMessageFr
},
minimumAge: data.minimumAgeApplied ? data.minimumAge : null,
subjectIdDisplayLength: data.subjectIdDisplayLength
}
});
Expand Down Expand Up @@ -207,7 +261,10 @@ const RouteComponent = () => {
defaultIdentificationMethod,
idValidationRegex: settings?.idValidationRegex,
idValidationRegexErrorMessageEn: settings?.idValidationRegexErrorMessage?.en,
idValidationRegexErrorMessageFr: settings?.idValidationRegexErrorMessage?.fr
idValidationRegexErrorMessageFr: settings?.idValidationRegexErrorMessage?.fr,
minimumAge: settings?.minimumAge,
minimumAgeApplied: typeof settings?.minimumAge === 'number',
subjectIdDisplayLength: settings?.subjectIdDisplayLength
};
for (const instrument of availableInstruments) {
if (instrument.kind === 'FORM') {
Expand Down
1 change: 1 addition & 0 deletions packages/schemas/src/group/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const $GroupSettings = z.object({
fr: z.string().nullish()
})
.nullish(),
minimumAge: z.number().int().positive().nullish(),
subjectIdDisplayLength: z.number().nullish()
});

Expand Down