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

feat OH2-285: exams crud #601

Merged
merged 14 commits into from
Jul 24, 2024
8 changes: 8 additions & 0 deletions src/components/accessories/admin/exams/Exams.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.exams {
display: flex;
flex-direction: column;
}
.actions {
display: flex;
justify-content: end;
}
43 changes: 42 additions & 1 deletion src/components/accessories/admin/exams/Exams.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
import React from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";

import { PATHS } from "../../../../consts";
import { deleteExam } from "../../../../state/exams/actions";
import { ExamDTO } from "../../../../generated";
import classes from "./Exams.module.scss";

import Button from "../../button/Button";
import ExamsTable from "./examsTable";

export const Exams = () => {
return <ExamsTable />;
const dispatch = useDispatch();
const navigate = useNavigate();
const { t } = useTranslation();

const handleDelete = (row: ExamDTO) => {
dispatch(deleteExam(row.code!));
};
const handleEdit = (row: ExamDTO) => {
navigate(PATHS.admin_exams_edit.replace(":id", `${row.code}`), {
state: row,
});
};

return (
<div className={classes.exams}>
<ExamsTable
headerActions={
<Button
onClick={() => {
navigate(PATHS.admin_exams_new);
}}
type="button"
variant="contained"
color="primary"
>
{t("exam.addExam")}
</Button>
}
onDelete={handleDelete}
onEdit={handleEdit}
/>
</div>
);
};
40 changes: 40 additions & 0 deletions src/components/accessories/admin/exams/editExam/EditExam.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useTranslation } from "react-i18next";
import ExamForm from "../examForm/ExamForm";
import React from "react";
import { getInitialFields } from "../examForm/consts";
import { useDispatch, useSelector } from "react-redux";
import { ExamDTO } from "../../../../../generated";
import { ApiResponse } from "../../../../../state/types";
import { updateExam } from "../../../../../state/exams/actions";
import { IState } from "../../../../../types";
import { Navigate, useLocation, useParams } from "react-router";
import { PATHS } from "../../../../../consts";

export const EditExam = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const { state }: { state: ExamDTO | undefined } = useLocation();
const { id } = useParams();
const update = useSelector<IState, ApiResponse<ExamDTO>>(
(state) => state.operations.update
);

const handleSubmit = (examDTO: ExamDTO) => {
dispatch(updateExam(examDTO));
};

if (state?.code !== id) {
return <Navigate to={PATHS.admin_exams} />;
}

