import type { FC } from 'react'; import { useEffect, useState, useCallback } from 'react'; import { Stethoscope, ChevronDown, ChevronRight, Coins, Target } from 'lucide-react'; import { Card, Input, EmptyState, Skeleton, toast } from '../ui'; import { ExportButton } from '../ui/ExportButton'; import { exportToExcel } from '../../lib/exporter'; import { odooApi, type ComisionRow, type ComisionReporteReglas, type ComisionEmpleadoCalc } from '../../services/odoo'; import { useReportRange } from '../../lib/reportRange'; import { useClinicSettings } from '../../lib/clinic-settings'; const formatCurrency = (value: number) => new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN', maximumFractionDigits: 0 }).format(value || 0); // --------------------------------------------------------------------------- // Reporte "de siempre": porcentaje plano por empleado (`/reports/comisiones`). // Se conserva intacto para cuando `comisiones.activo=false`. // --------------------------------------------------------------------------- const ReportePlano: FC = () => { const { start, end, setStart, setEnd } = useReportRange('comisiones'); const [rows, setRows] = useState([]); const [totales, setTotales] = useState({ vendido: 0, comision: 0 }); const [loading, setLoading] = useState(true); const load = useCallback(async () => { try { setLoading(true); const res = await odooApi.getComisiones(start, end); if (res.status === 'success') { setRows(res.comisiones); setTotales({ vendido: res.total_vendido, comision: res.total_comision }); } } catch (err) { toast.error('Error al cargar comisiones'); console.error(err); } finally { setLoading(false); } }, [start, end]); useEffect(() => { load(); }, [load]); const [exporting, setExporting] = useState(false); const exportar = async () => { try { setExporting(true); await exportToExcel({ filename: `comisiones-${start}_${end}`, sheetName: 'Comisiones', title: 'Comisiones por médico', subtitle: `Del ${start} al ${end}`, columns: [ { header: 'Médico', key: 'doctor' }, { header: 'Puesto', key: 'job_title' }, { header: 'Citas completadas', key: 'citas_done', format: 'number' }, { header: 'Líneas', key: 'items', format: 'number' }, { header: 'Ventas', key: 'sales', format: 'number' }, { header: 'Total vendido', key: 'total_vendido', format: 'currency' }, { header: '% Comisión', key: 'commission_pct', format: 'number' }, { header: 'Comisión estimada', key: 'comision', format: 'currency' }, ], rows: rows.map((r) => ({ ...r })), totals: { doctor: '', total_vendido: totales.vendido, comision: totales.comision }, }); toast.success('Excel descargado'); } catch (err) { toast.error('Error al exportar'); console.error(err); } finally { setExporting(false); } }; return (
setStart(e.target.value)} className="sm:max-w-[160px]" /> setEnd(e.target.value)} className="sm:max-w-[160px]" />

Vendido: {formatCurrency(totales.vendido)} {' · '}Comisiones: {formatCurrency(totales.comision)}

{loading ? ( ) : rows.length === 0 ? ( } /> ) : (
{rows.map((r) => ( ))}
Médico Citas completadas Líneas Ventas Total vendido % Comisión Comisión estimada

{r.doctor}

{r.job_title || '—'}

{r.citas_done} {r.items} {r.sales} {formatCurrency(r.total_vendido)} {r.commission_pct}% {formatCurrency(r.comision)}
)}
); }; // --------------------------------------------------------------------------- // Reporte del motor (`/reports/comisiones_reglas`): desglose por regla/línea, // por servicio/categoría y comparativa contra metas. // --------------------------------------------------------------------------- const FilaEmpleado: FC<{ emp: ComisionEmpleadoCalc }> = ({ emp }) => { const [open, setOpen] = useState(false); return ( <> setOpen((v) => !v)}>
{open ? : }

{emp.empleado}

{emp.staff_type} · {emp.citas_done} cita(s) · {emp.lineas_count} línea(s)

