3242 lines
113 KiB
TypeScript
3242 lines
113 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;
|
||
/** 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;
|
||
},
|
||
};
|