Files
HoruxDespachos/apps/web/app/(dashboard)/clientes/page.tsx

696 lines
31 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 { useTenantViewStore } from '@/stores/tenant-view-store';
import { useAuthStore } from '@/stores/auth-store';
import { Building, Plus, Users, Eye, Calendar, Pencil, Trash2, X, DollarSign, AlertCircle, ChevronRight } from 'lucide-react';
import type { Tenant } from '@/lib/api/tenants';
import { isGlobalAdminRfc, DESPACHO_PLAN_PRICES, permiteFrecuenciaMensual } from '@horux/shared';
import { getClientesStats, getTenantUsuarios, type TenantUsuario } from '@/lib/api/admin-clientes';
const PLAN_LABELS: Record<string, string> = {
trial: 'Trial Gratuito',
mi_empresa: 'Mi Empresa',
mi_empresa_plus: 'Mi Empresa +',
business_control: 'Business Control',
business_cloud: 'Enterprise (Despacho)',
custom: 'Custom',
// Legacy labels kept for display
starter: 'Starter (legacy)',
business: 'Business (legacy)',
business_ia: 'Business + IA (legacy)',
enterprise: 'Enterprise (legacy)',
};
type PlanType = 'trial' | 'mi_empresa' | 'mi_empresa_plus' | 'business_control' | 'business_cloud' | 'custom' | 'starter' | 'business' | 'business_ia' | 'enterprise';
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,
});
// Map tenantId → activeUsers para lookup O(1) cuando renderizamos la lista.
const usuariosPorTenant = useMemo(() => {
const m = new Map<string, number>();
(stats?.usuariosPorCliente ?? []).forEach(u => m.set(u.tenantId, u.activeUsers));
return m;
}, [stats]);
const [usuariosTenantId, setUsuariosTenantId] = useState<string | null>(null);
const [usuariosTenantNombre, setUsuariosTenantNombre] = useState<string>('');
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<Tenant | null>(null);
const [formData, setFormData] = useState<{
nombre: string;
rfc: string;
plan: PlanType;
verticalProfile: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA';
frequency: 'monthly' | 'annual';
adminEmail: string;
adminNombre: string;
amount: number;
}>({
nombre: '',
rfc: '',
plan: 'trial',
verticalProfile: 'CONTABLE',
frequency: 'annual',
adminEmail: '',
adminNombre: '',
amount: 0,
});
// Only global admin can access this page
if (!isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles)) {
return (
<>
<Header title="Clientes" />
<main className="p-6">
<Card>
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">
No tienes permisos para ver esta página.
</p>
</CardContent>
</Card>
</main>
</>
);
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingTenant) {
await updateTenant.mutateAsync({
id: editingTenant.id,
data: {
nombre: formData.nombre,
rfc: formData.rfc,
plan: formData.plan,
verticalProfile: formData.verticalProfile,
},
});
setEditingTenant(null);
} else {
await createTenant.mutateAsync(formData);
}
setFormData({ nombre: '', rfc: '', plan: 'trial', verticalProfile: 'CONTABLE', frequency: 'annual', adminEmail: '', adminNombre: '', amount: 0 });
setShowForm(false);
} catch (error) {
console.error('Error:', error);
}
};
const handleEdit = (tenant: Tenant) => {
setEditingTenant(tenant);
setFormData({
nombre: tenant.nombre,
rfc: tenant.rfc,
plan: (tenant.plan as PlanType) || 'trial',
verticalProfile: 'CONTABLE',
frequency: 'annual',
adminEmail: '',
adminNombre: '',
amount: 0,
});
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', verticalProfile: 'CONTABLE', frequency: 'annual', adminEmail: '', adminNombre: '', amount: 0 });
};
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<string, string> = {
trial: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-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',
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',
};
return (
<>
<Header title="Gestión de Clientes" />
<main className="p-6 space-y-6">
{/* Selector de periodo + acción */}
<Card>
<CardContent className="pt-6 flex flex-col md:flex-row items-start md:items-center gap-4 justify-between">
<div className="flex items-center gap-3">
<Calendar className="h-5 w-5 text-muted-foreground" />
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Periodo:</span>
<Input type="date" value={from} onChange={(e) => setFrom(e.target.value)} className="w-[150px]" />
<span className="text-muted-foreground">a</span>
<Input type="date" value={to} onChange={(e) => setTo(e.target.value)} className="w-[150px]" />
</div>
</div>
<Button onClick={() => setShowForm(true)}>
<Plus className="h-4 w-4 mr-2" />
Agregar Cliente
</Button>
</CardContent>
</Card>
{/* KPIs */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* Total clientes activos */}
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg">
<Building className="h-6 w-6 text-primary" />
</div>
<div>
<p className="text-2xl font-bold">{tenants?.length || 0}</p>
<p className="text-sm text-muted-foreground">Clientes registrados</p>
</div>
</div>
</CardContent>
</Card>
{/* Suscripciones activas con breakdown por plan */}
<Card>
<CardContent className="pt-6 space-y-2">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-100 dark:bg-emerald-900/30 rounded-lg">
<Users className="h-5 w-5 text-emerald-600" />
</div>
<div>
<p className="text-2xl font-bold">
{(stats?.suscripcionesPorPlan ?? []).reduce((s, p) => s + p.count, 0)}
</p>
<p className="text-sm text-muted-foreground">Suscripciones activas</p>
</div>
</div>
{stats && stats.suscripcionesPorPlan.length > 0 && (
<div className="pt-2 space-y-1 text-xs">
{stats.suscripcionesPorPlan.map(p => (
<div key={p.plan} className="flex justify-between">
<span className="text-muted-foreground">{PLAN_LABELS[p.plan] ?? p.plan}</span>
<span className="font-medium">{p.count}</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Ingresos del periodo */}
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
<DollarSign className="h-6 w-6 text-amber-600" />
</div>
<div>
<p className="text-2xl font-bold">
${(stats?.ingresos.total ?? 0).toLocaleString('es-MX', { maximumFractionDigits: 0 })}
</p>
<p className="text-sm text-muted-foreground">
Ingresos del periodo · {stats?.ingresos.paymentsCount ?? 0} pagos
</p>
</div>
</div>
</CardContent>
</Card>
{/* No renovaciones */}
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-red-100 dark:bg-red-900/30 rounded-lg">
<AlertCircle className="h-6 w-6 text-red-600" />
</div>
<div>
<p className="text-2xl font-bold">{stats?.noRenovaciones.length ?? 0}</p>
<p className="text-sm text-muted-foreground">No renovaron en el periodo</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Detalle de no renovaciones */}
{stats && stats.noRenovaciones.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600" />
Clientes que no renovaron en el periodo
</CardTitle>
<CardDescription>
Suscripciones cuyo periodo terminó dentro del rango y quedaron en estado terminal
(cancelada, prueba expirada o pausada).
</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="py-2 pr-4">Cliente</th>
<th className="py-2 pr-4">RFC</th>
<th className="py-2 pr-4">Plan</th>
<th className="py-2 pr-4">Vence</th>
<th className="py-2 pr-4">Estado</th>
</tr>
</thead>
<tbody>
{stats.noRenovaciones.map(nr => (
<tr key={nr.tenantId} className="border-b last:border-0 hover:bg-muted/40">
<td className="py-2 pr-4 font-medium">{nr.tenantNombre}</td>
<td className="py-2 pr-4 font-mono text-xs">{nr.rfc}</td>
<td className="py-2 pr-4">{PLAN_LABELS[nr.plan] ?? nr.plan}</td>
<td className="py-2 pr-4 text-xs">
{nr.currentPeriodEnd ? new Date(nr.currentPeriodEnd).toLocaleDateString('es-MX') : '—'}
</td>
<td className="py-2 pr-4">
<span className="px-2 py-0.5 rounded-full text-xs bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100">
{nr.statusActual}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
{/* Modal de usuarios por cliente */}
{usuariosTenantId && (
<div
className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4"
onClick={() => setUsuariosTenantId(null)}
>
<div
className="bg-background rounded-lg max-w-2xl w-full max-h-[80vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6 border-b flex items-center justify-between">
<div>
<h3 className="font-semibold">Usuarios de {usuariosTenantNombre}</h3>
<p className="text-sm text-muted-foreground">
{usuariosDetalle?.length ?? 0} usuarios activos
</p>
</div>
<Button variant="ghost" size="icon" onClick={() => setUsuariosTenantId(null)}>
<X className="h-4 w-4" />
</Button>
</div>
<div className="p-6">
{isUsuariosLoading ? (
<p className="text-sm text-muted-foreground">Cargando...</p>
) : (
<div className="space-y-2">
{(usuariosDetalle ?? []).map(u => (
<div key={u.userId} className="flex items-center justify-between p-3 rounded border">
<div>
<p className="font-medium text-sm">{u.nombre}</p>
<p className="text-xs text-muted-foreground">{u.email}</p>
</div>
<div className="flex items-center gap-2">
{u.isOwner && (
<span className="px-2 py-0.5 rounded-full text-xs bg-primary/10 text-primary">Owner</span>
)}
<span className="px-2 py-0.5 rounded-full text-xs bg-muted">{u.rol}</span>
</div>
</div>
))}
{(usuariosDetalle ?? []).length === 0 && (
<p className="text-sm text-muted-foreground text-center py-6">Sin usuarios activos</p>
)}
</div>
)}
</div>
</div>
</div>
)}
{/* Add/Edit Client Form */}
{showForm && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">
{editingTenant ? 'Editar Despacho' : 'Nuevo Despacho'}
</CardTitle>
<CardDescription>
{editingTenant
? 'Modifica los datos del despacho'
: 'Registra un nuevo despacho en Horux'}
</CardDescription>
</div>
<Button variant="ghost" size="icon" onClick={handleCancelForm}>
<X className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="nombre">Nombre del Despacho</Label>
<Input
id="nombre"
value={formData.nombre}
onChange={(e) => setFormData({ ...formData, nombre: e.target.value })}
placeholder="Despacho Pérez y Asociados"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="rfc">RFC del Despacho</Label>
<Input
id="rfc"
value={formData.rfc}
onChange={(e) => setFormData({ ...formData, rfc: e.target.value.toUpperCase() })}
placeholder="ABC010101ABC"
maxLength={13}
required
disabled={!!editingTenant}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="verticalProfile">Perfil Profesional</Label>
<Select
value={formData.verticalProfile}
onValueChange={(value) =>
setFormData({ ...formData, verticalProfile: value as 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA' })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="CONTABLE">📊 Contable Fiscal, CFDI, IVA/ISR</SelectItem>
<SelectItem value="JURIDICO"> Jurídico Próximamente</SelectItem>
<SelectItem value="ARQUITECTURA">🏗 Arquitectura Próximamente</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="plan">Plan</Label>
<Select
value={formData.plan}
onValueChange={(value) => {
const plan = value as PlanType;
const isCustom = plan === 'custom';
const isTrial = plan === 'trial';
let amount = 0;
if (!isCustom && !isTrial) {
const priceInfo = (DESPACHO_PLAN_PRICES as Record<string, { monthly: number | null; firstYear: number; renewal: number }>)[plan];
amount = priceInfo?.firstYear ?? 0;
}
setFormData({ ...formData, plan, amount });
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="trial">Trial Gratuito 30 días, 3 RFCs, 20 timbres</SelectItem>
<SelectItem value="mi_empresa">Mi Empresa 1 RFC, 3 usuarios, 50 timbres/mes</SelectItem>
<SelectItem value="mi_empresa_plus">Mi Empresa + Con API y Lolita IA</SelectItem>
<SelectItem value="business_control">Business Control 100 RFCs, BYO server</SelectItem>
<SelectItem value="business_cloud">Enterprise 100 RFCs, 3M CFDIs, BYO</SelectItem>
<SelectItem value="custom">Custom Sin cobro, asignado por admin</SelectItem>
<hr className="my-1" />
<SelectItem value="starter">Starter (legacy) Sin CFDIs, 1 usuario</SelectItem>
<SelectItem value="business">Business (legacy) 50 CFDIs, 3 usuarios</SelectItem>
<SelectItem value="business_ia">Business + IA (legacy)</SelectItem>
<SelectItem value="enterprise">Enterprise (legacy) 100 CFDIs, ilimitados</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Frequency selector for plans that allow monthly */}
{formData.plan !== 'custom' && formData.plan !== 'trial' && permiteFrecuenciaMensual(formData.plan) && (
<div className="space-y-2">
<Label htmlFor="frequency">Frecuencia de Pago</Label>
<Select
value={formData.frequency}
onValueChange={(value) => {
const freq = value as 'monthly' | 'annual';
const priceInfo = (DESPACHO_PLAN_PRICES as Record<string, { monthly: number | null; firstYear: number; renewal: number }>)[formData.plan];
const amount = freq === 'monthly' ? (priceInfo?.monthly ?? 0) : (priceInfo?.firstYear ?? 0);
setFormData({ ...formData, frequency: freq, amount });
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Mensual</SelectItem>
<SelectItem value="annual">Anual (ahorra ~17%)</SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* Campos de admin y suscripción — solo al crear */}
{!editingTenant && (
<>
<div className="border-t pt-4">
<p className="text-sm font-medium text-muted-foreground mb-3">Dueño del Despacho</p>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="adminNombre">Nombre del Dueño</Label>
<Input
id="adminNombre"
value={formData.adminNombre}
onChange={(e) => setFormData({ ...formData, adminNombre: e.target.value })}
placeholder="Juan Pérez"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="adminEmail">Email del Dueño</Label>
<Input
id="adminEmail"
type="email"
value={formData.adminEmail}
onChange={(e) => setFormData({ ...formData, adminEmail: e.target.value })}
placeholder="admin@despacho.com"
required
/>
</div>
</div>
</div>
{formData.plan !== 'custom' && formData.plan !== 'trial' && (
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="amount">Monto (MXN)</Label>
<Input
id="amount"
type="number"
min="0"
step="0.01"
value={formData.amount || ''}
onChange={(e) => setFormData({ ...formData, amount: parseFloat(e.target.value) || 0 })}
placeholder="0.00"
/>
<p className="text-xs text-muted-foreground">
Precio sugerido según catálogo. Puedes ajustarlo para descuentos especiales.
</p>
</div>
</div>
)}
{(formData.plan === 'custom' || formData.plan === 'trial') && (
<p className="text-xs text-muted-foreground">
{formData.plan === 'custom'
? 'Plan Custom no genera cobro ni suscripción. Vigencia indefinida.'
: 'Trial gratuito por 30 días. No requiere tarjeta.'}
</p>
)}
</>
)}
<div className="flex gap-2 justify-end">
<Button type="button" variant="outline" onClick={handleCancelForm}>
Cancelar
</Button>
<Button type="submit" disabled={createTenant.isPending || updateTenant.isPending}>
{editingTenant
? (updateTenant.isPending ? 'Guardando...' : 'Guardar Cambios')
: (createTenant.isPending ? 'Creando...' : 'Crear Cliente')}
</Button>
</div>
</form>
</CardContent>
</Card>
)}
{/* Clients List */}
<Card>
<CardHeader>
<CardTitle className="text-base">Lista de Clientes</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
) : tenants && tenants.length > 0 ? (
<div className="space-y-3">
{tenants.map((tenant) => (
<div
key={tenant.id}
className="flex items-center justify-between p-4 rounded-lg border hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center">
<span className="font-bold text-primary">
{tenant.nombre.substring(0, 2).toUpperCase()}
</span>
</div>
<div>
<p className="font-medium">{tenant.nombre}</p>
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<span>{tenant.rfc}</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${planColors[tenant.plan] ?? 'bg-muted text-muted-foreground'}`}>
{PLAN_LABELS[tenant.plan] ?? tenant.plan}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<button
type="button"
className="flex items-center gap-1 text-sm hover:text-primary transition-colors"
onClick={() => { setUsuariosTenantId(tenant.id); setUsuariosTenantNombre(tenant.nombre); }}
title="Ver usuarios"
>
<Users className="h-4 w-4 text-muted-foreground" />
<span>
{usuariosPorTenant.get(tenant.id) ?? tenant._count?.memberships ?? 0} usuarios
</span>
<ChevronRight className="h-3 w-3" />
</button>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="h-3 w-3" />
<span>{formatDate(tenant.createdAt)}</span>
</div>
</div>
<div className="flex gap-1">
<Button
variant="outline"
size="sm"
onClick={() => handleViewClient(tenant.id, tenant.nombre)}
>
<Eye className="h-4 w-4 mr-1" />
Ver
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleEdit(tenant)}
title="Editar"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(tenant)}
title="Eliminar"
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No hay clientes registrados
</div>
)}
</CardContent>
</Card>
</main>
</>
);
}