diff --git a/sanitas_backend/src/handlers/GetGynecoObstetricHistory/get-gynecoobstetric-history-patient.integration.test.mjs b/sanitas_backend/src/handlers/GetGynecoObstetricHistory/get-gynecoobstetric-history-patient.integration.test.mjs index 7c4ab932..5babf713 100644 --- a/sanitas_backend/src/handlers/GetGynecoObstetricHistory/get-gynecoobstetric-history-patient.integration.test.mjs +++ b/sanitas_backend/src/handlers/GetGynecoObstetricHistory/get-gynecoobstetric-history-patient.integration.test.mjs @@ -56,6 +56,7 @@ async function createPatientWithGynecologicalHistory() { otherCondition: [ { medication: { + illness: "illness A", medication: "Med D", dosage: "500mg", frequency: "Once a day", diff --git a/sanitas_backend/src/handlers/UpdateGynecoObstetricHistory/update-gynecoobstetric-history-patient.integration.test.mjs b/sanitas_backend/src/handlers/UpdateGynecoObstetricHistory/update-gynecoobstetric-history-patient.integration.test.mjs index 91419156..4abc1538 100644 --- a/sanitas_backend/src/handlers/UpdateGynecoObstetricHistory/update-gynecoobstetric-history-patient.integration.test.mjs +++ b/sanitas_backend/src/handlers/UpdateGynecoObstetricHistory/update-gynecoobstetric-history-patient.integration.test.mjs @@ -59,6 +59,7 @@ function generateValidUpdate(patientId) { otherCondition: [ { medication: { + illness: "illness A", medication: "Med D", dosage: "500mg", frequency: "Once a day", @@ -150,6 +151,7 @@ describe("Update Gynecological Medical History integration tests", () => { otherCondition: [ { medication: { + illness: "illness A", medication: "Med D", dosage: "500mg", frequency: "Once a day", diff --git a/sanitas_frontend/src/components/DashboardSidebar/index.jsx b/sanitas_frontend/src/components/DashboardSidebar/index.jsx index 554f8881..bdc140f9 100644 --- a/sanitas_frontend/src/components/DashboardSidebar/index.jsx +++ b/sanitas_frontend/src/components/DashboardSidebar/index.jsx @@ -166,7 +166,7 @@ export default function DashboardSidebar({ fontWeight: "normal", paddingBottom: "1rem", paddingTop: "1rem", - borderBottom: `0.1rem solid ${colors.darkerGrey}`, + borderBottom: `0.04rem solid ${colors.darkerGrey}`, }} > Antecedentes diff --git a/sanitas_frontend/src/components/DropdownMenu/index.jsx b/sanitas_frontend/src/components/DropdownMenu/index.jsx index 372777a0..c8c9b078 100644 --- a/sanitas_frontend/src/components/DropdownMenu/index.jsx +++ b/sanitas_frontend/src/components/DropdownMenu/index.jsx @@ -75,7 +75,7 @@ export default function DropdownMenu({ indicator: { position: "absolute", top: "50%", - right: "5%", + right: "6%", transform: `translateY(-50%) rotate(${isOpen ? "180deg" : "0deg"})`, transition: "transform 0.3s", pointerEvents: "none", diff --git a/sanitas_frontend/src/dataLayer.mjs b/sanitas_frontend/src/dataLayer.mjs index c49d3210..f624f680 100644 --- a/sanitas_frontend/src/dataLayer.mjs +++ b/sanitas_frontend/src/dataLayer.mjs @@ -1179,6 +1179,75 @@ export const updatePsichiatricHistory = async ( } }; +export const getGynecologicalHistory = async (patientId) => { + const sessionResponse = IS_PRODUCTION + ? await getSession() + : await mockGetSession(true); + if (sessionResponse.error) { + return { error: sessionResponse.error }; + } + + if (!sessionResponse.result.isValid()) { + return { error: "Invalid session!" }; + } + + const token = sessionResponse?.result?.idToken?.jwtToken ?? "no-token"; + const url = `${PROTECTED_URL}/patient/gyneco-history/${patientId}`; + + try { + const response = await axios.get(url, { + headers: { Authorization: token }, + }); + if (response.status === 200) { + return { result: response.data }; + } + } catch (error) { + if (error.response) { + return { error: error.response.data }; + } + return { error: error.message }; + } +}; + +export const updateGynecologicalHistory = async ( + patientId, + gynecologicalHistoryDetails, +) => { + const sessionResponse = IS_PRODUCTION + ? await getSession() + : await mockGetSession(true); + if (sessionResponse.error) { + return { error: sessionResponse.error }; + } + + if (!sessionResponse.result.isValid()) { + return { error: "Invalid session!" }; + } + + const token = sessionResponse?.result?.idToken?.jwtToken ?? "no-token"; + const url = `${PROTECTED_URL}/patient/gyneco-history`; + + const payload = { + patientId: patientId, + medicalHistory: gynecologicalHistoryDetails, + }; + + try { + const response = await axios.put(url, payload, { + headers: { Authorization: token }, + }); + if (response.status === 200) { + return { result: response.data }; + } + return { error: `Unexpected status code: ${response.status}` }; + } catch (error) { + if (error.response) { + return { error: error.response.data }; + } + return { error: error.message }; + } +}; + /** * @callback LinkAccountToPatientCallback * @param {string} cui diff --git a/sanitas_frontend/src/router.jsx b/sanitas_frontend/src/router.jsx index 088b1656..be6103ef 100644 --- a/sanitas_frontend/src/router.jsx +++ b/sanitas_frontend/src/router.jsx @@ -21,6 +21,8 @@ import { getStudentPatientInformation, getSurgicalHistory, getTraumatologicalHistory, + getAllergicHistory, + getGynecologicalHistory, searchPatient, submitPatientData, updateCollaboratorInformation, @@ -32,8 +34,8 @@ import { updateSurgicalHistory, updateStudentSurgicalHistory, updateTraumatologicalHistory, - getAllergicHistory, updateAllergicHistory, + updateGynecologicalHistory, getPsichiatricHistory, updatePsichiatricHistory, getRole, @@ -53,6 +55,7 @@ import SearchPatientView from "./views/SearchPatientView"; import UpdateInfoView from "./views/UpdateGeneralInformationView"; import { TraumatologicHistory } from "./views/History/Traumatological"; import { AllergicHistory } from "./views/History/Allergic"; +import { ObGynHistory } from "./views/History/ObGyn"; import { PsichiatricHistory } from "./views/History/Psichiatric"; import StudentWelcomeView from "./views/StudentWelcomeView"; import { LinkPatientView } from "./views/LinkPatientView"; @@ -80,6 +83,7 @@ export const UPDATE_PATIENT_NAV_PATHS = { PERSONAL_HISTORY: "personal", NONPATHOLOGICAL_HISTORY: "non-pathological", ALLERGIC_HISTORY: "allergic", + OBGYN_HISTORY: "obgyn", PSICHIATRIC_HISTORY: "psichiatric", // TODO: Add other Navigation routes... }; @@ -132,6 +136,11 @@ export const DEFAULT_DASHBOARD_SIDEBAR_PROPS = { `${NAV_PATHS.UPDATE_PATIENT}/${UPDATE_PATIENT_NAV_PATHS.ALLERGIC_HISTORY}`, ); }, + navigateToObstetrics: (navigate) => { + navigate( + `${NAV_PATHS.UPDATE_PATIENT}/${UPDATE_PATIENT_NAV_PATHS.OBGYN_HISTORY}`, + ); + }, navigateToPsiquiatric: (navigate) => { navigate( `${NAV_PATHS.UPDATE_PATIENT}/${UPDATE_PATIENT_NAV_PATHS.PSICHIATRIC_HISTORY}`, @@ -275,6 +284,21 @@ const psichiatricHistoryView = ( ); +const obgynHistoryView = ( + + + +); + export const ROUTES = [ { path: NAV_PATHS.SEARCH_PATIENT, @@ -374,6 +398,10 @@ export const ROUTES = [ path: UPDATE_PATIENT_NAV_PATHS.ALLERGIC_HISTORY, element: allergicHistoryView, }, + { + path: UPDATE_PATIENT_NAV_PATHS.OBGYN_HISTORY, + element: obgynHistoryView, + }, { path: UPDATE_PATIENT_NAV_PATHS.PSICHIATRIC_HISTORY, element: psichiatricHistoryView, diff --git a/sanitas_frontend/src/views/History/ObGyn/index.jsx b/sanitas_frontend/src/views/History/ObGyn/index.jsx new file mode 100644 index 00000000..f173ef69 --- /dev/null +++ b/sanitas_frontend/src/views/History/ObGyn/index.jsx @@ -0,0 +1,1714 @@ +import { Suspense, useEffect, useState, useMemo } from "react"; +import "react-toastify/dist/ReactToastify.css"; +import { toast } from "react-toastify"; +import BaseButton from "src/components/Button/Base/index"; +import DashboardSidebar from "src/components/DashboardSidebar"; +import DropdownMenu from "src/components/DropdownMenu"; +import { BaseInput } from "src/components/Input/index"; +import { RadioInput } from "src/components/Input/index"; +import Throbber from "src/components/Throbber"; +import { colors, fonts, fontSize } from "src/theme.mjs"; +import WrapPromise from "src/utils/promiseWrapper"; +import { useRef } from "react"; +import CheckIcon from "@tabler/icons/outline/check.svg"; +import EditIcon from "@tabler/icons/outline/edit.svg"; +import CancelIcon from "@tabler/icons/outline/x.svg"; +import IconButton from "src/components/Button/Icon"; + +export function ObGynHistory({ + getBirthdayPatientInfo, + getObGynHistory, + updateObGynHistory, + sidebarConfig, + useStore, +}) { + const id = useStore((s) => s.selectedPatientId); + const [reload, setReload] = useState(false); // Controls reload toggling for refetching data + + const LoadingView = () => { + return ( + + ); + }; + + // biome-ignore lint/correctness/useExhaustiveDependencies: Reload the page + const birthdayResource = useMemo( + () => WrapPromise(getBirthdayPatientInfo(id)), + [id, reload, getBirthdayPatientInfo], + ); + // biome-ignore lint/correctness/useExhaustiveDependencies: Reload the page + const obgynHistoryResource = useMemo( + () => WrapPromise(getObGynHistory(id)), + [id, reload, getObGynHistory], + ); + + // Triggers a state change to force reloading of data + const triggerReload = () => { + setReload((prev) => !prev); + }; + + return ( +
+
+ +
+ +
+
+
+

+ Antecedentes Ginecoobstétricos +

+

+ Registro de antecedentes ginecoobstétricos +

+
+ +
+ }> + + +
+
+
+
+ ); +} + +function DiagnosisSection({ + title, + diagnosisKey, + editable, + isNew, + isFirstTime, + onCancel, + diagnosisDetails, + handleDiagnosedChange, +}) { + const [diagnosed, setDiagnosed] = useState(!!diagnosisDetails.medication); + const [diagnosisName, setDiagnosisName] = useState( + diagnosisDetails.illness || "", + ); + const [medication, setMedication] = useState( + diagnosisDetails.medication || "", + ); + const [dose, setDose] = useState(diagnosisDetails.dosage || ""); + const [frequency, setFrequency] = useState(diagnosisDetails.frequency || ""); + + const showFields = isNew || diagnosed; + + const handleDiagnosedChangeRef = useRef(handleDiagnosedChange); + handleDiagnosedChangeRef.current = handleDiagnosedChange; + + useEffect(() => { + const stableHandleDiagnosedChange = handleDiagnosedChangeRef.current; + stableHandleDiagnosedChange(diagnosisKey, true, { + illness: diagnosisName, + medication: medication, + dosage: dose, + frequency: frequency, + }); + }, [diagnosisName, medication, dose, frequency, diagnosisKey]); + + return ( +
+

+ {title} +

+ {!isNew && ( +
+ { + const newState = !diagnosed; + setDiagnosed(newState); + handleDiagnosedChange(diagnosisKey, newState, { + medication: medication, + dosage: dose, + frequency: frequency, + }); + }} + label="Sí" + disabled={editable} + /> + { + setDiagnosed(false); + handleDiagnosedChange(diagnosisKey, false); + setDiagnosisName(""); + setMedication(""); + setDose(""); + setFrequency(""); + }} + label="No" + disabled={editable} + /> +
+ )} + {showFields && ( + <> + {isNew && ( +
+

+ Nombre del diagnóstico: +

+ + setDiagnosisName(e.target.value)} + readOnly={editable} + placeholder="Ingrese el nombre del diagnóstico." + style={{ + width: "60%", + height: "3rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> +
+ )} + +

+ Medicamento: +

+ + setMedication(e.target.value)} + readOnly={editable} + placeholder="Ingrese el medicamento administrado." + style={{ + width: "60%", + height: "3rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> +

+ Dosis: +

+ setDose(e.target.value)} + readOnly={editable} + placeholder="Ingrese cuánto. Ej. 50mg (Este campo es opcional)" + style={{ + width: "60%", + height: "3rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> + +

+ Frecuencia: +

+ + setFrequency(e.target.value)} + readOnly={editable} + placeholder="Ingrese cada cuándo administra el medicamento (Ej. Cada dos días, cada 12 horas...)" + style={{ + width: "60%", + height: "3rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> + + {(isFirstTime || editable) && isNew && ( +
+
+ +
+
+ )} + + )} +
+ ); +} + +function OperationSection({ + title, + operationKey, + editable, + isFirstTime, + operationDetailsResource, + updateGlobalOperations, + handlePerformedChange, + birthdayResource, +}) { + const checkPerformed = (resource) => { + if (Array.isArray(resource)) { + return resource.length > 0; + } + if (typeof resource === "object" && resource !== null) { + return Object.keys(resource).length > 0 && resource.year !== null; + } + return !!resource; + }; + + const [performed, setPerformed] = useState(() => + checkPerformed(operationDetailsResource), + ); + const isArray = Array.isArray(operationDetailsResource); + const [operationDetails, setOperationDetails] = useState(() => + isArray ? operationDetailsResource : [operationDetailsResource], + ); + + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Ignoring complexity for this function + const handlePerformedChangeInternal = (newPerformedStatus) => { + setPerformed(newPerformedStatus); + handlePerformedChange(operationKey, newPerformedStatus); + + if (!newPerformedStatus) { + const clearedDetails = isArray ? [] : {}; + setOperationDetails(clearedDetails); + updateGlobalOperations(operationKey, clearedDetails); + } else { + if (isArray && (!operationDetails || operationDetails.length === 0)) { + const defaultDetail = { year: "", complications: false }; + const newDetails = [defaultDetail]; + setOperationDetails(newDetails); + updateGlobalOperations(operationKey, newDetails); + } else if ( + !isArray && + (!operationDetails || Object.keys(operationDetails).length === 0) + ) { + const defaultDetail = { year: "", complications: false }; + setOperationDetails([defaultDetail]); + updateGlobalOperations(operationKey, [defaultDetail]); + } + } + }; + + const addOperationDetail = () => { + if (!canAddMore()) return; + const newDetail = { year: null, complications: false }; + const newDetails = [...operationDetails, newDetail]; + setOperationDetails(newDetails); + updateGlobalOperations(operationKey, newDetails); + }; + + const canAddMore = () => { + if ( + operationKey === "breastMassResection" || + operationKey === "ovarianCysts" + ) { + if (operationKey === "breastMassResection") { + return operationDetails.length < 2; + } + return true; + } + return false; + }; + + const handleComplicationChange = (index, value) => { + const updatedDetails = [...operationDetails]; + updatedDetails[index].complications = value; + setOperationDetails(updatedDetails); + updateGlobalOperations(operationKey, updatedDetails); + }; + + const removeOperationDetail = (index) => { + if (operationDetails.length > 1) { + const newDetails = operationDetails.filter((_, idx) => idx !== index); + setOperationDetails(newDetails); + updateGlobalOperations(operationKey, newDetails); + } + }; + + const [yearOptions, setYearOptions] = useState([]); + + const birthYearResult = birthdayResource.read(); + + const birthYearData = birthYearResult.result; + const currentYear = new Date().getFullYear(); + const birthYear = birthYearData?.birthdate + ? new Date(birthYearData.birthdate).getUTCFullYear() + : null; + + useEffect(() => { + if (birthYear) { + const options = []; + for (let year = birthYear; year <= currentYear; year++) { + options.push({ value: year, label: year.toString() }); + } + setYearOptions(options); + } + }, [birthYear, currentYear]); + + const handleYearChange = (index, year) => { + const updatedDetails = [...operationDetails]; + updatedDetails[index].year = year || ""; + setOperationDetails(updatedDetails); + updateGlobalOperations(operationKey, updatedDetails); + }; + + return ( +
+

+ {title} +

+
+ handlePerformedChangeInternal(true)} + label="Sí" + disabled={editable} + /> + handlePerformedChangeInternal(false)} + label="No" + disabled={editable} + /> +
+ {performed && + Array.isArray(operationDetails) && + operationDetails.map((detail, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: The index is used to identify the operation details +
+ {index !== 0 && ( +
+
+
+ )} + +

+ ¿En qué año? +

+ handleYearChange(index, e.target.value)} + style={{ + container: { + width: "60%", + height: "10%", + paddingLeft: "0.5rem", + }, + select: {}, + option: {}, + indicator: { + top: "48%", + right: "4%", + }, + }} + /> + +

+ ¿Tuvo alguna complicación?: +

+
+ handleComplicationChange(index, true)} + label="Sí" + disabled={editable} + /> + handleComplicationChange(index, false)} + label="No" + disabled={editable} + /> +
+ {index !== 0 && (isFirstTime || !editable) ? ( +
+ removeOperationDetail(index)} + style={{ + width: "25%", + height: "3rem", + backgroundColor: "#fff", + color: colors.primaryBackground, + border: `1.5px solid ${colors.primaryBackground}`, + }} + /> +
+ ) : null} +
+ ))} + + {performed && canAddMore() && ( +
+ {(isFirstTime || !editable) && ( +
+
+
+ +
+
+ )} +
+ )} +
+ ); +} + +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Ignoring complexity for this function +function ObGynView({ + id, + birthdayResource, + obgynHistoryResource, + updateObGynHistory, + triggerReload, +}) { + const gynecologicalHistoryResult = obgynHistoryResource.read(); + + let errorMessage = ""; + if (gynecologicalHistoryResult.error) { + const error = gynecologicalHistoryResult.error; + if (error?.response) { + const { status } = error.response; + errorMessage = + status < 500 + ? "Ha ocurrido un error en la búsqueda, ¡Por favor vuelve a intentarlo!" + : "Ha ocurrido un error interno, lo sentimos."; + } else { + errorMessage = + "Ha ocurrido un error procesando tu solicitud, por favor vuelve a intentarlo."; + } + } + + const { + firstMenstrualPeriod = { data: { age: null } }, + regularCycles = { data: { isRegular: null } }, + painfulMenstruation = { data: { isPainful: null, medication: "" } }, + pregnancies = { + data: { + totalPregnancies: null, + abortions: null, + cesareanSections: null, + vaginalDeliveries: null, + }, + }, + diagnosedIllnesses = { + data: { + ovarianCysts: { + medication: { dosage: "", frequency: "", medication: "" }, + }, + uterineMyomatosis: { + medication: { dosage: "", frequency: "", medication: "" }, + }, + endometriosis: { + medication: { dosage: "", frequency: "", medication: "" }, + }, + otherCondition: { + medication: { + dosage: "", + frequency: "", + medication: "", + illness: "", + }, + }, + }, + }, + hasSurgeries = { + data: { + ovarianCystsSurgery: [{ year: null, complications: false }], + hysterectomy: { year: null, complications: false }, + sterilizationSurgery: { year: null, complications: false }, + breastResection: [{ year: null, complications: false }], + }, + }, + } = gynecologicalHistoryResult.result?.medicalHistory || {}; + + const [age, setAge] = useState( + firstMenstrualPeriod.data.age != null + ? firstMenstrualPeriod.data.age.toString() + : "", + ); + + const [isRegular, setIsRegular] = useState( + regularCycles.data.isRegular != null ? regularCycles.data.isRegular : false, + ); + + const [isPainful, setIsPainful] = useState( + painfulMenstruation.data.isPainful != null + ? painfulMenstruation.data.isPainful + : false, + ); + + const [medication, setMedication] = useState( + painfulMenstruation.data.medication != null + ? painfulMenstruation.data.medication + : "", + ); + + const isFirstTime = !( + gynecologicalHistoryResult.result?.medicalHistory?.firstMenstrualPeriod + ?.data?.age || + gynecologicalHistoryResult.result?.medicalHistory?.diagnosedIllnesses?.data + .length || + gynecologicalHistoryResult.result?.medicalHistory?.hasSurgeries?.data.length + ); + + const [isEditable, setIsEditable] = useState(isFirstTime); + + // TOTAL P SECTION + + const [P, setP] = useState( + pregnancies.data.vaginalDeliveries != null + ? pregnancies.data.vaginalDeliveries + : 0, + ); + const [C, setC] = useState( + pregnancies.data.cesareanSections != null + ? pregnancies.data.cesareanSections + : 0, + ); + const [A, setA] = useState( + pregnancies.data.abortions != null ? pregnancies.data.abortions : 0, + ); // Abortos + const [G, setG] = useState(0); + + useEffect(() => { + setG(P + C + A); + }, [P, C, A]); + + // GENERAL INFO SECTION + + // RENDER BASE INPUT IF MENSTRUATION IS PAINFUL + const renderMedicationInput = () => { + if (isPainful) { + return ( +
+

+ ¿Qué medicamento toma? +

+ setMedication(e.target.value)} + readOnly={!isEditable} + placeholder="Ingrese el medicamento tomado para regular los dolores de menstruación." + style={{ + width: "60%", + height: "3rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> +
+ ); + } + return null; + }; + + // DIAGNOSIS SECTION + + const [diagnoses, setDiagnoses] = useState(() => { + const initialDiagnoses = [ + { + key: "ovarianCysts", + title: "Diagnóstico por Quistes Ováricos:", + active: true, + details: diagnosedIllnesses.data.ovarianCysts?.medication || {}, + }, + { + key: "uterineMyomatosis", + title: "Diagnóstico por Miomatosis Uterina:", + active: true, + details: diagnosedIllnesses.data.uterineMyomatosis?.medication || {}, + }, + { + key: "endometriosis", + title: "Diagnóstico por Endometriosis:", + active: true, + details: diagnosedIllnesses.data.endometriosis?.medication || {}, + }, + ]; + + const otherConditions = diagnosedIllnesses.data?.otherCondition ?? []; + for (const condition of otherConditions) { + initialDiagnoses.push({ + key: condition.medication.illness, + title: `Nuevo Diagnóstico: ${condition.medication.illness}`, + isNew: false, + active: true, + details: condition.medication, + }); + } + + return initialDiagnoses; + }); + + const addDiagnosis = () => { + const diagnosisCount = diagnoses.length + 1; + + const newDiagnosis = { + key: `Nuevo Diagnóstico ${diagnosisCount}`, + title: `Nuevo Diagnóstico ${diagnosisCount}`, + isNew: true, + active: true, + details: { + illness: "", + medication: "", + dosage: "", + frequency: "", + }, + }; + + setDiagnoses((prevDiagnoses) => [...prevDiagnoses, newDiagnosis]); + }; + + const removeDiagnosis = (key) => { + setDiagnoses(diagnoses.filter((diagnosis) => diagnosis.key !== key)); + }; + + const getDiagnosisDetails = (key) => { + const diagnosis = diagnoses.find((d) => d.key === key); + if (!diagnosis?.active) { + return { dosage: "", frequency: "", medication: "", illness: "" }; + } + + if (["ovarianCysts", "uterineMyomatosis", "endometriosis"].includes(key)) { + return ( + diagnosedIllnesses.data[key]?.medication || { + dosage: "", + frequency: "", + medication: "", + illness: "", + } + ); + } + + const condition = diagnosedIllnesses.data.otherCondition?.find( + (cond) => cond.medication.illness === key, + ); + + return condition?.medication + ? { + dosage: condition.medication.dosage || "", + frequency: condition.medication.frequency || "", + medication: condition.medication.medication || "", + illness: condition.medication.illness || "", + } + : { + dosage: "", + frequency: "", + medication: "", + illness: "", + }; + }; + + const handleDiagnosedChange = (diagnosisKey, isActive, newDetails = {}) => { + setDiagnoses((prevDiagnoses) => + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Ignoring complexity for this function + prevDiagnoses.map((diagnosis) => { + if (diagnosis.key === diagnosisKey) { + if (!isActive) { + return { + ...diagnosis, + active: false, + details: { medication: "", dosage: "", frequency: "" }, + }; + // biome-ignore lint/style/noUselessElse: Handles the data structure of the diagnoses for static ones and the new diagnostics + } else { + const updatedDetails = { + medication: + newDetails.medication || diagnosis.details.medication || "", + dosage: newDetails.dosage || diagnosis.details.dosage || "", + frequency: + newDetails.frequency || diagnosis.details.frequency || "", + }; + + if ( + diagnosis.isNew && + diagnosis.key.startsWith("Nuevo Diagnóstico") + ) { + updatedDetails.illness = + newDetails.illness || diagnosis.details.illness || ""; + } + + return { + ...diagnosis, + active: true, + details: updatedDetails, + }; + } + } + return diagnosis; + }), + ); + }; + + // OPERACIONES SECTION + + const [operations, setOperations] = useState(() => { + const initialOperations = [ + { + key: "hysterectomy", + title: "Operación por Histerectomía:", + details: hasSurgeries.data.hysterectomy || {}, + }, + { + key: "sterilization", + title: "Cirugía para no tener más hijos:", + details: hasSurgeries.data.sterilizationSurgery || {}, + }, + { + key: "ovarianCysts", + title: "Operación por Quistes Ováricos:", + details: hasSurgeries.data.ovarianCystsSurgery || [], + }, + { + key: "breastMassResection", + title: "Operación por Resección de masas en mamas:", + details: hasSurgeries.data.breastMassResection || [], + }, + ]; + + return initialOperations; + }); + + const mapOperationDetails = (operationKey) => { + const operation = operations.find((op) => op.key === operationKey); + if (!operation) return {}; + return operation.details; + }; + + const updateGlobalOperations = (operationKey, newDetails) => { + setOperations( + operations.map((op) => { + if (op.key === operationKey) { + return { ...op, details: newDetails }; + } + return op; + }), + ); + }; + + const handlePerformedChange = (operationKey, isPerformed) => { + setOperations((prevOperations) => + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Ignoring complexity for this function + prevOperations.map((operation) => { + if (operation.key === operationKey) { + const clearedDetails = isPerformed + ? operation.details + : operation.key === "ovarianCysts" || + operation.key === "breastMassResection" + ? [] + : {}; + + return { + ...operation, + details: clearedDetails, + }; + } + return operation; + }), + ); + }; + + const validateGynecologicalHistory = () => { + if (!age || Number.isNaN(age) || Number(age) < 1) { + toast.error("Por favor, ingrese una edad válida."); + return false; + } + + if ( + !Number.isInteger(G) || + G < 0 || + !Number.isInteger(C) || + C < 0 || + !Number.isInteger(A) || + A < 0 || + G < C + A + ) { + toast.error("Por favor, ingrese datos válidos en la sección de partos."); + return false; + } + + if (isPainful && medication.trim() === "") { + toast.error( + "Por favor, complete los detalles del medicamento para la menstruación dolorosa.", + ); + return false; + } + + const invalidDiagnosis = diagnoses.some((diagnosis) => { + const { medication, dosage, frequency } = diagnosis.details; + + if (diagnosis.active) { + const initialDetails = getDiagnosisDetails(diagnosis.key); + const isInitiallyFilled = (field) => + initialDetails[field] && initialDetails[field].trim() !== ""; + + return ( + (isInitiallyFilled("medication") && medication.trim() === "") || + (isInitiallyFilled("dosage") && dosage.trim() === "") || + (isInitiallyFilled("frequency") && frequency.trim() === "") + ); + } + return false; + }); + + if (invalidDiagnosis) { + toast.error( + "Por favor, complete los detalles de todos los diagnósticos activos.", + ); + return false; + } + + const invalidOperation = operations.some((operation) => { + const details = Array.isArray(operation.details) + ? operation.details + : [operation.details]; + return details.some((detail) => detail.year === null); + }); + + if (invalidOperation) { + toast.error("Por favor, complete los detalles de todas las operaciones."); + return false; + } + + return true; + }; + const structuredOperations = operations.reduce((acc, operation) => { + const details = mapOperationDetails(operation.key); + + acc[operation.key] = Array.isArray(details) + ? details.map(({ year = null, complications = false }) => ({ + year, + complications, + })) + : { ...details }; + + return acc; + }, {}); + + const fixedDiagnosesKeys = [ + "ovarianCysts", + "uterineMyomatosis", + "endometriosis", + ]; + + // Canceling update + const handleCancel = () => { + setIsEditable(false); + toast.info("Edición cancelada."); + }; + + const formattedDiagnosedIllnesses = { + version: diagnosedIllnesses?.version || 1, + data: diagnoses.reduce((acc, diagnosis) => { + if (fixedDiagnosesKeys.includes(diagnosis.key)) { + acc[diagnosis.key] = { + medication: { ...diagnosis.details }, + }; + } else { + acc.otherCondition = acc.otherCondition || []; + acc.otherCondition.push({ + medication: { + illness: diagnosis.key, + ...diagnosis.details, + }, + }); + } + return acc; + }, {}), + }; + + const handleSaveGynecologicalHistory = async () => { + if (!validateGynecologicalHistory()) { + return; + } + + const medicalHistory = { + firstMenstrualPeriod: { + version: firstMenstrualPeriod?.version || 1, + data: { + age: age, + }, + }, + regularCycles: { + version: regularCycles?.version || 1, + data: { + isRegular: isRegular, + }, + }, + painfulMenstruation: { + version: painfulMenstruation?.version || 1, + data: { + isPainful: isPainful, + medication: medication, + }, + }, + pregnancies: { + version: pregnancies?.version || 1, + data: { + totalPregnancies: G, + vaginalDeliveries: P, + cesareanSections: C, + abortions: A, + }, + }, + diagnosedIllnesses: formattedDiagnosedIllnesses, + hasSurgeries: { + version: hasSurgeries?.version || 1, + data: { + hysterectomy: structuredOperations.hysterectomy || {}, + sterilizationSurgery: structuredOperations.sterilization || {}, + ovarianCystsSurgery: structuredOperations.ovarianCysts || null, + breastMassResection: structuredOperations.breastMassResection || [], + }, + }, + }; + + toast.info("Guardando antecedente ginecoobstétrico..."); + + const result = await updateObGynHistory(id, medicalHistory); + if (!result.error) { + toast.success("Antecedentes ginecoobstétricos actualizados con éxito."); + triggerReload(); + setIsEditable(false); + } else { + toast.error( + `Error al actualizar los antecedentes ginecoobstétricos: ${result.error}`, + ); + } + }; + + useEffect(() => { + console.log("isEditable changed to:", isEditable); + }, [isEditable]); + + return ( +
+
+ {errorMessage ? ( +
+ {errorMessage} +
+ ) : ( + <> + {isFirstTime && ( +
+ Por favor, ingrese los datos del paciente. Parece que es su + primera visita aquí. +
+ )} + +
+

+ Información General:{" "} +

+ + {!isFirstTime && + (isEditable ? ( +
+ + +
+ ) : ( + setIsEditable(true)} + /> + ))} +
+ +

+ Ingrese la edad en la que tuvo la primera mestruación: +

+ setAge(e.target.value)} + readOnly={!isEditable} + placeholder="Ingrese la edad (Ej. 15, 16...)" + style={{ + width: "60%", + height: "3rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> +

+ ¿Sus ciclos son regulares? (Ciclos de 21-35 días) +

+
+ setIsRegular(true)} + label="Sí" + disabled={!isEditable} + /> + setIsRegular(false)} + label="No" + disabled={!isEditable} + /> +
+

+ ¿Normalmente tiene menstruación dolorosa? +

+
+ setIsPainful(true)} + label="Sí" + disabled={!isEditable} + /> + { + setIsPainful(false); + setMedication(""); + }} + label="No" + disabled={!isEditable} + /> +
+ + {renderMedicationInput()} + +
+
+
+
+
+

+ G: +

+ +
+

+ {" "} + ={" "} +

+
+

+ P: +

+ setP(Number(e.target.value) || 0)} + readOnly={!isEditable} + placeholder="# vía vaginal" + style={{ + width: "100%", + height: "2.5rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> +
+

+ {" "} + +{" "} +

+
+

+ C: +

+ setC(Number(e.target.value) || 0)} + readOnly={!isEditable} + placeholder="# cesáreas" + style={{ + width: "100%", + height: "2.5rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> +
+

+ {" "} + +{" "} +

+
+

+ A: +

+ setA(Number(e.target.value) || 0)} + readOnly={!isEditable} + placeholder="# abortos" + style={{ + width: "100%", + height: "2.5rem", + fontFamily: fonts.textFont, + fontSize: "1rem", + }} + /> +
+
+ +
+ +
+

+ Diagnóstico de Enfermedades{" "} +

+ + {diagnoses.map((diagnosis, index) => ( +
+ removeDiagnosis(diagnosis.key)} + diagnosisDetails={getDiagnosisDetails(diagnosis.key)} + handleDiagnosedChange={handleDiagnosedChange} + isFirstTime={isFirstTime} + /> + {index < diagnoses.length - 1 && ( +
+ )} +
+ ))} +
+ + {(isFirstTime || isEditable) && ( +
+
+ +
+ +
+
+ )} + +
+

+ Operaciones del Paciente:{" "} +

+ + {operations.map((operation, index) => ( +
+ + {index < operations.length - 1 && ( +
+ )} +
+ ))} +
+ + {isFirstTime && ( +
+ +
+ )} +
+
+ + )} +
+
+ ); +} diff --git a/sanitas_frontend/src/views/History/ObGyn/index.stories.jsx b/sanitas_frontend/src/views/History/ObGyn/index.stories.jsx new file mode 100644 index 00000000..31b4c555 --- /dev/null +++ b/sanitas_frontend/src/views/History/ObGyn/index.stories.jsx @@ -0,0 +1,232 @@ +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { ObGynHistory } from "src/views/History/ObGyn"; +import { createEmptyStore } from "src/store.mjs"; + +// Mock functions and data +const mockGetBirthdayPatientInfo = async () => ({ + result: { + birthdate: "1980-01-01", + }, +}); + +const mockGetObGynHistoryWithData = async () => ({ + result: { + medicalHistory: { + firstMenstrualPeriod: { data: { age: 15 } }, + regularCycles: { data: { isRegular: true } }, + painfulMenstruation: { data: { isPainful: false, medication: "" } }, + pregnancies: { + data: { + totalPregnancies: 2, + abortions: 0, + cesareanSections: 1, + vaginalDeliveries: 1, + }, + }, + diagnosedIllnesses: { + data: { + ovarianCysts: { + medication: { + dosage: "100mg", + frequency: "Daily", + medication: "Ibuprofen", + }, + }, + uterineMyomatosis: { + medication: { + dosage: "50mg", + frequency: "Twice a day", + medication: "Paracetamol", + }, + }, + endometriosis: { + medication: { dosage: "", frequency: "", medication: "" }, + }, + otherCondition: [ + { + medication: { + illness: "illness A", + medication: "Med D", + dosage: "500mg", + frequency: "Once a day", + }, + }, + ], + }, + }, + hasSurgeries: { + data: { + ovarianCystsSurgery: [{ year: 2005, complications: false }], + hysterectomy: { year: 2010, complications: true }, + sterilizationSurgery: { year: null, complications: false }, + breastMassResection: [{ year: 2015, complications: false }], + }, + }, + }, + }, +}); + +const mockGetObGynHistoryEmpty = async () => ({ + result: { + medicalHistory: { + firstMenstrualPeriod: { data: { age: null } }, + regularCycles: { data: { isRegular: null } }, + painfulMenstruation: { data: { isPainful: null, medication: "" } }, + pregnancies: { + data: { + totalPregnancies: null, + abortions: null, + cesareanSections: null, + vaginalDeliveries: null, + }, + }, + diagnosedIllnesses: { + data: { + ovarianCysts: {}, + uterineMyomatosis: {}, + endometriosis: {}, + otherCondition: [], + }, + }, + hasSurgeries: { + data: { + ovarianCystsSurgery: [], + hysterectomy: {}, + sterilizationSurgery: {}, + breastMassResection: [], + }, + }, + }, + }, +}); + +const mockGetObGynHistoryError = async () => ({ + result: { + medicalHistory: { + firstMenstrualPeriod: { data: { age: 15 } }, + regularCycles: { data: { isRegular: true } }, + painfulMenstruation: { data: { isPainful: false, medication: "" } }, + pregnancies: { + data: { + totalPregnancies: 2, + abortions: 0, + cesareanSections: 1, + vaginalDeliveries: 1, + }, + }, + diagnosedIllnesses: { + data: { + ovarianCysts: { + medication: { + dosage: "100mg", + frequency: "Daily", + medication: "Ibuprofen", + }, + }, + uterineMyomatosis: { + medication: { + dosage: "50mg", + frequency: "Twice a day", + medication: "Paracetamol", + }, + }, + endometriosis: { + medication: { dosage: "", frequency: "", medication: "" }, + }, + otherCondition: [ + { + medication: { + illness: "illness A", + medication: "Med D", + dosage: "500mg", + frequency: "Once a day", + }, + }, + ], + }, + }, + hasSurgeries: { + data: { + ovarianCystsSurgery: [{ year: 2005, complications: false }], + hysterectomy: { year: 2010, complications: true }, + sterilizationSurgery: { year: null, complications: false }, + breastMassResection: [{ year: 2015, complications: false }], + }, + }, + }, + }, + error: { + response: { + status: 400, + statusText: "Bad Request", + data: "Invalid request parameters.", + }, + }, +}); + +const mockUpdateObGynHistory = async () => ({ + success: true, +}); + +const store = createEmptyStore({ + selectedPatientId: 12345, // Mock patient ID +}); + +export default { + title: "Views/Antecedents/ObGynHistory", + component: ObGynHistory, + decorators: [ + (Story) => ( + + + } /> + + + ), + ], +}; + +export const WithData = { + args: { + getBirthdayPatientInfo: mockGetBirthdayPatientInfo, + getObGynHistory: mockGetObGynHistoryWithData, + updateObGynHistory: mockUpdateObGynHistory, + sidebarConfig: { + userInformation: { + displayName: "Dr. Jane Doe", + title: "Ginecóloga", + }, + }, + useStore: () => ({ selectedPatientId: store.selectedPatientId }), + }, +}; + +export const EmptyData = { + args: { + getBirthdayPatientInfo: mockGetBirthdayPatientInfo, + getObGynHistory: mockGetObGynHistoryEmpty, + updateObGynHistory: mockUpdateObGynHistory, + sidebarConfig: { + userInformation: { + displayName: "Dr. Jane Doe", + title: "Ginecóloga", + }, + }, + useStore: () => ({ selectedPatientId: store.selectedPatientId }), + }, +}; + +export const ErrorState = { + args: { + getBirthdayPatientInfo: mockGetBirthdayPatientInfo, + getObGynHistory: mockGetObGynHistoryError, + updateObGynHistory: mockUpdateObGynHistory, + sidebarConfig: { + userInformation: { + displayName: "Dr. Jane Doe", + title: "Ginecóloga", + }, + }, + useStore: () => ({ selectedPatientId: store.selectedPatientId }), + }, +}; diff --git a/sanitas_frontend/src/views/History/ObGyn/index.test.jsx b/sanitas_frontend/src/views/History/ObGyn/index.test.jsx new file mode 100644 index 00000000..a22ec1a9 --- /dev/null +++ b/sanitas_frontend/src/views/History/ObGyn/index.test.jsx @@ -0,0 +1,304 @@ +import { + render, + screen, + fireEvent, + waitForElementToBeRemoved, + waitFor, +} from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, test, vi } from "vitest"; +import { toast } from "react-toastify"; +import { ObGynHistory } from "."; + +vi.mock("react-toastify", () => ({ + toast: { + error: vi.fn(), + success: vi.fn(), + info: vi.fn(), + }, +})); + +const mockGetBirthdayPatientInfo = async () => ({ + result: { + birthdate: "1980-01-01", + }, +}); + +const mockGetObGynHistoryWithData = async () => ({ + result: { + medicalHistory: { + firstMenstrualPeriod: { data: { age: 15 } }, + regularCycles: { data: { isRegular: true } }, + painfulMenstruation: { data: { isPainful: false, medication: "" } }, + pregnancies: { + data: { + totalPregnancies: 2, + abortions: 0, + cesareanSections: 1, + vaginalDeliveries: 1, + }, + }, + diagnosedIllnesses: { + data: { + ovarianCysts: { + medication: { + dosage: "100mg", + frequency: "Daily", + medication: "Ibuprofen", + }, + }, + uterineMyomatosis: { + medication: { + dosage: "50mg", + frequency: "Twice a day", + medication: "Paracetamol", + }, + }, + endometriosis: { + medication: { dosage: "", frequency: "", medication: "" }, + }, + otherCondition: [ + { + medication: { + illness: "illness A", + medication: "Med D", + dosage: "500mg", + frequency: "Once a day", + }, + }, + ], + }, + }, + hasSurgeries: { + data: { + ovarianCystsSurgery: [{ year: 2005, complications: false }], + hysterectomy: { year: 2010, complications: true }, + sterilizationSurgery: { year: 2015, complications: false }, + breastMassResection: [{ year: 2015, complications: false }], + }, + }, + }, + }, +}); + +const mockGetObGynHistoryError = async () => ({ + result: { + medicalHistory: { + firstMenstrualPeriod: { data: { age: 15 } }, + regularCycles: { data: { isRegular: true } }, + painfulMenstruation: { data: { isPainful: false, medication: "" } }, + pregnancies: { + data: { + totalPregnancies: 2, + abortions: 0, + cesareanSections: 1, + vaginalDeliveries: 1, + }, + }, + diagnosedIllnesses: { + data: { + ovarianCysts: { + medication: { + dosage: "100mg", + frequency: "Daily", + medication: "Ibuprofen", + }, + }, + uterineMyomatosis: { + medication: { + dosage: "50mg", + frequency: "Twice a day", + medication: "Paracetamol", + }, + }, + endometriosis: { + medication: { dosage: "", frequency: "", medication: "" }, + }, + otherCondition: [ + { + medication: { + illness: "illness A", + medication: "Med D", + dosage: "500mg", + frequency: "Once a day", + }, + }, + ], + }, + }, + hasSurgeries: { + data: { + ovarianCystsSurgery: [{ year: 2005, complications: false }], + hysterectomy: { year: 2010, complications: true }, + sterilizationSurgery: { year: 2015, complications: false }, + breastMassResection: [{ year: 2015, complications: false }], + }, + }, + }, + }, + error: { + response: { + status: 400, + statusText: "Bad Request", + data: "Invalid request parameters.", + }, + }, +}); + +const mockUpdateObGynHistory = vi.fn(() => Promise.resolve({ success: true })); +const mockUseStore = vi.fn().mockReturnValue({ selectedPatientId: "12345" }); + +const Wrapper = ({ children }) => {children}; + +describe("ObGynHistory Component Tests", () => { + test("renders and displays general information", async () => { + render( + + + , + ); + + await waitFor(() => + expect( + screen.queryByText( + "Cargando información de los antecedentes ginecoobstétricos...", + ), + ).not.toBeInTheDocument(), + ); + + expect( + screen.getByText("Antecedentes Ginecoobstétricos"), + ).toBeInTheDocument(); + expect( + screen.getByText("Registro de antecedentes ginecoobstétricos"), + ).toBeInTheDocument(); + + const diagnosisText = document + .querySelector("div") + .textContent.includes("Diagnóstico por Quistes Ováricos:"); + expect(diagnosisText).toBe(true); + }); + + test("handles the addition of a new diagnosis", async () => { + render( + + + , + ); + + await waitForElementToBeRemoved(() => + screen.queryByText( + "Cargando información de los antecedentes ginecoobstétricos...", + ), + ); + + const allIcons = await screen.findAllByRole("img", { name: "Icon" }); + const editIcon = allIcons.find((icon) => + icon.src.includes("outline/edit.svg"), + ); + const editButton = editIcon.closest("button"); + fireEvent.click(editButton); + //fireEvent.click(editButtons[0]); + + const addButton = await screen.findByText("Agregar otro diagnóstico"); + fireEvent.click(addButton); + + const diagnosisInput = await screen.findByPlaceholderText( + "Ingrese el nombre del diagnóstico.", + ); + expect(diagnosisInput).toBeInTheDocument(); + }); + + test("displays error message when there is an error fetching data", async () => { + render( + + + , + ); + + await waitForElementToBeRemoved(() => + screen.queryByText( + "Cargando información de los antecedentes ginecoobstétricos...", + ), + ); + + await waitFor(() => { + const errorMessage = screen.getByText( + /Ha ocurrido un error en la búsqueda, ¡Por favor vuelve a intentarlo!/i, + ); + expect(errorMessage).toBeInTheDocument(); + }); + }); + + test("saves gynecological history successfully", async () => { + render( + + + , + ); + + await waitFor( + () => { + const loadingMessage = screen.queryByText( + "Cargando información de los antecedentes ginecoobstétricos...", + ); + expect(loadingMessage).not.toBeInTheDocument(); + }, + { timeout: 5000 }, + ); + + const allIconsBeforeEdit = await screen.findAllByRole("img", { + name: "Icon", + }); + const editIcon = allIconsBeforeEdit.find((icon) => + icon.src.includes("outline/edit.svg"), + ); + const editButton = editIcon.closest("button"); + fireEvent.click(editButton); + + const allIconsAfterEdit = await screen.findAllByRole("img", { + name: "Icon", + }); + const checkIcon = allIconsAfterEdit.find((icon) => + icon.src.includes("outline/check.svg"), + ); + const checkButton = checkIcon.closest("button"); + fireEvent.click(checkButton); + + await waitFor(() => + expect(toast.success).toHaveBeenCalledWith( + "Antecedentes ginecoobstétricos actualizados con éxito.", + ), + ); + }); +});