Files
HoruxDespachosNuevo/apps/web/app/(dashboard)/admin/facturas-pendientes/page.tsx
Horux Dev 9f11a0ba39 feat: facturación primer pago, fixes SAT/MP, autocompletado RFCs/conceptos
Backend:
- Notificación email al admin cuando llega primer pago aprobado (sin factura auto)
- Endpoints GET /pagos-sin-factura y POST /emitir-factura-pago para admin global
- Fix vinculación org Facturapi Horux 360 (69f23a5a242e0af47a41fa0d)
- Fix webhook MP: validación defensiva de x-signature header
- Fix autocompleto RFCs: eliminado filtro por contribuyenteId
- Fix autocompleto conceptos: eliminado filtro por contribuyenteId
- SAT fixes: anti-bot CSF scraper, request reuse, date range fix, stale job thresholds
- SAT sync request reuse across jobs para evitar agotar cuota diaria
- Typo fix MP_ACCESS_TOKEN en .env
- Trial invitations system backend

Frontend:
- Nueva página /admin/facturas-pendientes con tabla y emisión manual
- Métrica 'Facturas pendientes' en /clientes (clickable)
- Navegación onboarding FIEL/CSD corregida
- Sidebar themes sincronizados
- Fix SAT portal migration scraper (NetIQ)
- Trial invitation acceptance pages
2026-05-09 21:56:42 +00:00

170 lines
6.9 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Header } from '@/components/layouts/header';
import { Card, CardContent, CardHeader, CardTitle, Button } from '@horux/shared-ui';
import { useAuthStore } from '@/stores/auth-store';
import { isGlobalAdminRfc } from '@horux/shared';
import { usePagosSinFactura, useEmitirFacturaPago } from '@/lib/hooks/use-pagos-sin-factura';
import { ShieldAlert, FileText, RefreshCw, CheckCircle, AlertCircle, Receipt } from 'lucide-react';
const PLAN_LABELS: Record<string, string> = {
trial: 'Trial',
custom: 'Custom',
mi_empresa: 'Mi Empresa',
mi_empresa_plus: 'Mi Empresa Plus',
business_control: 'Business Control',
business_cloud: 'Enterprise',
};
const METHOD_LABELS: Record<string, string> = {
master: 'Mastercard',
visa: 'Visa',
amex: 'Amex',
debmaster: 'Débito Mastercard',
debvisa: 'Débito Visa',
account_money: 'MercadoPago',
bank_transfer: 'Transferencia',
};
function formatCurrency(amount: string | number): string {
const num = typeof amount === 'string' ? parseFloat(amount) : amount;
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(num);
}
function formatDate(iso: string | null): string {
if (!iso) return '—';
return new Date(iso).toLocaleString('es-MX', { dateStyle: 'short', timeStyle: 'short' });
}
export default function FacturasPendientesPage() {
const { user } = useAuthStore();
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles);
const { data: payments, isLoading, error } = usePagosSinFactura();
const emitir = useEmitirFacturaPago();
const [emitiendoId, setEmitiendoId] = useState<string | null>(null);
if (!isGlobalAdmin) {
return (
<>
<Header title="Facturas Pendientes" />
<main className="p-6">
<Card>
<CardContent className="py-12 text-center">
<ShieldAlert className="h-12 w-12 text-muted-foreground/40 mx-auto mb-3" />
<p className="font-semibold">Acceso restringido</p>
<p className="text-sm text-muted-foreground mt-1">
Solo el administrador global puede consultar pagos sin factura.
</p>
</CardContent>
</Card>
</main>
</>
);
}
async function handleEmitir(paymentId: string) {
setEmitiendoId(paymentId);
try {
await emitir.mutateAsync(paymentId);
} finally {
setEmitiendoId(null);
}
}
return (
<>
<Header title="Facturas Pendientes" />
<main className="p-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Receipt className="h-5 w-5" />
Pagos de suscripción sin factura
</CardTitle>
</CardHeader>
<CardContent>
{isLoading && (
<div className="flex items-center justify-center py-12 text-muted-foreground">
<RefreshCw className="h-5 w-5 animate-spin mr-2" />
Cargando...
</div>
)}
{error && (
<div className="flex items-center justify-center py-12 text-red-600">
<AlertCircle className="h-5 w-5 mr-2" />
Error al cargar los pagos
</div>
)}
{!isLoading && !error && payments && payments.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<CheckCircle className="h-10 w-10 mb-3 text-green-500" />
<p className="font-medium">No hay pagos pendientes de facturar</p>
<p className="text-sm mt-1">Todos los pagos aprobados ya tienen su factura emitida.</p>
</div>
)}
{!isLoading && !error && payments && payments.length > 0 && (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-3 px-2 font-medium text-muted-foreground">Cliente</th>
<th className="text-left py-3 px-2 font-medium text-muted-foreground">Plan</th>
<th className="text-left py-3 px-2 font-medium text-muted-foreground">Monto</th>
<th className="text-left py-3 px-2 font-medium text-muted-foreground">Método</th>
<th className="text-left py-3 px-2 font-medium text-muted-foreground">Fecha de pago</th>
<th className="text-right py-3 px-2 font-medium text-muted-foreground">Acción</th>
</tr>
</thead>
<tbody>
{payments.map((p) => (
<tr key={p.id} className="border-b last:border-0 hover:bg-muted/30">
<td className="py-3 px-2">
<div className="font-medium">{p.tenant.nombre}</div>
<div className="text-xs text-muted-foreground font-mono">{p.tenant.rfc || '—'}</div>
</td>
<td className="py-3 px-2">
<span className="inline-flex items-center rounded-md bg-secondary px-2 py-1 text-xs font-medium text-secondary-foreground ring-1 ring-inset ring-secondary/20">
{PLAN_LABELS[p.subscription?.plan || 'custom'] || p.subscription?.plan || 'Custom'}
</span>
</td>
<td className="py-3 px-2 font-semibold">{formatCurrency(p.amount)}</td>
<td className="py-3 px-2 text-muted-foreground">
{METHOD_LABELS[p.paymentMethod || ''] || p.paymentMethod || '—'}
</td>
<td className="py-3 px-2 text-muted-foreground">{formatDate(p.paidAt)}</td>
<td className="py-3 px-2 text-right">
<Button
size="sm"
onClick={() => handleEmitir(p.id)}
disabled={emitir.isPending && emitiendoId === p.id}
>
{emitir.isPending && emitiendoId === p.id ? (
<>
<RefreshCw className="h-4 w-4 mr-1 animate-spin" />
Emitiendo...
</>
) : (
<>
<FileText className="h-4 w-4 mr-1" />
Emitir factura
</>
)}
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</main>
</>
);
}