GUILHERME a48eea53bc
All checks were successful
Dev Build & Deploy Portal / build-deploy (push) Successful in 2m42s
feat(portal): módulo de emissão de taxas para o contribuinte
- Páginas taxas/index (listagem com status e ações) e taxas/emitir (wizard)
- CamposEmissaoTaxa: campos dinâmicos por tipo (data, numérico, inputmask, texto)
- useEmissaoTaxaPortal: composable com fluxo completo — busca catálogo, calcula
  vencimento/multa-juros, valida e emite taxa via API
- taxaService: client HTTP para os endpoints /api/v1/contribuinte/taxas
- atributoMascara, formatacao, formulaCalculo: utilitários de suporte
- portal.vue: item "Taxas" no menu de navegação
- painel.vue: atalho rápido "Emitir Taxa" com ícone pi-file-export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 17:58:52 -03:00

234 lines
10 KiB
Vue

<script setup>
import { onMounted, ref } from 'vue'
import CamposEmissaoTaxa from '@/components/taxas/CamposEmissaoTaxa.vue'
import { useEmissaoTaxaPortal } from '@/composables/useEmissaoTaxaPortal'
import { formatarDocumento } from '@/utils/formatacao'
definePageMeta({
layout: 'portal',
middleware: 'auth',
})
const router = useRouter()
const etapa = ref('formulario')
const {
contribuinte,
catalogo,
tributoSelecionadoId,
itensTributo,
itensInformativosDoc,
formulaSelecionada,
periodoReferencia,
vencimentoDebito,
vencimentoGuia,
valorTaxa,
valoresItens,
observacao,
totalizadores,
resultadoEmissao,
carregando,
carregandoTributo,
carregandoCalculo,
carregandoEmissao,
mensagemErro,
erros,
labelCatalogo,
carregarDadosIniciais,
onSelecionarTributo,
onPeriodoChange,
recalcularTotais,
emitirTaxa,
imprimirGuia,
reiniciar,
} = useEmissaoTaxaPortal()
onMounted(carregarDadosIniciais)
async function onTributoChange(id) {
await onSelecionarTributo(id)
}
async function onSubmit() {
const ok = await emitirTaxa()
if (ok) etapa.value = 'resultado'
}
function novaEmissao() {
reiniciar()
etapa.value = 'formulario'
carregarDadosIniciais()
}
</script>
<template>
<div class="space-y-6 max-w-3xl">
<div class="flex items-center gap-4">
<button
class="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
@click="router.push('/portal/taxas')"
>
<i class="pi pi-arrow-left text-xs" aria-hidden="true" />
Voltar
</button>
</div>
<div>
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100">Emitir Taxa</h1>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">Emissão simplificada de taxas disponíveis para o seu cadastro.</p>
</div>
<div v-if="carregando" class="space-y-4">
<div v-for="i in 3" :key="i" class="h-20 bg-slate-200 dark:bg-slate-700 rounded-xl animate-pulse" />
</div>
<template v-else-if="etapa === 'formulario'">
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5 space-y-4">
<h2 class="text-sm font-semibold text-slate-700 dark:text-slate-200 uppercase tracking-wide">Contribuinte</h2>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">CPF/CNPJ</label>
<InputText
:model-value="formatarDocumento(contribuinte?.documento)"
class="w-full"
size="small"
disabled
/>
</div>
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Nome</label>
<InputText
:model-value="contribuinte?.nomeCompleto"
class="w-full"
size="small"
disabled
/>
</div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5 space-y-4">
<h2 class="text-sm font-semibold text-slate-700 dark:text-slate-200 uppercase tracking-wide">Dados da taxa</h2>
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Taxa *</label>
<Select
:model-value="tributoSelecionadoId"
:options="catalogo"
:option-label="labelCatalogo"
option-value="id"
placeholder="Selecione a taxa"
show-clear
class="w-full"
size="small"
:loading="carregandoTributo"
@update:model-value="onTributoChange"
/>
<p v-if="erros.tributo" class="text-xs text-red-500 mt-1">{{ erros.tributo }}</p>
<p v-if="catalogo.length === 0" class="text-xs text-amber-600 dark:text-amber-400 mt-2">
Nenhuma taxa disponível para emissão no portal. Verifique com a prefeitura.
</p>
</div>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Período referência *</label>
<InputText
v-model="periodoReferencia"
type="month"
class="w-full"
size="small"
@change="onPeriodoChange"
/>
<p v-if="erros.periodoReferencia" class="text-xs text-red-500 mt-1">{{ erros.periodoReferencia }}</p>
</div>
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Vencimento débito</label>
<InputText
:model-value="vencimentoDebito"
class="w-full"
size="small"
disabled
/>
</div>
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Vencimento guia *</label>
<InputText
v-model="vencimentoGuia"
type="date"
class="w-full"
size="small"
/>
<p v-if="erros.vencimentoGuia" class="text-xs text-red-500 mt-1">{{ erros.vencimentoGuia }}</p>
</div>
</div>
<CamposEmissaoTaxa
v-if="tributoSelecionadoId"
:itens-tributo="itensTributo"
:itens-informativos-doc="itensInformativosDoc"
:formula-selecionada="formulaSelecionada"
:valores-itens="valoresItens"
:valor-taxa="valorTaxa"
:erros="erros"
@update:valores-itens="valoresItens = $event"
@update:valor-taxa="valorTaxa = $event"
/>
<div v-if="tributoSelecionadoId" class="flex gap-2">
<Button
label="Calcular valores"
icon="pi pi-calculator"
size="small"
severity="secondary"
outlined
:loading="carregandoCalculo"
@click="recalcularTotais"
/>
</div>
<div v-if="totalizadores" class="rounded-lg border border-slate-200 dark:border-slate-600 divide-y divide-slate-100 dark:divide-slate-700 text-sm">
<div class="flex justify-between px-4 py-2"><span class="text-slate-500">Principal</span><span>R$ {{ totalizadores.principal }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-slate-500">Multa</span><span>R$ {{ totalizadores.multa }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-slate-500">Juros</span><span>R$ {{ totalizadores.juros }}</span></div>
<div class="flex justify-between px-4 py-3 font-bold text-slate-800 dark:text-slate-100"><span>Total</span><span>R$ {{ totalizadores.total }}</span></div>
</div>
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Observação</label>
<InputText v-model="observacao" class="w-full" size="small" />
</div>
</div>
<p v-if="mensagemErro" class="text-sm text-red-600 dark:text-red-400">{{ mensagemErro }}</p>
<Button
label="Emitir taxa"
icon="pi pi-check"
:loading="carregandoEmissao"
:disabled="!tributoSelecionadoId"
@click="onSubmit"
/>
</template>
<template v-else>
<div class="bg-white dark:bg-slate-800 rounded-xl border border-emerald-200 dark:border-emerald-800 p-8 text-center space-y-4">
<div class="w-14 h-14 bg-emerald-100 dark:bg-emerald-900/30 rounded-full flex items-center justify-center mx-auto">
<i class="pi pi-check text-emerald-600 dark:text-emerald-400 text-2xl" aria-hidden="true" />
</div>
<div>
<h2 class="text-lg font-bold text-slate-800 dark:text-slate-100">Taxa emitida com sucesso</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
Protocolo: <strong>{{ resultadoEmissao?.numeroProtocolo || '—' }}</strong>
</p>
</div>
<div class="flex flex-wrap justify-center gap-3">
<Button label="Imprimir guia" icon="pi pi-file-pdf" @click="imprimirGuia" />
<Button label="Ver taxas emitidas" severity="secondary" outlined @click="router.push('/portal/taxas')" />
<Button label="Nova emissão" severity="secondary" text @click="novaEmissao" />
</div>
</div>
</template>
</div>
</template>