220 lines
9.8 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|