developer #4
@ -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.' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -24,10 +24,18 @@ export function useExtratoDebitosPortal() {
|
|||||||
{ label: 'Saldo Crédito', value: 3 },
|
{ label: 'Saldo Crédito', value: 3 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ESTADO_DEBITO = 1
|
||||||
|
|
||||||
const temSelecionado = computed(() =>
|
const temSelecionado = computed(() =>
|
||||||
resultados.value.some(t => t.selecionados?.length > 0)
|
resultados.value.some(t => t.selecionados?.length > 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const temDebitoSelecionado = computed(() =>
|
||||||
|
resultados.value.some(t =>
|
||||||
|
t.selecionados?.some(d => d.codigoEstadoConta === ESTADO_DEBITO)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const totalizadores = computed(() => {
|
const totalizadores = computed(() => {
|
||||||
let principal = 0, multa = 0, juros = 0, valorTotal = 0
|
let principal = 0, multa = 0, juros = 0, valorTotal = 0
|
||||||
resultados.value.forEach(t => {
|
resultados.value.forEach(t => {
|
||||||
@ -85,7 +93,13 @@ export function useExtratoDebitosPortal() {
|
|||||||
function montarDtoGuia() {
|
function montarDtoGuia() {
|
||||||
return {
|
return {
|
||||||
listaDebitos: resultados.value
|
listaDebitos: resultados.value
|
||||||
.filter(t => t.selecionados?.length > 0)
|
.map(t => ({
|
||||||
|
...t,
|
||||||
|
selecionados: (t.selecionados || []).filter(
|
||||||
|
d => d.codigoEstadoConta === ESTADO_DEBITO
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(t => t.selecionados.length > 0)
|
||||||
.map(t => ({
|
.map(t => ({
|
||||||
idTributo: t.idTributo,
|
idTributo: t.idTributo,
|
||||||
identificador: t.identificador,
|
identificador: t.identificador,
|
||||||
@ -139,8 +153,8 @@ export function useExtratoDebitosPortal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function gerarGuia() {
|
async function gerarGuia() {
|
||||||
if (!temSelecionado.value) {
|
if (!temDebitoSelecionado.value) {
|
||||||
mensagemErro.value = 'Selecione pelo menos um débito.'
|
mensagemErro.value = 'Selecione pelo menos um débito em aberto. Parcelas quitadas ou com saldo zerado não geram guia.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!dataVencimento.value) {
|
if (!dataVencimento.value) {
|
||||||
@ -202,6 +216,7 @@ export function useExtratoDebitosPortal() {
|
|||||||
filtro,
|
filtro,
|
||||||
opcoesEstadoConta,
|
opcoesEstadoConta,
|
||||||
temSelecionado,
|
temSelecionado,
|
||||||
|
temDebitoSelecionado,
|
||||||
totalizadores,
|
totalizadores,
|
||||||
consultar,
|
consultar,
|
||||||
limparFiltros,
|
limparFiltros,
|
||||||
|
|||||||
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>
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { portalService } from '@/services/portalService'
|
import { portalService } from '@/services/portalService'
|
||||||
|
import { formatDate } from '@/utils/formatador'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'portal',
|
layout: 'portal',
|
||||||
@ -134,8 +135,8 @@ const statusMap = {
|
|||||||
<p class="text-sm font-semibold text-slate-800 dark:text-slate-100 truncate">{{ cert.tipo }}</p>
|
<p class="text-sm font-semibold text-slate-800 dark:text-slate-100 truncate">{{ cert.tipo }}</p>
|
||||||
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5">Nº {{ cert.numero }}</p>
|
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5">Nº {{ cert.numero }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="hidden sm:block text-sm text-slate-500 dark:text-slate-400 w-28 text-right">{{ cert.dataEmissao }}</p>
|
<p class="hidden sm:block text-sm text-slate-500 dark:text-slate-400 w-28 text-right">{{ formatDate(cert.dataEmissao) }}</p>
|
||||||
<p class="hidden sm:block text-sm text-slate-500 dark:text-slate-400 w-28 text-right">{{ cert.dataValidade }}</p>
|
<p class="hidden sm:block text-sm text-slate-500 dark:text-slate-400 w-28 text-right">{{ formatDate(cert.dataValidade) }}</p>
|
||||||
<div class="w-20 flex justify-center">
|
<div class="w-20 flex justify-center">
|
||||||
<span :class="['text-xs font-semibold px-2 py-1 rounded-full', statusMap[cert.status]?.classe ?? 'bg-slate-100 text-slate-500']">
|
<span :class="['text-xs font-semibold px-2 py-1 rounded-full', statusMap[cert.status]?.classe ?? 'bg-slate-100 text-slate-500']">
|
||||||
{{ statusMap[cert.status]?.label ?? cert.status }}
|
{{ statusMap[cert.status]?.label ?? cert.status }}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const {
|
|||||||
filtro,
|
filtro,
|
||||||
opcoesEstadoConta,
|
opcoesEstadoConta,
|
||||||
temSelecionado,
|
temSelecionado,
|
||||||
|
temDebitoSelecionado,
|
||||||
totalizadores,
|
totalizadores,
|
||||||
consultar,
|
consultar,
|
||||||
limparFiltros,
|
limparFiltros,
|
||||||
@ -108,6 +109,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 +198,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"
|
||||||
@ -208,7 +210,7 @@ function getSegundoNome(texto) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-if="temSelecionado"
|
v-if="temDebitoSelecionado"
|
||||||
label="Gerar Guia de Pagamento"
|
label="Gerar Guia de Pagamento"
|
||||||
icon="pi pi-file-pdf"
|
icon="pi pi-file-pdf"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@ -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, formatDate } from '@/utils/formatador'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -24,6 +25,9 @@ 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 certidaoExistente = ref(null)
|
||||||
|
const carregandoDownloadExistente = ref(false)
|
||||||
const mensagemErro = ref('')
|
const mensagemErro = ref('')
|
||||||
const erros = ref({})
|
const erros = ref({})
|
||||||
|
|
||||||
@ -37,7 +41,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 +71,23 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
const COD_CERTIDAO_VALIDA_JA_EXISTE = '214'
|
||||||
|
|
||||||
|
// Respostas de erro do endpoint de emissão chegam como ArrayBuffer (responseType
|
||||||
|
// é 'arrayBuffer' para o PDF). Decodifica o envelope JSON para ler o StandardError.
|
||||||
|
function lerEnvelopeErro(e) {
|
||||||
|
let body = e?.data
|
||||||
|
if (body instanceof ArrayBuffer) {
|
||||||
|
try {
|
||||||
|
body = JSON.parse(new TextDecoder().decode(body))
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return body ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function carregarModelos() {
|
async function carregarModelos() {
|
||||||
@ -136,6 +165,21 @@ async function emitir() {
|
|||||||
if (!validarFormulario()) return
|
if (!validarFormulario()) return
|
||||||
carregandoEmissao.value = true
|
carregandoEmissao.value = true
|
||||||
mensagemErro.value = ''
|
mensagemErro.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resValida = await certidaoService.verificarValida(documento.value, idModeloSelecionado.value)
|
||||||
|
const existente = resValida?.data?.data ?? resValida?.data
|
||||||
|
if (existente?.id != null) {
|
||||||
|
certidaoExistente.value = existente
|
||||||
|
etapa.value = 'certidaoExistente'
|
||||||
|
carregandoEmissao.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Pré-checagem indisponível — a emissão é bloqueada de forma autoritativa
|
||||||
|
// pelo backend, que devolve o erro CERTIDAO_VALIDA_JA_EXISTE tratado abaixo.
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buf = await certidaoService.emitir(
|
const buf = await certidaoService.emitir(
|
||||||
documento.value,
|
documento.value,
|
||||||
@ -147,20 +191,63 @@ 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.'
|
const erroBackend = lerEnvelopeErro(e)?.data
|
||||||
|
const existente = erroBackend?.response
|
||||||
|
if (erroBackend?.internalCode === COD_CERTIDAO_VALIDA_JA_EXISTE && existente?.id != null) {
|
||||||
|
certidaoExistente.value = existente
|
||||||
|
etapa.value = 'certidaoExistente'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mensagemErro.value = erroBackend?.description ?? extrairErro(e) ?? 'Erro ao gerar o PDF. Tente novamente.'
|
||||||
} finally {
|
} finally {
|
||||||
carregandoEmissao.value = false
|
carregandoEmissao.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadCertidaoExistente() {
|
||||||
|
carregandoDownloadExistente.value = true
|
||||||
|
mensagemErro.value = ''
|
||||||
|
try {
|
||||||
|
const buf = await certidaoService.downloadExistente(certidaoExistente.value.id)
|
||||||
|
const slug = (certidaoExistente.value?.tituloModelo ?? 'certidao').replace(/\s+/g, '-').toLowerCase()
|
||||||
|
const url = URL.createObjectURL(new Blob([buf], { type: 'application/pdf' }))
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `certidao-${slug}-${documento.value.replace(/\D/g, '')}.pdf`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||||
|
} catch (e) {
|
||||||
|
mensagemErro.value = extrairErro(e) ?? 'Erro ao baixar a certidão. Tente novamente.'
|
||||||
|
} finally {
|
||||||
|
carregandoDownloadExistente.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reiniciar() {
|
function reiniciar() {
|
||||||
documento.value = isAuthenticated.value ? docUsuarioLogado.value : ''
|
documento.value = isAuthenticated.value ? docUsuarioLogado.value : ''
|
||||||
idModeloSelecionado.value = null
|
idModeloSelecionado.value = null
|
||||||
finalidade.value = ''
|
finalidade.value = ''
|
||||||
resultado.value = null
|
resultado.value = null
|
||||||
|
certidaoEmitida.value = null
|
||||||
|
certidaoExistente.value = null
|
||||||
mensagemErro.value = ''
|
mensagemErro.value = ''
|
||||||
erros.value = {}
|
erros.value = {}
|
||||||
etapa.value = 'formulario'
|
etapa.value = 'formulario'
|
||||||
@ -201,6 +288,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 +423,120 @@ function reiniciar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="etapa === 'certidaoExistente'" class="space-y-4">
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-amber-200 dark:border-amber-700/50 p-8 flex flex-col items-center text-center gap-4">
|
||||||
|
<div class="w-16 h-16 bg-amber-50 dark:bg-amber-900/20 rounded-2xl flex items-center justify-center">
|
||||||
|
<i class="pi pi-exclamation-triangle text-amber-500 dark:text-amber-400 text-3xl" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-lg font-bold text-slate-800 dark:text-slate-100">Já existe uma certidão válida</p>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||||
|
Existe uma certidão do mesmo tipo ainda dentro do prazo de validade.<br>
|
||||||
|
Você pode baixar a certidão existente em vez de emitir uma nova.
|
||||||
|
</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">Certidão existente</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<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">{{ certidaoExistente?.tituloModelo }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400">Nº certidão</span>
|
||||||
|
<span class="text-sm font-medium text-slate-800 dark:text-slate-100 text-right">{{ certidaoExistente?.numero }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400">Válida até</span>
|
||||||
|
<span class="text-sm font-semibold text-emerald-600 dark:text-emerald-400 text-right">{{ formatDate(certidaoExistente?.dataValidade) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="mensagemErro" 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" />
|
||||||
|
{{ mensagemErro }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<Button
|
||||||
|
label="Nova consulta"
|
||||||
|
severity="secondary"
|
||||||
|
icon="pi pi-arrow-left"
|
||||||
|
outlined
|
||||||
|
class="flex-1"
|
||||||
|
@click="reiniciar"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Baixar certidão"
|
||||||
|
icon="pi pi-download"
|
||||||
|
class="flex-1"
|
||||||
|
:loading="carregandoDownloadExistente"
|
||||||
|
:disabled="carregandoDownloadExistente"
|
||||||
|
@click="downloadCertidaoExistente"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|||||||
@ -26,4 +26,18 @@ export const certidaoService = {
|
|||||||
responseType: 'arrayBuffer',
|
responseType: 'arrayBuffer',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
verificarValida(documento, idModelo) {
|
||||||
|
return $fetch(proxyUrl('/publico/certidao/valida'), {
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
query: { documento, idModelo },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadExistente(idCertidao) {
|
||||||
|
return $fetch(proxyUrl(`/publico/certidao/emitida/${idCertidao}/arquivo`), {
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
responseType: 'arrayBuffer',
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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