'use client'; import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Header } from '@/components/layouts/header'; import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@horux/shared-ui'; import { useTenants, useCreateTenant, useUpdateTenant, useDeleteTenant } from '@/lib/hooks/use-tenants'; import { usePagosSinFactura } from '@/lib/hooks/use-pagos-sin-factura'; import { useTenantViewStore } from '@/stores/tenant-view-store'; import { useAuthStore } from '@/stores/auth-store'; import { Building, Plus, Users, Eye, Calendar, Pencil, Trash2, X, DollarSign, AlertCircle, ChevronRight, Receipt } from 'lucide-react'; import type { Tenant } from '@/lib/api/tenants'; import { isGlobalAdminRfc } from '@horux/shared'; import { getClientesStats, getTenantUsuarios, type TenantUsuario } from '@/lib/api/admin-clientes'; const PLAN_LABELS: Record = { trial: 'Prueba', custom: 'Custom', mi_empresa: 'Mi Empresa', mi_empresa_plus: 'Mi Empresa +', business_control: 'Business Control', business_cloud: 'Enterprise', }; type PlanType = 'trial' | 'custom' | 'mi_empresa' | 'mi_empresa_plus' | 'business_control' | 'business_cloud'; export default function ClientesPage() { const { user } = useAuthStore(); const { data: tenants, isLoading } = useTenants(); const createTenant = useCreateTenant(); const updateTenant = useUpdateTenant(); const deleteTenant = useDeleteTenant(); const { setViewingTenant } = useTenantViewStore(); const router = useRouter(); const queryClient = useQueryClient(); // Periodo del KPI: default mes en curso. El admin puede ajustar para ver // ingresos / no-renovaciones de otros rangos. const today = new Date(); const defaultFrom = useMemo(() => `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-01`, []); const defaultTo = useMemo(() => { const last = new Date(today.getFullYear(), today.getMonth() + 1, 0); return `${last.getFullYear()}-${String(last.getMonth() + 1).padStart(2, '0')}-${String(last.getDate()).padStart(2, '0')}`; }, []); const [from, setFrom] = useState(defaultFrom); const [to, setTo] = useState(defaultTo); const isGlobal = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles); // `enabled: !!user` en lugar de `isGlobal` — evita timing donde // platformRoles no haya llegado aún del store. El backend gatea por // requireStaff así que un user sin permiso recibe 403 y react-query // maneja el error sin crash. Lo importante es que el query CORRA al // montar la página. const { data: stats } = useQuery({ queryKey: ['admin-clientes-stats', from, to], queryFn: () => getClientesStats(from, to), enabled: !!user, }); const { data: pagosSinFactura } = usePagosSinFactura(); // Map tenantId → activeUsers para lookup O(1) cuando renderizamos la lista. const usuariosPorTenant = useMemo(() => { const m = new Map(); (stats?.usuariosPorCliente ?? []).forEach(u => m.set(u.tenantId, u.activeUsers)); return m; }, [stats]); const [usuariosTenantId, setUsuariosTenantId] = useState(null); const [usuariosTenantNombre, setUsuariosTenantNombre] = useState(''); const { data: usuariosDetalle, isLoading: isUsuariosLoading } = useQuery({ queryKey: ['admin-clientes-usuarios', usuariosTenantId], queryFn: () => usuariosTenantId ? getTenantUsuarios(usuariosTenantId) : Promise.resolve([] as TenantUsuario[]), enabled: !!usuariosTenantId, }); const [showForm, setShowForm] = useState(false); const [editingTenant, setEditingTenant] = useState(null); const [formData, setFormData] = useState<{ nombre: string; rfc: string; plan: PlanType; adminEmail: string; adminNombre: string; amount: number; firstPaymentDueAt: string; }>({ nombre: '', rfc: '', plan: 'trial', adminEmail: '', adminNombre: '', amount: 0, firstPaymentDueAt: '', }); // Only global admin can access this page if (!isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles)) { return ( <>

No tienes permisos para ver esta página.

); } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { if (editingTenant) { await updateTenant.mutateAsync({ id: editingTenant.id, data: formData }); setEditingTenant(null); } else { await createTenant.mutateAsync(formData); } setFormData({ nombre: '', rfc: '', plan: 'trial', adminEmail: '', adminNombre: '', amount: 0, firstPaymentDueAt: '' }); setShowForm(false); } catch (error) { console.error('Error:', error); } }; const handleEdit = (tenant: Tenant) => { setEditingTenant(tenant); const sub = tenant.subscriptions?.[0]; setFormData({ nombre: tenant.nombre, rfc: tenant.rfc, plan: tenant.plan as PlanType, adminEmail: '', adminNombre: '', amount: sub ? Number(sub.amount) : 0, firstPaymentDueAt: sub?.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toISOString().slice(0, 10) : '', }); setShowForm(true); }; const handleDelete = async (tenant: Tenant) => { if (confirm(`¿Eliminar el cliente "${tenant.nombre}"? Esta acción desactivará el cliente.`)) { try { await deleteTenant.mutateAsync(tenant.id); } catch (error) { console.error('Error deleting tenant:', error); } } }; const handleCancelForm = () => { setShowForm(false); setEditingTenant(null); setFormData({ nombre: '', rfc: '', plan: 'trial', adminEmail: '', adminNombre: '', amount: 0, firstPaymentDueAt: '' }); }; const handleViewClient = (tenantId: string, tenantName: string) => { setViewingTenant(tenantId, tenantName); queryClient.invalidateQueries(); router.push('/dashboard'); }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('es-MX', { year: 'numeric', month: 'short', day: 'numeric', }); }; // Reuse PLAN_LABELS global (declarado al top del archivo) que cubre todos // los planes — legacy + despacho + custom. El planColors local se mantiene // chico con un fallback genérico para planes nuevos. const planColors: Record = { starter: 'bg-muted text-muted-foreground', business: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100', business_ia: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100', enterprise: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100', mi_empresa: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100', mi_empresa_plus: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100', business_control: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100', business_cloud: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100', custom: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-100', }; return ( <>
{/* Selector de periodo + acción */}
Periodo: setFrom(e.target.value)} className="w-[150px]" /> a setTo(e.target.value)} className="w-[150px]" />
{/* KPIs */}
{/* Total clientes activos */}