{formatCurrency(emp.base)} {formatCurrency(emp.comision)} {formatCurrency(emp.bonos)} {formatCurrency(emp.ajustes)} {formatCurrency(emp.total)} {open && ( {emp.metas.length > 0 && (

Metas

{emp.metas.map((m, i) => (

{m.periodo}: objetivo {formatCurrency(m.monto_objetivo)} · {m.alcanzada ? 'alcanzada' : 'no alcanzada'} · bono {formatCurrency(m.bono)}

))}
)} {emp.por_regla.length > 0 && (

Por regla

{emp.por_regla.map((r, i) => ( ))}
{r.regla} ({r.lineas} línea/s){formatCurrency(r.base)}{formatCurrency(r.comision)}
)}

Desglose por línea

{emp.desglose.map((d, i) => ( ))}
ReferenciaBaseComisión
{d.referencia}{formatCurrency(d.base)}{formatCurrency(d.comision)}
)} ); }; const ReporteReglas: FC = () => { const { start, end, setStart, setEnd } = useReportRange('comisiones'); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [exporting, setExporting] = useState(false); const load = useCallback(async () => { try { setLoading(true); const res = await odooApi.getComisionesReglasReport(start, end); if (res.status === 'success') setData(res); } catch (err) { const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; toast.error(msg || 'Error al cargar comisiones'); } finally { setLoading(false); } }, [start, end]); useEffect(() => { load(); }, [load]); const exportar = async () => { if (!data) return; const filas = data.empleados.flatMap((e) => e.desglose.map((d) => ({ empleado: e.empleado, ...d }))); if (filas.length === 0) { toast.info('Sin líneas que exportar'); return; } setExporting(true); try { await exportToExcel({ filename: `comisiones-reglas-${start}_${end}`, sheetName: 'Comisiones', title: 'Comisiones por reglas', subtitle: `Del ${start} al ${end} · base ${data.config.base}`, columns: [ { header: 'Empleado', key: 'empleado' }, { header: 'Fecha', key: 'fecha' }, { header: 'Folio', key: 'folio' }, { header: 'Servicio', key: 'servicio' }, { header: 'Categoría', key: 'categoria' }, { header: 'Regla', key: 'regla' }, { header: 'Base', key: 'base', format: 'currency' }, { header: 'Comisión', key: 'comision', format: 'currency' }, { header: 'Referencia', key: 'referencia' }, ], rows: filas as unknown as Record[], }); toast.success('Excel descargado'); } catch { toast.error('Error al exportar'); } finally { setExporting(false); } }; return (
setStart(e.target.value)} className="sm:max-w-[160px]" /> setEnd(e.target.value)} className="sm:max-w-[160px]" />
{data && (

Base: {formatCurrency(data.totales.base)} {' · '}Comisión: {formatCurrency(data.totales.comision)} {' · '}Total: {formatCurrency(data.totales.total)}

)}

Motor activo · base {data?.config.base} · {data?.config.incluir_impuestos ? 'con impuestos' : 'sin impuestos'} · {' '}{data?.config.neto_de_devoluciones ? 'neto de devoluciones' : 'sin descontar devoluciones'}

{loading ? ( ) : !data || data.empleados.length === 0 ? ( } /> ) : ( <>
{data.empleados.map((e) => )}
Empleado Base Comisión Bonos Ajustes Total

Por servicio

{(data.por_servicio || []).map((s, i) => ( ))}
ServicioBaseComisión
{s.servicio}{formatCurrency(s.base)}{formatCurrency(s.comision)}

Por categoría

{(data.por_categoria || []).map((c, i) => ( ))}
CategoríaBaseComisión
{c.categoria}{formatCurrency(c.base)}{formatCurrency(c.comision)}
)}
); }; const ComisionesReport: FC = () => { const { comisiones } = useClinicSettings(); // Con el motor apagado se muestra EXACTAMENTE el reporte de siempre. if (!comisiones.activo) return ; return ; }; export default ComisionesReport;