import axios from 'axios'; const ODOO_BASE = import.meta.env.VITE_API_BASE_URL || '/api/odoo'; // ============================================================ // Auth: adjuntar token y manejar 401 globalmente // ============================================================ axios.interceptors.request.use((config) => { const token = localStorage.getItem('skeen_token'); if (token) { config.headers = config.headers || {}; (config.headers as Record).Authorization = `Bearer ${token}`; } return config; }); axios.interceptors.response.use( (res) => res, (error) => { if (error?.response?.status === 401) { const onLogin = window.location.pathname === '/login'; const isLoginCall = String(error.config?.url || '').includes('/auth/login'); if (!onLogin && !isLoginCall) { localStorage.removeItem('skeen_token'); localStorage.removeItem('skeen_user'); window.location.href = '/login'; } } return Promise.reject(error); }, ); // ============================================================ // Tipos // ============================================================ export interface DashboardStats { appointments_today: number; appointments_month: number; new_patients_month: number; total_patients: number; revenue_month: number; pending_payments: number; total_wallet_points: number; sales_today: number; // Comparativas del período anterior equivalente (deltas) prev_revenue_month?: number; prev_sales_today?: number; } export interface Appointment { id: number; reference: string; patient_id: number; patient: string; phone: string; service_id: number; service: string; service_category?: string; date: string; time: string; state: string; payment_state: string; price: number; amount_paid?: number; duration?: number; is_first_visit?: boolean; doctor_id?: number | null; doctor?: string; branch?: string; medium?: string; notes?: string; package_finished?: boolean; package_finished_date?: string | null; // 1ra confirmación (teléfono). La 2da es la llegada (state 'arrived'). phone_confirmed?: boolean; phone_confirmed_at?: string | null; phone_confirmed_by?: string | null; // Timestamp del check-in de llegada (2da confirmación) arrived_at?: string | null; // El paciente cumple años el día de la cita (badge pastel en agenda) patient_birthday?: boolean; } export interface Bloqueo { id: number; doctor_id: number; doctor: string; date: string; all_day: boolean; time_from: number; time_to: number; motivo: string; } export type VisitaState = 'en_curso' | 'completada' | 'cancelada'; export interface VisitaInsumo { id: number; item_id: number; item: string; unit: string; qty: number; notes: string; } export interface VisitaAdjunto { id: number; kind: 'antes' | 'despues' | 'documento'; name: string; mimetype: string; notes: string; create_date: string | null; url: string; } export interface Visita { id: number; name: string; cita_id: number | null; partner_id: number; patient: string; phone: string; doctor_id: number | null; doctor: string | null; cosmetologa_id: number | null; cosmetologa: string | null; servicio_id: number | null; servicio: string | null; motivo: string; diagnostico: string; tratamiento: string; notas: string; receta_text?: string; state: VisitaState; date_start: string | null; date_end: string | null; insumos: VisitaInsumo[]; insumos_descargados: boolean; adjuntos: VisitaAdjunto[]; } export interface Patient { id: number; patient_id: string; name: string; phone: string; email: string; birth_date: string | null; age?: number; gender?: string | false; blood_type?: string | false; last_visit: string | null; total_visits: number; wallet_points: number; total_spent: number; source: string; is_vip: boolean; primary_doctor_id?: number | null; primary_doctor?: string; whatsapp?: string; has_photo?: boolean; photo_url?: string; photo?: string; // Datos personales extendidos birthplace?: string; occupation?: string; marital_status?: string; emergency_contact?: string; emergency_phone?: string; home_phone?: string; mobile?: string; address_notes?: string; referred_by?: string; patient_comments?: string; internal_notes?: string; // Historia clínica is_pregnant?: boolean; is_breastfeeding?: boolean; uses_contraceptives?: boolean; children_count?: number; kidney_problems?: boolean; back_pain?: boolean; heart_disease?: boolean; respiratory_problems?: boolean; blood_pressure?: boolean; diabetes?: boolean; thyroid?: boolean; colitis?: boolean; constipation?: boolean; liver_problems?: boolean; surgeries?: boolean; surgeries_notes?: string; varicose_veins?: boolean; migraine?: boolean; faints_with_needles?: boolean; medical_notes?: string; // Legacy medical fields allergies?: string; medical_history?: string; current_medication?: string; // Completitud de expediente expediente_completion?: number; expediente_missing?: string[]; } export interface EstadoCuenta { puntos_monedero: number; total_gastado: number; adeudo: number; visitas_total: number; } export interface PatientAdjunto { id: number; kind: 'expediente' | 'imagen'; name: string; mimetype: string; notes: string; antes_despues: 'sin_clasificar' | 'antes' | 'despues'; visita_id: number | null; cita_id: number | null; servicio_id: number | null; servicio: string; create_date: string | null; url: string; } export interface ExpedienteReciente { id: number; patient_id: string; name: string; phone: string; age: number; birth_date: string | null; primary_doctor: string | null; last_visit: string | null; internal_notes: string; expediente_count: number; imagenes_count: number; has_photo: boolean; photo_url: string; } export interface Service { id: number; code: string; name: string; category: string; price: number; package_price?: number; package_sessions?: number; package_notes?: string; duration_min: number; description: string; color: string; service_group: string; is_favorite: boolean; sequence?: number; sold_90d?: number | null; genera_monedero?: boolean; } export interface Doctor { id: number; name: string; job_title: string; work_phone: string; work_email: string; commission_pct: number; staff_type?: 'doctor' | 'cosmetologa'; monthly_goal?: number; month_total?: number; horario?: { day_of_week: string; hour_from: number; hour_to: number }[]; tiene_horario?: boolean; horario_resumen?: string; patient_count?: number; recomendaciones_30d?: number; ventas_30d?: number; total_30d?: number; total_30d_usd?: number | null; card_30d?: number; } export interface DoctorHistoryCita { id: number; date: string | null; time: string; patient: string; service: string; state: string; } export interface DoctorHistoryVisita { id: number; date_start: string | null; patient: string; motivo: string; state: string; } export interface DoctorHistoryVenta { id: number; venta: string; date: string | null; patient: string; item: string; subtotal: number; state: string; } export interface DoctorHistory { status: string; citas: DoctorHistoryCita[]; visitas: DoctorHistoryVisita[]; ventas: DoctorHistoryVenta[]; totales: { citas_total: number; visitas_total: number; ventas_total_lineas: number; monto_total: number; }; } export interface Product { id: number; name: string; list_price: number; default_code: string; type: string; } export interface Wallet { id: number; patient_id: number; patient: string; phone: string; points: number; equivalent_mxn: number; } export interface Payment { id: number; name: string; patient: string; patient_id: number; amount: number; payment_method: string; state: string; payment_url?: string; provider_reference?: string; create_date?: string; } export interface SaleLine { id?: number; service_id: number; service?: string; item_id?: number | null; item?: string; description: string; quantity: number; price_unit: number; subtotal: number; is_prescribed?: boolean; prescribed_by_id?: number | null; prescribed_by?: string | null; } export interface CommissionRow { doctor_id: number; doctor: string; job_title: string; commission_pct: number; base: number; commission: number; items: number; sales: number; } export interface Sale { id: number; name: string; patient_id: number; patient: string; date: string; subtotal: number; discount: number; tax: number; total: number; amount_paid: number; amount_due: number; state: string; lines: SaleLine[]; refunded?: boolean; refund_amount?: number; refund_reason?: string; refunded_at?: string | null; notes?: string; cancel_reason?: string; cancelled_by?: string; cancelled_at?: string | null; cancel_points_reversed?: number; cancel_points_restored?: number; } // "Repetir última venta" (POS): línea de la última venta con el precio vigente export interface LastSaleLine { kind: 'service' | 'item'; service_id: number | null; item_id: number | null; name: string; description: string; quantity: number; price_last: number; price_current: number | null; price_changed: boolean; missing: boolean; active: boolean; stock: number | null; unit: string | null; } export interface LastSaleResponse { status: string; sale: Sale | null; lines: LastSaleLine[]; warnings: string[]; } export interface PatientPackage { service_id: number; name: string; package_sessions: number; total: number; used: number; remaining: number; next_date: string | null; } export interface MySaleItem { id: number; name: string; date: string; patient: string; items: string[]; amount: number; payment_method: string; state: string; } export interface MySalesResponse { status: string; medico_sin_employee?: boolean; doctor?: { id: number; name: string }; range?: string; date_from?: string; date_to?: string; kpis: { ventas: number; total_mxn: number; total_usd: number | null; total_card: number }; sales: MySaleItem[]; daily_breakdown?: { date: string; total: number }[]; commission_pct: number; commission_source?: string; commission_estimate: number; monthly_goal?: { goal: number; real: number; pct: number | null; from: string; to: string }; } export interface HorarioDia { day_of_week: string; hour_from: number; hour_to: number; } export interface CashClosing { id: number; name: string; date: string; user: string | false; opening_cash: number; total_cash: number; total_card: number; total_transfer: number; total_other: number; total_sales: number; closing_cash: number; difference: number; counted_cash: number; counted_card: number; counted_transfer: number; counted_other: number; diff_cash: number; diff_card: number; diff_transfer: number; diff_other: number; notes: string; state: string; } export interface ChartPoint { name: string; appointments: number; revenue: number; patients: number; } export interface Birthday { id: number; name: string; phone: string; whatsapp?: string; birth_date: string; occurs_on: string; turning_age: number; last_visit: string | null; total_spent: number; amount_due: number; is_vip: boolean; } export type InventoryKind = 'producto' | 'consumible'; export type InventoryLevel = 'out' | 'critical' | 'low' | 'optimal'; export type InventoryMoveType = 'compra' | 'venta' | 'baja' | 'ajuste' | 'cancelacion'; export interface InventoryItem { id: number; name: string; kind: InventoryKind; sku: string; category: string; unit: string; qty: number; qty_optimal: number; qty_min: number; cost: number; price?: number; inventory_value: number; expiry_date: string | null; last_count_date: string | null; active: boolean; genera_monedero?: boolean; notes: string; stock_level: InventoryLevel; } export interface InventoryMove { id: number; item_id: number; type: InventoryMoveType; qty: number; before_qty: number; after_qty: number; date: string | null; reference: string; notes: string; } export interface InventorySummary { levels: Record; 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; } export interface WacrmMessage { id: number; external_id: string; conversation_id: string; contact_name: string; contact_phone: string; sender_type: 'customer' | 'agent' | 'bot'; content_type: string; content_text: string; media_url: string; template_name: string; status: string; created_at: string | null; } export interface WacrmStage { id: number; external_id: string; name: string; position: number; color: string; } export interface WacrmPipeline { id: number; external_id: string; name: string; stages: WacrmStage[]; } export interface WacrmLead { id: number; external_id: string; title: string; contact_id?: string; contact_name: string; contact_phone: string; pipeline: string; pipeline_id?: number; stage: string; stage_id?: number; stage_color: string; value: number; currency: string; status: 'open' | 'won' | 'lost'; expected_close_date: string | null; notes: string; assigned_to: string; partner_id?: number | null; conversation_id?: string; created_at: string | null; } export type FrontendRole = 'admin' | 'medico' | 'recepcion' | 'enfermeria' | 'cosmetologia' | 'psicologa' | 'farmacia' | 'gerencia' | 'marketing' | 'lectura'; export interface FrontendUser { id: number; login: string; name: string; role: FrontendRole; must_change_password?: boolean; last_login?: string | null; active?: boolean; allowed_menus?: string[] | null; // Solo rol 'medico': hr.employee vinculado (null si no hay vínculo) employee_id?: number | null; employee_name?: string | null; } export interface Receta { id: number; name: string; categoria: string; contenido: string; } // ---- Chat interno ---- export interface ChatUser { id: number; name: string; role: FrontendRole; unread: number; last_message: string | null; last_date: string | null; last_mine: boolean | null; } 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; } export type WalletTxType = 'accrual' | 'redemption' | 'adjustment' | 'expiration'; export interface WalletTransaction { id: number; type: WalletTxType; type_label: string; points: number; narration: string; description: string; reference: string; operator: string; date: string | null; } export interface InventoryConteoLine { id: number; item_id: number; item: string; unit: string; qty_sistema: number; qty_contado: number; difference: number; cost: number; value_diff: number; move_id: number | null; } export interface InventoryConteo { id: number; name: string; date: string | null; user_name: string; notes: string; total_items: number; total_diferencias: number; total_faltante: number; total_sobrante: number; total_value_diff: number; lines?: InventoryConteoLine[]; } export interface ComisionRow { doctor_id: number; doctor: string; job_title: string; items: number; sales: number; citas_done: number; total_vendido: number; commission_pct: number; comision: number; } export interface MovimientoRow extends InventoryMove { item: string; unit: string; } export interface CaducidadRow { id: number; name: string; category: string; qty: number; unit: string; expiry_date: string; dias_restantes: number; vencido: boolean; } export interface SugerenciaRow { id: number; name: string; category: string; unit: string; qty: number; qty_min: number; qty_optimal: number; sugerido: number; costo_estimado: number; } export interface PagoServicioRow { servicio: string; cantidad: number; total: number; } export interface PagoClienteRow { id: number; name: string; num_pagos: number; total_cobrado: number; ultimo_pago: string | null; } export interface DevolucionRow { id: number; folio: string; paciente: string; fecha: string | null; total: number; refund_amount: number; refund_reason: string; refunded_at: string | null; } export interface TopClienteRow { id: number; name: string; num_ventas: number; total: number; ticket_promedio: number; } export interface DailyPorMedico { medico: string; citas: number; by_state: Record; ventas: number; cobros: number; } export interface DailyPorRecepcion { usuario: string; ventas_count: number; ventas_total: number; cobros_total: number; } export interface HoraAgendaRow { hora: string; total: number; by_state: Record; } export interface PaqueteRow { id: number; name: string; package_sessions: number; package_price: number; price: number; citas: number; terminados: number; } export interface VendedorRow { usuario: string; num_ventas: number; total: number; cobrado: number; } export interface ConcentradoReport { start: string; end: string; citas: { total: number; by_state: Record }; ventas: { count: number; total: number; total_paid: number; total_due: number }; pagos: { total: number; count: number; by_method: Record }; devoluciones: { count: number; total: number }; pacientes_nuevos: number; visitas: { total: number; by_state: Record }; } export interface RecomendacionRow { recomendador: string; pacientes: number; } export interface MonederoPuntosReport { by_type: Record; puntos_activos: number; cuentas: number; equivalente_mxn: number; } export interface KpisNoShowMedico { medico: string; citas: number; no_shows: number; tasa: number; } export interface KpisOcupacionMedico { medico: string; horas_vendidas: number; horas_disponibles: number; horas_libres: number; ocupacion: number; } export interface KpisReport { start: string; end: string; no_show: { tasa: number; total_citas: number; total_no_shows: number; por_medico: KpisNoShowMedico[] }; ocupacion: { promedio: number; dias_habiles: number; por_medico: KpisOcupacionMedico[] }; leads: { total: number; ganados: number; tasa: number; dias_promedio_ganar: number | null }; primera_vez: { primera: number; subsecuentes: number; tasa: number }; } export interface AppNotification { tipo: string; titulo: string; detalle: string; link: string; severity: 'info' | 'warning' | 'danger'; } export interface PosCheckoutPayload { partner_id: number; lines: { service_id?: number; item_id?: number; quantity: number; price_unit: number; description?: string; prescribed_by_id?: number }[]; discount?: number; payment_method?: string; amount_received?: number; pay_with_points?: boolean; currency?: 'MXN' | 'USD'; } export interface PosCheckoutResult { status: string; sale: Sale; payment: Payment | null; cambio: number; cambio_usd?: number; rate_used?: number | null; recibido_usd?: number; puntos_usados: number; puntos_ganados: number; wallet_points: number; stock_warnings?: string[]; } export interface DailyReport { date: string; citas: { total: number; by_state: Record }; ventas: { count: number; total: number; total_paid: number; total_due: number }; pagos: { total: number; count: number; by_method: Record }; inventario: { total: number; by_type: Record }; } export interface InventoryReportSummary { total_value: number; count: number; out: number; critical: number; } export interface ApiResponse { status: string; message?: string; data?: T; } export interface Paginated { status: string; total: number; page: number; page_size: number; total_pages: number; items: T; } // ============================================================ // API // ============================================================ export const odooApi = { // Auth (frontend React) async login(login: string, password: string): Promise<{ status: string; token: string; user: FrontendUser; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/auth/login`, { login, password }); return data; }, async me(): Promise<{ status: string; user: FrontendUser }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/auth/me`); return data; }, async logout(): Promise<{ status: string }> { try { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/auth/logout`); return data; } catch { return { status: 'success' }; } }, async listFrontendUsers(): Promise<{ status: string; users: FrontendUser[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/auth/users`); return data; }, async createFrontendUser(payload: { login: string; name: string; password: string; role: FrontendRole }): Promise<{ status: string; user: FrontendUser; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/auth/users`, payload); return data; }, async updateFrontendUser(id: number, payload: Partial<{ name: string; role: FrontendRole; active: boolean; password: string; must_change_password: boolean; allowed_menus: string[] | null }>): Promise<{ status: string; user: FrontendUser; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/auth/users/${id}`, payload); return data; }, async deleteFrontendUser(id: number): Promise<{ status: string; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/auth/users/${id}`); return data; }, async getRolePermissions(): Promise<{ status: string; matrix: Record; roles: { key: FrontendRole; label: string }[]; message?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/roles/permissions`); return data; }, async updateRolePermissions(matrix: Record): Promise<{ status: string; matrix: Record; message?: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/roles/permissions`, { matrix }); return data; }, // Health async healthCheck(): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/health`); return data; }, // Dashboard async getDashboard(): Promise<{ status: string; stats: DashboardStats }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/dashboard`); return data; }, async getWeeklyChart(): Promise<{ status: string; data: ChartPoint[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/weekly_chart`); return data; }, // Citas async getAppointments(params?: Record): Promise<{ status: string; appointments: Appointment[]; bloqueos?: Bloqueo[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments`, { params }); return data; }, async createAppointment(appointment: Partial & { sessions?: number }): Promise<{ status: string; appointment: Appointment; appointments?: Appointment[]; sessions?: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments`, appointment); return data; }, async createQuickAppointment(payload: { name: string; phone: string; date: string; time: number; doctor_id?: number; notes?: string }): Promise<{ status: string; appointment: Appointment; patient: Patient; patient_created: boolean; message?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/quick`, payload); return data; }, async updateAppointment(id: number, appointment: Partial): Promise<{ status: string; appointment: Appointment }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}`, appointment); return data; }, async deleteAppointment(id: number): Promise<{ status: string; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}`); return data; }, async getAppointment(id: number): Promise<{ status: string; appointment: Appointment }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}`); return data; }, async bulkPhoneConfirm(ids: number[], confirmed = true): Promise<{ status: string; confirmed_count: number; skipped_invalid_state: number[]; not_found: number[] }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/bulk_phone_confirm`, { ids, confirmed }); return data; }, async checkPatientPhone(phone: string, excludeId?: number): Promise<{ status: string; exists: boolean; patient: { id: number; name: string; patient_id: string; phone: string; mobile: string } | null }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/check_phone`, { phone, exclude_id: excludeId }); return data; }, async updateAppointmentStatus(id: number, action: string, reason?: string): Promise<{ status: string; appointment: Appointment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}/status`, { action, reason }); 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): Promise<{ status: string; messages: ChatMessage[] }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/chat/messages`, { user_id: userId, since_id: sinceId || undefined }); 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): Promise<{ status: string; slots: { time: number; time_str: string }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments/slots`, { params: { date, servicio_id } }); return data; }, // Bloqueos de agenda async getBloqueos(params?: Record): Promise<{ status: string; bloqueos: Bloqueo[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/bloqueos`, { params }); return data; }, async createBloqueo(payload: { doctor_id: number; date: string; all_day?: boolean; time_from?: number; time_to?: number; motivo?: string }): Promise<{ status: string; bloqueo: Bloqueo }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/bloqueos`, 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; }, // Visitas clínicas async getVisitas(params?: Record): Promise<{ status: string; visitas: Visita[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/visitas`, { params }); return data; }, async getVisita(id: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}`); return data; }, async updateVisita(id: number, visita: Partial): Promise<{ status: string; visita: Visita }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}`, visita); return data; }, async completeVisita(id: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/complete`); return data; }, async cancelVisita(id: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/cancel`); return data; }, async addVisitaInsumo(id: number, payload: { item_id: number; qty: number; notes?: string }): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/insumos`, payload); return data; }, async deleteVisitaInsumo(id: number, lineId: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/insumos/${lineId}`); return data; }, async getInventarioItems(search?: string): Promise<{ status: string; items: InventoryItem[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventory/items`, { params: search ? { search } : {} }); return data; }, async uploadVisitaAdjunto(id: number, payload: { kind: string; name: string; file_b64: string; mimetype: string; notes?: string; antes_despues?: string }): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/visitas/${id}/adjuntos`, payload); return data; }, async deleteVisitaAdjunto(adjId: number): Promise<{ status: string; visita: Visita }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/visitas/adjuntos/${adjId}`); return data; }, // URL del binario de un adjunto con el token en query (para ) visitaAdjuntoUrl(url: string): string { const token = localStorage.getItem('skeen_token') || ''; return `${ODOO_BASE}${url}?token=${encodeURIComponent(token)}`; }, // Pacientes async getPatients(params?: string | { search?: string; page?: number; page_size?: number; incomplete?: number; vip?: number; recent?: number; doctor_id?: number }): Promise<{ status: string; patients: Patient[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const p = typeof params === 'string' ? { search: params } : params || {}; const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients`, { params: p }); return data; }, async getPatient(id: number): Promise<{ status: string; patient: Patient }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}`); return data; }, async createPatient(patient: Partial): Promise<{ status: string; patient: Patient }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients`, patient); return data; }, async updatePatient(id: number, patient: Partial): Promise<{ status: string; patient: Patient }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}`, patient); return data; }, async getPatientHistory(id: number): Promise<{ status: string; appointments: Appointment[]; sales: Sale[]; visitas: Visita[]; pagos: Payment[]; estado_cuenta: EstadoCuenta }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}/history`); return data; }, // URL de la foto del paciente con el token en query (para ) patientPhotoUrl(url: string): string { const token = localStorage.getItem('skeen_token') || ''; return `${ODOO_BASE}${url}?token=${encodeURIComponent(token)}`; }, // Expedientes recientes y adjuntos de paciente async getExpedientesRecientes(params?: Record): Promise<{ status: string; expedientes: ExpedienteReciente[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/expedientes-recientes`, { params }); return data; }, async getPatientAdjuntos(id: number, kind?: string): Promise<{ status: string; adjuntos: PatientAdjunto[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}/adjuntos`, { params: kind ? { kind } : {} }); return data; }, async uploadPatientAdjunto(id: number, payload: { kind: string; name: string; file_b64: string; mimetype: string; notes?: string; antes_despues?: string }): Promise<{ status: string; adjunto: PatientAdjunto }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/${id}/adjuntos`, payload); return data; }, async deletePatientAdjunto(adjId: number): Promise<{ status: string; message: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/patients/adjuntos/${adjId}`); return data; }, // URL del binario de un adjunto de paciente con el token en query (para ) patientAdjuntoUrl(url: string): string { const token = localStorage.getItem('skeen_token') || ''; return `${ODOO_BASE}${url}?token=${encodeURIComponent(token)}`; }, // Recetas (plantillas) async getRecetas(search?: string): Promise<{ status: string; recetas: Receta[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/recetas`, { params: search ? { search } : {} }); return data; }, async createReceta(receta: Partial): Promise<{ status: string; receta: Receta }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/recetas`, receta); return data; }, async updateReceta(id: number, receta: Partial): Promise<{ status: string; receta: Receta }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/recetas/${id}`, receta); return data; }, async deleteReceta(id: number): Promise<{ status: string; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/recetas/${id}`); return data; }, // Catálogos: diagnósticos y procedimientos async getDiagnosticos(search?: string): Promise<{ status: string; items: Diagnostico[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/diagnosticos`, { params: search ? { search } : {} }); return data; }, async createDiagnostico(payload: Partial): Promise<{ status: string; item: Diagnostico }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/diagnosticos`, payload); return data; }, async updateDiagnostico(id: number, payload: Partial): Promise<{ status: string; item: Diagnostico }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/diagnosticos/${id}`, payload); return data; }, async deleteDiagnostico(id: number): Promise<{ status: string; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/diagnosticos/${id}`); return data; }, async getProcedimientos(search?: string): Promise<{ status: string; items: Procedimiento[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/procedimientos`, { params: search ? { search } : {} }); return data; }, async createProcedimiento(payload: Partial): Promise<{ status: string; item: Procedimiento }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/procedimientos`, payload); return data; }, async updateProcedimiento(id: number, payload: Partial): Promise<{ status: string; item: Procedimiento }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/procedimientos/${id}`, payload); return data; }, async deleteProcedimiento(id: number): Promise<{ status: string; message?: string }> { const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/procedimientos/${id}`); return data; }, // Consultas médicas directas async createConsulta(payload: { partner_id: number; doctor_id?: number | null; motivo?: string; diagnostico?: string; tratamiento?: string; notas?: string }): Promise<{ status: string; visita: Visita }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/consultas`, payload); return data; }, // Reporte de adeudos (cartera por cobrar) async getAdeudos(): Promise<{ status: string; adeudos: AdeudoRow[]; total_cartera: number; total_pacientes: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/adeudos`); return data; }, // Cobro del adeudo del paciente (parcial o total) + plan de pagos async payAdeudo(patientId: number, payload: { amount: number; payment_method: string }): Promise<{ status: string; payment: Payment; receipt: ReceiptData }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/pay_adeudo`, payload); return data; }, async setPaymentPlan(patientId: number, payload: { count: number; amount: number; next_payment_date?: string | null; note?: string }): Promise<{ status: string; plan: PaymentPlan }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/payment_plan`, payload); return data; }, // Servicios async getServices(params?: { search?: string; category?: string; page?: number; page_size?: number; sort?: 'manual' | 'best_sellers' }): Promise<{ status: string; services: Service[]; total?: number; page?: number; total_pages?: number; sort?: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/services`, { params }); return data; }, async globalSearch(q: string): Promise<{ status: string; patients: { id: number; name: string; phone: string }[]; appointments: { id: number; patient: string; service: string; date: string; time: string }[]; services: { id: number; name: string; price: number }[]; }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/search`, { params: { q } }); return data; }, async createService(service: Partial): Promise<{ status: string; service: Service }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/services`, service); return data; }, async reorderServices(ids: number[], to?: 'front'): Promise<{ status: string; total: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/services/reorder`, { ids, ...(to ? { to } : {}) }); return data; }, async updateService(id: number, service: Partial): Promise<{ status: string; service: Service }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/services/${id}`, service); return data; }, // Médicos async getDoctors(): Promise<{ status: string; doctors: Doctor[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors`); return data; }, async updateDoctor(id: number, payload: { commission_pct?: number; staff_type?: 'doctor' | 'cosmetologa'; monthly_goal?: number }): Promise<{ status: string; doctor: Doctor }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}`, payload); return data; }, // Horario semanal del médico (reemplaza la lista completa) async getDoctorHorario(id: number): Promise<{ status: string; horario: HorarioDia[]; tiene_horario: boolean; horario_resumen: string }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/horario`); return data; }, async setDoctorHorario(id: number, horario: HorarioDia[]): Promise<{ status: string; horario: HorarioDia[]; tiene_horario: boolean; horario_resumen: string }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/horario`, { horario }); return data; }, async setDoctorCommission(id: number, commission_pct: number): Promise<{ status: string; doctor: { id: number; name: string; commission_pct: number } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/commission`, { commission_pct }); return data; }, async getCommissionDefault(): Promise<{ status: string; commission_pct: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/config/commission_default`); return data; }, async setCommissionDefault(commission_pct: number): Promise<{ status: string; commission_pct: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/config/commission_default`, { commission_pct }); return data; }, async getDoctorHistory(id: number): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/history`); return data; }, // Comisiones async getCommissions(start?: string, end?: string): Promise<{ status: string; start: string; end: string; total_base: number; total_commission: number; commissions: CommissionRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/commissions`, { params: { start, end } }); return data; }, // Productos async getProducts(): Promise<{ status: string; products: Product[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/products`); return data; }, // Monederos async getWallets(params?: string | { phone?: string; search?: string; page?: number; page_size?: number }): Promise<{ status: string; wallets: Wallet[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const p = typeof params === 'string' ? { phone: params } : params || {}; const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wallets`, { params: p }); return data; }, async getWalletByPhone(phone: string): Promise<{ status: string; wallet: { points: number; equivalent_mxn: number; history: unknown[] } }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wallets/${phone}`); return data; }, async walletTransaction(payload: { phone: string; type: 'accrual' | 'redemption'; points: number; description?: string }): Promise<{ status: string; wallet: Wallet }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wallets/transaction`, payload); return data; }, // Movimientos del monedero con narración legible async getWalletTransactions(walletId: number, params?: { type?: string; date_from?: string; date_to?: string }): Promise<{ status: string; wallet: Wallet; movimientos: WalletTransaction[]; total: number; sum_positive: number; sum_negative: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wallets/${walletId}/transactions`, { params: params || {} }); return data; }, // Ajuste manual auditado de puntos (admin/gerencia) async adjustWallet(walletId: number, payload: { points: number; reason: string }): Promise<{ status: string; wallet: Wallet; applied: number; movimiento: { id: number; points: number; date: string; operator: string; narration: string } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wallets/${walletId}/adjust`, payload); return data; }, // Pagos async getPayments(state?: string): Promise<{ status: string; payments: Payment[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/payments`, { params: state ? { state } : {} }); return data; }, async createPayment(payment: Partial): Promise<{ status: string; payment: Payment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/payments`, payment); return data; }, async confirmPayment(id: number): Promise<{ status: string; payment: Payment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/payments/${id}/confirm`); return data; }, // Ventas async getSales(params?: Record): Promise<{ status: string; sales: Sale[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/sales`, { params }); return data; }, // "Mis ventas" (rol medico): el servidor filtra por el employee del token async getMySales(range: '30d' | 'month' | 'last_month' = '30d'): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/my/sales`, { params: { range } }); return data; }, async createSale(sale: Omit, 'lines'> & { lines: Omit[] }): Promise<{ status: string; sale: Sale }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales`, sale); return data; }, async paySale(id: number, payload: { payment_method: string; amount?: number }): Promise<{ status: string; sale: Sale; payment: Payment }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales/${id}/pay`, payload); return data; }, async refundSale(id: number, payload: { amount?: number; reason?: string }): Promise<{ status: string; sale: Sale; result: { refund_amount: number; points_reversed: number; new_state: string; new_amount_paid: number } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales/${id}/refund`, payload); return data; }, // Cancelación con nota de crédito (permiso admin/gerencia) async cancelSale(id: number, payload: { reason: string; restore_stock?: boolean }): Promise<{ status: string; sale: Sale; result: { points_reversed: number; points_restored: number; stock_moves: number } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales/${id}/cancel`, payload); return data; }, // Última venta del paciente con precios actuales del catálogo (POS: repetir venta) async getLastSale(patientId: number): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/sales/last`, { params: { patient_id: patientId } }); return data; }, // Paquetes del paciente con sesiones restantes (POS: aviso en el ticket) async getPatientPackages(patientId: number): Promise<{ status: string; packages: PatientPackage[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/packages`); return data; }, // Cortes de caja async getCashClosings(date?: string): Promise<{ status: string; cash_closings: CashClosing[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/cash_closings`, { params: date ? { date } : {} }); return data; }, async createCashClosing(payload: { date?: string; opening_cash: number; notes?: string }): Promise<{ status: string; cash_closing: CashClosing }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/cash_closings`, payload); return data; }, async closeCashClosing(id: number, payload: { closing_cash?: number; counted_cash?: number; counted_card?: number; counted_transfer?: number; counted_other?: number; notes?: string }): Promise<{ status: string; cash_closing: CashClosing }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/cash_closings/${id}/close`, payload); return data; }, async updatePatientAdjunto(adjId: number, payload: { antes_despues?: string; visita_id?: number | null; cita_id?: number | null; servicio_id?: number | null }): Promise<{ status: string; adjunto: PatientAdjunto }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/patients/adjuntos/${adjId}`, payload); return data; }, // Reportes async getSalesReport(start?: string, end?: string): Promise<{ status: string; total_sales: number; total_paid: number; total_due: number; count: number; by_day?: { date: string; total: number }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/sales`, { params: { start, end } }); return data; }, async getDailyReport(date?: string, by?: 'medico' | 'recepcion'): Promise<{ status: string } & DailyReport & { por_medico?: DailyPorMedico[]; por_recepcion?: DailyPorRecepcion[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/daily`, { params: { ...(date ? { date } : {}), ...(by ? { by } : {}) }, }); return data; }, async getComisiones(start?: string, end?: string): Promise<{ status: string; start: string; end: string; comisiones: ComisionRow[]; total_vendido: number; total_comision: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/comisiones`, { params: { start, end } }); return data; }, async getPagosServicios(start?: string, end?: string): Promise<{ status: string; total_cobrado: number; total_pagos: number; by_method: Record; servicios: PagoServicioRow[]; total_pagado_completo: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/pagos-servicios`, { params: { start, end } }); return data; }, async getPagosClientes(start?: string, end?: string): Promise<{ status: string; clientes: PagoClienteRow[]; total_cobrado: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/pagos-clientes`, { params: { start, end } }); return data; }, async getDevoluciones(start?: string, end?: string): Promise<{ status: string; devoluciones: DevolucionRow[]; total_devuelto: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/devoluciones`, { params: { start, end } }); return data; }, async getTopClientes(start?: string, end?: string, limit = 20): Promise<{ status: string; clientes: TopClienteRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/top-clientes`, { params: { start, end, limit } }); return data; }, async getMovimientos(params?: Record): Promise<{ status: string; movimientos: MovimientoRow[]; total?: number; page?: number; page_size?: number; total_pages?: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventario/movimientos`, { params }); return data; }, async createCompra(payload: { item_id: number; qty: number; cost?: number; reference?: string; notes?: string }): Promise<{ status: string; move: InventoryMove; item: InventoryItem }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventario/compras`, payload); return data; }, async createBaja(payload: { item_id: number; qty: number; reference?: string; notes?: string }): Promise<{ status: string; move: InventoryMove; item: InventoryItem }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventario/bajas`, payload); return data; }, async getInventarioAlertas(): Promise<{ status: string; caducidades: CaducidadRow[]; sugerencia_compra: SugerenciaRow[]; sugerencia_total: { items: number; costo_total: number } }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventario/alertas`); return data; }, // Conteo físico cíclico de inventario async getConteos(limit = 30): Promise<{ status: string; conteos: InventoryConteo[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventario/conteos`, { params: { limit } }); return data; }, async getConteo(id: number): Promise<{ status: string; conteo: InventoryConteo }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventario/conteos/${id}`); return data; }, async createConteo(payload: { notes?: string; lines: { item_id: number; qty_contado: number }[] }): Promise<{ status: string; conteo: InventoryConteo; ajustes: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventario/conteos`, payload); return data; }, async getHorasAgenda(start?: string, end?: string): Promise<{ status: string; horas: HoraAgendaRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/horas-agenda`, { params: { start, end } }); return data; }, async getPaquetes(): Promise<{ status: string; paquetes: PaqueteRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/paquetes`); return data; }, async getVendedores(start?: string, end?: string): Promise<{ status: string; vendedores: VendedorRow[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/vendedores`, { params: { start, end } }); return data; }, async getConcentrado(start?: string, end?: string): Promise<{ status: string } & ConcentradoReport> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/concentrado`, { params: { start, end } }); return data; }, async getRecomendaciones(): Promise<{ status: string; recomendaciones: RecomendacionRow[]; total: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/recomendaciones`); return data; }, async getMonederoPuntos(start?: string, end?: string): Promise<{ status: string } & MonederoPuntosReport> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/monedero-puntos`, { params: { start, end } }); return data; }, async getKpis(start?: string, end?: string): Promise<{ status: string } & KpisReport> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/kpis`, { params: { start, end } }); return data; }, async getNotifications(): Promise<{ status: string; notifications: AppNotification[]; total: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/notifications`); return data; }, // POS: venta + pago en una sola operación async posCheckout(payload: PosCheckoutPayload): Promise { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/pos/checkout`, payload); return data; }, async getCashReportRange(start: string, end: string): Promise<{ status: string; total: number; by_method: Record }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/cash`, { params: { start, end } }); return data; }, async getWacrmConversationMessages(conversationId: string, page = 1, pageSize = 50): Promise<{ status: string; messages: WacrmMessage[]; page: number; page_size: number; has_more: boolean }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations/${conversationId}/messages`, { params: { page, page_size: pageSize } }); return data; }, async getInventoryReport(): Promise<{ status: string; summary: InventoryReportSummary; items: InventoryItem[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/inventory`); return data; }, async getAppointmentsReport(start?: string, end?: string): Promise<{ status: string; total: number; by_state: Record }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/appointments`, { params: { start, end } }); return data; }, async getTopServices(start?: string, end?: string, limit = 8): Promise<{ status: string; start: string; end: string; services: { service_id: number; service: string; qty: number; revenue: number }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/top_services`, { params: { start, end, limit } }); return data; }, async getCashReport(date?: string): Promise<{ status: string; total: number; by_method: Record }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/cash`, { params: { date } }); return data; }, // Tipo de cambio async getExchangeRate(): Promise<{ status: string; current: { rate: number; date: string; source: string } | null; history: { id: number; date: string; rate: number; source: string }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/exchange_rate`); return data; }, async setExchangeRate(rate: number, source = 'manual'): Promise<{ status: string; exchange_rate: { id: number; date: string; rate: number; source: string } }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/exchange_rate`, { rate, source }); return data; }, // Meta de ventas mensual async getSalesGoal(): Promise<{ status: string; goal: number; current: number; pct: number; start: string; end: string; count: number }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/sales_goal`); return data; }, async setSalesGoal(goal: number): Promise<{ status: string; goal: number }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/sales_goal`, { goal }); return data; }, // Cumpleañeros async getBirthdays(period: 'today' | 'week' | 'month' = 'month'): Promise<{ status: string; period: string; start: string; end: string; total: number; birthdays: Birthday[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/birthdays`, { params: { period } }); return data; }, // Inventario / Consumibles async getInventory(params?: { kind?: InventoryKind; level?: InventoryLevel; search?: string }): Promise<{ status: string; items: InventoryItem[]; summary: InventorySummary }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventory/items`, { params }); return data; }, async createInventoryItem(item: Partial): Promise<{ status: string; item: InventoryItem }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventory/items`, item); return data; }, async updateInventoryItem(id: number, item: Partial): Promise<{ status: string; item: InventoryItem }> { const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/inventory/items/${id}`, item); return data; }, async getInventoryMoves(id: number): Promise<{ status: string; moves: InventoryMove[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/inventory/items/${id}/moves`); return data; }, async createInventoryMove(id: number, payload: { type: InventoryMoveType; qty: number; reference?: string; notes?: string }): Promise<{ status: string; move: InventoryMove; item: InventoryItem }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/inventory/items/${id}/moves`, payload); return data; }, // ============================================================ // WACRM Proxy // ============================================================ async syncWacrm(): Promise<{ status: string; result: Record }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/sync`); return data; }, async getWacrmMembers(): Promise<{ status: string; members: { id: string; profile_id?: string; name: string; email?: string; avatar_url?: string; role: string }[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/members`); return data; }, async assignWacrmConversation(external_id: string, assigned_agent_id: string | null): Promise<{ status: string; assigned_agent_id?: string }> { const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations/${external_id}/assign`, { assigned_agent_id, }); return data; }, async getWacrmConversations(search?: string): Promise<{ status: string; conversations: WacrmConversation[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations`, { params: search ? { search } : {} }); return data; }, 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; }, };