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

OH2-388 | Exam - implement multiple options procedure #679

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
5,311 changes: 2,667 additions & 2,644 deletions api/oh.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ describe("Add exam Activity specs", () => {
cy.byId("examtype").click();
cy.get('.MuiAutocomplete-popper li[data-option-index="0"]').click();
cy.byId("selectedType").last().click();
cy.get('.MuiPopover-paper li[data-value="1"]').last().click();
cy.get('.MuiPopover-paper li[data-value="3"]').last().click().click();
cy.byId("description").type("Children exam");
cy.byId("defaultResult").type("POSITIVE");
cy.dataCy("submit-form").click();
cy.dataCy("info-box").contains("Fail");
});
Expand Down
32 changes: 25 additions & 7 deletions src/components/accessories/admin/exams/editExam/EditExam.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
import { useAppDispatch, useAppSelector } from "libraries/hooks/redux";
import React from "react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Navigate, useLocation, useParams } from "react-router";
import { PATHS } from "../../../../../consts";
import { ExamDTO } from "../../../../../generated";
import { updateExam } from "../../../../../state/exams";
import { getInitialFields } from "../examForm/consts";
import { getExamRows, updateExam } from "../../../../../state/exams";
import ExamForm from "../examForm/ExamForm";
import { getInitialFields } from "../examForm/consts";

export const EditExam = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { state }: { state: ExamDTO | undefined } = useLocation();
const { id } = useParams();
const update = useAppSelector((state) => state.operations.update);
const update = useAppSelector((state) => state.exams.examUpdate);

const examRows: string[] | undefined = useAppSelector((state) =>
state.exams.examRowsByExamCode.data?.map((row) => row.description!)
);

useEffect(() => {
if (id) {
dispatch(getExamRows(id));
}
}, [dispatch, id]);

