Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
"crud.download": "Download",
"crud.edit": "Edit",
"crud.goto": "Go to",
"crud.reiterate": "Reiterate",
"crud.save": "Save",
"crud.want_to_deactivate": "Do you want to deactivate the selected items?",
"crud.want_to_delete": "Do you want to delete the selected items?",
"crud.want_to_merge": "Do you want to merge the selected items?",
"crud.want_to_reiterate": "Do you want to reiterate the case communication?",
"date.attend": "Attend date",
"date.condition_from": "Date from",
"date.condition_to": "Date to",
Expand Down Expand Up @@ -336,6 +338,11 @@
"ngen.merge.header.event": "Events merge",
"ngen.merge.message.case": "Do you want to merge the selected cases?",
"ngen.merge.message.event": "Do you want to merge the selected events?",
"ngen.reiterate": "Reiterate",
"ngen.reiterate.case.error": "The case communication could not be reiterated",
"ngen.reiterate.case.success": "The case communication was successfully reiterated",
"ngen.reiterate.header.case": "Reiterate communication",
"ngen.reiterate.message.case": "Do you want to reiterate the case communication?",
"ngen.name.invalid": "Enter a name that has up to 100 characters, only letters and is not empty.",
"ngen.name.placeholder": "Enter a name",
"ngen.name_one": "Name",
Expand Down
7 changes: 7 additions & 0 deletions frontend/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
"crud.download": "Descargar",
"crud.edit": "Editar",
"crud.goto": "Ir a",
"crud.reiterate": "Reiterar",
"crud.save": "Guardar",
"crud.want_to_deactivate": "¿Está seguro de que desea desactivar los ítems seleccionados?",
"crud.want_to_delete": "¿Está seguro de que desea borrar los ítems seleccionados?",
"crud.want_to_merge": "¿Está seguro de que desea fusionar los ítems seleccionados?",
"crud.want_to_reiterate": "¿Está seguro de que desea reiterar la comunicación del caso?",
"date.attend": "Fecha de atención",
"date.condition_from": "Fecha desde",
"date.condition_to": "Fecha hasta",
Expand Down Expand Up @@ -336,6 +338,11 @@
"ngen.merge.header.event": "Fusión de eventos",
"ngen.merge.message.case": "¿Desea fusionar los casos seleccionados?",
"ngen.merge.message.event": "¿Desea fusionar los eventos seleccionados?",
"ngen.reiterate": "Reiterar",
"ngen.reiterate.case.error": "No se pudo reiterar la comunicación del caso",
"ngen.reiterate.case.success": "La comunicación del caso se reiteró con éxito",
"ngen.reiterate.header.case": "Reiterar comunicación",
"ngen.reiterate.message.case": "¿Desea reiterar la comunicación del caso?",
"ngen.name.invalid": "Ingrese un nombre de hasta 100 caracteres, que contenga sólo letras y que no esté vacío.",
"ngen.name.placeholder": "Ingrese un nombre",
"ngen.name_one": "Nombre",
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/api/services/cases.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,21 @@ const mergeCase = (urlParent, urlChildren) => {
});
};

export { getCases, getAllCases, getOrderingCases, getCase, postCase, putCase, deleteCase, mergeCase, patchCase, getMinifiedCase };
const reiterateCase = (url) => {
let messageSuccess = i18next.t("ngen.reiterate.case.success");
let messageError = i18next.t("ngen.reiterate.case.error") + " .";
return apiInstance
.post(url + "reiterate/")
.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, reiterateCase };
7 changes: 7 additions & 0 deletions frontend/src/components/Button/CrudButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ const CrudButton = ({ type, name, onClick, disabled = false, to, state, permissi
icon: " fas fa-arrow-right",
text: "",
},
reiterate: {
class: text ? "text-capitalize" : "btn-icon btn-rounded",
variant: "outline-info",
title: t("crud.reiterate"),
icon: "fas fa-redo",
text: text ? text : ""
},
};

