Remove src/pages/login.vue e atualiza index.vue e primeiro-acesso.vue para chamar login() diretamente com login_hint pré-preenchido, eliminando o passo desnecessário antes do redirect ao Keycloak. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
391 lines
20 KiB
Vue
391 lines
20 KiB
Vue
<script setup>
|
|
import { ref, computed, watch, nextTick } from 'vue'
|
|
import { usePrefeituraStore } from '@/stores/prefeituraStore'
|
|
import { useAuth } from '@/composables/useAuth'
|
|
import { useFocusLoginInput } from '@/composables/useFocusLoginInput'
|
|
import { useMotion } from '@/composables/useMotion'
|
|
|
|
import bgTutoia from '@/assets/images/bg-tutoia.jpeg'
|
|
|
|
const { prefersReducedMotion } = useMotion()
|
|
|
|
const router = useRouter()
|
|
const prefeitura = usePrefeituraStore()
|
|
const { isAuthenticated, nomeUsuario, login } = useAuth()
|
|
const { requested: focusLoginRequested, consume: consumeFocusLogin } = useFocusLoginInput()
|
|
|
|
const documento = ref('')
|
|
const erro = ref('')
|
|
const carregando = ref(false)
|
|
|
|
// Ref ao DocumentoInput — usado pelo botão "Entrar" do AppHeader pra focar o campo
|
|
const documentoRef = ref(null)
|
|
|
|
// Quando o AppHeader sinaliza intenção de login (clique no botão Entrar),
|
|
// foca o campo + scroll suave pra ele.
|
|
watch(focusLoginRequested, async (v) => {
|
|
if (!v || isAuthenticated.value) {
|
|
if (v) consumeFocusLogin()
|
|
return
|
|
}
|
|
await nextTick()
|
|
const el = documentoRef.value?.$el ?? documentoRef.value
|
|
el?.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
|
|
documentoRef.value?.focus?.()
|
|
consumeFocusLogin()
|
|
}, { immediate: true })
|
|
|
|
const heroBgMap = {
|
|
tutoia: bgTutoia,
|
|
}
|
|
|
|
const heroBgUrl = computed(() => heroBgMap[prefeitura.template] ?? null)
|
|
|
|
const heroBgStyle = computed(() => {
|
|
const url = heroBgUrl.value
|
|
if (!url) return {}
|
|
return {
|
|
backgroundImage: `linear-gradient(to bottom right, rgba(15,23,42,0.70), rgba(15,23,42,0.50)), url(${url})`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center',
|
|
}
|
|
})
|
|
|
|
const heroHasPhoto = computed(() => !!heroBgUrl.value)
|
|
|
|
// Dados mockados — conectar ao endpoint /publico/avisos/{dominio} futuramente
|
|
const avisos = ref([
|
|
{
|
|
id: 1,
|
|
tipo: 'prazo',
|
|
icone: 'pi-calendar',
|
|
titulo: 'IPTU 2025 — Parcela única com desconto',
|
|
descricao: 'Pague até 31/07 e ganhe 10% de desconto. Emita seu boleto agora mesmo.',
|
|
cor: 'amber',
|
|
acao: { label: 'Emitir boleto', to: '/servicos/iptu' },
|
|
},
|
|
{
|
|
id: 2,
|
|
tipo: 'novidade',
|
|
icone: 'pi-star',
|
|
titulo: 'Novo serviço: Certidão Online Instantânea',
|
|
descricao: 'Emita certidões negativas em segundos, sem sair de casa e com validade legal.',
|
|
cor: 'green',
|
|
acao: { label: 'Emitir agora', to: '/servicos/certidao' },
|
|
},
|
|
{
|
|
id: 3,
|
|
tipo: 'info',
|
|
icone: 'pi-info-circle',
|
|
titulo: 'Atualização cadastral obrigatória',
|
|
descricao: 'Contribuintes com dados desatualizados devem regularizar até 30/08 para evitar multas.',
|
|
cor: 'blue',
|
|
acao: null,
|
|
},
|
|
])
|
|
|
|
const corAviso = {
|
|
amber: { bg: 'bg-amber-50 dark:bg-amber-900/20', borda: 'border-amber-200 dark:border-amber-700/40', icone: 'text-amber-600 dark:text-amber-400', tag: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300' },
|
|
green: { bg: 'bg-emerald-50 dark:bg-emerald-900/20', borda: 'border-emerald-200 dark:border-emerald-700/40', icone: 'text-emerald-600 dark:text-emerald-400', tag: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' },
|
|
blue: { bg: 'bg-blue-50 dark:bg-blue-900/20', borda: 'border-blue-200 dark:border-blue-700/40', icone: 'text-blue-600 dark:text-blue-400', tag: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' },
|
|
}
|
|
|
|
const servicosPublicos = [
|
|
{ icon: 'pi-file-check', titulo: 'Emissão de Certidão', descricao: 'Certidão negativa, positiva e situação fiscal.', to: '/servicos/certidao' },
|
|
{ icon: 'pi-home', titulo: 'IPTU — Débitos e Carnê', descricao: 'Consulte débitos e imprima o carnê de pagamento.', to: '/servicos/iptu' },
|
|
{ icon: 'pi-verified', titulo: 'Validar Documento', descricao: 'Verifique a autenticidade de certidões emitidas.', to: '/servicos' },
|
|
]
|
|
|
|
const servicosAutenticados = [
|
|
{ icon: 'pi-receipt', titulo: 'Débitos e Guias', descricao: 'Emita guias de pagamento de todos os seus tributos.', to: '/portal/debitos' },
|
|
{ icon: 'pi-file-edit', titulo: 'Certidões Ativas', descricao: 'Acesse e reemita certidões emitidas anteriormente.', to: '/portal/certidoes' },
|
|
{ icon: 'pi-briefcase', titulo: 'Alvarás', descricao: 'Acompanhe processos e solicite novos alvarás.', to: '/portal/alvaras' },
|
|
{ icon: 'pi-credit-card', titulo: 'Pagamentos', descricao: 'Histórico completo com comprovantes para download.', to: '/portal/pagamentos' },
|
|
{ icon: 'pi-user', titulo: 'Dados Cadastrais', descricao: 'Visualize e mantenha seus dados sempre atualizados.', to: '/portal/dados' },
|
|
]
|
|
|
|
async function continuar() {
|
|
const doc = documento.value.replace(/\D/g, '')
|
|
if (doc.length < 11) {
|
|
erro.value = 'Informe um CPF (11 dígitos) ou CNPJ (14 dígitos) válido.'
|
|
return
|
|
}
|
|
erro.value = ''
|
|
carregando.value = true
|
|
try {
|
|
await login(doc, '/portal/painel')
|
|
// login() faz window.location → não retorna aqui em condições normais
|
|
} catch (e) {
|
|
carregando.value = false
|
|
erro.value = e?.data?.statusMessage ?? 'Não foi possível iniciar o login.'
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
|
|
<!-- ─── HERO ─────────────────────────────────────────────────────── -->
|
|
<section
|
|
class="text-white relative overflow-hidden"
|
|
:class="heroHasPhoto ? '' : 'bg-gradient-to-br from-slate-900 via-primary-900 to-slate-800'"
|
|
:style="heroBgStyle"
|
|
>
|
|
<div
|
|
v-if="!heroHasPhoto"
|
|
class="absolute inset-0 opacity-5 pointer-events-none"
|
|
style="background-image: radial-gradient(circle at 25px 25px, white 2px, transparent 0); background-size: 50px 50px;"
|
|
/>
|
|
|
|
<div
|
|
v-if="heroHasPhoto"
|
|
class="absolute inset-0 pointer-events-none"
|
|
style="background: linear-gradient(to right, rgba(15,23,42,0.88) 0%, rgba(15,23,42,0.65) 55%, rgba(15,23,42,0.15) 100%);"
|
|
/>
|
|
|
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-14 lg:py-20">
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
|
|
|
<div>
|
|
<div class="flex items-center gap-3 mb-6">
|
|
<img
|
|
v-if="prefeitura.pathLogo"
|
|
:src="prefeitura.pathLogo"
|
|
alt="Logo"
|
|
class="h-12 w-auto object-contain drop-shadow-md"
|
|
/>
|
|
<div
|
|
v-else
|
|
class="w-12 h-12 bg-white/15 backdrop-blur-sm rounded-xl flex items-center justify-center"
|
|
>
|
|
<i class="pi pi-building text-white text-xl" />
|
|
</div>
|
|
<div>
|
|
<p class="text-white/60 text-xs uppercase tracking-widest font-medium">Portal do Contribuinte</p>
|
|
<p class="text-white font-bold text-lg leading-tight">
|
|
{{ prefeitura.nomePrefeitura || 'Prefeitura Municipal' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h1 class="text-3xl lg:text-[2.6rem] font-bold leading-tight mb-4 tracking-tight">
|
|
Serviços municipais<br />
|
|
<span class="text-white/80">na palma da mão</span>
|
|
</h1>
|
|
<p class="text-white/80 text-base mb-8 leading-relaxed max-w-md">
|
|
Emita certidões, consulte débitos e acesse todos os serviços
|
|
da prefeitura sem precisar sair de casa.
|
|
</p>
|
|
|
|
<ul class="space-y-2 mb-8">
|
|
<li
|
|
v-for="s in servicosPublicos"
|
|
:key="s.titulo"
|
|
class="flex items-center gap-3 group cursor-pointer rounded-xl border border-white/10 px-3 py-2.5 hover:border-primary/70 transition-colors"
|
|
@click="router.push(s.to)"
|
|
>
|
|
<i :class="['pi', s.icon, 'text-white/70 text-sm flex-shrink-0 group-hover:text-primary transition-colors']" />
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-semibold text-white leading-tight">{{ s.titulo }}</p>
|
|
<p class="text-xs text-white/60 mt-0.5 truncate">{{ s.descricao }}</p>
|
|
</div>
|
|
<i class="pi pi-chevron-right text-white/30 text-xs flex-shrink-0 group-hover:text-primary/70 transition-colors" />
|
|
</li>
|
|
</ul>
|
|
|
|
<NuxtLink
|
|
to="/servicos"
|
|
class="inline-flex items-center gap-2 text-sm text-white/80 hover:text-white transition-colors font-medium"
|
|
>
|
|
Ver todos os serviços disponíveis
|
|
<i class="pi pi-arrow-right text-xs" />
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<div class="flex justify-center lg:justify-end">
|
|
<!-- ── Card de saudação (usuário autenticado) ── -->
|
|
<div v-if="isAuthenticated" class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl p-8 w-full max-w-sm backdrop-blur-sm">
|
|
<div class="flex items-center gap-3 mb-6">
|
|
<div class="w-11 h-11 bg-primary rounded-xl flex items-center justify-center shadow-sm">
|
|
<i class="pi pi-user text-white text-lg" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Bem-vindo(a) de volta</p>
|
|
<p class="font-bold text-slate-800 dark:text-slate-100 text-base truncate">{{ nomeUsuario || 'Contribuinte' }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-sm text-slate-600 dark:text-slate-300 mb-6 leading-relaxed">
|
|
Você já está autenticado. Acesse seu painel para emitir guias,
|
|
consultar débitos e gerenciar seus serviços.
|
|
</p>
|
|
|
|
<NuxtLink to="/portal/painel" class="block">
|
|
<Button
|
|
label="Ir para o painel"
|
|
icon="pi pi-arrow-right"
|
|
icon-pos="right"
|
|
class="w-full"
|
|
size="large"
|
|
/>
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<!-- ── Card de acesso (visitante) ── -->
|
|
<div v-else class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl p-8 w-full max-w-sm backdrop-blur-sm">
|
|
|
|
<div class="flex items-center gap-3 mb-7">
|
|
<div class="w-11 h-11 bg-primary rounded-xl flex items-center justify-center shadow-sm">
|
|
<i class="pi pi-lock-open text-white text-lg" />
|
|
</div>
|
|
<div>
|
|
<p class="font-bold text-slate-800 dark:text-slate-100 text-base">Área do Contribuinte</p>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Acesso seguro ao portal</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-200 mb-1.5">
|
|
CPF ou CNPJ
|
|
</label>
|
|
<DocumentoInput
|
|
ref="documentoRef"
|
|
v-model="documento"
|
|
@keyup.enter="continuar"
|
|
/>
|
|
<p v-if="erro" class="mt-1.5 text-xs text-red-600 flex items-center gap-1">
|
|
<i class="pi pi-exclamation-circle text-xs" />
|
|
{{ erro }}
|
|
</p>
|
|
</div>
|
|
|
|
<Button
|
|
label="Continuar"
|
|
icon="pi pi-arrow-right"
|
|
icon-pos="right"
|
|
class="w-full"
|
|
size="large"
|
|
:loading="carregando"
|
|
@click="continuar"
|
|
/>
|
|
</div>
|
|
|
|
<Divider>
|
|
<span class="text-xs text-slate-400 font-normal">Primeiro acesso?</span>
|
|
</Divider>
|
|
|
|
<div class="space-y-2.5 text-center">
|
|
<NuxtLink
|
|
to="/primeiro-acesso"
|
|
class="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-lg border border-slate-200 dark:border-slate-600 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700 hover:border-slate-300 dark:hover:border-slate-500 transition-colors font-medium"
|
|
>
|
|
<i class="pi pi-key text-slate-500 dark:text-slate-400 text-sm" />
|
|
Criar minha senha
|
|
</NuxtLink>
|
|
<NuxtLink
|
|
to="/credenciamento"
|
|
class="block text-xs text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
|
>
|
|
Ainda não cadastrado? <span class="text-primary font-semibold">Credenciar-se</span>
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ─── CAROUSEL DE AVISOS ────────────────────────────────────────── -->
|
|
<section class="bg-white dark:bg-slate-900 border-b border-slate-100 dark:border-slate-800" aria-label="Avisos e comunicados">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<Carousel
|
|
:value="avisos"
|
|
:num-visible="1"
|
|
:num-scroll="1"
|
|
:autoplay-interval="prefersReducedMotion ? 0 : 6000"
|
|
circular
|
|
class="aviso-carousel"
|
|
aria-label="Avisos da prefeitura"
|
|
>
|
|
<template #item="{ data: aviso }">
|
|
<div
|
|
:class="[
|
|
'mx-2 rounded-xl border p-4 flex items-start gap-4',
|
|
corAviso[aviso.cor].bg,
|
|
corAviso[aviso.cor].borda,
|
|
]"
|
|
>
|
|
<div class="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-white shadow-sm">
|
|
<i :class="['pi', aviso.icone, 'text-lg', corAviso[aviso.cor].icone]" />
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="font-semibold text-slate-800 dark:text-slate-100 text-sm">{{ aviso.titulo }}</p>
|
|
<p class="text-xs text-slate-600 dark:text-slate-300 mt-0.5 leading-relaxed">{{ aviso.descricao }}</p>
|
|
<NuxtLink
|
|
v-if="aviso.acao"
|
|
:to="aviso.acao.to"
|
|
class="inline-block mt-3"
|
|
>
|
|
<Button
|
|
:label="aviso.acao.label"
|
|
size="small"
|
|
outlined
|
|
class="whitespace-nowrap"
|
|
/>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Carousel>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ─── SERVIÇOS AUTENTICADOS ─────────────────────────────────────── -->
|
|
<section class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12" aria-labelledby="servicos-logados-titulo">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h2 id="servicos-logados-titulo" class="text-xl font-bold text-slate-800 dark:text-slate-100">Área logada</h2>
|
|
<p class="text-sm text-slate-600 dark:text-slate-400 mt-0.5">Serviços disponíveis após login</p>
|
|
</div>
|
|
<span class="hidden sm:flex items-center gap-1.5 bg-primary/8 text-primary text-xs font-semibold px-3 py-1.5 rounded-full border border-primary/15">
|
|
<i class="pi pi-lock text-xs" />
|
|
Requer login
|
|
</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
|
|
<ServiceCard
|
|
v-for="s in servicosAutenticados"
|
|
:key="s.titulo"
|
|
v-bind="s"
|
|
:requires-auth="true"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mt-10 bg-gradient-to-r from-primary/5 to-primary/10 dark:from-primary/10 dark:to-primary/15 border border-primary/15 dark:border-primary/20 rounded-2xl p-6 flex flex-col sm:flex-row items-center justify-between gap-5">
|
|
<div>
|
|
<p class="font-bold text-slate-800 dark:text-slate-100 text-base">Ainda não tem acesso ao portal?</p>
|
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1 max-w-md">
|
|
Solicite seu credenciamento e passe a gerenciar todos os seus tributos municipais de forma online.
|
|
</p>
|
|
</div>
|
|
<NuxtLink to="/credenciamento">
|
|
<Button label="Solicitar Credenciamento" icon="pi pi-user-plus" class="whitespace-nowrap" />
|
|
</NuxtLink>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.aviso-carousel :deep(.p-carousel-prev),
|
|
.aviso-carousel :deep(.p-carousel-next) {
|
|
width: 1.75rem;
|
|
height: 1.75rem;
|
|
}
|
|
</style>
|