developer #3
@ -1,17 +1,439 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { credenciamentoService } from '@/services/credenciamentoService'
|
||||
import DocumentoInput from '@/components/auth/DocumentoInput.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// ─── Wizard state ─────────────────────────────────────────────────────────────
|
||||
const etapa = ref(0)
|
||||
// 0 Documento | 1 Dados Pessoais | 2 Endereço | 3 Contato | 4 Representante | 5 Revisão | 6 Sucesso
|
||||
|
||||
const carregando = ref(false)
|
||||
const erro = ref('')
|
||||
|
||||
// ─── Dados por etapa ──────────────────────────────────────────────────────────
|
||||
const form = ref({
|
||||
// Etapa 0
|
||||
documento: '',
|
||||
tipoPessoa: '', // FISICA | JURIDICA
|
||||
|
||||
// Etapa 1 — dados pessoais/empresariais
|
||||
nomeCompleto: '',
|
||||
nomeFantasia: '', // só PJ
|
||||
dataNascimento: '',
|
||||
inscricaoEstadual: '', // só PJ
|
||||
|
||||
// Etapa 2 — endereço
|
||||
cep: '',
|
||||
logradouro: '',
|
||||
numero: '',
|
||||
complemento: '',
|
||||
bairro: '',
|
||||
cidade: '',
|
||||
uf: '',
|
||||
|
||||
// Etapa 3 — contato
|
||||
email: '',
|
||||
emailConfirm: '',
|
||||
telefone: '',
|
||||
whatsapp: false,
|
||||
|
||||
// Etapa 4 — representante legal (só PJ)
|
||||
representanteNome: '',
|
||||
representanteCpf: '',
|
||||
representanteCargo: '',
|
||||
})
|
||||
|
||||
// ─── Computeds ───────────────────────────────────────────────────────────────
|
||||
const docDigitos = computed(() => form.value.documento.replace(/\D/g, ''))
|
||||
const isPJ = computed(() => form.value.tipoPessoa === 'JURIDICA')
|
||||
|
||||
const totalEtapas = computed(() => isPJ.value ? 6 : 5)
|
||||
|
||||
const etapaLabels = computed(() => {
|
||||
const base = ['Documento', 'Dados', 'Endereço', 'Contato']
|
||||
if (isPJ.value) base.push('Representante')
|
||||
base.push('Revisão')
|
||||
return base
|
||||
})
|
||||
|
||||
const emailsIguais = computed(() => form.value.email === form.value.emailConfirm)
|
||||
const emailValido = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.email))
|
||||
|
||||
// ─── Etapa 0 — verificar documento ───────────────────────────────────────────
|
||||
async function verificarDocumento() {
|
||||
const d = docDigitos.value
|
||||
if (d.length !== 11 && d.length !== 14) return
|
||||
carregando.value = true
|
||||
erro.value = ''
|
||||
try {
|
||||
const { data } = await credenciamentoService.verificarDocumento(d)
|
||||
if (data.data.situacao === 'JA_CREDENCIADO') {
|
||||
erro.value = 'Este documento já possui cadastro. Use "Esqueci minha senha" se precisar recuperar o acesso.'
|
||||
return
|
||||
}
|
||||
form.value.tipoPessoa = d.length === 14 ? 'JURIDICA' : 'FISICA'
|
||||
etapa.value = 1
|
||||
} catch (e) {
|
||||
erro.value = e.response?.data?.description ?? 'Não foi possível verificar o documento. Tente novamente.'
|
||||
} finally {
|
||||
carregando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Etapa 2 — busca de CEP ───────────────────────────────────────────────────
|
||||
const buscandoCep = ref(false)
|
||||
async function buscarCep() {
|
||||
const cep = form.value.cep.replace(/\D/g, '')
|
||||
if (cep.length !== 8) return
|
||||
buscandoCep.value = true
|
||||
try {
|
||||
const { data } = await credenciamentoService.buscarCep(cep)
|
||||
const end = data.data
|
||||
form.value.logradouro = end.logradouro ?? ''
|
||||
form.value.bairro = end.bairro ?? ''
|
||||
form.value.cidade = end.localidade ?? ''
|
||||
form.value.uf = end.uf ?? ''
|
||||
} catch {
|
||||
// silencioso — usuário preenche manualmente
|
||||
} finally {
|
||||
buscandoCep.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => form.value.cep, (v) => {
|
||||
if (v.replace(/\D/g, '').length === 8) buscarCep()
|
||||
})
|
||||
|
||||
// ─── Navegação ────────────────────────────────────────────────────────────────
|
||||
function avancar() {
|
||||
erro.value = ''
|
||||
etapa.value++
|
||||
}
|
||||
|
||||
function voltar() {
|
||||
erro.value = ''
|
||||
etapa.value--
|
||||
}
|
||||
|
||||
// ─── Submissão final ──────────────────────────────────────────────────────────
|
||||
async function solicitar() {
|
||||
carregando.value = true
|
||||
erro.value = ''
|
||||
try {
|
||||
await credenciamentoService.solicitar({
|
||||
documento: docDigitos.value,
|
||||
tipoPessoa: form.value.tipoPessoa,
|
||||
nomeCompleto: form.value.nomeCompleto,
|
||||
nomeFantasia: form.value.nomeFantasia || undefined,
|
||||
dataNascimento: form.value.dataNascimento || undefined,
|
||||
inscricaoEstadual: form.value.inscricaoEstadual || undefined,
|
||||
endereco: {
|
||||
cep: form.value.cep.replace(/\D/g, ''),
|
||||
logradouro: form.value.logradouro,
|
||||
numero: form.value.numero,
|
||||
complemento: form.value.complemento || undefined,
|
||||
bairro: form.value.bairro,
|
||||
cidade: form.value.cidade,
|
||||
uf: form.value.uf,
|
||||
},
|
||||
contato: {
|
||||
email: form.value.email,
|
||||
telefone: form.value.telefone.replace(/\D/g, ''),
|
||||
whatsapp: form.value.whatsapp,
|
||||
},
|
||||
representante: isPJ.value ? {
|
||||
nome: form.value.representanteNome,
|
||||
cpf: form.value.representanteCpf.replace(/\D/g, ''),
|
||||
cargo: form.value.representanteCargo,
|
||||
} : undefined,
|
||||
})
|
||||
const ultimaEtapa = isPJ.value ? 6 : 5
|
||||
etapa.value = ultimaEtapa
|
||||
} catch (e) {
|
||||
erro.value = e.response?.data?.description ?? 'Erro ao enviar solicitação. Tente novamente.'
|
||||
} finally {
|
||||
carregando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatarTelefone(e) {
|
||||
const d = e.target.value.replace(/\D/g, '').slice(0, 11)
|
||||
form.value.telefone = d
|
||||
.replace(/(\d{2})(\d)/, '($1) $2')
|
||||
.replace(/(\d{5})(\d{1,4})$/, '$1-$2')
|
||||
}
|
||||
|
||||
function formatarCep(e) {
|
||||
const d = e.target.value.replace(/\D/g, '').slice(0, 8)
|
||||
form.value.cep = d.replace(/(\d{5})(\d{1,3})/, '$1-$2')
|
||||
}
|
||||
|
||||
const estados = ['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-2xl mx-auto px-4 py-16 text-center">
|
||||
<div class="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<i class="pi pi-user-plus text-green-700 text-2xl" />
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-slate-800 mb-3">Credenciamento</h1>
|
||||
<p class="text-slate-500 mb-8">
|
||||
Wizard de cadastro do contribuinte (6 etapas) — a ser implementado.
|
||||
<div class="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-lg">
|
||||
|
||||
<!-- Barra de progresso -->
|
||||
<div v-if="etapa < totalEtapas" class="mb-8">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">
|
||||
Passo {{ etapa + 1 }} de {{ totalEtapas }}
|
||||
</p>
|
||||
<RouterLink :to="{ name: 'home' }">
|
||||
<Button label="Voltar ao início" severity="secondary" icon="pi pi-arrow-left" outlined />
|
||||
</RouterLink>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">{{ etapaLabels[etapa] }}</p>
|
||||
</div>
|
||||
<div class="h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-primary rounded-full transition-all duration-300"
|
||||
:style="{ width: `${((etapa + 1) / totalEtapas) * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="bg-gradient-to-r from-primary to-primary-700 px-8 py-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center" aria-hidden="true">
|
||||
<i class="pi pi-user-plus text-white text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white font-bold text-base">Credenciamento</h1>
|
||||
<p class="text-white/80 text-xs mt-0.5">
|
||||
{{ etapa < totalEtapas ? etapaLabels[etapa] : 'Solicitação enviada!' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-8 py-8 space-y-5">
|
||||
|
||||
<!-- ── ETAPA 0 — DOCUMENTO ───────────────────────────── -->
|
||||
<template v-if="etapa === 0">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
Informe seu CPF (pessoa física) ou CNPJ (empresa) para iniciar o credenciamento.
|
||||
</p>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">CPF ou CNPJ</label>
|
||||
<DocumentoInput v-model="form.documento" @keyup.enter="verificarDocumento" />
|
||||
</div>
|
||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||
</p>
|
||||
<Button
|
||||
label="Verificar documento"
|
||||
icon="pi pi-arrow-right" icon-pos="right"
|
||||
class="w-full" size="large"
|
||||
:loading="carregando"
|
||||
:disabled="docDigitos.length !== 11 && docDigitos.length !== 14"
|
||||
@click="verificarDocumento"
|
||||
/>
|
||||
<p class="text-center text-sm text-slate-500 dark:text-slate-400">
|
||||
Já tem cadastro?
|
||||
<RouterLink :to="{ name: 'home' }" class="text-primary font-semibold hover:underline">Entrar</RouterLink>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- ── ETAPA 1 — DADOS PESSOAIS ──────────────────────── -->
|
||||
<template v-else-if="etapa === 1">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">
|
||||
{{ isPJ ? 'Razão Social' : 'Nome completo' }}
|
||||
</label>
|
||||
<InputText v-model="form.nomeCompleto" :placeholder="isPJ ? 'Nome da empresa conforme CNPJ' : 'Seu nome completo'" class="w-full" size="large" />
|
||||
</div>
|
||||
<div v-if="isPJ">
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Nome fantasia <span class="font-normal text-slate-400">(opcional)</span></label>
|
||||
<InputText v-model="form.nomeFantasia" placeholder="Nome fantasia" class="w-full" size="large" />
|
||||
</div>
|
||||
<div v-if="!isPJ">
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Data de nascimento</label>
|
||||
<DatePicker v-model="form.dataNascimento" date-format="dd/mm/yy" placeholder="DD/MM/AAAA" class="w-full" size="large" show-icon />
|
||||
</div>
|
||||
<div v-if="isPJ">
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Inscrição estadual <span class="font-normal text-slate-400">(opcional)</span></label>
|
||||
<InputText v-model="form.inscricaoEstadual" placeholder="IE ou ISENTO" class="w-full" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<Button label="Voltar" severity="secondary" outlined class="flex-1" @click="voltar" />
|
||||
<Button label="Continuar" icon="pi pi-arrow-right" icon-pos="right" class="flex-1" :disabled="!form.nomeCompleto.trim()" @click="avancar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ── ETAPA 2 — ENDEREÇO ────────────────────────────── -->
|
||||
<template v-else-if="etapa === 2">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">CEP</label>
|
||||
<div class="relative">
|
||||
<InputText :value="form.cep" placeholder="00000-000" class="w-full" size="large" @input="formatarCep" />
|
||||
<i v-if="buscandoCep" class="pi pi-spin pi-spinner absolute right-3 top-1/2 -translate-y-1/2 text-slate-400" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="col-span-2">
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Logradouro</label>
|
||||
<InputText v-model="form.logradouro" placeholder="Rua, Av., ..." class="w-full" size="large" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Número</label>
|
||||
<InputText v-model="form.numero" placeholder="Nº" class="w-full" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Complemento <span class="font-normal text-slate-400">(opcional)</span></label>
|
||||
<InputText v-model="form.complemento" placeholder="Apto, sala, bloco..." class="w-full" size="large" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Bairro</label>
|
||||
<InputText v-model="form.bairro" class="w-full" size="large" />
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="col-span-2">
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Cidade</label>
|
||||
<InputText v-model="form.cidade" class="w-full" size="large" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">UF</label>
|
||||
<Select v-model="form.uf" :options="estados" placeholder="UF" class="w-full" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<Button label="Voltar" severity="secondary" outlined class="flex-1" @click="voltar" />
|
||||
<Button label="Continuar" icon="pi pi-arrow-right" icon-pos="right" class="flex-1"
|
||||
:disabled="!form.logradouro || !form.numero || !form.bairro || !form.cidade || !form.uf"
|
||||
@click="avancar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ── ETAPA 3 — CONTATO ─────────────────────────────── -->
|
||||
<template v-else-if="etapa === 3">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">E-mail</label>
|
||||
<InputText v-model="form.email" type="email" placeholder="seu@email.com" class="w-full" size="large" :invalid="form.email.length > 0 && !emailValido" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Confirmar e-mail</label>
|
||||
<InputText v-model="form.emailConfirm" type="email" placeholder="Repita o e-mail" class="w-full" size="large" :invalid="form.emailConfirm.length > 0 && !emailsIguais" />
|
||||
<p v-if="form.emailConfirm.length > 0 && !emailsIguais" class="mt-1 text-xs text-red-600 dark:text-red-400">Os e-mails não coincidem.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Telefone / Celular</label>
|
||||
<InputText :value="form.telefone" placeholder="(00) 00000-0000" class="w-full" size="large" inputmode="numeric" @input="formatarTelefone" />
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-4 bg-slate-50 dark:bg-slate-700/50 rounded-xl">
|
||||
<Checkbox v-model="form.whatsapp" :binary="true" input-id="whatsapp" />
|
||||
<label for="whatsapp" class="text-sm text-slate-700 dark:text-slate-200 cursor-pointer">
|
||||
Este número também recebe WhatsApp
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||
</p>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<Button label="Voltar" severity="secondary" outlined class="flex-1" @click="voltar" />
|
||||
<Button label="Continuar" icon="pi pi-arrow-right" icon-pos="right" class="flex-1"
|
||||
:disabled="!emailValido || !emailsIguais || form.telefone.replace(/\D/g,'').length < 10"
|
||||
@click="avancar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ── ETAPA 4 — REPRESENTANTE LEGAL (só PJ) ──────────── -->
|
||||
<template v-else-if="etapa === 4 && isPJ">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
Informe os dados do representante legal da empresa que será responsável pelo acesso ao portal.
|
||||
</p>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Nome completo</label>
|
||||
<InputText v-model="form.representanteNome" placeholder="Nome do representante" class="w-full" size="large" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">CPF</label>
|
||||
<DocumentoInput v-model="form.representanteCpf" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Cargo / Função</label>
|
||||
<InputText v-model="form.representanteCargo" placeholder="Ex.: Sócio-Administrador, Diretor..." class="w-full" size="large" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<Button label="Voltar" severity="secondary" outlined class="flex-1" @click="voltar" />
|
||||
<Button label="Continuar" icon="pi pi-arrow-right" icon-pos="right" class="flex-1"
|
||||
:disabled="!form.representanteNome || form.representanteCpf.replace(/\D/g,'').length !== 11 || !form.representanteCargo"
|
||||
@click="avancar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ── REVISÃO ──────────────────────────────────────────── -->
|
||||
<template v-else-if="(isPJ && etapa === 5) || (!isPJ && etapa === 4)">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300 mb-2">Confira os dados antes de enviar a solicitação.</p>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-xl p-4 space-y-2">
|
||||
<p class="font-semibold text-slate-500 dark:text-slate-400 text-xs uppercase tracking-wide">Identificação</p>
|
||||
<p class="text-slate-800 dark:text-slate-100"><span class="text-slate-500 dark:text-slate-400">Documento:</span> {{ form.documento }}</p>
|
||||
<p class="text-slate-800 dark:text-slate-100"><span class="text-slate-500 dark:text-slate-400">Nome:</span> {{ form.nomeCompleto }}</p>
|
||||
</div>
|
||||
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-xl p-4 space-y-2">
|
||||
<p class="font-semibold text-slate-500 dark:text-slate-400 text-xs uppercase tracking-wide">Endereço</p>
|
||||
<p class="text-slate-800 dark:text-slate-100">{{ form.logradouro }}, {{ form.numero }}{{ form.complemento ? ` — ${form.complemento}` : '' }}</p>
|
||||
<p class="text-slate-800 dark:text-slate-100">{{ form.bairro }} — {{ form.cidade }}/{{ form.uf }} — CEP {{ form.cep }}</p>
|
||||
</div>
|
||||
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-xl p-4 space-y-2">
|
||||
<p class="font-semibold text-slate-500 dark:text-slate-400 text-xs uppercase tracking-wide">Contato</p>
|
||||
<p class="text-slate-800 dark:text-slate-100">{{ form.email }}</p>
|
||||
<p class="text-slate-800 dark:text-slate-100">{{ form.telefone }}{{ form.whatsapp ? ' (WhatsApp)' : '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||
</p>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<Button label="Voltar" severity="secondary" outlined class="flex-1" @click="voltar" />
|
||||
<Button label="Enviar solicitação" icon="pi pi-send" class="flex-1" :loading="carregando" @click="solicitar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ── SUCESSO ─────────────────────────────────────────── -->
|
||||
<template v-else>
|
||||
<div class="text-center py-4">
|
||||
<div class="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/30 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<i class="pi pi-check-circle text-emerald-600 dark:text-emerald-400 text-3xl" aria-hidden="true" />
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-slate-800 dark:text-slate-100 mb-2">Solicitação enviada!</h2>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
Sua solicitação de credenciamento foi recebida e será analisada em até
|
||||
<strong class="text-slate-700 dark:text-slate-300">2 dias úteis</strong>.
|
||||
Você receberá um e-mail em <strong class="text-slate-700 dark:text-slate-300">{{ form.email }}</strong> com o resultado.
|
||||
</p>
|
||||
</div>
|
||||
<Button label="Voltar à página inicial" icon="pi pi-home" class="w-full" size="large" @click="router.push({ name: 'home' })" />
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link voltar para home -->
|
||||
<div v-if="etapa === 0" class="text-center mt-4">
|
||||
<button
|
||||
class="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors py-3 px-4 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
@click="router.push({ name: 'home' })"
|
||||
>
|
||||
<i class="pi pi-arrow-left text-xs" aria-hidden="true" />
|
||||
Voltar à página inicial
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,17 +1,262 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { primeiroAcessoService } from '@/services/primeiroAcessoService'
|
||||
import DocumentoInput from '@/components/auth/DocumentoInput.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// ─── Estado do wizard ─────────────────────────────────────────────────────────
|
||||
const etapa = ref(0) // 0 identificação | 1 canal | 2 código | 3 senha | 4 sucesso
|
||||
const carregando = ref(false)
|
||||
const erro = ref('')
|
||||
|
||||
// Etapa 0 — identificação
|
||||
const documento = ref('')
|
||||
const contribuinteNome = ref('')
|
||||
|
||||
// Etapa 1 — canal de envio do código
|
||||
const canais = ref([]) // [{ tipo: 'EMAIL', valor: 'jo**@gmail.com' }, ...]
|
||||
const canalSelecionado = ref(null)
|
||||
|
||||
// Etapa 2 — código de verificação
|
||||
const codigo = ref('')
|
||||
const tokenValidacao = ref('')
|
||||
|
||||
// Etapa 3 — senha
|
||||
const senha = ref('')
|
||||
const senhaConfirm = ref('')
|
||||
|
||||
const docDigitos = computed(() => documento.value.replace(/\D/g, ''))
|
||||
const docValido = computed(() => docDigitos.value.length === 11 || docDigitos.value.length === 14)
|
||||
|
||||
const senhaForte = computed(() => senha.value.length >= 8)
|
||||
const senhasIguais = computed(() => senha.value === senhaConfirm.value)
|
||||
|
||||
// ─── Ações ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function identificar() {
|
||||
if (!docValido.value) return
|
||||
carregando.value = true
|
||||
erro.value = ''
|
||||
try {
|
||||
const { data } = await primeiroAcessoService.verificarDocumento(docDigitos.value)
|
||||
contribuinteNome.value = data.data.nome
|
||||
canais.value = data.data.canais
|
||||
canalSelecionado.value = canais.value[0] ?? null
|
||||
etapa.value = 1
|
||||
} catch (e) {
|
||||
erro.value = e.response?.data?.description ?? 'Documento não encontrado ou não habilitado para este fluxo.'
|
||||
} finally {
|
||||
carregando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function enviarCodigo() {
|
||||
if (!canalSelecionado.value) return
|
||||
carregando.value = true
|
||||
erro.value = ''
|
||||
try {
|
||||
await primeiroAcessoService.solicitarCodigo(docDigitos.value, canalSelecionado.value.tipo)
|
||||
etapa.value = 2
|
||||
} catch (e) {
|
||||
erro.value = e.response?.data?.description ?? 'Não foi possível enviar o código. Tente novamente.'
|
||||
} finally {
|
||||
carregando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function validarCodigo() {
|
||||
if (codigo.value.length < 6) return
|
||||
carregando.value = true
|
||||
erro.value = ''
|
||||
try {
|
||||
const { data } = await primeiroAcessoService.validarCodigo(docDigitos.value, codigo.value)
|
||||
tokenValidacao.value = data.data.token
|
||||
etapa.value = 3
|
||||
} catch (e) {
|
||||
erro.value = e.response?.data?.description ?? 'Código inválido ou expirado. Solicite um novo.'
|
||||
} finally {
|
||||
carregando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function definirSenha() {
|
||||
if (!senhaForte.value || !senhasIguais.value) return
|
||||
carregando.value = true
|
||||
erro.value = ''
|
||||
try {
|
||||
await primeiroAcessoService.definirSenha(tokenValidacao.value, senha.value)
|
||||
etapa.value = 4
|
||||
} catch (e) {
|
||||
erro.value = e.response?.data?.description ?? 'Não foi possível definir a senha. Tente novamente.'
|
||||
} finally {
|
||||
carregando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const iconeCanal = { EMAIL: 'pi-envelope', SMS: 'pi-mobile', WHATSAPP: 'pi-whatsapp' }
|
||||
const labelCanal = { EMAIL: 'E-mail', SMS: 'SMS', WHATSAPP: 'WhatsApp' }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-2xl mx-auto px-4 py-16 text-center">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<i class="pi pi-key text-blue-700 text-2xl" />
|
||||
<div class="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
|
||||
<!-- Progresso -->
|
||||
<div v-if="etapa < 4" class="flex items-center justify-center gap-2 mb-8">
|
||||
<div
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:class="[
|
||||
i - 1 <= etapa ? 'bg-primary' : 'bg-slate-200 dark:bg-slate-700',
|
||||
i - 1 === etapa ? 'w-8' : 'w-4',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-slate-800 mb-3">Primeiro Acesso / Esqueci minha senha</h1>
|
||||
<p class="text-slate-500 mb-8">
|
||||
Fluxo de criação e recuperação de senha — a ser implementado.
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="bg-gradient-to-r from-primary to-primary-700 px-8 py-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center" aria-hidden="true">
|
||||
<i class="pi pi-key text-white text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white font-bold text-base leading-tight">Primeiro Acesso</h1>
|
||||
<p class="text-white/80 text-xs mt-0.5">
|
||||
{{ ['Identificação', 'Canal de envio', 'Código de verificação', 'Crie sua senha', 'Pronto!'][etapa] }}
|
||||
</p>
|
||||
<RouterLink :to="{ name: 'home' }">
|
||||
<Button label="Voltar ao início" severity="secondary" icon="pi pi-arrow-left" outlined />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-8 py-8 space-y-6">
|
||||
|
||||
<!-- ETAPA 0 — Identificação -->
|
||||
<template v-if="etapa === 0">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
Informe seu CPF ou CNPJ para verificarmos seu cadastro e iniciar a criação de senha.
|
||||
</p>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">CPF ou CNPJ</label>
|
||||
<DocumentoInput v-model="documento" @keyup.enter="identificar" />
|
||||
</div>
|
||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||
</p>
|
||||
<Button label="Continuar" icon="pi pi-arrow-right" icon-pos="right" class="w-full" size="large" :loading="carregando" :disabled="!docValido" @click="identificar" />
|
||||
</template>
|
||||
|
||||
<!-- ETAPA 1 — Canal -->
|
||||
<template v-else-if="etapa === 1">
|
||||
<div>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300 mb-1">Olá, <strong class="text-slate-800 dark:text-slate-100">{{ contribuinteNome }}</strong>.</p>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
Enviaremos um código de verificação. Escolha como prefere receber:
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="canal in canais"
|
||||
:key="canal.tipo"
|
||||
class="w-full flex items-center gap-3 p-4 rounded-xl border transition-colors text-left"
|
||||
:class="canalSelecionado?.tipo === canal.tipo
|
||||
? 'border-primary bg-primary/5 dark:bg-primary/10'
|
||||
: 'border-slate-200 dark:border-slate-600 hover:border-slate-300 dark:hover:border-slate-500'"
|
||||
@click="canalSelecionado = canal"
|
||||
>
|
||||
<div class="w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
:class="canalSelecionado?.tipo === canal.tipo ? 'bg-primary text-white' : 'bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400'">
|
||||
<i :class="['pi', iconeCanal[canal.tipo] ?? 'pi-send', 'text-sm']" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-slate-800 dark:text-slate-100 text-sm">{{ labelCanal[canal.tipo] ?? canal.tipo }}</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">{{ canal.valor }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<Button label="Voltar" severity="secondary" outlined class="flex-1" @click="etapa = 0" />
|
||||
<Button label="Enviar código" icon="pi pi-send" class="flex-1" :loading="carregando" :disabled="!canalSelecionado" @click="enviarCodigo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ETAPA 2 — Código -->
|
||||
<template v-else-if="etapa === 2">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
Enviamos um código de 6 dígitos para
|
||||
<strong class="text-slate-800 dark:text-slate-100">{{ canalSelecionado?.valor }}</strong>.
|
||||
Ele expira em 10 minutos.
|
||||
</p>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Código de verificação</label>
|
||||
<InputOtp v-model="codigo" :length="6" class="justify-center gap-2" integer-only @keyup.enter="validarCodigo" />
|
||||
</div>
|
||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||
</p>
|
||||
<Button label="Verificar código" class="w-full" size="large" :loading="carregando" :disabled="codigo.length < 6" @click="validarCodigo" />
|
||||
<button class="w-full text-center text-sm text-primary hover:underline" @click="enviarCodigo">
|
||||
Não recebi o código — reenviar
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- ETAPA 3 — Senha -->
|
||||
<template v-else-if="etapa === 3">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
Crie uma senha segura com pelo menos 8 caracteres.
|
||||
</p>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Nova senha</label>
|
||||
<Password v-model="senha" :feedback="true" toggle-mask placeholder="Mínimo 8 caracteres" class="w-full" input-class="w-full" size="large" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Confirmar senha</label>
|
||||
<Password v-model="senhaConfirm" :feedback="false" toggle-mask placeholder="Repita a senha" class="w-full" input-class="w-full" size="large" :invalid="senhaConfirm.length > 0 && !senhasIguais" />
|
||||
<p v-if="senhaConfirm.length > 0 && !senhasIguais" class="mt-1 text-xs text-red-600 dark:text-red-400">As senhas não coincidem.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||
</p>
|
||||
<Button label="Criar senha" icon="pi pi-check" class="w-full" size="large" :loading="carregando" :disabled="!senhaForte || !senhasIguais" @click="definirSenha" />
|
||||
</template>
|
||||
|
||||
<!-- ETAPA 4 — Sucesso -->
|
||||
<template v-else>
|
||||
<div class="text-center py-4">
|
||||
<div class="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/30 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<i class="pi pi-check-circle text-emerald-600 dark:text-emerald-400 text-3xl" aria-hidden="true" />
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-slate-800 dark:text-slate-100 mb-2">Senha criada com sucesso!</h2>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
Você já pode acessar o portal com seu CPF/CNPJ e a nova senha.
|
||||
</p>
|
||||
</div>
|
||||
<Button label="Ir para o login" icon="pi pi-sign-in" class="w-full" size="large" @click="router.push({ name: 'login', query: { doc: docDigitos } })" />
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link voltar -->
|
||||
<div v-if="etapa < 4" class="text-center mt-4">
|
||||
<button
|
||||
class="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors py-3 px-4 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
@click="router.push({ name: 'home' })"
|
||||
>
|
||||
<i class="pi pi-arrow-left text-xs" aria-hidden="true" />
|
||||
Voltar à página inicial
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user