diff --git a/cypress/integrations/admin_activities/hospital/edit_hospital.cy.ts b/cypress/integrations/admin_activities/hospital/edit_hospital.cy.ts new file mode 100644 index 000000000..389641541 --- /dev/null +++ b/cypress/integrations/admin_activities/hospital/edit_hospital.cy.ts @@ -0,0 +1,48 @@ +/// + +const STARTS_PATH = "/admin"; + +describe("Edit Hospital Activity specs", () => { + it("should render the ui", () => { + cy.authenticate(STARTS_PATH); + cy.dataCy("hospital-infos").click(); + cy.dataCy("edit-hospital").click(); + cy.dataCy("activity-title").contains("Edit hospital"); + }); + + it("should fail to edit the hospitalInfo", () => { + cy.byId("description").clear().type("FAIL"); + cy.byId("email").clear().type("luke@hospital.com"); + cy.dataCy("submit-form").click(); + cy.dataCy("dialog-info").should("not.exist"); + cy.dataCy("info-box").contains("Invalid payload"); + }); + + it("should successfully save hospital infos changes", () => { + cy.byId("description").clear().type("St. LUKE Hospital"); + cy.byId("email").clear().type("luke@hospital.com"); + cy.byId("currencyCod").clear().type("FCFA"); + cy.dataCy("submit-form").click(); + cy.dataCy("dialog-info").contains("updated successfully"); + cy.dataCy("approve-dialog").click(); + }); + + it("should redirect after hospital info update", () => { + cy.dataCy("activity-title").contains("Wards"); + }); + + it("should cancel the discard of the hospital infos", () => { + cy.dataCy("edit-hospital").first().click(); + cy.dataCy("cancel-form").click(); + cy.dataCy("dialog-info").contains("lost"); + cy.dataCy("close-dialog").click().click(); + cy.dataCy("dialog-info").should("not.be.visible"); + }); + + it("should cancel the update of the hospital infos", () => { + cy.dataCy("cancel-form").click(); + cy.dataCy("dialog-info").contains("lost"); + cy.dataCy("approve-dialog").click(); + cy.dataCy("activity-title").contains("Wards"); + }); +}); diff --git a/src/components/accessories/admin/hospital/editHospital/EditHospital.tsx b/src/components/accessories/admin/hospital/editHospital/EditHospital.tsx new file mode 100644 index 000000000..0a902508c --- /dev/null +++ b/src/components/accessories/admin/hospital/editHospital/EditHospital.tsx @@ -0,0 +1,44 @@ +import { useAppDispatch, useAppSelector } from "libraries/hooks/redux"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router"; +import { updateHospital } from "state/hospital"; +import { PATHS } from "../../../../../consts"; +import { HospitalDTO } from "../../../../../generated"; +import HospitalForm from "../hospitalForm/HospitalForm"; +import { getInitialFields } from "../hospitalForm/consts"; + +export const EditHospital = () => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const hospital = useAppSelector((state) => state.hospital.getHospital.data); + const update = useAppSelector((state) => state.hospital.updateHospital); + + const navigate = useNavigate(); + + const handleSubmit = (value: HospitalDTO) => { + dispatch( + updateHospital({ + code: hospital!.code!!, + hospitalDTO: { ...hospital, ...value }, + }) + ); + }; + + useEffect(() => { + if (!hospital) { + navigate(PATHS.admin); + } + }, [navigate, hospital]); + + return ( + + ); +}; diff --git a/src/components/accessories/admin/hospital/editHospital/index.ts b/src/components/accessories/admin/hospital/editHospital/index.ts new file mode 100644 index 000000000..8c2ff285b --- /dev/null +++ b/src/components/accessories/admin/hospital/editHospital/index.ts @@ -0,0 +1 @@ +export * from "./EditHospital"; diff --git a/src/components/accessories/admin/hospital/hospitalForm/HospitalForm.tsx b/src/components/accessories/admin/hospital/hospitalForm/HospitalForm.tsx new file mode 100644 index 000000000..f13e8884e --- /dev/null +++ b/src/components/accessories/admin/hospital/hospitalForm/HospitalForm.tsx @@ -0,0 +1,232 @@ +import { useFormik } from "formik"; +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 { object, 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 { updateHospitalReset } from "../../../../../state/hospital"; +import Button from "../../../button/Button"; +import ConfirmationDialog from "../../../confirmationDialog/ConfirmationDialog"; +import InfoBox from "../../../infoBox/InfoBox"; +import TextField from "../../../textField/TextField"; +import "./styles.scss"; +import { IHospitalFormProps } from "./types"; + +const HospitalForm: FC = ({ + fields, + onSubmit, + submitButtonLabel, + resetButtonLabel, + isLoading, +}) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const infoBoxRef = useRef(null); + const [openDiscardConfirmation, setOpenDiscardConfirmation] = useState(false); + + const hospitalStore = useAppSelector((state) => state.hospital); + + const errorMessage = useMemo( + () => + hospitalStore.updateHospital.error?.message ?? t("common.somethingwrong"), + [t, hospitalStore.updateHospital.error?.message] + ); + + const initialValues = getFromFields(fields, "value"); + + const validationSchema = object({ + description: string().required(t("common.required")), + address: string().notRequired(), + city: string().notRequired(), + fax: string().notRequired(), + telephone: string().notRequired(), + email: string().notRequired().email(t("validations.email")), + currencyCod: string().notRequired(), + }); + + const formik = useFormik({ + initialValues, + validationSchema, + enableReinitialize: true, + onSubmit: (values) => { + const formattedValues = formatAllFieldValues(fields, values); + onSubmit(formattedValues as any); + }, + }); + + const isValid = (fieldName: string): boolean => { + return has(formik.touched, fieldName) && has(formik.errors, fieldName); + }; + + const getErrorText = (fieldName: string): string => { + return has(formik.touched, fieldName) + ? (get(formik.errors, fieldName) as string) + : ""; + }; + + const handleDiscardConfirmation = () => { + setOpenDiscardConfirmation(false); + navigate(-1); + }; + + useEffect(() => { + return () => { + dispatch(updateHospitalReset()); + }; + }, []); + + return ( +
+
+
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ setOpenDiscardConfirmation(false)} + /> + {hospitalStore.updateHospital.status === "FAIL" && ( +
+ +
+ )} + { + navigate(PATHS.admin); + }} + handleSecondaryButtonClick={() => ({})} + /> + +
+ ); +}; + +export default HospitalForm; diff --git a/src/components/accessories/admin/hospital/hospitalForm/consts.ts b/src/components/accessories/admin/hospital/hospitalForm/consts.ts new file mode 100644 index 000000000..8118c33cd --- /dev/null +++ b/src/components/accessories/admin/hospital/hospitalForm/consts.ts @@ -0,0 +1,15 @@ +import { HospitalFormFieldName } from "."; +import { HospitalDTO } from "../../../../../generated"; +import { TFields } from "../../../../../libraries/formDataHandling/types"; + +export const getInitialFields: ( + hospital: HospitalDTO | undefined +) => TFields = (hospital) => ({ + description: { type: "text", value: hospital?.description ?? "" }, + address: { type: "text", value: hospital?.address ?? "" }, + city: { type: "text", value: hospital?.city ?? "" }, + telephone: { type: "text", value: hospital?.telephone ?? "" }, + fax: { type: "text", value: hospital?.fax ?? "" }, + email: { type: "text", value: hospital?.email ?? "" }, + currencyCod: { type: "text", value: hospital?.currencyCod ?? "" }, +}); diff --git a/src/components/accessories/admin/hospital/hospitalForm/index.ts b/src/components/accessories/admin/hospital/hospitalForm/index.ts new file mode 100644 index 000000000..f98448f8d --- /dev/null +++ b/src/components/accessories/admin/hospital/hospitalForm/index.ts @@ -0,0 +1,2 @@ +export * from "./HospitalForm"; +export * from "./types"; diff --git a/src/components/accessories/admin/hospital/hospitalForm/styles.scss b/src/components/accessories/admin/hospital/hospitalForm/styles.scss new file mode 100644 index 000000000..a0ca81274 --- /dev/null +++ b/src/components/accessories/admin/hospital/hospitalForm/styles.scss @@ -0,0 +1,85 @@ +@import "../../../../../styles/variables"; +@import "../../../../../../node_modules/susy/sass/susy"; + +.hospitalForm { + display: inline-block; + flex-direction: column; + align-items: center; + width: 100%; + + .formInsertMode { + margin: 0px 0px 20px; + } + + .row { + justify-content: space-between; + } + + .hospitalForm__item { + margin: 7px 0px; + padding: 0px 15px; + width: 50%; + @include susy-media($narrow) { + padding: 0px 10px; + } + @include susy-media($tablet_land) { + padding: 0px 10px; + } + @include susy-media($medium-up) { + width: 25%; + } + @include susy-media($tablet_port) { + width: 50%; + } + @include susy-media($smartphone) { + width: 100%; + } + .textField, + .selectField { + width: 100%; + } + + &.fullWidth { + width: 100%; + } + + &.halfWidth { + width: 50%; + @include susy-media($smartphone) { + width: 100%; + } + } + &.thirdWidth { + width: 33%; + @include susy-media($smartphone) { + width: 100%; + } + } + } + + .hospitalForm__buttonSet { + display: flex; + margin-top: 25px; + padding: 0px 15px; + gap: 4px; + flex-direction: row-reverse; + @include susy-media($smartphone_small) { + display: block; + } + + .submit_button, + .discard_button { + .MuiButton-label { + font-size: smaller; + letter-spacing: 1px; + font-weight: 600; + } + button { + @include susy-media($smartphone_small) { + width: 100%; + margin-top: 10px; + } + } + } + } +} diff --git a/src/components/accessories/admin/hospital/hospitalForm/types.ts b/src/components/accessories/admin/hospital/hospitalForm/types.ts new file mode 100644 index 000000000..ffb0f8623 --- /dev/null +++ b/src/components/accessories/admin/hospital/hospitalForm/types.ts @@ -0,0 +1,19 @@ +import { HospitalDTO } from "../../../../../generated"; +import { TFields } from "../../../../../libraries/formDataHandling/types"; + +export interface IHospitalFormProps { + fields: TFields; + onSubmit: (adm: HospitalDTO) => void; + submitButtonLabel: string; + resetButtonLabel: string; + isLoading: boolean; +} + +export type HospitalFormFieldName = + | "description" + | "address" + | "city" + | "telephone" + | "fax" + | "email" + | "currencyCod"; diff --git a/src/components/accessories/admin/hospital/index.ts b/src/components/accessories/admin/hospital/index.ts new file mode 100644 index 000000000..ab01c1d62 --- /dev/null +++ b/src/components/accessories/admin/hospital/index.ts @@ -0,0 +1 @@ +export * from "./editHospital"; diff --git a/src/components/accessories/button/Button.tsx b/src/components/accessories/button/Button.tsx index 964238b57..d2a8493d5 100644 --- a/src/components/accessories/button/Button.tsx +++ b/src/components/accessories/button/Button.tsx @@ -11,10 +11,11 @@ const Button: FunctionComponent = ({ disabled, dataCy, onClick, + className, }) => { return ( ) => void; } diff --git a/src/components/accessories/menuItem/MenuItem.tsx b/src/components/accessories/menuItem/MenuItem.tsx index 3e5177cdd..6347ca63d 100644 --- a/src/components/accessories/menuItem/MenuItem.tsx +++ b/src/components/accessories/menuItem/MenuItem.tsx @@ -14,6 +14,7 @@ interface IOwnProps { label: string; selected?: boolean; expandedContent?: ReactNode; + dataCy?: string; onClick: () => void; } @@ -23,11 +24,13 @@ export const MenuItem = ({ label, selected, expandedContent, + dataCy, onClick, }: IOwnProps) => { const [expanded, setExpanded] = useState(false); const menu = (
diff --git a/src/components/activities/adminActivity/SideMenu/SideMenu.module.scss b/src/components/activities/adminActivity/SideMenu/SideMenu.module.scss index 7f9c69479..d002276a1 100644 --- a/src/components/activities/adminActivity/SideMenu/SideMenu.module.scss +++ b/src/components/activities/adminActivity/SideMenu/SideMenu.module.scss @@ -20,6 +20,14 @@ gap: 8px; } +.editButton { + color: black !important; + background-color: white !important; + &:hover { + background-color: gray !important; + } +} + .item { display: flex; flex-direction: column; diff --git a/src/components/activities/adminActivity/SideMenu/SideMenu.tsx b/src/components/activities/adminActivity/SideMenu/SideMenu.tsx index a3b6e55f6..81c8b9806 100644 --- a/src/components/activities/adminActivity/SideMenu/SideMenu.tsx +++ b/src/components/activities/adminActivity/SideMenu/SideMenu.tsx @@ -10,10 +10,12 @@ import { People, SupervisedUserCircle, } from "@mui/icons-material"; +import { PATHS } from "consts"; import React, { ReactNode, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router"; import { useAppSelector } from "../../../../libraries/hooks/redux"; +import Button from "../../../accessories/button/Button"; import { MenuItem } from "../../../accessories/menuItem"; import { IAdminSection } from "../types"; import classes from "./SideMenu.module.scss"; @@ -86,6 +88,7 @@ const SideMenu = () => { ))}
{t("nav.hospital")}
} label={t(`nav.hospitalInfo`)} onClick={() => {}} @@ -102,6 +105,17 @@ const SideMenu = () => { {entry[1] ?? "---"}
))} + } /> diff --git a/src/consts.ts b/src/consts.ts index 91fe44272..ccda8c1ad 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -72,4 +72,5 @@ export const PATHS = { admin_admissions_types: "/admin/types/admissions", admin_admissions_types_new: "/admin/types/admissions/new", admin_admissions_types_edit: "/admin/types/admissions/:code/edit", + admin_hospital_edit: "/admin/hospital/edit", }; diff --git a/src/mockServer/routes/hospital.js b/src/mockServer/routes/hospital.js index 09083c8c6..e5e3704a9 100644 --- a/src/mockServer/routes/hospital.js +++ b/src/mockServer/routes/hospital.js @@ -19,5 +19,15 @@ export const hospitalRoutes = (server) => { res.status(200).json(hospitalDTO); } }); + server.put("/:code").intercept((req, res) => { + const body = req.jsonBody(); + switch (body.description) { + case "FAIL": + res.status(400).json({ message: "Invalid payload" }); + break; + default: + res.status(200).json(body); + } + }); }); }; diff --git a/src/resources/i18n/en.json b/src/resources/i18n/en.json index d897b0658..f743ad2aa 100644 --- a/src/resources/i18n/en.json +++ b/src/resources/i18n/en.json @@ -45,7 +45,13 @@ "telephone": "Telephone", "fax": "Fax", "email": "Email Address", - "currencyCod": "Currency Code" + "currencyCod": "Currency Code", + "editHospital": "Edit hospital infos", + "updateHospital": "Save changes", + "saveHospital": "Save", + "updated": "Hospital infos updated", + "updateSuccess": "Hospital infos have been updated successfully!", + "discardChanges": "Changes will be lost. Do you want to discard anyway ?" }, "login": { "username": "Username", diff --git a/src/routes/Admin/AdminRoutes.tsx b/src/routes/Admin/AdminRoutes.tsx index 2e092a382..ceddc2a48 100644 --- a/src/routes/Admin/AdminRoutes.tsx +++ b/src/routes/Admin/AdminRoutes.tsx @@ -1,3 +1,4 @@ +import { EditHospital } from "components/accessories/admin/hospital"; import React, { ReactNode, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Navigate, Route, Routes } from "react-router"; @@ -6,7 +7,11 @@ import { EditDisease, NewDisease, } from "../../components/accessories/admin/diseases"; -import { EditExam, Exams, NewExam } from "../../components/accessories/admin/exams"; +import { + EditExam, + Exams, + NewExam, +} from "../../components/accessories/admin/exams"; import { EditOperation, NewOperation, @@ -100,13 +105,19 @@ export const AdminRoutes = () => { { path: getPath(PATHS.admin_exams_new), element: ( - } /> + } + /> ), }, { path: getPath(PATHS.admin_exams_edit), element: ( - } /> + } + /> ), }, { @@ -211,6 +222,15 @@ export const AdminRoutes = () => { /> ), }, + { + path: getPath(PATHS.admin_hospital_edit), + element: ( + } + /> + ), + }, ], [t] ); diff --git a/src/state/hospital/initial.ts b/src/state/hospital/initial.ts index 83501116d..afff62c89 100644 --- a/src/state/hospital/initial.ts +++ b/src/state/hospital/initial.ts @@ -1,6 +1,7 @@ -import { IHospitalState } from "./types"; import { ApiResponse } from "../types"; +import { IHospitalState } from "./types"; export const initial: IHospitalState = { getHospital: new ApiResponse({ status: "IDLE" }), + updateHospital: new ApiResponse({ status: "IDLE" }), }; diff --git a/src/state/hospital/slice.ts b/src/state/hospital/slice.ts index 31306fa90..b011df253 100644 --- a/src/state/hospital/slice.ts +++ b/src/state/hospital/slice.ts @@ -6,7 +6,11 @@ import * as thunks from "./thunk"; export const hospitalSlice = createSlice({ name: "hospitals", initialState: initial, - reducers: {}, + reducers: { + updateHospitalReset: (state) => { + state.updateHospital = initial.updateHospital; + }, + }, extraReducers: (builder) => builder // Get Hospital @@ -18,5 +22,19 @@ export const hospitalSlice = createSlice({ }) .addCase(thunks.getHospital.rejected, (state, action) => { state.getHospital = ApiResponse.error(action.payload); + }) + // Update Hospital + .addCase(thunks.updateHospital.pending, (state) => { + state.updateHospital = ApiResponse.loading(); + }) + .addCase(thunks.updateHospital.fulfilled, (state, action) => { + state.updateHospital = state.getHospital = ApiResponse.value( + action.payload + ); + }) + .addCase(thunks.updateHospital.rejected, (state, action) => { + state.updateHospital = ApiResponse.error(action.payload); }), }); + +export const { updateHospitalReset } = hospitalSlice.actions; diff --git a/src/state/hospital/thunk.ts b/src/state/hospital/thunk.ts index b16279824..5bc077173 100644 --- a/src/state/hospital/thunk.ts +++ b/src/state/hospital/thunk.ts @@ -1,9 +1,11 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; -import { HospitalsApi } from "../../generated"; +import { HospitalDTO, HospitalsApi } from "../../generated"; import { customConfiguration } from "../../libraries/apiUtils/configuration"; const api = new HospitalsApi(customConfiguration(false)); +const securedApi = new HospitalsApi(customConfiguration()); + export const getHospital = createAsyncThunk( "hospitals/getHospital", async (_, thunkApi) => @@ -12,3 +14,12 @@ export const getHospital = createAsyncThunk( .toPromise() .catch((error) => thunkApi.rejectWithValue(error.response)) ); + +export const updateHospital = createAsyncThunk( + "hospitals/updateHospital", + async (payload: { code: string; hospitalDTO: HospitalDTO }, thunkApi) => + securedApi + .updateHospital(payload) + .toPromise() + .catch((error) => thunkApi.rejectWithValue(error.response)) +); diff --git a/src/state/hospital/types.ts b/src/state/hospital/types.ts index a3e5810fe..0b0f26c78 100644 --- a/src/state/hospital/types.ts +++ b/src/state/hospital/types.ts @@ -3,4 +3,5 @@ import { ApiResponse } from "../types"; export type IHospitalState = { getHospital: ApiResponse; + updateHospital: ApiResponse; };