Files
SKEEN-Proyecto/frontend/src/services/odoo.ts

3242 lines
113 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string>).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<InventoryLevel, number>;
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<string, string>;
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<GlossaryKey, string>;
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<string, number>;
message?: string;
}
export interface ProcesarNotificacionesResult {
status: string;
message: string;
encoladas: number;
revisadas: number;
enviadas: number;
simuladas: number;
fallidas: number;
canceladas: number;
}
export interface ProbarNotificacionResult {
status: string;
ok: boolean;
simulado: boolean;
message: string;
}
// Contrato del portal público (endpoints /public/booking/*)
export interface BookingConfig {
activo: boolean;
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<string, unknown>;
campos?: FormularioCampo[];
texto_legal?: string;
notas?: string;
firma_url?: string | null;
fotos?: FormularioAdjunto[];
}
export interface FormularioPublicoConfig {
status: string;
brand: { nombre: string; logo_claro: string; logo_oscuro: string; colores: { primario: string; acento: string } };
terms: GlossaryTerms;
paciente: { nombre: string };
cita: { fecha: string | null; hora: string | null; servicio: string | null } | null;
estado: FormularioEstado;
ya_llenado: boolean;
plantilla: {
nombre: string;
tipo: FormularioPlantillaTipo;
tipo_label?: string;
descripcion: string;
texto_legal: string;
requiere_firma: boolean;
requiere_foto: boolean;
duracion_estimada_min: number;
campos: FormularioCampo[];
};
}
export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
nombre: BRAND_DEFAULT.nombre,
telefono: '',
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<string, boolean> | 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<string, Record<string, boolean>>;
defaults: Record<string, Record<string, boolean>>;
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<string, number>;
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<string, number>;
}
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<string, number> };
ventas: { count: number; total: number; total_paid: number; total_due: number };
pagos: { total: number; count: number; by_method: Record<string, number> };
devoluciones: { count: number; total: number };
pacientes_nuevos: number;
visitas: { total: number; by_state: Record<string, number> };
}
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<string, number>;
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<string, number>;
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<string, number> };
ventas: { count: number; total: number; total_paid: number; total_due: number };
pagos: { total: number; count: number; by_method: Record<string, number> };
inventario: { total: number; by_type: Record<string, number> };
}
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<T> {
status: string;
message?: string;
data?: T;
}
export interface Paginated<T> {
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<string, FrontendRole[]>; 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<ClinicBrand>; 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<GlossaryTerms>; 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<Sucursal>): 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<Sucursal>): 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<ClinicSettings> & { 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<BookingConfig> {
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<BookingCreateResult> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/public/booking/create`, payload);
return data;
},
async getBookingCita(token: string): Promise<BookingCita> {
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<string, string | number> = {};
if (opts?.includeInactive) params.include_inactive = 1;
if (opts?.tipo) params.tipo = opts.tipo;
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas`, { params });
return data;
},
async getFormularioPlantilla(id: number): Promise<{ status: string; plantilla: FormularioPlantilla; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas/${id}`);
return data;
},
async createFormularioPlantilla(payload: Partial<FormularioPlantilla> & { sucursal_ids?: number[]; servicio_ids?: number[] }): Promise<{ status: string; plantilla: FormularioPlantilla; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas`, payload);
return data;
},
async updateFormularioPlantilla(id: number, payload: Partial<FormularioPlantilla> & { sucursal_ids?: number[]; servicio_ids?: number[] }): Promise<{ status: string; plantilla: FormularioPlantilla; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas/${id}`, payload);
return data;
},
async deleteFormularioPlantilla(id: number): Promise<{ status: string; desactivada: boolean; message: string }> {
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/formularios/plantillas/${id}`);
return data;
},
// ---- Formularios digitales: respuestas ----
async getFormularioRespuestas(params?: Record<string, string | number>): Promise<{ status: string; respuestas: FormularioRespuesta[]; total: number; page: number; page_size: number; total_pages: number; resumen: Record<string, number>; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/respuestas`, { params });
return data;
},
async getFormularioRespuesta(id: number): Promise<{ status: string; respuesta: FormularioRespuesta; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/respuestas/${id}`);
return data;
},
async getFormularioFotos(params: { partner_id?: number; respuesta_id?: number }): Promise<{ status: string; total: number; fotos: FormularioFoto[]; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/formularios/fotos`, { params });
return data;
},
async enviarFormulario(payload: { plantilla_id: number; partner_id: number; cita_id?: number | null; canal: FormularioCanal }): Promise<{ status: string; respuesta: FormularioRespuesta; token: string; enlace: string; enviado_por: string; notificado: boolean; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/formularios/enviar`, payload);
return data;
},
async llenarFormulario(id: number, payload: { respuestas: Record<string, unknown>; firma_imagen?: string; firma_nombre?: string }): Promise<{ status: string; respuesta: FormularioRespuesta; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/formularios/respuestas/${id}/llenar`, payload);
return data;
},
async adjuntarFormulario(payload: { respuesta_id: number; campo_clave?: string; kind?: FormularioMomento; name?: string; file: string }): Promise<{ status: string; adjunto: FormularioAdjunto; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/formularios/adjuntar`, payload);
return data;
},
// URL del binario (firma/foto) con el token de sesión en query para <img src>
formularioArchivoUrl(url: string): string {
const token = localStorage.getItem('skeen_token') || '';
return `${ODOO_BASE}${url}?token=${encodeURIComponent(token)}`;
},
// ---- Portal público de formularios (sin autenticación) ----
async getFormularioPublicoConfig(token: string): Promise<FormularioPublicoConfig> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/formulario/config`, { params: { token } });
return data;
},
async getFormularioPublicoEstado(token: string): Promise<{ status: string; estado: FormularioEstado; ya_llenado: boolean; paciente: { nombre: string }; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/formulario`, { params: { token } });
return data;
},
async llenarFormularioPublico(payload: { token: string; respuestas: Record<string, unknown>; firma_imagen?: string; firma_nombre?: string; fotos?: { campo_clave?: string; kind?: FormularioMomento; name?: string; data_url: string }[]; hp?: string }): Promise<{ status: string; message: string; errores?: string[] }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/public/formulario/llenar`, payload);
return data;
},
// ---- Notificaciones y recordatorios de citas ----
async getNotificaciones(params?: Record<string, string | number>): Promise<NotificacionesResponse> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/notificaciones`, { params });
return data;
},
async procesarNotificaciones(limit?: number): Promise<ProcesarNotificacionesResult> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/procesar`, { limit });
return data;
},
async enviarNotificacion(payload: { cita_id: number; tipo?: string; canal?: NotificacionCanal }): Promise<{ status: string; notificaciones: Notificacion[]; simulado: boolean; message: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/enviar`, payload);
return data;
},
async reintentarNotificacion(id: number): Promise<{ status: string; estado: string; notificacion: Notificacion; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/${id}/reintentar`);
return data;
},
async probarNotificacion(payload: { canal: NotificacionCanal; destino?: string }): Promise<ProbarNotificacionResult> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/notificaciones/probar`, payload);
return data;
},
async updateRolePermissions(matrix: Record<string, FrontendRole[]>): Promise<{ status: string; matrix: Record<string, FrontendRole[]>; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/roles/permissions`, { matrix });
return data;
},
// ---- 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<PermsResponse> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/permisos`);
return data;
},
async putPermisos(payload: { role_code?: string; permisos?: Record<string, boolean>; por_rol?: Record<string, Record<string, boolean>> }): Promise<{ status: string; por_rol: Record<string, Record<string, boolean>>; guardados: number; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/permisos`, payload);
return data;
},
// Health
async healthCheck(): Promise<unknown> {
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<string, string | number>): 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<Appointment> & { 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<Appointment>): 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<string, string | number> = { 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<string, string | number>): 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<string, string | number>): 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<string, string | number>): 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<Visita>): 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 <img src>)
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<Patient>): 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<Patient>): 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 <img src>)
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<string, string | number>): 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 <img src>)
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<Receta>): 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<Receta>): 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<Diagnostico>): 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<Diagnostico>): 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<Procedimiento>): 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<Procedimiento>): 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<Service>): 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<Service>): 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<ImpactSimulation> {
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<ConfigAuditResponse> {
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<string, string>; 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<DoctorHistory> {
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<Payment>): 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<string, string | number>): 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<MySalesResponse> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/my/sales`, { params: { range } });
return data;
},
async createSale(sale: Omit<Partial<Sale>, 'lines'> & { lines: Omit<SaleLine, 'subtotal'>[] }): 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<PatientWallet> {
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<LastSaleResponse> {
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<string, number>; 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<string, string | number>): 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<ConcentradoDetalleReport> {
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<PosCheckoutResult> {
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<string, number> }> {
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<string, number> }> {
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<string, number> }> {
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<string, number> }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/cash`, { params: { date } });
return data;
},
async getTaxReport(start?: string, end?: string): Promise<TaxReportResult> {
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<InventoryItem>): 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<InventoryItem>): 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<string, number | string | null> }> {
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<WacrmTag>): 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<WacrmMetrics> {
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;
},
};