developer #4
@ -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`
|
#### `GET /publico/certidao/emitir`
|
||||||
|
|
||||||
Emite a certidão em PDF.
|
Emite a certidão em PDF.
|
||||||
@ -94,7 +127,8 @@ Emite a certidão em PDF.
|
|||||||
| Param | Tipo | Obrigatório | Descrição |
|
| Param | Tipo | Obrigatório | Descrição |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `documento` | string | sim | CPF/CNPJ sem formataçã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)
|
**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`
|
#### `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:**
|
**Query params:** `idTaxa`, `idContaTributo`, `periodoIni` (YYYYMM), `periodoFim` (YYYYMM), `inscMunicipal`, `idEstadoConta` (1=débito, 2=zero, 3=crédito)
|
||||||
| Param | Tipo | Obrigatório | Descrição |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `tipo` | string | não | `IPTU` · `ISS` · `TAXA` · `MULTA` |
|
|
||||||
| `status` | string | não | `VENCIDO` · `A_VENCER` · `PARCELADO` |
|
|
||||||
|
|
||||||
**Response `data`:**
|
**Response `data`:** array de `ContaCorrenteTributoDTO` com lista `debitos` aninhada.
|
||||||
```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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### `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`
|
**Response:** `application/pdf`
|
||||||
|
|
||||||
@ -514,47 +562,58 @@ Lista os processos de alvará do contribuinte.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Pagamentos
|
### Pagamentos (Extrato)
|
||||||
|
|
||||||
#### `GET /contribuinte/pagamentos`
|
#### `GET /contribuinte/pagamentos`
|
||||||
|
|
||||||
Histórico de pagamentos do contribuinte, filtrável por ano.
|
Lista pagamentos agrupados por tributo (`ExtratoPagamentoTributoDTO`).
|
||||||
|
|
||||||
**Query params:**
|
**Query params:** `idTaxa`, `idContaTributo`, `pagInicio`, `pagFim`, `periodoIni`, `periodoFim`, `ano` (atalho para intervalo anual)
|
||||||
| Param | Tipo | Obrigatório | Descrição |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `ano` | integer | não | Ano de referência (padrão: ano atual) |
|
|
||||||
|
|
||||||
**Response `data`:**
|
**Response `data`:** array com `pagamentos` aninhados (principal, multa, juros, desconto, total).
|
||||||
```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`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### `GET /contribuinte/pagamentos/{id}/comprovante`
|
#### `POST /contribuinte/pagamentos/extrato-pdf`
|
||||||
|
|
||||||
Baixa o comprovante de um pagamento em PDF.
|
Gera PDF do extrato de pagamentos. Body: `GerarExtratoPagamentosRequestDTO`.
|
||||||
|
|
||||||
**Path param:** `id` — ID do pagamento
|
|
||||||
|
|
||||||
**Response:** `application/pdf`
|
**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<GuiaConsultaDTO>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `GET /contribuinte/guias/{id}`
|
||||||
|
|
||||||
|
Detalhes completos da guia (`GuiaPagamentoDTO`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `GET /contribuinte/guias/{id}/pdf`
|
||||||
|
|
||||||
|
PDF da guia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Dados Cadastrais
|
### Dados Cadastrais
|
||||||
|
|
||||||
#### `GET /contribuinte/dados`
|
#### `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 |
|
| Endpoint | Frontend pronto | Backend pronto | Mock |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `GET /publico/prefeitura/{dominio}` | ✓ | ✓ | — |
|
| `GET /publico/prefeitura/{dominio}` | ✓ | ✓ | — |
|
||||||
| `GET /publico/certidao/consultar` | ✓ | pendente | ✓ |
|
| `GET /publico/certidao/consultar` | ✓ | ✓ | — |
|
||||||
| `GET /publico/certidao/emitir` | ✓ | pendente | ✓ |
|
| `GET /publico/certidao/modelos` | ✓ | ✓ | — |
|
||||||
|
| `GET /publico/certidao/emitir` | ✓ | ✓ | — |
|
||||||
| `GET /publico/iptu/consultar` | ✓ | pendente | ✓ |
|
| `GET /publico/iptu/consultar` | ✓ | pendente | ✓ |
|
||||||
| `GET /publico/iptu/carne` | ✓ | pendente | ✓ |
|
| `GET /publico/iptu/carne` | ✓ | pendente | ✓ |
|
||||||
| `GET /publico/iptu/boleto` | ✓ | pendente | ✓ |
|
| `GET /publico/iptu/boleto` | ✓ | pendente | ✓ |
|
||||||
|
|||||||
40
package-lock.json
generated
40
package-lock.json
generated
@ -1686,24 +1686,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/@nuxt/telemetry": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.8.0.tgz",
|
||||||
@ -5587,17 +5569,6 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/commondir": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||||
@ -7940,7 +7911,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -7961,7 +7931,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -7982,7 +7951,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -8003,7 +7971,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -8024,7 +7991,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -8045,7 +8011,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -8066,7 +8031,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -8087,7 +8051,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -8108,7 +8071,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -8129,7 +8091,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@ -8150,7 +8111,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@ -58,8 +58,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
setResponseStatus(event, res.status)
|
setResponseStatus(event, res.status)
|
||||||
|
const skipResponseHeaders = new Set(['transfer-encoding', 'content-encoding', 'content-length'])
|
||||||
for (const [name, value] of res.headers.entries()) {
|
for (const [name, value] of res.headers.entries()) {
|
||||||
if (name === 'transfer-encoding') continue
|
if (skipResponseHeaders.has(name.toLowerCase())) continue
|
||||||
setResponseHeader(event, name, value)
|
setResponseHeader(event, name, value)
|
||||||
}
|
}
|
||||||
return res._data
|
return res._data
|
||||||
|
|||||||
@ -51,14 +51,22 @@ export async function exchangeCodeForTokens(opts: {
|
|||||||
redirect_uri: opts.redirectUri,
|
redirect_uri: opts.redirectUri,
|
||||||
code_verifier: opts.codeVerifier,
|
code_verifier: opts.codeVerifier,
|
||||||
})
|
})
|
||||||
return await $fetch<TokenResponse>(
|
try {
|
||||||
`${realmBase()}/protocol/openid-connect/token`,
|
return await $fetch<TokenResponse>(
|
||||||
{
|
`${realmBase()}/protocol/openid-connect/token`,
|
||||||
method: 'POST',
|
{
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
method: 'POST',
|
||||||
body: body.toString(),
|
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<TokenResponse> {
|
export async function refreshTokens(refreshToken: string): Promise<TokenResponse> {
|
||||||
|
|||||||
124
src/components/extrato/ModalDetalhesGuia.vue
Normal file
124
src/components/extrato/ModalDetalhesGuia.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { portalService } from '@/services/portalService'
|
||||||
|
import { formatCurrency, formatDate, abrirPdf } from '@/utils/formatador'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
guiaId: { type: Number, default: null },
|
||||||
|
statusCodigo: { type: Number, default: null },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible'])
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isLoadingPdf = ref(false)
|
||||||
|
const guia = ref(null)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.guiaId],
|
||||||
|
([vis, id]) => {
|
||||||
|
if (vis && id) carregarDetalhes()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function carregarDetalhes() {
|
||||||
|
if (!props.guiaId) return
|
||||||
|
isLoading.value = true
|
||||||
|
guia.value = null
|
||||||
|
try {
|
||||||
|
const res = await portalService.buscarGuia(props.guiaId)
|
||||||
|
guia.value = res.data
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fechar() {
|
||||||
|
emit('update:visible', false)
|
||||||
|
guia.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatarNumeroGuia(numero) {
|
||||||
|
return numero ? String(numero).padStart(10, '0') : '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeverityStatus(codigo) {
|
||||||
|
return { 0: 'danger', 1: 'info', 2: 'success', 3: 'warn' }[codigo ?? props.statusCodigo] ?? 'secondary'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function visualizarPdf() {
|
||||||
|
if (!props.guiaId) return
|
||||||
|
isLoadingPdf.value = true
|
||||||
|
try {
|
||||||
|
const buf = await portalService.baixarGuiaEmitidaPdf(props.guiaId)
|
||||||
|
abrirPdf(buf)
|
||||||
|
} finally {
|
||||||
|
isLoadingPdf.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:visible="visible"
|
||||||
|
modal
|
||||||
|
:header="guia ? `Detalhes da Guia #${formatarNumeroGuia(guia.numGuia)}` : 'Detalhes da Guia'"
|
||||||
|
:style="{ width: 'min(95vw, 56rem)' }"
|
||||||
|
@update:visible="(v) => emit('update:visible', v)"
|
||||||
|
>
|
||||||
|
<div v-if="isLoading" class="flex justify-center p-8">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="guia" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Número</p>
|
||||||
|
<p class="font-bold">{{ formatarNumeroGuia(guia.numGuia) }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Status</p>
|
||||||
|
<Tag :value="guia.statusDescricao ?? 'Emitida'" :severity="getSeverityStatus()" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Emissão</p>
|
||||||
|
<p>{{ formatDate(guia.dataEmissao) }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Vencimento</p>
|
||||||
|
<p>{{ formatDate(guia.dataVencimento) }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Valor Total</p>
|
||||||
|
<p class="font-bold">{{ formatCurrency(guia.valorTotal) }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="guia.linhaDigitavel" class="sm:col-span-3">
|
||||||
|
<p class="text-xs text-slate-500">Linha Digitável</p>
|
||||||
|
<p class="font-mono text-xs break-all">{{ guia.linhaDigitavel }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable :value="guia.itens ?? []" size="small" show-gridlines>
|
||||||
|
<Column field="tributo" header="Tributo" />
|
||||||
|
<Column field="periodoRef" header="Período" />
|
||||||
|
<Column field="doc" header="Documento" />
|
||||||
|
<Column field="valorTotal" header="Total" body-class="text-right">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.valorTotal) }}</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Fechar" severity="secondary" outlined @click="fechar" />
|
||||||
|
<Button label="Visualizar PDF" icon="pi pi-file-pdf" :loading="isLoadingPdf" @click="visualizarPdf" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-mono {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
src/components/extrato/ModalTransacoesContaCorrente.vue
Normal file
71
src/components/extrato/ModalTransacoesContaCorrente.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { portalService } from '@/services/portalService'
|
||||||
|
import { formatCurrency, formatDate } from '@/utils/formatador'
|
||||||
|
|
||||||
|
const visivel = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const transacoes = ref([])
|
||||||
|
const mensagemErro = ref('')
|
||||||
|
|
||||||
|
async function abrir(idContaCorrente) {
|
||||||
|
visivel.value = true
|
||||||
|
isLoading.value = true
|
||||||
|
transacoes.value = []
|
||||||
|
mensagemErro.value = ''
|
||||||
|
try {
|
||||||
|
const res = await portalService.getTransacoes(idContaCorrente)
|
||||||
|
transacoes.value = res.data ?? []
|
||||||
|
} catch (e) {
|
||||||
|
mensagemErro.value = e?.data?.description ?? 'Erro ao buscar transações.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ abrir })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="visivel"
|
||||||
|
modal
|
||||||
|
header="Transações da Conta Corrente"
|
||||||
|
:style="{ width: 'min(95vw, 56rem)' }"
|
||||||
|
>
|
||||||
|
<div v-if="isLoading" class="flex justify-center p-8">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
<p v-else-if="mensagemErro" class="text-sm text-red-600">{{ mensagemErro }}</p>
|
||||||
|
<DataTable
|
||||||
|
v-else
|
||||||
|
:value="transacoes"
|
||||||
|
show-gridlines
|
||||||
|
size="small"
|
||||||
|
scrollable
|
||||||
|
scroll-height="400px"
|
||||||
|
empty-message="Nenhuma transação encontrada."
|
||||||
|
>
|
||||||
|
<Column field="id" header="ID" style="width: 60px" />
|
||||||
|
<Column field="data" header="Data" style="width: 100px">
|
||||||
|
<template #body="{ data }">{{ formatDate(data.data) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="tipoTransacao" header="Tipo" />
|
||||||
|
<Column field="numDocOrigem" header="Doc. Origem" style="width: 110px" />
|
||||||
|
<Column field="valorPrincipal" header="Principal" body-class="text-right">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.valorPrincipal) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="valorMulta" header="Multa" body-class="text-right">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.valorMulta) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="valorJuros" header="Juros" body-class="text-right">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.valorJuros) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="valorTotal" header="Total" body-class="text-right">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<strong>{{ formatCurrency(data.valorTotal) }}</strong>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
212
src/composables/useExtratoDebitosPortal.js
Normal file
212
src/composables/useExtratoDebitosPortal.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
197
src/composables/useExtratoPagamentosPortal.js
Normal file
197
src/composables/useExtratoPagamentosPortal.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/composables/useGuiasEmitidasPortal.js
Normal file
106
src/composables/useGuiasEmitidasPortal.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ const menuAberto = ref(false)
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: '/portal/painel', label: 'Painel', icon: 'pi-home' },
|
{ path: '/portal/painel', label: 'Painel', icon: 'pi-home' },
|
||||||
{ path: '/portal/debitos', label: 'Débitos', icon: 'pi-receipt' },
|
{ 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/taxas', label: 'Taxas', icon: 'pi-file-export' },
|
||||||
{ path: '/portal/certidoes', label: 'Certidões', icon: 'pi-file-check' },
|
{ path: '/portal/certidoes', label: 'Certidões', icon: 'pi-file-check' },
|
||||||
{ path: '/portal/pagamentos', label: 'Pagamentos', icon: 'pi-credit-card' },
|
{ path: '/portal/pagamentos', label: 'Pagamentos', icon: 'pi-credit-card' },
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import bgTutoia from '@/assets/images/bg-tutoia.jpeg'
|
|||||||
const { prefersReducedMotion } = useMotion()
|
const { prefersReducedMotion } = useMotion()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const prefeitura = usePrefeituraStore()
|
const prefeitura = usePrefeituraStore()
|
||||||
const { isAuthenticated, nomeUsuario, login } = useAuth()
|
const { isAuthenticated, nomeUsuario, login } = useAuth()
|
||||||
const { requested: focusLoginRequested, consume: consumeFocusLogin } = useFocusLoginInput()
|
const { requested: focusLoginRequested, consume: consumeFocusLogin } = useFocusLoginInput()
|
||||||
@ -19,6 +20,12 @@ const documento = ref('')
|
|||||||
const erro = ref('')
|
const erro = ref('')
|
||||||
const carregando = ref(false)
|
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
|
// Ref ao DocumentoInput — usado pelo botão "Entrar" do AppHeader pra focar o campo
|
||||||
const documentoRef = ref(null)
|
const documentoRef = ref(null)
|
||||||
|
|
||||||
@ -89,6 +96,14 @@ const AVISOS_FALLBACK = [
|
|||||||
const avisos = ref(AVISOS_FALLBACK)
|
const avisos = ref(AVISOS_FALLBACK)
|
||||||
|
|
||||||
onMounted(async () => {
|
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 {
|
try {
|
||||||
const res = await avisoService.listar(prefeitura.dominio)
|
const res = await avisoService.listar(prefeitura.dominio)
|
||||||
const lista = (res.data ?? []).map(a => ({
|
const lista = (res.data ?? []).map(a => ({
|
||||||
@ -119,7 +134,8 @@ const servicosPublicos = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const servicosAutenticados = [
|
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-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-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' },
|
{ icon: 'pi-user', titulo: 'Dados Cadastrais', descricao: 'Visualize e mantenha seus dados sempre atualizados.', to: '/portal/dados' },
|
||||||
|
|||||||
@ -1,286 +1,224 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { portalService } from '@/services/portalService'
|
import { useExtratoDebitosPortal } from '@/composables/useExtratoDebitosPortal'
|
||||||
|
import { formatCurrency, formatDate } from '@/utils/formatador'
|
||||||
|
import ModalTransacoesContaCorrente from '@/components/extrato/ModalTransacoesContaCorrente.vue'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'portal',
|
layout: 'portal',
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
})
|
})
|
||||||
|
|
||||||
const debitos = ref([])
|
const modalTransacoes = ref(null)
|
||||||
const carregando = ref(true)
|
|
||||||
const carregandoGuia = ref(null)
|
|
||||||
const filtroTipo = ref(null)
|
|
||||||
const filtroStatus = ref(null)
|
|
||||||
const mensagemErro = ref('')
|
|
||||||
|
|
||||||
const tiposDisponiveis = ['IPTU', 'ISS', 'TAXA', 'MULTA', 'DIVIDA_ATIVA']
|
const {
|
||||||
const statusDisponiveis = [
|
resultados,
|
||||||
{ value: 'VENCIDO', label: 'Vencido', classe: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400' },
|
isLoading,
|
||||||
{ value: 'A_VENCER', label: 'A vencer', classe: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400' },
|
isLoadingGuia,
|
||||||
{ value: 'PARCELADO', label: 'Parcelado', classe: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400' },
|
isLoadingExtrato,
|
||||||
]
|
mensagemErro,
|
||||||
|
dataVencimento,
|
||||||
|
erros,
|
||||||
|
filtro,
|
||||||
|
opcoesEstadoConta,
|
||||||
|
temSelecionado,
|
||||||
|
totalizadores,
|
||||||
|
consultar,
|
||||||
|
limparFiltros,
|
||||||
|
gerarGuia,
|
||||||
|
gerarExtratoPdf,
|
||||||
|
getEstadoSeverity,
|
||||||
|
} = useExtratoDebitosPortal()
|
||||||
|
|
||||||
const statusClasse = Object.fromEntries(statusDisponiveis.map(s => [s.value, s.classe]))
|
onMounted(() => consultar())
|
||||||
const statusLabel = Object.fromEntries(statusDisponiveis.map(s => [s.value, s.label]))
|
|
||||||
|
|
||||||
onMounted(() => carregar())
|
function getSegundoNome(texto) {
|
||||||
|
const palavras = (texto || '').trim().split(/\s+/)
|
||||||
async function carregar() {
|
return palavras.length > 1 ? palavras[1] : texto
|
||||||
carregando.value = true
|
|
||||||
mensagemErro.value = ''
|
|
||||||
try {
|
|
||||||
const params = {}
|
|
||||||
if (filtroTipo.value) params.tipo = filtroTipo.value
|
|
||||||
if (filtroStatus.value) params.status = filtroStatus.value
|
|
||||||
const res = await portalService.getDebitos(params)
|
|
||||||
debitos.value = res.data ?? []
|
|
||||||
} catch (e) {
|
|
||||||
mensagemErro.value = e?.data?.description ?? 'Não foi possível carregar os débitos.'
|
|
||||||
} finally {
|
|
||||||
carregando.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function emitirGuia(debito) {
|
|
||||||
carregandoGuia.value = debito.id
|
|
||||||
try {
|
|
||||||
const buf = await portalService.emitirGuia(debito.id)
|
|
||||||
const url = URL.createObjectURL(new Blob([buf], { type: 'application/pdf' }))
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = `guia-${debito.id}.pdf`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
} catch (e) {
|
|
||||||
const status = e?.status ?? e?.response?.status
|
|
||||||
mensagemErro.value = status === 501
|
|
||||||
? 'Emissão de guia em configuração. Procure a prefeitura para geração manual.'
|
|
||||||
: 'Erro ao gerar a guia. Tente novamente.'
|
|
||||||
} finally {
|
|
||||||
carregandoGuia.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalSelecionado = computed(() =>
|
|
||||||
debitos.value
|
|
||||||
.filter(d => d._selecionado)
|
|
||||||
.reduce((sum, d) => sum + (d.valorAtualizado ?? d.valor), 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
const temSelecionados = computed(() => debitos.value.some(d => d._selecionado))
|
|
||||||
|
|
||||||
function toggleTodos(val) {
|
|
||||||
debitos.value.forEach(d => (d._selecionado = val))
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatarMoeda(valor) {
|
|
||||||
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valor ?? 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function aplicarFiltro() {
|
|
||||||
carregar()
|
|
||||||
}
|
|
||||||
|
|
||||||
function limparFiltros() {
|
|
||||||
filtroTipo.value = null
|
|
||||||
filtroStatus.value = null
|
|
||||||
carregar()
|
|
||||||
}
|
|
||||||
|
|
||||||
function tituloDebito(d) {
|
|
||||||
if (d.tipo && d.referencia) return `${d.tipo} — ${d.referencia}`
|
|
||||||
if (d.tipo) return d.tipo
|
|
||||||
return d.descricao ?? 'Débito'
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatarData(iso) {
|
|
||||||
if (!iso) return '—'
|
|
||||||
const partes = String(iso).split('-')
|
|
||||||
if (partes.length !== 3) return iso
|
|
||||||
return `${partes[2]}/${partes[1]}/${partes[0]}`
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100">Extrato de Débitos</h1>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
|
Consulte seus débitos, selecione parcelas e emita guias de pagamento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4 flex-wrap">
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 space-y-4">
|
||||||
<div>
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100">Débitos e Guias</h1>
|
<div>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">Consulte seus débitos e emita guias de pagamento.</p>
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Período Início</label>
|
||||||
|
<DatePicker v-model="filtro.periodoIni" view="month" date-format="mm/yy" show-icon class="w-full" size="small" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Período Fim</label>
|
||||||
|
<DatePicker v-model="filtro.periodoFim" view="month" date-format="mm/yy" show-icon class="w-full" size="small" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Estado da Conta</label>
|
||||||
|
<Select
|
||||||
|
v-model="filtro.estadoConta"
|
||||||
|
:options="opcoesEstadoConta"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
placeholder="Todos"
|
||||||
|
class="w-full"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<Button label="Consultar" icon="pi pi-search" size="small" :loading="isLoading" @click="consultar" />
|
||||||
|
<Button label="Limpar" icon="pi pi-eraser" size="small" severity="secondary" outlined @click="limparFiltros" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 flex flex-wrap gap-3 items-end">
|
<Message v-if="mensagemErro" severity="warn" :closable="false">{{ mensagemErro }}</Message>
|
||||||
<div class="flex-1 min-w-[160px]">
|
|
||||||
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Tipo</label>
|
<div v-if="isLoading" class="space-y-3">
|
||||||
<Select
|
<div v-for="i in 3" :key="i" class="h-24 bg-slate-200 dark:bg-slate-700 rounded-xl animate-pulse" />
|
||||||
v-model="filtroTipo"
|
|
||||||
:options="tiposDisponiveis"
|
|
||||||
placeholder="Todos"
|
|
||||||
show-clear
|
|
||||||
class="w-full"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 min-w-[160px]">
|
|
||||||
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1.5">Status</label>
|
|
||||||
<Select
|
|
||||||
v-model="filtroStatus"
|
|
||||||
:options="statusDisponiveis"
|
|
||||||
option-label="label"
|
|
||||||
option-value="value"
|
|
||||||
placeholder="Todos"
|
|
||||||
show-clear
|
|
||||||
class="w-full"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Button label="Filtrar" icon="pi pi-filter" size="small" @click="aplicarFiltro" />
|
|
||||||
<Button label="Limpar" severity="secondary" size="small" outlined @click="limparFiltros" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Transition
|
<div v-else-if="resultados.length === 0 && !mensagemErro" class="bg-white dark:bg-slate-800 rounded-xl border p-12 text-center">
|
||||||
enter-active-class="transition-all duration-200"
|
<i class="pi pi-check-circle text-emerald-400 text-4xl mb-3 block" />
|
||||||
enter-from-class="opacity-0 -translate-y-2"
|
<p class="font-semibold text-slate-700 dark:text-slate-200">Nenhum débito encontrado</p>
|
||||||
enter-to-class="opacity-100 translate-y-0"
|
</div>
|
||||||
leave-active-class="transition-all duration-150"
|
|
||||||
leave-from-class="opacity-100 translate-y-0"
|
|
||||||
leave-to-class="opacity-0 -translate-y-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="temSelecionados"
|
|
||||||
class="bg-primary/8 dark:bg-primary/15 border border-primary/20 rounded-xl p-4 flex items-center justify-between gap-4"
|
|
||||||
>
|
|
||||||
<p class="text-sm font-semibold text-primary">
|
|
||||||
Total selecionado: {{ formatarMoeda(totalSelecionado) }}
|
|
||||||
</p>
|
|
||||||
<Button label="Emitir guia unificada" icon="pi pi-download" size="small" />
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
<template v-else-if="resultados.length">
|
||||||
|
<Accordion :multiple="true" :active-index="[0]">
|
||||||
<div v-if="carregando" class="divide-y divide-slate-100 dark:divide-slate-700">
|
<AccordionPanel v-for="(tributo, index) in resultados" :key="tributo.idContaTributo" :value="String(index)">
|
||||||
<div v-for="i in 5" :key="i" class="p-5 flex items-center gap-4">
|
<AccordionHeader>
|
||||||
<div class="w-4 h-4 bg-slate-200 dark:bg-slate-700 rounded animate-pulse" />
|
<div class="flex items-center justify-between w-full gap-2 pr-2">
|
||||||
<div class="flex-1 space-y-2">
|
<span class="text-sm font-semibold truncate">
|
||||||
<div class="h-3.5 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-1/3" />
|
Conta {{ tributo.idContaTributo }} — {{ tributo.descricaoContaTributo }} / {{ tributo.descricaoTributo }}
|
||||||
<div class="h-3 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-1/4" />
|
</span>
|
||||||
</div>
|
<span class="text-sm shrink-0">
|
||||||
<div class="h-3.5 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-20" />
|
Total: <strong>{{ formatCurrency(tributo.totalPagamentos) }}</strong>
|
||||||
<div class="h-6 bg-slate-200 dark:bg-slate-700 rounded-full animate-pulse w-16" />
|
</span>
|
||||||
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded-lg animate-pulse w-24" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="debitos.length === 0 && !mensagemErro" class="p-12 text-center">
|
|
||||||
<i class="pi pi-check-circle text-emerald-400 dark:text-emerald-500 text-4xl mb-3 block" aria-hidden="true" />
|
|
||||||
<p class="font-semibold text-slate-700 dark:text-slate-200">Nenhum débito encontrado</p>
|
|
||||||
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1">Sua situação fiscal está regularizada.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="mensagemErro" class="p-8 text-center">
|
|
||||||
<i class="pi pi-exclamation-triangle text-amber-400 text-3xl mb-3 block" aria-hidden="true" />
|
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-300">{{ mensagemErro }}</p>
|
|
||||||
<Button label="Tentar novamente" severity="secondary" size="small" class="mt-4" @click="carregar" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<!-- Cabeçalho — apenas desktop -->
|
|
||||||
<div class="hidden sm:flex items-center gap-4 px-5 py-3 bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">
|
|
||||||
<Checkbox :binary="true" @change="toggleTodos($event.target.checked)" />
|
|
||||||
<span class="flex-1">Descrição</span>
|
|
||||||
<span class="w-28 text-right">Vencimento</span>
|
|
||||||
<span class="w-28 text-right">Valor</span>
|
|
||||||
<span class="w-20 text-center">Status</span>
|
|
||||||
<span class="w-28" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divide-y divide-slate-100 dark:divide-slate-700">
|
|
||||||
<div
|
|
||||||
v-for="debito in debitos"
|
|
||||||
:key="debito.id"
|
|
||||||
class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors"
|
|
||||||
>
|
|
||||||
<!-- MOBILE -->
|
|
||||||
<div class="sm:hidden p-4 space-y-3">
|
|
||||||
<div class="flex items-start gap-3">
|
|
||||||
<Checkbox v-model="debito._selecionado" :binary="true" class="mt-0.5 shrink-0" />
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="text-sm font-bold text-slate-800 dark:text-slate-100 leading-snug">{{ tituloDebito(debito) }}</p>
|
|
||||||
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5">Nº {{ debito.descricao }}</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
:class="['text-xs font-semibold px-2 py-1 rounded-full whitespace-nowrap shrink-0', statusClasse[debito.status] ?? 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300']"
|
|
||||||
>
|
|
||||||
{{ statusLabel[debito.status] ?? debito.status }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-end justify-between pl-7">
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-slate-400 dark:text-slate-500">Vence em {{ formatarData(debito.vencimento) }}</p>
|
|
||||||
<p class="text-base font-bold text-slate-800 dark:text-slate-100 mt-0.5">{{ formatarMoeda(debito.valorAtualizado ?? debito.valor) }}</p>
|
|
||||||
<p v-if="debito.valorAtualizado > debito.valor" class="text-xs text-slate-400 line-through">{{ formatarMoeda(debito.valor) }}</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
icon="pi pi-download"
|
|
||||||
label="Emitir guia"
|
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
:loading="carregandoGuia === debito.id"
|
|
||||||
:disabled="!!carregandoGuia"
|
|
||||||
@click="emitirGuia(debito)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionContent>
|
||||||
|
<DataTable
|
||||||
|
v-model:selection="tributo.selecionados"
|
||||||
|
:value="tributo.debitos"
|
||||||
|
data-key="id"
|
||||||
|
size="small"
|
||||||
|
show-gridlines
|
||||||
|
selection-mode="multiple"
|
||||||
|
scrollable
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<Column selection-mode="multiple" header-style="width: 3rem" />
|
||||||
|
<Column field="numDoc" header="Nº Doc" style="min-width: 90px" />
|
||||||
|
<Column field="estadoConta" header="Estado" style="min-width: 90px">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag :value="getSegundoNome(data.estadoConta)" :severity="getEstadoSeverity(data.codigoEstadoConta)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="periodoRef" header="Período" style="min-width: 80px" />
|
||||||
|
<Column field="dataVencimento" header="Vencimento" style="min-width: 95px">
|
||||||
|
<template #body="{ data }">{{ formatDate(data.dataVencimento) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="valorPrincipal" header="Principal" body-class="text-right" style="min-width: 100px">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.valorPrincipal) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="valorMulta" header="Multa" body-class="text-right" style="min-width: 90px">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.valorMulta) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="valorJuros" header="Juros" body-class="text-right" style="min-width: 90px">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.valorJuros) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="valorTotal" header="Total" body-class="text-right" style="min-width: 100px">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<strong>{{ formatCurrency(data.valorTotal) }}</strong>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="" style="width: 3rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-search"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
@click="modalTransacoes?.abrir(data.idContaCorrente)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionPanel>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<!-- DESKTOP -->
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 space-y-4">
|
||||||
<div class="hidden sm:flex items-center gap-4 px-5 py-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<Checkbox v-model="debito._selecionado" :binary="true" />
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Vencimento da Guia</label>
|
||||||
<div class="flex-1 min-w-0">
|
<DatePicker
|
||||||
<p class="text-sm font-semibold text-slate-800 dark:text-slate-100 truncate">{{ tituloDebito(debito) }}</p>
|
v-model="dataVencimento"
|
||||||
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5">Nº {{ debito.descricao }}</p>
|
show-icon
|
||||||
</div>
|
:min-date="new Date()"
|
||||||
|
date-format="dd/mm/yy"
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400 w-28 text-right whitespace-nowrap">
|
class="w-full"
|
||||||
{{ formatarData(debito.vencimento) }}
|
size="small"
|
||||||
</p>
|
/>
|
||||||
|
<p v-if="erros.dataVencimento" class="text-xs text-red-500 mt-1">{{ erros.dataVencimento }}</p>
|
||||||
<div class="w-28 text-right">
|
</div>
|
||||||
<p class="text-sm font-bold text-slate-800 dark:text-slate-100">{{ formatarMoeda(debito.valorAtualizado ?? debito.valor) }}</p>
|
<div class="sm:col-span-3 grid grid-cols-2 sm:grid-cols-4 gap-3 items-end">
|
||||||
<p v-if="debito.valorAtualizado > debito.valor" class="text-xs text-slate-400 line-through">{{ formatarMoeda(debito.valor) }}</p>
|
<div>
|
||||||
</div>
|
<p class="text-xs text-slate-500">Principal</p>
|
||||||
|
<p class="font-bold">{{ formatCurrency(totalizadores.principal) }}</p>
|
||||||
<div class="w-20 flex justify-center">
|
</div>
|
||||||
<span
|
<div>
|
||||||
:class="['text-xs font-semibold px-2 py-1 rounded-full whitespace-nowrap', statusClasse[debito.status] ?? 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300']"
|
<p class="text-xs text-slate-500">Multa</p>
|
||||||
>
|
<p class="font-bold">{{ formatCurrency(totalizadores.multa) }}</p>
|
||||||
{{ statusLabel[debito.status] ?? debito.status }}
|
</div>
|
||||||
</span>
|
<div>
|
||||||
</div>
|
<p class="text-xs text-slate-500">Juros</p>
|
||||||
|
<p class="font-bold">{{ formatCurrency(totalizadores.juros) }}</p>
|
||||||
<div class="w-28 flex justify-end">
|
</div>
|
||||||
<Button
|
<div>
|
||||||
label="Emitir guia"
|
<p class="text-xs text-slate-500">Total a Pagar</p>
|
||||||
icon="pi pi-download"
|
<p class="font-bold text-primary">{{ formatCurrency(totalizadores.valorTotal) }}</p>
|
||||||
size="small"
|
|
||||||
outlined
|
|
||||||
class="whitespace-nowrap"
|
|
||||||
:loading="carregandoGuia === debito.id"
|
|
||||||
:disabled="!!carregandoGuia"
|
|
||||||
@click="emitirGuia(debito)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 justify-between">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
label="Imprimir Extrato (todos)"
|
||||||
|
icon="pi pi-print"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
:loading="isLoadingExtrato"
|
||||||
|
@click="gerarExtratoPdf(false)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="temSelecionado"
|
||||||
|
label="Imprimir Selecionados"
|
||||||
|
icon="pi pi-print"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
:loading="isLoadingExtrato"
|
||||||
|
@click="gerarExtratoPdf(true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="temSelecionado"
|
||||||
|
label="Gerar Guia de Pagamento"
|
||||||
|
icon="pi pi-file-pdf"
|
||||||
|
size="small"
|
||||||
|
:loading="isLoadingGuia"
|
||||||
|
@click="gerarGuia"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ModalTransacoesContaCorrente ref="modalTransacoes" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
170
src/pages/portal/guias.vue
Normal file
170
src/pages/portal/guias.vue
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useGuiasEmitidasPortal } from '@/composables/useGuiasEmitidasPortal'
|
||||||
|
import { formatCurrency, formatDate } from '@/utils/formatador'
|
||||||
|
import ModalDetalhesGuia from '@/components/extrato/ModalDetalhesGuia.vue'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'portal',
|
||||||
|
middleware: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const guiaSelecionadaId = ref(null)
|
||||||
|
const guiaSelecionadaStatus = ref(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
guias,
|
||||||
|
isLoading,
|
||||||
|
mensagemErro,
|
||||||
|
pagina,
|
||||||
|
totalPaginas,
|
||||||
|
totalElementos,
|
||||||
|
filtro,
|
||||||
|
statusOptions,
|
||||||
|
consultar,
|
||||||
|
limparFiltros,
|
||||||
|
mudarPagina,
|
||||||
|
visualizarPdf,
|
||||||
|
formatarNumeroGuia,
|
||||||
|
getSeverityStatus,
|
||||||
|
} = useGuiasEmitidasPortal()
|
||||||
|
|
||||||
|
onMounted(() => consultar())
|
||||||
|
|
||||||
|
function detalhar(guia) {
|
||||||
|
guiaSelecionadaId.value = guia.id
|
||||||
|
guiaSelecionadaStatus.value = guia.statusCodigo
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function paginaAnterior() {
|
||||||
|
if (pagina.value > 0) mudarPagina(pagina.value - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function proximaPagina() {
|
||||||
|
if (pagina.value < totalPaginas.value - 1) mudarPagina(pagina.value + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function aplicarFiltros() {
|
||||||
|
pagina.value = 0
|
||||||
|
consultar()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100">Guias Emitidas</h1>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">Consulte e baixe as guias de pagamento emitidas em seu nome.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 space-y-4">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Nº Guia</label>
|
||||||
|
<InputText v-model="filtro.numeroGuia" class="w-full" size="small" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Status</label>
|
||||||
|
<Select
|
||||||
|
v-model="filtro.status"
|
||||||
|
:options="statusOptions"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
placeholder="Todos"
|
||||||
|
show-clear
|
||||||
|
class="w-full"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Emissão (início)</label>
|
||||||
|
<DatePicker v-model="filtro.dataEmissaoInicio" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Emissão (fim)</label>
|
||||||
|
<DatePicker v-model="filtro.dataEmissaoFim" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Vencimento (início)</label>
|
||||||
|
<DatePicker v-model="filtro.dataVencimentoInicio" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Vencimento (fim)</label>
|
||||||
|
<DatePicker v-model="filtro.dataVencimentoFim" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Valor mínimo</label>
|
||||||
|
<InputNumber v-model="filtro.valorMinimo" mode="currency" currency="BRL" locale="pt-BR" class="w-full" size="small" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1.5">Valor máximo</label>
|
||||||
|
<InputNumber v-model="filtro.valorMaximo" mode="currency" currency="BRL" locale="pt-BR" class="w-full" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button label="Consultar" icon="pi pi-search" size="small" :loading="isLoading" @click="aplicarFiltros" />
|
||||||
|
<Button label="Limpar" icon="pi pi-eraser" size="small" severity="secondary" outlined @click="limparFiltros" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Message v-if="mensagemErro" severity="warn" :closable="false">{{ mensagemErro }}</Message>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
|
<div v-if="isLoading" class="p-8 flex justify-center">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="guias.length === 0" class="p-12 text-center">
|
||||||
|
<p class="font-semibold text-slate-700 dark:text-slate-200">Nenhuma guia encontrada</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="hidden sm:grid grid-cols-[1fr_1fr_100px_100px_120px_140px] gap-3 px-5 py-3 bg-slate-50 dark:bg-slate-700/50 border-b text-xs font-semibold text-slate-500 uppercase">
|
||||||
|
<span>Nº Guia</span>
|
||||||
|
<span>Contribuinte</span>
|
||||||
|
<span>Emissão</span>
|
||||||
|
<span>Vencimento</span>
|
||||||
|
<span class="text-right">Valor</span>
|
||||||
|
<span class="text-right">Ações</span>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-slate-100 dark:divide-slate-700">
|
||||||
|
<div
|
||||||
|
v-for="guia in guias"
|
||||||
|
:key="guia.id"
|
||||||
|
class="px-5 py-4 flex flex-col sm:grid sm:grid-cols-[1fr_1fr_100px_100px_120px_140px] sm:items-center gap-2 sm:gap-3"
|
||||||
|
>
|
||||||
|
<p class="font-semibold text-sm">{{ formatarNumeroGuia(guia.numeroGuia) }}</p>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm truncate">{{ guia.nomeContribuinte }}</p>
|
||||||
|
<Tag :value="guia.statusDescricao" :severity="getSeverityStatus(guia.statusCodigo)" class="mt-1" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-slate-500">{{ formatDate(guia.dataEmissao) }}</p>
|
||||||
|
<p class="text-sm text-slate-500">{{ formatDate(guia.dataVencimento) }}</p>
|
||||||
|
<p class="text-sm font-bold text-right">{{ formatCurrency(guia.valorTotal) }}</p>
|
||||||
|
<div class="flex gap-1 justify-end">
|
||||||
|
<Button icon="pi pi-info-circle" text rounded size="small" @click="detalhar(guia)" />
|
||||||
|
<Button icon="pi pi-file-pdf" text rounded size="small" @click="visualizarPdf(guia.id)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="totalPaginas > 1" class="flex items-center justify-between px-5 py-3 border-t text-sm">
|
||||||
|
<span class="text-slate-500">{{ totalElementos }} guia(s)</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button icon="pi pi-chevron-left" size="small" text :disabled="pagina === 0" @click="paginaAnterior" />
|
||||||
|
<span class="px-2 py-1">{{ pagina + 1 }} / {{ totalPaginas }}</span>
|
||||||
|
<Button icon="pi pi-chevron-right" size="small" text :disabled="pagina >= totalPaginas - 1" @click="proximaPagina" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModalDetalhesGuia
|
||||||
|
v-model:visible="modalVisible"
|
||||||
|
:guia-id="guiaSelecionadaId"
|
||||||
|
:status-codigo="guiaSelecionadaStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,197 +1,151 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { portalService } from '@/services/portalService'
|
import { useExtratoPagamentosPortal } from '@/composables/useExtratoPagamentosPortal'
|
||||||
|
import { formatCurrency, formatDate, abrirPdf } from '@/utils/formatador'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'portal',
|
layout: 'portal',
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── MOCKS APRESENTAÇÃO — remover antes do deploy ─────────────────────────
|
|
||||||
const MOCK_ATIVO = true
|
|
||||||
const PAGAMENTOS_MOCK = [
|
|
||||||
{
|
|
||||||
id: 101,
|
|
||||||
idContaCorrente: 201,
|
|
||||||
idTaxa: 11,
|
|
||||||
descricao: 'IPTU 2024 — Parcela 1/3',
|
|
||||||
referencia: '202401',
|
|
||||||
dataPagamento: '2024-02-05',
|
|
||||||
formaPagamento: 'GUIA',
|
|
||||||
valor: 382.50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 102,
|
|
||||||
idContaCorrente: 202,
|
|
||||||
idTaxa: 12,
|
|
||||||
descricao: 'Taxa de Licença de Funcionamento',
|
|
||||||
referencia: '202403',
|
|
||||||
dataPagamento: '2024-03-20',
|
|
||||||
formaPagamento: 'GUIA',
|
|
||||||
valor: 215.00,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 103,
|
|
||||||
idContaCorrente: null,
|
|
||||||
idTaxa: null,
|
|
||||||
descricao: 'ISSQN 2023 — 4º Trimestre',
|
|
||||||
referencia: '202312',
|
|
||||||
dataPagamento: '2024-01-10',
|
|
||||||
formaPagamento: 'DIRETO',
|
|
||||||
valor: 540.00,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const pagamentos = ref([])
|
|
||||||
const carregando = ref(true)
|
|
||||||
const carregandoComprovante = ref(null)
|
const carregandoComprovante = ref(null)
|
||||||
const mensagemErro = ref('')
|
|
||||||
const filtroAno = ref(new Date().getFullYear())
|
|
||||||
|
|
||||||
const anosDisponiveis = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i)
|
const {
|
||||||
|
tributos,
|
||||||
|
isLoading,
|
||||||
|
isLoadingPdf,
|
||||||
|
mensagemErro,
|
||||||
|
filtro,
|
||||||
|
totais,
|
||||||
|
consultar,
|
||||||
|
limparFiltros,
|
||||||
|
gerarPdf,
|
||||||
|
baixarComprovante,
|
||||||
|
} = useExtratoPagamentosPortal()
|
||||||
|
|
||||||
onMounted(() => carregar())
|
onMounted(() => consultar())
|
||||||
|
|
||||||
async function carregar() {
|
async function emitirComprovante(pag) {
|
||||||
carregando.value = true
|
|
||||||
mensagemErro.value = ''
|
|
||||||
try {
|
|
||||||
const res = await portalService.getPagamentos({ ano: filtroAno.value })
|
|
||||||
pagamentos.value = res.data ?? []
|
|
||||||
if (MOCK_ATIVO && pagamentos.value.length === 0) {
|
|
||||||
pagamentos.value = PAGAMENTOS_MOCK
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
mensagemErro.value = e?.data?.description ?? 'Não foi possível carregar os pagamentos.'
|
|
||||||
if (MOCK_ATIVO) {
|
|
||||||
pagamentos.value = PAGAMENTOS_MOCK
|
|
||||||
mensagemErro.value = ''
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
carregando.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function baixarComprovante(pag) {
|
|
||||||
carregandoComprovante.value = pag.id
|
carregandoComprovante.value = pag.id
|
||||||
try {
|
try {
|
||||||
const buf = await portalService.getComprovanteByTaxa(pag.idTaxa)
|
const buf = await baixarComprovante(pag)
|
||||||
const blob = new Blob([buf], { type: 'application/pdf' })
|
if (buf?.byteLength) {
|
||||||
const url = URL.createObjectURL(blob)
|
abrirPdf(buf)
|
||||||
const janela = window.open(url, '_blank')
|
} else {
|
||||||
if (!janela) {
|
mensagemErro.value = 'Comprovante não disponível para este pagamento.'
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = `comprovante-${pag.idTaxa}.pdf`
|
|
||||||
a.click()
|
|
||||||
}
|
}
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 60000)
|
|
||||||
} catch {
|
|
||||||
mensagemErro.value = 'Erro ao gerar o comprovante.'
|
|
||||||
} finally {
|
} finally {
|
||||||
carregandoComprovante.value = null
|
carregandoComprovante.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatarMoeda(valor) {
|
|
||||||
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valor ?? 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formaPagMap = {
|
|
||||||
BOLETO: { label: 'Boleto', icone: 'pi-barcode' },
|
|
||||||
PIX: { label: 'Pix', icone: 'pi-qrcode' },
|
|
||||||
CARTAO: { label: 'Cartão', icone: 'pi-credit-card' },
|
|
||||||
TRANSFERENCIA: { label: 'Transferência', icone: 'pi-arrow-right-arrow-left' },
|
|
||||||
GUIA: { label: 'Guia', icone: 'pi-file' },
|
|
||||||
DIRETO: { label: 'Direto', icone: 'pi-check' },
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100">Histórico de Pagamentos</h1>
|
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100">Extrato de Pagamentos</h1>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">Todos os seus pagamentos com comprovantes para download.</p>
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">Histórico de pagamentos com detalhamento por tributo.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 space-y-4">
|
||||||
<button
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
v-for="ano in anosDisponiveis"
|
<div>
|
||||||
:key="ano"
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase mb-1.5">Período Débitos (início)</label>
|
||||||
:class="['px-4 py-2 rounded-lg text-sm font-semibold border transition-colors', filtroAno === ano ? 'bg-primary text-white border-primary' : 'bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-700 hover:border-slate-300']"
|
<DatePicker v-model="filtro.debitosInicio" view="month" date-format="mm/yy" show-icon class="w-full" size="small" />
|
||||||
@click="filtroAno = ano; carregar()"
|
</div>
|
||||||
>
|
<div>
|
||||||
{{ ano }}
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase mb-1.5">Período Débitos (fim)</label>
|
||||||
</button>
|
<DatePicker v-model="filtro.debitosFim" view="month" date-format="mm/yy" show-icon class="w-full" size="small" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase mb-1.5">Pagamento (início)</label>
|
||||||
|
<DatePicker v-model="filtro.pagInicio" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase mb-1.5">Pagamento (fim)</label>
|
||||||
|
<DatePicker v-model="filtro.pagFim" show-icon class="w-full" size="small" date-format="dd/mm/yy" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<Button label="Consultar" icon="pi pi-search" size="small" :loading="isLoading" @click="consultar" />
|
||||||
|
<Button label="Limpar" icon="pi pi-eraser" size="small" severity="secondary" outlined @click="limparFiltros" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
<Message v-if="mensagemErro" severity="warn" :closable="false">{{ mensagemErro }}</Message>
|
||||||
|
|
||||||
<div v-if="carregando" class="divide-y divide-slate-100 dark:divide-slate-700">
|
<div v-if="isLoading" class="space-y-3">
|
||||||
<div v-for="i in 5" :key="i" class="p-5 flex items-center gap-4">
|
<div v-for="i in 3" :key="i" class="h-20 bg-slate-200 dark:bg-slate-700 rounded-xl animate-pulse" />
|
||||||
<div class="flex-1 space-y-2">
|
|
||||||
<div class="h-3.5 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-2/5" />
|
|
||||||
<div class="h-3 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-1/4" />
|
|
||||||
</div>
|
|
||||||
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-20" />
|
|
||||||
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded-lg animate-pulse w-28" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="mensagemErro" class="p-8 text-center">
|
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-300">{{ mensagemErro }}</p>
|
|
||||||
<Button label="Tentar novamente" severity="secondary" size="small" class="mt-4" @click="carregar" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="pagamentos.length === 0" class="p-12 text-center">
|
|
||||||
<i class="pi pi-credit-card text-slate-300 dark:text-slate-600 text-4xl mb-3 block" aria-hidden="true" />
|
|
||||||
<p class="font-semibold text-slate-700 dark:text-slate-200">Nenhum pagamento em {{ filtroAno }}</p>
|
|
||||||
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1">Pagamentos realizados aparecerão aqui.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex items-center gap-4 px-5 py-3 bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">
|
|
||||||
<span class="flex-1">Descrição</span>
|
|
||||||
<span class="hidden sm:block w-28 text-right">Data</span>
|
|
||||||
<span class="hidden sm:block w-24 text-center">Forma</span>
|
|
||||||
<span class="w-28 text-right">Valor</span>
|
|
||||||
<span class="w-28" />
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-slate-100 dark:divide-slate-700">
|
|
||||||
<div
|
|
||||||
v-for="pag in pagamentos"
|
|
||||||
:key="pag.id"
|
|
||||||
class="flex items-center gap-4 px-5 py-4 hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors"
|
|
||||||
>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="text-sm font-semibold text-slate-800 dark:text-slate-100 truncate">{{ pag.descricao }}</p>
|
|
||||||
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5">Ref: {{ pag.referencia }}</p>
|
|
||||||
</div>
|
|
||||||
<p class="hidden sm:block text-sm text-slate-500 dark:text-slate-400 w-28 text-right whitespace-nowrap">{{ pag.dataPagamento }}</p>
|
|
||||||
<div class="hidden sm:flex w-24 justify-center">
|
|
||||||
<span class="inline-flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded-full">
|
|
||||||
<i :class="['pi', formaPagMap[pag.formaPagamento]?.icone ?? 'pi-circle', 'text-xs']" aria-hidden="true" />
|
|
||||||
{{ formaPagMap[pag.formaPagamento]?.label ?? pag.formaPagamento }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm font-bold text-emerald-600 dark:text-emerald-400 w-28 text-right whitespace-nowrap">
|
|
||||||
{{ formatarMoeda(pag.valor) }}
|
|
||||||
</p>
|
|
||||||
<div class="w-28 flex justify-end">
|
|
||||||
<Button
|
|
||||||
icon="pi pi-print"
|
|
||||||
label="Comprovante"
|
|
||||||
size="small"
|
|
||||||
text
|
|
||||||
:loading="carregandoComprovante === pag.id"
|
|
||||||
:disabled="!!carregandoComprovante || !pag.idTaxa"
|
|
||||||
@click="baixarComprovante(pag)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tributos.length === 0 && !mensagemErro" class="bg-white dark:bg-slate-800 rounded-xl border p-12 text-center">
|
||||||
|
<p class="font-semibold text-slate-700 dark:text-slate-200">Nenhum pagamento encontrado</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="tributos.length">
|
||||||
|
<Accordion :multiple="true" :active-index="[0]">
|
||||||
|
<AccordionPanel v-for="(t, index) in tributos" :key="t.idContaTributo ?? index" :value="String(index)">
|
||||||
|
<AccordionHeader>
|
||||||
|
<span class="text-sm font-semibold truncate">
|
||||||
|
{{ t.descricaoTributo }} — {{ t.descricaoContaTributo }}
|
||||||
|
({{ t.pagamentos?.length ?? 0 }} pagamentos)
|
||||||
|
</span>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionContent>
|
||||||
|
<DataTable :value="t.pagamentos" size="small" show-gridlines scrollable class="mt-2">
|
||||||
|
<Column field="ndoc" header="Nº Doc" />
|
||||||
|
<Column field="refer" header="Período" />
|
||||||
|
<Column field="vencimento" header="Vencimento">
|
||||||
|
<template #body="{ data }">{{ formatDate(data.vencimento) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="pagamento" header="Pagamento">
|
||||||
|
<template #body="{ data }">{{ formatDate(data.pagamento) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="lancado" header="Principal" body-class="text-right">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.lancado) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="multa" header="Multa" body-class="text-right">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.multa) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="juros" header="Juros" body-class="text-right">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.juros) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="desconto" header="Desconto" body-class="text-right">
|
||||||
|
<template #body="{ data }">{{ formatCurrency(data.desconto) }}</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="total" header="Total" body-class="text-right">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<strong class="text-emerald-600">{{ formatCurrency(data.total) }}</strong>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="" style="width: 7rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-print"
|
||||||
|
label="Comprovante"
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
:loading="carregandoComprovante === data.id"
|
||||||
|
:disabled="!data.idContaCorrente"
|
||||||
|
@click="emitirComprovante(data)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionPanel>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border p-4 flex flex-wrap items-end justify-between gap-4">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-5 gap-4 text-sm">
|
||||||
|
<div><p class="text-slate-500 text-xs">Principal</p><p class="font-bold">{{ formatCurrency(totais.principal) }}</p></div>
|
||||||
|
<div><p class="text-slate-500 text-xs">Multa</p><p class="font-bold">{{ formatCurrency(totais.multa) }}</p></div>
|
||||||
|
<div><p class="text-slate-500 text-xs">Juros</p><p class="font-bold">{{ formatCurrency(totais.juros) }}</p></div>
|
||||||
|
<div><p class="text-slate-500 text-xs">Desconto</p><p class="font-bold">{{ formatCurrency(totais.desconto) }}</p></div>
|
||||||
|
<div><p class="text-slate-500 text-xs">Total</p><p class="font-bold text-emerald-600">{{ formatCurrency(totais.total) }}</p></div>
|
||||||
|
</div>
|
||||||
|
<Button label="Gerar PDF" icon="pi pi-file-pdf" size="small" :loading="isLoadingPdf" @click="gerarPdf" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -60,7 +60,8 @@ async function carregar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const acesRapidos = [
|
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-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-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' },
|
{ icon: 'pi-credit-card', label: 'Pagamentos', to: '/portal/pagamentos', cor: 'text-amber-600 dark:text-amber-400' },
|
||||||
|
|||||||
@ -22,8 +22,8 @@ const filtroProtocolo = ref('')
|
|||||||
|
|
||||||
const statusOpcoes = [
|
const statusOpcoes = [
|
||||||
{ value: 1, label: 'Emitida', classe: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400' },
|
{ 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: 2, label: 'Paga', classe: 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400' },
|
||||||
{ value: 3, 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]))
|
const statusMap = Object.fromEntries(statusOpcoes.map(s => [s.value, s]))
|
||||||
@ -199,7 +199,7 @@ function formatarData(val) {
|
|||||||
@click="baixar(taxa, 'guia')"
|
@click="baixar(taxa, 'guia')"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="taxa.status === 3"
|
v-if="taxa.status === 2"
|
||||||
icon="pi pi-check"
|
icon="pi pi-check"
|
||||||
label="Compr."
|
label="Compr."
|
||||||
size="small"
|
size="small"
|
||||||
@ -209,7 +209,7 @@ function formatarData(val) {
|
|||||||
@click="baixar(taxa, 'comprovante')"
|
@click="baixar(taxa, 'comprovante')"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="taxa.status === 3 && taxa.possuiDocComprobatorio"
|
v-if="taxa.status === 2 && taxa.possuiDocComprobatorio"
|
||||||
icon="pi pi-verified"
|
icon="pi pi-verified"
|
||||||
label="Autor."
|
label="Autor."
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { z } from 'zod'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { certidaoService } from '@/services/certidaoService'
|
import { certidaoService } from '@/services/certidaoService'
|
||||||
|
|
||||||
@ -15,26 +16,109 @@ const labelVoltar = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const documento = ref(isAuthenticated.value ? docUsuarioLogado.value : '')
|
const documento = ref(isAuthenticated.value ? docUsuarioLogado.value : '')
|
||||||
const tipoCertidao = ref('negativa')
|
const idModeloSelecionado = ref(null)
|
||||||
|
const finalidade = ref('')
|
||||||
|
const modelos = ref([])
|
||||||
const etapa = ref('formulario')
|
const etapa = ref('formulario')
|
||||||
|
const carregandoModelos = ref(false)
|
||||||
const carregandoConsulta = ref(false)
|
const carregandoConsulta = ref(false)
|
||||||
const carregandoEmissao = ref(false)
|
const carregandoEmissao = ref(false)
|
||||||
const resultado = ref(null)
|
const resultado = ref(null)
|
||||||
const mensagemErro = ref('')
|
const mensagemErro = ref('')
|
||||||
|
const erros = ref({})
|
||||||
|
|
||||||
const tiposCertidao = [
|
const schemaEmissao = z.object({
|
||||||
{ value: 'negativa', label: 'Certidão Negativa', descricao: 'Confirma que não há débitos pendentes.' },
|
idModeloSelecionado: z
|
||||||
{ value: 'positiva_efeitos_negativa', label: 'Positiva com Efeitos de Negativa', descricao: 'Débitos com parcelamento em dia ou com exigibilidade suspensa.' },
|
.number({ required_error: 'Selecione o modelo de certidão.' })
|
||||||
{ value: 'positiva', label: 'Certidão Positiva', descricao: 'Confirma a existência de débitos.' },
|
.nullable()
|
||||||
]
|
.refine(v => v !== null, 'Selecione o modelo de certidão.'),
|
||||||
|
finalidade: z.string().trim().min(1, 'Finalidade é obrigatória.'),
|
||||||
|
})
|
||||||
|
|
||||||
const docValido = computed(() => {
|
const docValido = computed(() => {
|
||||||
const d = documento.value.replace(/\D/g, '')
|
const d = documento.value.replace(/\D/g, '')
|
||||||
return d.length === 11 || d.length === 14
|
return d.length === 11 || d.length === 14
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const modeloSelecionado = computed(() =>
|
||||||
|
modelos.value.find(m => m.id === idModeloSelecionado.value) ?? null
|
||||||
|
)
|
||||||
|
|
||||||
|
const validadeLabel = computed(() => {
|
||||||
|
const dias = modeloSelecionado.value?.validadeDias
|
||||||
|
if (!dias) return 'conforme modelo selecionado'
|
||||||
|
return dias === 1 ? '1 dia a partir da emissão' : `${dias} dias a partir da emissão`
|
||||||
|
})
|
||||||
|
|
||||||
|
let debounceTimer = null
|
||||||
|
|
||||||
|
function resetModelos() {
|
||||||
|
modelos.value = []
|
||||||
|
idModeloSelecionado.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function extrairErro(e) {
|
||||||
|
return e?.data?.description ?? e?.data?.message ?? e?.statusMessage ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function carregarModelos() {
|
||||||
|
if (!docValido.value) {
|
||||||
|
resetModelos()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
carregandoModelos.value = true
|
||||||
|
mensagemErro.value = ''
|
||||||
|
try {
|
||||||
|
const res = await certidaoService.listarModelos(documento.value, { size: 50 })
|
||||||
|
modelos.value = res.data?.data ?? []
|
||||||
|
if (modelos.value.length === 0) {
|
||||||
|
mensagemErro.value = 'Não há certidões disponíveis para este tipo de contribuinte.'
|
||||||
|
}
|
||||||
|
if (!modelos.value.some(m => m.id === idModeloSelecionado.value)) {
|
||||||
|
idModeloSelecionado.value = null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resetModelos()
|
||||||
|
mensagemErro.value = extrairErro(e) ?? 'Não foi possível carregar os modelos de certidão.'
|
||||||
|
} finally {
|
||||||
|
carregandoModelos.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(documento, () => {
|
||||||
|
if (isAuthenticated.value) return
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
resetModelos()
|
||||||
|
if (docValido.value) carregarModelos()
|
||||||
|
}, 400)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (docValido.value) {
|
||||||
|
carregarModelos()
|
||||||
|
}
|
||||||
|
|
||||||
|
function validarFormulario() {
|
||||||
|
const parsed = schemaEmissao.safeParse({
|
||||||
|
idModeloSelecionado: idModeloSelecionado.value,
|
||||||
|
finalidade: finalidade.value,
|
||||||
|
})
|
||||||
|
if (!parsed.success) {
|
||||||
|
const mapa = {}
|
||||||
|
for (const issue of parsed.error.issues) {
|
||||||
|
const campo = issue.path[0]
|
||||||
|
if (campo && !mapa[campo]) mapa[campo] = issue.message
|
||||||
|
}
|
||||||
|
erros.value = mapa
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
erros.value = {}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
async function consultar() {
|
async function consultar() {
|
||||||
if (!docValido.value) return
|
if (!docValido.value) return
|
||||||
|
if (!validarFormulario()) return
|
||||||
carregandoConsulta.value = true
|
carregandoConsulta.value = true
|
||||||
mensagemErro.value = ''
|
mensagemErro.value = ''
|
||||||
try {
|
try {
|
||||||
@ -42,24 +126,31 @@ async function consultar() {
|
|||||||
resultado.value = res.data
|
resultado.value = res.data
|
||||||
etapa.value = 'resultado'
|
etapa.value = 'resultado'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
mensagemErro.value = e?.data?.description ?? 'Não foi possível consultar a situação fiscal. Tente novamente.'
|
mensagemErro.value = extrairErro(e) ?? 'Não foi possível consultar a situação fiscal. Tente novamente.'
|
||||||
} finally {
|
} finally {
|
||||||
carregandoConsulta.value = false
|
carregandoConsulta.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emitir() {
|
async function emitir() {
|
||||||
|
if (!validarFormulario()) return
|
||||||
carregandoEmissao.value = true
|
carregandoEmissao.value = true
|
||||||
|
mensagemErro.value = ''
|
||||||
try {
|
try {
|
||||||
const buf = await certidaoService.emitir(documento.value, tipoCertidao.value)
|
const buf = await certidaoService.emitir(
|
||||||
|
documento.value,
|
||||||
|
idModeloSelecionado.value,
|
||||||
|
finalidade.value.trim(),
|
||||||
|
)
|
||||||
|
const slug = (modeloSelecionado.value?.titulo ?? 'certidao').replace(/\s+/g, '-').toLowerCase()
|
||||||
const url = URL.createObjectURL(new Blob([buf], { type: 'application/pdf' }))
|
const url = URL.createObjectURL(new Blob([buf], { type: 'application/pdf' }))
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
a.href = url
|
||||||
a.download = `certidao-${tipoCertidao.value}-${documento.value}.pdf`
|
a.download = `certidao-${slug}-${documento.value.replace(/\D/g, '')}.pdf`
|
||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
} catch {
|
} catch (e) {
|
||||||
mensagemErro.value = 'Erro ao gerar o PDF. Tente novamente.'
|
mensagemErro.value = extrairErro(e) ?? 'Erro ao gerar o PDF. Tente novamente.'
|
||||||
} finally {
|
} finally {
|
||||||
carregandoEmissao.value = false
|
carregandoEmissao.value = false
|
||||||
}
|
}
|
||||||
@ -67,9 +158,14 @@ async function emitir() {
|
|||||||
|
|
||||||
function reiniciar() {
|
function reiniciar() {
|
||||||
documento.value = isAuthenticated.value ? docUsuarioLogado.value : ''
|
documento.value = isAuthenticated.value ? docUsuarioLogado.value : ''
|
||||||
|
idModeloSelecionado.value = null
|
||||||
|
finalidade.value = ''
|
||||||
resultado.value = null
|
resultado.value = null
|
||||||
mensagemErro.value = ''
|
mensagemErro.value = ''
|
||||||
|
erros.value = {}
|
||||||
etapa.value = 'formulario'
|
etapa.value = 'formulario'
|
||||||
|
resetModelos()
|
||||||
|
if (docValido.value) carregarModelos()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -111,24 +207,42 @@ function reiniciar() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div v-if="docValido">
|
||||||
<p class="text-sm font-semibold text-slate-700 dark:text-slate-200 mb-3">Tipo de certidão</p>
|
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">
|
||||||
<div class="space-y-2">
|
Modelo de certidão
|
||||||
<label
|
</label>
|
||||||
v-for="tipo in tiposCertidao"
|
<Select
|
||||||
:key="tipo.value"
|
v-model="idModeloSelecionado"
|
||||||
class="flex items-start gap-3 p-4 rounded-xl border cursor-pointer transition-colors"
|
:options="modelos"
|
||||||
:class="tipoCertidao === tipo.value
|
option-label="titulo"
|
||||||
? 'border-primary bg-primary/5 dark:bg-primary/10'
|
option-value="id"
|
||||||
: 'border-slate-200 dark:border-slate-600 hover:border-slate-300 dark:hover:border-slate-500'"
|
placeholder="Selecione o modelo de certidão"
|
||||||
>
|
class="w-full"
|
||||||
<RadioButton v-model="tipoCertidao" :value="tipo.value" :input-id="tipo.value" class="mt-0.5 flex-shrink-0" />
|
:class="{ 'p-invalid': erros.idModeloSelecionado }"
|
||||||
<div>
|
:loading="carregandoModelos"
|
||||||
<p class="text-sm font-semibold text-slate-800 dark:text-slate-100">{{ tipo.label }}</p>
|
:disabled="carregandoModelos || modelos.length === 0"
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ tipo.descricao }}</p>
|
/>
|
||||||
</div>
|
<p v-if="erros.idModeloSelecionado" class="mt-1.5 text-xs text-red-600 dark:text-red-400">
|
||||||
</label>
|
{{ erros.idModeloSelecionado }}
|
||||||
</div>
|
</p>
|
||||||
|
<p v-else-if="modeloSelecionado?.descricaoTipoCertidao" class="mt-1.5 text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{{ modeloSelecionado.descricaoTipoCertidao }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="docValido">
|
||||||
|
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">
|
||||||
|
Finalidade da certidão
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="finalidade"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': erros.finalidade }"
|
||||||
|
placeholder="Informe a finalidade (ex.: licitação, financiamento)"
|
||||||
|
/>
|
||||||
|
<p v-if="erros.finalidade" class="mt-1.5 text-xs text-red-600 dark:text-red-400">
|
||||||
|
{{ erros.finalidade }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="mensagemErro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
<p v-if="mensagemErro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||||
@ -142,7 +256,7 @@ function reiniciar() {
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
size="large"
|
size="large"
|
||||||
:loading="carregandoConsulta"
|
:loading="carregandoConsulta"
|
||||||
:disabled="!docValido"
|
:disabled="!docValido || carregandoModelos || modelos.length === 0"
|
||||||
@click="consultar"
|
@click="consultar"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -183,10 +297,13 @@ function reiniciar() {
|
|||||||
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 p-6">
|
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 p-6">
|
||||||
<p class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1">Certidão a emitir</p>
|
<p class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1">Certidão a emitir</p>
|
||||||
<p class="font-semibold text-slate-800 dark:text-slate-100">
|
<p class="font-semibold text-slate-800 dark:text-slate-100">
|
||||||
{{ tiposCertidao.find(t => t.value === tipoCertidao)?.label }}
|
{{ modeloSelecionado?.titulo ?? '—' }}
|
||||||
|
</p>
|
||||||
|
<p v-if="finalidade" class="text-sm text-slate-600 dark:text-slate-300 mt-2">
|
||||||
|
Finalidade: {{ finalidade }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-4">
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-4">
|
||||||
Validade: <span class="font-medium">180 dias a partir da emissão</span>
|
Validade: <span class="font-medium">{{ validadeLabel }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -12,10 +12,17 @@ export const certidaoService = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
emitir(documento, tipoCertidao) {
|
listarModelos(documento, params = {}) {
|
||||||
|
return $fetch(proxyUrl('/publico/certidao/modelos'), {
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
query: { documento, ...params },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
emitir(documento, idModelo, finalidade) {
|
||||||
return $fetch(proxyUrl('/publico/certidao/emitir'), {
|
return $fetch(proxyUrl('/publico/certidao/emitir'), {
|
||||||
headers: FETCH_HEADERS,
|
headers: FETCH_HEADERS,
|
||||||
query: { documento, tipoCertidao },
|
query: { documento, idModelo, finalidade },
|
||||||
responseType: 'arrayBuffer',
|
responseType: 'arrayBuffer',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
// Todas as chamadas vão via /api/proxy/** (BFF injeta Bearer + tenant headers).
|
// Todas as chamadas vão via /api/proxy/** (BFF injeta Bearer + tenant headers).
|
||||||
// Cada método retorna o envelope cru do core-api: { data, message, statusCode, ... }
|
|
||||||
// — exceto rotas binárias (PDF), que retornam ArrayBuffer.
|
|
||||||
|
|
||||||
const FETCH_HEADERS = { 'X-Requested-With': 'fetch' }
|
const FETCH_HEADERS = { 'X-Requested-With': 'fetch' }
|
||||||
|
|
||||||
@ -8,8 +6,15 @@ function proxyUrl(path) {
|
|||||||
return `/api/proxy${path}`
|
return `/api/proxy${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function limparParams(params) {
|
||||||
|
const out = {}
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
if (v !== null && v !== undefined && v !== '') out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
export const portalService = {
|
export const portalService = {
|
||||||
// ─── Painel ──────────────────────────────────────────────────────────────
|
|
||||||
getPainelResumo() {
|
getPainelResumo() {
|
||||||
return $fetch(proxyUrl('/contribuinte/painel/resumo'), { headers: FETCH_HEADERS })
|
return $fetch(proxyUrl('/contribuinte/painel/resumo'), { headers: FETCH_HEADERS })
|
||||||
},
|
},
|
||||||
@ -21,17 +26,45 @@ export const portalService = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// ─── Débitos ─────────────────────────────────────────────────────────────
|
// ─── Débitos / Extrato ───────────────────────────────────────────────────
|
||||||
getDebitos(params = {}) {
|
getDebitosExtrato(params = {}) {
|
||||||
return $fetch(proxyUrl('/contribuinte/debitos'), {
|
return $fetch(proxyUrl('/contribuinte/debitos'), {
|
||||||
headers: FETCH_HEADERS,
|
headers: FETCH_HEADERS,
|
||||||
query: params,
|
query: limparParams(params),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
emitirGuia(idDebito) {
|
getTransacoes(idContaCorrente) {
|
||||||
return $fetch(proxyUrl(`/contribuinte/debitos/${idDebito}/guia`), {
|
return $fetch(proxyUrl(`/contribuinte/debitos/${idContaCorrente}/transacoes`), { headers: FETCH_HEADERS })
|
||||||
|
},
|
||||||
|
|
||||||
|
gerarGuiaDebitos(dto) {
|
||||||
|
return $fetch(proxyUrl('/contribuinte/debitos/gerar-guia'), {
|
||||||
|
method: 'POST',
|
||||||
headers: FETCH_HEADERS,
|
headers: FETCH_HEADERS,
|
||||||
|
body: dto,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
baixarGuiaPdf(idDoctoArr) {
|
||||||
|
return $fetch(proxyUrl(`/contribuinte/debitos/guia/${idDoctoArr}`), {
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
responseType: 'arrayBuffer',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
emitirGuia(idContaCorrente) {
|
||||||
|
return $fetch(proxyUrl(`/contribuinte/debitos/${idContaCorrente}/guia`), {
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
responseType: 'arrayBuffer',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
gerarExtratoDebitosPdf(dto) {
|
||||||
|
return $fetch(proxyUrl('/contribuinte/debitos/extrato-pdf'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
body: dto,
|
||||||
responseType: 'arrayBuffer',
|
responseType: 'arrayBuffer',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -48,23 +81,44 @@ export const portalService = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// ─── Pagamentos ──────────────────────────────────────────────────────────
|
// ─── Pagamentos / Extrato ─────────────────────────────────────────────────
|
||||||
getPagamentos(params = {}) {
|
getPagamentosExtrato(params = {}) {
|
||||||
return $fetch(proxyUrl('/contribuinte/pagamentos'), {
|
return $fetch(proxyUrl('/contribuinte/pagamentos'), {
|
||||||
headers: FETCH_HEADERS,
|
headers: FETCH_HEADERS,
|
||||||
query: params,
|
query: limparParams(params),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getComprovante(idPagamento) {
|
gerarExtratoPagamentosPdf(dto) {
|
||||||
return $fetch(proxyUrl(`/contribuinte/pagamentos/${idPagamento}/comprovante`), {
|
return $fetch(proxyUrl('/contribuinte/pagamentos/extrato-pdf'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
body: dto,
|
||||||
|
responseType: 'arrayBuffer',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getComprovante(idContaCorrente) {
|
||||||
|
return $fetch(proxyUrl(`/contribuinte/pagamentos/${idContaCorrente}/comprovante`), {
|
||||||
headers: FETCH_HEADERS,
|
headers: FETCH_HEADERS,
|
||||||
responseType: 'arrayBuffer',
|
responseType: 'arrayBuffer',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getComprovanteByTaxa(idTaxa) {
|
// ─── Guias emitidas ──────────────────────────────────────────────────────
|
||||||
return $fetch(proxyUrl(`/arrecadacao/taxas/${idTaxa}/autorizacao`), {
|
listarGuias(params = {}) {
|
||||||
|
return $fetch(proxyUrl('/contribuinte/guias'), {
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
query: limparParams(params),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
buscarGuia(id) {
|
||||||
|
return $fetch(proxyUrl(`/contribuinte/guias/${id}`), { headers: FETCH_HEADERS })
|
||||||
|
},
|
||||||
|
|
||||||
|
baixarGuiaEmitidaPdf(id) {
|
||||||
|
return $fetch(proxyUrl(`/contribuinte/guias/${id}/pdf`), {
|
||||||
headers: FETCH_HEADERS,
|
headers: FETCH_HEADERS,
|
||||||
responseType: 'arrayBuffer',
|
responseType: 'arrayBuffer',
|
||||||
})
|
})
|
||||||
|
|||||||
49
src/utils/formatador.js
Normal file
49
src/utils/formatador.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { formatarMoeda, formatarDataParaAPI, baixarPdf } from '@/utils/formatacao'
|
||||||
|
|
||||||
|
export function formatDate(value) {
|
||||||
|
if (!value) return '—'
|
||||||
|
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||||
|
const [y, m, d] = value.split('-')
|
||||||
|
return `${d}/${m}/${y}`
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const date = new Date(value)
|
||||||
|
if (isNaN(date.getTime())) return '—'
|
||||||
|
return `${String(date.getDate()).padStart(2, '0')}/${String(date.getMonth() + 1).padStart(2, '0')}/${date.getFullYear()}`
|
||||||
|
} catch {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCurrency(value) {
|
||||||
|
if (value == null) return 'R$ 0,00'
|
||||||
|
return Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateISO(value) {
|
||||||
|
return formatarDataParaAPI(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateToYYYYMM(date) {
|
||||||
|
if (!date) return null
|
||||||
|
const d = new Date(date)
|
||||||
|
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseYYYYMM(str) {
|
||||||
|
if (!str) return null
|
||||||
|
const s = String(str)
|
||||||
|
if (s.length !== 6 || !/^\d{6}$/.test(s)) return null
|
||||||
|
return new Date(parseInt(s.substring(0, 4)), parseInt(s.substring(4, 6)) - 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function abrirPdf(buf) {
|
||||||
|
const url = URL.createObjectURL(new Blob([buf], { type: 'application/pdf' }))
|
||||||
|
const janela = window.open(url, '_blank')
|
||||||
|
if (!janela) {
|
||||||
|
baixarPdf(buf, 'documento.pdf')
|
||||||
|
}
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 60000)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { formatarMoeda, baixarPdf }
|
||||||
Loading…
x
Reference in New Issue
Block a user