Skip to content

Commit

Permalink
(feat) Add support for pre-filled questions (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
icrc-jofrancisco authored Feb 1, 2024
1 parent 25076e5 commit c98ac69
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 6 deletions.
11 changes: 10 additions & 1 deletion src/FormBootstrap.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -98,12 +99,18 @@ export interface Encounter {
obs: Array<Observation>;
orders: Array<Order>;
}

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;
Expand All @@ -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");
Expand Down Expand Up @@ -156,6 +164,7 @@ const FormBootstrap = ({
handleEncounterCreate,
handleOnValidate,
showDiscardSubmitButtons: false,
preFilledQuestions: { ...activeSessionMeta },
}}
/>
)}
Expand Down
20 changes: 20 additions & 0 deletions src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
54 changes: 50 additions & 4 deletions src/group-form-entry-workflow/SessionDetailsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,15 +66,15 @@ 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")}
/>
<TextInput
id="text"
type="text"
labelText={t("practitionerName", "Practitioner Name")}
{...register("practitionerName", { required: true })}
invalid={errors.practitionerName}
invalidText={"This field is required"}
invalidText={t("requiredField", "This field is required")}
/>
<Controller
name="sessionDate"
Expand All @@ -81,7 +93,10 @@ const SessionDetailsForm = () => {
placeholder="mm/dd/yyyy"
size="md"
invalid={errors.sessionDate}
invalidText={"This field is required"}
invalidText={t(
"requiredField",
"This field is required"
)}
/>
</DatePicker>
)}
Expand All @@ -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")}
/>
</div>
</Layer>
Expand Down Expand Up @@ -122,6 +137,37 @@ const SessionDetailsForm = () => {
</Layer>
</Tile>
</Layer>
{questions?.length > 0 ? (
<>
<h4>{t("sessionSpecificDetails", "3. Specific details")}</h4>
<div>
<p>
{t(
"sessionSpecificDetailsDescription",
"They will be mapped to form responses for all patients as pre-filled data."
)}
</p>
</div>
<Layer>
<Tile className={styles.formSectionTile}>
<Layer>
<div
style={{
display: "flex",
flexDirection: "column",
rowGap: "1.5rem",
}}
>
<ConfigurableQuestionsSection
register={register}
specificQuestions={questions}
/>
</div>
</Layer>
</Tile>
</Layer>
</>
) : null}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SpecificQuestion>;
register?: UseFormRegister<FieldValues>;
}

const ConfigurableQuestionsSection: React.FC<
ConfigurableQuestionsSectionProps
> = ({ register, specificQuestions }) => {
return (
<>
{specificQuestions?.map((specificQuestion) => (
<div key={specificQuestion.question.id}>
{specificQuestion?.answers?.length > 0 ? (
<Select
{...register(specificQuestion.question.id, { required: false })}
id={specificQuestion.question.id}
labelText={specificQuestion.question.display}
>
<SelectItem value="" text="" />
{specificQuestion.answers.map((answer) => (
<SelectItem
key={answer.value}
value={answer.value}
text={answer.display}
/>
))}
</Select>
) : (
<TextInput
id={specificQuestion.question.id}
{...register(specificQuestion.question.id, { required: false })}
type="text"
labelText={specificQuestion.question.display}
/>
)}
</div>
))}
</>
);
};

export default ConfigurableQuestionsSection;
9 changes: 8 additions & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
69 changes: 69 additions & 0 deletions src/hooks/useForm.ts
Original file line number Diff line number Diff line change
@@ -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<SpecificQuestionConfig>
) => {
const specificQuestionsToLoad = useMemo(
() => getQuestionIdsByFormId(formUuid, specificQuestionConfig),
[formUuid, specificQuestionConfig]
);

const { data, error } = useSWR<FetchResponse, Error>(
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<SpecificQuestionConfig>
) {
const matchingQuestions = specificQuestionConfig.filter((question) =>
question.forms.includes(formUuid)
);
return matchingQuestions.map((question) => question.questionId);
}

function getQuestionsByIds(questionIds, formSchema): Array<SpecificQuestion> {
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;
20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
questionId: string;
}
3 changes: 3 additions & 0 deletions translations/am.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions translations/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"postError": "خطأ POST",
"practitionerName": "اسم الممارس",
"remove": "إزالة",
"requiredField": "This field is required",
"resumeGroupSession": "استئناف جلسة المجموعة",
"resumeSession": "استئناف الجلسة",
"save": "حفظ",
Expand All @@ -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": "غير معروف",
Expand Down
3 changes: 3 additions & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Loading

0 comments on commit c98ac69

Please sign in to comment.