Merge branch 'developer' of https://git.modumsolucao.com.br/ModumSolucao/portal-modumfiscal-web into developer
Some checks failed
Dev Build & Deploy Portal / build-deploy (push) Has been cancelled
Some checks failed
Dev Build & Deploy Portal / build-deploy (push) Has been cancelled
This commit is contained in:
commit
0ec38639ab
@ -16,11 +16,16 @@ export default defineEventHandler(async (event) => {
|
||||
const { codeVerifier, codeChallenge, state } = await generatePkce()
|
||||
const returnTo = body.data.returnTo ?? '/portal/painel'
|
||||
|
||||
try {
|
||||
await savePkceState(state, {
|
||||
codeVerifier,
|
||||
returnTo,
|
||||
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 authUrl = buildAuthUrl({
|
||||
|
||||
@ -97,6 +97,9 @@ export default defineEventHandler(async (event) => {
|
||||
setResponseHeader(event, 'content-type', 'application/json; charset=utf-8')
|
||||
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 },
|
||||
]
|
||||
|
||||
const ESTADO_DEBITO = 1
|
||||
|
||||
const temSelecionado = computed(() =>
|
||||
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(() => {
|
||||
let principal = 0, multa = 0, juros = 0, valorTotal = 0
|
||||
resultados.value.forEach(t => {
|
||||
@ -85,7 +93,13 @@ export function useExtratoDebitosPortal() {
|
||||
function montarDtoGuia() {
|
||||
return {
|
||||
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 => ({
|
||||
idTributo: t.idTributo,
|
||||
identificador: t.identificador,
|
||||
@ -139,8 +153,8 @@ export function useExtratoDebitosPortal() {
|
||||
}
|
||||
|
||||
async function gerarGuia() {
|
||||
if (!temSelecionado.value) {
|
||||
mensagemErro.value = 'Selecione pelo menos um débito.'
|
||||
if (!temDebitoSelecionado.value) {
|
||||
mensagemErro.value = 'Selecione pelo menos um débito em aberto. Parcelas quitadas ou com saldo zerado não geram guia.'
|
||||
return
|
||||
}
|
||||
if (!dataVencimento.value) {
|
||||
@ -202,6 +216,7 @@ export function useExtratoDebitosPortal() {
|
||||
filtro,
|
||||
opcoesEstadoConta,
|
||||
temSelecionado,
|
||||
temDebitoSelecionado,
|
||||
totalizadores,
|
||||
consultar,
|
||||
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>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { portalService } from '@/services/portalService'
|
||||
import { formatDate } from '@/utils/formatador'
|
||||
|
||||
definePageMeta({
|
||||
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-xs text-slate-400 dark:text-slate-500 mt-0.5">Nº {{ cert.numero }}</p>
|
||||
</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">{{ cert.dataValidade }}</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">{{ formatDate(cert.dataValidade) }}</p>
|
||||
<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']">
|
||||
{{ statusMap[cert.status]?.label ?? cert.status }}
|
||||
|
||||
@ -22,6 +22,7 @@ const {
|
||||
filtro,
|
||||
opcoesEstadoConta,
|
||||
temSelecionado,
|
||||
temDebitoSelecionado,
|
||||
totalizadores,
|
||||
consultar,
|
||||
limparFiltros,
|
||||
@ -108,6 +109,7 @@ function getSegundoNome(texto) {
|
||||
size="small"
|
||||
show-gridlines
|
||||
selection-mode="multiple"
|
||||
:is-row-selectable="({ data }) => data.codigoEstadoConta === 1"
|
||||
scrollable
|
||||
class="mt-2"
|
||||
>
|
||||
@ -196,7 +198,7 @@ function getSegundoNome(texto) {
|
||||
:loading="isLoadingExtrato"
|
||||
@click="gerarExtratoPdf(false)"
|
||||
/>
|
||||
<Button
|
||||
<Button
|
||||
v-if="temSelecionado"
|
||||
label="Imprimir Selecionados"
|
||||
icon="pi pi-print"
|
||||
@ -208,7 +210,7 @@ function getSegundoNome(texto) {
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-if="temSelecionado"
|
||||
v-if="temDebitoSelecionado"
|
||||
label="Gerar Guia de Pagamento"
|
||||
icon="pi pi-file-pdf"
|
||||
size="small"
|
||||
|
||||
@ -60,12 +60,12 @@ function aplicarFiltros() {
|
||||
</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="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="lg:col-span-2">
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="lg:col-span-2">
|
||||
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Status</label>
|
||||
<Select
|
||||
v-model="filtro.status"
|
||||
@ -79,26 +79,26 @@ function aplicarFiltros() {
|
||||
/>
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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>
|
||||
<InputNumber v-model="filtro.valorMinimo" mode="currency" currency="BRL" locale="pt-BR" class="w-full" size="small" />
|
||||
</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>
|
||||
<InputNumber v-model="filtro.valorMaximo" mode="currency" currency="BRL" locale="pt-BR" class="w-full" size="small" />
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@ import { ref, computed, watch } from 'vue'
|
||||
import { z } from 'zod'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { certidaoService } from '@/services/certidaoService'
|
||||
import { validarCpf, validarCnpj, formatDate } from '@/utils/formatador'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -24,6 +25,9 @@ const carregandoModelos = ref(false)
|
||||
const carregandoConsulta = ref(false)
|
||||
const carregandoEmissao = ref(false)
|
||||
const resultado = ref(null)
|
||||
const certidaoEmitida = ref(null)
|
||||
const certidaoExistente = ref(null)
|
||||
const carregandoDownloadExistente = ref(false)
|
||||
const mensagemErro = ref('')
|
||||
const erros = ref({})
|
||||
|
||||
@ -37,7 +41,16 @@ const schemaEmissao = z.object({
|
||||
|
||||
const docValido = computed(() => {
|
||||
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(() =>
|
||||
@ -58,7 +71,23 @@ function resetModelos() {
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -136,6 +165,21 @@ async function emitir() {
|
||||
if (!validarFormulario()) return
|
||||
carregandoEmissao.value = true
|
||||
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 {
|
||||
const buf = await certidaoService.emitir(
|
||||
documento.value,
|
||||
@ -147,20 +191,63 @@ async function emitir() {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `certidao-${slug}-${documento.value.replace(/\D/g, '')}.pdf`
|
||||
document.body.appendChild(a)
|
||||
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) {
|
||||
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 {
|
||||
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() {
|
||||
documento.value = isAuthenticated.value ? docUsuarioLogado.value : ''
|
||||
idModeloSelecionado.value = null
|
||||
finalidade.value = ''
|
||||
resultado.value = null
|
||||
certidaoEmitida.value = null
|
||||
certidaoExistente.value = null
|
||||
mensagemErro.value = ''
|
||||
erros.value = {}
|
||||
etapa.value = 'formulario'
|
||||
@ -201,6 +288,10 @@ function reiniciar() {
|
||||
:disabled="isAuthenticated"
|
||||
@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">
|
||||
<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>
|
||||
@ -332,5 +423,120 @@ function reiniciar() {
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -37,7 +37,7 @@ async function consultar() {
|
||||
? await iptuService.consultarPorDocumento(documento.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]
|
||||
etapa.value = 'resultado'
|
||||
} catch (e) {
|
||||
@ -188,7 +188,19 @@ function formatarMoeda(valor) {
|
||||
|
||||
<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">
|
||||
{{ imoveis.length }} imóveis encontrados — selecione um
|
||||
</p>
|
||||
|
||||
@ -26,4 +26,18 @@ export const certidaoService = {
|
||||
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 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