feat: módulo de inteligência de mercado
This commit is contained in:
85
front-end/app/pages/inteligencia/index.vue
Normal file
85
front-end/app/pages/inteligencia/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<!-- front-end/app/pages/inteligencia/index.vue -->
|
||||
<script setup lang="ts">
|
||||
const { taxaVitoria, valorGanho, ticketMedio, porModalidade, motivoPerda, concorrentesFrequentes } = useInteligencia()
|
||||
|
||||
const modalidadeLabel: Record<string, string> = {
|
||||
pregao_eletronico: 'Pregão Eletrônico',
|
||||
pregao_presencial: 'Pregão Presencial',
|
||||
concorrencia: 'Concorrência',
|
||||
dispensa: 'Dispensa',
|
||||
inexigibilidade: 'Inexigibilidade',
|
||||
}
|
||||
|
||||
const fmt = (v: number) => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }).format(v)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<AppTopbar title="Inteligência de Mercado" breadcrumb="Inteligência" />
|
||||
<div class="content">
|
||||
|
||||
<!-- Stats topo -->
|
||||
<div class="stats-grid mb-4">
|
||||
<StatCard label="Taxa de Vitória" :value="`${taxaVitoria}%`" color="#10b981" sub="Processos ganhos" />
|
||||
<StatCard label="Valor Ganho Total" :value="fmt(valorGanho)" color="#667eea" />
|
||||
<StatCard label="Ticket Médio" :value="fmt(ticketMedio)" color="#764ba2" />
|
||||
<StatCard label="Concorrentes" :value="concorrentesFrequentes.length" sub="Cadastrados" />
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mb-4">
|
||||
<!-- Por modalidade -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>Modalidades</h3></div>
|
||||
<div class="pad">
|
||||
<div v-for="(val, key) in porModalidade" :key="key" class="modal-row">
|
||||
<span class="modal-nome">{{ modalidadeLabel[key] }}</span>
|
||||
<span class="modal-stats">{{ val.vitorias }}/{{ val.total }} — {{ val.total > 0 ? Math.round((val.vitorias / val.total) * 100) : 0 }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Motivo de perda -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>Classificação de Perdas</h3></div>
|
||||
<div class="pad">
|
||||
<div v-for="m in motivoPerda" :key="m.motivo" class="modal-row">
|
||||
<span class="modal-nome">{{ m.motivo }}</span>
|
||||
<UBadge :label="String(m.quantidade)" variant="soft" color="error" size="xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Concorrentes frequentes -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>Concorrentes Mais Frequentes</h3></div>
|
||||
<UTable
|
||||
:data="concorrentesFrequentes"
|
||||
:columns="[
|
||||
{ key: 'nome', label: 'Empresa' },
|
||||
{ key: 'cnpj', label: 'CNPJ' },
|
||||
{ key: 'totalDisputas', label: 'Disputas' },
|
||||
{ key: 'totalVitorias', label: 'Vitórias' },
|
||||
]"
|
||||
/>
|
||||
</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; }
|
||||
.mb-4 { margin-bottom: 14px; }
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.card { background: white; border-radius: 11px; border: 1px solid #e2e8f0; overflow: hidden; }
|
||||
.card-header { padding: 14px 18px 10px; border-bottom: 1px solid #f1f5f9; }
|
||||
.card-header h3 { font-size: 13px; font-weight: 700; color: #0f172a; }
|
||||
.pad { padding: 12px 18px; }
|
||||
.modal-row { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; border-bottom: 1px solid #f8fafc; font-size: 12px; }
|
||||
.modal-row:last-child { border-bottom: none; }
|
||||
.modal-nome { color: #374151; }
|
||||
.modal-stats { font-weight: 600; color: #667eea; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user