Centro de mando de formaciones (VIII): Automatismos, recordatorios y experiencia integrada
En esta octava entrega culminamos la integración funcional del planificador de formaciones. Los módulos de recordatorios, interacciones y sesiones ya trabajan de forma automática y sincronizada. Incorporamos además la vista de Recordatorios, con notificaciones y avisos locales.
Automatismos, recordatorios y experiencia integrada
Esta fase 8 representa el cierre funcional del desarrollo:
hemos conectado todas las piezas — datos, lógica de negocio y UI — para que el sistema funcione como un centro de mando de formaciones totalmente operativo.
El planificador ahora genera recordatorios automáticos, sincroniza sesiones con interacciones y muestra todo en una nueva vista de Recordatorios integrada en la interfaz principal.
1. Servicio de recordatorios automáticos (servicio_recordatorios.py)
El servicio centraliza la lógica de generación, comprobación y aviso de recordatorios.
Se apoya en las interacciones comerciales y las sesiones programadas.
from datetime import datetime, timedelta
from planificador.data.repositories.interaccion_repo import InteraccionRepository
from planificador.data.repositories.sesion_repo import SesionRepository
from planificador.data.repositories.recordatorio_repo import RecordatorioRepository
from planificador.common.registro import get_logger
log = get_logger(__name__)
class ServicioRecordatorios:
"""
Gestiona la creación y verificación de recordatorios automáticos
a partir de interacciones y sesiones planificadas.
"""
@staticmethod
def generar_desde_interacciones():
interacciones = InteraccionRepository.listar_todas()
nuevos = []
for inter in interacciones:
if not inter.get("crear_recordatorio"):
continue
fecha_accion = inter.get("fecha_proxima_accion")
if not fecha_accion:
continue
recordatorio = {
"tipo": inter["tipo"],
"cliente": inter["id_cliente"],
"fecha": fecha_accion,
"descripcion": inter.get("proxima_accion", ""),
}
RecordatorioRepository.crear(recordatorio)
nuevos.append(recordatorio)
log.info(f"Recordatorio generado desde interacción {inter['id_interaccion']}: {recordatorio}")
log.info(f"Total recordatorios generados: {len(nuevos)}")
return nuevos
@staticmethod
def comprobar_sesiones_proximas(horas_anticipacion=24):
"""
Devuelve lista de sesiones próximas dentro del rango de anticipación (horas).
"""
sesiones = SesionRepository.listar_todas()
proximas = []
ahora = datetime.now()
for ses in sesiones:
fecha_sesion = datetime.strptime(
f"{ses['fecha']} {ses['hora_inicio']}", "%Y-%m-%d %H:%M"
)
diff = fecha_sesion - ahora
if timedelta(0) <= diff <= timedelta(hours=horas_anticipacion):
proximas.append(ses)
return proximas
@staticmethod
def avisar_proximos_eventos():
"""
Combina interacciones y sesiones próximas en una lista de avisos.
"""
avisos = []
interacciones = InteraccionRepository.listar_todas()
sesiones = ServicioRecordatorios.comprobar_sesiones_proximas()
for i in interacciones:
if i.get("fecha_proxima_accion") == datetime.now().strftime("%Y-%m-%d"):
avisos.append({
"tipo": "interacción",
"cliente": i["id_cliente"],
"descripcion": i.get("proxima_accion", ""),
"fecha": i["fecha_proxima_accion"]
})
for s in sesiones:
avisos.append({
"tipo": "sesión",
"cliente": s["id_contratacion"],
"descripcion": f"Sesión próxima ({s['fecha']} {s['hora_inicio']})",
"fecha": s["fecha"]
})
log.info(f"Generados {len(avisos)} avisos combinados.")
return avisos
2. Repositorio de recordatorios (recordatorio_repo.py)
Un nuevo repositorio en planificador/data/repositories/ dedicado a almacenar los recordatorios generados.
from planificador.data.db_manager import get_connection
from planificador.common.registro import get_logger
log = get_logger(__name__)
class RecordatorioRepository:
"""
Repositorio CRUD para los recordatorios automáticos.
"""
@staticmethod
def crear(recordatorio):
with get_connection() as conn:
conn.execute("""
INSERT INTO Recordatorio (tipo, cliente, fecha, descripcion)
VALUES (?, ?, ?, ?)
""", (recordatorio["tipo"], recordatorio["cliente"],
recordatorio["fecha"], recordatorio["descripcion"]))
conn.commit()
@staticmethod
def listar_todos():
with get_connection() as conn:
cur = conn.execute("""
SELECT id, tipo, cliente, fecha, descripcion
FROM Recordatorio
ORDER BY fecha ASC
""")
return [dict(row) for row in cur.fetchall()]
3. Vista de recordatorios (vista_recordatorios.py)
Un módulo completamente nuevo en la capa UI, integrado como una pestaña adicional en la interfaz principal.
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QLabel, QTableWidget, QTableWidgetItem,
QPushButton, QHBoxLayout, QMessageBox
)
from planificador.common.registro import get_logger
log = get_logger(__name__)
try:
from planificador.data.repositories.recordatorio_repo import RecordatorioRepository
from planificador.servicios.servicio_recordatorios import ServicioRecordatorios
except Exception:
RecordatorioRepository = None
ServicioRecordatorios = None
log.warning("Repositorios o servicios no disponibles en vista_recordatorios.")
class VistaRecordatorios(QWidget):
"""
Vista para consultar y generar recordatorios automáticos.
"""
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
self.setLayout(layout)
titulo = QLabel("Recordatorios automáticos")
titulo.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 8px;")
layout.addWidget(titulo)
self.tabla = QTableWidget()
self.tabla.setColumnCount(4)
self.tabla.setHorizontalHeaderLabels(["Tipo", "Cliente", "Fecha", "Descripción"])
layout.addWidget(self.tabla)
botones = QHBoxLayout()
self.btn_recargar = QPushButton("Recargar")
self.btn_generar = QPushButton("Generar desde interacciones")
botones.addWidget(self.btn_recargar)
botones.addWidget(self.btn_generar)
layout.addLayout(botones)
self.btn_recargar.clicked.connect(self.cargar_recordatorios)
self.btn_generar.clicked.connect(self.generar_desde_interacciones)
self.cargar_recordatorios()
def cargar_recordatorios(self):
self.tabla.setRowCount(0)
if not RecordatorioRepository:
QMessageBox.warning(self, "Error", "Repositorio no disponible.")
return
try:
datos = RecordatorioRepository.listar_todos()
if not datos:
log.info("No hay recordatorios disponibles.")
return
self.tabla.setRowCount(len(datos))
for i, rec in enumerate(datos):
self.tabla.setItem(i, 0, QTableWidgetItem(str(rec.get("tipo", ""))))
self.tabla.setItem(i, 1, QTableWidgetItem(str(rec.get("cliente", ""))))
self.tabla.setItem(i, 2, QTableWidgetItem(str(rec.get("fecha", ""))))
self.tabla.setItem(i, 3, QTableWidgetItem(str(rec.get("descripcion", ""))[:100]))
except Exception as e:
log.error(f"Error al cargar recordatorios: {e}")
QMessageBox.warning(self, "Error", f"No se pudieron cargar recordatorios: {e}")
def generar_desde_interacciones(self):
if not ServicioRecordatorios:
QMessageBox.warning(self, "Error", "Servicio no disponible.")
return
try:
nuevos = ServicioRecordatorios.generar_desde_interacciones()
QMessageBox.information(self, "Recordatorios", f"Generados {len(nuevos)} nuevos recordatorios.")
self.cargar_recordatorios()
except Exception as e:
log.error(f"Error generando recordatorios: {e}")
QMessageBox.warning(self, "Error", f"No se pudieron generar recordatorios: {e}")
4. Integración en la interfaz principal (main_window.py)
El módulo main_window.py se amplía con una pestaña adicional para Recordatorios, consolidando todas las funciones en un solo entorno.
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QListWidget,
QStackedWidget, QStatusBar, QMessageBox
)
from PyQt6.QtGui import QAction
import sys
from planificador.ui.vistas.vista_clientes import VistaClientes
from planificador.ui.vistas.vista_calendario import VistaCalendario
from planificador.ui.vistas.vista_formaciones import VistaFormaciones
from planificador.ui.vistas.vista_interacciones import VistaInteracciones
from planificador.ui.vistas.vista_configuracion import VistaConfiguracion
from planificador.ui.vistas.vista_recordatorios import VistaRecordatorios
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Planificador de Formaciones")
self.resize(1200, 800)
contenedor = QWidget()
layout = QHBoxLayout(contenedor)
self.menu_lateral = QListWidget()
self.menu_lateral.addItems([
"Clientes", "Calendario", "Formaciones", "Interacciones", "Recordatorios", "Configuración"
])
self.menu_lateral.setMaximumWidth(200)
self.menu_lateral.currentRowChanged.connect(self._cambiar_vista)
self.vistas = QStackedWidget()
self.vistas.addWidget(VistaClientes())
self.vistas.addWidget(VistaCalendario())
self.vistas.addWidget(VistaFormaciones())
self.vistas.addWidget(VistaInteracciones())
self.vistas.addWidget(VistaRecordatorios())
self.vistas.addWidget(VistaConfiguracion())
layout.addWidget(self.menu_lateral)
layout.addWidget(self.vistas, 1)
self.setCentralWidget(contenedor)
self.setStatusBar(QStatusBar())
def _cambiar_vista(self, indice):
self.vistas.setCurrentIndex(indice)
def main():
app = QApplication(sys.argv)
ventana = MainWindow()
ventana.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Resultado final
Con esta integración, el planificador de formaciones ya:
- ✅ Genera recordatorios automáticos desde interacciones y sesiones.
- ✅ Muestra avisos en una vista dedicada dentro de la interfaz.
- ✅ Permite gestionar próximas acciones y avisos desde un solo lugar.
- ✅ Mantiene coherencia total entre interacciones, sesiones y contrataciones.
- ✅ Mejora la experiencia de usuario con un flujo más natural y visual.
Próximos pasos
En la siguiente entrega cerraremos el ciclo con la publicación del ejecutable y la documentación técnica completa del proyecto.
El planificador ya no es solo una agenda, sino un centro de mando inteligente que conecta todos los procesos de formación y relación con clientes.