fase 2.4: recordatorios y notificaciones automaticas (modelo+cola+bitacora, cron cada 5 min, recordatorios por ventana, confirmacion, cumpleanos, post-visita, recuperacion de no-show, modo prueba, plantillas con glosario, historial y reintentos); apagado por defecto

This commit is contained in:
2026-10-11 09:05:19 +00:00
parent 9257586943
commit dabd8c6bd7
16 changed files with 1832 additions and 12 deletions

View File

@@ -1013,6 +1013,7 @@ export interface ClinicSettings {
nota_ticket: string;
impuestos: ImpuestosSettings;
reservas_online: ReservasOnlineSettings;
notificaciones: NotificacionesSettings;
}
// ---- Reservas en línea (portal público de auto-agendado) ----
@@ -1046,6 +1047,115 @@ export const RESERVAS_ONLINE_DEFAULT: ReservasOnlineSettings = {
telefono_contacto: '',
};
// ---- Notificaciones y recordatorios de citas ----
export type NotificacionCanal = 'whatsapp' | 'email';
export type NotificacionCanalConfig = NotificacionCanal | 'ambos';
export type NotificacionTipo = 'recordatorio' | 'confirmacion' | 'cumpleanos' | 'post_visita' | 'recuperacion' | 'manual';
export type NotificacionEstado = 'pendiente' | 'enviada' | 'simulada' | 'error' | 'cancelada';
export interface NotificacionesSettings {
/** Enciende recordatorios y notificaciones. Apagado = no se envía ni se encola nada. */
activo: boolean;
/** Modo prueba: registra "simulada" sin llamar a ningún proveedor externo. */
modo_prueba: boolean;
canal: NotificacionCanalConfig;
/** Ventanas (horas antes de la cita) en que se avisa. */
recordatorio_horas_antes: number[];
confirmacion_al_crear: boolean;
cumpleanos: boolean;
hora_envio_cumpleanos: string;
/** Horas tras la visita para el mensaje de seguimiento (0 = apagado). */
post_visita_horas: number;
recuperacion_no_show: boolean;
plantilla_recordatorio: string;
plantilla_confirmacion: string;
plantilla_cumpleanos: string;
plantilla_post_visita: string;
whatsapp_numero: string;
email_remitente: string;
}
export const NOTIFICACIONES_DEFAULT: NotificacionesSettings = {
activo: false,
modo_prueba: true,
canal: 'whatsapp',
recordatorio_horas_antes: [24, 2],
confirmacion_al_crear: false,
cumpleanos: false,
hora_envio_cumpleanos: '09:00',
post_visita_horas: 0,
recuperacion_no_show: false,
plantilla_recordatorio: 'Hola {nombre}, te recordamos tu {cita} de {servicio} el {fecha} a las {hora} con {doctor} en {sucursal}. Responde CONFIRMAR o CANCELAR.',
plantilla_confirmacion: 'Hola {nombre}, tu {cita} de {servicio} quedó registrada para el {fecha} a las {hora} con {doctor} en {sucursal}. Te esperamos.',
plantilla_cumpleanos: '¡Feliz cumpleaños, {nombre}! Todo el equipo de {sucursal} te desea un gran día. Te esperamos pronto.',
plantilla_post_visita: 'Hola {nombre}, gracias por tu visita de {servicio} el {fecha}. Si tienes dudas o quieres agendar de nuevo, llámanos al {telefono}.',
whatsapp_numero: '',
email_remitente: '',
};
/** Variables permitidas en las plantillas (espejo del backend). */
export const NOTIF_VARIABLES = ['nombre', 'cita', 'servicio', 'fecha', 'hora', 'doctor', 'sucursal', 'telefono'] as const;
/** Ventanas sugeridas para los chips de recordatorio. */
export const NOTIF_VENTANAS: { horas: number; label: string }[] = [
{ horas: 1, label: '1 h' },
{ horas: 2, label: '2 h' },
{ horas: 24, label: '24 h' },
{ horas: 48, label: '48 h' },
{ horas: 168, label: '1 semana' },
];
export interface Notificacion {
id: number;
partner_id: number;
patient: string;
phone: string;
email: string;
cita_id: number | null;
cita_ref: string;
tipo: NotificacionTipo;
tipo_label: string;
canal: NotificacionCanal;
canal_label: string;
estado: NotificacionEstado;
estado_label: string;
asunto: string;
mensaje: string;
programada_para: string | null;
enviada_at: string | null;
error: string;
intentos: number;
}
export interface NotificacionesResponse {
status: string;
notificaciones: Notificacion[];
total: number;
page: number;
page_size: number;
total_pages: number;
resumen: Record<string, number>;
message?: string;
}
export interface ProcesarNotificacionesResult {
status: string;
message: string;
encoladas: number;
revisadas: number;
enviadas: number;
simuladas: number;
fallidas: number;
canceladas: number;
}
export interface ProbarNotificacionResult {
status: string;
ok: boolean;
simulado: boolean;
message: string;
}
// Contrato del portal público (endpoints /public/booking/*)
export interface BookingConfig {
activo: boolean;
@@ -1129,6 +1239,7 @@ export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
nota_ticket: '',
impuestos: IMPUESTOS_DEFAULT,
reservas_online: RESERVAS_ONLINE_DEFAULT,
notificaciones: NOTIFICACIONES_DEFAULT,
};
export interface FrontendUser {
@@ -1815,6 +1926,32 @@ export const odooApi = {
return data;
},
// ---- Notificaciones y recordatorios de citas ----
async getNotificaciones(params?: Record<string, string | number>): Promise<NotificacionesResponse> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/notificaciones`, { params });
return data;
},
async procesarNotificaciones(limit?: number): Promise<ProcesarNotificacionesResult> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/procesar`, { limit });
return data;
},
async enviarNotificacion(payload: { cita_id: number; tipo?: string; canal?: NotificacionCanal }): Promise<{ status: string; notificaciones: Notificacion[]; simulado: boolean; message: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/enviar`, payload);
return data;
},
async reintentarNotificacion(id: number): Promise<{ status: string; estado: string; notificacion: Notificacion; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/${id}/reintentar`);
return data;
},
async probarNotificacion(payload: { canal: NotificacionCanal; destino?: string }): Promise<ProbarNotificacionResult> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/probar`, payload);
return data;
},
async updateRolePermissions(matrix: Record<string, FrontendRole[]>): Promise<{ status: string; matrix: Record<string, FrontendRole[]>; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/roles/permissions`, { matrix });
return data;