feat: redesign superadmin agencies list, implement flat design, add date filters, and fix UI bugs

This commit is contained in:
Erik Silva
2025-12-11 23:39:54 -03:00
parent 053e180321
commit dc98d5dccc
129 changed files with 20730 additions and 1611 deletions

View File

@@ -0,0 +1,267 @@
"use client";
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Input from '@/components/ui/Input';
import Button from '@/components/ui/Button';
import { CheckCircleIcon } from '@heroicons/react/24/solid';
interface FormField {
name: string;
label: string;
type: string;
required: boolean;
order: number;
}
interface SignupTemplate {
id: string;
name: string;
description: string;
slug: string;
form_fields: FormField[];
enabled_modules: string[];
redirect_url?: string;
success_message?: string;
custom_logo_url?: string;
custom_primary_color?: string;
}
export default function CustomSignupPage({ params }: { params: Promise<{ slug: string }> }) {
const router = useRouter();
const [template, setTemplate] = useState<SignupTemplate | null>(null);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState<Record<string, string>>({});
const [slug, setSlug] = useState<string>('');
useEffect(() => {
params.then(p => {
setSlug(p.slug);
});
}, [params]);
useEffect(() => {
if (slug) {
loadTemplate();
}
}, [slug]);
const loadTemplate = async () => {
try {
const response = await fetch(`/api/signup-templates/slug/${slug}`);
if (response.ok) {
const data = await response.json();
setTemplate(data);
// Inicializar formData com campos vazios
const initialData: Record<string, string> = {};
data.form_fields.forEach((field: FormField) => {
initialData[field.name] = '';
});
setFormData(initialData);
} else {
setError('Template de cadastro não encontrado');
}
} catch (err) {
setError('Erro ao carregar formulário de cadastro');
} finally {
setLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
setError('');
try {
// Registro público via template
const payload = {
template_slug: slug,
email: formData.email,
password: formData.password,
name: formData.company_name || formData.subdomain || 'Cliente',
subdomain: formData.subdomain,
company_name: formData.company_name,
...formData, // Incluir todos os campos adicionais
};
const response = await fetch('/api/signup/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (response.ok) {
setSuccess(true);
// Redirecionar após 2 segundos
setTimeout(() => {
if (template?.redirect_url) {
window.location.href = template.redirect_url;
} else {
router.push('/login');
}
}, 2000);
} else {
const data = await response.json();
setError(data.error || 'Erro ao realizar cadastro');
}
} catch (err) {
setError('Erro ao processar cadastro');
} finally {
setSubmitting(false);
}
};
const handleInputChange = (fieldName: string, value: string) => {
setFormData(prev => ({
...prev,
[fieldName]: value,
}));
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 dark:border-white"></div>
</div>
);
}
if (error && !template) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950 p-4">
<div className="bg-white dark:bg-gray-900 rounded-lg p-8 max-w-md w-full text-center border border-gray-200 dark:border-gray-800">
<div className="w-16 h-16 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-3xl"></span>
</div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Link Inválido
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
{error}
</p>
<Button onClick={() => router.push('/')}>
Voltar para Início
</Button>
</div>
</div>
);
}
if (success) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950 p-4">
<div className="bg-white dark:bg-gray-900 rounded-lg p-8 max-w-md w-full text-center border border-gray-200 dark:border-gray-800">
<div className="w-16 h-16 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircleIcon className="w-10 h-10 text-green-600 dark:text-green-400" />
</div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Cadastro Realizado!
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
{template?.success_message || 'Seu cadastro foi realizado com sucesso. Redirecionando...'}
</p>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white mx-auto"></div>
</div>
</div>
);
}
const sortedFields = [...(template?.form_fields || [])].sort((a, b) => a.order - b.order);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950 p-4">
<div className="bg-white dark:bg-gray-900 rounded-lg p-8 max-w-md w-full border border-gray-200 dark:border-gray-800">
{/* Logo personalizado */}
{template?.custom_logo_url && (
<div className="flex justify-center mb-6">
<img
src={template.custom_logo_url}
alt="Logo"
className="h-12 object-contain"
/>
</div>
)}
{/* Cabeçalho */}
<div className="text-center mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
{template?.name}
</h1>
{template?.description && (
<p className="text-sm text-gray-600 dark:text-gray-400">
{template.description}
</p>
)}
</div>
{/* Módulos incluídos */}
{template && template.enabled_modules.length > 0 && (
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
Módulos incluídos:
</p>
<div className="flex flex-wrap gap-2">
{template.enabled_modules.map((module) => (
<span
key={module}
className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded text-xs font-medium"
>
{module}
</span>
))}
</div>
</div>
)}
{/* Formulário */}
<form onSubmit={handleSubmit} className="space-y-4">
{sortedFields.map((field) => (
<Input
key={field.name}
label={field.label}
type={field.type}
value={formData[field.name] || ''}
onChange={(e) => handleInputChange(field.name, e.target.value)}
required={field.required}
placeholder={`Digite ${field.label.toLowerCase()}`}
/>
))}
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<Button
type="submit"
className="w-full"
disabled={submitting}
style={template?.custom_primary_color ? {
background: template.custom_primary_color
} : undefined}
>
{submitting ? 'Cadastrando...' : 'Criar Conta'}
</Button>
</form>
{/* Link para login */}
<p className="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
tem uma conta?{' '}
<a href="/login" className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">
Fazer login
</a>
</p>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff