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/public/brasao.png b/public/brasao.png new file mode 100644 index 0000000..ef58b93 Binary files /dev/null and b/public/brasao.png differ diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index 7087e04..8dc90c4 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -3,6 +3,8 @@ import { z } from 'zod' const bodySchema = z.object({ documento: z.string().trim().min(11).max(20).optional(), returnTo: z.string().startsWith('/').max(200).optional(), + primary: z.string().regex(/^[0-9a-fA-F]{6}$/).optional(), + dark: z.boolean().optional(), }) export default defineEventHandler(async (event) => { @@ -14,11 +16,16 @@ export default defineEventHandler(async (event) => { const { codeVerifier, codeChallenge, state } = await generatePkce() const returnTo = body.data.returnTo ?? '/portal/painel' - await savePkceState(state, { - codeVerifier, - returnTo, - createdAt: Date.now(), - }) + 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({ @@ -26,6 +33,8 @@ export default defineEventHandler(async (event) => { state, redirectUri, loginHint: body.data.documento?.replace(/\D/g, ''), + primary: body.data.primary, + dark: body.data.dark, }) return { authUrl } diff --git a/server/api/proxy/[...path].ts b/server/api/proxy/[...path].ts index f1a3d27..88a6c86 100644 --- a/server/api/proxy/[...path].ts +++ b/server/api/proxy/[...path].ts @@ -46,6 +46,8 @@ export default defineEventHandler(async (event) => { const contentType = getHeader(event, 'content-type') if (contentType) headers['Content-Type'] = contentType + console.log(`[proxy] ${method} ${url} | X-Municipio: ${headers['X-Municipio']} | X-Dominio: ${headers['X-Dominio']} | auth: ${!!accessToken}`) + try { const res = await $fetch.raw(url, { method: method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', @@ -56,17 +58,48 @@ 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 } catch (err: unknown) { const fetchErr = err as { response?: { status?: number; _data?: unknown } } if (fetchErr.response) { - setResponseStatus(event, fetchErr.response.status ?? 500) - return fetchErr.response._data + const status = fetchErr.response.status ?? 500 + const raw = fetchErr.response._data + + // responseType: 'stream' faz _data ser ReadableStream mesmo em erros — + // lemos o stream e parseamos como JSON para que o cliente veja o envelope de erro + let body: unknown = raw + if (raw && typeof (raw as ReadableStream).getReader === 'function') { + const reader = (raw as ReadableStream).getReader() + const chunks: Uint8Array[] = [] + for (;;) { + const { value, done } = await reader.read() + if (done) break + if (value) chunks.push(value) + } + const totalLen = chunks.reduce((n, c) => n + c.length, 0) + const merged = new Uint8Array(totalLen) + let offset = 0 + for (const c of chunks) { merged.set(c, offset); offset += c.length } + const text = new TextDecoder().decode(merged) + try { body = JSON.parse(text) } catch { body = text } + } + + if (import.meta.dev) { + console.error(`[proxy] ERRO ${status} ← ${url}`, body) + } + + setResponseStatus(event, status) + 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.' }) } }) diff --git a/server/utils/keycloak.ts b/server/utils/keycloak.ts index 77abd80..0067dc5 100644 --- a/server/utils/keycloak.ts +++ b/server/utils/keycloak.ts @@ -18,6 +18,8 @@ export function buildAuthUrl(opts: { state: string redirectUri: string loginHint?: string + primary?: string + dark?: boolean }): string { const cfg = useRuntimeConfig() const params = new URLSearchParams({ @@ -30,6 +32,8 @@ export function buildAuthUrl(opts: { state: opts.state, }) if (opts.loginHint) params.set('login_hint', opts.loginHint) + if (opts.primary) params.set('primary', opts.primary) + if (opts.dark !== undefined) params.set('dark', String(opts.dark)) return `${realmBase()}/protocol/openid-connect/auth?${params.toString()}` } @@ -47,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/server/utils/prefeitura.ts b/server/utils/prefeitura.ts index 56b3dc4..9eda259 100644 --- a/server/utils/prefeitura.ts +++ b/server/utils/prefeitura.ts @@ -17,13 +17,18 @@ export async function fetchPrefeituraInfo(dominio: string): Promise {}) + } return info } catch (err) { console.error(`[prefeitura] lookup falhou para '${dominio}':`, (err as Error).message) diff --git a/src/assets/images/brasao.png b/src/assets/images/brasao.png new file mode 100644 index 0000000..ef58b93 Binary files /dev/null and b/src/assets/images/brasao.png differ diff --git a/src/components/auth/LoginModal.vue b/src/components/auth/LoginModal.vue index b281602..4417979 100644 --- a/src/components/auth/LoginModal.vue +++ b/src/components/auth/LoginModal.vue @@ -86,6 +86,7 @@ function onHide() { class="w-full" size="large" :loading="carregando" + :disabled="carregando" @click="entrar" /> diff --git a/src/components/common/AccessibilityWidget.vue b/src/components/common/AccessibilityWidget.vue index e673b71..a4e4dd1 100644 --- a/src/components/common/AccessibilityWidget.vue +++ b/src/components/common/AccessibilityWidget.vue @@ -1,5 +1,5 @@ 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/components/taxas/CamposEmissaoTaxa.vue b/src/components/taxas/CamposEmissaoTaxa.vue new file mode 100644 index 0000000..5ee0537 --- /dev/null +++ b/src/components/taxas/CamposEmissaoTaxa.vue @@ -0,0 +1,133 @@ + + + diff --git a/src/composables/useApi.ts b/src/composables/useApi.ts index 21d67b5..1569be7 100644 --- a/src/composables/useApi.ts +++ b/src/composables/useApi.ts @@ -18,13 +18,21 @@ export function useApi() { } async function request(path: string, options: FetchOptions = {}): Promise { - return await $fetch(buildUrl(path), { - ...options, - headers: { - 'X-Requested-With': 'fetch', - ...(options.headers ?? {}), - }, - }) + try { + return await $fetch(buildUrl(path), { + ...options, + headers: { + 'X-Requested-With': 'fetch', + ...(options.headers ?? {}), + }, + }) + } catch (err: unknown) { + if (import.meta.dev) { + const e = err as { status?: number; data?: unknown } + console.error(`[api] ${(options.method ?? 'GET').toUpperCase()} ${path} → ${e.status}`, e.data) + } + throw err + } } return { diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts index 659d803..74bd760 100644 --- a/src/composables/useAuth.ts +++ b/src/composables/useAuth.ts @@ -1,5 +1,6 @@ import { computed } from 'vue' import { useAuthStore } from '@/stores/authStore' +import { usePrefeituraStore } from '@/stores/prefeituraStore' interface MeResponse { name: string @@ -10,15 +11,33 @@ interface MeResponse { const FETCH_HEADERS = { 'X-Requested-With': 'fetch' } +const TEMPLATE_COLORS: Record = { + tutoia: 'f97316', + amber: 'f59e0b', + blue: '3b82f6', + indigo: '6366f1', + violet: '8b5cf6', + emerald: '10b981', + teal: '14b8a6', + rose: 'f43f5e', + zinc: '71717a', +} + export function useAuth() { const store = useAuthStore() const router = useRouter() async function login(documento?: string, returnTo?: string) { + const template = usePrefeituraStore().template as string | null + const primary = template ? (TEMPLATE_COLORS[template] ?? '') : '' + const dark = import.meta.client + ? (window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false) + : false + const res = await $fetch<{ authUrl: string }>('/api/auth/login', { method: 'POST', headers: FETCH_HEADERS, - body: { documento, returnTo }, + body: { documento, returnTo, primary: primary || undefined, dark }, }) if (import.meta.client) { window.location.href = res.authUrl diff --git a/src/composables/useEmissaoTaxaPortal.js b/src/composables/useEmissaoTaxaPortal.js new file mode 100644 index 0000000..d7d7159 --- /dev/null +++ b/src/composables/useEmissaoTaxaPortal.js @@ -0,0 +1,381 @@ +import { ref, computed } from 'vue' +import { z } from 'zod' +import { portalService } from '@/services/portalService' +import { taxaService } from '@/services/taxaService' +import { resolveCampoInformativo } from '@/utils/atributoMascara' +import { + formatarMoeda, + formatarDataParaAPI, + obterValorNumerico, + periodoMesParaAPI, + baixarPdf, +} from '@/utils/formatacao' +import { calcularValorTotal } from '@/utils/formulaCalculo' + +export function useEmissaoTaxaPortal() { + const contribuinte = ref(null) + const catalogo = ref([]) + const tributoSelecionadoId = ref(null) + const tributo = ref(null) + const itensTributo = ref([]) + const itensInformativosDoc = ref([]) + const formulaSelecionada = ref(null) + + const periodoReferencia = ref('') + const vencimentoDebito = ref('') + const vencimentoGuia = ref('') + const valorTaxa = ref('') + const valoresItens = ref({}) + const observacao = ref('') + + const dadosVencimento = ref(null) + const dadosMultaJuros = ref(null) + const resultadoEmissao = ref(null) + + const carregando = ref(false) + const carregandoCatalogo = ref(false) + const carregandoTributo = ref(false) + const carregandoCalculo = ref(false) + const carregandoEmissao = ref(false) + const mensagemErro = ref('') + const erros = ref({}) + + const temItensCalculo = computed(() => itensTributo.value.length > 0) + + const valorTotalCalculado = computed(() => + calcularValorTotal( + formulaSelecionada.value?.formula, + itensTributo.value, + valoresItens.value, + obterValorNumerico, + ) + ) + + const totalizadores = computed(() => { + if (!dadosMultaJuros.value) return null + const mj = dadosMultaJuros.value + const principal = temItensCalculo.value ? valorTotalCalculado.value : valorTaxa.value + const totalDescontos = (mj.descontoValorPrincipal || 0) + (mj.descontoValorMulta || 0) + (mj.descontoValorJuros || 0) + return { + principal: formatarMoeda(obterValorNumerico(principal)), + principalAtualizado: formatarMoeda(mj.valorPrincipalAtualizado || 0), + multa: formatarMoeda(mj.valorMulta || 0), + juros: formatarMoeda(mj.valorJuros || 0), + desconto: formatarMoeda(totalDescontos), + total: formatarMoeda(mj.valorTotalComDescontos ?? mj.valorTotal ?? 0), + } + }) + + async function carregarDadosIniciais() { + carregando.value = true + mensagemErro.value = '' + try { + const [resDados, resCatalogo] = await Promise.all([ + portalService.getDadosCadastrais(), + taxaService.getCatalogo(), + ]) + contribuinte.value = resDados.data + catalogo.value = resCatalogo.data ?? [] + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Não foi possível carregar os dados.' + } finally { + carregando.value = false + } + } + + function labelCatalogo(item) { + if (!item) return '' + return item.sigla ? `${item.sigla} — ${item.descricaoResumida || item.descricao}` : item.descricao + } + + async function onSelecionarTributo(id) { + tributoSelecionadoId.value = id + tributo.value = null + itensTributo.value = [] + itensInformativosDoc.value = [] + formulaSelecionada.value = null + valoresItens.value = {} + valorTaxa.value = '' + dadosVencimento.value = null + dadosMultaJuros.value = null + vencimentoDebito.value = '' + vencimentoGuia.value = '' + + if (!id) return + + carregandoTributo.value = true + try { + const res = await taxaService.getTributo(id) + aplicarTributo(res.data) + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao carregar a taxa selecionada.' + } finally { + carregandoTributo.value = false + } + } + + function aplicarTributo(taxa) { + tributo.value = taxa + if (taxa.formulasCalculo?.length > 0) { + const formulaAtiva = taxa.formulasCalculo.find(f => !f.dataFim) || taxa.formulasCalculo[0] + formulaSelecionada.value = formulaAtiva || null + itensTributo.value = formulaAtiva?.itensCalculo || [] + } else { + formulaSelecionada.value = null + itensTributo.value = [] + } + itensInformativosDoc.value = taxa.atributosInformativosDoc || [] + + const novosValores = {} + itensTributo.value.forEach(item => { + if (item.tipoVariavel === 2) { + novosValores[item.id] = item.valor ? formatarMoeda(item.valor) : '0,00' + } else if (item.tipoVariavel === 1 && item.tipoDado === 2) { + novosValores[item.id] = '0,00' + } else { + novosValores[item.id] = '' + } + }) + itensInformativosDoc.value.forEach(item => { + const campo = resolveCampoInformativo(item) + novosValores[item.id] = campo.tipoCampo === 'number' || item.tipoDado === 2 ? '0,00' : '' + }) + valoresItens.value = novosValores + } + + async function calcularVencimento() { + if (!tributo.value || !periodoReferencia.value) return null + const periodoApi = periodoMesParaAPI(periodoReferencia.value) + if (!periodoApi) return null + + carregandoCalculo.value = true + try { + const res = await taxaService.calcularVencimento({ + tributoId: tributo.value.idTaxa, + periodoReferencia: periodoApi, + }) + dadosVencimento.value = res.data + vencimentoDebito.value = res.data?.dataVencimento ?? '' + if (res.data?.dataVencimentoGuia) { + vencimentoGuia.value = res.data.dataVencimentoGuia + } + return res.data + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao calcular vencimento.' + return null + } finally { + carregandoCalculo.value = false + } + } + + async function calcularMultaJuros() { + if (!tributo.value || !periodoReferencia.value || !vencimentoDebito.value) return null + + const periodoApi = periodoMesParaAPI(periodoReferencia.value) + const valorPrincipal = temItensCalculo.value + ? obterValorNumerico(valorTotalCalculado.value) + : obterValorNumerico(valorTaxa.value) + + if (valorPrincipal <= 0) return null + + carregandoCalculo.value = true + try { + const res = await taxaService.calcularMultaJuros({ + tributoId: tributo.value.idTaxa, + periodoReferencia: periodoApi, + dataVencimentoDebito: vencimentoDebito.value, + valorPrincipal, + dataVencimentoGuia: vencimentoGuia.value || vencimentoDebito.value, + }) + dadosMultaJuros.value = res.data + return res.data + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao calcular multa e juros.' + return null + } finally { + carregandoCalculo.value = false + } + } + + async function onPeriodoChange() { + dadosMultaJuros.value = null + await calcularVencimento() + } + + async function recalcularTotais() { + if (!periodoReferencia.value || !vencimentoDebito.value) return + await calcularMultaJuros() + } + + function montarAtributosPayload() { + const itensEntrada = itensTributo.value.filter(i => i.tipoVariavel === 1) + const informativos = [ + ...itensTributo.value.filter(i => i.tipoVariavel === 3), + ...itensInformativosDoc.value, + ] + return [...itensEntrada, ...informativos].map(item => ({ + idItemCalculo: item.idTaxaCalc != null ? item.id : null, + idAtributo: item.idAtributo ?? null, + atributo: item.atributo, + valor: String(valoresItens.value[item.id] ?? ''), + tipoDado: item.tipoDado ?? item.tipoVariavel ?? null, + })) + } + + function validar() { + erros.value = {} + if (!tributoSelecionadoId.value) { + erros.value.tributo = 'Selecione uma taxa' + } + if (!periodoReferencia.value) { + erros.value.periodoReferencia = 'Período de referência é obrigatório' + } + if (!vencimentoDebito.value) { + erros.value.vencimentoDebito = 'Vencimento do débito é obrigatório' + } + if (!vencimentoGuia.value) { + erros.value.vencimentoGuia = 'Vencimento da guia é obrigatório' + } + + if (temItensCalculo.value) { + itensTributo.value.filter(i => i.tipoVariavel === 1).forEach(item => { + if (item.obrigatorio === false) return + const valor = valoresItens.value[item.id] + if (!valor || obterValorNumerico(valor) <= 0) { + erros.value[`item_${item.id}`] = `${item.descricao} deve ser maior que zero` + } + }) + } else { + const schema = z.string().refine(v => v.trim() !== '' && obterValorNumerico(v) > 0, { + message: 'Valor da taxa é obrigatório', + }) + const r = schema.safeParse(valorTaxa.value || '') + if (!r.success) erros.value.valorTaxa = r.error.issues[0].message + } + + const informativos = [ + ...itensTributo.value.filter(i => i.tipoVariavel === 3), + ...itensInformativosDoc.value, + ] + informativos.forEach(item => { + if (item.obrigatorio === false) return + const valor = valoresItens.value[item.id] + const label = item.descricao || item.atributo + if (item.tipoDado === 2) { + if (!valor || obterValorNumerico(valor) <= 0) { + erros.value[`item_${item.id}`] = `${label} é obrigatório` + } + } else if (!valor || String(valor).trim() === '') { + erros.value[`item_${item.id}`] = `${label} é obrigatório` + } + }) + + return Object.keys(erros.value).length === 0 + } + + async function emitirTaxa() { + mensagemErro.value = '' + if (!validar()) return false + + const mj = dadosMultaJuros.value || await calcularMultaJuros() + if (!mj) { + mensagemErro.value = 'Não foi possível calcular os valores da taxa.' + return false + } + + const periodoApi = periodoMesParaAPI(periodoReferencia.value) + const valorPrincipal = temItensCalculo.value + ? obterValorNumerico(valorTotalCalculado.value) + : obterValorNumerico(valorTaxa.value) + const totalDescontos = (mj.descontoValorPrincipal || 0) + (mj.descontoValorMulta || 0) + (mj.descontoValorJuros || 0) + + const payload = { + tributoId: tributo.value.idTaxa, + taxaCalcId: formulaSelecionada.value?.id ?? undefined, + periodoReferencia: periodoApi, + dataVencimentoGuia: formatarDataParaAPI(vencimentoGuia.value), + dataVencimentoDebito: formatarDataParaAPI(vencimentoDebito.value), + observacao: observacao.value?.trim() || undefined, + atributosTaxa: montarAtributosPayload(), + valores: { + valorPrincipal, + valorPrincipalAtualizado: mj.valorPrincipalAtualizado || valorPrincipal, + valorMulta: mj.valorMulta || 0, + valorJuros: mj.valorJuros || 0, + totalDescontos, + valorTotalComDescontos: mj.valorTotalComDescontos ?? mj.valorTotal ?? 0, + }, + } + + carregandoEmissao.value = true + try { + const res = await taxaService.lancar(payload) + resultadoEmissao.value = res.data + return true + } catch (e) { + mensagemErro.value = e?.data?.description ?? 'Erro ao emitir a taxa.' + return false + } finally { + carregandoEmissao.value = false + } + } + + async function imprimirGuia() { + if (!resultadoEmissao.value?.taxaId) return + try { + const buf = await taxaService.baixarGuia(resultadoEmissao.value.taxaId) + baixarPdf(buf, `guia-${resultadoEmissao.value.numeroProtocolo || resultadoEmissao.value.taxaId}.pdf`) + } catch { + mensagemErro.value = 'Erro ao gerar a guia.' + } + } + + function reiniciar() { + tributoSelecionadoId.value = null + tributo.value = null + periodoReferencia.value = '' + vencimentoDebito.value = '' + vencimentoGuia.value = '' + valorTaxa.value = '' + valoresItens.value = {} + observacao.value = '' + dadosVencimento.value = null + dadosMultaJuros.value = null + resultadoEmissao.value = null + erros.value = {} + mensagemErro.value = '' + } + + return { + contribuinte, + catalogo, + tributoSelecionadoId, + tributo, + itensTributo, + itensInformativosDoc, + formulaSelecionada, + periodoReferencia, + vencimentoDebito, + vencimentoGuia, + valorTaxa, + valoresItens, + observacao, + totalizadores, + resultadoEmissao, + carregando, + carregandoCatalogo, + carregandoTributo, + carregandoCalculo, + carregandoEmissao, + mensagemErro, + erros, + labelCatalogo, + carregarDadosIniciais, + onSelecionarTributo, + onPeriodoChange, + recalcularTotais, + emitirTaxa, + imprimirGuia, + reiniciar, + } +} diff --git a/src/composables/useExtratoDebitosPortal.js b/src/composables/useExtratoDebitosPortal.js new file mode 100644 index 0000000..94f357f --- /dev/null +++ b/src/composables/useExtratoDebitosPortal.js @@ -0,0 +1,227 @@ +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 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 => { + 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 + .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, + 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 (!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) { + 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, + temDebitoSelecionado, + 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/error.vue b/src/error.vue new file mode 100644 index 0000000..f7235f1 --- /dev/null +++ b/src/error.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/layouts/portal.vue b/src/layouts/portal.vue index b9cea00..4198a24 100644 --- a/src/layouts/portal.vue +++ b/src/layouts/portal.vue @@ -1,44 +1,83 @@ diff --git a/src/pages/entrar.vue b/src/pages/entrar.vue new file mode 100644 index 0000000..cbb404e --- /dev/null +++ b/src/pages/entrar.vue @@ -0,0 +1,5 @@ + diff --git a/src/pages/index.vue b/src/pages/index.vue index 8cabcb5..d6ab3ca 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -1,15 +1,17 @@ - - diff --git a/src/pages/portal/certidoes.vue b/src/pages/portal/certidoes.vue index 1e5fa65..645e9cc 100644 --- a/src/pages/portal/certidoes.vue +++ b/src/pages/portal/certidoes.vue @@ -1,12 +1,35 @@ diff --git a/src/pages/portal/guias.vue b/src/pages/portal/guias.vue new file mode 100644 index 0000000..7dd8ccf --- /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 7629017..6a14a2d 100644 --- a/src/pages/portal/pagamentos.vue +++ b/src/pages/portal/pagamentos.vue @@ -1,147 +1,151 @@ diff --git a/src/pages/portal/painel.vue b/src/pages/portal/painel.vue index dd1ab1b..026e78a 100644 --- a/src/pages/portal/painel.vue +++ b/src/pages/portal/painel.vue @@ -2,6 +2,7 @@ import { ref, onMounted } from 'vue' import { useAuth } from '@/composables/useAuth' import { portalService } from '@/services/portalService' +import { usePrefeituraStore } from '@/stores/prefeituraStore' definePageMeta({ layout: 'portal', @@ -9,13 +10,34 @@ definePageMeta({ }) const { nomeUsuario } = useAuth() +const prefeitura = usePrefeituraStore() const router = useRouter() +// ─── MOCKS APRESENTAÇÃO — remover antes do deploy ───────────────────────── +const MOCK_ATIVO = true +const RESUMO_MOCK = { + totalDebitos: 1250.90, + certidoesAtivas: 2, + ultimoPagamento: '2025-05-15', + valorUltimoPagamento: 430.00, + debitosVencidos: 1, +} +const ATIVIDADES_MOCK = [ + { tipo: 'PAGAMENTO', descricao: 'IPTU 2025 — Parcela 3/10 paga', data: '15/05/2025' }, + { tipo: 'CERTIDAO', descricao: 'Certidão Negativa emitida', data: '10/05/2025' }, + { tipo: 'DEBITO', descricao: 'Guia IPTU 2025 — Cota 4 emitida', data: '02/05/2025' }, + { tipo: 'CADASTRO', descricao: 'E-mail cadastral atualizado', data: '20/04/2025' }, +] +// ────────────────────────────────────────────────────────────────────────── + const resumo = ref(null) const atividades = ref([]) const carregando = ref(true) -onMounted(async () => { +onMounted(carregar) + +async function carregar() { + carregando.value = true try { const [resResumo, resAtividades] = await Promise.all([ portalService.getPainelResumo(), @@ -23,28 +45,42 @@ onMounted(async () => { ]) resumo.value = resResumo.data atividades.value = resAtividades.data?.content ?? [] + if (MOCK_ATIVO) { + if (!resumo.value) resumo.value = RESUMO_MOCK + if (atividades.value.length === 0) atividades.value = ATIVIDADES_MOCK + } } catch { - // silencioso — exibe zeros + if (MOCK_ATIVO) { + resumo.value = RESUMO_MOCK + atividades.value = ATIVIDADES_MOCK + } } finally { carregando.value = false } -}) +} 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-briefcase', label: 'Acompanhar Alvará', to: '/portal/alvaras', cor: 'text-amber-600 dark:text-amber-400' }, + { icon: 'pi-credit-card', label: 'Pagamentos', to: '/portal/pagamentos', cor: 'text-amber-600 dark:text-amber-400' }, { icon: 'pi-user', label: 'Meus Dados', to: '/portal/dados', cor: 'text-violet-600 dark:text-violet-400' }, ] function formatarMoeda(valor) { - return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valor ?? 0) + const n = Number(valor ?? 0) + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number.isNaN(n) ? 0 : n) +} + +function formatarData(data) { + if (!data) return '' + return new Date(data + 'T00:00:00').toLocaleDateString('pt-BR') } const iconeAtividade = { DEBITO: 'pi-receipt', CERTIDAO: 'pi-file-check', - ALVARA: 'pi-briefcase', PAGAMENTO: 'pi-credit-card', CADASTRO: 'pi-user', } @@ -53,14 +89,25 @@ const iconeAtividade = {