return (
<ExamForm
creationMode={false}
onSubmit={handleSubmit}
isLoading={!!update.isLoading}
resetButtonLabel={t("common.cancel")}
submitButtonLabel={t("exam.updateExam")}
fields={getInitialFields(state)}
/>
);
};
1 change: 1 addition & 0 deletions src/components/accessories/admin/exams/editExam/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./EditExam";
279 changes: 279 additions & 0 deletions src/components/accessories/admin/exams/examForm/ExamForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import { useFormik } from "formik";
import { get, has } from "lodash";
import React, {
FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { object, string, number } from "yup";
import warningIcon from "../../../../../assets/warning-icon.png";
import {
formatAllFieldValues,
getFromFields,
} from "../../../../../libraries/formDataHandling/functions";
import checkIcon from "../../../../../assets/check-icon.png";
import Button from "../../../button/Button";
import ConfirmationDialog from "../../../confirmationDialog/ConfirmationDialog";
import TextField from "../../../textField/TextField";
import "./styles.scss";
import { IExamProps } from "./types";

import { useDispatch, useSelector } from "react-redux";
import { IState } from "../../../../../types";
import { useNavigate } from "react-router";
import { IExamState } from "../../../../../state/exams/types";
import { PATHS } from "../../../../../consts";
import {
createExamReset,
updateExamReset,
} from "../../../../../state/exams/actions";
import { getExamTypes } from "../../../../../state/types/exams/actions";

import InfoBox from "../../../infoBox/InfoBox";
import AutocompleteField from "../../../autocompleteField/AutocompleteField";

const ExamForm: FC<IExamProps> = ({
fields,
onSubmit,
creationMode,
submitButtonLabel,
resetButtonLabel,
isLoading,
}) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
const infoBoxRef = useRef<HTMLDivElement>(null);
const [openResetConfirmation, setOpenResetConfirmation] = useState(false);

const examStore = useSelector<IState, IExamState>((state) => state.exams);

const examTypeState = useSelector(
(state: IState) => state.types.exams.getAll
);
const examTypeStateOptions = useMemo(
() =>
examTypeState.data?.map((item) => ({
value: item.code,
label: item.description,
})) ?? [],
[examTypeState.data]
);

const errorMessage = useMemo(
() =>
(creationMode
? examStore.examCreate.error?.message
: examStore.examUpdate.error?.message) ?? t("common.somethingwrong"),
[
creationMode,
t,
examStore.examCreate.error?.message,
examStore.examUpdate.error?.message,
]
);

const initialValues = getFromFields(fields, "value");

const validationSchema = object({
code: string().required(t("common.required")),
description: string().required(t("common.required")),
type: string().required(t("common.required")),
procedure: number()
.test({
name: "onetwothree",
exclusive: true,
test: (value) => [1, 2, 3].includes(value),
message: t("exam.validateProcedureMinMax"),
})
.required(t("common.required")),
});

const formik = useFormik({
initialValues,
validationSchema,
enableReinitialize: true,
onSubmit: (values) => {
const formattedValues = formatAllFieldValues(fields, values);
formattedValues.type = examTypeState.data?.find(
(item) => item.code === values.type
);
onSubmit(formattedValues as any);
},
});

const { setFieldValue, handleBlur } = formik;

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 handleResetConfirmation = () => {
setOpenResetConfirmation(false);
navigate(-1);
};

const onBlurCallback = useCallback(
(fieldName: string) =>
(e: React.FocusEvent<HTMLDivElement>, value: string) => {
handleBlur(e);
setFieldValue(fieldName, value);
},
[handleBlur, setFieldValue]
);

const cleanUp = useCallback(() => {
if (creationMode) {
dispatch(createExamReset());
} else {
dispatch(updateExamReset());
}
}, [creationMode, dispatch]);

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

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

return (
<div className="examForm">
<form className="examForm__form" onSubmit={formik.handleSubmit}>
<div className="row start-sm center-xs">
<div className="examForm__item fullWidth">
<AutocompleteField
fieldName="type"
fieldValue={formik.values.type}
label={t("exam.examtype")}
isValid={isValid("type")}
errorText={getErrorText("type")}
onBlur={onBlurCallback("type")}
options={examTypeStateOptions}
loading={examTypeState.status === "LOADING"}
disabled={isLoading}
/>
</div>
<div className="examForm__item halfWidth">
<TextField
field={formik.getFieldProps("code")}
theme="regular"
label={t("exam.code")}
isValid={isValid("code")}
errorText={getErrorText("code")}
onBlur={formik.handleBlur}
type="text"
disabled={isLoading || !creationMode}
/>
</div>
<div className="examForm__item halfWidth">
<TextField
field={formik.getFieldProps("description")}
theme="regular"
label={t("exam.description")}
isValid={isValid("description")}
errorText={getErrorText("description")}
onBlur={formik.handleBlur}
type="text"
disabled={isLoading}
/>
</div>
</div>

<div className="row start-sm center-xs">
<div className="examForm__item halfWidth">
<TextField
field={formik.getFieldProps("procedure")}
theme="regular"
label={t("exam.procedure")}
isValid={isValid("procedure")}
errorText={getErrorText("procedure")}
onBlur={formik.handleBlur}
type="number"
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")}
onBlur={formik.handleBlur}
type="text"
disabled={isLoading}
/>
</div>
</div>

<div className="examForm__buttonSet">
<div className="submit_button">
<Button type="submit" variant="contained" disabled={isLoading}>
{submitButtonLabel}
</Button>
</div>
<div className="reset_button">
<Button
type="reset"
variant="text"
disabled={isLoading}
onClick={() => setOpenResetConfirmation(true)}
>
{resetButtonLabel}
</Button>
</div>
</div>
<ConfirmationDialog
isOpen={openResetConfirmation}
title={resetButtonLabel.toUpperCase()}
info={t("common.resetform")}
icon={warningIcon}
primaryButtonLabel={t("common.ok")}
secondaryButtonLabel={t("common.discard")}
handlePrimaryButtonClick={handleResetConfirmation}
handleSecondaryButtonClick={() => setOpenResetConfirmation(false)}
/>
{(creationMode
? examStore.examCreate.status === "FAIL"
: examStore.examUpdate.status === "FAIL") && (
<div ref={infoBoxRef} className="info-box-container">
<InfoBox type="error" message={errorMessage} />
</div>
)}
<ConfirmationDialog
isOpen={
!!(creationMode
? examStore.examCreate.hasSucceeded
: examStore.examUpdate.hasSucceeded)
}
title={creationMode ? t("exam.created") : t("exam.updated")}
icon={checkIcon}
info={
creationMode
? t("exam.createSuccess")
: t("exam.updateSuccess", { code: formik.values.code })
}
primaryButtonLabel="Ok"
handlePrimaryButtonClick={() => {
navigate(PATHS.admin_exams);
}}
handleSecondaryButtonClick={() => ({})}
/>
</form>
</div>
);
};

export default ExamForm;
13 changes: 13 additions & 0 deletions src/components/accessories/admin/exams/examForm/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ExamDTO } from "../../../../../generated";
import { TFields } from "../../../../../libraries/formDataHandling/types";
import { ExamProps } from "../types";

export const getInitialFields: (
operation: ExamDTO | undefined
) => TFields<ExamProps> = (exam) => ({
code: { type: "text", value: exam?.code ?? "" },
type: { type: "text", value: exam?.examtype?.code ?? "" },
description: { type: "text", value: exam?.description ?? "" },
procedure: { type: "number", value: exam?.procedure?.toString() ?? "" },
defaultResult: { type: "text", value: exam?.defaultResult ?? "" },
});
Loading