import type { FC } from 'react'; import { useCallback, useEffect, useState } from 'react'; import { Repeat, RefreshCw } from 'lucide-react'; import { Card, Button, EmptyState, Skeleton, Badge, toast } from '../ui'; import { odooApi, type MembresiasReporte } from '../../services/odoo'; import { useReportRange } from '../../lib/reportRange'; const fmtMoney = (value: number, currency = 'MXN') => new Intl.NumberFormat('es-MX', { style: 'currency', currency, maximumFractionDigits: 2 }).format(value || 0); // Pestaña "Membresías" del módulo de Reportes: altas, cancelaciones, ingresos // recurrentes por mes, próximas renovaciones y sesiones consumidas. const MembresiasReport: FC = () => { const { start, end } = useReportRange('membresias'); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const load = useCallback(async () => { try { setLoading(true); const res = await odooApi.getMembresiasReporte({ start, end }); if (res.status === 'success') setData(res); else toast.error(res.message || 'No se pudo cargar el reporte'); } catch (err) { const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; toast.error(msg || 'Error al cargar el reporte de membresías'); } finally { setLoading(false); } }, [start, end]); useEffect(() => { load(); }, [load]); if (loading) { return ; } if (!data) { return ( } /> ); } const totalConsumidas = data.sesiones_consumidas || 0; const incluidas = data.sesiones_incluidas_activas || 0; return (

Periodo {data.start} a {data.end}

Altas del periodo

{data.altas}

Cancelaciones

{data.cancelaciones}

Membresías activas

{data.membresias_activas}

Ingresos por membresía

{fmtMoney(data.total_ingresos)}

de ellos {fmtMoney(data.total_recurrente)} recurrentes

Ingresos por mes

{data.recurrentes_por_mes.length === 0 ? (

Sin cobros de membresía en el periodo.

) : (
{data.ingresos_por_mes.map((row) => { const rec = data.recurrentes_por_mes.find((r) => r.mes === row.mes)?.total ?? 0; return ( ); })}
Mes Ingreso recurrente Total cobrado
{row.mes} {fmtMoney(rec)} {fmtMoney(row.total)}
)}

Próximas renovaciones (60 días)

{data.proximas_renovaciones.length === 0 ? (

No hay renovaciones próximas.

) : (
    {data.proximas_renovaciones.map((r) => (
  • {r.patient}

    {r.folio} · {r.plan}

    {r.proximo_cobro}

    {fmtMoney(r.precio, r.moneda)}

  • ))}
)}

Sesiones

Consumidas en el periodo {totalConsumidas}
Incluidas en membresías activas {incluidas}
{incluidas > 0 && (

Uso estimado del {Math.min(100, Math.round((totalConsumidas / incluidas) * 100))}% de las sesiones incluidas.

)} Nota: comparación por periodo; estimación de uso.
); }; export default MembresiasReport;