feat: portal autenticado — Painel e Débitos com UI real
portalService: endpoints para todas as rotas /portal/* (débitos, certidões, alvarás, pagamentos, dados) PainelView: - Cards de resumo (totalDebitos, certidoesAtivas, alvarasAndamento, ultimoPagamento) com skeleton loading - Alerta destacado quando há débitos vencidos - Grid de acesso rápido (4 atalhos) - Timeline de atividade recente com skeleton DebitosView: - Filtros por tipo (IPTU, ISS, TAXA...) e status (VENCIDO, A_VENCER, PARCELADO) - Lista com checkbox de seleção múltipla + barra de total selecionado - Chips de status coloridos por situação - Emissão de guia individual por blob PDF - Skeleton loading e estado vazio/erro Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1b2aea444e
commit
fed6998a6f
61
src/services/portalService.js
Normal file
61
src/services/portalService.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import apiClient from '@/config/apiClient'
|
||||||
|
|
||||||
|
export const portalService = {
|
||||||
|
// Painel
|
||||||
|
getPainelResumo() {
|
||||||
|
return apiClient.get('/contribuinte/painel/resumo')
|
||||||
|
},
|
||||||
|
|
||||||
|
getAtividades(pagina = 0, tamanho = 5) {
|
||||||
|
return apiClient.get('/contribuinte/painel/atividades', {
|
||||||
|
params: { pagina, tamanho },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Débitos
|
||||||
|
getDebitos(params = {}) {
|
||||||
|
return apiClient.get('/contribuinte/debitos', { params })
|
||||||
|
},
|
||||||
|
|
||||||
|
emitirGuia(idDebito) {
|
||||||
|
return apiClient.get(`/contribuinte/debitos/${idDebito}/guia`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Certidões
|
||||||
|
getCertidoes() {
|
||||||
|
return apiClient.get('/contribuinte/certidoes')
|
||||||
|
},
|
||||||
|
|
||||||
|
reemitirCertidao(idCertidao) {
|
||||||
|
return apiClient.get(`/contribuinte/certidoes/${idCertidao}/pdf`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Alvarás
|
||||||
|
getAlvaras(params = {}) {
|
||||||
|
return apiClient.get('/contribuinte/alvaras', { params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pagamentos
|
||||||
|
getPagamentos(params = {}) {
|
||||||
|
return apiClient.get('/contribuinte/pagamentos', { params })
|
||||||
|
},
|
||||||
|
|
||||||
|
getComprovante(idPagamento) {
|
||||||
|
return apiClient.get(`/contribuinte/pagamentos/${idPagamento}/comprovante`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dados cadastrais
|
||||||
|
getDadosCadastrais() {
|
||||||
|
return apiClient.get('/contribuinte/dados')
|
||||||
|
},
|
||||||
|
|
||||||
|
atualizarContato(payload) {
|
||||||
|
return apiClient.put('/contribuinte/dados/contato', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,12 +1,237 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { portalService } from '@/services/portalService'
|
||||||
|
|
||||||
|
const debitos = ref([])
|
||||||
|
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 statusDisponiveis = [
|
||||||
|
{ value: 'VENCIDO', label: 'Vencido', classe: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400' },
|
||||||
|
{ value: 'A_VENCER', label: 'A vencer', classe: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400' },
|
||||||
|
{ value: 'PARCELADO', label: 'Parcelado', classe: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusClasse = Object.fromEntries(statusDisponiveis.map(s => [s.value, s.classe]))
|
||||||
|
const statusLabel = Object.fromEntries(statusDisponiveis.map(s => [s.value, s.label]))
|
||||||
|
|
||||||
|
onMounted(() => carregar())
|
||||||
|
|
||||||
|
async function carregar() {
|
||||||
|
carregando.value = true
|
||||||
|
mensagemErro.value = ''
|
||||||
|
try {
|
||||||
|
const params = {}
|
||||||
|
if (filtroTipo.value) params.tipo = filtroTipo.value
|
||||||
|
if (filtroStatus.value) params.status = filtroStatus.value
|
||||||
|
const { data } = await portalService.getDebitos(params)
|
||||||
|
debitos.value = data.data?.content ?? []
|
||||||
|
} catch (e) {
|
||||||
|
mensagemErro.value = e.response?.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 { data } = await portalService.emitirGuia(debito.id)
|
||||||
|
const url = URL.createObjectURL(new Blob([data], { type: 'application/pdf' }))
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `guia-${debito.id}.pdf`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} catch {
|
||||||
|
mensagemErro.value = '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()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<h1 class="text-2xl font-bold text-slate-800">Débitos e Guias</h1>
|
|
||||||
<div class="bg-white rounded-xl border border-slate-200 p-8 text-center text-slate-400">
|
<!-- Cabeçalho -->
|
||||||
<i class="pi pi-receipt text-4xl mb-3 block" />
|
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||||
<p>Consulta de débitos e emissão de guias — a ser implementado.</p>
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100">Débitos e Guias</h1>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">Consulte seus débitos e emita guias de pagamento.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros -->
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
<Select
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- Barra de ação para selecionados -->
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200"
|
||||||
|
enter-from-class="opacity-0 -translate-y-2"
|
||||||
|
enter-to-class="opacity-100 translate-y-0"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- Tabela -->
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
|
|
||||||
|
<!-- Loading skeleton -->
|
||||||
|
<div v-if="carregando" class="divide-y divide-slate-100 dark:divide-slate-700">
|
||||||
|
<div v-for="i in 5" :key="i" class="p-5 flex items-center gap-4">
|
||||||
|
<div class="w-4 h-4 bg-slate-200 dark:bg-slate-700 rounded 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-1/3" />
|
||||||
|
<div class="h-3 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-1/4" />
|
||||||
|
</div>
|
||||||
|
<div class="h-3.5 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-20" />
|
||||||
|
<div class="h-6 bg-slate-200 dark:bg-slate-700 rounded-full animate-pulse w-16" />
|
||||||
|
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded-lg animate-pulse w-24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vazio -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Erro -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Lista -->
|
||||||
|
<div v-else>
|
||||||
|
<!-- Header da lista -->
|
||||||
|
<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">
|
||||||
|
<Checkbox :binary="true" @change="toggleTodos($event.target.checked)" />
|
||||||
|
<span class="flex-1">Descrição</span>
|
||||||
|
<span class="hidden sm:block 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="flex items-center gap-4 px-5 py-4 hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors"
|
||||||
|
>
|
||||||
|
<Checkbox v-model="debito._selecionado" :binary="true" />
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-slate-800 dark:text-slate-100 truncate">{{ debito.descricao }}</p>
|
||||||
|
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5">{{ debito.tipo }} · Ref: {{ debito.referencia }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="hidden sm:block text-sm text-slate-500 dark:text-slate-400 w-28 text-right whitespace-nowrap">
|
||||||
|
{{ debito.vencimento }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="w-28 text-right">
|
||||||
|
<p class="text-sm font-bold text-slate-800 dark:text-slate-100">{{ 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>
|
||||||
|
|
||||||
|
<div class="w-20 flex justify-center">
|
||||||
|
<span
|
||||||
|
: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']"
|
||||||
|
>
|
||||||
|
{{ statusLabel[debito.status] ?? debito.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-28 flex justify-end">
|
||||||
|
<Button
|
||||||
|
label="Emitir guia"
|
||||||
|
icon="pi pi-download"
|
||||||
|
size="small"
|
||||||
|
outlined
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
:loading="carregandoGuia === debito.id"
|
||||||
|
@click="emitirGuia(debito)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,55 +1,188 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
|
import { portalService } from '@/services/portalService'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const cards = [
|
const resumo = ref(null)
|
||||||
{ icon: 'pi-receipt', label: 'Débitos em Aberto', valor: '—', cor: 'red' },
|
const atividades = ref([])
|
||||||
{ icon: 'pi-file-check', label: 'Certidões Ativas', valor: '—', cor: 'green' },
|
const carregando = ref(true)
|
||||||
{ icon: 'pi-briefcase', label: 'Alvarás em Andamento', valor: '—', cor: 'orange' },
|
|
||||||
{ icon: 'pi-credit-card', label: 'Último Pagamento', valor: '—', cor: 'blue' },
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const [resResumo, resAtividades] = await Promise.all([
|
||||||
|
portalService.getPainelResumo(),
|
||||||
|
portalService.getAtividades(),
|
||||||
|
])
|
||||||
|
resumo.value = resResumo.data.data
|
||||||
|
atividades.value = resAtividades.data.data?.content ?? []
|
||||||
|
} catch {
|
||||||
|
// silencioso — exibe zeros
|
||||||
|
} finally {
|
||||||
|
carregando.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const acesRapidos = [
|
||||||
|
{ icon: 'pi-receipt', label: 'Emitir Guia', to: 'debitos', cor: 'text-primary' },
|
||||||
|
{ icon: 'pi-file-check', label: 'Nova Certidão', to: 'certidoes-portal', cor: 'text-emerald-600 dark:text-emerald-400' },
|
||||||
|
{ icon: 'pi-briefcase', label: 'Acompanhar Alvará', to: 'alvaras', cor: 'text-amber-600 dark:text-amber-400' },
|
||||||
|
{ icon: 'pi-user', label: 'Meus Dados', to: 'dados', cor: 'text-violet-600 dark:text-violet-400' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const corMap = {
|
function formatarMoeda(valor) {
|
||||||
red: 'bg-red-50 text-red-700',
|
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valor ?? 0)
|
||||||
green: 'bg-green-50 text-green-700',
|
}
|
||||||
orange: 'bg-orange-50 text-orange-700',
|
|
||||||
blue: 'bg-blue-50 text-blue-700',
|
const iconeAtividade = {
|
||||||
|
DEBITO: 'pi-receipt',
|
||||||
|
CERTIDAO: 'pi-file-check',
|
||||||
|
ALVARA: 'pi-briefcase',
|
||||||
|
PAGAMENTO: 'pi-credit-card',
|
||||||
|
CADASTRO: 'pi-user',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
|
|
||||||
|
<!-- Saudação -->
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-slate-800">
|
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-100">
|
||||||
Olá, {{ auth.nomeUsuario || 'Contribuinte' }}
|
Olá, {{ auth.nomeUsuario || 'Contribuinte' }} 👋
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-slate-500 mt-1">Bem-vindo ao seu painel de gestão fiscal.</p>
|
<p class="text-slate-500 dark:text-slate-400 mt-1">Bem-vindo ao seu painel de gestão fiscal.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cards resumo -->
|
<!-- Cards de resumo -->
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<div
|
|
||||||
v-for="card in cards"
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5 space-y-3">
|
||||||
:key="card.label"
|
<div class="w-10 h-10 bg-red-50 dark:bg-red-900/20 rounded-lg flex items-center justify-center">
|
||||||
class="bg-white rounded-xl border border-slate-200 p-5 space-y-3"
|
<i class="pi pi-receipt text-red-600 dark:text-red-400 text-sm" aria-hidden="true" />
|
||||||
>
|
|
||||||
<div :class="['w-10 h-10 rounded-lg flex items-center justify-center', corMap[card.cor]]">
|
|
||||||
<i :class="['pi', card.icon]" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-2xl font-bold text-slate-800">{{ card.valor }}</p>
|
<p class="text-2xl font-bold text-slate-800 dark:text-slate-100">
|
||||||
<p class="text-xs text-slate-500 mt-0.5">{{ card.label }}</p>
|
<span v-if="carregando" class="inline-block w-12 h-7 bg-slate-200 dark:bg-slate-700 rounded animate-pulse" />
|
||||||
|
<template v-else>{{ formatarMoeda(resumo?.totalDebitos) }}</template>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Débitos em aberto</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5 space-y-3">
|
||||||
|
<div class="w-10 h-10 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<i class="pi pi-file-check text-emerald-600 dark:text-emerald-400 text-sm" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-2xl font-bold text-slate-800 dark:text-slate-100">
|
||||||
|
<span v-if="carregando" class="inline-block w-8 h-7 bg-slate-200 dark:bg-slate-700 rounded animate-pulse" />
|
||||||
|
<template v-else>{{ resumo?.certidoesAtivas ?? 0 }}</template>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Certidões ativas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5 space-y-3">
|
||||||
|
<div class="w-10 h-10 bg-amber-50 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
|
||||||
|
<i class="pi pi-briefcase text-amber-600 dark:text-amber-400 text-sm" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-2xl font-bold text-slate-800 dark:text-slate-100">
|
||||||
|
<span v-if="carregando" class="inline-block w-8 h-7 bg-slate-200 dark:bg-slate-700 rounded animate-pulse" />
|
||||||
|
<template v-else>{{ resumo?.alvarasAndamento ?? 0 }}</template>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Alvarás em andamento</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5 space-y-3">
|
||||||
|
<div class="w-10 h-10 bg-primary/10 dark:bg-primary/20 rounded-lg flex items-center justify-center">
|
||||||
|
<i class="pi pi-credit-card text-primary text-sm" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-2xl font-bold text-slate-800 dark:text-slate-100">
|
||||||
|
<span v-if="carregando" class="inline-block w-16 h-7 bg-slate-200 dark:bg-slate-700 rounded animate-pulse" />
|
||||||
|
<template v-else>{{ formatarMoeda(resumo?.ultimoPagamento) }}</template>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Último pagamento</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Placeholder conteúdo -->
|
<!-- Alerta de débitos vencidos -->
|
||||||
<div class="bg-blue-50 border border-blue-100 rounded-xl p-6 text-center">
|
<div
|
||||||
<i class="pi pi-info-circle text-blue-500 text-2xl mb-3 block" />
|
v-if="!carregando && resumo?.debitosVencidos > 0"
|
||||||
<p class="text-sm text-slate-600">
|
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700/50 rounded-xl p-4 flex items-center gap-4"
|
||||||
Os dados do painel serão carregados quando a integração com a API estiver configurada.
|
>
|
||||||
</p>
|
<i class="pi pi-exclamation-triangle text-red-600 dark:text-red-400 text-xl flex-shrink-0" aria-hidden="true" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="font-semibold text-red-800 dark:text-red-300 text-sm">
|
||||||
|
{{ resumo.debitosVencidos }} débito{{ resumo.debitosVencidos > 1 ? 's' : '' }} vencido{{ resumo.debitosVencidos > 1 ? 's' : '' }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-red-600 dark:text-red-400 mt-0.5">Regularize para evitar juros e negativação.</p>
|
||||||
|
</div>
|
||||||
|
<Button label="Ver débitos" size="small" severity="danger" @click="router.push({ name: 'debitos' })" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
<!-- Acesso rápido -->
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-6">
|
||||||
|
<p class="text-sm font-bold text-slate-700 dark:text-slate-200 mb-4">Acesso rápido</p>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
v-for="a in acesRapidos"
|
||||||
|
:key="a.label"
|
||||||
|
class="flex flex-col items-center gap-2 p-4 rounded-xl border border-slate-100 dark:border-slate-700 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-primary/10 transition-colors group"
|
||||||
|
@click="router.push({ name: a.to })"
|
||||||
|
>
|
||||||
|
<i :class="['pi', a.icon, a.cor, 'text-xl']" aria-hidden="true" />
|
||||||
|
<span class="text-xs font-semibold text-slate-600 dark:text-slate-300 text-center leading-tight group-hover:text-primary transition-colors">{{ a.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Atividades recentes -->
|
||||||
|
<div class="lg:col-span-2 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-6">
|
||||||
|
<p class="text-sm font-bold text-slate-700 dark:text-slate-200 mb-4">Atividade recente</p>
|
||||||
|
|
||||||
|
<div v-if="carregando" class="space-y-3">
|
||||||
|
<div v-for="i in 4" :key="i" class="flex gap-3 items-center">
|
||||||
|
<div class="w-8 h-8 bg-slate-200 dark:bg-slate-700 rounded-lg animate-pulse flex-shrink-0" />
|
||||||
|
<div class="flex-1 space-y-1.5">
|
||||||
|
<div class="h-3 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-3/4" />
|
||||||
|
<div class="h-2.5 bg-slate-200 dark:bg-slate-700 rounded animate-pulse w-1/2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="atividades.length === 0" class="text-center py-6">
|
||||||
|
<i class="pi pi-inbox text-slate-300 dark:text-slate-600 text-3xl mb-2 block" aria-hidden="true" />
|
||||||
|
<p class="text-sm text-slate-400 dark:text-slate-500">Nenhuma atividade registrada.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="(ativ, i) in atividades"
|
||||||
|
:key="i"
|
||||||
|
class="flex items-center gap-3 py-3 border-b border-slate-100 dark:border-slate-700 last:border-0"
|
||||||
|
>
|
||||||
|
<div class="w-8 h-8 bg-primary/8 dark:bg-primary/15 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
|
<i :class="['pi', iconeAtividade[ativ.tipo] ?? 'pi-circle', 'text-primary text-sm']" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-slate-800 dark:text-slate-100 truncate">{{ ativ.descricao }}</p>
|
||||||
|
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5">{{ ativ.data }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user