From 1b75a80b6be8217ebec28ba8d7ea9abbb147fdea Mon Sep 17 00:00:00 2001 From: ucabrera Date: Wed, 24 Jun 2026 11:39:25 -0300 Subject: [PATCH 1/2] feat(case): add functionality to close cases from the events table --- frontend/public/locales/en/translation.json | 7 ++ frontend/public/locales/es/translation.json | 7 ++ frontend/src/api/services/cases.jsx | 19 ++- .../src/views/case/components/TableCase.jsx | 118 +++++++++++++++++- 4 files changed, 147 insertions(+), 4 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 52211075..ab90ed5b 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -106,6 +106,11 @@ "ngen.auth.register.success": "Successful registration", "ngen.cancel": "Cancel", "ngen.case.addto": "Add to a case", + "ngen.case.close": "Close case", + "ngen.case.close.confirm": "Are you sure you want to close this case?", + "ngen.case.close.warning": "This case has {{count}} associated events. Closing it will affect all of them.", + "ngen.case.close.no_transition": "No transition available to close the case from its current state.", + "ngen.case.close.already_closed": "The case is already closed.", "ngen.case.create": "Create case", "ngen.case.error": "Unable to retrieve cases information", "ngen.case.info.error": "Unable to retrieve case information", @@ -160,6 +165,8 @@ "ngen.contactcheck.public.send": "Send Confirmation", "ngen.contact_one": "Contact", "ngen.contact_other": "Contacts", + "ngen.close.case.error": "The case could not be closed", + "ngen.close.case.success": "The case was successfully closed", "ngen.create.case.error": "The case could not be created", "ngen.create.case.error.network": "Parent network is invalid", "ngen.create.case.error.user": "Assigned user is invalid", diff --git a/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json index dd1a086a..2498758f 100644 --- a/frontend/public/locales/es/translation.json +++ b/frontend/public/locales/es/translation.json @@ -106,6 +106,11 @@ "ngen.auth.register.success": "Registro exitoso", "ngen.cancel": "Cancelar", "ngen.case.addto": "Agregar a un caso", + "ngen.case.close": "Cerrar caso", + "ngen.case.close.confirm": "¿Está seguro de que desea cerrar este caso?", + "ngen.case.close.warning": "Este caso tiene {{count}} eventos asociados. Al cerrarlo se afectarán todos.", + "ngen.case.close.no_transition": "No hay una transición disponible para cerrar el caso desde su estado actual.", + "ngen.case.close.already_closed": "El caso ya se encuentra cerrado.", "ngen.case.create": "Crear caso", "ngen.case.error": "No se puede recuperar la información de los casos", "ngen.case.info.error": "No se puede recuperar la información del caso", @@ -160,6 +165,8 @@ "ngen.contactcheck.public.send": "Enviar confirmación", "ngen.contact_one": "Contacto", "ngen.contact_other": "Contactos", + "ngen.close.case.error": "No se pudo cerrar el caso", + "ngen.close.case.success": "El caso fue cerrado exitosamente", "ngen.create.case.error": "No se pudo crear el caso", "ngen.create.case.error.network": "La red principal no es válida", "ngen.create.case.error.user": "El usuario asignado no es válido", diff --git a/frontend/src/api/services/cases.jsx b/frontend/src/api/services/cases.jsx index c1e27cd2..5e8f3233 100644 --- a/frontend/src/api/services/cases.jsx +++ b/frontend/src/api/services/cases.jsx @@ -180,4 +180,21 @@ const mergeCase = (urlParent, urlChildren) => { }); }; -export { getCases, getAllCases, getOrderingCases, getCase, postCase, putCase, deleteCase, mergeCase, patchCase, getMinifiedCase }; +const patchCaseState = (url, stateUrl) => { + let messageSuccess = i18next.t("ngen.close.case.success"); + let messageError = i18next.t("ngen.close.case.error") + " ."; + return apiInstance + .patch(url, { state: stateUrl }) + .then((response) => { + setAlert(messageSuccess, "success", "case"); + return response; + }) + .catch((error) => { + let statusText = error.response.statusText; + messageError += statusText; + setAlert(messageError, "error", "case"); + return Promise.reject(error); + }); +}; + +export { getCases, getAllCases, getOrderingCases, getCase, postCase, putCase, deleteCase, mergeCase, patchCase, getMinifiedCase, patchCaseState }; diff --git a/frontend/src/views/case/components/TableCase.jsx b/frontend/src/views/case/components/TableCase.jsx index 262d92e5..0e136c60 100644 --- a/frontend/src/views/case/components/TableCase.jsx +++ b/frontend/src/views/case/components/TableCase.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; -import { Form, Row, Spinner, Table } from "react-bootstrap"; +import { Alert, Button, Form, Modal, Row, Spinner, Table } from "react-bootstrap"; import CrudButton from "components/Button/CrudButton"; -import { deleteCase } from "api/services/cases"; +import { deleteCase, patchCaseState } from "api/services/cases"; import ModalConfirm from "components/Modal/ModalConfirm"; import Ordering from "components/Ordering/Ordering"; import LetterFormat from "components/LetterFormat"; @@ -15,6 +15,8 @@ import PriorityComponent from "../../tanstackquery/PriorityComponent"; import StateComponent from "../../tanstackquery/StateComponent"; import EventComponent from "views/tanstackquery/EventComponent"; import TaxonomyComponent from "views/tanstackquery/TaxonomyComponent"; +import { getState } from "api/services/states"; +import apiInstance from "api/api"; @@ -52,6 +54,7 @@ const TableCase = ({ disableUuid, disableDateModified, disableEvents, + disableCloseCase, basePath = "" }) => { const [url, setUrl] = useState(null); @@ -62,14 +65,91 @@ const TableCase = ({ const [isCheckAll, setIsCheckAll] = useState(false); const [showFullUuid, setShowFullUuid] = useState(false); const [list, setList] = useState([]); + const [showCloseCaseModal, setShowCloseCaseModal] = useState(false); + const [closeCaseInfo, setCloseCaseInfo] = useState(null); + const [closingCase, setClosingCase] = useState(false); + const [solvedCases, setSolvedCases] = useState(new Set()); const { t } = useTranslation(); - //ORDER useEffect(() => { setList(cases); + const stateUrlToCases = {}; + cases.forEach((c) => { + if (c?.state) { + if (!stateUrlToCases[c.state]) stateUrlToCases[c.state] = []; + stateUrlToCases[c.state].push(c.url); + } + }); + const stateUrls = Object.keys(stateUrlToCases); + if (!stateUrls.length) { setSolvedCases(new Set()); return; } + let cancelled = false; + Promise.all(stateUrls.map((url) => apiInstance.get(url).catch(() => null))) + .then((responses) => { + if (!cancelled) { + const solved = new Set(); + responses.forEach((r, i) => { + if (r?.data?.solved) { + stateUrlToCases[stateUrls[i]].forEach((caseUrl) => solved.add(caseUrl)); + } + }); + setSolvedCases(solved); + } + }) + .catch(() => {}); + return () => { cancelled = true; }; }, [cases]); + const handleCloseCaseClick = async (caseItem) => { + setClosingCase(true); + try { + const stateResponse = await getState(caseItem.state); + const stateData = stateResponse.data; + + if (stateData.solved) { + alert(t("ngen.case.close.already_closed")); + return; + } + + let targetStateUrl = null; + for (const childUrl of (stateData.children || [])) { + const childResponse = await getState(childUrl); + if (childResponse.data.solved) { + targetStateUrl = childResponse.data.url; + break; + } + } + + if (!targetStateUrl) { + alert(t("ngen.case.close.no_transition")); + return; + } + + setCloseCaseInfo({ + caseUrl: caseItem.url, + eventCount: caseItem.events?.length ?? caseItem.events_count ?? 0, + targetStateUrl + }); + setShowCloseCaseModal(true); + } catch (error) { + console.error(error); + } finally { + setClosingCase(false); + } + }; + + const handleCloseCaseConfirm = () => { + patchCaseState(closeCaseInfo.caseUrl, closeCaseInfo.targetStateUrl) + .then((response) => { + setIfModify(response); + setShowCloseCaseModal(false); + setCloseCaseInfo(null); + }) + .catch((error) => { + console.error(error); + }); + }; + const storageCaseUrl = (url) => { localStorage.removeItem("case"); localStorage.removeItem("navigation"); @@ -326,6 +406,17 @@ const TableCase = ({ ) : ( Delete(caseItem.url)} permissions="delete_case" /> ))} + {!disableCloseCase && ( + + )} ); @@ -340,6 +431,27 @@ const TableCase = ({ onHide={() => setModalDelete(false)} ifConfirm={() => removeCase(url)} /> + setShowCloseCaseModal(false)} centered> + + {t("ngen.case.close")} + + + {closeCaseInfo?.eventCount > 1 && ( + + {t("ngen.case.close.warning", { count: closeCaseInfo.eventCount })} + + )} +

{t("ngen.case.close.confirm")}

+
+ + + + +
); }; From b16b3e75aeef1fdf3203a2480cd52cb4c3a24b86 Mon Sep 17 00:00:00 2001 From: ucabrera Date: Wed, 24 Jun 2026 12:08:11 -0300 Subject: [PATCH 2/2] feat(case): add error handling for closing cases and update alerts --- frontend/public/locales/en/translation.json | 1 + frontend/public/locales/es/translation.json | 1 + .../src/views/case/components/TableCase.jsx | 20 +++++++++++++++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index ab90ed5b..cc686146 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -111,6 +111,7 @@ "ngen.case.close.warning": "This case has {{count}} associated events. Closing it will affect all of them.", "ngen.case.close.no_transition": "No transition available to close the case from its current state.", "ngen.case.close.already_closed": "The case is already closed.", + "ngen.case.close.error": "An error occurred while closing the case.", "ngen.case.create": "Create case", "ngen.case.error": "Unable to retrieve cases information", "ngen.case.info.error": "Unable to retrieve case information", diff --git a/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json index 2498758f..445e2304 100644 --- a/frontend/public/locales/es/translation.json +++ b/frontend/public/locales/es/translation.json @@ -111,6 +111,7 @@ "ngen.case.close.warning": "Este caso tiene {{count}} eventos asociados. Al cerrarlo se afectarán todos.", "ngen.case.close.no_transition": "No hay una transición disponible para cerrar el caso desde su estado actual.", "ngen.case.close.already_closed": "El caso ya se encuentra cerrado.", + "ngen.case.close.error": "Ocurrió un error al cerrar el caso.", "ngen.case.create": "Crear caso", "ngen.case.error": "No se puede recuperar la información de los casos", "ngen.case.info.error": "No se puede recuperar la información del caso", diff --git a/frontend/src/views/case/components/TableCase.jsx b/frontend/src/views/case/components/TableCase.jsx index 0e136c60..15480681 100644 --- a/frontend/src/views/case/components/TableCase.jsx +++ b/frontend/src/views/case/components/TableCase.jsx @@ -17,6 +17,7 @@ import EventComponent from "views/tanstackquery/EventComponent"; import TaxonomyComponent from "views/tanstackquery/TaxonomyComponent"; import { getState } from "api/services/states"; import apiInstance from "api/api"; +import setAlert from "utils/setAlert"; @@ -107,7 +108,7 @@ const TableCase = ({ const stateData = stateResponse.data; if (stateData.solved) { - alert(t("ngen.case.close.already_closed")); + setAlert(t("ngen.case.close.already_closed"), "error", "case"); return; } @@ -121,7 +122,7 @@ const TableCase = ({ } if (!targetStateUrl) { - alert(t("ngen.case.close.no_transition")); + setAlert(t("ngen.case.close.no_transition"), "error", "case"); return; } @@ -139,6 +140,8 @@ const TableCase = ({ }; const handleCloseCaseConfirm = () => { + if (!closeCaseInfo || closingCase) return; + setClosingCase(true); patchCaseState(closeCaseInfo.caseUrl, closeCaseInfo.targetStateUrl) .then((response) => { setIfModify(response); @@ -147,6 +150,13 @@ const TableCase = ({ }) .catch((error) => { console.error(error); + const msg = error.response?.data?.detail + || error.response?.data?.state?.[0] + || t("ngen.case.close.error"); + setAlert(msg, "error", "case"); + }) + .finally(() => { + setClosingCase(false); }); }; @@ -408,9 +418,11 @@ const TableCase = ({ ))} {!disableCloseCase && ( -