Files
HoruxDespachos/apps/web/app/(dashboard)/facturacion/timbres/page.tsx
2026-04-27 22:09:36 -06:00

220 lines
9.8 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';
import { Header } from '@/components/layouts/header';
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button } from '@horux/shared-ui';
import { useTimbres } from '@/lib/hooks/use-facturacion';
import { getPaquetesCatalogo, comprarPaquete } from '@/lib/api/facturacion';
import { formatCurrency } from '@/lib/utils';
import { Receipt, Zap, Package, ArrowLeft, ShoppingCart, Loader2, CheckCircle2, AlertTriangle, Calendar } from 'lucide-react';
import { useAuthStore } from '@/stores/auth-store';
export default function TimbresPage() {
const router = useRouter();
const { user } = useAuthStore();
const { data: timbres, isLoading } = useTimbres();
const { data: catalogo = [] } = useQuery({
queryKey: ['timbres-paquetes-catalogo'],
queryFn: getPaquetesCatalogo,
});
const [buying, setBuying] = useState<number | null>(null);
const canBuy = user?.role === 'owner' || user?.role === 'cfo';
const handleComprar = async (catalogoId: number) => {
if (!canBuy) return;
setBuying(catalogoId);
try {
const { checkoutUrl } = await comprarPaquete(catalogoId);
window.location.href = checkoutUrl;
} catch (err: any) {
alert(err?.response?.data?.message || 'Error al iniciar compra');
setBuying(null);
}
};
const mensualDisp = timbres?.mensual?.disponibles ?? (timbres?.disponibles ?? 0);
const mensualTotal = timbres?.mensual?.limite ?? (timbres?.limite ?? 0);
const adicionales = timbres?.adicionales;
return (
<>
<Header title="Timbres adicionales">
<button onClick={() => router.push('/facturacion')} className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
<ArrowLeft className="h-4 w-4" /> Volver a facturación
</button>
</Header>
<main className="p-6 space-y-6">
{/* Status actual */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<Zap className="h-4 w-4 text-amber-500" /> Plan mensual
</CardTitle>
<CardDescription className="text-xs">Se resetea cada mes. No acumulable.</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{mensualDisp}</div>
<p className="text-xs text-muted-foreground">de {mensualTotal} disponibles</p>
{timbres?.mensual?.periodoFin && (
<p className="text-xs text-muted-foreground mt-1">
Renueva: {new Date(timbres.mensual.periodoFin).toLocaleDateString('es-MX')}
</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<Package className="h-4 w-4 text-blue-500" /> Adicionales
</CardTitle>
<CardDescription className="text-xs">Comprados. Vigencia 1 año c/u.</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{adicionales?.disponibles ?? 0}</div>
<p className="text-xs text-muted-foreground">
de {adicionales?.total ?? 0} ({adicionales?.usados ?? 0} usados)
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<Receipt className="h-4 w-4 text-green-600" /> Total disponible
</CardTitle>
<CardDescription className="text-xs">Suma mensual + adicionales.</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{timbres?.totalDisponibles ?? mensualDisp}</div>
<p className="text-xs text-muted-foreground">timbres listos para emitir</p>
</CardContent>
</Card>
</div>
{/* Explicación de orden de consumo */}
<Card className="bg-muted/30 border-dashed">
<CardContent className="py-4 text-sm flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" />
<div>
<strong>Orden de consumo:</strong> cada factura emitida descuenta primero
de tus timbres mensuales del plan. Solo cuando estos se agoten empieza a
consumir de tus paquetes adicionales, comenzando por los más próximos a
vencer para no desperdiciarlos.
</div>
</CardContent>
</Card>
{/* Catálogo de paquetes */}
<div>
<h2 className="text-lg font-semibold mb-3">Comprar paquetes</h2>
{!canBuy && (
<div className="text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded px-3 py-2 mb-3">
Solo el dueño o CFO de la empresa pueden comprar paquetes adicionales.
</div>
)}
<div className="grid gap-4 md:grid-cols-3">
{catalogo.map(p => (
<Card key={p.id} className="flex flex-col">
<CardHeader>
<CardTitle className="text-2xl">
{p.cantidad.toLocaleString('es-MX')}
<span className="text-sm font-normal text-muted-foreground ml-1">timbres</span>
</CardTitle>
<CardDescription>Vigencia 1 año desde la compra</CardDescription>
</CardHeader>
<CardContent className="flex-1 flex flex-col justify-between">
<div>
<div className="text-3xl font-bold">{formatCurrency(p.precio)}</div>
<p className="text-xs text-muted-foreground">
{formatCurrency(p.precio / p.cantidad)} por timbre · IVA incluido
</p>
</div>
<Button
className="mt-4 w-full"
onClick={() => handleComprar(p.id)}
disabled={!canBuy || buying !== null}
>
{buying === p.id ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Redirigiendo...</>
) : (
<><ShoppingCart className="h-4 w-4 mr-2" /> Comprar</>
)}
</Button>
</CardContent>
</Card>
))}
</div>
<p className="text-xs text-muted-foreground mt-3">
Al completar el pago en MercadoPago, tu factura se emitirá automáticamente.
</p>
</div>
{/* Paquetes vigentes */}
{adicionales && adicionales.paquetes.length > 0 && (
<div>
<h2 className="text-lg font-semibold mb-3">Paquetes vigentes</h2>
<Card>
<CardContent className="pt-6">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 font-medium">Cantidad</th>
<th className="pb-2 font-medium">Usados</th>
<th className="pb-2 font-medium">Disponibles</th>
<th className="pb-2 font-medium">Adquirido</th>
<th className="pb-2 font-medium">Expira</th>
</tr>
</thead>
<tbody>
{adicionales.paquetes.map(p => {
const expira = new Date(p.expiraEn);
const diasRestantes = Math.floor((expira.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return (
<tr key={p.id} className="border-b last:border-0">
<td className="py-2">{p.cantidad.toLocaleString('es-MX')}</td>
<td className="py-2">{p.usados.toLocaleString('es-MX')}</td>
<td className="py-2 font-medium">{p.disponibles.toLocaleString('es-MX')}</td>
<td className="py-2 text-xs text-muted-foreground">
{new Date(p.adquiridoEn).toLocaleDateString('es-MX')}
</td>
<td className="py-2 text-xs">
<span className={diasRestantes < 30 ? 'text-amber-600 font-medium' : 'text-muted-foreground'}>
<Calendar className="h-3 w-3 inline mr-1" />
{expira.toLocaleDateString('es-MX')} ({diasRestantes} días)
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
)}
{/* Success banner post-MP */}
{typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('timbres') === 'success' && (
<Card className="border-green-200 bg-green-50">
<CardContent className="py-4 flex items-start gap-2 text-sm text-green-800">
<CheckCircle2 className="h-5 w-5 flex-shrink-0" />
<div>
<strong>Pago recibido.</strong> Tu paquete se activará en cuanto MercadoPago confirme la transacción (~1-2 minutos). Recarga la página si no lo ves enseguida.
</div>
</CardContent>
</Card>
)}
</main>
</>
);
}