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

145 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Button, Input, Label, Card, CardContent, CardHeader, CardTitle } from '@horux/shared-ui';
import { useAuthStore } from '@/stores/auth-store';
import { apiClient } from '@/lib/api/client';
import { CheckCircle2, ArrowLeft } from 'lucide-react';
type VerticalProfile = 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA';
export default function RegisterDespachoPage() {
const router = useRouter();
const { setUser, setTokens } = useAuthStore();
const [step, setStep] = useState(1);
const [verticalProfile, setVerticalProfile] = useState<VerticalProfile | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [form, setForm] = useState({
despachoNombre: '',
ownerNombre: '',
ownerEmail: '',
ownerPassword: '',
acceptedTerms: false,
});
const handleChange = (field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
setForm((prev) => ({ ...prev, [field]: e.target.value }));
setError('');
};
const handleSubmit = async () => {
if (!form.acceptedTerms) { setError('Debes aceptar los términos y condiciones'); return; }
if (!verticalProfile) { setError('Selecciona un tipo de despacho'); return; }
setLoading(true);
setError('');
try {
const { data } = await apiClient.post('/despachos/signup', {
despacho: {
nombre: form.despachoNombre,
verticalProfile,
},
owner: {
nombre: form.ownerNombre,
email: form.ownerEmail,
password: form.ownerPassword,
},
});
setTokens(data.accessToken, data.refreshToken);
setUser(data.user);
router.push('/onboarding');
} catch (err: any) {
setError(err.response?.data?.message || 'Error al registrar el despacho');
setStep(1);
} finally {
setLoading(false);
}
};
// =================== STEP 1: Registration Form ===================
if (step === 1) {
const canProceed = form.despachoNombre && form.ownerNombre && form.ownerEmail && form.ownerPassword.length >= 10 && form.acceptedTerms;
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 p-4">
<Card className="w-full max-w-lg">
<CardHeader className="text-center">
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground mb-4">
<span className="bg-primary text-primary-foreground rounded-full w-6 h-6 flex items-center justify-center font-bold">1</span>
<span className="w-8 h-px bg-muted" />
<span className="bg-muted text-muted-foreground rounded-full w-6 h-6 flex items-center justify-center">2</span>
</div>
<CardTitle className="text-2xl font-bold">Crea tu cuenta</CardTitle>
<p className="text-sm text-muted-foreground mt-1">Plataforma para despachos profesionales</p>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-3">
<div><Label htmlFor="dn">Nombre del despacho</Label><Input id="dn" value={form.despachoNombre} onChange={handleChange('despachoNombre')} placeholder="Despacho Pérez y Asociados" required /></div>
</div>
<div className="space-y-3">
<div><Label htmlFor="on">Tu nombre completo</Label><Input id="on" value={form.ownerNombre} onChange={handleChange('ownerNombre')} placeholder="Juan Pérez" required /></div>
<div><Label htmlFor="oe">Email</Label><Input id="oe" type="email" value={form.ownerEmail} onChange={handleChange('ownerEmail')} placeholder="juan@despacho.com" required /></div>
<div><Label htmlFor="op">Contraseña</Label><Input id="op" type="password" value={form.ownerPassword} onChange={handleChange('ownerPassword')} placeholder="Mínimo 10 caracteres" minLength={10} required /></div>
</div>
<div className="flex items-start gap-2">
<input type="checkbox" id="terms" checked={form.acceptedTerms} onChange={(e) => setForm((p) => ({ ...p, acceptedTerms: e.target.checked }))} className="mt-1" />
<label htmlFor="terms" className="text-sm text-muted-foreground">Acepto los <Link href="/terminos" target="_blank" className="underline text-primary">términos y condiciones</Link></label>
</div>
{error && <p className="text-sm text-destructive bg-destructive/10 p-3 rounded-md">{error}</p>}
<Button onClick={() => canProceed && setStep(2)} className="w-full" disabled={!canProceed}>
Continuar
</Button>
<p className="text-center text-sm text-muted-foreground">
¿Ya tienes cuenta? <Link href="/login" className="text-primary underline">Inicia sesión</Link>
</p>
</CardContent>
</Card>
</div>
);
}
// =================== STEP 2: Vertical Selection ===================
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 p-4">
<div className="w-full max-w-3xl space-y-8 text-center">
<div>
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground mb-4">
<span className="bg-green-500 text-white rounded-full w-6 h-6 flex items-center justify-center"><CheckCircle2 className="h-4 w-4" /></span>
<span className="w-8 h-px bg-primary" />
<span className="bg-primary text-primary-foreground rounded-full w-6 h-6 flex items-center justify-center font-bold">2</span>
</div>
<h1 className="text-3xl font-bold">¿Qué tipo de despacho eres?</h1>
<p className="text-muted-foreground mt-2">Selecciona tu área profesional</p>
</div>
<div className="grid md:grid-cols-3 gap-4">
<button
onClick={() => { setVerticalProfile('CONTABLE'); handleSubmit(); }}
disabled={loading}
className="p-8 rounded-xl border-2 border-primary bg-card hover:bg-accent transition-all text-center space-y-3"
>
<div className="text-4xl">📊</div>
<h3 className="text-lg font-semibold">Contable</h3>
<p className="text-sm text-muted-foreground">Gestión fiscal, CFDI, IVA/ISR, SAT sync</p>
</button>
<div className="p-8 rounded-xl border-2 border-dashed border-muted bg-muted/30 text-center space-y-3 opacity-50 cursor-not-allowed">
<div className="text-4xl"></div>
<h3 className="text-lg font-semibold">Jurídico</h3>
<p className="text-sm text-muted-foreground">Próximamente</p>
</div>
<div className="p-8 rounded-xl border-2 border-dashed border-muted bg-muted/30 text-center space-y-3 opacity-50 cursor-not-allowed">
<div className="text-4xl">🏗</div>
<h3 className="text-lg font-semibold">Arquitectura</h3>
<p className="text-sm text-muted-foreground">Próximamente</p>
</div>
</div>
{error && <p className="text-sm text-destructive bg-destructive/10 p-3 rounded-md max-w-lg mx-auto">{error}</p>}
<button onClick={() => setStep(1)} className="text-sm text-muted-foreground underline">
<ArrowLeft className="h-3 w-3 inline mr-1" />Volver al formulario
</button>
</div>
</div>
);
}