From c98ac695df6ba891f9f58032c29746c4c788b729 Mon Sep 17 00:00:00 2001 From: Jose Francisco <94977371+icrc-jofrancisco@users.noreply.github.com> Date: Thu, 1 Feb 2024 19:37:58 +0000 Subject: [PATCH] (feat) Add support for pre-filled questions (#70) --- src/FormBootstrap.tsx | 11 ++- src/config-schema.ts | 20 ++++++ .../SessionDetailsForm.tsx | 54 +++++++++++++-- .../ConfigurableQuestionsSection.tsx | 47 +++++++++++++ src/hooks/index.ts | 9 ++- src/hooks/useForm.ts | 69 +++++++++++++++++++ src/types.ts | 20 ++++++ translations/am.json | 3 + translations/ar.json | 3 + translations/en.json | 3 + translations/es.json | 3 + translations/fr.json | 3 + translations/he.json | 3 + translations/km.json | 3 + 14 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 src/group-form-entry-workflow/configurable-questions/ConfigurableQuestionsSection.tsx create mode 100644 src/hooks/useForm.ts create mode 100644 src/types.ts diff --git a/src/FormBootstrap.tsx b/src/FormBootstrap.tsx index 93d5369..eedf776 100644 --- a/src/FormBootstrap.tsx +++ b/src/FormBootstrap.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { detach, ExtensionSlot } from "@openmrs/esm-framework"; import useGetPatient from "./hooks/useGetPatient"; +import GroupFormWorkflowContext from "./context/GroupFormWorkflowContext"; export interface Order { uuid: string; @@ -98,12 +99,18 @@ export interface Encounter { obs: Array; orders: Array; } + +type PreFilledQuestions = { + [key: string]: string; +}; + interface FormParams { formUuid: string; patientUuid: string; visitUuid?: string; visitTypeUuid?: string; encounterUuid?: string; + preFilledQuestions?: PreFilledQuestions; showDiscardSubmitButtons?: boolean; handlePostResponse?: (Encounter) => void; handleEncounterCreate?: (Object) => void; @@ -121,6 +128,7 @@ const FormBootstrap = ({ handleOnValidate, }: FormParams) => { const patient = useGetPatient(patientUuid); + const { activeSessionMeta } = useContext(GroupFormWorkflowContext); useEffect(() => { return () => detach("form-widget-slot", "form-widget-slot"); @@ -156,6 +164,7 @@ const FormBootstrap = ({ handleEncounterCreate, handleOnValidate, showDiscardSubmitButtons: false, + preFilledQuestions: { ...activeSessionMeta }, }} /> )} diff --git a/src/config-schema.ts b/src/config-schema.ts index b50f1b9..9ffbc38 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -82,6 +82,26 @@ export const configSchema = { _default: "fa8fedc0-c066-4da3-8dc1-2ad8621fc480", }, }, + specificQuestions: { + _type: Type.Array, + _description: "List of specific questions to populate forms.", + _elements: { + forms: { + _type: Type.Array, + _description: "List of form UUIDs for which the question applies.", + _elements: { + _type: Type.UUID, + }, + _default: [], + }, + questionId: { + _type: Type.String, + _description: "ID of the question.", + _default: "", + }, + }, + _default: [], + }, }; export type Form = { diff --git a/src/group-form-entry-workflow/SessionDetailsForm.tsx b/src/group-form-entry-workflow/SessionDetailsForm.tsx index 391042a..785ecfc 100644 --- a/src/group-form-entry-workflow/SessionDetailsForm.tsx +++ b/src/group-form-entry-workflow/SessionDetailsForm.tsx @@ -7,14 +7,26 @@ import { DatePickerInput, } from "@carbon/react"; import React, { useContext } from "react"; +import { useConfig } from "@openmrs/esm-framework"; +import { useParams } from "react-router-dom"; import styles from "./styles.scss"; import { useTranslation } from "react-i18next"; import { Controller, useFormContext } from "react-hook-form"; import { AttendanceTable } from "./attendance-table"; import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext"; import useGetPatients from "../hooks/useGetPatients"; +import ConfigurableQuestionsSection from "./configurable-questions/ConfigurableQuestionsSection"; +import useSpecificQuestions from "../hooks/useForm"; + +interface ParamTypes { + formUuid?: string; +} const SessionDetailsForm = () => { + const { specificQuestions } = useConfig(); + const { formUuid } = useParams() as ParamTypes; + const { questions } = useSpecificQuestions(formUuid, specificQuestions); + const { t } = useTranslation(); const { register, @@ -54,7 +66,7 @@ const SessionDetailsForm = () => { labelText={t("sessionName", "Session Name")} {...register("sessionName", { required: true })} invalid={errors.sessionName} - invalidText={"This field is required"} + invalidText={t("requiredField", "This field is required")} /> { labelText={t("practitionerName", "Practitioner Name")} {...register("practitionerName", { required: true })} invalid={errors.practitionerName} - invalidText={"This field is required"} + invalidText={t("requiredField", "This field is required")} /> { placeholder="mm/dd/yyyy" size="md" invalid={errors.sessionDate} - invalidText={"This field is required"} + invalidText={t( + "requiredField", + "This field is required" + )} /> )} @@ -92,7 +107,7 @@ const SessionDetailsForm = () => { labelText={t("sessionNotes", "Session Notes")} {...register("sessionNotes", { required: true })} invalid={errors.sessionNotes} - invalidText={"This field is required"} + invalidText={t("requiredField", "This field is required")} /> @@ -122,6 +137,37 @@ const SessionDetailsForm = () => { + {questions?.length > 0 ? ( + <> +

{t("sessionSpecificDetails", "3. Specific details")}

+
+

+ {t( + "sessionSpecificDetailsDescription", + "They will be mapped to form responses for all patients as pre-filled data." + )} +

+
+ + + +
+ +
+
+
+
+ + ) : null} )} diff --git a/src/group-form-entry-workflow/configurable-questions/ConfigurableQuestionsSection.tsx b/src/group-form-entry-workflow/configurable-questions/ConfigurableQuestionsSection.tsx new file mode 100644 index 0000000..d99ea0f --- /dev/null +++ b/src/group-form-entry-workflow/configurable-questions/ConfigurableQuestionsSection.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { TextInput, Select, SelectItem } from "@carbon/react"; +import { FieldValues, UseFormRegister } from "react-hook-form"; +import { SpecificQuestion } from "../../types"; + +interface ConfigurableQuestionsSectionProps { + specificQuestions: Array; + register?: UseFormRegister; +} + +const ConfigurableQuestionsSection: React.FC< + ConfigurableQuestionsSectionProps +> = ({ register, specificQuestions }) => { + return ( + <> + {specificQuestions?.map((specificQuestion) => ( +
+ {specificQuestion?.answers?.length > 0 ? ( + + ) : ( + + )} +
+ ))} + + ); +}; + +export default ConfigurableQuestionsSection; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 163e05f..199daaa 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,6 +2,13 @@ import useGetAllForms from "./useGetAllForms"; import useGetPatient from "./useGetPatient"; import useFormState from "./useFormState"; import useGetEncounter from "./useGetEncounter"; +import useForm from "./useForm"; -export { useGetAllForms, useGetPatient, useFormState, useGetEncounter }; +export { + useGetAllForms, + useGetPatient, + useFormState, + useGetEncounter, + useForm, +}; export * from "./usePostEndpoint"; diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts new file mode 100644 index 0000000..ee0a1d8 --- /dev/null +++ b/src/hooks/useForm.ts @@ -0,0 +1,69 @@ +import { type FetchResponse, openmrsFetch } from "@openmrs/esm-framework"; +import useSWR from "swr"; +import { SpecificQuestion, SpecificQuestionConfig } from "../types"; +import { useMemo } from "react"; + +const formUrl = "/ws/rest/v1/o3/forms"; + +export const useSpecificQuestions = ( + formUuid: string, + specificQuestionConfig: Array +) => { + const specificQuestionsToLoad = useMemo( + () => getQuestionIdsByFormId(formUuid, specificQuestionConfig), + [formUuid, specificQuestionConfig] + ); + + const { data, error } = useSWR( + specificQuestionsToLoad ? `${formUrl}/${formUuid}` : null, + openmrsFetch + ); + + const specificQuestions = getQuestionsByIds( + specificQuestionsToLoad, + data?.data + ); + + return { + questions: specificQuestions || null, + isError: error, + isLoading: !data && !error, + }; +}; + +function getQuestionIdsByFormId( + formUuid: string, + specificQuestionConfig: Array +) { + const matchingQuestions = specificQuestionConfig.filter((question) => + question.forms.includes(formUuid) + ); + return matchingQuestions.map((question) => question.questionId); +} + +function getQuestionsByIds(questionIds, formSchema): Array { + if (!formSchema || questionIds.lenght <= 0) { + return []; + } + const conceptLabels = formSchema.conceptReferences; + return formSchema.pages.flatMap((page) => + page.sections.flatMap((section) => + section.questions + .filter((question) => questionIds.includes(question.id)) + .map((question) => ({ + question: { + display: + question.label ?? + conceptLabels[question.questionOptions.concept]?.display, + id: question.id, + }, + answers: (question.questionOptions.answers ?? []).map((answer) => ({ + value: answer.concept, + display: answer.label ?? conceptLabels[answer.concept]?.display, + })), + })) + ) + ); +} + +export default useSpecificQuestions; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1f0f08a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,20 @@ +export interface Concept { + uuid: string; + display: string; +} + +export interface SpecificQuestion { + question: { + id: string; + display: string; + }; + answers: Array<{ + value: string; + display: string; + }>; +} + +export interface SpecificQuestionConfig { + forms: Array; + questionId: string; +} diff --git a/translations/am.json b/translations/am.json index 48fa82a..a5808b3 100644 --- a/translations/am.json +++ b/translations/am.json @@ -45,6 +45,7 @@ "postError": "POST Error", "practitionerName": "Practitioner Name", "remove": "Remove", + "requiredField": "This field is required", "resumeGroupSession": "Resume Group Session", "resumeSession": "Resume Session", "save": "Save", @@ -62,6 +63,8 @@ "sessionName": "Session Name", "sessionNotes": "Session Notes", "sessionParticipants": "2. Session participants", + "sessionSpecificDetails": "3. Specific details", + "sessionSpecificDetailsDescription": "They will be mapped to form responses for all patients as pre-filled data.", "startGroupSession": "Start Group Session", "trySearchWithPatientUniqueID": "Try searching with the cohort's description", "unknown": "Unknown", diff --git a/translations/ar.json b/translations/ar.json index ff0bd4d..60519f5 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -45,6 +45,7 @@ "postError": "خطأ POST", "practitionerName": "اسم الممارس", "remove": "إزالة", + "requiredField": "This field is required", "resumeGroupSession": "استئناف جلسة المجموعة", "resumeSession": "استئناف الجلسة", "save": "حفظ", @@ -62,6 +63,8 @@ "sessionName": "اسم الجلسة", "sessionNotes": "ملاحظات الجلسة", "sessionParticipants": "2. المشاركون في الجلسة", + "sessionSpecificDetails": "3. Specific details", + "sessionSpecificDetailsDescription": "They will be mapped to form responses for all patients as pre-filled data.", "startGroupSession": "بدء جلسة المجموعة", "trySearchWithPatientUniqueID": "حاول البحث باستخدام وصف الفوج", "unknown": "غير معروف", diff --git a/translations/en.json b/translations/en.json index 14adf0f..4b2b61e 100644 --- a/translations/en.json +++ b/translations/en.json @@ -45,6 +45,7 @@ "postError": "POST Error", "practitionerName": "Practitioner Name", "remove": "Remove", + "requiredField": "This field is required", "resumeGroupSession": "Resume Group Session", "resumeSession": "Resume Session", "save": "Save", @@ -62,6 +63,8 @@ "sessionName": "Session Name", "sessionNotes": "Session Notes", "sessionParticipants": "2. Session participants", + "sessionSpecificDetails": "3. Specific details", + "sessionSpecificDetailsDescription": "They will be mapped to form responses for all patients as pre-filled data.", "startGroupSession": "Start Group Session", "trySearchWithPatientUniqueID": "Try searching with the cohort's description", "unknown": "Unknown", diff --git a/translations/es.json b/translations/es.json index c1803e9..c362a19 100644 --- a/translations/es.json +++ b/translations/es.json @@ -45,6 +45,7 @@ "postError": "Error de POST", "practitionerName": "Nombre del Practicante", "remove": "Eliminar", + "requiredField": "Este campo es obligatorio", "resumeGroupSession": "Continuar Sesión de Grupo", "resumeSession": "Continuar Sesión", "save": "Guardar", @@ -62,6 +63,8 @@ "sessionName": "Nombre de la Sesión", "sessionNotes": "Notas de la Sesión", "sessionParticipants": "2. Participantes de la Sesión", + "sessionSpecificDetails": "3. Detalles específicos", + "sessionSpecificDetailsDescription": "Se asignarán a las respuestas de los formularios de todos los pacientes como datos precumplimentados.", "startGroupSession": "Iniciar Sesión de Grupo", "trySearchWithPatientUniqueID": "Intente buscar con la descripción del grupo", "unknown": "Desconocido", diff --git a/translations/fr.json b/translations/fr.json index b654fb6..80f26a8 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -45,6 +45,7 @@ "postError": "POST Error", "practitionerName": "Nom du praticien", "remove": "Enlever", + "requiredField": "Ce champ est obligatoire", "resumeGroupSession": "Reprendre la session de groupe", "resumeSession": "Reprendre la session", "save": "Save", @@ -62,6 +63,8 @@ "sessionName": "Nom de la session", "sessionNotes": "Notes de la session", "sessionParticipants": "2. Session participants", + "sessionSpecificDetails": "3. Détails spécifiques", + "sessionSpecificDetailsDescription": "Elles seront intégrées aux réponses des formulaires pour tous les patients en tant que données pré-remplies.", "startGroupSession": "Démarrer la session de groupe", "trySearchWithPatientUniqueID": "Essayez de rechercher la description de la cohorte", "unknown": "Unknown", diff --git a/translations/he.json b/translations/he.json index 48fa82a..a5808b3 100644 --- a/translations/he.json +++ b/translations/he.json @@ -45,6 +45,7 @@ "postError": "POST Error", "practitionerName": "Practitioner Name", "remove": "Remove", + "requiredField": "This field is required", "resumeGroupSession": "Resume Group Session", "resumeSession": "Resume Session", "save": "Save", @@ -62,6 +63,8 @@ "sessionName": "Session Name", "sessionNotes": "Session Notes", "sessionParticipants": "2. Session participants", + "sessionSpecificDetails": "3. Specific details", + "sessionSpecificDetailsDescription": "They will be mapped to form responses for all patients as pre-filled data.", "startGroupSession": "Start Group Session", "trySearchWithPatientUniqueID": "Try searching with the cohort's description", "unknown": "Unknown", diff --git a/translations/km.json b/translations/km.json index 48fa82a..a5808b3 100644 --- a/translations/km.json +++ b/translations/km.json @@ -45,6 +45,7 @@ "postError": "POST Error", "practitionerName": "Practitioner Name", "remove": "Remove", + "requiredField": "This field is required", "resumeGroupSession": "Resume Group Session", "resumeSession": "Resume Session", "save": "Save", @@ -62,6 +63,8 @@ "sessionName": "Session Name", "sessionNotes": "Session Notes", "sessionParticipants": "2. Session participants", + "sessionSpecificDetails": "3. Specific details", + "sessionSpecificDetailsDescription": "They will be mapped to form responses for all patients as pre-filled data.", "startGroupSession": "Start Group Session", "trySearchWithPatientUniqueID": "Try searching with the cohort's description", "unknown": "Unknown",