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