{tenants?.length || 0}

Clientes registrados

{/* Suscripciones activas con breakdown por plan */}

{(stats?.suscripcionesPorPlan ?? []).reduce((s, p) => s + p.count, 0)}

Suscripciones activas

{stats && stats.suscripcionesPorPlan.length > 0 && (
{stats.suscripcionesPorPlan.map(p => (
{PLAN_LABELS[p.plan] ?? p.plan} {p.count}
))}
)}
{/* Ingresos del periodo */}

${(stats?.ingresos.total ?? 0).toLocaleString('es-MX', { maximumFractionDigits: 0 })}

Ingresos del periodo · {stats?.ingresos.paymentsCount ?? 0} pagos

{/* No renovaciones */}

{stats?.noRenovaciones.length ?? 0}

No renovaron en el periodo

{/* Facturas pendientes */} router.push('/admin/facturas-pendientes')} >

{pagosSinFactura?.length ?? 0}

Facturas pendientes

{/* Detalle de no renovaciones */} {stats && stats.noRenovaciones.length > 0 && ( Clientes que no renovaron en el periodo Suscripciones cuyo periodo terminó dentro del rango y quedaron en estado terminal (cancelada, prueba expirada o pausada).
{stats.noRenovaciones.map(nr => ( ))}
Cliente RFC Plan Vence Estado
{nr.tenantNombre} {nr.rfc} {PLAN_LABELS[nr.plan] ?? nr.plan} {nr.currentPeriodEnd ? new Date(nr.currentPeriodEnd).toLocaleDateString('es-MX') : '—'} {nr.statusActual}
)} {/* Modal de usuarios por cliente */} {usuariosTenantId && (
setUsuariosTenantId(null)} >
e.stopPropagation()} >

Usuarios de {usuariosTenantNombre}

{usuariosDetalle?.length ?? 0} usuarios activos

{isUsuariosLoading ? (

Cargando...

) : (
{(usuariosDetalle ?? []).map(u => (

{u.nombre}

{u.email}

{u.isOwner && ( Owner )} {u.rol}
))} {(usuariosDetalle ?? []).length === 0 && (

Sin usuarios activos

)}
)}
)} {/* Add/Edit Client Form */} {showForm && (
{editingTenant ? 'Editar Cliente' : 'Nuevo Cliente'} {editingTenant ? 'Modifica los datos del cliente' : 'Registra un nuevo cliente para gestionar su facturación'}
setFormData({ ...formData, nombre: e.target.value })} placeholder="Empresa SA de CV" required />
setFormData({ ...formData, rfc: e.target.value.toUpperCase() })} placeholder="XAXX010101000" maxLength={14} required disabled={!!editingTenant} // Can't change RFC after creation />
{/* Campos de admin — solo al crear */} {!editingTenant && (

Dueño del Cliente

setFormData({ ...formData, adminNombre: e.target.value })} placeholder="Juan Pérez" required />
setFormData({ ...formData, adminEmail: e.target.value })} placeholder="admin@empresa.com" required />
)} {/* Campos de suscripción — crear y editar (solo planes pagados / custom) */} {formData.plan !== 'trial' && (
setFormData({ ...formData, amount: parseFloat(e.target.value) || 0 })} placeholder="0.00" /> {formData.plan === 'custom' && (

Custom: monto variable. Si dejas $0, no se cobra ni se solicita tarjeta. Si pones >$0, se generará Subscription con preapproval MercadoPago mensual.

)}
{formData.plan === 'custom' && (
setFormData({ ...formData, firstPaymentDueAt: e.target.value })} />

Deadline visible al cliente para realizar su primer pago. Opcional.

)}
)} {formData.plan === 'trial' && (

Plan de prueba: 30 días gratis sin tarjeta. Se convierte a un plan pagado al final del periodo.

)}
)} {/* Clients List */} Lista de Clientes {isLoading ? (
Cargando...
) : tenants && tenants.length > 0 ? (
{tenants.map((tenant) => (
{tenant.nombre.substring(0, 2).toUpperCase()}

{tenant.nombre}

{tenant.rfc} {PLAN_LABELS[tenant.plan] ?? tenant.plan}
{formatDate(tenant.createdAt)}
))}
) : (
No hay clientes registrados
)}
); }