Files
lic/front-end/app/pages/index.vue
Junior 587a0d4f62 fix: corrige formato de colunas UTable para Nuxt UI v3 (TanStack Table)
Substitui o formato de colunas { key, label } (Nuxt UI v2) pelo formato
correto { id, accessorKey, header } exigido pelo TanStack Table no Nuxt UI v3.
Afetou 7 arquivos: EditaisTable, index, orgaos, concorrentes, contratos,
inteligencia/index e sistema/usuarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:03:39 -03:00

174 lines
7.5 KiB
Vue

<!-- front-end/app/pages/index.vue -->
<script setup lang="ts">
import { editais } from '~/data/mock/editais'
import { prazos } from '~/data/mock/prazos'
import { documentos } from '~/data/mock/documentos'
import { dashboardStats } from '~/data/mock/stats'
const hoje = new Date()
const dataFormatada = hoje.toLocaleDateString('pt-BR', { day: 'numeric', month: 'long', year: 'numeric' })
const valorFormatado = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }).format(dashboardStats.valorGanho)
const editaisRecentes = computed(() => [...editais].sort((a, b) => b.dataAbertura.getTime() - a.dataAbertura.getTime()).slice(0, 5))
const prazosUrgentes = computed(() => prazos.filter(p => p.urgencia === 'critico' || p.urgencia === 'urgente'))
const docsAlerta = computed(() => documentos.filter(d => d.status === 'vencendo' || d.status === 'vencida'))
function urgenciaCor(urgencia: string) {
if (urgencia === 'critico') return '#ef4444'
if (urgencia === 'urgente') return '#f59e0b'
return '#667eea'
}
function urgenciaLabel(p: typeof prazos[0]) {
const diff = Math.ceil((p.dataLimite.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24))
if (diff <= 0) return 'Hoje'
if (diff === 1) return 'Amanhã'
return p.dataLimite.toLocaleDateString('pt-BR')
}
function docStatusCor(status: string) {
if (status === 'vencida') return '#dc2626'
if (status === 'vencendo') return '#d97706'
return '#16a34a'
}
const modalidadeLabel: Record<string, string> = {
pregao_eletronico: 'Pregão Eletrônico',
pregao_presencial: 'Pregão Presencial',
concorrencia: 'Concorrência',
dispensa: 'Dispensa',
inexigibilidade: 'Inexigibilidade',
}
</script>
<template>
<div class="page">
<AppTopbar title="Dashboard" :breadcrumb="`Visão geral · ${dataFormatada}`">
<template #actions>
<UButton variant="outline" color="neutral" size="sm">Importar Edital</UButton>
<UButton size="sm" class="btn-primary">+ Nova Oportunidade</UButton>
</template>
</AppTopbar>
<div class="content">
<!-- Stats -->
<div class="stats-grid">
<StatCard label="Total de Editais" :value="dashboardStats.totalEditais" sub="Este ano" />
<StatCard label="Taxa de Vitória" :value="`${dashboardStats.taxaVitoria}%`" sub="Processos ganhos" color="#10b981" />
<StatCard label="Valor Ganho" :value="valorFormatado" sub="Em contratos ativos" color="#667eea" />
<StatCard label="Alertas Ativos" :value="dashboardStats.alertasAtivos" sub="Requerem atenção" color="#f59e0b" />
</div>
<!-- Pipeline -->
<div class="card mb-4">
<div class="card-header">
<h3>Pipeline de Oportunidades</h3>
<NuxtLink to="/pipeline" class="card-link">Ver kanban </NuxtLink>
</div>
<div class="px-4">
<PipelineBar :contagem-por-etapa="dashboardStats.editalsPorEtapaPipeline" />
</div>
</div>
<!-- Grid 2 colunas -->
<div class="grid-2 mb-4">
<!-- Alertas de Prazo -->
<div class="card">
<div class="card-header">
<h3>Alertas de Prazo</h3>
<NuxtLink to="/gestao/prazos" class="card-link">Ver todos </NuxtLink>
</div>
<div v-for="p in prazosUrgentes" :key="p.id" class="alert-item">
<div class="alert-dot" :style="{ background: urgenciaCor(p.urgencia) }" />
<div class="alert-text">
<p class="at">{{ p.titulo }}</p>
<p class="as">{{ p.descricao }}</p>
</div>
<span class="alert-date" :style="{ color: urgenciaCor(p.urgencia) }">{{ urgenciaLabel(p) }}</span>
</div>
</div>
<!-- Documentos -->
<div class="card">
<div class="card-header">
<h3>Documentos da Empresa</h3>
<NuxtLink to="/gestao/documentos" class="card-link">Gerenciar </NuxtLink>
</div>
<div v-for="d in docsAlerta" :key="d.id" class="doc-item">
<div>
<p class="doc-nome">{{ d.nome }}</p>
<p class="doc-sub">
{{ d.status === 'vencida' ? 'Vencida' : 'Vencendo em breve' }}
</p>
</div>
<span class="doc-badge" :style="{ color: docStatusCor(d.status), background: docStatusCor(d.status) + '18' }">
{{ d.status === 'vencida' ? 'Vencida' : 'Vencendo' }}
</span>
</div>
</div>
</div>
<!-- Tabela Editais Recentes -->
<div class="card">
<div class="card-header">
<h3>Editais Recentes</h3>
<NuxtLink to="/oportunidades" class="card-link">Ver todos </NuxtLink>
</div>
<UTable
:data="editaisRecentes"
:columns="[
{ id: 'numero', accessorKey: 'numero', header: 'Nº Edital' },
{ id: 'objeto', accessorKey: 'objeto', header: 'Objeto' },
{ id: 'orgao', accessorKey: 'orgao', header: 'Órgão' },
{ id: 'modalidade', accessorKey: 'modalidade', header: 'Modalidade' },
{ id: 'valorEstimado', accessorKey: 'valorEstimado', header: 'Valor Est.' },
{ id: 'status', accessorKey: 'status', header: 'Status' },
{ id: 'dataAbertura', accessorKey: 'dataAbertura', header: 'Abertura' },
]"
>
<template #modalidade-cell="{ row }">
{{ modalidadeLabel[row.original.modalidade] }}
</template>
<template #valorEstimado-cell="{ row }">
{{ new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }).format(row.original.valorEstimado) }}
</template>
<template #status-cell="{ row }">
<StatusChip :status="row.original.status" />
</template>
<template #dataAbertura-cell="{ row }">
{{ row.original.dataAbertura.toLocaleDateString('pt-BR') }}
</template>
</UTable>
</div>
</div>
</div>
</template>
<style scoped>
.page { display: flex; flex-direction: column; height: 100vh; }
.content { padding: 20px 22px; flex: 1; overflow-y: auto; }
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 16px; }
.card { background: white; border-radius: 11px; border: 1px solid #e2e8f0; overflow: hidden; }
.mb-4 { margin-bottom: 14px; }
.card-header { padding: 14px 18px 10px; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; justify-content: space-between; }
.card-header h3 { font-size: 13px; font-weight: 700; color: #0f172a; }
.card-link { font-size: 11px; color: #667eea; text-decoration: none; font-weight: 500; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.alert-item { display: flex; align-items: center; gap: 10px; padding: 10px 18px; border-bottom: 1px solid #f8fafc; }
.alert-item:last-child { border-bottom: none; }
.alert-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.at { font-size: 12px; font-weight: 600; color: #0f172a; }
.as { font-size: 11px; color: #94a3b8; }
.alert-date { font-size: 11px; font-weight: 600; flex-shrink: 0; }
.doc-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 18px; border-bottom: 1px solid #f8fafc; }
.doc-item:last-child { border-bottom: none; }
.doc-nome { font-size: 12px; font-weight: 500; color: #0f172a; }
.doc-sub { font-size: 10.5px; color: #94a3b8; }
.doc-badge { font-size: 10.5px; font-weight: 600; padding: 2px 8px; border-radius: 20px; }
.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2) !important; }
.px-4 { padding: 0 18px; }
</style>