let component = (
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/Modal/ModalConfirm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ const ModalConfirm = (props) => {
message: eventOrCase.includes("event") ? `${t("ngen.merge.message.event")} ${warning}` : `${t("ngen.merge.message.case")} ${warning}`,
variantButtonConfirm: "outline-primary",
textButtonConfirm: t("ngen.accept")
},
reiterate: {
header: `${props.component ? t("ngen.reiterate.header.case") : t("ngen.reiterate")}`,
message: `${t("ngen.reiterate.message.case")}`,
variantButtonConfirm: "outline-info",
textButtonConfirm: t("ngen.reiterate")
}
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/views/case/ListCase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ const ListCase = ({ routeParams }) => {
userNames={userNames}
editColum={true}
deleteColum={true}
reiterateColum={true}
navigationRow={true}
buttonReturn={false}
disableNubersOfEvents={true}
Expand Down
40 changes: 39 additions & 1 deletion frontend/src/views/case/components/TableCase.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { Form, Row, Spinner, Table } from "react-bootstrap";
import CrudButton from "components/Button/CrudButton";
import { deleteCase } from "api/services/cases";
import { deleteCase, reiterateCase } from "api/services/cases";
import ModalConfirm from "components/Modal/ModalConfirm";
import Ordering from "components/Ordering/Ordering";
import LetterFormat from "components/LetterFormat";
Expand Down Expand Up @@ -33,6 +33,7 @@ const TableCase = ({
userNames,
editColum,
deleteColum,
reiterateColum = false,
detailModal,
modalCaseDetail,
navigationRow,
Expand All @@ -56,6 +57,7 @@ const TableCase = ({
}) => {
const [url, setUrl] = useState(null);
const [modalDelete, setModalDelete] = useState(false);
const [modalReiterate, setModalReiterate] = useState(false);
const [id, setId] = useState(null);

//checkbox
Expand Down Expand Up @@ -113,6 +115,26 @@ const TableCase = ({
});
};

//Reiterate Case
const Reiterate = (url, id) => {
setId(id);
setUrl(url);
setModalReiterate(true);
};

const reiterate = (url) => {
reiterateCase(url)
.then((response) => {
setIfModify(response);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
setModalReiterate(false);
});
};

//Checkbox
const handleToggleUuidDisplay = () => {
setShowFullUuid(!showFullUuid);
Expand Down Expand Up @@ -319,6 +341,14 @@ const TableCase = ({
{!disableColumOption && editColum && (
<CrudButton type="edit" to={`${basePath}/cases/edit/${itemNumber}`} checkPermRoute />
)}
{!disableColumOption && reiterateColum && (
<CrudButton
type="reiterate"
onClick={() => Reiterate(caseItem.url, itemNumber)}
permissions="change_case"
disabled={stateNames[caseItem.state]?.toLowerCase() !== "open"}
/>
)}
{!disableColumOption &&
deleteColum &&
(deleteColumForm ? (
Expand All @@ -340,6 +370,14 @@ const TableCase = ({
onHide={() => setModalDelete(false)}
ifConfirm={() => removeCase(url)}
/>
<ModalConfirm
type="reiterate"
component={t("ngen.case_one")}
name={id}
showModal={modalReiterate}
onHide={() => setModalReiterate(false)}
ifConfirm={() => reiterate(url)}
/>
</React.Fragment>
);
};
Expand Down
30 changes: 30 additions & 0 deletions ngen/locale/es/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -788,3 +788,33 @@ msgid ""
msgstr ""
"Permitir la creación automática de taxonomías y grupos por el slug al crear "
"un evento"

#: ngen/templates/reports/case_reiterate.html
msgid "Case Follow-up"
msgstr "Seguimiento de caso"

#: ngen/templates/reports/case_reiterate.html
msgid "Updated with new evidence"
msgstr "Actualizado con nueva evidencia"

#: ngen/templates/reports/case_reiterate.html
msgid "Reminder - No new evidence"
msgstr "Recordatorio - Sin nueva evidencia"

#: ngen/templates/reports/case_reiterate.html
#, python-format
msgid ""
"This is a follow-up communication for case %(id)s with %(count)s new "
"evidence file(s) attached."
msgstr ""
"Esta es una comunicación de seguimiento para el caso %(id)s con %(count)s "
"archivo(s) de evidencia nueva adjunto(s)."

#: ngen/templates/reports/case_reiterate.html
#, python-format
msgid ""
"This is a follow-up reminder for case %(id)s. There is no new evidence "
"since the last communication."
msgstr ""
"Este es un recordatorio de seguimiento para el caso %(id)s. No hay nueva "
"evidencia desde la última comunicación."
1 change: 1 addition & 0 deletions ngen/mailer/email_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"case_closed_report": "reports/case_closed_report.html",
"case_change_state": "reports/case_change_state.html",
"case_assign": "reports/case_assign.html",
"case_reiterate": "reports/case_reiterate.html",
}


Expand Down
45 changes: 45 additions & 0 deletions ngen/models/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,51 @@ def communicate_v2(
self.communicate_intern(template, template_params, send_attachments)
self.notification_count += 1


def communicate_reiterate(self, template="case_reiterate"):
"""Reitera comunicación con evidencia nueva en todos los canales"""
template_params = self.template_params

# Recolectar TODA la evidencia: caso + eventos + eventos hijos
all_evidence = list(self.evidence.all())
for event in self.events.all():
all_evidence.extend(list(event.evidence.all()))
# Agregar evidencia de eventos hijos (merged events)
for child_event in event.children.all():
all_evidence.extend(list(child_event.evidence.all()))

# Reiterar en canal interno
if config.CREATE_INTERNAL_COMMUNICATION_CHANNEL:
intern_channel = ngen.models.CommunicationChannel.get_or_create_channel_with_intern(
channelable=self
)
intern_channel.communicate_reiterate(
all_evidence=all_evidence,
subject=self.subject_v2(channel_type="INTERN-REITERATE"),
template=template,
template_params=template_params
)

# Reiterar en canales de eventos afectados
for event in self.events.all():
# Crear canales de comunicación si no existen
ngen.models.CommunicationChannel.get_or_create_channel_with_affected(
channelable=event
)

# Enviar reiteración a todos los canales del evento
for channel in event.communication_channels.all():
channel.communicate_reiterate(
all_evidence=all_evidence,
subject=self.subject_v2(channel_type="AFFECTED-REITERATE"),
template=template,
template_params=template_params,
bcc_recipients=self.get_team_and_assigned_contacts()
)

self.notification_count += 1
self.save()

def get_team_and_assigned_contacts(self):
"""
Returns a list of internal contacts of the case.
Expand Down
52 changes: 52 additions & 0 deletions ngen/models/communication_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,58 @@ def get_last_message(self):
messages = self.get_messages()
return messages.last() if messages else None

def get_evidence_since_last_communication(self, all_evidence: list) -> list:
"""
Filtra evidencias creadas después de la última comunicación del canal.
Si no hay comunicaciones previas, retorna todas las evidencias.
"""
last_msg = self.get_last_message()

if not last_msg:
return all_evidence # Primera comunicación

return [
evidence for evidence in all_evidence
if evidence.created > last_msg.created
]

def communicate_reiterate(
self,
all_evidence: list,
subject: str = None,
template: str = None,
template_params: dict = None,
**kwargs
):
"""
Reitera comunicación enviando SOLO evidencia nueva desde última comunicación.
Si no hay evidencia nueva, envía el email sin adjuntos (recordatorio).
"""
new_evidence = self.get_evidence_since_last_communication(all_evidence)

# Formatear evidencias como adjuntos (puede ser lista vacía)
attachments = [
{
"name": ev.attachment_name,
"file": ev.directory_path(ev.filename)
}
for ev in new_evidence
]

# Agregar información sobre nueva evidencia a los parámetros del template
if template_params is None:
template_params = {}
template_params['has_new_evidence'] = len(new_evidence) > 0
template_params['new_evidence_count'] = len(new_evidence)

return self.communicate(
subject=subject,
template=template,
template_params=template_params,
attachments=attachments,
**kwargs
)

def communicate(
self,
subject: Optional[str] = None,
Expand Down
43 changes: 43 additions & 0 deletions ngen/templates/reports/case_reiterate.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{% extends 'reports/content_base.html' %}
{% load i18n %}
{% language lang %}
{% block content_header %}
<h4>
{% translate 'Case Follow-up' %}: {{ case.uuid }}
</h4>
<small class="lead">
<strong>{% translate 'Case state' %}:</strong> {{ case.state.name }}
</small>
<br>
{% if has_new_evidence %}
<span class="badge badge-info">
{% translate 'Updated with new evidence' %}
</span>
{% else %}
<span class="badge badge-warning">
{% translate 'Reminder - No new evidence' %}
</span>
{% endif %}
{% endblock %}

{% block content_body %}
{% if has_new_evidence %}
<p class="lead">
{% blocktranslate trimmed with id=case.uuid count=new_evidence_count %}
This is a follow-up communication for case {{ id }}
with {{ count }} new evidence file(s) attached.
{% endblocktranslate %}
</p>
{% else %}
<p class="lead">
{% blocktranslate trimmed with id=case.uuid %}
This is a follow-up reminder for case {{ id }}.
There is no new evidence since the last communication.
{% endblocktranslate %}
</p>
{% endif %}
{% for event in events %}
{% include "reports/event_detail.html" with event=event %}
{% endfor %}
{% endblock %}
{% endlanguage %}
Loading