1088 lines
62 KiB
TypeScript
1088 lines
62 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
UserIcon,
|
|
EnvelopeIcon,
|
|
PhoneIcon,
|
|
BuildingOfficeIcon,
|
|
ChatBubbleLeftRightIcon,
|
|
MapPinIcon,
|
|
IdentificationIcon,
|
|
CheckIcon,
|
|
ArrowRightIcon,
|
|
ArrowLeftIcon,
|
|
PhotoIcon,
|
|
LockClosedIcon,
|
|
EyeIcon,
|
|
EyeSlashIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { CheckCircleIcon } from '@heroicons/react/24/solid';
|
|
import { registerCustomer } from '@/lib/register-customer';
|
|
|
|
type PersonType = 'pf' | 'pj';
|
|
|
|
type Step = {
|
|
id: number;
|
|
name: string;
|
|
description: string;
|
|
};
|
|
|
|
type AgencyBranding = {
|
|
primary_color: string;
|
|
logo_url?: string;
|
|
name: string;
|
|
};
|
|
type CustomerFormState = {
|
|
cpf: string;
|
|
full_name: string;
|
|
cnpj: string;
|
|
company_name: string;
|
|
trade_name: string;
|
|
responsible_name: string;
|
|
email: string;
|
|
phone: string;
|
|
password: string;
|
|
password_confirmation: string;
|
|
postal_code: string;
|
|
street: string;
|
|
number: string;
|
|
complement: string;
|
|
neighborhood: string;
|
|
city: string;
|
|
state: string;
|
|
message: string;
|
|
};
|
|
|
|
interface CadastroClientePageProps {
|
|
branding: AgencyBranding;
|
|
}
|
|
|
|
const steps: Step[] = [
|
|
{ id: 1, name: 'Identificação', description: 'Tipo e dados principais' },
|
|
{ id: 2, name: 'Contato', description: 'Email e telefone' },
|
|
{ id: 3, name: 'Endereço', description: 'Localização completa' },
|
|
{ id: 4, name: 'Logo & Mensagem', description: 'Personalização' },
|
|
{ id: 5, name: 'Segurança', description: 'Defina sua senha de acesso' },
|
|
{ id: 6, name: 'Revisão', description: 'Confirmar dados' },
|
|
];
|
|
|
|
const initialFormState: CustomerFormState = {
|
|
cpf: '',
|
|
full_name: '',
|
|
cnpj: '',
|
|
company_name: '',
|
|
trade_name: '',
|
|
responsible_name: '',
|
|
email: '',
|
|
phone: '',
|
|
password: '',
|
|
password_confirmation: '',
|
|
postal_code: '',
|
|
street: '',
|
|
number: '',
|
|
complement: '',
|
|
neighborhood: '',
|
|
city: '',
|
|
state: '',
|
|
message: '',
|
|
};
|
|
|
|
const lightenColor = (hexColor: string, amount = 20) => {
|
|
const fallback = '#2563eb';
|
|
if (!hexColor) return fallback;
|
|
|
|
let color = hexColor.replace('#', '');
|
|
if (color.length === 3) {
|
|
color = color.split('').map(char => char + char).join('');
|
|
}
|
|
|
|
if (color.length !== 6) return fallback;
|
|
|
|
const num = parseInt(color, 16);
|
|
if (Number.isNaN(num)) return fallback;
|
|
|
|
const clamp = (value: number) => Math.max(0, Math.min(255, value));
|
|
const r = clamp((num >> 16) + amount);
|
|
const g = clamp(((num >> 8) & 0x00ff) + amount);
|
|
const b = clamp((num & 0x0000ff) + amount);
|
|
|
|
const nextColor = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
return `#${nextColor}`;
|
|
};
|
|
|
|
export default function CadastroClientePage({ branding }: CadastroClientePageProps) {
|
|
const router = useRouter();
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [personType, setPersonType] = useState<PersonType>('pf');
|
|
const [logoFile, setLogoFile] = useState<File | null>(null);
|
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
|
const [formData, setFormData] = useState<CustomerFormState>({ ...initialFormState });
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isCepLoading, setIsCepLoading] = useState(false);
|
|
const [isCnpjLoading, setIsCnpjLoading] = useState(false);
|
|
const [lastCnpjFetched, setLastCnpjFetched] = useState<string | null>(null);
|
|
const [autoFilledFields, setAutoFilledFields] = useState<Partial<Record<keyof CustomerFormState, boolean>>>({});
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showPasswordConfirmation, setShowPasswordConfirmation] = useState(false);
|
|
|
|
const primaryColor = branding.primary_color?.trim() || '#2563eb';
|
|
const gradientStyle = {
|
|
backgroundImage: `linear-gradient(120deg, ${primaryColor}, ${lightenColor(primaryColor, 25)})`,
|
|
boxShadow: `0 12px 22px -12px ${primaryColor}66`,
|
|
};
|
|
const buttonBase = 'inline-flex items-center gap-2 rounded-lg font-semibold transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--brand-color)] disabled:opacity-60 disabled:cursor-not-allowed';
|
|
const hasAutoFilledFields = Object.keys(autoFilledFields).length > 0;
|
|
|
|
useEffect(() => {
|
|
document.documentElement.style.setProperty('--brand-color', primaryColor);
|
|
}, [primaryColor]);
|
|
|
|
const formatCpf = (value: string) => {
|
|
return value
|
|
.replace(/\D/g, '')
|
|
.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4')
|
|
.substring(0, 14);
|
|
};
|
|
|
|
const formatCnpj = (value: string) => {
|
|
return value
|
|
.replace(/\D/g, '')
|
|
.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5')
|
|
.substring(0, 18);
|
|
};
|
|
|
|
const formatPhone = (value: string) => {
|
|
const digits = value.replace(/\D/g, '').substring(0, 11);
|
|
if (digits.length <= 10) {
|
|
return digits.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3').trim();
|
|
}
|
|
return digits.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3').trim();
|
|
};
|
|
|
|
const formatCep = (value: string) => {
|
|
return value
|
|
.replace(/\D/g, '')
|
|
.replace(/(\d{5})(\d{3})/, '$1-$2')
|
|
.substring(0, 9);
|
|
};
|
|
|
|
const clearAutoFilledFields = () => {
|
|
setAutoFilledFields({});
|
|
setLastCnpjFetched(null);
|
|
};
|
|
|
|
const isFieldLocked = (field: keyof CustomerFormState) => personType === 'pj' && Boolean(autoFilledFields[field]);
|
|
const inputLockClasses = (field: keyof CustomerFormState) => (isFieldLocked(field) ? 'bg-gray-100 cursor-not-allowed border-gray-200' : '');
|
|
|
|
const applyAutoFilledData = (
|
|
data: Partial<CustomerFormState>,
|
|
fieldsToLock: Array<keyof CustomerFormState> = []
|
|
) => {
|
|
if (!Object.keys(data).length) return;
|
|
|
|
setFormData(prev => ({ ...prev, ...data }));
|
|
|
|
if (!fieldsToLock.length) return;
|
|
|
|
setAutoFilledFields(prev => {
|
|
const updated = { ...prev };
|
|
fieldsToLock.forEach(field => {
|
|
if (data[field]) {
|
|
updated[field] = true;
|
|
}
|
|
});
|
|
return updated;
|
|
});
|
|
};
|
|
|
|
const handleCpfChange = (value: string) => {
|
|
if (error) setError(null);
|
|
setFormData(prev => ({ ...prev, cpf: formatCpf(value) }));
|
|
};
|
|
|
|
const handleCnpjChange = (value: string) => {
|
|
if (error) setError(null);
|
|
setFormData(prev => ({ ...prev, cnpj: formatCnpj(value) }));
|
|
if (Object.keys(autoFilledFields).length) {
|
|
setAutoFilledFields({});
|
|
}
|
|
if (lastCnpjFetched) {
|
|
setLastCnpjFetched(null);
|
|
}
|
|
};
|
|
|
|
const handlePhoneChange = (value: string) => {
|
|
if (error) setError(null);
|
|
setFormData(prev => ({ ...prev, phone: formatPhone(value) }));
|
|
};
|
|
|
|
const handlePostalCodeChange = (value: string) => {
|
|
if (error) setError(null);
|
|
setFormData(prev => ({ ...prev, postal_code: formatCep(value) }));
|
|
};
|
|
|
|
const handleCnpjBlur = async () => {
|
|
if (personType !== 'pj') return;
|
|
|
|
const digits = formData.cnpj.replace(/\D/g, '');
|
|
if (digits.length !== 14 || digits === lastCnpjFetched) return;
|
|
|
|
setIsCnpjLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetch(`https://brasilapi.com.br/api/cnpj/v1/${digits}`, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('CNPJ não encontrado');
|
|
}
|
|
|
|
const data = await response.json();
|
|
const lockedFields: Array<keyof CustomerFormState> = ['company_name', 'trade_name', 'responsible_name'];
|
|
const formattedCnpj = formatCnpj(formData.cnpj);
|
|
|
|
const autoData: Partial<CustomerFormState> = {
|
|
cnpj: formattedCnpj,
|
|
};
|
|
|
|
if (data.razao_social) autoData.company_name = String(data.razao_social).trim();
|
|
if (data.nome_fantasia) autoData.trade_name = String(data.nome_fantasia).trim();
|
|
const responsibleName = data.qsa?.[0]?.nome?.trim();
|
|
if (responsibleName) autoData.responsible_name = responsibleName;
|
|
if (data.telefone) autoData.phone = formatPhone(String(data.telefone));
|
|
if (data.email) autoData.email = String(data.email).toLowerCase();
|
|
if (data.cep) autoData.postal_code = formatCep(String(data.cep));
|
|
if (data.logradouro) autoData.street = String(data.logradouro);
|
|
if (data.numero) autoData.number = String(data.numero);
|
|
if (data.complemento) autoData.complement = String(data.complemento);
|
|
if (data.bairro) autoData.neighborhood = String(data.bairro);
|
|
if (data.municipio) autoData.city = String(data.municipio);
|
|
if (data.uf) autoData.state = String(data.uf);
|
|
|
|
applyAutoFilledData(autoData, lockedFields);
|
|
setLastCnpjFetched(digits);
|
|
} catch (err) {
|
|
console.error(err);
|
|
setError('Não conseguimos localizar este CNPJ. Confira os números e tente novamente.');
|
|
} finally {
|
|
setIsCnpjLoading(false);
|
|
}
|
|
};
|
|
|
|
const validateEmail = (email: string): boolean => {
|
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
return emailRegex.test(email);
|
|
};
|
|
|
|
const validateStep = (step: number): boolean => {
|
|
switch (step) {
|
|
case 1:
|
|
if (personType === 'pf') {
|
|
if (!formData.cpf) {
|
|
setError('❌ CPF é obrigatório');
|
|
return false;
|
|
}
|
|
if (formData.cpf.replace(/\D/g, '').length !== 11) {
|
|
setError('❌ CPF deve conter 11 dígitos');
|
|
return false;
|
|
}
|
|
if (!formData.full_name) {
|
|
setError('❌ Nome Completo é obrigatório');
|
|
return false;
|
|
}
|
|
if (formData.full_name.trim().split(' ').length < 2) {
|
|
setError('❌ Digite seu nome completo (nome e sobrenome)');
|
|
return false;
|
|
}
|
|
} else {
|
|
if (!formData.cnpj) {
|
|
setError('❌ CNPJ é obrigatório');
|
|
return false;
|
|
}
|
|
if (formData.cnpj.replace(/\D/g, '').length !== 14) {
|
|
setError('❌ CNPJ deve conter 14 dígitos');
|
|
return false;
|
|
}
|
|
if (!formData.company_name) {
|
|
setError('❌ Razão Social é obrigatória');
|
|
return false;
|
|
}
|
|
if (!formData.responsible_name) {
|
|
setError('❌ Nome do responsável é obrigatório');
|
|
return false;
|
|
}
|
|
if (formData.responsible_name.trim().split(' ').length < 2) {
|
|
setError('❌ Informe o nome completo do responsável');
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
case 2:
|
|
if (!formData.email) {
|
|
setError('❌ E-mail é obrigatório');
|
|
return false;
|
|
}
|
|
if (!validateEmail(formData.email)) {
|
|
setError('❌ Digite um e-mail válido (exemplo: seu@email.com)');
|
|
return false;
|
|
}
|
|
if (!formData.phone) {
|
|
setError('❌ Telefone é obrigatório');
|
|
return false;
|
|
}
|
|
const phoneDigits = formData.phone.replace(/\D/g, '');
|
|
if (phoneDigits.length < 10) {
|
|
setError('❌ Telefone deve conter pelo menos 10 dígitos');
|
|
return false;
|
|
}
|
|
break;
|
|
case 3:
|
|
if (!formData.postal_code) {
|
|
setError('❌ CEP é obrigatório');
|
|
return false;
|
|
}
|
|
if (formData.postal_code.replace(/\D/g, '').length !== 8) {
|
|
setError('❌ CEP deve conter 8 dígitos');
|
|
return false;
|
|
}
|
|
if (!formData.street) {
|
|
setError('❌ Logradouro é obrigatório');
|
|
return false;
|
|
}
|
|
if (!formData.number) {
|
|
setError('❌ Número é obrigatório');
|
|
return false;
|
|
}
|
|
if (!formData.neighborhood) {
|
|
setError('❌ Bairro é obrigatório');
|
|
return false;
|
|
}
|
|
if (!formData.city) {
|
|
setError('❌ Cidade é obrigatória');
|
|
return false;
|
|
}
|
|
if (!formData.state) {
|
|
setError('❌ Estado é obrigatório');
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
setError(null);
|
|
return true;
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (validateStep(currentStep)) {
|
|
setCurrentStep(prev => Math.min(prev + 1, steps.length));
|
|
}
|
|
};
|
|
|
|
const handlePrevious = () => {
|
|
setError(null);
|
|
setCurrentStep(prev => Math.max(prev - 1, 1));
|
|
};
|
|
|
|
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
setError('A imagem deve ter no máximo 5MB');
|
|
return;
|
|
}
|
|
setLogoFile(file);
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setLogoPreview(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError(null);
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
const submitData = new FormData();
|
|
submitData.append('person_type', personType);
|
|
|
|
Object.entries(formData).forEach(([key, value]) => {
|
|
if (value) submitData.append(key, value);
|
|
});
|
|
|
|
if (logoFile) {
|
|
submitData.append('logo', logoFile);
|
|
}
|
|
|
|
const result = await registerCustomer(submitData);
|
|
|
|
if (!result.success) {
|
|
// Mensagens de erro específicas para duplicidade
|
|
if (result.error?.includes('e-mail')) {
|
|
throw new Error('⚠️ Já existe uma conta cadastrada com este e-mail. Se você já é cliente, faça login no portal.');
|
|
} else if (result.error?.includes('CPF')) {
|
|
throw new Error('⚠️ Já existe uma conta cadastrada com este CPF. Se você já é cliente, faça login no portal.');
|
|
} else if (result.error?.includes('CNPJ')) {
|
|
throw new Error('⚠️ Já existe uma conta cadastrada com este CNPJ. Se você já é cliente, faça login no portal.');
|
|
} else {
|
|
throw new Error(result.error || 'Erro ao enviar cadastro');
|
|
}
|
|
}
|
|
|
|
// Salvar nome do cliente no sessionStorage para exibir na página de sucesso
|
|
const customerName = personType === 'pf' ? formData.full_name : (formData.trade_name || formData.company_name);
|
|
sessionStorage.setItem('customer_name', customerName);
|
|
sessionStorage.setItem('customer_email', formData.email);
|
|
|
|
// Redirecionar para página de sucesso com mensagem
|
|
router.push('/cliente/cadastro/sucesso');
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
// Scroll para o topo para garantir que o usuário veja o erro
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
if (error) setError(null);
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({ ...prev, [name]: value }));
|
|
};
|
|
|
|
const handlePersonTypeChange = (type: PersonType) => {
|
|
setPersonType(type);
|
|
setError(null);
|
|
|
|
if (type === 'pf') {
|
|
clearAutoFilledFields();
|
|
setFormData(prev => ({
|
|
...prev,
|
|
cnpj: '',
|
|
company_name: '',
|
|
trade_name: '',
|
|
responsible_name: '',
|
|
}));
|
|
} else {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
cpf: '',
|
|
full_name: '',
|
|
}));
|
|
}
|
|
};
|
|
|
|
const handleCepBlur = async () => {
|
|
const cep = formData.postal_code.replace(/\D/g, '');
|
|
if (cep.length !== 8) return;
|
|
|
|
setIsCepLoading(true);
|
|
try {
|
|
const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
|
|
const data = await response.json();
|
|
|
|
if (data.erro) {
|
|
setError('CEP não encontrado');
|
|
return;
|
|
}
|
|
|
|
setFormData(prev => ({
|
|
...prev,
|
|
street: data.logradouro || '',
|
|
neighborhood: data.bairro || '',
|
|
city: data.localidade || '',
|
|
state: data.uf || '',
|
|
}));
|
|
} catch (err) {
|
|
setError('Erro ao buscar CEP');
|
|
} finally {
|
|
setIsCepLoading(false);
|
|
}
|
|
};
|
|
|
|
const renderReviewSection = () => (
|
|
<div className="space-y-6">
|
|
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-lg p-6">
|
|
<div className="flex items-center mb-4">
|
|
<CheckCircleIcon className="h-6 w-6 text-green-600 mr-2" />
|
|
<h3 className="text-lg font-semibold text-gray-900">Revise seus dados antes de enviar</h3>
|
|
</div>
|
|
</div>
|
|
|
|
{logoPreview && (
|
|
<div className="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
|
|
<img src={logoPreview} alt="Logo" className="h-20 w-20 object-contain rounded-lg border-2 border-gray-200" />
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900">Logo carregado</p>
|
|
<p className="text-xs text-gray-500">{logoFile?.name}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="p-4 bg-gray-50 rounded-lg">
|
|
<p className="text-xs font-medium text-gray-500 uppercase">Tipo</p>
|
|
<p className="text-sm font-semibold text-gray-900 mt-1">{personType === 'pf' ? 'Pessoa Física' : 'Pessoa Jurídica'}</p>
|
|
</div>
|
|
{personType === 'pf' ? (
|
|
<>
|
|
<div className="p-4 bg-gray-50 rounded-lg"><p className="text-xs font-medium text-gray-500 uppercase">CPF</p><p className="text-sm font-semibold text-gray-900 mt-1">{formData.cpf}</p></div>
|
|
<div className="p-4 bg-gray-50 rounded-lg md:col-span-2"><p className="text-xs font-medium text-gray-500 uppercase">Nome</p><p className="text-sm font-semibold text-gray-900 mt-1">{formData.full_name}</p></div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="p-4 bg-gray-50 rounded-lg"><p className="text-xs font-medium text-gray-500 uppercase">CNPJ</p><p className="text-sm font-semibold text-gray-900 mt-1">{formData.cnpj}</p></div>
|
|
<div className="p-4 bg-gray-50 rounded-lg md:col-span-2"><p className="text-xs font-medium text-gray-500 uppercase">Razão Social</p><p className="text-sm font-semibold text-gray-900 mt-1">{formData.company_name}</p></div>
|
|
{formData.trade_name && <div className="p-4 bg-gray-50 rounded-lg md:col-span-2"><p className="text-xs font-medium text-gray-500 uppercase">Nome Fantasia</p><p className="text-sm font-semibold text-gray-900 mt-1">{formData.trade_name}</p></div>}
|
|
<div className="p-4 bg-gray-50 rounded-lg md:col-span-2"><p className="text-xs font-medium text-gray-500 uppercase">Responsável</p><p className="text-sm font-semibold text-gray-900 mt-1">{formData.responsible_name}</p></div>
|
|
</>
|
|
)}
|
|
<div className="p-4 bg-gray-50 rounded-lg"><p className="text-xs font-medium text-gray-500 uppercase">Email</p><p className="text-sm font-semibold text-gray-900 mt-1">{formData.email}</p></div>
|
|
<div className="p-4 bg-gray-50 rounded-lg"><p className="text-xs font-medium text-gray-500 uppercase">Telefone</p><p className="text-sm font-semibold text-gray-900 mt-1">{formData.phone}</p></div>
|
|
<div className="p-4 bg-gray-50 rounded-lg md:col-span-2">
|
|
<p className="text-xs font-medium text-gray-500 uppercase">Endereço</p>
|
|
<p className="text-sm font-semibold text-gray-900 mt-1">{formData.street}, {formData.number}{formData.complement && `, ${formData.complement}`}</p>
|
|
<p className="text-sm text-gray-700">{formData.neighborhood} - {formData.city}/{formData.state}</p>
|
|
<p className="text-sm text-gray-700">CEP: {formData.postal_code}</p>
|
|
</div>
|
|
{formData.message && <div className="p-4 bg-gray-50 rounded-lg md:col-span-2"><p className="text-xs font-medium text-gray-500 uppercase">Mensagem</p><p className="text-sm text-gray-900 mt-1">{formData.message}</p></div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-12 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-3xl mx-auto">
|
|
{/* Header com Logo e Nome da Agência */}
|
|
<div className="text-center mb-8">
|
|
{branding.logo_url ? (
|
|
<img
|
|
src={branding.logo_url}
|
|
alt={branding.name || 'Logo'}
|
|
className="mx-auto h-20 w-auto object-contain mb-4"
|
|
/>
|
|
) : (
|
|
<div className="mx-auto h-20 w-20 rounded-xl flex items-center justify-center mb-4 shadow-lg" style={{ backgroundColor: primaryColor }}>
|
|
<span className="text-3xl font-bold text-white">
|
|
{branding.name?.substring(0, 2).toUpperCase() || 'AG'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{branding.name && (
|
|
<div className="mb-6">
|
|
<p className="text-sm font-medium text-gray-600 mb-1">Bem-vindo ao portal de</p>
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">{branding.name}</h1>
|
|
</div>
|
|
)}
|
|
|
|
<h2 className="text-3xl font-bold text-gray-900">Cadastre-se como Cliente</h2>
|
|
<p className="mt-2 text-sm text-gray-600">Preencha o formulário em etapas para solicitar acesso ao nosso portal</p>
|
|
</div>
|
|
|
|
{/* Progress Stepper */}
|
|
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<nav aria-label="Progress">
|
|
<ol className="flex items-start justify-between gap-2">
|
|
{steps.map((step, stepIdx) => (
|
|
<li key={step.id} className={`relative flex flex-col items-center ${stepIdx !== steps.length - 1 ? 'flex-1' : ''}`}>
|
|
<div className="flex flex-col items-center gap-2">
|
|
<div className="relative flex items-center justify-center">
|
|
<div className={`h-10 w-10 rounded-full flex items-center justify-center border-2 transition-all ${step.id < currentStep
|
|
? 'bg-green-500 border-green-500'
|
|
: step.id === currentStep
|
|
? 'border-[var(--brand-color)] bg-white'
|
|
: 'border-gray-300 bg-white'
|
|
}`} style={step.id === currentStep ? { borderColor: primaryColor } : {}}>
|
|
{step.id < currentStep ? (
|
|
<CheckIcon className="h-5 w-5 text-white" />
|
|
) : (
|
|
<span className={`text-sm font-semibold ${step.id === currentStep ? 'text-gray-900' : 'text-gray-400'}`} style={step.id === currentStep ? { color: primaryColor } : {}}>
|
|
{step.id}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className={`text-xs font-medium ${step.id <= currentStep ? 'text-gray-900' : 'text-gray-500'}`}>{step.name}</p>
|
|
<p className="text-xs text-gray-500 hidden sm:block">{step.description}</p>
|
|
</div>
|
|
</div>
|
|
{stepIdx !== steps.length - 1 && (
|
|
<div className="hidden lg:block absolute top-5 left-[calc(50%+20px)] w-[calc(100%-40px)] h-0.5 bg-gray-300" style={step.id < currentStep ? { backgroundColor: '#10b981' } : {}} />
|
|
)}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow-xl p-8">
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Step 1: Identificação */}
|
|
{currentStep === 1 && (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">Tipo de Cadastro *</label>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<button type="button" onClick={() => handlePersonTypeChange('pf')} className={`inline-flex items-center justify-center px-5 py-3 border-2 rounded-lg text-sm font-semibold transition-all ${personType === 'pf' ? 'border-[var(--brand-color)] bg-blue-50 shadow-sm' : 'border-gray-300 hover:border-gray-400'}`} style={personType === 'pf' ? { borderColor: primaryColor, backgroundColor: primaryColor + '10' } : {}}>
|
|
<UserIcon className="h-5 w-5 mr-2" />
|
|
<span className="font-semibold">Pessoa Física</span>
|
|
</button>
|
|
<button type="button" onClick={() => handlePersonTypeChange('pj')} className={`inline-flex items-center justify-center px-5 py-3 border-2 rounded-lg text-sm font-semibold transition-all ${personType === 'pj' ? 'border-[var(--brand-color)] bg-blue-50 shadow-sm' : 'border-gray-300 hover:border-gray-400'}`} style={personType === 'pj' ? { borderColor: primaryColor, backgroundColor: primaryColor + '10' } : {}}>
|
|
<BuildingOfficeIcon className="h-5 w-5 mr-2" />
|
|
<span className="font-semibold">Pessoa Jurídica</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{personType === 'pf' ? (
|
|
<>
|
|
<div>
|
|
<label htmlFor="cpf" className="block text-sm font-medium text-gray-700 mb-2">CPF *</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<IdentificationIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
id="cpf"
|
|
name="cpf"
|
|
required
|
|
value={formData.cpf}
|
|
onChange={(e) => handleCpfChange(e.target.value)}
|
|
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent"
|
|
placeholder="000.000.000-00"
|
|
maxLength={14}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="full_name" className="block text-sm font-medium text-gray-700 mb-2">Nome Completo *</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<UserIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input type="text" id="full_name" name="full_name" required value={formData.full_name} onChange={handleChange} className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent" placeholder="Seu nome completo" />
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div>
|
|
<label htmlFor="cnpj" className="block text-sm font-medium text-gray-700 mb-2">CNPJ *</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<IdentificationIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
id="cnpj"
|
|
name="cnpj"
|
|
required
|
|
value={formData.cnpj}
|
|
onChange={(e) => handleCnpjChange(e.target.value)}
|
|
onBlur={handleCnpjBlur}
|
|
className={`block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent ${isCnpjLoading ? 'pr-10' : ''}`}
|
|
placeholder="00.000.000/0000-00"
|
|
maxLength={18}
|
|
/>
|
|
{isCnpjLoading && (
|
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
|
|
<div className="animate-spin h-5 w-5 border-2 border-gray-300 border-t-gray-600 rounded-full" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
{hasAutoFilledFields && (
|
|
<div className="flex flex-wrap items-center justify-between gap-2 mt-2 text-xs text-gray-500">
|
|
<span className="flex items-center gap-1">
|
|
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
|
Campos preenchidos automaticamente foram bloqueados.
|
|
</span>
|
|
<button type="button" onClick={clearAutoFilledFields} className="font-semibold" style={{ color: primaryColor }}>
|
|
Limpar dados
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label htmlFor="company_name" className="block text-sm font-medium text-gray-700 mb-2">Razão Social *</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<BuildingOfficeIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
id="company_name"
|
|
name="company_name"
|
|
required
|
|
value={formData.company_name}
|
|
onChange={handleChange}
|
|
readOnly={isFieldLocked('company_name')}
|
|
className={`block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent ${inputLockClasses('company_name')}`}
|
|
placeholder="Razão social da empresa"
|
|
/>
|
|
</div>
|
|
{isFieldLocked('company_name') && <p className="mt-1 text-xs text-gray-500">Dado oficial preenchido automaticamente via CNPJ.</p>}
|
|
</div>
|
|
<div>
|
|
<label htmlFor="responsible_name" className="block text-sm font-medium text-gray-700 mb-2">Nome do Responsável *</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<UserIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
id="responsible_name"
|
|
name="responsible_name"
|
|
required
|
|
value={formData.responsible_name}
|
|
onChange={handleChange}
|
|
readOnly={isFieldLocked('responsible_name')}
|
|
className={`block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent ${inputLockClasses('responsible_name')}`}
|
|
placeholder="Nome completo do responsável"
|
|
/>
|
|
</div>
|
|
{isFieldLocked('responsible_name') && <p className="mt-1 text-xs text-gray-500">Campo bloqueado para manter os dados cadastrais.</p>}
|
|
</div>
|
|
<div>
|
|
<label htmlFor="trade_name" className="block text-sm font-medium text-gray-700 mb-2">Nome Fantasia</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<BuildingOfficeIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
id="trade_name"
|
|
name="trade_name"
|
|
value={formData.trade_name}
|
|
onChange={handleChange}
|
|
readOnly={isFieldLocked('trade_name')}
|
|
className={`block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent ${inputLockClasses('trade_name')}`}
|
|
placeholder="Nome fantasia (opcional)"
|
|
/>
|
|
</div>
|
|
{isFieldLocked('trade_name') && <p className="mt-1 text-xs text-gray-500">Nome fantasia sincronizado com a Receita Federal.</p>}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Contato */}
|
|
{currentStep === 2 && (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">E-mail *</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<EnvelopeIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
name="email"
|
|
required
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
if (e.target.value && !validateEmail(e.target.value)) {
|
|
setError('❌ Digite um e-mail válido (exemplo: seu@email.com)');
|
|
} else {
|
|
setError(null);
|
|
}
|
|
}}
|
|
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent"
|
|
placeholder="seu@email.com"
|
|
/>
|
|
</div>
|
|
<p className="mt-1 text-xs text-gray-500">Digite um e-mail válido para receber suas credenciais de acesso</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-2">Telefone *</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<PhoneIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="tel"
|
|
id="phone"
|
|
name="phone"
|
|
required
|
|
value={formData.phone}
|
|
onChange={(e) => handlePhoneChange(e.target.value)}
|
|
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent"
|
|
placeholder="(00) 00000-0000"
|
|
maxLength={15}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Endereço */}
|
|
{currentStep === 3 && (
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Endereço Completo</h3>
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label htmlFor="postal_code" className="block text-sm font-medium text-gray-700 mb-2">CEP *</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<MapPinIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
id="postal_code"
|
|
name="postal_code"
|
|
required
|
|
value={formData.postal_code}
|
|
onChange={(e) => handlePostalCodeChange(e.target.value)}
|
|
onBlur={handleCepBlur}
|
|
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent"
|
|
placeholder="00000-000"
|
|
maxLength={9}
|
|
/>
|
|
{isCepLoading && <div className="absolute inset-y-0 right-0 pr-3 flex items-center"><div className="animate-spin h-5 w-5 border-2 border-gray-300 border-t-gray-600 rounded-full"></div></div>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="col-span-2">
|
|
<label htmlFor="street" className="block text-sm font-medium text-gray-700 mb-2">Rua *</label>
|
|
<input type="text" id="street" name="street" required value={formData.street} onChange={handleChange} className="block w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent" />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="number" className="block text-sm font-medium text-gray-700 mb-2">Número *</label>
|
|
<input type="text" id="number" name="number" required value={formData.number} onChange={handleChange} className="block w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label htmlFor="complement" className="block text-sm font-medium text-gray-700 mb-2">Complemento</label>
|
|
<input type="text" id="complement" name="complement" value={formData.complement} onChange={handleChange} className="block w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent" placeholder="Apto, Bloco, etc." />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="neighborhood" className="block text-sm font-medium text-gray-700 mb-2">Bairro *</label>
|
|
<input type="text" id="neighborhood" name="neighborhood" required value={formData.neighborhood} onChange={handleChange} className="block w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="col-span-2">
|
|
<label htmlFor="city" className="block text-sm font-medium text-gray-700 mb-2">Cidade *</label>
|
|
<input type="text" id="city" name="city" required value={formData.city} onChange={handleChange} className="block w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent" />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="state" className="block text-sm font-medium text-gray-700 mb-2">Estado *</label>
|
|
<input type="text" id="state" name="state" required value={formData.state} onChange={handleChange} className="block w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent" placeholder="UF" maxLength={2} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 4: Logo & Mensagem */}
|
|
{currentStep === 4 && (
|
|
<div className="space-y-6">
|
|
{/* Upload de Logo */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Logo da Empresa/Foto (opcional)</label>
|
|
<div className="mt-2 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-lg hover:border-gray-400 transition-colors">
|
|
<div className="space-y-2 text-center">
|
|
{logoPreview ? (
|
|
<div className="relative">
|
|
<img src={logoPreview} alt="Preview" className="mx-auto h-32 w-32 object-contain rounded-lg" />
|
|
<button type="button" onClick={() => { setLogoFile(null); setLogoPreview(null); }} className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600">
|
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<PhotoIcon className="mx-auto h-12 w-12 text-gray-400" />
|
|
<div className="flex text-sm text-gray-600">
|
|
<label htmlFor="logo-upload" className="relative cursor-pointer rounded-md font-medium hover:text-indigo-500 focus-within:outline-none" style={{ color: primaryColor }}>
|
|
<span>Carregar arquivo</span>
|
|
<input id="logo-upload" name="logo-upload" type="file" className="sr-only" accept="image/*" onChange={handleLogoChange} />
|
|
</label>
|
|
<p className="pl-1">ou arraste e solte</p>
|
|
</div>
|
|
<p className="text-xs text-gray-500">PNG, JPG até 5MB</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mensagem */}
|
|
<div>
|
|
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-2">Mensagem Adicional (opcional)</label>
|
|
<div className="relative">
|
|
<div className="absolute top-3 left-3 pointer-events-none">
|
|
<ChatBubbleLeftRightIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<textarea id="message" name="message" rows={4} value={formData.message} onChange={handleChange} className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent resize-none" placeholder="Conte-nos um pouco sobre seu interesse..." />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 5: Segurança - Senha */}
|
|
{currentStep === 5 && (
|
|
<div className="space-y-6">
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
<div className="flex items-start gap-3">
|
|
<LockClosedIcon className="h-5 w-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-blue-900">Defina sua senha de acesso</h4>
|
|
<p className="text-sm text-blue-700 mt-1">
|
|
Você usará esta senha para acessar o portal do cliente assim que seu cadastro for aprovado pela agência.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Senha de acesso *
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<LockClosedIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type={showPassword ? 'text' : 'password'}
|
|
id="password"
|
|
name="password"
|
|
required
|
|
minLength={6}
|
|
value={formData.password}
|
|
onChange={handleChange}
|
|
className="block w-full pl-10 pr-12 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent"
|
|
placeholder="Mínimo 6 caracteres"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showPassword ? (
|
|
<EyeSlashIcon className="h-5 w-5" />
|
|
) : (
|
|
<EyeIcon className="h-5 w-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="mt-1 text-xs text-gray-500">Use no mínimo 6 caracteres</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="password_confirmation" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Confirme a senha *
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<LockClosedIcon className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type={showPasswordConfirmation ? 'text' : 'password'}
|
|
id="password_confirmation"
|
|
name="password_confirmation"
|
|
required
|
|
minLength={6}
|
|
value={formData.password_confirmation}
|
|
onChange={handleChange}
|
|
onBlur={() => {
|
|
if (formData.password && formData.password_confirmation && formData.password !== formData.password_confirmation) {
|
|
setError('❌ As senhas não coincidem');
|
|
} else {
|
|
setError(null);
|
|
}
|
|
}}
|
|
className="block w-full pl-10 pr-12 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-opacity-50 focus:border-transparent"
|
|
placeholder="Digite a mesma senha"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPasswordConfirmation(!showPasswordConfirmation)}
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showPasswordConfirmation ? (
|
|
<EyeSlashIcon className="h-5 w-5" />
|
|
) : (
|
|
<EyeIcon className="h-5 w-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 6: Revisão */}
|
|
{currentStep === 6 && renderReviewSection()}
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">{error}</div>
|
|
)}
|
|
|
|
{/* Navigation Buttons */}
|
|
<div className="flex justify-between pt-6 border-t border-gray-200">
|
|
{currentStep > 1 && (
|
|
<button type="button" onClick={handlePrevious} className={`${buttonBase} px-5 py-2.5 border-2 border-gray-300 text-gray-700 hover:bg-gray-50`}>
|
|
<ArrowLeftIcon className="h-5 w-5" />
|
|
Voltar
|
|
</button>
|
|
)}
|
|
|
|
{currentStep < steps.length ? (
|
|
<button type="button" onClick={handleNext} className={`${buttonBase} ml-auto px-5 py-2.5 text-white shadow-lg hover:-translate-y-0.5`} style={gradientStyle}>
|
|
Próximo
|
|
<ArrowRightIcon className="h-5 w-5" />
|
|
</button>
|
|
) : (
|
|
<button type="submit" disabled={isSubmitting} className={`${buttonBase} ml-auto px-5 py-2.5 text-white shadow-lg hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed`} style={gradientStyle}>
|
|
{isSubmitting ? 'Enviando...' : 'Confirmar Cadastro'}
|
|
<CheckIcon className="h-5 w-5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Footer com informações da agência */}
|
|
<div className="mt-8 text-center space-y-4">
|
|
<p className="text-sm text-gray-600">
|
|
Já possui cadastro?{' '}
|
|
<a href="/login" className="font-medium hover:underline" style={{ color: primaryColor }}>
|
|
Faça login aqui
|
|
</a>
|
|
</p>
|
|
|
|
{branding.name && (
|
|
<div className="pt-4 border-t border-gray-200">
|
|
<p className="text-xs text-gray-500">
|
|
Powered by <span className="font-semibold text-gray-700">{branding.name}</span>
|
|
</p>
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
Ao se cadastrar, você concorda com nossos termos de uso
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|