feat: dashboard completo com stats, pipeline, alertas e tabela
This commit is contained in:
@@ -1,76 +1,173 @@
|
|||||||
|
<!-- 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>
|
<template>
|
||||||
<div>
|
<div class="page">
|
||||||
<UPageHero
|
<AppTopbar title="Dashboard" :breadcrumb="`Visão geral · ${dataFormatada}`">
|
||||||
title="Nuxt Starter Template"
|
<template #actions>
|
||||||
description="A production-ready starter template powered by Nuxt UI. Build beautiful, accessible, and performant applications in minutes, not hours."
|
<UButton variant="outline" color="neutral" size="sm">Importar Edital</UButton>
|
||||||
:links="[{
|
<UButton size="sm" class="btn-primary">+ Nova Oportunidade</UButton>
|
||||||
label: 'Get started',
|
</template>
|
||||||
to: 'https://ui.nuxt.com/docs/getting-started/installation/nuxt',
|
</AppTopbar>
|
||||||
target: '_blank',
|
|
||||||
trailingIcon: 'i-lucide-arrow-right',
|
|
||||||
size: 'xl'
|
|
||||||
}, {
|
|
||||||
label: 'Use this template',
|
|
||||||
to: 'https://github.com/nuxt-ui-templates/starter',
|
|
||||||
target: '_blank',
|
|
||||||
icon: 'i-simple-icons-github',
|
|
||||||
size: 'xl',
|
|
||||||
color: 'neutral',
|
|
||||||
variant: 'subtle'
|
|
||||||
}]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UPageSection
|
<div class="content">
|
||||||
id="features"
|
<!-- Stats -->
|
||||||
title="Everything you need to build modern Nuxt apps"
|
<div class="stats-grid">
|
||||||
description="Start with a solid foundation. This template includes all the essentials for building production-ready applications with Nuxt UI's powerful component system."
|
<StatCard label="Total de Editais" :value="dashboardStats.totalEditais" sub="Este ano" />
|
||||||
:features="[{
|
<StatCard label="Taxa de Vitória" :value="`${dashboardStats.taxaVitoria}%`" sub="Processos ganhos" color="#10b981" />
|
||||||
icon: 'i-lucide-rocket',
|
<StatCard label="Valor Ganho" :value="valorFormatado" sub="Em contratos ativos" color="#667eea" />
|
||||||
title: 'Production-ready from day one',
|
<StatCard label="Alertas Ativos" :value="dashboardStats.alertasAtivos" sub="Requerem atenção" color="#f59e0b" />
|
||||||
description: 'Pre-configured with TypeScript, ESLint, Tailwind CSS, and all the best practices. Focus on building features, not setting up tooling.'
|
</div>
|
||||||
}, {
|
|
||||||
icon: 'i-lucide-palette',
|
|
||||||
title: 'Beautiful by default',
|
|
||||||
description: 'Leveraging Nuxt UI\'s design system with automatic dark mode, consistent spacing, and polished components that look great out of the box.'
|
|
||||||
}, {
|
|
||||||
icon: 'i-lucide-zap',
|
|
||||||
title: 'Lightning fast',
|
|
||||||
description: 'Optimized for performance with SSR/SSG support, automatic code splitting, and edge-ready deployment. Your users will love the speed.'
|
|
||||||
}, {
|
|
||||||
icon: 'i-lucide-blocks',
|
|
||||||
title: '100+ components included',
|
|
||||||
description: 'Access Nuxt UI\'s comprehensive component library. From forms to navigation, everything is accessible, responsive, and customizable.'
|
|
||||||
}, {
|
|
||||||
icon: 'i-lucide-code-2',
|
|
||||||
title: 'Developer experience first',
|
|
||||||
description: 'Auto-imports, hot module replacement, and TypeScript support. Write less boilerplate and ship more features.'
|
|
||||||
}, {
|
|
||||||
icon: 'i-lucide-shield-check',
|
|
||||||
title: 'Built for scale',
|
|
||||||
description: 'Enterprise-ready architecture with proper error handling, SEO optimization, and security best practices built-in.'
|
|
||||||
}]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UPageSection>
|
<!-- Pipeline -->
|
||||||
<UPageCTA
|
<div class="card mb-4">
|
||||||
title="Ready to build your next Nuxt app?"
|
<div class="card-header">
|
||||||
description="Join thousands of developers building with Nuxt and Nuxt UI. Get this template and start shipping today."
|
<h3>Pipeline de Oportunidades</h3>
|
||||||
variant="subtle"
|
<NuxtLink to="/pipeline" class="card-link">Ver kanban →</NuxtLink>
|
||||||
:links="[{
|
</div>
|
||||||
label: 'Start building',
|
<div class="px-4">
|
||||||
to: 'https://ui.nuxt.com/docs/getting-started/installation/nuxt',
|
<PipelineBar :contagem-por-etapa="dashboardStats.editalsPorEtapaPipeline" />
|
||||||
target: '_blank',
|
</div>
|
||||||
trailingIcon: 'i-lucide-arrow-right',
|
</div>
|
||||||
color: 'neutral'
|
|
||||||
}, {
|
<!-- Grid 2 colunas -->
|
||||||
label: 'View on GitHub',
|
<div class="grid-2 mb-4">
|
||||||
to: 'https://github.com/nuxt-ui-templates/starter',
|
<!-- Alertas de Prazo -->
|
||||||
target: '_blank',
|
<div class="card">
|
||||||
icon: 'i-simple-icons-github',
|
<div class="card-header">
|
||||||
color: 'neutral',
|
<h3>Alertas de Prazo</h3>
|
||||||
variant: 'outline'
|
<NuxtLink to="/gestao/prazos" class="card-link">Ver todos →</NuxtLink>
|
||||||
}]"
|
</div>
|
||||||
/>
|
<div v-for="p in prazosUrgentes" :key="p.id" class="alert-item">
|
||||||
</UPageSection>
|
<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="[
|
||||||
|
{ key: 'numero', label: 'Nº Edital' },
|
||||||
|
{ key: 'objeto', label: 'Objeto' },
|
||||||
|
{ key: 'orgao', label: 'Órgão' },
|
||||||
|
{ key: 'modalidade', label: 'Modalidade' },
|
||||||
|
{ key: 'valorEstimado', label: 'Valor Est.' },
|
||||||
|
{ key: 'status', label: 'Status' },
|
||||||
|
{ key: 'dataAbertura', label: '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>
|
</div>
|
||||||
</template>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user