fase 2.5: formularios digitales y consentimientos (modulo skeen_formularios: plantillas con 12 tipos de campo, versionado, respuestas con firma PNG y fotos antes/despues, portal publico con token HMAC y limite de subida, historial por paciente, permisos y notificacion de envio)
This commit is contained in:
@@ -1071,6 +1071,8 @@ export interface NotificacionesSettings {
|
||||
plantilla_confirmacion: string;
|
||||
plantilla_cumpleanos: string;
|
||||
plantilla_post_visita: string;
|
||||
/** Aviso de un formulario digital: usa {formulario} y {enlace}. */
|
||||
plantilla_formulario: string;
|
||||
whatsapp_numero: string;
|
||||
email_remitente: string;
|
||||
}
|
||||
@@ -1089,12 +1091,13 @@ export const NOTIFICACIONES_DEFAULT: NotificacionesSettings = {
|
||||
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}.',
|
||||
plantilla_formulario: 'Hola {nombre}, te compartimos el formulario "{formulario}" para completar. Ábrelo desde tu celular aquí: {enlace}',
|
||||
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;
|
||||
export const NOTIF_VARIABLES = ['nombre', 'cita', 'servicio', 'fecha', 'hora', 'doctor', 'sucursal', 'telefono', 'enlace', 'formulario'] as const;
|
||||
|
||||
/** Ventanas sugeridas para los chips de recordatorio. */
|
||||
export const NOTIF_VENTANAS: { horas: number; label: string }[] = [
|
||||
@@ -1224,6 +1227,114 @@ export interface BookingCita {
|
||||
telefono_contacto: string;
|
||||
}
|
||||
|
||||
// ---- Formularios digitales y consentimientos ----
|
||||
export type FormularioCampoTipo =
|
||||
| 'texto' | 'texto_largo' | 'numero' | 'fecha' | 'seleccion' | 'multi_seleccion'
|
||||
| 'checkbox' | 'escala' | 'firma' | 'foto' | 'archivo' | 'consentimiento';
|
||||
export type FormularioPlantillaTipo = 'consentimiento' | 'expediente' | 'encuesta' | 'autorizacion';
|
||||
export type FormularioEstado = 'borrador' | 'enviado' | 'llenado' | 'cancelado';
|
||||
export type FormularioMomento = 'antes' | 'despues' | 'otro';
|
||||
export type FormularioCanal = 'whatsapp' | 'email' | 'enlace';
|
||||
|
||||
export interface FormularioCampo {
|
||||
clave: string;
|
||||
etiqueta: string;
|
||||
tipo: FormularioCampoTipo;
|
||||
obligatorio: boolean;
|
||||
ayuda: string;
|
||||
opciones: string[];
|
||||
/** Solo para tipo foto/archivo: para la galería antes/después del expediente. */
|
||||
momento?: FormularioMomento;
|
||||
}
|
||||
|
||||
export interface FormularioPlantilla {
|
||||
id: number;
|
||||
name: string;
|
||||
tipo: FormularioPlantillaTipo;
|
||||
tipo_label?: string;
|
||||
activo: boolean;
|
||||
version: number;
|
||||
descripcion: string;
|
||||
campos: FormularioCampo[];
|
||||
texto_legal: string;
|
||||
requiere_firma: boolean;
|
||||
requiere_foto: boolean;
|
||||
duracion_estimada_min: number;
|
||||
secuencia: number;
|
||||
respuestas_count: number;
|
||||
sucursales?: { id: number; name: string }[];
|
||||
servicios?: { id: number; name: string }[];
|
||||
}
|
||||
|
||||
export interface FormularioAdjunto {
|
||||
id: number;
|
||||
campo_clave: string;
|
||||
kind: FormularioMomento;
|
||||
kind_label?: string;
|
||||
name: string;
|
||||
mimetype: string;
|
||||
notes: string;
|
||||
create_date: string | null;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface FormularioFoto extends FormularioAdjunto {
|
||||
respuesta_id: number;
|
||||
respuesta_name: string;
|
||||
plantilla: string;
|
||||
cita_id: number | null;
|
||||
}
|
||||
|
||||
export interface FormularioRespuesta {
|
||||
id: number;
|
||||
name: string;
|
||||
plantilla_id: number;
|
||||
plantilla: string;
|
||||
plantilla_tipo?: FormularioPlantillaTipo;
|
||||
plantilla_version: number;
|
||||
partner_id: number;
|
||||
patient: string;
|
||||
cita_id: number | null;
|
||||
estado: FormularioEstado;
|
||||
estado_label: string;
|
||||
canal_envio: string;
|
||||
creado_por_empleado: boolean;
|
||||
tiene_firma: boolean;
|
||||
firma_nombre: string;
|
||||
enviado_at: string | null;
|
||||
llenado_at: string | null;
|
||||
create_date: string | null;
|
||||
fotos_count: number;
|
||||
// Solo en el detalle
|
||||
respuestas?: Record<string, unknown>;
|
||||
campos?: FormularioCampo[];
|
||||
texto_legal?: string;
|
||||
notas?: string;
|
||||
firma_url?: string | null;
|
||||
fotos?: FormularioAdjunto[];
|
||||
}
|
||||
|
||||
export interface FormularioPublicoConfig {
|
||||
status: string;
|
||||
brand: { nombre: string; logo_claro: string; logo_oscuro: string; colores: { primario: string; acento: string } };
|
||||
terms: GlossaryTerms;
|
||||
paciente: { nombre: string };
|
||||
cita: { fecha: string | null; hora: string | null; servicio: string | null } | null;
|
||||
estado: FormularioEstado;
|
||||
ya_llenado: boolean;
|
||||
plantilla: {
|
||||
nombre: string;
|
||||
tipo: FormularioPlantillaTipo;
|
||||
tipo_label?: string;
|
||||
descripcion: string;
|
||||
texto_legal: string;
|
||||
requiere_firma: boolean;
|
||||
requiere_foto: boolean;
|
||||
duracion_estimada_min: number;
|
||||
campos: FormularioCampo[];
|
||||
};
|
||||
}
|
||||
|
||||
export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
|
||||
nombre: BRAND_DEFAULT.nombre,
|
||||
telefono: '',
|
||||
@@ -1926,6 +2037,88 @@ export const odooApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
// ---- Formularios digitales: plantillas ----
|
||||
async getFormularioPlantillas(opts?: { includeInactive?: boolean; tipo?: string }): Promise<{ status: string; plantillas: FormularioPlantilla[]; total: number; message?: string }> {
|
||||
const params: Record<string, string | number> = {};
|
||||
if (opts?.includeInactive) params.include_inactive = 1;
|
||||
if (opts?.tipo) params.tipo = opts.tipo;
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas`, { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
async getFormularioPlantilla(id: number): Promise<{ status: string; plantilla: FormularioPlantilla; message?: string }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas/${id}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
async createFormularioPlantilla(payload: Partial<FormularioPlantilla> & { sucursal_ids?: number[]; servicio_ids?: number[] }): Promise<{ status: string; plantilla: FormularioPlantilla; message?: string }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async updateFormularioPlantilla(id: number, payload: Partial<FormularioPlantilla> & { sucursal_ids?: number[]; servicio_ids?: number[] }): Promise<{ status: string; plantilla: FormularioPlantilla; message?: string }> {
|
||||
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas/${id}`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async deleteFormularioPlantilla(id: number): Promise<{ status: string; desactivada: boolean; message: string }> {
|
||||
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas/${id}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
// ---- Formularios digitales: respuestas ----
|
||||
async getFormularioRespuestas(params?: Record<string, string | number>): Promise<{ status: string; respuestas: FormularioRespuesta[]; total: number; page: number; page_size: number; total_pages: number; resumen: Record<string, number>; message?: string }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/respuestas`, { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
async getFormularioRespuesta(id: number): Promise<{ status: string; respuesta: FormularioRespuesta; message?: string }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/respuestas/${id}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
async getFormularioFotos(params: { partner_id?: number; respuesta_id?: number }): Promise<{ status: string; total: number; fotos: FormularioFoto[]; message?: string }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/fotos`, { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
async enviarFormulario(payload: { plantilla_id: number; partner_id: number; cita_id?: number | null; canal: FormularioCanal }): Promise<{ status: string; respuesta: FormularioRespuesta; token: string; enlace: string; enviado_por: string; notificado: boolean; message?: string }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/formularios/enviar`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async llenarFormulario(id: number, payload: { respuestas: Record<string, unknown>; firma_imagen?: string; firma_nombre?: string }): Promise<{ status: string; respuesta: FormularioRespuesta; message?: string }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/formularios/respuestas/${id}/llenar`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async adjuntarFormulario(payload: { respuesta_id: number; campo_clave?: string; kind?: FormularioMomento; name?: string; file: string }): Promise<{ status: string; adjunto: FormularioAdjunto; message?: string }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/formularios/adjuntar`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
// URL del binario (firma/foto) con el token de sesión en query para <img src>
|
||||
formularioArchivoUrl(url: string): string {
|
||||
const token = localStorage.getItem('skeen_token') || '';
|
||||
return `${ODOO_BASE}${url}?token=${encodeURIComponent(token)}`;
|
||||
},
|
||||
|
||||
// ---- Portal público de formularios (sin autenticación) ----
|
||||
async getFormularioPublicoConfig(token: string): Promise<FormularioPublicoConfig> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/formulario/config`, { params: { token } });
|
||||
return data;
|
||||
},
|
||||
|
||||
async getFormularioPublicoEstado(token: string): Promise<{ status: string; estado: FormularioEstado; ya_llenado: boolean; paciente: { nombre: string }; message?: string }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/formulario`, { params: { token } });
|
||||
return data;
|
||||
},
|
||||
|
||||
async llenarFormularioPublico(payload: { token: string; respuestas: Record<string, unknown>; firma_imagen?: string; firma_nombre?: string; fotos?: { campo_clave?: string; kind?: FormularioMomento; name?: string; data_url: string }[]; hp?: string }): Promise<{ status: string; message: string; errores?: string[] }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/public/formulario/llenar`, payload);
|
||||
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 });
|
||||
|
||||
Reference in New Issue
Block a user