Files
HoruxDespachosNuevo/apps/web/app/(auth)/reset-password/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

167 lines
5.5 KiB
TypeScript

'use client';
import { Suspense, useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button, Input, Label, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@horux/shared-ui';
import { confirmPasswordReset } from '@/lib/api/auth';
import { KeyRound, CheckCircle2, AlertTriangle } from 'lucide-react';
function ResetPasswordContent() {
const router = useRouter();
const searchParams = useSearchParams();
const token = searchParams.get('token') || '';
const [isLoading, setIsLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
if (!token) {
return (
<Card>
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<div className="rounded-full bg-red-100 p-3">
<AlertTriangle className="h-10 w-10 text-red-600" />
</div>
</div>
<CardTitle>Enlace inválido</CardTitle>
<CardDescription>
El enlace no incluye un token de recuperación válido. Solicita uno nuevo desde la pantalla de login.
</CardDescription>
</CardHeader>
<CardFooter>
<Button className="w-full" asChild>
<Link href="/forgot-password">Solicitar nuevo enlace</Link>
</Button>
</CardFooter>
</Card>
);
}
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError('');
if (newPassword.length < 8) {
setError('La contraseña debe tener al menos 8 caracteres');
return;
}
if (newPassword !== confirmPassword) {
setError('Las contraseñas no coinciden');
return;
}
setIsLoading(true);
try {
await confirmPasswordReset(token, newPassword);
setSuccess(true);
setTimeout(() => router.push('/login'), 3000);
} catch (err: any) {
setError(err.response?.data?.message || 'Error al actualizar contraseña');
} finally {
setIsLoading(false);
}
}
if (success) {
return (
<Card>
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<div className="rounded-full bg-green-100 p-3">
<CheckCircle2 className="h-10 w-10 text-green-600" />
</div>
</div>
<CardTitle>Contraseña actualizada</CardTitle>
<CardDescription>
Tu contraseña fue cambiada exitosamente. Redireccionando al login...
</CardDescription>
</CardHeader>
<CardFooter>
<Button className="w-full" asChild>
<Link href="/login">Ir al login</Link>
</Button>
</CardFooter>
</Card>
);
}
return (
<Card>
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<Image src="/logo.jpg" alt="Horux360" width={80} height={80} className="rounded-full" priority />
</div>
<CardTitle className="text-2xl flex items-center justify-center gap-2">
<KeyRound className="h-5 w-5" />
Nueva contraseña
</CardTitle>
<CardDescription>
Ingresa tu nueva contraseña. Mínimo 8 caracteres.
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
{error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
)}
<div className="space-y-2">
<Label htmlFor="newPassword">Nueva contraseña</Label>
<Input
id="newPassword"
type="password"
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
autoFocus
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirmar contraseña</Label>
<Input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
/>
</div>
<p className="text-xs text-muted-foreground">
Al actualizar, se cerrarán todas tus sesiones activas por seguridad. Tendrás que volver a iniciar sesión.
</p>
</CardContent>
<CardFooter className="flex flex-col gap-2">
<Button type="submit" className="w-full" disabled={isLoading || !newPassword || !confirmPassword}>
{isLoading ? 'Actualizando...' : 'Actualizar contraseña'}
</Button>
<Link href="/login" className="text-sm text-primary hover:underline">
Volver al login
</Link>
</CardFooter>
</form>
</Card>
);
}
export default function ResetPasswordPage() {
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-md">
<Suspense fallback={<Card><CardContent className="py-8 text-center">Cargando...</CardContent></Card>}>
<ResetPasswordContent />
</Suspense>
</div>
</div>
);
}