diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index ceea55bb5..1d265937e 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -99,6 +99,7 @@ type GroupSettings { idValidationRegex String? idValidationRegexErrorMessage ErrorMessage? subjectIdDisplayLength Int? + minimumAge Int? } model Group { diff --git a/apps/web/src/components/StartSessionForm/StartSessionForm.tsx b/apps/web/src/components/StartSessionForm/StartSessionForm.tsx index be271c85e..4b3879738 100644 --- a/apps/web/src/components/StartSessionForm/StartSessionForm.tsx +++ b/apps/web/src/components/StartSessionForm/StartSessionForm.tsx @@ -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'; @@ -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 (
{ + 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 diff --git a/apps/web/src/routes/_app/group/manage.tsx b/apps/web/src/routes/_app/group/manage.tsx index a5275c13a..9d0e75ecc 100644 --- a/apps/web/src/routes/_app/group/manage.tsx +++ b/apps/web/src/routes/_app/group/manage.tsx @@ -32,7 +32,9 @@ type ManageGroupFormProps = { accessibleInteractiveInstrumentIds: Set; defaultIdentificationMethod?: SubjectIdentificationMethod; idValidationRegex?: null | string; - subjectIdDisplayLength?: number; + minimumAge?: null | number; + minimumAgeApplied?: boolean | null; + subjectIdDisplayLength?: null | number; }; }; onSubmit: (data: Partial) => Promisable; @@ -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: { @@ -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], @@ -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 } }); @@ -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') { diff --git a/packages/schemas/src/group/group.ts b/packages/schemas/src/group/group.ts index e73cfd053..48f04c462 100644 --- a/packages/schemas/src/group/group.ts +++ b/packages/schemas/src/group/group.ts @@ -13,6 +13,7 @@ export const $GroupSettings = z.object({ fr: z.string().nullish() }) .nullish(), + minimumAge: z.number().int().positive().nullish(), subjectIdDisplayLength: z.number().nullish() });