feat(auth): adicionar tratamento de erro ao salvar estado PKCE
All checks were successful
Dev Build & Deploy Portal / build-deploy (push) Successful in 2m36s
All checks were successful
Dev Build & Deploy Portal / build-deploy (push) Successful in 2m36s
feat(proxy): melhorar tratamento de erro para backend inacessível feat(certidao): adicionar validação de CPF e CNPJ feat(iptu): ajustar manipulação de dados de imóveis retornados feat(error): criar componente de erro para exibição de mensagens
This commit is contained in:
parent
e4c468e61e
commit
5e528f234b
@ -16,11 +16,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
const { codeVerifier, codeChallenge, state } = await generatePkce()
|
const { codeVerifier, codeChallenge, state } = await generatePkce()
|
||||||
const returnTo = body.data.returnTo ?? '/portal/painel'
|
const returnTo = body.data.returnTo ?? '/portal/painel'
|
||||||
|
|
||||||
|
try {
|
||||||
await savePkceState(state, {
|
await savePkceState(state, {
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
returnTo,
|
returnTo,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[auth/login] falha ao salvar estado PKCE (Redis indisponível?):', (err as Error).message)
|
||||||
|
throw createError({ statusCode: 503, statusMessage: 'Serviço temporariamente indisponível. Tente novamente em instantes.' })
|
||||||
|
}
|
||||||
|
|
||||||
const redirectUri = callbackUrlFromEvent(event)
|
const redirectUri = callbackUrlFromEvent(event)
|
||||||
const authUrl = buildAuthUrl({
|
const authUrl = buildAuthUrl({
|
||||||
|
|||||||
@ -97,6 +97,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
setResponseHeader(event, 'content-type', 'application/json; charset=utf-8')
|
setResponseHeader(event, 'content-type', 'application/json; charset=utf-8')
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
throw err
|
|
||||||
|
// Erro de rede (backend inacessível — ECONNREFUSED, timeout, etc.)
|
||||||
|
console.error(`[proxy] backend inacessível: ${url}`, (err as Error).message)
|
||||||
|
throw createError({ statusCode: 503, statusMessage: 'Sistema temporariamente indisponível.' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
58
src/error.vue
Normal file
58
src/error.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
error: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const is404 = computed(() => props.error?.statusCode === 404)
|
||||||
|
|
||||||
|
function voltar() {
|
||||||
|
clearError({ redirect: '/' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-slate-50 dark:bg-slate-950 flex flex-col items-center justify-center px-4">
|
||||||
|
<div class="w-full max-w-md text-center">
|
||||||
|
|
||||||
|
<div class="w-20 h-20 mx-auto mb-6 rounded-2xl flex items-center justify-center"
|
||||||
|
:class="is404 ? 'bg-slate-100 dark:bg-slate-800' : 'bg-amber-50 dark:bg-amber-900/20'">
|
||||||
|
<svg v-if="is404" xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-slate-400 dark:text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-amber-500 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-3">
|
||||||
|
{{ is404 ? 'Página não encontrada' : 'Portal temporariamente indisponível' }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed mb-8">
|
||||||
|
<template v-if="is404">
|
||||||
|
O endereço que você acessou não existe ou foi movido.
|
||||||
|
Verifique o link e tente novamente.
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Estamos realizando manutenção ou enfrentando uma instabilidade momentânea.
|
||||||
|
Aguarde alguns minutos e tente novamente. Se o problema persistir,
|
||||||
|
entre em contato com a prefeitura.
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-primary text-white font-semibold text-sm hover:bg-primary/90 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
||||||
|
@click="voltar"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||||
|
</svg>
|
||||||
|
{{ is404 ? 'Ir para a página inicial' : 'Tentar novamente' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -108,6 +108,7 @@ function getSegundoNome(texto) {
|
|||||||
size="small"
|
size="small"
|
||||||
show-gridlines
|
show-gridlines
|
||||||
selection-mode="multiple"
|
selection-mode="multiple"
|
||||||
|
:is-row-selectable="({ data }) => data.codigoEstadoConta === 1"
|
||||||
scrollable
|
scrollable
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
>
|
>
|
||||||
@ -196,7 +197,7 @@ function getSegundoNome(texto) {
|
|||||||
:loading="isLoadingExtrato"
|
:loading="isLoadingExtrato"
|
||||||
@click="gerarExtratoPdf(false)"
|
@click="gerarExtratoPdf(false)"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="temSelecionado"
|
v-if="temSelecionado"
|
||||||
label="Imprimir Selecionados"
|
label="Imprimir Selecionados"
|
||||||
icon="pi pi-print"
|
icon="pi pi-print"
|
||||||
|
|||||||
@ -60,12 +60,12 @@ function aplicarFiltros() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 space-y-4">
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 space-y-4">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<div>
|
<div class="lg:col-span-2">
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Nº Guia</label>
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Nº Guia</label>
|
||||||
<InputText v-model="filtro.numeroGuia" class="w-full" size="small" />
|
<InputText v-model="filtro.numeroGuia" class="w-full" size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="lg:col-span-2">
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Status</label>
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Status</label>
|
||||||
<Select
|
<Select
|
||||||
v-model="filtro.status"
|
v-model="filtro.status"
|
||||||
@ -79,26 +79,26 @@ function aplicarFiltros() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Emissão (início)</label>
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Emissão início</label>
|
||||||
<DatePicker v-model="filtro.dataEmissaoInicio" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
<DatePicker v-model="filtro.dataEmissaoInicio" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Emissão (fim)</label>
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Emissão fim</label>
|
||||||
<DatePicker v-model="filtro.dataEmissaoFim" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
<DatePicker v-model="filtro.dataEmissaoFim" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Vencimento (início)</label>
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Vencimento início</label>
|
||||||
<DatePicker v-model="filtro.dataVencimentoInicio" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
<DatePicker v-model="filtro.dataVencimentoInicio" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Vencimento (fim)</label>
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Vencimento fim</label>
|
||||||
<DatePicker v-model="filtro.dataVencimentoFim" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
<DatePicker v-model="filtro.dataVencimentoFim" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="lg:col-span-2">
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Valor mínimo</label>
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Valor mínimo</label>
|
||||||
<InputNumber v-model="filtro.valorMinimo" mode="currency" currency="BRL" locale="pt-BR" class="w-full" size="small" />
|
<InputNumber v-model="filtro.valorMinimo" mode="currency" currency="BRL" locale="pt-BR" class="w-full" size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="lg:col-span-2">
|
||||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Valor máximo</label>
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Valor máximo</label>
|
||||||
<InputNumber v-model="filtro.valorMaximo" mode="currency" currency="BRL" locale="pt-BR" class="w-full" size="small" />
|
<InputNumber v-model="filtro.valorMaximo" mode="currency" currency="BRL" locale="pt-BR" class="w-full" size="small" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { ref, computed, watch } from 'vue'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { certidaoService } from '@/services/certidaoService'
|
import { certidaoService } from '@/services/certidaoService'
|
||||||
|
import { validarCpf, validarCnpj } from '@/utils/formatador'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -24,6 +25,7 @@ const carregandoModelos = ref(false)
|
|||||||
const carregandoConsulta = ref(false)
|
const carregandoConsulta = ref(false)
|
||||||
const carregandoEmissao = ref(false)
|
const carregandoEmissao = ref(false)
|
||||||
const resultado = ref(null)
|
const resultado = ref(null)
|
||||||
|
const certidaoEmitida = ref(null)
|
||||||
const mensagemErro = ref('')
|
const mensagemErro = ref('')
|
||||||
const erros = ref({})
|
const erros = ref({})
|
||||||
|
|
||||||
@ -37,7 +39,16 @@ const schemaEmissao = z.object({
|
|||||||
|
|
||||||
const docValido = computed(() => {
|
const docValido = computed(() => {
|
||||||
const d = documento.value.replace(/\D/g, '')
|
const d = documento.value.replace(/\D/g, '')
|
||||||
return d.length === 11 || d.length === 14
|
if (d.length === 11) return validarCpf(d)
|
||||||
|
if (d.length === 14) return validarCnpj(d)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const docInvalido = computed(() => {
|
||||||
|
const d = documento.value.replace(/\D/g, '')
|
||||||
|
if (d.length === 11) return !validarCpf(d)
|
||||||
|
if (d.length === 14) return !validarCnpj(d)
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const modeloSelecionado = computed(() =>
|
const modeloSelecionado = computed(() =>
|
||||||
@ -58,7 +69,7 @@ function resetModelos() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extrairErro(e) {
|
function extrairErro(e) {
|
||||||
return e?.data?.description ?? e?.data?.message ?? e?.statusMessage ?? null
|
return e?.data?.description ?? e?.data?.data?.description ?? e?.statusMessage ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function carregarModelos() {
|
async function carregarModelos() {
|
||||||
@ -147,8 +158,21 @@ async function emitir() {
|
|||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
a.href = url
|
||||||
a.download = `certidao-${slug}-${documento.value.replace(/\D/g, '')}.pdf`
|
a.download = `certidao-${slug}-${documento.value.replace(/\D/g, '')}.pdf`
|
||||||
|
document.body.appendChild(a)
|
||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(url)
|
document.body.removeChild(a)
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||||
|
if (isAuthenticated.value || route.query.from === 'portal') {
|
||||||
|
router.push('/portal/certidoes')
|
||||||
|
} else {
|
||||||
|
certidaoEmitida.value = {
|
||||||
|
titulo: modeloSelecionado.value?.titulo,
|
||||||
|
finalidade: finalidade.value.trim(),
|
||||||
|
validade: validadeLabel.value,
|
||||||
|
nomeContribuinte: resultado.value?.nomeContribuinte ?? documento.value,
|
||||||
|
}
|
||||||
|
etapa.value = 'sucesso'
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
mensagemErro.value = extrairErro(e) ?? 'Erro ao gerar o PDF. Tente novamente.'
|
mensagemErro.value = extrairErro(e) ?? 'Erro ao gerar o PDF. Tente novamente.'
|
||||||
} finally {
|
} finally {
|
||||||
@ -201,6 +225,10 @@ function reiniciar() {
|
|||||||
:disabled="isAuthenticated"
|
:disabled="isAuthenticated"
|
||||||
@keyup.enter="consultar"
|
@keyup.enter="consultar"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="docInvalido && !isAuthenticated" class="mt-1.5 text-xs text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||||
|
<i class="pi pi-exclamation-circle" aria-hidden="true" />
|
||||||
|
CPF ou CNPJ inválido.
|
||||||
|
</p>
|
||||||
<p v-if="isAuthenticated" class="mt-1.5 text-xs text-slate-500 dark:text-slate-400 flex items-start gap-1.5">
|
<p v-if="isAuthenticated" class="mt-1.5 text-xs text-slate-500 dark:text-slate-400 flex items-start gap-1.5">
|
||||||
<i class="pi pi-info-circle text-xs text-primary mt-0.5 shrink-0" aria-hidden="true" />
|
<i class="pi pi-info-circle text-xs text-primary mt-0.5 shrink-0" aria-hidden="true" />
|
||||||
<span>Logado como <strong class="text-slate-700 dark:text-slate-200">{{ nomeUsuario }}</strong> — documento bloqueado para segurança.</span>
|
<span>Logado como <strong class="text-slate-700 dark:text-slate-200">{{ nomeUsuario }}</strong> — documento bloqueado para segurança.</span>
|
||||||
@ -332,5 +360,61 @@ function reiniciar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="etapa === 'sucesso'" class="space-y-4">
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-emerald-200 dark:border-emerald-700/50 p-8 flex flex-col items-center text-center gap-4">
|
||||||
|
<div class="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/30 rounded-2xl flex items-center justify-center">
|
||||||
|
<i class="pi pi-check-circle text-emerald-600 dark:text-emerald-400 text-3xl" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-lg font-bold text-slate-800 dark:text-slate-100">Certidão emitida com sucesso!</p>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||||
|
O PDF foi baixado automaticamente para o seu dispositivo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 p-6 space-y-3">
|
||||||
|
<p class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">Detalhes da certidão emitida</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400">Contribuinte</span>
|
||||||
|
<span class="text-sm font-medium text-slate-800 dark:text-slate-100 text-right">{{ certidaoEmitida?.nomeContribuinte }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400">Modelo</span>
|
||||||
|
<span class="text-sm font-medium text-slate-800 dark:text-slate-100 text-right">{{ certidaoEmitida?.titulo }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="certidaoEmitida?.finalidade" class="flex justify-between gap-4">
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400">Finalidade</span>
|
||||||
|
<span class="text-sm font-medium text-slate-800 dark:text-slate-100 text-right">{{ certidaoEmitida.finalidade }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400">Validade</span>
|
||||||
|
<span class="text-sm font-medium text-slate-800 dark:text-slate-100 text-right">{{ certidaoEmitida?.validade }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<Button
|
||||||
|
label="Emitir nova certidão"
|
||||||
|
severity="secondary"
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
outlined
|
||||||
|
class="flex-1"
|
||||||
|
@click="reiniciar"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Voltar aos serviços"
|
||||||
|
icon="pi pi-home"
|
||||||
|
class="flex-1"
|
||||||
|
severity="secondary"
|
||||||
|
@click="router.push('/servicos')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -37,7 +37,7 @@ async function consultar() {
|
|||||||
? await iptuService.consultarPorDocumento(documento.value)
|
? await iptuService.consultarPorDocumento(documento.value)
|
||||||
: await iptuService.consultarPorInscricao(inscricao.value)
|
: await iptuService.consultarPorInscricao(inscricao.value)
|
||||||
|
|
||||||
imoveis.value = res.data ?? []
|
imoveis.value = res.data?.content ?? res.data ?? []
|
||||||
if (imoveis.value.length === 1) imovelSelecionado.value = imoveis.value[0]
|
if (imoveis.value.length === 1) imovelSelecionado.value = imoveis.value[0]
|
||||||
etapa.value = 'resultado'
|
etapa.value = 'resultado'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -188,7 +188,19 @@ function formatarMoeda(valor) {
|
|||||||
|
|
||||||
<div v-else-if="etapa === 'resultado'" class="space-y-4">
|
<div v-else-if="etapa === 'resultado'" class="space-y-4">
|
||||||
|
|
||||||
<div v-if="imoveis.length > 1" class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 p-6">
|
<div v-if="imoveis.length === 0" class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 p-8 text-center">
|
||||||
|
<div class="w-14 h-14 bg-primary/10 dark:bg-primary/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<i class="pi pi-search text-primary text-2xl" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<p class="text-base font-semibold text-slate-800 dark:text-slate-100 mb-1">Nenhum imóvel encontrado</p>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
Não foram encontrados imóveis cadastrados para o
|
||||||
|
{{ modoConsulta === 'documento' ? 'documento informado' : 'número de inscrição informado' }}.
|
||||||
|
Verifique os dados e tente novamente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="imoveis.length > 1" class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 p-6">
|
||||||
<p class="text-sm font-semibold text-slate-700 dark:text-slate-200 mb-3">
|
<p class="text-sm font-semibold text-slate-700 dark:text-slate-200 mb-3">
|
||||||
{{ imoveis.length }} imóveis encontrados — selecione um
|
{{ imoveis.length }} imóveis encontrados — selecione um
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -47,3 +47,30 @@ export function abrirPdf(buf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { formatarMoeda, baixarPdf }
|
export { formatarMoeda, baixarPdf }
|
||||||
|
|
||||||
|
export function validarCpf(cpf) {
|
||||||
|
const d = String(cpf).replace(/\D/g, '')
|
||||||
|
if (d.length !== 11 || /^(\d)\1{10}$/.test(d)) return false
|
||||||
|
const calc = (len) => {
|
||||||
|
let sum = 0
|
||||||
|
for (let i = 0; i < len; i++) sum += parseInt(d[i]) * (len + 1 - i)
|
||||||
|
const r = sum % 11
|
||||||
|
return r < 2 ? 0 : 11 - r
|
||||||
|
}
|
||||||
|
return calc(9) === parseInt(d[9]) && calc(10) === parseInt(d[10])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validarCnpj(cnpj) {
|
||||||
|
const d = String(cnpj).replace(/\D/g, '')
|
||||||
|
if (d.length !== 14 || /^(\d)\1{13}$/.test(d)) return false
|
||||||
|
const calc = (len) => {
|
||||||
|
const weights = len === 12
|
||||||
|
? [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
|
||||||
|
: [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
|
||||||
|
let sum = 0
|
||||||
|
for (let i = 0; i < len; i++) sum += parseInt(d[i]) * weights[i]
|
||||||
|
const r = sum % 11
|
||||||
|
return r < 2 ? 0 : 11 - r
|
||||||
|
}
|
||||||
|
return calc(12) === parseInt(d[12]) && calc(13) === parseInt(d[13])
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user