Mejoras integrales: visitas, POS, WACRM, reportes, temas y módulos legacy

- Módulo Visitas completo (auto desde agenda, insumos con descargo de
  inventario, fotos antes/después y documentos, receta imprimible)
- Punto de Venta (catálogo + ticket sticky, cobro con cambio, pago con
  puntos monedero, ticket imprimible)
- WACRM: leads automáticos desde WhatsApp, asignación de conversaciones
  y leads a agentes, conversión lead→paciente, ficha del paciente en chat
- Pacientes: completitud de expediente, alertas clínicas, historial
  unificado con detalle, foto, WhatsApp, estado de cuenta, filtros
  rápidos (VIP/recientes/médico), documentos (expediente escaneado + galería)
- Agenda: vistas por médico y por hora, filtros rápidos (libres, primera
  vez, check-in, no-show), modal de acciones, bloqueos por médico,
  drag&drop para mover citas
- Reportes: 18 pestañas (diario, cortes, ingresos, inventario, adeudos,
  comisiones, pagos, devoluciones, top clientes, horas, paquetes,
  vendedores, concentrado, recomendaciones, KPIs) con exportación Excel
- Temas: nuevo tema Clásico (look legacy AdminLTE) con submenús tipo
  treeview, selector de tema; accesos rápidos personalizables con 3
  presentaciones; búsqueda global; notificaciones reales
- Configuración: secciones (clínica, usuarios con permisos por sección,
  recetas, catálogos de diagnósticos y procedimientos)
- Inventario: alertas de caducidad y sugerencia de compra, cron diario
  que descuenta artículos caducados, compras/bajas
- Consultas Médicas, página Expedientes, importadores delta
  (citas/visitas legacy idempotentes), depuración de duplicados
- Infra: tema Tailwind conectado (@config), gzip en nginx, secuencias
  Odoo corregidas (noupdate, company_id), rollback en validaciones
This commit is contained in:
2026-08-13 23:30:08 +00:00
parent a718592291
commit 01f6007e30
109 changed files with 19067 additions and 931 deletions

View File

