Initial commit - Horux Despachos NL
This commit is contained in:
146
apps/web/app/(dashboard)/configuracion/suscripcion/page.tsx
Normal file
146
apps/web/app/(dashboard)/configuracion/suscripcion/page.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user