- BloqueosModal: alta multi-médico, rango repetible por día de la semana, motivos predefinidos, edición/borrado y cierre rápido desde la cabecera de cada columna (todo el día / resto del día / rango). - skeen.lista.espera + panel y modal en la agenda: prioridad, franja, rango de fechas, agendar precargado, avisar por WhatsApp y aviso cuando se libera un hueco compatible. - duration en skeen.cita (0 = la del servicio): la cita ocupa sus bloques de 15 min reales en las 4 vistas, con celdas de continuación. - Checklist de estados (Agendada → Confirmada → En cabina → En consulta → Completada) con avance/retroceso, cancelación y no-show con confirmación, y bitácora de quién/cuándo en skeen.cita.state.log. - Etiquetas del sistema legacy alineadas (Canceló / Lista de espera / En cabina) en badges de agenda y dashboard. - Auto refresco silencioso cada 30 s (pausable, se pospone con modales, escritura o drag) en citas + bloqueos + lista de espera en un solo request.
2170 lines
75 KiB
TypeScript
2170 lines
75 KiB
TypeScript
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;
|
|
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';
|
|
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;
|
|
}
|
|
|
|
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;
|
|
message?: string;
|
|
}
|
|
|
|
export interface Doctor {
|
|
id: number;
|
|
name: string;
|
|
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;
|
|
is_prescribed?: boolean;
|
|
prescribed_by_id?: number | null;
|
|
prescribed_by?: string | null;
|
|
}
|
|
|
|
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;
|
|
date: string;
|
|
subtotal: number;
|
|
discount: number;
|
|
tax: 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 {
|
|
service_id: number;
|
|
name: string;
|
|
package_sessions: number;
|
|
total: number;
|
|
used: number;
|
|
remaining: number;
|
|
next_date: string | null;
|
|
}
|
|
|
|
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;
|
|
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;
|
|
}
|
|
|
|
export type FrontendRole = 'admin' | 'medico' | 'recepcion' | 'enfermeria' | 'cosmetologia' | 'psicologa' | 'farmacia' | 'gerencia' | 'marketing' | 'lectura';
|
|
|
|
export interface FrontendUser {
|
|
id: number;
|
|
login: string;
|
|
name: string;
|
|
role: FrontendRole;
|
|
must_change_password?: boolean;
|
|
last_login?: string | null;
|
|
active?: boolean;
|
|
allowed_menus?: string[] | null;
|
|
// Solo rol 'medico': hr.employee vinculado (null si no hay vínculo)
|
|
employee_id?: number | null;
|
|
employee_name?: string | null;
|
|
}
|
|
|
|
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 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; quantity: number; price_unit: number; description?: string; prescribed_by_id?: number }[];
|
|
discount?: number;
|
|
payment_method?: string;
|
|
amount_received?: number;
|
|
pay_with_points?: boolean;
|
|
currency?: 'MXN' | 'USD';
|
|
}
|
|
|
|
export interface PosCheckoutResult {
|
|
status: string;
|
|
sale: Sale;
|
|
payment: Payment | null;
|
|
cambio: 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;
|
|
}
|
|
|
|
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 }): 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;
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
// 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 }): 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;
|
|
}): 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;
|
|
}>): 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(): Promise<{ status: string; doctors: Doctor[] }> {
|
|
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors`);
|
|
return data;
|
|
},
|
|
|
|
async updateDoctor(id: number, payload: { commission_pct?: number; staff_type?: 'doctor' | 'cosmetologa'; monthly_goal?: number }): 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;
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
// Ú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 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;
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
};
|