@@ -58,7 +58,10 @@ export interface Appointment {
state: string;
payment_state: string;
price: number;
doctor_id?: number;
amount_paid?: number;
duration?: number;
is_first_visit?: boolean;
doctor_id?: number | null;
doctor?: string;
branch?: string;
medium?: string;
@@ -67,6 +70,64 @@ export interface Appointment {
package_finished_date?: string | null;
}
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;
@@ -83,8 +144,12 @@ export interface Patient {
total_spent: number;
source: string;
is_vip: boolean;
primary_doctor_id?: number;
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;
@@ -122,6 +187,42 @@ export interface Patient {
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;
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 {
@@ -147,6 +248,7 @@ export interface Doctor {
work_phone: string;
work_email: string;
commission_pct: number;
patient_count?: number;
}
export interface Product {
@@ -364,6 +466,8 @@ export interface WacrmLead {
expected_close_date: string | null;
notes: string;
assigned_to: string;
partner_id?: number | null;
conversation_id?: string;
created_at: string | null;
}
@@ -377,6 +481,237 @@ export interface FrontendUser {
must_change_password?: boolean;
last_login?: string | null;
active?: boolean;
allowed_menus?: string[] | null;
}
export interface Receta {
id: number;
name: string;
categoria: string;
contenido: string;
}
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;
}
export interface ComisionRow {
doctor_id: number;
doctor: string;
job_title: string;
items: number;
sales: number;
citas_done: number;
total_vendido: number;
commission_pct: number;
comision: number;
}
export interface MovimientoRow extends InventoryMove {
item: string;
unit: string;
}
export interface CaducidadRow {
id: number;
name: string;
category: string;
qty: number;
unit: string;
expiry_date: string;
dias_restantes: number;
vencido: boolean;
}
export interface SugerenciaRow {
id: number;
name: string;
category: string;
unit: string;
qty: number;
qty_min: number;
qty_optimal: number;
sugerido: number;
costo_estimado: number;
}
export interface PagoServicioRow {
servicio: string;
cantidad: number;
total: number;
}
export interface PagoClienteRow {
id: number;
name: string;
num_pagos: number;
total_cobrado: number;
ultimo_pago: string | null;
}
export interface DevolucionRow {
id: number;
folio: string;
paciente: string;
fecha: string | null;
total: number;
refund_amount: number;
refund_reason: string;
refunded_at: string | null;
}
export interface TopClienteRow {
id: number;
name: string;
num_ventas: number;
total: number;
ticket_promedio: number;
}
export interface DailyPorMedico {
medico: string;
citas: number;
by_state: Record<string, number>;
ventas: number;
cobros: number;
}
export interface DailyPorRecepcion {
usuario: string;
ventas_count: number;
ventas_total: number;
cobros_total: number;
}
export interface HoraAgendaRow {
hora: string;
total: number;
by_state: Record<string, number>;
}
export interface PaqueteRow {
id: number;
name: string;
package_sessions: number;
package_price: number;
price: number;
citas: number;
terminados: number;
}
export interface VendedorRow {
usuario: string;
num_ventas: number;
total: number;
cobrado: number;
}
export interface ConcentradoReport {
start: string;
end: string;
citas: { total: number; by_state: Record<string, number> };
ventas: { count: number; total: number; total_paid: number; total_due: number };
pagos: { total: number; count: number; by_method: Record<string, number> };
devoluciones: { count: number; total: number };
pacientes_nuevos: number;
visitas: { total: number; by_state: Record<string, number> };
}
export interface RecomendacionRow {
recomendador: string;
pacientes: number;
}
export interface MonederoPuntosReport {
by_type: Record<string, number>;
puntos_activos: number;
cuentas: number;
equivalente_mxn: number;
}
export interface KpisNoShowMedico {
medico: string;
citas: number;
no_shows: number;
tasa: number;
}
export interface KpisOcupacionMedico {
medico: string;
horas_vendidas: number;
horas_disponibles: number;
horas_libres: number;
ocupacion: number;
}
export interface KpisReport {
start: string;
end: string;
no_show: { tasa: number; total_citas: number; total_no_shows: number; por_medico: KpisNoShowMedico[] };
ocupacion: { promedio: number; dias_habiles: number; por_medico: KpisOcupacionMedico[] };
leads: { total: number; ganados: number; tasa: number; dias_promedio_ganar: number | null };
primera_vez: { primera: number; subsecuentes: number; tasa: number };
}
export interface AppNotification {
tipo: string;
titulo: string;
detalle: string;
link: string;
severity: 'info' | 'warning' | 'danger';
}
export interface PosCheckoutPayload {
partner_id: number;
lines: { service_id: number; quantity: number; price_unit: number; description?: string; prescribed_by_id?: number }[];
discount?: number;
payment_method?: string;
amount_received?: number;
pay_with_points?: boolean;
}
export interface PosCheckoutResult {
status: string;
sale: Sale;
payment: Payment | null;
cambio: number;
puntos_usados: number;
puntos_ganados: number;
wallet_points: number;
}
export interface DailyReport {
date: string;
citas: { total: number; by_state: Record<string, number> };
ventas: { count: number; total: number; total_paid: number; total_due: number };
pagos: { total: number; count: number; by_method: Record<string, number> };
inventario: { total: number; by_type: Record<string, number> };
}
export interface InventoryReportSummary {
total_value: number;
count: number;
out: number;
critical: number;
}
export interface ApiResponse<T> {
@@ -428,11 +763,16 @@ export const odooApi = {
return data;
},
async updateFrontendUser(id: number, payload: Partial<{ name: string; role: FrontendRole; active: boolean; password: string; must_change_password: boolean }>): Promise<{ status: string; user: FrontendUser; message?: string }> {
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;
},
// Health
async healthCheck(): Promise<unknown> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/health`);
@@ -451,7 +791,7 @@ export const odooApi = {
},
// Citas
async getAppointments(params?: Record<string, string | number>): Promise<{ status: string; appointments: Appointment[]; total?: number; page?: number; page_size?: number; total_pages?: number }> {
async getAppointments(params?: Record<string, string | number>): 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;
},
@@ -486,8 +826,81 @@ export const odooApi = {
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_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<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 }): 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 }): Promise<{ status: string; patients: Patient[]; total?: number; page?: number; page_size?: number; total_pages?: number }> {
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;
@@ -503,14 +916,131 @@ export const odooApi = {
return data;
},
async getPatientHistory(id: number): Promise<{ status: string; appointments: Appointment[]; sales: Sale[] }> {
async getPatientHistory(id: number): Promise<{ status: string; appointments: Appointment[]; sales: Sale[]; visitas: Visita[]; 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 }): 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;
},
// Servicios
async getServices(): Promise<{ status: string; services: Service[] }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/services`);
async getServices(params?: { search?: string; category?: string; page?: number; page_size?: number }): Promise<{ status: string; services: Service[]; total?: number; page?: number; total_pages?: number }> {
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;
},
@@ -618,11 +1148,119 @@ export const odooApi = {
},
// Reportes
async getSalesReport(start?: string, end?: string): Promise<{ status: string; total_sales: number; total_paid: number; total_due: number; count: number }> {
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;
},
async getHorasAgenda(start?: string, end?: string): Promise<{ status: string; horas: HoraAgendaRow[] }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/horas-agenda`, { params: { start, end } });
return data;
},
async getPaquetes(): Promise<{ status: string; paquetes: PaqueteRow[] }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/paquetes`);
return data;
},
async getVendedores(start?: string, end?: string): Promise<{ status: string; vendedores: VendedorRow[] }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/vendedores`, { params: { start, end } });
return data;
},
async getConcentrado(start?: string, end?: string): Promise<{ status: string } & ConcentradoReport> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/concentrado`, { params: { start, end } });
return data;
},
async getRecomendaciones(): Promise<{ status: string; recomendaciones: RecomendacionRow[]; total: number }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/recomendaciones`);
return data;
},
async getMonederoPuntos(start?: string, end?: string): Promise<{ status: string } & MonederoPuntosReport> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/monedero-puntos`, { params: { start, end } });
return data;
},
async getKpis(start?: string, end?: string): Promise<{ status: string } & KpisReport> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/kpis`, { params: { start, end } });
return data;
},
async getNotifications(): Promise<{ status: string; notifications: AppNotification[]; total: number }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/notifications`);
return data;
},
// POS: venta + pago en una sola operación
async posCheckout(payload: PosCheckoutPayload): Promise<PosCheckoutResult> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/pos/checkout`, payload);
return data;
},
async getCashReportRange(start: string, end: string): Promise<{ status: string; total: number; by_method: Record<string, number> }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/cash`, { params: { start, end } });
return data;
},
async 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;
@@ -700,7 +1338,7 @@ export const odooApi = {
return data;
},
async getWacrmMembers(): Promise<{ status: string; members: { id: string; name: string; email?: string; avatar_url?: string; role: string }[] }> {
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;
},
@@ -741,8 +1379,13 @@ export const odooApi = {
return data;
},
async updateWacrmLeadStatus(id: number, payload: { status?: 'open' | 'won' | 'lost'; stage_id?: string }): Promise<{ status: string; lead: WacrmLead }> {
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;
},
};