147 lines
8.6 KiB
TypeScript
147 lines
8.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Header } from '@/components/layouts/header';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@horux/shared-ui';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { isGlobalAdminRfc } from '@horux/shared';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { apiClient } from '@/lib/api/client';
|
|
import {
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Clock,
|
|
XCircle,
|
|
Building,
|
|
Sparkles,
|
|
} from 'lucide-react';
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
const statusConfig: Record<string, { label: string; color: string; bgColor: string; icon: typeof CheckCircle }> = {
|
|
authorized: { label: 'Activa', color: 'text-green-700', bgColor: 'bg-green-50 border-green-200', icon: CheckCircle },
|
|
trial: { label: 'Prueba gratis', color: 'text-blue-700', bgColor: 'bg-blue-50 border-blue-200', icon: Sparkles },
|
|
trial_expired: { label: 'Prueba vencida', color: 'text-red-700', bgColor: 'bg-red-50 border-red-200', icon: XCircle },
|
|
trial_converted: { label: 'Prueba convertida', color: 'text-green-700', bgColor: 'bg-green-50 border-green-200', icon: CheckCircle },
|
|
pending: { label: 'Pendiente de pago', color: 'text-yellow-700', bgColor: 'bg-yellow-50 border-yellow-200', icon: Clock },
|
|
paused: { label: 'Pausada', color: 'text-orange-700', bgColor: 'bg-orange-50 border-orange-200', icon: AlertCircle },
|
|
cancelled: { label: 'Cancelada', color: 'text-red-700', bgColor: 'bg-red-50 border-red-200', icon: XCircle },
|
|
};
|
|
|
|
// ============================================================================
|
|
// Admin global: vista de todas las suscripciones
|
|
// ============================================================================
|
|
// Edición de precios de planes movida a /configuracion/precios-suscripcion.
|
|
// Vista self-serve para clientes movida a /configuracion/planes-despacho.
|
|
|
|
function AdminGlobalSubscriptions() {
|
|
const { data: subscriptions, isLoading } = useQuery({
|
|
queryKey: ['all-subscriptions'],
|
|
queryFn: () => apiClient.get('/subscriptions').then(r => r.data),
|
|
});
|
|
|
|
if (isLoading) return <div className="text-center py-8 text-muted-foreground">Cargando...</div>;
|
|
|
|
const subs = (subscriptions || []) as any[];
|
|
const activas = subs.filter((s: any) => s.status === 'authorized' || s.status === 'active');
|
|
const pendientes = subs.filter((s: any) => s.status === 'pending');
|
|
const canceladas = subs.filter((s: any) => s.status === 'cancelled' || s.status === 'paused');
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="grid gap-4 md:grid-cols-4">
|
|
<Card><CardContent className="pt-6"><div className="flex items-center gap-3"><div className="p-2 bg-primary/10 rounded-lg"><Building className="h-5 w-5 text-primary" /></div><div><p className="text-2xl font-bold">{subs.length}</p><p className="text-xs text-muted-foreground">Total</p></div></div></CardContent></Card>
|
|
<Card><CardContent className="pt-6"><div className="flex items-center gap-3"><div className="p-2 bg-green-100 rounded-lg"><CheckCircle className="h-5 w-5 text-green-600" /></div><div><p className="text-2xl font-bold">{activas.length}</p><p className="text-xs text-muted-foreground">Activas</p></div></div></CardContent></Card>
|
|
<Card><CardContent className="pt-6"><div className="flex items-center gap-3"><div className="p-2 bg-yellow-100 rounded-lg"><Clock className="h-5 w-5 text-yellow-600" /></div><div><p className="text-2xl font-bold">{pendientes.length}</p><p className="text-xs text-muted-foreground">Pendientes</p></div></div></CardContent></Card>
|
|
<Card><CardContent className="pt-6"><div className="flex items-center gap-3"><div className="p-2 bg-red-100 rounded-lg"><XCircle className="h-5 w-5 text-red-600" /></div><div><p className="text-2xl font-bold">{canceladas.length}</p><p className="text-xs text-muted-foreground">Canceladas</p></div></div></CardContent></Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader><CardTitle className="text-base">Todas las Suscripciones</CardTitle></CardHeader>
|
|
<CardContent>
|
|
{subs.length === 0 ? (
|
|
<p className="text-center py-8 text-muted-foreground">No hay suscripciones</p>
|
|
) : (
|
|
<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">Estado</th><th className="py-2 pr-4 text-right">Monto</th><th className="py-2 pr-4">Frecuencia</th><th className="py-2 pr-4">Siguiente pago</th><th className="py-2">Creada</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{subs.map((s: any) => {
|
|
const st = statusConfig[s.status] || statusConfig.pending;
|
|
const StIcon = st.icon;
|
|
return (
|
|
<tr key={s.id} className="border-b last:border-b-0 hover:bg-muted/50">
|
|
<td className="py-3 pr-4 font-medium">{s.tenant?.nombre || '—'}</td>
|
|
<td className="py-3 pr-4 font-mono text-xs">{s.tenant?.rfc || '—'}</td>
|
|
<td className="py-3 pr-4"><span className="px-2 py-0.5 rounded text-xs font-medium bg-muted capitalize">{s.plan}</span></td>
|
|
<td className="py-3 pr-4"><span className={`inline-flex items-center gap-1 text-xs font-medium ${st.color}`}><StIcon className="h-3 w-3" />{st.label}</span></td>
|
|
<td className="py-3 pr-4 text-right font-medium">${Number(s.amount).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td>
|
|
<td className="py-3 pr-4 text-muted-foreground capitalize">{s.frequency}</td>
|
|
<td className="py-3 pr-4">{s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString('es-MX') : '—'}</td>
|
|
<td className="py-3 text-muted-foreground">{new Date(s.createdAt).toLocaleDateString('es-MX')}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Frequency Toggle
|
|
// ============================================================================
|
|
|
|
function FrequencyToggle({ value, onChange }: { value: 'monthly' | 'annual'; onChange: (v: 'monthly' | 'annual') => void }) {
|
|
return (
|
|
<div className="inline-flex items-center rounded-lg border bg-card p-1 text-sm">
|
|
<button type="button" onClick={() => onChange('monthly')} className={`px-4 py-1.5 rounded-md font-medium transition-colors ${value === 'monthly' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}`}>Mensual</button>
|
|
<button type="button" onClick={() => onChange('annual')} className={`px-4 py-1.5 rounded-md font-medium transition-colors ${value === 'annual' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}`}>Anual <span className="ml-1 text-xs opacity-75">(ahorra 17%)</span></button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Page
|
|
// ============================================================================
|
|
|
|
export default function SuscripcionPage() {
|
|
const { user } = useAuthStore();
|
|
const router = useRouter();
|
|
const isAdmin = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles);
|
|
|
|
// Para clientes la gestión de suscripción vive en /configuracion/planes-despacho.
|
|
// Esta página queda como panel agregado del admin global (ver TODAS las suscripciones).
|
|
// Si por algún link viejo cae un cliente regular, lo enviamos a Planes.
|
|
useEffect(() => {
|
|
if (!isAdmin) router.replace('/configuracion/planes-despacho');
|
|
}, [isAdmin, router]);
|
|
|
|
if (!isAdmin) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center text-muted-foreground">
|
|
Redirigiendo a Planes…
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Admin global: vista agregada de todas las suscripciones de la plataforma.
|
|
return (
|
|
<>
|
|
<Header title="Suscripciones" />
|
|
<main className="p-6"><AdminGlobalSubscriptions /></main>
|
|
</>
|
|
);
|
|
}
|