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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user