From e4c468e61ee638d8ad82cc39c430080a541f1c0f Mon Sep 17 00:00:00 2001 From: GUILHERME Date: Fri, 22 May 2026 16:21:59 -0300 Subject: [PATCH] =?UTF-8?q?feat(portal):=20extratos=20reais,=20certid?= =?UTF-8?q?=C3=A3o=20din=C3=A2mica=20e=20filtros=20self-scoped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integra débitos, pagamentos e guias emitidas com API via composables e modais de extrato. Simplifica filtros do portal ao escopo do contribuinte logado. Refatora emissão pública de certidão com modelos dinâmicos e contrato idModelo. Corrige status de taxas (2=Paga, 3=Cancelada) e melhorias no proxy BFF/Keycloak. Co-authored-by: Cursor --- docs/api-backend.md | 178 ++++--- package-lock.json | 40 -- server/api/proxy/[...path].ts | 3 +- server/utils/keycloak.ts | 24 +- src/components/extrato/ModalDetalhesGuia.vue | 124 +++++ .../extrato/ModalTransacoesContaCorrente.vue | 71 +++ src/composables/useExtratoDebitosPortal.js | 212 +++++++++ src/composables/useExtratoPagamentosPortal.js | 197 ++++++++ src/composables/useGuiasEmitidasPortal.js | 106 +++++ src/layouts/portal.vue | 1 + src/pages/index.vue | 18 +- src/pages/portal/debitos.vue | 450 ++++++++---------- src/pages/portal/guias.vue | 170 +++++++ src/pages/portal/pagamentos.vue | 284 +++++------ src/pages/portal/painel.vue | 3 +- src/pages/portal/taxas/index.vue | 8 +- src/pages/servicos/certidao.vue | 183 +++++-- src/services/certidaoService.js | 11 +- src/services/portalService.js | 84 +++- src/utils/formatador.js | 49 ++ 20 files changed, 1631 insertions(+), 585 deletions(-) create mode 100644 src/components/extrato/ModalDetalhesGuia.vue create mode 100644 src/components/extrato/ModalTransacoesContaCorrente.vue create mode 100644 src/composables/useExtratoDebitosPortal.js create mode 100644 src/composables/useExtratoPagamentosPortal.js create mode 100644 src/composables/useGuiasEmitidasPortal.js create mode 100644 src/pages/portal/guias.vue create mode 100644 src/utils/formatador.js diff --git a/docs/api-backend.md b/docs/api-backend.md index 67bf6ea..c5f5263 100644 --- a/docs/api-backend.md +++ b/docs/api-backend.md @@ -86,6 +86,39 @@ Consulta a situação fiscal do contribuinte. --- +#### `GET /publico/certidao/modelos` + +Lista modelos de certidão **públicos** disponíveis para o contribuinte, filtrados pelo `cadastro.tipo` (PF=1, PJ=2) — paridade com o combo do core. + +**Query params:** +| Param | Tipo | Obrigatório | Descrição | +|---|---|---|---| +| `documento` | string | sim | CPF/CNPJ sem formatação — resolve o destinatário via cadastro | +| `titulo` | string | não | Filtro parcial no título do modelo | +| `page` | int | não | Página (default: 0) | +| `size` | int | não | Tamanho (default: 50) | + +**Response `data`:** +```json +{ + "paginasTotais": 1, + "elementosTotais": 2, + "data": [ + { + "id": 12, + "titulo": "Certidão Negativa de Débitos", + "descricaoTipoCertidao": "Certidão Negativa", + "validadeDias": 180, + "destinatario": 1 + } + ] +} +``` + +Filtros aplicados no backend: `destinatario` = tipo do cadastro · `publico=1` · vigência válida · modelo com arquivo DOCX anexado. + +--- + #### `GET /publico/certidao/emitir` Emite a certidão em PDF. @@ -94,7 +127,8 @@ Emite a certidão em PDF. | Param | Tipo | Obrigatório | Descrição | |---|---|---|---| | `documento` | string | sim | CPF/CNPJ sem formatação | -| `tipoCertidao` | string | sim | `NEGATIVA` · `POSITIVA` · `POSITIVA_EFEITOS_NEGATIVA` | +| `idModelo` | long | sim | ID do modelo selecionado em `/modelos` | +| `finalidade` | string | não | Finalidade informada pelo contribuinte (default: "Emissão pelo portal público") | **Response:** `application/pdf` (binário direto, sem envelope) @@ -396,45 +430,59 @@ Lista das últimas atividades do contribuinte. --- -### Débitos +### Débitos (Extrato) #### `GET /contribuinte/debitos` -Lista os débitos do contribuinte com filtros opcionais. +Lista débitos agrupados por conta/tributo (`ContaCorrenteTributoDTO`), com os mesmos filtros do extrato interno. -**Query params:** -| Param | Tipo | Obrigatório | Descrição | -|---|---|---|---| -| `tipo` | string | não | `IPTU` · `ISS` · `TAXA` · `MULTA` | -| `status` | string | não | `VENCIDO` · `A_VENCER` · `PARCELADO` | +**Query params:** `idTaxa`, `idContaTributo`, `periodoIni` (YYYYMM), `periodoFim` (YYYYMM), `inscMunicipal`, `idEstadoConta` (1=débito, 2=zero, 3=crédito) -**Response `data`:** -```json -{ - "content": [ - { - "id": "deb1", - "descricao": "IPTU 2025 — Cota 4/10", - "tipo": "IPTU", - "referencia": "ABR/2025", - "vencimento": "30/04/2025", - "valor": 125.90, - "valorAtualizado": 138.49, - "status": "VENCIDO" - } - ] -} -``` - -`valorAtualizado` inclui juros e multa (pode ser igual a `valor` se em dia). +**Response `data`:** array de `ContaCorrenteTributoDTO` com lista `debitos` aninhada. --- -#### `GET /contribuinte/debitos/{id}/guia` +#### `GET /contribuinte/debitos/tributos/{codigo}` -Emite a guia de pagamento (boleto/DAM) de um débito específico em PDF. +Busca tributo por código para filtros. -**Path param:** `id` — ID do débito +--- + +#### `GET /contribuinte/debitos/contas-tributo/{codigo}` + +Busca conta tributo por código para filtros. + +--- + +#### `GET /contribuinte/debitos/{idContaCorrente}/transacoes` + +Transações da conta corrente (somente se pertencer ao contribuinte logado). + +--- + +#### `POST /contribuinte/debitos/gerar-guia` + +Gera guia para débitos selecionados. Body: `GerarGuiaDebitosRequestDTO`. + +**Response `data`:** `GerarGuiaResponseDTO` (`idDoctoArr`, `numeroGuia`, etc.) + +--- + +#### `GET /contribuinte/debitos/guia/{idDoctoArr}` + +PDF de guia já emitida (validação de ownership). + +--- + +#### `GET /contribuinte/debitos/{idContaCorrente}/guia` + +Gera e retorna PDF de guia para um único débito. + +--- + +#### `POST /contribuinte/debitos/extrato-pdf` + +Gera PDF do extrato de débitos. Body: `GerarExtratoDebitosRequestDTO`. **Response:** `application/pdf` @@ -514,47 +562,58 @@ Lista os processos de alvará do contribuinte. --- -### Pagamentos +### Pagamentos (Extrato) #### `GET /contribuinte/pagamentos` -Histórico de pagamentos do contribuinte, filtrável por ano. +Lista pagamentos agrupados por tributo (`ExtratoPagamentoTributoDTO`). -**Query params:** -| Param | Tipo | Obrigatório | Descrição | -|---|---|---|---| -| `ano` | integer | não | Ano de referência (padrão: ano atual) | +**Query params:** `idTaxa`, `idContaTributo`, `pagInicio`, `pagFim`, `periodoIni`, `periodoFim`, `ano` (atalho para intervalo anual) -**Response `data`:** -```json -{ - "content": [ - { - "id": "pag1", - "descricao": "IPTU 2025 — Cota 3/10", - "referencia": "MAR/2025", - "dataPagamento": "28/03/2025", - "formaPagamento": "PIX", - "valor": 125.90 - } - ] -} -``` - -`formaPagamento` possíveis: `BOLETO` · `PIX` · `CARTAO` · `TRANSFERENCIA` · `ESPECIE` +**Response `data`:** array com `pagamentos` aninhados (principal, multa, juros, desconto, total). --- -#### `GET /contribuinte/pagamentos/{id}/comprovante` +#### `POST /contribuinte/pagamentos/extrato-pdf` -Baixa o comprovante de um pagamento em PDF. - -**Path param:** `id` — ID do pagamento +Gera PDF do extrato de pagamentos. Body: `GerarExtratoPagamentosRequestDTO`. **Response:** `application/pdf` --- +#### `GET /contribuinte/pagamentos/{idContaCorrente}/comprovante` + +Baixa comprovante de pagamento em PDF (quando houver `LancamentoTaxa` vinculado). + +**Response:** `application/pdf` ou `404` + +--- + +### Guias Emitidas + +#### `GET /contribuinte/guias` + +Lista guias do contribuinte logado (CPF/CNPJ injetado no backend). Paginado. + +**Query params:** `numeroGuia`, `dataEmissaoInicio/Fim`, `dataVencimentoInicio/Fim`, `status`, `valorMinimo`, `valorMaximo`, `page`, `size` + +**Response `data`:** `PageDTO` + +--- + +#### `GET /contribuinte/guias/{id}` + +Detalhes completos da guia (`GuiaPagamentoDTO`). + +--- + +#### `GET /contribuinte/guias/{id}/pdf` + +PDF da guia. + +--- + ### Dados Cadastrais #### `GET /contribuinte/dados` @@ -673,8 +732,9 @@ O `preferred_username` do JWT é o CPF/CNPJ sem formatação — usado como iden | Endpoint | Frontend pronto | Backend pronto | Mock | |---|---|---|---| | `GET /publico/prefeitura/{dominio}` | ✓ | ✓ | — | -| `GET /publico/certidao/consultar` | ✓ | pendente | ✓ | -| `GET /publico/certidao/emitir` | ✓ | pendente | ✓ | +| `GET /publico/certidao/consultar` | ✓ | ✓ | — | +| `GET /publico/certidao/modelos` | ✓ | ✓ | — | +| `GET /publico/certidao/emitir` | ✓ | ✓ | — | | `GET /publico/iptu/consultar` | ✓ | pendente | ✓ | | `GET /publico/iptu/carne` | ✓ | pendente | ✓ | | `GET /publico/iptu/boleto` | ✓ | pendente | ✓ | diff --git a/package-lock.json b/package-lock.json index 6b6b1ae..8ef36aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1686,24 +1686,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@nuxt/schema": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.4.6.tgz", - "integrity": "sha512-7FDMuD+skbFMgfF2ORYKEAKEuEFbu2oS60dln5uVtn94c8DHWCseJSrT3FUHzVUlVwyhztPU6stzB44dEoWAzw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vue/shared": "^3.5.34", - "defu": "^6.1.7", - "pathe": "^2.0.3", - "pkg-types": "^2.3.1", - "std-env": "^4.1.0" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/@nuxt/telemetry": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.8.0.tgz", @@ -5587,17 +5569,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -7940,7 +7911,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7961,7 +7931,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7982,7 +7951,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8003,7 +7971,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8024,7 +7991,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8045,7 +8011,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8066,7 +8031,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8087,7 +8051,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8108,7 +8071,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8129,7 +8091,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8150,7 +8111,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ diff --git a/server/api/proxy/[...path].ts b/server/api/proxy/[...path].ts index d71fdc1..bb915bf 100644 --- a/server/api/proxy/[...path].ts +++ b/server/api/proxy/[...path].ts @@ -58,8 +58,9 @@ export default defineEventHandler(async (event) => { }) setResponseStatus(event, res.status) + const skipResponseHeaders = new Set(['transfer-encoding', 'content-encoding', 'content-length']) for (const [name, value] of res.headers.entries()) { - if (name === 'transfer-encoding') continue + if (skipResponseHeaders.has(name.toLowerCase())) continue setResponseHeader(event, name, value) } return res._data diff --git a/server/utils/keycloak.ts b/server/utils/keycloak.ts index 6f9eb37..0067dc5 100644 --- a/server/utils/keycloak.ts +++ b/server/utils/keycloak.ts @@ -51,14 +51,22 @@ export async function exchangeCodeForTokens(opts: { redirect_uri: opts.redirectUri, code_verifier: opts.codeVerifier, }) - return await $fetch( - `${realmBase()}/protocol/openid-connect/token`, - { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: body.toString(), - }, - ) + try { + return await $fetch( + `${realmBase()}/protocol/openid-connect/token`, + { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }, + ) + } catch (err: unknown) { + const fetchErr = err as { data?: { error?: string; error_description?: string }; message?: string } + const kcError = fetchErr.data?.error + const kcDesc = fetchErr.data?.error_description + const detail = kcDesc ?? kcError ?? fetchErr.message ?? 'erro desconhecido' + throw new Error(`Keycloak token: ${detail}`) + } } export async function refreshTokens(refreshToken: string): Promise { diff --git a/src/components/extrato/ModalDetalhesGuia.vue b/src/components/extrato/ModalDetalhesGuia.vue new file mode 100644 index 0000000..f4119fa --- /dev/null +++ b/src/components/extrato/ModalDetalhesGuia.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/components/extrato/ModalTransacoesContaCorrente.vue b/src/components/extrato/ModalTransacoesContaCorrente.vue new file mode 100644 index 0000000..3e5be52 --- /dev/null +++ b/src/components/extrato/ModalTransacoesContaCorrente.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/composables/useExtratoDebitosPortal.js b/src/composables/useExtratoDebitosPortal.js new file mode 100644 index 0000000..2e0c325 --- /dev/null +++ b/src/composables/useExtratoDebitosPortal.js @@ -0,0 +1,212 @@ +import { ref, computed } from 'vue' +import { portalService } from '@/services/portalService' +import { formatDateISO, dateToYYYYMM, abrirPdf, baixarPdf } from '@/utils/formatador' + +export function useExtratoDebitosPortal() { + const resultados = ref([]) + const isLoading = ref(false) + const isLoadingGuia = ref(false) + const isLoadingExtrato = ref(false) + const mensagemErro = ref('') + const dataVencimento = ref(null) + const erros = ref({}) + + const filtro = ref({ + periodoIni: null, + periodoFim: null, + estadoConta: null, + }) + + const opcoesEstadoConta = [ + { label: 'Todos', value: null }, + { label: 'Saldo Débito', value: 1 }, + { label: 'Saldo Zero', value: 2 }, + { label: 'Saldo Crédito', value: 3 }, + ] + + const temSelecionado = computed(() => + resultados.value.some(t => t.selecionados?.length > 0) + ) + + const totalizadores = computed(() => { + let principal = 0, multa = 0, juros = 0, valorTotal = 0 + resultados.value.forEach(t => { + t.selecionados?.forEach(d => { + principal += d.valorAtulPrincipal ?? d.valorPrincipal ?? 0 + multa += d.valorAtulMulta ?? d.valorMulta ?? 0 + juros += d.valorAtulJuros ?? d.valorJuros ?? 0 + valorTotal += d.valorTotal ?? 0 + }) + }) + return { principal, multa, juros, valorTotal } + }) + + function mapearDebitos(dados) { + return dados.map(item => ({ + ...item, + debitos: (item.debitos || []).map((debito, idx) => ({ + ...debito, + id: `${item.idContaTributo}_${idx}`, + })), + selecionados: [], + })) + } + + async function consultar() { + isLoading.value = true + mensagemErro.value = '' + resultados.value = [] + try { + const params = { + periodoIni: filtro.value.periodoIni ? dateToYYYYMM(filtro.value.periodoIni) : undefined, + periodoFim: filtro.value.periodoFim ? dateToYYYYMM(filtro.value.periodoFim) : undefined, + idEstadoConta: filtro.value.estadoConta, + } + const res = await portalService.getDebitosExtrato(params) + const dados = res.data ?? [] + resultados.value = mapearDebitos(dados) + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao consultar débitos.' + } finally { + isLoading.value = false + } + } + + function limparFiltros() { + filtro.value = { + periodoIni: null, + periodoFim: null, + estadoConta: null, + } + resultados.value = [] + mensagemErro.value = '' + } + + function montarDtoGuia() { + return { + listaDebitos: resultados.value + .filter(t => t.selecionados?.length > 0) + .map(t => ({ + idTributo: t.idTributo, + identificador: t.identificador, + descricaoTributo: t.descricaoTributo, + siglaTributo: t.siglaTributo, + descricaoContaTributo: t.descricaoContaTributo, + idContaTributo: t.idContaTributo, + totalPagamentos: t.totalPagamentos, + debitos: t.selecionados.map(mapDebitoPayload), + })), + dataVencimento: formatDateISO(dataVencimento.value), + } + } + + function montarDtoExtrato(usarSelecionados) { + return { + listaDebitos: resultados.value + .filter(t => (usarSelecionados ? t.selecionados?.length > 0 : t.debitos?.length > 0)) + .map(t => ({ + idTributo: t.idTributo, + identificador: t.identificador, + descricaoTributo: t.descricaoTributo, + siglaTributo: t.siglaTributo, + descricaoContaTributo: t.descricaoContaTributo, + idContaTributo: t.idContaTributo, + totalPagamentos: t.totalPagamentos, + debitos: (usarSelecionados ? t.selecionados : t.debitos).map(mapDebitoPayload), + })), + } + } + + function mapDebitoPayload(debito) { + return { + idContaCorrente: debito.idContaCorrente, + numDoc: debito.numDoc, + codigoEstadoConta: debito.codigoEstadoConta, + estadoConta: debito.estadoConta, + dataVencimento: debito.dataVencimento, + periodoRef: debito.periodoRef, + numParcela: debito.numParcela, + valorPrincipal: debito.valorPrincipal, + valorJuros: debito.valorJuros, + valorMulta: debito.valorMulta, + valorCorrecao: debito.valorCorrecao, + valorAtulPrincipal: debito.valorAtulPrincipal, + valorAtulJuros: debito.valorAtulJuros, + valorAtulMulta: debito.valorAtulMulta, + valorAtulCorrecao: debito.valorAtulCorrecao, + valorTotal: debito.valorTotal, + } + } + + async function gerarGuia() { + if (!temSelecionado.value) { + mensagemErro.value = 'Selecione pelo menos um débito.' + return + } + if (!dataVencimento.value) { + erros.value.dataVencimento = 'Informe a data de vencimento.' + return + } + isLoadingGuia.value = true + mensagemErro.value = '' + try { + const res = await portalService.gerarGuiaDebitos(montarDtoGuia()) + const idDoctoArr = res.data?.idDoctoArr ?? res.data?.idDoctoarr + const buf = await portalService.baixarGuiaPdf(idDoctoArr) + abrirPdf(buf) + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao gerar guia de pagamento.' + } finally { + isLoadingGuia.value = false + } + } + + async function gerarExtratoPdf(selecionados = true) { + const dto = montarDtoExtrato(selecionados) + const total = dto.listaDebitos.reduce((s, t) => s + (t.debitos?.length ?? 0), 0) + if (total === 0) { + mensagemErro.value = selecionados + ? 'Selecione pelo menos um débito.' + : 'Não há débitos para gerar o extrato.' + return + } + isLoadingExtrato.value = true + try { + const buf = await portalService.gerarExtratoDebitosPdf(dto) + baixarPdf(buf, selecionados ? 'extrato-debitos.pdf' : 'extrato-todos-debitos.pdf') + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao gerar extrato.' + } finally { + isLoadingExtrato.value = false + } + } + + function getEstadoSeverity(codigo) { + switch (codigo) { + case 0: return 'warning' + case 1: return 'danger' + case 2: return 'success' + case 3: return 'danger' + default: return 'info' + } + } + + return { + resultados, + isLoading, + isLoadingGuia, + isLoadingExtrato, + mensagemErro, + dataVencimento, + erros, + filtro, + opcoesEstadoConta, + temSelecionado, + totalizadores, + consultar, + limparFiltros, + gerarGuia, + gerarExtratoPdf, + getEstadoSeverity, + } +} diff --git a/src/composables/useExtratoPagamentosPortal.js b/src/composables/useExtratoPagamentosPortal.js new file mode 100644 index 0000000..b6fce27 --- /dev/null +++ b/src/composables/useExtratoPagamentosPortal.js @@ -0,0 +1,197 @@ +import { ref, computed } from 'vue' +import { portalService } from '@/services/portalService' +import { formatDateISO, baixarPdf } from '@/utils/formatador' + +function formatDateForDisplay(date, monthOnly = false) { + if (!date) return '-' + const d = new Date(date) + if (isNaN(d.getTime())) return '-' + if (monthOnly) { + return `${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}` + } + return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}` +} + +function montarParametrosTributoConta(lista) { + if (!lista?.length) { + return { + codTributo: '-', + descricaoTributo: '-', + codContaTributo: '-', + descricaoContaTributo: '-', + } + } + + const chaves = new Map() + for (const item of lista) { + const key = `${item.idTributo ?? ''}_${item.idContaTributo ?? ''}` + if (!chaves.has(key)) chaves.set(key, item) + } + + if (chaves.size === 1) { + const t = [...chaves.values()][0] + return { + codTributo: t.idTributo != null ? String(t.idTributo) : (t.identificador ?? '-'), + descricaoTributo: t.descricaoTributo || '-', + codContaTributo: t.idContaTributo != null ? String(t.idContaTributo) : '-', + descricaoContaTributo: t.descricaoContaTributo || '-', + } + } + + const tributos = [...new Set(lista.map(t => t.descricaoTributo).filter(Boolean))] + return { + codTributo: 'Diversos', + descricaoTributo: tributos.length ? tributos.join(', ') : 'Diversos tributos', + codContaTributo: '-', + descricaoContaTributo: '-', + } +} + +export function useExtratoPagamentosPortal() { + const tributos = ref([]) + const isLoading = ref(false) + const isLoadingPdf = ref(false) + const mensagemErro = ref('') + + const filtro = ref({ + debitosInicio: null, + debitosFim: null, + pagInicio: null, + pagFim: null, + }) + + const totais = computed(() => { + let principal = 0, multa = 0, juros = 0, desconto = 0, total = 0 + for (const t of tributos.value) { + for (const p of t.pagamentos || []) { + principal += p.lancado ?? 0 + multa += p.multa ?? 0 + juros += p.juros ?? 0 + desconto += p.desconto ?? 0 + total += p.total ?? 0 + } + } + return { principal, multa, juros, desconto, total } + }) + + async function consultar() { + isLoading.value = true + mensagemErro.value = '' + tributos.value = [] + try { + const params = { + pagInicio: formatDateISO(filtro.value.pagInicio) || undefined, + pagFim: formatDateISO(filtro.value.pagFim) || undefined, + periodoIni: formatDateISO(filtro.value.debitosInicio) || undefined, + periodoFim: formatDateISO(filtro.value.debitosFim) || undefined, + } + const res = await portalService.getPagamentosExtrato(params) + const dados = res.data ?? [] + tributos.value = dados.map((item, idx) => ({ + idTributo: item.idTributo, + idContaTributo: item.idContaTributo, + identificador: item.identificador, + descricaoTributo: item.descricaoTributo, + descricaoContaTributo: item.descricaoContaTributo, + totalPagamentos: item.totalPagamentos ?? 0, + pagamentos: (item.pagamentos || []).map((p, i) => ({ + id: `${item.idContaTributo}_${i}_${p.idContaCorrente ?? i}`, + idContaCorrente: p.idContaCorrente, + ndoc: p.numDoc, + nguia: p.numGuia, + vencimento: p.dataVencimento, + pagamento: p.dataPagamento, + refer: p.periodoRef, + lancado: p.valorPrincipal ?? 0, + multa: p.valorMulta ?? 0, + juros: p.valorJuros ?? 0, + desconto: p.valorDesconto ?? 0, + total: p.valorTotal ?? 0, + })), + })) + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao consultar pagamentos.' + } finally { + isLoading.value = false + } + } + + function limparFiltros() { + filtro.value = { + debitosInicio: null, + debitosFim: null, + pagInicio: null, + pagFim: null, + } + tributos.value = [] + } + + async function gerarPdf() { + if (!tributos.value.length) { + mensagemErro.value = 'Consulte os pagamentos antes de gerar o PDF.' + return + } + const listaPagamentos = tributos.value.map(t => ({ + idTributo: t.idTributo, + identificador: t.identificador, + descricaoTributo: t.descricaoTributo, + siglaTributo: t.siglaTributo, + idContaTributo: t.idContaTributo, + descricaoContaTributo: t.descricaoContaTributo, + totalPagamentos: t.totalPagamentos, + pagamentos: (t.pagamentos || []).map(p => ({ + numDoc: p.ndoc, + numGuia: p.nguia, + dataVencimento: p.vencimento, + dataPagamento: p.pagamento, + periodoRef: p.refer, + valorPrincipal: p.lancado, + valorJuros: p.juros, + valorMulta: p.multa, + valorDesconto: p.desconto, + valorTotal: p.total, + })), + })) + + const dto = { + listaPagamentos, + ...montarParametrosTributoConta(tributos.value), + periodoDebitosInicio: formatDateForDisplay(filtro.value.debitosInicio, true), + periodoDebitosFim: formatDateForDisplay(filtro.value.debitosFim, true), + periodoPagamentosInicio: formatDateForDisplay(filtro.value.pagInicio), + periodoPagamentosFim: formatDateForDisplay(filtro.value.pagFim), + } + + isLoadingPdf.value = true + try { + const buf = await portalService.gerarExtratoPagamentosPdf(dto) + baixarPdf(buf, 'extrato-pagamentos.pdf') + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao gerar PDF.' + } finally { + isLoadingPdf.value = false + } + } + + async function baixarComprovante(pag) { + if (!pag.idContaCorrente) return null + try { + return await portalService.getComprovante(pag.idContaCorrente) + } catch { + return null + } + } + + return { + tributos, + isLoading, + isLoadingPdf, + mensagemErro, + filtro, + totais, + consultar, + limparFiltros, + gerarPdf, + baixarComprovante, + } +} diff --git a/src/composables/useGuiasEmitidasPortal.js b/src/composables/useGuiasEmitidasPortal.js new file mode 100644 index 0000000..94a15f4 --- /dev/null +++ b/src/composables/useGuiasEmitidasPortal.js @@ -0,0 +1,106 @@ +import { ref } from 'vue' +import { portalService } from '@/services/portalService' +import { formatDateISO, abrirPdf } from '@/utils/formatador' + +export function useGuiasEmitidasPortal() { + const guias = ref([]) + const isLoading = ref(false) + const mensagemErro = ref('') + const pagina = ref(0) + const totalPaginas = ref(0) + const totalElementos = ref(0) + const tamanhoPagina = ref(15) + + const filtro = ref({ + numeroGuia: '', + status: null, + valorMinimo: null, + valorMaximo: null, + dataEmissaoInicio: null, + dataEmissaoFim: null, + dataVencimentoInicio: null, + dataVencimentoFim: null, + }) + + const statusOptions = [ + { label: 'Não Processado', value: 0 }, + { label: 'Emitida/Ativa', value: 1 }, + { label: 'Paga', value: 2 }, + { label: 'Cancelada', value: 3 }, + ] + + async function consultar() { + isLoading.value = true + mensagemErro.value = '' + try { + const params = { + page: pagina.value, + size: tamanhoPagina.value, + numeroGuia: filtro.value.numeroGuia || undefined, + status: filtro.value.status ?? undefined, + valorMinimo: filtro.value.valorMinimo ?? undefined, + valorMaximo: filtro.value.valorMaximo ?? undefined, + dataEmissaoInicio: formatDateISO(filtro.value.dataEmissaoInicio) || undefined, + dataEmissaoFim: formatDateISO(filtro.value.dataEmissaoFim) || undefined, + dataVencimentoInicio: formatDateISO(filtro.value.dataVencimentoInicio) || undefined, + dataVencimentoFim: formatDateISO(filtro.value.dataVencimentoFim) || undefined, + } + const res = await portalService.listarGuias(params) + const pageData = res.data ?? {} + guias.value = pageData.data ?? [] + totalPaginas.value = pageData.paginasTotais ?? 0 + totalElementos.value = pageData.elementosTotais ?? 0 + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao consultar guias.' + guias.value = [] + } finally { + isLoading.value = false + } + } + + function limparFiltros() { + filtro.value = { + numeroGuia: '', status: null, valorMinimo: null, valorMaximo: null, + dataEmissaoInicio: null, dataEmissaoFim: null, + dataVencimentoInicio: null, dataVencimentoFim: null, + } + pagina.value = 0 + consultar() + } + + function mudarPagina(novaPagina) { + pagina.value = novaPagina + consultar() + } + + async function visualizarPdf(id) { + const buf = await portalService.baixarGuiaEmitidaPdf(id) + abrirPdf(buf) + } + + function formatarNumeroGuia(numero) { + return numero ? String(numero).padStart(10, '0') : '—' + } + + function getSeverityStatus(codigo) { + return { 0: 'danger', 1: 'info', 2: 'success', 3: 'warn' }[codigo] ?? 'secondary' + } + + return { + guias, + isLoading, + mensagemErro, + pagina, + totalPaginas, + totalElementos, + tamanhoPagina, + filtro, + statusOptions, + consultar, + limparFiltros, + mudarPagina, + visualizarPdf, + formatarNumeroGuia, + getSeverityStatus, + } +} diff --git a/src/layouts/portal.vue b/src/layouts/portal.vue index e3cfa24..4198a24 100644 --- a/src/layouts/portal.vue +++ b/src/layouts/portal.vue @@ -13,6 +13,7 @@ const menuAberto = ref(false) const navItems = [ { path: '/portal/painel', label: 'Painel', icon: 'pi-home' }, { path: '/portal/debitos', label: 'Débitos', icon: 'pi-receipt' }, + { path: '/portal/guias', label: 'Guias Emitidas', icon: 'pi-file' }, { path: '/portal/taxas', label: 'Taxas', icon: 'pi-file-export' }, { path: '/portal/certidoes', label: 'Certidões', icon: 'pi-file-check' }, { path: '/portal/pagamentos', label: 'Pagamentos', icon: 'pi-credit-card' }, diff --git a/src/pages/index.vue b/src/pages/index.vue index 032751d..d6ab3ca 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -11,6 +11,7 @@ import bgTutoia from '@/assets/images/bg-tutoia.jpeg' const { prefersReducedMotion } = useMotion() const router = useRouter() +const route = useRoute() const prefeitura = usePrefeituraStore() const { isAuthenticated, nomeUsuario, login } = useAuth() const { requested: focusLoginRequested, consume: consumeFocusLogin } = useFocusLoginInput() @@ -19,6 +20,12 @@ const documento = ref('') const erro = ref('') const carregando = ref(false) +const AUTH_ERROR_MESSAGES = { + exchange_failed: 'Login cancelado: o servidor não conseguiu validar a sessão com o Keycloak. O client secret local provavelmente está incorreto — peça o valor atual ao admin.', + invalid_state: 'Sessão de login expirada. Tente entrar novamente.', + missing_params: 'Resposta inválida do login. Tente entrar novamente.', +} + // Ref ao DocumentoInput — usado pelo botão "Entrar" do AppHeader pra focar o campo const documentoRef = ref(null) @@ -89,6 +96,14 @@ const AVISOS_FALLBACK = [ const avisos = ref(AVISOS_FALLBACK) onMounted(async () => { + const authError = route.query.auth_error + if (typeof authError === 'string') { + erro.value = AUTH_ERROR_MESSAGES[authError] ?? `Erro ao autenticar (${authError}).` + const query = { ...route.query } + delete query.auth_error + router.replace({ query }) + } + try { const res = await avisoService.listar(prefeitura.dominio) const lista = (res.data ?? []).map(a => ({ @@ -119,7 +134,8 @@ const servicosPublicos = [ ] const servicosAutenticados = [ - { icon: 'pi-receipt', titulo: 'Débitos e Guias', descricao: 'Emita guias de pagamento de todos os seus tributos.', to: '/portal/debitos' }, + { icon: 'pi-receipt', titulo: 'Extrato de Débitos', descricao: 'Consulte débitos e emita guias de pagamento.', to: '/portal/debitos' }, + { icon: 'pi-file', titulo: 'Guias Emitidas', descricao: 'Consulte e baixe guias de pagamento já emitidas.', to: '/portal/guias' }, { icon: 'pi-file-edit', titulo: 'Certidões Ativas', descricao: 'Acesse e reemita certidões emitidas anteriormente.', to: '/portal/certidoes' }, { icon: 'pi-credit-card', titulo: 'Pagamentos', descricao: 'Histórico completo com comprovantes para download.', to: '/portal/pagamentos' }, { icon: 'pi-user', titulo: 'Dados Cadastrais', descricao: 'Visualize e mantenha seus dados sempre atualizados.', to: '/portal/dados' }, diff --git a/src/pages/portal/debitos.vue b/src/pages/portal/debitos.vue index a456f82..602ab98 100644 --- a/src/pages/portal/debitos.vue +++ b/src/pages/portal/debitos.vue @@ -1,286 +1,224 @@ diff --git a/src/pages/portal/guias.vue b/src/pages/portal/guias.vue new file mode 100644 index 0000000..4ff4c9e --- /dev/null +++ b/src/pages/portal/guias.vue @@ -0,0 +1,170 @@ + + + diff --git a/src/pages/portal/pagamentos.vue b/src/pages/portal/pagamentos.vue index 3d395c7..6a14a2d 100644 --- a/src/pages/portal/pagamentos.vue +++ b/src/pages/portal/pagamentos.vue @@ -1,197 +1,151 @@ diff --git a/src/pages/portal/painel.vue b/src/pages/portal/painel.vue index 3ffcd84..026e78a 100644 --- a/src/pages/portal/painel.vue +++ b/src/pages/portal/painel.vue @@ -60,7 +60,8 @@ async function carregar() { } const acesRapidos = [ - { icon: 'pi-receipt', label: 'Emitir Guia', to: '/portal/debitos', cor: 'text-primary' }, + { icon: 'pi-receipt', label: 'Extrato Débitos', to: '/portal/debitos', cor: 'text-primary' }, + { icon: 'pi-file', label: 'Guias Emitidas', to: '/portal/guias', cor: 'text-primary' }, { icon: 'pi-file-export', label: 'Emitir Taxa', to: '/portal/taxas/emitir', cor: 'text-sky-600 dark:text-sky-400' }, { icon: 'pi-file-check', label: 'Nova Certidão', to: '/portal/certidoes', cor: 'text-emerald-600 dark:text-emerald-400' }, { icon: 'pi-credit-card', label: 'Pagamentos', to: '/portal/pagamentos', cor: 'text-amber-600 dark:text-amber-400' }, diff --git a/src/pages/portal/taxas/index.vue b/src/pages/portal/taxas/index.vue index d108ea6..718290d 100644 --- a/src/pages/portal/taxas/index.vue +++ b/src/pages/portal/taxas/index.vue @@ -22,8 +22,8 @@ const filtroProtocolo = ref('') const statusOpcoes = [ { value: 1, label: 'Emitida', classe: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400' }, - { value: 2, label: 'Cancelada', classe: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400' }, - { value: 3, label: 'Paga', classe: 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400' }, + { value: 2, label: 'Paga', classe: 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400' }, + { value: 3, label: 'Cancelada', classe: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400' }, ] const statusMap = Object.fromEntries(statusOpcoes.map(s => [s.value, s])) @@ -199,7 +199,7 @@ function formatarData(val) { @click="baixar(taxa, 'guia')" />