saveinmed/marketplace/src/pages/CompleteRegistrationPage.tsx
2026-02-19 13:23:41 -03:00

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>
)
}