const handleSubmit = (examDTO: ExamDTO) => {
dispatch(updateExam({ code: examDTO.code!!, examDTO }));
const handleSubmit = ({
rows,
...examDTO
}: ExamDTO & { rows: string[] | undefined }) => {
dispatch(
updateExam({
code: examDTO.code!!,
examWithRowsDTO: { exam: examDTO, rows },
})
);
};

if (state?.code !== id) {
Expand All @@ -30,7 +48,7 @@ export const EditExam = () => {
isLoading={!!update.isLoading}
resetButtonLabel={t("common.cancel")}
submitButtonLabel={t("exam.updateExam")}
fields={getInitialFields(state)}
fields={getInitialFields(state, examRows)}
/>
);
};
162 changes: 135 additions & 27 deletions src/components/accessories/admin/exams/examForm/ExamForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import classnames from "classnames/dedupe";
import { useFormik } from "formik";
import { get, has } from "lodash";
import { get, has, isEmpty } from "lodash";
import React, {
ChangeEvent,
FC,
Fragment,
useCallback,
useEffect,
useMemo,
Expand All @@ -10,7 +13,7 @@ import React, {
} from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { number, object, string } from "yup";
import { array, number, object, string } from "yup";
import checkIcon from "../../../../../assets/check-icon.png";
import warningIcon from "../../../../../assets/warning-icon.png";
import { PATHS } from "../../../../../consts";
Expand All @@ -28,6 +31,8 @@ import TextField from "../../../textField/TextField";
import "./styles.scss";
import { IExamProps } from "./types";

import { AddCircle, Delete } from "@mui/icons-material";
import { IconButton, Radio, RadioGroup } from "@mui/material";
import { useAppDispatch, useAppSelector } from "libraries/hooks/redux";
import AutocompleteField from "../../../autocompleteField/AutocompleteField";
import InfoBox from "../../../infoBox/InfoBox";
Expand Down Expand Up @@ -60,6 +65,15 @@ const ExamForm: FC<IExamProps> = ({
[examTypeState.data]
);

const procedureOptions = useMemo(
() => [
{ value: "1", label: t("exam.procedures.1") },
{ value: "2", label: t("exam.procedures.2") },
{ value: "3", label: t("exam.procedures.3") },
],
[t]
);

const errorMessage = useMemo(
() =>
(creationMode
Expand All @@ -79,6 +93,19 @@ const ExamForm: FC<IExamProps> = ({
code: string().required(t("common.required")),
description: string().required(t("common.required")),
examtype: string().required(t("common.required")),
rows: array().of(string().required(t("common.required"))),
defaultResult: string().test({
name: "procedure-1",
exclusive: true,
test: function (value) {
return (
this.parent.procedure !== 1 ||
(!isEmpty(value) &&
(this.parent.rows as string[]).some((item) => item == value))
);
},
message: t("exam.invalidDefaultResult"),
}),
procedure: number()
.test({
name: "onetwothree",
Expand All @@ -93,6 +120,7 @@ const ExamForm: FC<IExamProps> = ({
initialValues,
validationSchema,
enableReinitialize: true,
validateOnChange: true,
onSubmit: (values) => {
const formattedValues = formatAllFieldValues(fields, values);
formattedValues.examtype = examTypeState.data?.find(
Expand All @@ -119,6 +147,20 @@ const ExamForm: FC<IExamProps> = ({
navigate(-1);
};

const addExamRow = useCallback(() => {
formik.setFieldValue("rows", [...formik.values.rows, ""]);
}, [formik]);

const removeExamRow = useCallback(
(index: number) => () => {
formik.setFieldValue(
"rows",
(formik.values.rows as string[]).toSpliced(index, 1)
);
},
[formik]
);

const onBlurCallback = useCallback(
(fieldName: string) =>
(e: React.FocusEvent<HTMLDivElement>, value: string) => {
Expand All @@ -128,6 +170,28 @@ const ExamForm: FC<IExamProps> = ({
[handleBlur, setFieldValue]
);

const handleProcedureChange = useCallback(
(value: string) => {
setFieldValue("procedure", value);
const rows = formik.values.rows as string[];
if (value === "3") {
setFieldValue("rows", []);
} else if (rows.length < 2) {
setFieldValue("rows", rows.length === 1 ? [rows[0], ""] : ["", ""]);
}
setFieldValue("defaultResult", "");
},
[handleBlur, setFieldValue]
);

const handleDefaultResultChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue("defaultResult", e.target.value);
formik.setFieldError("defaultResult", "");
},
[formik, dispatch]
);

const cleanUp = useCallback(() => {
if (creationMode) {
dispatch(createExamReset());
Expand Down Expand Up @@ -193,39 +257,83 @@ const ExamForm: FC<IExamProps> = ({
fieldName="selectedType"
fieldValue={formik.values.procedure}
label={t("exam.procedure")}
options={[
{
label: '1: a list of available "string" results',
value: "1",
},
{ label: '2: a list of all "boolean" results', value: "2" },
{
label:
"3: exact value (it will be typed in by the laboratorist)",
value: "3",
},
]}
options={procedureOptions}
errorText={getErrorText("procedure")}
isValid={isValid("procedure")}
onChange={(v) => formik.setFieldValue("procedure", v)}
onBlur={formik.handleBlur}
disabled={isLoading}
/>
</div>
<div className="examForm__item halfWidth">
<TextField
field={formik.getFieldProps("defaultResult")}
theme="regular"
label={t("exam.defaultResult")}
isValid={isValid("defaultResult")}
errorText={getErrorText("defaultResult")}
onChange={handleProcedureChange}
onBlur={formik.handleBlur}
type="text"
disabled={isLoading}
/>
</div>
{formik.values.procedure !== "2" && (
<div className="examForm__item halfWidth">
<TextField
field={formik.getFieldProps("defaultResult")}
theme="regular"
label={t("exam.defaultResult")}
isValid={isValid("defaultResult")}
errorText={getErrorText("defaultResult")}
onBlur={formik.handleBlur}
type="text"
disabled={isLoading || formik.values.procedure === "1"}
/>
</div>
)}
</div>

{["1", "2"].includes(formik.values.procedure) && (
<RadioGroup
value={formik.values.defaultResult}
onChange={handleDefaultResultChange}
className={classnames("exam-rows", {
"procedure-2": formik.values.procedure === "2",
})}
>
<span className="title">{t("exam.possibleValues")}</span>
{formik.values.procedure === "1" && (
<span className="trailing">{t("exam.defaultValue")}</span>
)}
{(formik.values.rows as string[]).map((row, index) => (
<Fragment key={index}>
<div className="col-start-1 examForm__item fullWidth">
<TextField
field={formik.getFieldProps(`rows.${index}`)}
theme="regular"
label={t("common.option")}
isValid={isValid(`rows.${index}`)}
errorText={getErrorText(`rows.${index}`)}
onBlur={formik.handleBlur}
type="text"
disabled={isLoading}
/>
</div>
{(formik.values.rows as string[]).length > 2 && (
<IconButton onClick={removeExamRow(index)}>
<Delete />
</IconButton>
)}
{formik.values.procedure === "1" && (
<Radio
checked={
!isEmpty(formik.values.defaultResult) &&
formik.values.defaultResult === row
}
value={row}
className="radio"
/>
)}
</Fragment>
))}
<IconButton
onClick={addExamRow}
className="add-row-icon"
data-cy="add-exam-row"
size="medium"
>
<AddCircle />
</IconButton>
</RadioGroup>
)}
<div className="examForm__buttonSet">
<div className="submit_button">
<Button
Expand Down
16 changes: 12 additions & 4 deletions src/components/accessories/admin/exams/examForm/consts.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { ExamDTO } from "../../../../../generated";
import { TFields } from "../../../../../libraries/formDataHandling/types";
import { ExamProps } from "../types";
import { ExamFormFieldName } from "../types";

export const getInitialFields: (
operation: ExamDTO | undefined
) => TFields<ExamProps> = (exam) => ({
exam: ExamDTO | undefined,
rows: string[] | undefined
) => TFields<ExamFormFieldName> = (exam, rows) => ({
code: { type: "text", value: exam?.code ?? "" },
examtype: { type: "text", value: exam?.examtype?.code ?? "" },
description: { type: "text", value: exam?.description ?? "" },
procedure: { type: "number", value: exam?.procedure?.toString() ?? "" },
procedure: { type: "number", value: exam?.procedure?.toString() ?? "3" },
defaultResult: { type: "text", value: exam?.defaultResult ?? "" },
lock: { type: "number", value: exam?.lock ?? "" },
rows: {
type: "text",
isArray: true,
value: JSON.stringify(
rows ?? ((exam?.procedure ?? 3) !== 3 ? ["", ""] : [])
),
},
});
61 changes: 61 additions & 0 deletions src/components/accessories/admin/exams/examForm/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,67 @@
justify-content: space-between;
}

.column {
display: flex;
flex-direction: column;
}

.exam-rows {
display: grid;
grid-template-columns: 1fr max-content max-content;
width: 70%;
align-items: center;
&.procedure-2 {
width: 50%;
}
@include susy-media($smartphone) {
width: 100%;
&.procedure-2 {
width: 100%;
}
}

.col-start-1 {
grid-column-start: 1;
}

.row {
display: flex;
gap: 4px;
}

.radio {
justify-self: center;
grid-column-start: 3;
}
.error {
color: red;
font-size: x-small;
align-self: flex-start;
justify-self: start;
grid-column-start: 1;
grid-column: span 3;
margin: 7px 0px;
padding: 0px 15px;
}
.title {
font-size: large;
margin: 7px 0px;
padding: 0px 15px;
}
.trailing {
margin: 7px 0px;
padding: 0px 15px;
grid-column-start: 3;
}
.add-row-icon {
margin-left: 16px;
width: 40px;
height: 40px;
grid-column-start: 1;
}
}

.examForm__item {
margin: 7px 0px;
padding: 0px 15px;
Expand Down
Loading