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-415 | OH2-316 | Types / Ages CRUD #689

Merged
merged 11 commits into from
Nov 13, 2024
209 changes: 108 additions & 101 deletions api/oh.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/// <reference types="cypress" />

const AGE_TYPE_START_PATH = "/admin/types/ages";

describe("Admission types Edit Activity specs", () => {
it("should render the ui", () => {
cy.authenticate(AGE_TYPE_START_PATH);
cy.dataCy("sub-activity-title").contains("Manage age types");
});

it("should show age types edit form", () => {
cy.dataCy("edit-age-types").click();
cy.dataCy("sub-activity-title").contains("Edit age types");
});

it("should fail to edit the age type", () => {
cy.byId("ageTypes\\[0\\]\\.to").type("1");
cy.dataCy("submit-form").click();
cy.dataCy("dialog-info").should("not.exist");
});

it("should successfully save age types changes", () => {
cy.byId("ageTypes\\[0\\]\\.to").clear().type("0");
cy.byId("ageTypes\\[5\\]\\.to").clear().type("104");
cy.dataCy("submit-form").click();
cy.dataCy("dialog-info").contains("have been updated successfully!");
cy.dataCy("approve-dialog").click();
});

it("should redirect after age types update", () => {
cy.dataCy("sub-activity-title").contains("Manage age types");
});

it("should cancel the cancellation of the age types update", () => {
cy.dataCy("edit-age-types").click();
cy.dataCy("cancel-form").click();
cy.dataCy("dialog-info").contains(
"Are you sure to cancel the age types update?"
);
cy.dataCy("close-dialog").click();
cy.dataCy("dialog-info").should("not.exist");
});

it("should cancel the age types update", () => {
cy.dataCy("cancel-form").click();
cy.dataCy("approve-dialog").click();
cy.dataCy("dialog-info").should("not.exist");
cy.dataCy("sub-activity-title").contains("Manage age types");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference types="cypress" />

const AGE_TYPES_START_PATH = "/admin/types/ages";

describe("Age types Activity specs", () => {
it("should render the ui", () => {
cy.authenticate(AGE_TYPES_START_PATH);
cy.dataCy("sub-activity-title").contains("Manage age types");
});

it("should present the table with 6 rows", () => {
cy.dataCy("age-types-table")
.find("table")
.then(($table) => {
const rows = $table.find("tbody tr");
expect(rows.length).equal(6);
});
});
});
9 changes: 5 additions & 4 deletions src/components/accessories/admin/types/TypesAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ const TypesAdmin = () => {
[
defaultTypeOption,
{ label: t("types.exams"), value: "exams" },
{ label: t("types.ages"), value: "ages" },
{ label: t("types.vaccines"), value: "vaccines" },
{ label: t("types.operations"), value: "operations" },
{ label: t("types.medicals"), value: "medicals" },
{ label: t("types.diseases"), value: "diseases" },
{ label: t("types.deliveries"), value: "deliveries" },
{ label: t("types.admissions"), value: "admissions" },
{ label: t("types.deliveryResultType"), value: "deliveryresulttypes" },
{ label: t("types.deliveries"), value: "deliveries" },
{ label: t("types.discharges"), value: "discharges" },
{ label: t("types.medicals"), value: "medicals" },
{ label: t("types.operations"), value: "operations" },
{ label: t("types.deliveryResultType"), value: "deliveryresulttypes" },
{ label: t("types.pregnantTreatment"), value: "pregnanttreatmenttypes" },
],
(type) => type.label
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useAppDispatch } from "libraries/hooks/redux";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { getAgeTypes } from "state/types/ageTypes";
import { setTypeMode } from "../../../../../../state/types/config";
import Button from "../../../../button/Button";
import AgeTypesTable from "./ageTypesTable";
import "./styles.scss";

const AgeTypes = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();

useEffect(() => {
dispatch(getAgeTypes());
dispatch(setTypeMode("manage"));
}, [dispatch]);

const { t } = useTranslation();

return (
<>
<h3 data-cy="sub-activity-title">{t("ageTypes.title")}</h3>

<div className="ageTypes" data-cy="age-types-table">
<AgeTypesTable
headerActions={
<Button
onClick={() => {
navigate("./edit");
}}
type="button"
variant="contained"
color="primary"
dataCy="edit-age-types"
>
{t("ageTypes.editAgeTypes")}
</Button>
}
/>
</div>
</>
);
};

export default AgeTypes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import TextField from "components/accessories/textField/TextField";
import React, { FC } from "react";
import { IAgeTypeFormProps } from "./types";

const AgeTypeForm: FC<IAgeTypeFormProps> = ({
formik,
getErrorText,
isValid,
t,
index,
}) => {
SilverD3 marked this conversation as resolved.
Show resolved Hide resolved
return (
<tr className="ageTypeFormRow">
<td>{formik.values.ageTypes[index].code}</td>
<td className="fromField">
<TextField
field={formik.getFieldProps(`ageTypes[${index}].from`)}
theme="regular"
label={t("ageTypes.from")}
isValid={isValid("from", index)}
errorText={getErrorText("from", index)}
onBlur={formik.handleBlur}
type="number"
/>
</td>
<td className="toField">
<TextField
field={formik.getFieldProps(`ageTypes[${index}].to`)}
theme="regular"
label={t("ageTypes.to")}
isValid={isValid("to", index)}
errorText={getErrorText("to", index)}
onBlur={formik.handleBlur}
type="number"
/>
</td>
<td>{t(formik.values.ageTypes[index].description)}</td>
</tr>
);
};

export default AgeTypeForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { useFormik } from "formik";
import { AgeTypeDTO } from "generated";
import { useAppDispatch, useAppSelector } from "libraries/hooks/redux";
import { get, has } from "lodash";
import React, { FC, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { updateAgeTypeReset } from "state/types/ageTypes";
import { array, number, object, ref, string } from "yup";
import checkIcon from "../../../../../../../assets/check-icon.png";
import warningIcon from "../../../../../../../assets/warning-icon.png";
import { PATHS } from "../../../../../../../consts";
import {
formatAllFieldValues,
getFromFields,
} from "../../../../../../../libraries/formDataHandling/functions";
import Button from "../../../../../button/Button";
import ConfirmationDialog from "../../../../../confirmationDialog/ConfirmationDialog";
import InfoBox from "../../../../../infoBox/InfoBox";
import AgeTypeForm from "./AgeTypeForm";
import { validateRange } from "./consts";
import "./styles.scss";
import { IAgeTypesFormProps } from "./types";

const AgeTypesForm: FC<IAgeTypesFormProps> = ({
onSubmit,
rows,
submitButtonLabel,
resetButtonLabel,
isLoading,
}) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
const infoBoxRef = useRef<HTMLDivElement>(null);
const [openResetConfirmation, setOpenResetConfirmation] = useState(false);
const [validationErrors, setValidationErrors] = useState<string[]>([]);

const updateAgeTypes = useAppSelector((state) => state.types.ageTypes.update);

const errorMessage = useMemo(
() => updateAgeTypes.error?.message ?? t("common.somethingwrong"),
[t, updateAgeTypes.error?.message]
);

const initialValues = {
ageTypes: rows.map((fields) => getFromFields(fields, "value")),
};

const validationSchema = object({
ageTypes: array(
object({
code: string().required(t("common.required")),
description: string().required(t("common.required")),
from: number()
.required(t("common.required"))
.min(0, t("common.greaterthan", { value: 0 })),
to: number()
.required(t("common.required"))
.min(ref("from"), t("ageTypes.shouldbegreaterthanfrom")),
})
),
});

const formik = useFormik({
initialValues,
validationSchema,
enableReinitialize: true,
onSubmit: (values) => {
const formattedValues = rows.map((fields, index) =>
formatAllFieldValues(fields, values.ageTypes[index])
);

const errors = validateRange(formattedValues as AgeTypeDTO[], t);
setValidationErrors(errors);
if (errors.length === 0) {
onSubmit(formattedValues as any);
}
},
});

const isValid = (fieldName: string, index: number): boolean => {
return (
has(formik.touched.ageTypes?.[index], fieldName) &&
has(formik.errors.ageTypes?.[index], fieldName)
);
};

const getErrorText = (fieldName: string, index: number): string => {
return has(formik.touched.ageTypes?.[index], fieldName)
? (get(formik.errors.ageTypes?.[index], fieldName) as string)
: "";
};

const handleResetConfirmation = () => {
setOpenResetConfirmation(false);
navigate(-1);
};

useEffect(() => {
return () => {
dispatch(updateAgeTypeReset());
};
}, [dispatch]);

return (
<div className="ageTypesForm">
<form className="ageTypesForm__form" onSubmit={formik.handleSubmit}>
<div className="row">
<table className="ageTypesFormTable">
<thead>
<tr>
<th>{t("ageTypes.code")}</th>
<th>{t("ageTypes.from")}</th>
<th>{t("ageTypes.to")}</th>
<th>{t("ageTypes.description")}</th>
</tr>
</thead>
<tbody>
{rows.map((fields, index) => (
<AgeTypeForm
formik={formik}
getErrorText={getErrorText}
index={index}
isValid={isValid}
t={t}
key={index}
/>
SilverD3 marked this conversation as resolved.
Show resolved Hide resolved
))}
</tbody>
</table>
</div>

<div className="ageTypesForm__buttonSet">
<div className="submit_button">
<Button
type="submit"
dataCy="submit-form"
variant="contained"
disabled={isLoading}
>
{submitButtonLabel}
</Button>
</div>
<div className="reset_button">
<Button
type="reset"
variant="text"
dataCy="cancel-form"
disabled={isLoading}
onClick={() => setOpenResetConfirmation(true)}
>
{resetButtonLabel}
</Button>
</div>
</div>
<ConfirmationDialog
isOpen={openResetConfirmation}
title={resetButtonLabel.toUpperCase()}
info={t("ageTypes.cancelUpdate")}
icon={warningIcon}
primaryButtonLabel={t("common.ok")}
secondaryButtonLabel={t("common.discard")}
handlePrimaryButtonClick={handleResetConfirmation}
handleSecondaryButtonClick={() => setOpenResetConfirmation(false)}
/>
{updateAgeTypes.status === "FAIL" && (
<div ref={infoBoxRef} className="info-box-container">
<InfoBox type="error" message={errorMessage} />
</div>
)}
{validationErrors.length > 0 && (
<div ref={infoBoxRef} className="info-box-container">
<InfoBox type="error" message={validationErrors.join("; ")} />
</div>
)}
<ConfirmationDialog
isOpen={!!updateAgeTypes.hasSucceeded}
title={t("ageTypes.updated")}
icon={checkIcon}
info={t("ageTypes.updateSuccess")}
primaryButtonLabel="Ok"
handlePrimaryButtonClick={() => {
navigate(PATHS.admin_age_types);
}}
handleSecondaryButtonClick={() => ({})}
/>
</form>
</div>
);
};

export default AgeTypesForm;
Loading