387 lines
22 KiB
TypeScript
387 lines
22 KiB
TypeScript
import { useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { maskCNPJ, isValidCNPJ, maskCPF, isValidCPF, maskCEP } from '../utils/validators'
|
|
|
|
// Máscara local para telefone
|
|
const maskPhone = (v: string) => v.replace(/\D/g, '').replace(/^(\d{2})(\d)/g, '($1) $2').replace(/(\d)(\d{4})$/, '$1-$2').substring(0, 15)
|
|
|
|
export function CompleteRegistrationPage() {
|
|
const navigate = useNavigate()
|
|
const [step, setStep] = useState(1) // 1: Pessoal, 2: Endereço, 3: Empresa
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
// Erros
|
|
const [cpfError, setCpfError] = useState('')
|
|
const [cnpjError, setCnpjError] = useState('')
|
|
const [cepError, setCepError] = useState('')
|
|
const [cepLoading, setCepLoading] = useState(false)
|
|
|
|
// Dados Pessoais
|
|
const [personalData, setPersonalData] = useState({
|
|
nomeCivil: '',
|
|
nomeSocial: '',
|
|
cpf: '',
|
|
})
|
|
|
|
// Endereço
|
|
const [addressData, setAddressData] = useState({
|
|
cep: '',
|
|
logradouro: '',
|
|
numero: '',
|
|
complemento: '',
|
|
bairro: '',
|
|
cidade: '',
|
|
estado: '',
|
|
})
|
|
|
|
// Empresa
|
|
const [companyData, setCompanyData] = useState({
|
|
cnpj: '',
|
|
razaoSocial: '',
|
|
nomeFantasia: '',
|
|
telefone: '',
|
|
email: '',
|
|
})
|
|
|
|
const handleBlurCPF = () => {
|
|
if (personalData.cpf && !isValidCPF(personalData.cpf)) {
|
|
setCpfError('CPF inválido')
|
|
} else {
|
|
setCpfError('')
|
|
}
|
|
}
|
|
|
|
const handleBlurCNPJ = () => {
|
|
if (companyData.cnpj && !isValidCNPJ(companyData.cnpj)) {
|
|
setCnpjError('CNPJ inválido')
|
|
} else {
|
|
setCnpjError('')
|
|
}
|
|
}
|
|
|
|
const handleBlurCEP = async () => {
|
|
const rawCep = addressData.cep.replace(/\D/g, '')
|
|
if (rawCep.length !== 8) {
|
|
// Se estiver vazio não mostra erro, só se tiver incompleto
|
|
if (rawCep.length > 0) setCepError('CEP incompleto')
|
|
return
|
|
}
|
|
|
|
setCepLoading(true)
|
|
setCepError('')
|
|
try {
|
|
const response = await fetch(`https://viacep.com.br/ws/${rawCep}/json/`)
|
|
const data = await response.json()
|
|
if (data.erro) {
|
|
setCepError('CEP não encontrado')
|
|
return
|
|
}
|
|
setAddressData(prev => ({
|
|
...prev,
|
|
logradouro: data.logradouro,
|
|
bairro: data.bairro,
|
|
cidade: data.localidade,
|
|
estado: data.uf,
|
|
// Mantém número e complemento se já digitados? Geralmente CEP preenche logs...
|
|
// Se complemento vier da API (raro em viacep genérico), preenche.
|
|
}))
|
|
} catch (err) {
|
|
setCepError('Erro ao buscar CEP')
|
|
} finally {
|
|
setCepLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleNext = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
// Validar passo atual antes de avançar
|
|
if (step === 1) {
|
|
if (cpfError || !personalData.nomeCivil || !isValidCPF(personalData.cpf)) {
|
|
if (!isValidCPF(personalData.cpf)) setCpfError('CPF inválido')
|
|
return
|
|
}
|
|
}
|
|
if (step === 2) {
|
|
if (cepError || !addressData.logradouro || !addressData.numero || !addressData.bairro || !addressData.cidade || !addressData.estado) {
|
|
return
|
|
}
|
|
}
|
|
|
|
setStep(step + 1)
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
if (cnpjError || !isValidCNPJ(companyData.cnpj)) {
|
|
if (!isValidCNPJ(companyData.cnpj)) setCnpjError('CNPJ inválido')
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
try {
|
|
// Simular envio
|
|
console.log('Dados completos:', { personalData, addressData, companyData })
|
|
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
|
|
alert('Cadastro completado com sucesso! Aguarde a aprovação.')
|
|
navigate('/login')
|
|
} catch (error) {
|
|
alert('Erro ao salvar dados.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-3xl mx-auto">
|
|
<div className="bg-white shadow-xl rounded-lg overflow-hidden">
|
|
<div className="bg-blue-600 px-6 py-4 text-white">
|
|
<h1 className="text-2xl font-bold">Completar Registro</h1>
|
|
<p className="text-blue-100 text-sm">Passo {step} de 3</p>
|
|
</div>
|
|
|
|
<div className="p-8">
|
|
{/* Progress Bar */}
|
|
<div className="mb-8 flex items-center justify-between">
|
|
<div className={`flex-1 h-2 rounded-full ${step >= 1 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
|
|
<div className="w-2"></div>
|
|
<div className={`flex-1 h-2 rounded-full ${step >= 2 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
|
|
<div className="w-2"></div>
|
|
<div className={`flex-1 h-2 rounded-full ${step >= 3 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
|
|
</div>
|
|
|
|
<form onSubmit={step === 3 ? handleSubmit : handleNext}>
|
|
|
|
{/* Step 1: Dados Pessoais */}
|
|
{step === 1 && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-xl font-semibold text-gray-800">Dados Pessoais</h2>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Nome Civil</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
value={personalData.nomeCivil}
|
|
onChange={e => setPersonalData({ ...personalData, nomeCivil: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">CPF</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-1 ${cpfError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'}`}
|
|
value={personalData.cpf}
|
|
onChange={e => {
|
|
setPersonalData({ ...personalData, cpf: maskCPF(e.target.value) })
|
|
setCpfError('')
|
|
}}
|
|
onBlur={handleBlurCPF}
|
|
placeholder="000.000.000-00"
|
|
maxLength={14}
|
|
/>
|
|
{cpfError && <p className="mt-1 text-xs text-red-500">{cpfError}</p>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Endereço */}
|
|
{step === 2 && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-xl font-semibold text-gray-800">Endereço</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700">CEP</label>
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
required
|
|
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-1 ${cepError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'}`}
|
|
value={addressData.cep}
|
|
onChange={e => {
|
|
setAddressData({ ...addressData, cep: maskCEP(e.target.value) })
|
|
setCepError('')
|
|
}}
|
|
onBlur={handleBlurCEP}
|
|
placeholder="00000-000"
|
|
maxLength={9}
|
|
/>
|
|
{cepLoading && (
|
|
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{cepError && <p className="mt-1 text-xs text-red-500">{cepError}</p>}
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700">Logradouro</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50 bg-opacity-50"
|
|
value={addressData.logradouro}
|
|
onChange={e => setAddressData({ ...addressData, logradouro: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Número</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
value={addressData.numero}
|
|
onChange={e => setAddressData({ ...addressData, numero: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Complemento</label>
|
|
<input
|
|
type="text"
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
value={addressData.complemento}
|
|
onChange={e => setAddressData({ ...addressData, complemento: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Bairro</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50 bg-opacity-50"
|
|
value={addressData.bairro}
|
|
onChange={e => setAddressData({ ...addressData, bairro: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Cidade</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50 bg-opacity-50"
|
|
value={addressData.cidade}
|
|
onChange={e => setAddressData({ ...addressData, cidade: e.target.value })}
|
|
/>
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-20 rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50 bg-opacity-50 text-center"
|
|
value={addressData.estado}
|
|
placeholder="UF"
|
|
onChange={e => setAddressData({ ...addressData, estado: e.target.value })}
|
|
maxLength={2}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Empresa */}
|
|
{step === 3 && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-xl font-semibold text-gray-800">Dados da Empresa</h2>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">CNPJ</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-1 ${cnpjError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'}`}
|
|
value={companyData.cnpj}
|
|
onChange={e => {
|
|
setCompanyData({ ...companyData, cnpj: maskCNPJ(e.target.value) })
|
|
setCnpjError('')
|
|
}}
|
|
onBlur={handleBlurCNPJ}
|
|
placeholder="00.000.000/0000-00"
|
|
maxLength={18}
|
|
/>
|
|
{cnpjError && <p className="mt-1 text-xs text-red-500">{cnpjError}</p>}
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Razão Social</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
value={companyData.razaoSocial}
|
|
onChange={e => setCompanyData({ ...companyData, razaoSocial: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Nome Fantasia</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
value={companyData.nomeFantasia}
|
|
onChange={e => setCompanyData({ ...companyData, nomeFantasia: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Telefone</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
value={companyData.telefone}
|
|
onChange={e => setCompanyData({ ...companyData, telefone: maskPhone(e.target.value) })}
|
|
placeholder="(00) 00000-0000"
|
|
maxLength={15}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Email da Empresa</label>
|
|
<input
|
|
type="email"
|
|
required
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
value={companyData.email}
|
|
onChange={e => setCompanyData({ ...companyData, email: e.target.value })}
|
|
placeholder="contato@empresa.com"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-8 flex justify-between">
|
|
{step > 1 ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep(step - 1)}
|
|
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
>
|
|
Voltar
|
|
</button>
|
|
) : <div></div>}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
{loading ? 'Salvando...' : step === 3 ? 'Finalizar Cadastro' : 'Próximo'}
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{/* Botão de Skip para teste (remover em prod) */}
|
|
<div className="text-center mt-4">
|
|
<button onClick={() => navigate('/login')} className="text-xs text-gray-400 hover:text-gray-600">
|
|
Cancelar e voltar ao login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|