import axios from 'axios'; const ODOO_BASE = import.meta.env.VITE_API_BASE_URL || '/api/odoo'; // ============================================================ // Auth: adjuntar token y manejar 401 globalmente // ============================================================ axios.interceptors.request.use((config) => { const token = localStorage.getItem('skeen_token'); if (token) { config.headers = config.headers || {}; (config.headers as Record).Authorization = `Bearer ${token}`; } return config; }); axios.interceptors.response.use( (res) => res, (error) => { if (error?.response?.status === 401) { const onLogin = window.location.pathname === '/login'; const isLoginCall = String(error.config?.url || '').includes('/auth/login'); if (!onLogin && !isLoginCall) { localStorage.removeItem('skeen_token'); localStorage.removeItem('skeen_user'); window.location.href = '/login'; } } return Promise.reject(error); }, ); // ============================================================ // Tipos // ============================================================ export interface DashboardStats { appointments_today: number; appointments_month: number; new_patients_month: number; total_patients: number; revenue_month: number; pending_payments: number; total_wallet_points: number; sales_today: number; // Comparativas del período anterior equivalente (deltas) prev_revenue_month?: number; prev_sales_today?: number; } export interface Appointment { id: number; reference: string; patient_id: number; patient: string; phone: string; service_id: number; service: string; service_category?: string; date: string; time: string; state: string; payment_state: string; price: number; amount_paid?: number; /** Duración efectiva en minutos (propia de la cita o la del servicio) */ duration?: number; /** Duración propia de la cita (0 = la del servicio) */ duration_override?: number; service_duration?: number; is_first_visit?: boolean; doctor_id?: number | null; doctor?: string; branch?: string; medium?: string; /** Origen de la reserva: 'internal' (default) o 'public_portal' (portal en línea) */ booking_source?: string; notes?: string; package_finished?: boolean; package_finished_date?: string | null; // 1ra confirmación (teléfono). La 2da es la llegada (state 'arrived'). phone_confirmed?: boolean; phone_confirmed_at?: string | null; phone_confirmed_by?: string | null; // Timestamp del check-in de llegada (2da confirmación) arrived_at?: string | null; // El paciente cumple años el día de la cita (badge pastel en agenda) patient_birthday?: boolean; /** Hora en float (p. ej. 9.5) tal como la guarda Odoo */ time_float?: number; /** Etiqueta del estado con el vocabulario de la clínica */ state_label?: string; } /** Motivos predefinidos de un bloqueo de agenda */ export type BloqueoMotivo = 'vacaciones' | 'junta' | 'comida' | 'personal' | 'otro'; export interface Bloqueo { id: number; doctor_id: number; doctor: string; date: string; all_day: boolean; time_from: number; time_to: number; motivo: string; motivo_tipo?: BloqueoMotivo; motivo_tipo_label?: string; motivo_label?: string; grupo?: string; created_by?: string; } /** Entrada en la lista de espera de pacientes */ export interface ListaEspera { id: number; reference: string; patient_id: number; patient: string; phone: string; doctor_id: number | null; doctor: string | null; service_id: number | null; service: string | null; priority: 'alta' | 'normal' | 'baja'; priority_label: string; franja: '' | 'manana' | 'tarde'; // Dónde espera: consultoría (médico) o cabina (tratamiento) sala?: '' | 'consultoria' | 'cabina'; sala_label?: string; date_from: string | null; date_to: string | null; notes: string; state: 'esperando' | 'agendado' | 'descartado'; cita_id: number | null; created_by: string; create_date: string | null; resolved_at: string | null; espera_dias: number; match_servicio?: boolean; match_doctor?: boolean; } /** Cambio de estado de una cita (bitácora del checklist) */ export interface AppointmentStateLog { id: number; from_state: string; from_label: string; to_state: string; to_label: string; user: string; note: string; date: string; } export type VisitaState = 'en_curso' | 'completada' | 'cancelada'; export interface VisitaInsumo { id: number; item_id: number; item: string; unit: string; qty: number; notes: string; } export interface VisitaAdjunto { id: number; kind: 'antes' | 'despues' | 'documento'; name: string; mimetype: string; notes: string; create_date: string | null; url: string; } export interface Visita { id: number; name: string; cita_id: number | null; partner_id: number; patient: string; phone: string; doctor_id: number | null; doctor: string | null; cosmetologa_id: number | null; cosmetologa: string | null; servicio_id: number | null; servicio: string | null; motivo: string; diagnostico: string; tratamiento: string; notas: string; receta_text?: string; state: VisitaState; date_start: string | null; date_end: string | null; insumos: VisitaInsumo[]; insumos_descargados: boolean; adjuntos: VisitaAdjunto[]; } export interface Patient { id: number; patient_id: string; name: string; phone: string; email: string; birth_date: string | null; age?: number; gender?: string | false; blood_type?: string | false; last_visit: string | null; total_visits: number; wallet_points: number; total_spent: number; source: string; is_vip: boolean; primary_doctor_id?: number | null; primary_doctor?: string; whatsapp?: string; has_photo?: boolean; photo_url?: string; photo?: string; // Datos personales extendidos birthplace?: string; occupation?: string; marital_status?: string; emergency_contact?: string; emergency_phone?: string; home_phone?: string; mobile?: string; address_notes?: string; referred_by?: string; patient_comments?: string; internal_notes?: string; // Historia clínica is_pregnant?: boolean; is_breastfeeding?: boolean; uses_contraceptives?: boolean; children_count?: number; kidney_problems?: boolean; back_pain?: boolean; heart_disease?: boolean; respiratory_problems?: boolean; blood_pressure?: boolean; diabetes?: boolean; thyroid?: boolean; colitis?: boolean; constipation?: boolean; liver_problems?: boolean; surgeries?: boolean; surgeries_notes?: string; varicose_veins?: boolean; migraine?: boolean; faints_with_needles?: boolean; medical_notes?: string; // Legacy medical fields allergies?: string; medical_history?: string; current_medication?: string; // Completitud de expediente expediente_completion?: number; expediente_missing?: string[]; } export interface EstadoCuenta { puntos_monedero: number; total_gastado: number; adeudo: number; visitas_total: number; } export interface PatientAdjunto { id: number; kind: 'expediente' | 'imagen'; name: string; mimetype: string; notes: string; antes_despues: 'sin_clasificar' | 'antes' | 'despues'; visita_id: number | null; cita_id: number | null; servicio_id: number | null; servicio: string; create_date: string | null; url: string; } export interface ExpedienteReciente { id: number; patient_id: string; name: string; phone: string; age: number; birth_date: string | null; primary_doctor: string | null; last_visit: string | null; internal_notes: string; expediente_count: number; imagenes_count: number; has_photo: boolean; photo_url: string; } export interface Service { id: number; code: string; name: string; category: string; price: number; package_price?: number; package_sessions?: number; package_notes?: string; duration_min: number; description: string; color: string; service_group: string; is_favorite: boolean; sequence?: number; sold_90d?: number | null; genera_monedero?: boolean; /** Impuesto propio del servicio (%) y si usa la tasa por defecto de la config */ tax_rate?: number; tax_use_default?: boolean; } export interface ImpactSimulationDoctor { doctor_id: number; doctor: string; job_title: string; base: number; commission_pct_propio: number; commission_pct_efectivo: number; origen_pct: 'propio' | 'global' | 'propuesto'; commission: number; } export interface ImpactSimulation { status: string; date_from: string | null; date_to: string | null; commission_pct_global: number; commission_pct_propuesto: number | null; doctores: ImpactSimulationDoctor[]; total_base: number; total_commission: number; ventas: number; rate: number | null; rate_actual: number | null; total_usd: number | null; message?: string; } export interface ConfigAuditRow { id: number; date: string | null; user_login: string; user_name: string; user_role: string; module: string; module_label: string; entity: string; entity_label: string; field: string; old_value: string; new_value: string; notes: string; } export interface ConfigAuditResponse { status: string; registros: ConfigAuditRow[]; total: number; page: number; page_size: number; total_pages: number; /** Módulos posibles según la selección del modelo (fallback: MODULOS local) */ modulos?: { value: string; label: string }[]; message?: string; } export interface Doctor { id: number; name: string; /** false = dado de baja: no aparece en agenda, pacientes ni ventas */ active?: boolean; job_title: string; work_phone: string; work_email: string; commission_pct: number; staff_type?: 'doctor' | 'cosmetologa'; monthly_goal?: number; month_total?: number; horario?: { day_of_week: string; hour_from: number; hour_to: number }[]; tiene_horario?: boolean; horario_resumen?: string; patient_count?: number; recomendaciones_30d?: number; ventas_30d?: number; total_30d?: number; total_30d_usd?: number | null; card_30d?: number; } export interface DoctorHistoryCita { id: number; date: string | null; time: string; patient: string; service: string; state: string; } export interface DoctorHistoryVisita { id: number; date_start: string | null; patient: string; motivo: string; state: string; } export interface DoctorHistoryVenta { id: number; venta: string; date: string | null; patient: string; item: string; subtotal: number; state: string; } export interface DoctorHistory { status: string; citas: DoctorHistoryCita[]; visitas: DoctorHistoryVisita[]; ventas: DoctorHistoryVenta[]; totales: { citas_total: number; visitas_total: number; ventas_total_lineas: number; monto_total: number; }; } export interface Product { id: number; name: string; list_price: number; default_code: string; type: string; } export interface Wallet { id: number; patient_id: number; patient: string; phone: string; points: number; equivalent_mxn: number; } export interface Payment { id: number; name: string; patient: string; patient_id: number; amount: number; payment_method: string; state: string; payment_url?: string; provider_reference?: string; create_date?: string; } export interface SaleLine { id?: number; service_id: number; service?: string; item_id?: number | null; item?: string; description: string; quantity: number; price_unit: number; subtotal: number; /** Tasa (%) e importe del impuesto de la línea (0 con impuestos apagados) */ tax_rate?: number; tax_amount?: number; is_prescribed?: boolean; prescribed_by_id?: number | null; prescribed_by?: string | null; /** La línea vendió un paquete de sesiones: cantidad y precio no se editan * libremente (el saldo del paciente depende de ellos). */ paquete_id?: number | null; paquete?: string; sesiones?: number; } /** Edición de un ticket ya emitido (gerencia): reemplaza líneas e importes. */ export interface SaleEditLine { id?: number; service_id?: number | null; item_id?: number | null; paquete_id?: number | null; description: string; quantity: number; price_unit: number; prescribed_by_id?: number | null; } export interface SaleEditResult { prev_total: number; total: number; diff: number; points_adjusted: number; stock_moves: number; packages_cancelled: number; warnings: string[]; } export interface CommissionRow { doctor_id: number; doctor: string; job_title: string; commission_pct: number; base: number; commission: number; items: number; sales: number; } export interface Sale { id: number; name: string; patient_id: number; patient: string; /** Teléfono y médico del ticket: los muestra el diseño impreso si están activos */ patient_phone?: string; doctor?: string; date: string; subtotal: number; discount: number; tax: number; /** Impuesto sumado de las líneas y base sin impuesto (0 = sin impuestos) */ tax_total?: number; subtotal_sin_impuesto?: number; total: number; amount_paid: number; amount_due: number; state: string; lines: SaleLine[]; refunded?: boolean; refund_amount?: number; refund_reason?: string; refunded_at?: string | null; notes?: string; cancel_reason?: string; cancelled_by?: string; cancelled_at?: string | null; cancel_points_reversed?: number; cancel_points_restored?: number; /** Cobro: quién cobró y con qué método (módulo Tickets) */ payment_method?: string; user_login?: string; /** Pagos ligados al ticket (los nuevos; los antiguos no tienen vínculo) */ pagos?: Payment[]; } // "Repetir última venta" (POS): línea de la última venta con el precio vigente export interface LastSaleLine { kind: 'service' | 'item'; service_id: number | null; item_id: number | null; name: string; description: string; quantity: number; price_last: number; price_current: number | null; price_changed: boolean; missing: boolean; active: boolean; stock: number | null; unit: string | null; } export interface LastSaleResponse { status: string; sale: Sale | null; lines: LastSaleLine[]; warnings: string[]; } export interface PatientPackage { id?: number | null; compra?: boolean; service_id: number; name: string; package_sessions: number; total: number; used: number; remaining: number; next_date: string | null; state?: string; } // ---------- Paquetes de sesiones ---------- export interface Paquete { id: number; name: string; code: string; service_id: number | null; service: string; sessions: number; price: number; price_per_session: number; session_price: number; duration_min: number; validity_days: number; notes: string; color: string; genera_monedero: boolean; active: boolean; sequence: number; compras_count: number; } export interface PaqueteCompra { id: number; date: string | null; patient_id: number; patient: string; phone: string; paquete_id: number | null; paquete: string; service_id: number | null; service: string; sessions_total: number; sessions_used: number; sessions_remaining: number; price: number; expiry_date: string | null; state: 'active' | 'finished' | 'expired' | 'cancelled'; venta_id: number | null; venta: string; user_login: string; } export interface PaqueteConsumo { id: number; date: string | null; state: string; cita_id: number | null; cita_fecha: string | null; cita_hora: string; doctor: string; estado_cita: string; } export interface PaquetePayload { name?: string; code?: string; service_id?: number; sessions?: number; price?: number; duration_min?: number; validity_days?: number; notes?: string; color?: string; genera_monedero?: boolean; active?: boolean; } export interface MySaleItem { id: number; name: string; date: string; patient: string; items: string[]; amount: number; payment_method: string; state: string; } export interface MySalesResponse { status: string; medico_sin_employee?: boolean; doctor?: { id: number; name: string }; range?: string; date_from?: string; date_to?: string; kpis: { ventas: number; total_mxn: number; total_usd: number | null; total_card: number }; sales: MySaleItem[]; daily_breakdown?: { date: string; total: number }[]; commission_pct: number; commission_source?: string; commission_estimate: number; monthly_goal?: { goal: number; real: number; pct: number | null; from: string; to: string }; } export interface HorarioDia { day_of_week: string; hour_from: number; hour_to: number; } export interface CashClosing { id: number; name: string; date: string; user: string | false; opening_cash: number; total_cash: number; total_card: number; total_transfer: number; total_other: number; total_sales: number; closing_cash: number; difference: number; counted_cash: number; counted_card: number; counted_transfer: number; counted_other: number; diff_cash: number; diff_card: number; diff_transfer: number; diff_other: number; notes: string; state: string; } export interface ChartPoint { name: string; appointments: number; revenue: number; patients: number; } export interface Birthday { id: number; name: string; phone: string; whatsapp?: string; birth_date: string; occurs_on: string; turning_age: number; last_visit: string | null; total_spent: number; amount_due: number; is_vip: boolean; } export type InventoryKind = 'producto' | 'consumible'; export type InventoryLevel = 'out' | 'critical' | 'low' | 'optimal'; export type InventoryMoveType = 'compra' | 'venta' | 'baja' | 'ajuste' | 'cancelacion'; export interface InventoryItem { id: number; name: string; kind: InventoryKind; sku: string; category: string; unit: string; qty: number; qty_optimal: number; qty_min: number; cost: number; price?: number; inventory_value: number; expiry_date: string | null; last_count_date: string | null; active: boolean; genera_monedero?: boolean; /** Impuesto propio del artículo (%) y si usa la tasa por defecto */ tax_rate?: number; tax_use_default?: boolean; notes: string; stock_level: InventoryLevel; } export interface InventoryMove { id: number; item_id: number; type: InventoryMoveType; qty: number; before_qty: number; after_qty: number; date: string | null; reference: string; notes: string; } export interface InventorySummary { levels: Record; total_value: number; count: number; } // WACRM export interface WacrmConversation { id: number; external_id: string; contact_id?: string; contact_name: string; contact_phone: string; status: string; assigned_agent: string; assigned_agent_id: string; last_message_text: string; last_message_at: string | null; unread_count: number; created_at: string | null; tags?: { id: number; name: string; color: string }[]; } export interface WacrmTag { id: number; name: string; color: string; sequence: number; active: boolean; } export interface WacrmNote { id: number; body: string; author: string; date: string | null; resolved: boolean; resolved_at: string | null; resolved_by: string; } export interface WacrmMetrics { status: string; generado: string; periodo: { desde: string; hasta: string }; sin_respuesta_horas: number; conversaciones: { hoy: number; semana: number; mes: number; total: number }; mensajes: { hoy: { entrantes: number; salientes: number }; semana: { entrantes: number; salientes: number }; mes: { entrantes: number; salientes: number }; }; primera_respuesta: { muestras: number; promedio_min: number | null; mediana_min: number | null }; sin_responder: { total: number; horas_minimas: number; detalle: { conversation_id: string; horas: number; ultimo_mensaje: string }[] }; leads: { ganados: number; perdidos: number; abiertos: number; valor_ganado: number }; criterios: Record; message?: string; } export interface WacrmMessage { id: number; external_id: string; conversation_id: string; contact_name: string; contact_phone: string; sender_type: 'customer' | 'agent' | 'bot'; content_type: string; content_text: string; media_url: string; template_name: string; status: string; created_at: string | null; } export interface WacrmStage { id: number; external_id: string; name: string; position: number; color: string; } export interface WacrmPipeline { id: number; external_id: string; name: string; stages: WacrmStage[]; } export interface WacrmLead { id: number; external_id: string; title: string; contact_id?: string; contact_name: string; contact_phone: string; pipeline: string; pipeline_id?: number; stage: string; stage_id?: number; stage_color: string; value: number; currency: string; status: 'open' | 'won' | 'lost'; expected_close_date: string | null; notes: string; assigned_to: string; partner_id?: number | null; conversation_id?: string; created_at: string | null; } // Los roles son configurables (modelo skeen.role): el código es libre, por eso // el tipo ya no es una unión cerrada. ROLE_LABELS queda solo como respaldo. export type FrontendRole = string; // Acciones de la Agenda (espejo de AGENDA_PERMS en skeen_whatsapp/roles.py). export type AgendaPerm = 'ver' | 'agendar' | 'editar' | 'eliminar' | 'visitas'; export const AGENDA_PERM_ITEMS: { key: AgendaPerm; label: string; nota: string }[] = [ { key: 'ver', label: 'Consultar agenda', nota: 'Siempre activa: sin ella no hay pantalla que ver.' }, { key: 'agendar', label: 'Agendar cita', nota: 'Cita normal, cita rápida, lista de espera y cierres de agenda.' }, { key: 'editar', label: 'Editar citas', nota: 'Reprogramar, mover de horario y marcar confirmaciones.' }, { key: 'eliminar', label: 'Eliminar citas', nota: 'Borrar una cita o cancelarla.' }, { key: 'visitas', label: 'Generar visitas', nota: 'Check-in (En cabina) y todo el expediente del día.' }, ]; // Brand de la clínica (white-label). El backend resuelve herencia: si la // clínica no configuró un campo, devuelve el default de SKEEN. export interface ClinicBrand { nombre: string; logo_claro: string; // fondos oscuros (login SKEEN, sidebar) logo_oscuro: string; // fondos claros (tema HomeNest) pie: string; // línea del pie (login / documentos) url_publica?: string; // URL pública de la instancia (opcional) // Colores de marca (opcional). Vacíos = se usa el color del tema activo. colores?: { primario?: string; acento?: string }; } export const BRAND_DEFAULT: ClinicBrand = { nombre: 'Clínica', logo_claro: '/brand/logo-blanco.svg', logo_oscuro: '/brand/logo-negro.svg', pie: 'Sistema de gestión clínica', }; // ---- Glosario / terminología white-label ---- export const GLOSSARY_KEYS = [ 'patient_singular', 'patient_plural', 'appointment_singular', 'appointment_plural', 'visit_singular', 'visit_plural', 'record_singular', 'record_plural', 'doctor_singular', 'doctor_plural', 'staff_singular', 'staff_plural', 'branch_singular', 'branch_plural', 'room_singular', 'room_plural', 'service_singular', 'service_plural', 'package_singular', 'package_plural', 'prescription_singular', 'prescription_plural', 'treatment_singular', 'treatment_plural', ] as const; export type GlossaryKey = typeof GLOSSARY_KEYS[number]; export type GlossaryTerms = Record; export interface GlossaryPack { code: string; name: string; } // ---- Sucursales ---- export interface Sucursal { id: number; code: string; name: string; sequence: number; active: boolean; is_default: boolean; address: string; phone: string; notes: string; } // ---- Categorías de servicio ---- export interface ServiceCategory { id: number; code: string; name: string; sequence: number; active: boolean; is_system: boolean; } // ---- Ajustes de la clínica ---- export interface ImpuestosSettings { /** Enciende el cálculo del impuesto en POS, tickets y reportes */ activo: boolean; /** Etiqueta del impuesto (p. ej. 'IVA') */ nombre: string; /** % aplicado cuando el catálogo no fija uno */ tasa_default: number; /** El precio capturado ya trae impuesto: el subtotal se desglosa hacia atrás */ precios_incluyen_impuesto: boolean; /** Tasas ofrecidas en los formularios (%) */ tasa_opciones: number[]; } export const IMPUESTOS_DEFAULT: ImpuestosSettings = { activo: false, nombre: 'IVA', tasa_default: 0, precios_incluyen_impuesto: false, tasa_opciones: [0, 8, 16], }; export interface ClinicSettings { nombre: string; telefono: string; email: string; direccion: string; web: string; rfc: string; horario_apertura: string; horario_cierre: string; zona_horaria: string; url_publica: string; moneda_base: string; nota_ticket: string; impuestos: ImpuestosSettings; reservas_online: ReservasOnlineSettings; notificaciones: NotificacionesSettings; } // ---- Reservas en línea (portal público de auto-agendado) ---- export interface ReservasOnlineSettings { activo: boolean; titulo: string; descripcion: string; requiere_confirmacion: boolean; permitir_eleccion_doctor: boolean; permitir_sucursal: boolean; max_dias_futuro: number; min_horas_anticipacion: number; permitir_cancelar: boolean; cancelar_hasta_horas: number; mensaje_confirmacion: string; telefono_contacto: string; } export const RESERVAS_ONLINE_DEFAULT: ReservasOnlineSettings = { activo: false, titulo: 'Reserva tu cita en línea', descripcion: '', requiere_confirmacion: true, permitir_eleccion_doctor: true, permitir_sucursal: false, max_dias_futuro: 60, min_horas_anticipacion: 2, permitir_cancelar: true, cancelar_hasta_horas: 24, mensaje_confirmacion: '', 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; /** Aviso de un formulario digital: usa {formulario} y {enlace}. */ plantilla_formulario: 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}.', 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', 'enlace', 'formulario'] 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; 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; brand: ClinicBrand; terms: GlossaryTerms; sucursales: { code: string; name: string; address?: string; phone?: string }[]; categorias: { code: string; name: string }[]; servicios: { id: number; nombre: string; categoria: string; precio: number; duracion_min: number }[]; medicos: { id: number; nombre: string; staff_type: string }[]; config: { max_dias_futuro: number; min_horas_anticipacion: number; permite_sucursal: boolean; permite_eleccion_doctor: boolean; requiere_confirmacion: boolean; permitir_cancelar: boolean; cancelar_hasta_horas: number; telefono_contacto: string; titulo: string; descripcion: string; mensaje_confirmacion: string; zona_horaria?: string; }; } export interface BookingCreatePayload { servicio_id: number; fecha: string; hora: string; medico_id?: number | null; sucursal?: string | null; nombre: string; telefono: string; email?: string; notas?: string; hp?: string; } export interface BookingCreateResult { ok: boolean; folio: string; fecha: string; hora: string; medico: string | null; servicio: string; sucursal: string; estado: string; requiere_confirmacion: boolean; mensaje_confirmacion: string; token_de_gestion: string; } export interface BookingCita { status: string; folio: string; fecha: string; hora: string; servicio: string; medico: string | null; sucursal: string; estado: string; estado_label: string; puede_cancelar: boolean; permitir_cancelar: boolean; cancelar_hasta_horas: number; 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; 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: '', email: '', direccion: '', web: '', rfc: '', horario_apertura: '08:00', horario_cierre: '19:00', zona_horaria: 'America/Tijuana', url_publica: '', moneda_base: 'MXN', nota_ticket: '', impuestos: IMPUESTOS_DEFAULT, reservas_online: RESERVAS_ONLINE_DEFAULT, notificaciones: NOTIFICACIONES_DEFAULT, }; export interface FrontendUser { id: number; login: string; name: string; role: FrontendRole; // Nombre del rol configurado en skeen.role (para roles personalizados). role_name?: string; must_change_password?: boolean; last_login?: string | null; active?: boolean; allowed_menus?: string[] | null; // Acciones de Agenda que el backend resolvió (lista efectiva del usuario). agenda_perms?: AgendaPerm[] | null; // Lista propia del usuario; null = hereda del rol. La usa solo el editor. agenda_perms_custom?: AgendaPerm[] | null; // Permisos por acción vigentes (rol + overrides), resueltos por el backend. perms?: Record | null; // Solo rol 'medico': hr.employee vinculado (null si no hay vínculo) employee_id?: number | null; employee_name?: string | null; } // ---- Roles configurables (skeen.role) ---- export interface Role { id: number; code: string; name: string; rank: number; sequence: number; active: boolean; is_system: boolean; description: string; } export interface RoleUserRef { code: string; name: string; rank: number; active: boolean; is_system: boolean; } // ---- Permisos por acción ---- export interface PermItem { key: string; label: string; } export interface PermGroup { grupo: string; label: string; permisos: PermItem[]; } export interface PermsResponse { status: string; catalogo: PermGroup[]; por_rol: Record>; defaults: Record>; roles: RoleUserRef[]; message?: string; } export interface Receta { id: number; name: string; categoria: string; contenido: string; } // ---- Chat interno ---- export interface ChatUser { id: number; name: string; role: FrontendRole; unread: number; last_message: string | null; last_date: string | null; last_mine: boolean | null; online: boolean; last_seen: string | null; } export interface ChatMessage { id: number; sender_id: number; recipient_id: number; body: string; is_read: boolean; mine: boolean; date: string | null; } export interface Diagnostico { id: number; name: string; categoria: string; descripcion: string; } export interface Procedimiento { id: number; name: string; categoria: string; descripcion: string; } export interface AdeudoRow { id: number; name: string; phone: string; num_ventas: number; total_adeudo: number; ultima_venta: string | null; plan?: PaymentPlan | null; } export interface PaymentPlan { count: number; amount: number; next_payment_date: string | null; note: string; start: string | null; by: string; paid_count: number; remaining_count: number; } export interface ReceiptApplied { id: number; name: string; date: string | null; applied: number; total: number; state: string; amount_due: number; } export interface ReceiptData { folio: string; date: string; patient_id: number; patient: string; patient_code: string; phone: string; amount: number; payment_method: string; adeudo_anterior: number; saldo_restante: number; aplicado: ReceiptApplied[]; operator: string; wallet_points: number; /** Recibo de recarga de saldo a favor (tipo 'deposito') */ tipo?: 'pago' | 'deposito'; saldo_favor?: number; nota?: string; } export type WalletTxType = 'deposito' | 'accrual' | 'redemption' | 'adjustment' | 'reversion' | 'expiration'; /** Saldo a favor del paciente (monedero: 1 pt = $1 MXN) */ export interface PatientWallet { status: string; patient_id: number; patient: string; wallet_id: number | null; points: number; equivalent_mxn: number; equivalent_usd: number | null; rate: number | null; depositado: number; movimientos: WalletTransaction[]; message?: string; } export interface WalletTransaction { id: number; type: WalletTxType; type_label: string; points: number; narration: string; description: string; reference: string; operator: string; date: string | null; } export interface InventoryConteoLine { id: number; item_id: number; item: string; unit: string; qty_sistema: number; qty_contado: number; difference: number; cost: number; value_diff: number; move_id: number | null; } export interface InventoryConteo { id: number; name: string; date: string | null; user_name: string; notes: string; total_items: number; total_diferencias: number; total_faltante: number; total_sobrante: number; total_value_diff: number; lines?: InventoryConteoLine[]; } export interface ComisionRow { doctor_id: number; doctor: string; job_title: string; items: number; sales: number; citas_done: number; total_vendido: number; commission_pct: number; comision: number; } export interface MovimientoRow extends InventoryMove { item: string; unit: string; } export interface CaducidadRow { id: number; name: string; category: string; qty: number; unit: string; expiry_date: string; dias_restantes: number; vencido: boolean; } export interface SugerenciaRow { id: number; name: string; category: string; unit: string; qty: number; qty_min: number; qty_optimal: number; sugerido: number; costo_estimado: number; } export interface PagoServicioRow { servicio: string; cantidad: number; total: number; } export interface PagoClienteRow { id: number; name: string; num_pagos: number; total_cobrado: number; ultimo_pago: string | null; } export interface DevolucionRow { id: number; folio: string; paciente: string; fecha: string | null; total: number; refund_amount: number; refund_reason: string; refunded_at: string | null; } export interface TopClienteRow { id: number; name: string; num_ventas: number; total: number; ticket_promedio: number; } export interface DailyPorMedico { medico: string; citas: number; by_state: Record; ventas: number; cobros: number; } export interface DailyPorRecepcion { usuario: string; ventas_count: number; ventas_total: number; cobros_total: number; } export interface HoraAgendaRow { hora: string; total: number; by_state: Record; } export interface PaqueteRow { id: number; name: string; package_sessions: number; package_price: number; price: number; citas: number; terminados: number; } export interface VendedorRow { usuario: string; num_ventas: number; total: number; cobrado: number; } export interface ConcentradoReport { start: string; end: string; citas: { total: number; by_state: Record }; ventas: { count: number; total: number; total_paid: number; total_due: number }; pagos: { total: number; count: number; by_method: Record }; devoluciones: { count: number; total: number }; pacientes_nuevos: number; visitas: { total: number; by_state: Record }; } export interface ConcentradoDetalleRow { folio: string; fecha: string; paciente: string; folio_dr: string | null; medico: string; vendedor: string; concepto: string; efectivo_usd: number; efectivo_mxn: number; tarjeta_usd: number; tarjeta_mxn: number; transferencia_usd: number; transferencia_mxn: number; saldo_favor_usd: number; saldo_favor_mxn: number; monedero_usd: number; monedero_mxn: number; } export type ConcentradoDetalleTotals = Record; export interface ConcentradoDetalleReport { status: string; start: string; end: string; rows: ConcentradoDetalleRow[]; totals: ConcentradoDetalleTotals; pacientes_atendidos: number; ticket_promedio: number; } export interface RecomendacionRow { recomendador: string; pacientes: number; } export interface MonederoPuntosReport { by_type: Record; puntos_activos: number; cuentas: number; equivalente_mxn: number; } export interface KpisNoShowMedico { medico: string; citas: number; no_shows: number; tasa: number; } export interface KpisOcupacionMedico { medico: string; horas_vendidas: number; horas_disponibles: number; horas_libres: number; ocupacion: number; } export interface KpisReport { start: string; end: string; no_show: { tasa: number; total_citas: number; total_no_shows: number; por_medico: KpisNoShowMedico[] }; ocupacion: { promedio: number; dias_habiles: number; por_medico: KpisOcupacionMedico[] }; leads: { total: number; ganados: number; tasa: number; dias_promedio_ganar: number | null }; primera_vez: { primera: number; subsecuentes: number; tasa: number }; } export interface AppNotification { tipo: string; titulo: string; detalle: string; link: string; severity: 'info' | 'warning' | 'danger'; } export interface PosCheckoutPayload { partner_id: number; lines: { service_id?: number; item_id?: number; paquete_id?: number; quantity: number; price_unit: number; description?: string; prescribed_by_id?: number; tax_rate?: number }[]; discount?: number; /** Tasa de impuesto a nivel venta (opcional); cada línea puede traer la suya */ tax_rate?: number; payment_method?: string; amount_received?: number; pay_with_points?: boolean; currency?: 'MXN' | 'USD'; } export interface ImpuestosDesglose { activo: boolean; nombre: string; incluido: boolean; subtotal: number; descuento: number; impuesto: number; total: number; subtotal_usd?: number; impuesto_usd?: number; total_usd?: number; } export interface PosCheckoutResult { status: string; sale: Sale; payment: Payment | null; /** Desglose de la venta (subtotal, descuento, impuesto, total) */ subtotal?: number; descuento?: number; impuesto?: number; total?: number; impuestos?: ImpuestosDesglose; cambio: number; /** Efectivo recibido en caja (MXN), para imprimir "Recibido" en el ticket */ recibido?: number; cambio_usd?: number; rate_used?: number | null; recibido_usd?: number; puntos_usados: number; puntos_ganados: number; wallet_points: number; stock_warnings?: string[]; } export interface DailyReport { date: string; citas: { total: number; by_state: Record }; ventas: { count: number; total: number; total_paid: number; total_due: number }; pagos: { total: number; count: number; by_method: Record }; inventario: { total: number; by_type: Record }; } export interface InventoryReportSummary { total_value: number; count: number; out: number; critical: number; } // ---- Reporte de impuestos ---- export interface TaxMonthRow { mes: string; base: number; impuesto: number; ventas: number; } export interface TaxServiceRow { servicio: string; base: number; impuesto: number; cantidad: number; } export interface TaxBranchRow { sucursal: string; base: number; impuesto: number; } export interface TaxReportResult { status: string; start: string; end: string; activo: boolean; nombre: string; incluido: boolean; total_base: number; total_impuesto: number; total: number; por_mes: TaxMonthRow[]; por_servicio: TaxServiceRow[]; por_sucursal: TaxBranchRow[]; } export interface ApiResponse { status: string; message?: string; data?: T; } export interface Paginated { status: string; total: number; page: number; page_size: number; total_pages: number; items: T; } // ============================================================ // API // ============================================================ export const odooApi = { // Auth (frontend React) async login(login: string, password: string): Promise<{ status: string; token: string; user: FrontendUser; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/auth/login`, { login, password }); return data; }, async me(): Promise<{ status: string; user: FrontendUser }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/auth/me`); return data; }, async logout(): Promise<{ status: string }> { try { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/auth/logout`); return data; } catch { return { status: 'success' }; } }, async listFrontendUsers(): Promise<{ status: string; users: FrontendUser[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/auth/users`); return data; }, async createFrontendUser(payload: { login: string; name: string; password: string; role: FrontendRole }): Promise<{ status: string; user: FrontendUser; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/auth/users`, payload); return data; }, async updateFrontendUser(id: number, payload: { name?: string; login?: string; role?: FrontendRole; active?: boolean; password?: string; must_change_password?: boolean; allowed_menus?: string[] | null; agenda_perms?: AgendaPerm[] | null }): Promise<{ status: string; user: FrontendUser; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/auth/users/${id}`, payload); return data; }, async deleteFrontendUser(id: number): Promise<{ status: string; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/auth/users/${id}`); return data; }, async getRolePermissions(): Promise<{ status: string; matrix: Record; roles: { key: FrontendRole; label: string }[]; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/roles/permissions`); return data; }, // Brand de la clínica (white-label): nombre, logos y pie desde Config. async getBrand(): Promise<{ status: string; brand: ClinicBrand; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/brand`); return data; }, async putBrand(payload: { brand: Partial; motivo?: string }): Promise<{ status: string; brand: ClinicBrand; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/brand`, payload); return data; }, // ---- Glosario / terminología ---- async getGlosario(): Promise<{ status: string; pack: string; terms: GlossaryTerms; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/glosario`); return data; }, async getGlosarioPacks(): Promise<{ status: string; packs: GlossaryPack[]; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/glosario/packs`); return data; }, async putGlosario(payload: { pack?: string; overrides?: Partial; motivo?: string }): Promise<{ status: string; pack: string; terms: GlossaryTerms; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/glosario`, payload); return data; }, // ---- Sucursales ---- async getSucursales(opts?: { includeInactive?: boolean }): Promise<{ status: string; sucursales: Sucursal[]; total: number; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/sucursales`, { params: opts?.includeInactive ? { include_inactive: 1 } : undefined, }); return data; }, async createSucursal(payload: Partial): Promise<{ status: string; sucursal: Sucursal; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sucursales`, payload); return data; }, async updateSucursal(id: number, payload: Partial): Promise<{ status: string; sucursal: Sucursal; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/sucursales/${id}`, payload); return data; }, async deleteSucursal(id: number): Promise<{ status: string; sucursal: Sucursal; en_uso: number; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/sucursales/${id}`); return data; }, // ---- Categorías de servicio ---- async getServiceCategories(opts?: { includeInactive?: boolean }): Promise<{ status: string; categories: ServiceCategory[]; total: number; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/service-categories`, { params: opts?.includeInactive ? { include_inactive: 1 } : undefined, }); return data; }, async createServiceCategory(payload: { name: string; code?: string; sequence?: number; active?: boolean }): Promise<{ status: string; category: ServiceCategory; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/service-categories`, payload); return data; }, async updateServiceCategory(id: number, payload: { name?: string; code?: string; sequence?: number; active?: boolean }): Promise<{ status: string; category: ServiceCategory; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/service-categories/${id}`, payload); return data; }, async deleteServiceCategory(id: number): Promise<{ status: string; category?: ServiceCategory; en_uso?: number; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/service-categories/${id}`); return data; }, // ---- Ajustes de la clínica ---- async getClinicSettings(): Promise<{ status: string; settings: ClinicSettings; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/clinic-settings`); return data; }, async putClinicSettings(settings: Partial & { motivo?: string }): Promise<{ status: string; settings: ClinicSettings; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/clinic-settings`, settings); return data; }, // ---- Portal público de reservas en línea (sin autenticación) ---- async getBookingConfig(): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/booking/config`); return data; }, async getBookingSlots(params: { servicio_id: number; fecha: string; medico_id?: number | null; sucursal?: string | null }): Promise<{ status: string; slots: string[]; medico_id: number | null; duracion_min: number; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/booking/slots`, { params }); return data; }, async createBooking(payload: BookingCreatePayload): Promise { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/public/booking/create`, payload); return data; }, async getBookingCita(token: string): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/booking/cita`, { params: { token } }); return data; }, async cancelBooking(token: string, motivo?: string): Promise<{ status: string; ok?: boolean; folio?: string; estado?: string; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/public/booking/cancel`, { token, motivo }); return data; }, // ---- Formularios digitales: plantillas ---- async getFormularioPlantillas(opts?: { includeInactive?: boolean; tipo?: string }): Promise<{ status: string; plantillas: FormularioPlantilla[]; total: number; message?: string }> { const params: Record = {}; 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 & { 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 & { 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): Promise<{ status: string; respuestas: FormularioRespuesta[]; total: number; page: number; page_size: number; total_pages: number; resumen: Record; 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; 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 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 { 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; 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): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/notificaciones`, { params }); return data; }, async procesarNotificaciones(limit?: number): Promise { 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 { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/probar`, payload); return data; }, async updateRolePermissions(matrix: Record): Promise<{ status: string; matrix: Record; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/roles/permissions`, { matrix }); return data; }, // ---- Roles configurables ---- async listRoles(opts?: { includeInactive?: boolean }): Promise<{ status: string; roles: Role[]; total: number; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/roles`, { params: opts?.includeInactive ? { include_inactive: 1 } : undefined, }); return data; }, async createRole(payload: { name: string; rank?: number; sequence?: number; description?: string; active?: boolean }): Promise<{ status: string; role: Role; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/roles`, payload); return data; }, async updateRole(id: number, payload: { name?: string; rank?: number; sequence?: number; active?: boolean; description?: string }): Promise<{ status: string; role: Role; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/roles/${id}`, payload); return data; }, async deleteRole(id: number): Promise<{ status: string; role?: Role; en_uso?: number; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/roles/${id}`); return data; }, // ---- Permisos por acción ---- async getPermisos(): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/permisos`); return data; }, async putPermisos(payload: { role_code?: string; permisos?: Record; por_rol?: Record> }): Promise<{ status: string; por_rol: Record>; guardados: number; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/permisos`, payload); return data; }, // Health async healthCheck(): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/health`); return data; }, // Dashboard async getDashboard(): Promise<{ status: string; stats: DashboardStats }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/dashboard`); return data; }, async getWeeklyChart(): Promise<{ status: string; data: ChartPoint[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/weekly_chart`); return data; }, // Citas async getAppointments(params?: Record): Promise<{ status: string; appointments: Appointment[]; bloqueos?: Bloqueo[]; lista_espera?: ListaEspera[]; total?: number; page?: number; page_size?: number; total_pages?: number; medico_sin_employee?: boolean }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments`, { params }); return data; }, async createAppointment(appointment: Partial & { sessions?: number }): Promise<{ status: string; appointment: Appointment; appointments?: Appointment[]; sessions?: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments`, appointment); return data; }, async createQuickAppointment(payload: { name: string; phone: string; date: string; time: number; doctor_id?: number; notes?: string; duration?: number; branch?: string }): Promise<{ status: string; appointment: Appointment; patient: Patient; patient_created: boolean; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/quick`, payload); return data; }, async updateAppointment(id: number, appointment: Partial): Promise<{ status: string; appointment: Appointment }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}`, appointment); return data; }, async deleteAppointment(id: number): Promise<{ status: string; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}`); return data; }, async getAppointment(id: number): Promise<{ status: string; appointment: Appointment }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}`); return data; }, async bulkPhoneConfirm(ids: number[], confirmed = true): Promise<{ status: string; confirmed_count: number; skipped_invalid_state: number[]; not_found: number[] }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/bulk_phone_confirm`, { ids, confirmed }); return data; }, async checkPatientPhone(phone: string, excludeId?: number): Promise<{ status: string; exists: boolean; patient: { id: number; name: string; patient_id: string; phone: string; mobile: string } | null }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/check_phone`, { phone, exclude_id: excludeId }); return data; }, async updateAppointmentStatus(id: number, action: string, reason?: string, state?: string): Promise<{ status: string; appointment: Appointment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}/status`, { action, reason, state }); return data; }, /** Bitácora de cambios de estado de la cita (checklist con retroceso) */ async getAppointmentStateHistory(id: number): Promise<{ status: string; history: AppointmentStateLog[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}/state_history`); return data; }, async packageFinished(id: number, finished = true): Promise<{ status: string; appointment: Appointment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}/package_finished`, { finished }); return data; }, async phoneConfirm(id: number, confirmed: boolean): Promise<{ status: string; appointment: Appointment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}/phone_confirm`, { confirmed }); return data; }, // Chat interno entre usuarios de la plataforma async getChatUsers(): Promise<{ status: string; users: ChatUser[] }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/chat/users`); return data; }, async getChatMessages(userId: number, sinceId?: number, aroundId?: number): Promise<{ status: string; messages: ChatMessage[]; around_id?: number | null }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/chat/messages`, { user_id: userId, since_id: sinceId || undefined, around_id: aroundId || undefined, }); return data; }, /** Búsqueda en el historial con otro usuario (servidor). */ async searchChatMessages(userId: number, q: string, limit = 50): Promise<{ status: string; query: string; count: number; results: ChatMessage[] }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/chat/search`, { user_id: userId, q, limit }); return data; }, /** Latido de presencia (se manda cada ~30 s con la app abierta). */ async chatHeartbeat(): Promise<{ status: string; online_count: number; online_window_seconds: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/chat/heartbeat`); return data; }, async sendChatMessage(userId: number, message: string): Promise<{ status: string; message: ChatMessage }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/chat/send`, { user_id: userId, message }); return data; }, async getChatUnreadCount(): Promise<{ status: string; unread: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/chat/unread_count`); return data; }, async getAvailableSlots(date: string, servicio_id: number, doctor_id?: number, duration?: number): Promise<{ status: string; slots: { time: number; time_str: string }[] }> { const params: Record = { date, servicio_id }; if (doctor_id) params.doctor_id = doctor_id; if (duration) params.duration = duration; const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments/slots`, { params }); return data; }, // Bloqueos de agenda async getBloqueos(params?: Record): Promise<{ status: string; bloqueos: Bloqueo[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/bloqueos`, { params }); return data; }, async createBloqueo(payload: { doctor_ids: number[]; date_from: string; date_to?: string; days?: number[]; all_day?: boolean; time_from?: number; time_to?: number; motivo_tipo?: BloqueoMotivo; motivo?: string; force?: boolean; }): Promise<{ status: string; grupo: string; creados: number; bloqueos: Bloqueo[] }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/bloqueos`, payload); return data; }, async updateBloqueo(id: number, payload: Partial<{ doctor_id: number; date: string; all_day: boolean; time_from: number; time_to: number; motivo_tipo: BloqueoMotivo; motivo: string; force: boolean; }>): Promise<{ status: string; bloqueo: Bloqueo }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/bloqueos/${id}`, payload); return data; }, async deleteBloqueo(id: number): Promise<{ status: string; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/bloqueos/${id}`); return data; }, /** Elimina la serie completa de bloqueos creados juntos (mismo grupo) */ async deleteBloqueoGrupo(grupo: string): Promise<{ status: string; eliminados: number }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/bloqueos/grupo/${grupo}`); return data; }, // Lista de espera de pacientes async getListaEspera(params?: Record): Promise<{ status: string; lista: ListaEspera[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/lista_espera`, { params }); return data; }, async createListaEspera(payload: { partner_id: number; doctor_id?: number | null; service_id?: number | null; priority?: string; franja?: string; date_from?: string; date_to?: string; notes?: string; sala?: string; }): Promise<{ status: string; registro: ListaEspera }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/lista_espera`, payload); return data; }, async updateListaEspera(id: number, payload: Partial<{ state: 'esperando' | 'agendado' | 'descartado'; cita_id: number; priority: string; franja: string; date_from: string; date_to: string; notes: string; doctor_id: number; service_id: number; sala: string; }>): Promise<{ status: string; registro: ListaEspera }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/lista_espera/${id}`, payload); return data; }, async deleteListaEspera(id: number): Promise<{ status: string; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/lista_espera/${id}`); return data; }, /** Pacientes en espera compatibles con un hueco que se liberó */ async getListaEsperaCompatibles(params: { doctor_id?: number; servicio_id?: number; date?: string }): Promise<{ status: string; lista: ListaEspera[]; total: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/lista_espera/compatibles`, { params }); return data; }, // Visitas clínicas async getVisitas(params?: Record): Promise<{ status: string; visitas: Visita[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/visitas`, { params }); return data; }, async getVisita(id: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}`); return data; }, async updateVisita(id: number, visita: Partial): Promise<{ status: string; visita: Visita }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}`, visita); return data; }, async completeVisita(id: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/complete`); return data; }, async cancelVisita(id: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/cancel`); return data; }, async addVisitaInsumo(id: number, payload: { item_id: number; qty: number; notes?: string }): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/insumos`, payload); return data; }, async deleteVisitaInsumo(id: number, lineId: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/insumos/${lineId}`); return data; }, async getInventarioItems(search?: string): Promise<{ status: string; items: InventoryItem[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventory/items`, { params: search ? { search } : {} }); return data; }, async uploadVisitaAdjunto(id: number, payload: { kind: string; name: string; file_b64: string; mimetype: string; notes?: string; antes_despues?: string }): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/adjuntos`, payload); return data; }, async deleteVisitaAdjunto(adjId: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/visitas/adjuntos/${adjId}`); return data; }, // URL del binario de un adjunto con el token en query (para ) visitaAdjuntoUrl(url: string): string { const token = localStorage.getItem('skeen_token') || ''; return `${ODOO_BASE}${url}?token=${encodeURIComponent(token)}`; }, // Pacientes async getPatients(params?: string | { search?: string; page?: number; page_size?: number; incomplete?: number; vip?: number; recent?: number; doctor_id?: number }): Promise<{ status: string; patients: Patient[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const p = typeof params === 'string' ? { search: params } : params || {}; const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients`, { params: p }); return data; }, /** Exportación completa: TODOS los pacientes del filtro, sin paginar. */ async exportPatients(params?: { search?: string; incomplete?: number; vip?: number; recent?: number; doctor_id?: number }): Promise<{ status: string; patients: Patient[]; total: number; total_disponible: number; truncado: boolean; limite: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/export`, { params: params || {} }); return data; }, async getPatient(id: number): Promise<{ status: string; patient: Patient }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}`); return data; }, async createPatient(patient: Partial): Promise<{ status: string; patient: Patient }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients`, patient); return data; }, async updatePatient(id: number, patient: Partial): Promise<{ status: string; patient: Patient }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}`, patient); return data; }, async getPatientHistory(id: number): Promise<{ status: string; appointments: Appointment[]; sales: Sale[]; visitas: Visita[]; pagos: Payment[]; estado_cuenta: EstadoCuenta }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}/history`); return data; }, // URL de la foto del paciente con el token en query (para ) patientPhotoUrl(url: string): string { const token = localStorage.getItem('skeen_token') || ''; return `${ODOO_BASE}${url}?token=${encodeURIComponent(token)}`; }, // Expedientes recientes y adjuntos de paciente async getExpedientesRecientes(params?: Record): Promise<{ status: string; expedientes: ExpedienteReciente[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/expedientes-recientes`, { params }); return data; }, async getPatientAdjuntos(id: number, kind?: string): Promise<{ status: string; adjuntos: PatientAdjunto[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}/adjuntos`, { params: kind ? { kind } : {} }); return data; }, async uploadPatientAdjunto(id: number, payload: { kind: string; name: string; file_b64: string; mimetype: string; notes?: string; antes_despues?: string }): Promise<{ status: string; adjunto: PatientAdjunto }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}/adjuntos`, payload); return data; }, async deletePatientAdjunto(adjId: number): Promise<{ status: string; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/patients/adjuntos/${adjId}`); return data; }, // URL del binario de un adjunto de paciente con el token en query (para ) patientAdjuntoUrl(url: string): string { const token = localStorage.getItem('skeen_token') || ''; return `${ODOO_BASE}${url}?token=${encodeURIComponent(token)}`; }, // Recetas (plantillas) async getRecetas(search?: string): Promise<{ status: string; recetas: Receta[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/recetas`, { params: search ? { search } : {} }); return data; }, async createReceta(receta: Partial): Promise<{ status: string; receta: Receta }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/recetas`, receta); return data; }, async updateReceta(id: number, receta: Partial): Promise<{ status: string; receta: Receta }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/recetas/${id}`, receta); return data; }, async deleteReceta(id: number): Promise<{ status: string; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/recetas/${id}`); return data; }, // Catálogos: diagnósticos y procedimientos async getDiagnosticos(search?: string): Promise<{ status: string; items: Diagnostico[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/diagnosticos`, { params: search ? { search } : {} }); return data; }, async createDiagnostico(payload: Partial): Promise<{ status: string; item: Diagnostico }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/diagnosticos`, payload); return data; }, async updateDiagnostico(id: number, payload: Partial): Promise<{ status: string; item: Diagnostico }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/diagnosticos/${id}`, payload); return data; }, async deleteDiagnostico(id: number): Promise<{ status: string; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/diagnosticos/${id}`); return data; }, async getProcedimientos(search?: string): Promise<{ status: string; items: Procedimiento[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/procedimientos`, { params: search ? { search } : {} }); return data; }, async createProcedimiento(payload: Partial): Promise<{ status: string; item: Procedimiento }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/procedimientos`, payload); return data; }, async updateProcedimiento(id: number, payload: Partial): Promise<{ status: string; item: Procedimiento }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/procedimientos/${id}`, payload); return data; }, async deleteProcedimiento(id: number): Promise<{ status: string; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/procedimientos/${id}`); return data; }, // Consultas médicas directas async createConsulta(payload: { partner_id: number; doctor_id?: number | null; motivo?: string; diagnostico?: string; tratamiento?: string; notas?: string }): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/consultas`, payload); return data; }, // Reporte de adeudos (cartera por cobrar) async getAdeudos(): Promise<{ status: string; adeudos: AdeudoRow[]; total_cartera: number; total_pacientes: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/adeudos`); return data; }, // Cobro del adeudo del paciente (parcial o total) + plan de pagos async payAdeudo(patientId: number, payload: { amount: number; payment_method: string }): Promise<{ status: string; payment: Payment; receipt: ReceiptData }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/pay_adeudo`, payload); return data; }, async setPaymentPlan(patientId: number, payload: { count: number; amount: number; next_payment_date?: string | null; note?: string }): Promise<{ status: string; plan: PaymentPlan }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/payment_plan`, payload); return data; }, // Servicios async getServices(params?: { search?: string; category?: string; page?: number; page_size?: number; sort?: 'manual' | 'best_sellers' }): Promise<{ status: string; services: Service[]; total?: number; page?: number; total_pages?: number; sort?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/services`, { params }); return data; }, async globalSearch(q: string): Promise<{ status: string; patients: { id: number; name: string; phone: string }[]; appointments: { id: number; patient: string; service: string; date: string; time: string }[]; services: { id: number; name: string; price: number }[]; }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/search`, { params: { q } }); return data; }, async createService(service: Partial): Promise<{ status: string; service: Service }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/services`, service); return data; }, async reorderServices(ids: number[], to?: 'front'): Promise<{ status: string; total: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/services/reorder`, { ids, ...(to ? { to } : {}) }); return data; }, async updateService(id: number, service: Partial): Promise<{ status: string; service: Service }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/services/${id}`, service); return data; }, // Médicos async getDoctors(opts?: { includeInactive?: boolean }): Promise<{ status: string; doctors: Doctor[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors`, { params: opts?.includeInactive ? { include_inactive: 1 } : undefined, }); return data; }, async updateDoctor(id: number, payload: { commission_pct?: number; staff_type?: 'doctor' | 'cosmetologa'; monthly_goal?: number; active?: boolean }): Promise<{ status: string; doctor: Doctor }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}`, payload); return data; }, // Horario semanal del médico (reemplaza la lista completa) async getDoctorHorario(id: number): Promise<{ status: string; horario: HorarioDia[]; tiene_horario: boolean; horario_resumen: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/horario`); return data; }, async setDoctorHorario(id: number, horario: HorarioDia[]): Promise<{ status: string; horario: HorarioDia[]; tiene_horario: boolean; horario_resumen: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/horario`, { horario }); return data; }, async setDoctorCommission(id: number, commission_pct: number): Promise<{ status: string; doctor: { id: number; name: string; commission_pct: number } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/commission`, { commission_pct }); return data; }, async getCommissionDefault(): Promise<{ status: string; commission_pct: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/config/commission_default`); return data; }, async setCommissionDefault(commission_pct: number): Promise<{ status: string; commission_pct: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/config/commission_default`, { commission_pct }); return data; }, // Simulación de impacto ("Ver efecto") con las ventas atribuidas del periodo async getImpactSimulation(params: { date_from?: string; date_to?: string; commission_pct?: number; rate?: number; }): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/config/impact_simulation`, { params }); return data; }, // Auditoría de configuración async getConfigAudit(params?: { date_from?: string; date_to?: string; module?: string; user?: string; entity?: string; page?: number; page_size?: number; }): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/config/audit`, { params }); return data; }, // ---- Diseño del ticket impreso (Configuración › Tickets) ---- async getTicketConfig(): Promise<{ status: string; layout?: unknown; clinica?: unknown; logo?: string; message?: string; }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/ticket_config`); return data; }, async putTicketConfig(layout: unknown, motivo?: string): Promise<{ status: string; layout?: unknown; message?: string; }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/ticket_config`, { layout, motivo }); return data; }, async putTicketClinica(payload: { clinica?: Record; logo?: string; motivo?: string; }): Promise<{ status: string; layout?: unknown; clinica?: unknown; logo?: string; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/ticket_config/clinica`, payload); return data; }, async resetTicketConfig(): Promise<{ status: string; layout?: unknown; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/ticket_config/reset`, {}); return data; }, async getDoctorHistory(id: number): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/history`); return data; }, // Comisiones async getCommissions(start?: string, end?: string): Promise<{ status: string; start: string; end: string; total_base: number; total_commission: number; commissions: CommissionRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/commissions`, { params: { start, end } }); return data; }, // Productos async getProducts(): Promise<{ status: string; products: Product[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/products`); return data; }, // Monederos async getWallets(params?: string | { phone?: string; search?: string; page?: number; page_size?: number }): Promise<{ status: string; wallets: Wallet[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const p = typeof params === 'string' ? { phone: params } : params || {}; const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wallets`, { params: p }); return data; }, async getWalletByPhone(phone: string): Promise<{ status: string; wallet: { points: number; equivalent_mxn: number; history: unknown[] } }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wallets/${phone}`); return data; }, async walletTransaction(payload: { phone: string; type: 'accrual' | 'redemption'; points: number; description?: string }): Promise<{ status: string; wallet: Wallet }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wallets/transaction`, payload); return data; }, // Movimientos del monedero con narración legible async getWalletTransactions(walletId: number, params?: { type?: string; date_from?: string; date_to?: string }): Promise<{ status: string; wallet: Wallet; movimientos: WalletTransaction[]; total: number; sum_positive: number; sum_negative: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wallets/${walletId}/transactions`, { params: params || {} }); return data; }, // Ajuste manual auditado de puntos (admin/gerencia) async adjustWallet(walletId: number, payload: { points: number; reason: string }): Promise<{ status: string; wallet: Wallet; applied: number; movimiento: { id: number; points: number; date: string; operator: string; narration: string } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wallets/${walletId}/adjust`, payload); return data; }, // Pagos async getPayments(state?: string): Promise<{ status: string; payments: Payment[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/payments`, { params: state ? { state } : {} }); return data; }, async createPayment(payment: Partial): Promise<{ status: string; payment: Payment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/payments`, payment); return data; }, async confirmPayment(id: number): Promise<{ status: string; payment: Payment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/payments/${id}/confirm`); return data; }, // Ventas async getSales(params?: Record): Promise<{ status: string; sales: Sale[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/sales`, { params }); return data; }, // "Mis ventas" (rol medico): el servidor filtra por el employee del token async getMySales(range: '30d' | 'month' | 'last_month' = '30d'): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/my/sales`, { params: { range } }); return data; }, async createSale(sale: Omit, 'lines'> & { lines: Omit[] }): Promise<{ status: string; sale: Sale }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales`, sale); return data; }, async paySale(id: number, payload: { payment_method: string; amount?: number }): Promise<{ status: string; sale: Sale; payment: Payment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales/${id}/pay`, payload); return data; }, async refundSale(id: number, payload: { amount?: number; reason?: string }): Promise<{ status: string; sale: Sale; result: { refund_amount: number; points_reversed: number; new_state: string; new_amount_paid: number } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales/${id}/refund`, payload); return data; }, // Cancelación con nota de crédito (permiso admin/gerencia) /** Saldo a favor del paciente + movimientos narrados. */ async getPatientWallet(patientId: number): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/wallet`); return data; }, /** Recarga de saldo a favor (anticipo). Permiso: recepción o más. */ async depositPatientWallet(patientId: number, payload: { amount: number; payment_method: string; notes?: string; cita_id?: number }): Promise<{ status: string; movimiento: WalletTransaction; wallet: { id: number; points: number; equivalent_mxn: number; equivalent_usd: number | null }; receipt: ReceiptData; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/deposit`, payload); return data; }, async cancelSale(id: number, payload: { reason: string; restore_stock?: boolean }): Promise<{ status: string; sale: Sale; result: { points_reversed: number; points_restored: number; stock_moves: number } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales/${id}/cancel`, payload); return data; }, /** Corrige un ticket ya emitido sin cancelarlo (gerencia / admin). */ async updateSale(id: number, payload: { motivo: string; lines?: SaleEditLine[]; discount?: number; notes?: string; date?: string; }): Promise<{ status: string; sale: Sale; result: SaleEditResult }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/sales/${id}`, payload); return data; }, // Última venta del paciente con precios actuales del catálogo (POS: repetir venta) async getLastSale(patientId: number): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/sales/last`, { params: { patient_id: patientId } }); return data; }, // Paquetes del paciente con sesiones restantes (POS: aviso en el ticket) async getPatientPackages(patientId: number): Promise<{ status: string; packages: PatientPackage[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/packages`); return data; }, // Cortes de caja async getCashClosings(date?: string): Promise<{ status: string; cash_closings: CashClosing[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/cash_closings`, { params: date ? { date } : {} }); return data; }, async createCashClosing(payload: { date?: string; opening_cash: number; notes?: string }): Promise<{ status: string; cash_closing: CashClosing }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/cash_closings`, payload); return data; }, async closeCashClosing(id: number, payload: { closing_cash?: number; counted_cash?: number; counted_card?: number; counted_transfer?: number; counted_other?: number; notes?: string }): Promise<{ status: string; cash_closing: CashClosing }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/cash_closings/${id}/close`, payload); return data; }, async updatePatientAdjunto(adjId: number, payload: { antes_despues?: string; visita_id?: number | null; cita_id?: number | null; servicio_id?: number | null }): Promise<{ status: string; adjunto: PatientAdjunto }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/patients/adjuntos/${adjId}`, payload); return data; }, // Reportes async getSalesReport(start?: string, end?: string): Promise<{ status: string; total_sales: number; total_paid: number; total_due: number; count: number; by_day?: { date: string; total: number }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/sales`, { params: { start, end } }); return data; }, async getDailyReport(date?: string, by?: 'medico' | 'recepcion'): Promise<{ status: string } & DailyReport & { por_medico?: DailyPorMedico[]; por_recepcion?: DailyPorRecepcion[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/daily`, { params: { ...(date ? { date } : {}), ...(by ? { by } : {}) }, }); return data; }, async getComisiones(start?: string, end?: string): Promise<{ status: string; start: string; end: string; comisiones: ComisionRow[]; total_vendido: number; total_comision: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/comisiones`, { params: { start, end } }); return data; }, async getPagosServicios(start?: string, end?: string): Promise<{ status: string; total_cobrado: number; total_pagos: number; by_method: Record; servicios: PagoServicioRow[]; total_pagado_completo: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/pagos-servicios`, { params: { start, end } }); return data; }, async getPagosClientes(start?: string, end?: string): Promise<{ status: string; clientes: PagoClienteRow[]; total_cobrado: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/pagos-clientes`, { params: { start, end } }); return data; }, async getDevoluciones(start?: string, end?: string): Promise<{ status: string; devoluciones: DevolucionRow[]; total_devuelto: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/devoluciones`, { params: { start, end } }); return data; }, async getTopClientes(start?: string, end?: string, limit = 20): Promise<{ status: string; clientes: TopClienteRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/top-clientes`, { params: { start, end, limit } }); return data; }, async getMovimientos(params?: Record): Promise<{ status: string; movimientos: MovimientoRow[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventario/movimientos`, { params }); return data; }, async createCompra(payload: { item_id: number; qty: number; cost?: number; reference?: string; notes?: string }): Promise<{ status: string; move: InventoryMove; item: InventoryItem }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventario/compras`, payload); return data; }, async createBaja(payload: { item_id: number; qty: number; reference?: string; notes?: string }): Promise<{ status: string; move: InventoryMove; item: InventoryItem }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventario/bajas`, payload); return data; }, async getInventarioAlertas(): Promise<{ status: string; caducidades: CaducidadRow[]; sugerencia_compra: SugerenciaRow[]; sugerencia_total: { items: number; costo_total: number } }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventario/alertas`); return data; }, // Conteo físico cíclico de inventario async getConteos(limit = 30): Promise<{ status: string; conteos: InventoryConteo[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventario/conteos`, { params: { limit } }); return data; }, async getConteo(id: number): Promise<{ status: string; conteo: InventoryConteo }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventario/conteos/${id}`); return data; }, async createConteo(payload: { notes?: string; lines: { item_id: number; qty_contado: number }[] }): Promise<{ status: string; conteo: InventoryConteo; ajustes: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventario/conteos`, payload); return data; }, async getHorasAgenda(start?: string, end?: string): Promise<{ status: string; horas: HoraAgendaRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/horas-agenda`, { params: { start, end } }); return data; }, async getPaquetes(): Promise<{ status: string; paquetes: PaqueteRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/paquetes`); return data; }, async getVendedores(start?: string, end?: string): Promise<{ status: string; vendedores: VendedorRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/vendedores`, { params: { start, end } }); return data; }, async getConcentrado(start?: string, end?: string): Promise<{ status: string } & ConcentradoReport> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/concentrado`, { params: { start, end } }); return data; }, async getConcentradoDetalle(params: { start?: string; end?: string; doctor_ids?: string; categoria_ids?: string; servicio_ids?: string; vip?: boolean }): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/concentrado/detalle`, { params }); return data; }, async getRecomendaciones(): Promise<{ status: string; recomendaciones: RecomendacionRow[]; total: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/recomendaciones`); return data; }, async getMonederoPuntos(start?: string, end?: string): Promise<{ status: string } & MonederoPuntosReport> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/monedero-puntos`, { params: { start, end } }); return data; }, async getKpis(start?: string, end?: string): Promise<{ status: string } & KpisReport> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/kpis`, { params: { start, end } }); return data; }, async getNotifications(): Promise<{ status: string; notifications: AppNotification[]; total: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/notifications`); return data; }, // POS: venta + pago en una sola operación async posCheckout(payload: PosCheckoutPayload): Promise { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/pos/checkout`, payload); return data; }, // ---------- Paquetes de sesiones (catálogo y saldos) ---------- async getPaqueteCatalogo(params?: { search?: string; includeInactive?: boolean }): Promise<{ status: string; paquetes: Paquete[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/paquetes`, { params: { search: params?.search || undefined, include_inactive: params?.includeInactive ? '1' : undefined, }, }); return data; }, async createPaquete(payload: PaquetePayload): Promise<{ status: string; paquete: Paquete; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/paquetes`, payload); return data; }, async updatePaquete(id: number, payload: PaquetePayload): Promise<{ status: string; paquete: Paquete; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/paquetes/${id}`, payload); return data; }, async getPaqueteCompras(params?: { patient_id?: number; state?: string; search?: string; doctor_id?: number; start?: string; end?: string; includeAll?: boolean; }): Promise<{ status: string; compras: PaqueteCompra[]; totales: Record }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/paquetes/compras`, { params: { patient_id: params?.patient_id || undefined, state: params?.state || undefined, search: params?.search || undefined, doctor_id: params?.doctor_id || undefined, start: params?.start || undefined, end: params?.end || undefined, include_all: params?.includeAll ? '1' : undefined, }, }); return data; }, async getPaqueteConsumos(id: number): Promise<{ status: string; compra: PaqueteCompra; consumos: PaqueteConsumo[]; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/paquetes/compras/${id}/consumos`); return data; }, async ajustarPaqueteCompra(id: number, payload: { sessions_total?: number; sessions_used?: number }): Promise<{ status: string; compra: PaqueteCompra; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/paquetes/compras/${id}`, payload); return data; }, async anularPaqueteCompra(id: number, reason: string): Promise<{ status: string; compra: PaqueteCompra; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/paquetes/compras/${id}/anular`, { reason }); return data; }, async getCashReportRange(start: string, end: string): Promise<{ status: string; total: number; by_method: Record }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/cash`, { params: { start, end } }); return data; }, async getWacrmConversationMessages(conversationId: string, page = 1, pageSize = 50): Promise<{ status: string; messages: WacrmMessage[]; page: number; page_size: number; has_more: boolean }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations/${conversationId}/messages`, { params: { page, page_size: pageSize } }); return data; }, async getInventoryReport(): Promise<{ status: string; summary: InventoryReportSummary; items: InventoryItem[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/inventory`); return data; }, async getAppointmentsReport(start?: string, end?: string): Promise<{ status: string; total: number; by_state: Record }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/appointments`, { params: { start, end } }); return data; }, async getTopServices(start?: string, end?: string, limit = 8): Promise<{ status: string; start: string; end: string; services: { service_id: number; service: string; qty: number; revenue: number }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/top_services`, { params: { start, end, limit } }); return data; }, async getCashReport(date?: string): Promise<{ status: string; total: number; by_method: Record }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/cash`, { params: { date } }); return data; }, async getTaxReport(start?: string, end?: string): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/taxes`, { params: { start, end } }); return data; }, // Tipo de cambio async getExchangeRate(): Promise<{ status: string; current: { rate: number; date: string; source: string } | null; history: { id: number; date: string; rate: number; source: string }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/exchange_rate`); return data; }, async setExchangeRate(rate: number, source = 'manual'): Promise<{ status: string; exchange_rate: { id: number; date: string; rate: number; source: string } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/exchange_rate`, { rate, source }); return data; }, // Meta de ventas mensual async getSalesGoal(): Promise<{ status: string; goal: number; current: number; pct: number; start: string; end: string; count: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/sales_goal`); return data; }, async setSalesGoal(goal: number): Promise<{ status: string; goal: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales_goal`, { goal }); return data; }, // Cumpleañeros async getBirthdays(period: 'today' | 'week' | 'month' = 'month'): Promise<{ status: string; period: string; start: string; end: string; total: number; birthdays: Birthday[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/birthdays`, { params: { period } }); return data; }, // Inventario / Consumibles async getInventory(params?: { kind?: InventoryKind; level?: InventoryLevel; search?: string }): Promise<{ status: string; items: InventoryItem[]; summary: InventorySummary }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventory/items`, { params }); return data; }, async createInventoryItem(item: Partial): Promise<{ status: string; item: InventoryItem }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventory/items`, item); return data; }, async updateInventoryItem(id: number, item: Partial): Promise<{ status: string; item: InventoryItem }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/inventory/items/${id}`, item); return data; }, async getInventoryMoves(id: number): Promise<{ status: string; moves: InventoryMove[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventory/items/${id}/moves`); return data; }, async createInventoryMove(id: number, payload: { type: InventoryMoveType; qty: number; reference?: string; notes?: string }): Promise<{ status: string; move: InventoryMove; item: InventoryItem }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventory/items/${id}/moves`, payload); return data; }, // ============================================================ // WACRM Proxy // ============================================================ async syncWacrm(): Promise<{ status: string; result: Record }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/sync`); return data; }, async getWacrmMembers(): Promise<{ status: string; members: { id: string; profile_id?: string; name: string; email?: string; avatar_url?: string; role: string }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/members`); return data; }, async assignWacrmConversation(external_id: string, assigned_agent_id: string | null): Promise<{ status: string; assigned_agent_id?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations/${external_id}/assign`, { assigned_agent_id, }); return data; }, async getWacrmConversations(search?: string): Promise<{ status: string; conversations: WacrmConversation[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations`, { params: search ? { search } : {} }); return data; }, // ---- Notas internas ---- async getWacrmNotes(conversationId: string): Promise<{ status: string; notes: WacrmNote[]; pendientes: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations/${conversationId}/notes`); return data; }, async createWacrmNote(conversationId: string, body: string): Promise<{ status: string; note: WacrmNote }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations/${conversationId}/notes`, { body }); return data; }, async updateWacrmNote(noteId: number, payload: { body?: string; resolved?: boolean }): Promise<{ status: string; note: WacrmNote }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/wacrm/notes/${noteId}`, payload); return data; }, async deleteWacrmNote(noteId: number): Promise<{ status: string; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/wacrm/notes/${noteId}`); return data; }, // ---- Etiquetas ---- async getWacrmTags(): Promise<{ status: string; tags: WacrmTag[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/tags`); return data; }, async createWacrmTag(payload: { name: string; color?: string; sequence?: number }): Promise<{ status: string; tag: WacrmTag }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/tags`, payload); return data; }, async updateWacrmTag(tagId: number, payload: Partial): Promise<{ status: string; tag: WacrmTag }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/wacrm/tags/${tagId}`, payload); return data; }, async deleteWacrmTag(tagId: number): Promise<{ status: string; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/wacrm/tags/${tagId}`); return data; }, async assignWacrmTag(conversationId: string, tagId: number, action: 'add' | 'remove' = 'add'): Promise<{ status: string; tags: { id: number; name: string; color: string }[] }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations/${conversationId}/tags`, { tag_id: tagId, action }); return data; }, // ---- Métricas para gerencia ---- async getWacrmMetrics(params?: { sin_respuesta_horas?: number }): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/metrics`, { params }); return data; }, async sendWacrmMessage(payload: { conversation_id: string; text: string; assigned_agent_id: string }): Promise<{ status: string; message: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/messages`, payload); return data; }, async getWacrmMessages(search?: string, conversation_id?: string): Promise<{ status: string; messages: WacrmMessage[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/messages`, { params: { ...(search ? { search } : {}), ...(conversation_id ? { conversation_id } : {}) }, }); return data; }, async getWacrmPipelines(): Promise<{ status: string; pipelines: WacrmPipeline[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/pipelines`); return data; }, async getWacrmLeads(search?: string, status?: string, pipeline_id?: string): Promise<{ status: string; leads: WacrmLead[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/leads`, { params: { ...(search ? { search } : {}), ...(status ? { status } : {}), ...(pipeline_id ? { pipeline_id } : {}) }, }); return data; }, async updateWacrmLeadStatus(id: number, payload: { status?: 'open' | 'won' | 'lost'; stage_id?: string; assigned_to?: string | null }): Promise<{ status: string; lead: WacrmLead }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/wacrm/leads/${id}/status`, payload); return data; }, async convertWacrmLead(id: number): Promise<{ status: string; lead: WacrmLead; patient_id: number; created: boolean; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/leads/${id}/convert`); return data; }, };