This commit is contained in:
parent
ceaffc088e
commit
46e76acad3
@ -145,3 +145,22 @@ export interface OpenCandDataAvailabilityStats {
|
||||
redeSocialCandidatos: number[];
|
||||
fotosCandidatos: number[];
|
||||
}
|
||||
|
||||
export interface OpenCandDatabaseStats {
|
||||
tables: {
|
||||
name: string;
|
||||
totalSize: number; // in bytes
|
||||
entries: number; // number of rows
|
||||
}[];
|
||||
materializedViews: {
|
||||
name: string;
|
||||
totalSize: number; // in bytes
|
||||
entries: number; // number of rows
|
||||
}[];
|
||||
indexes: {
|
||||
amount: number; // number of indexes
|
||||
size: number; // total size of indexes in bytes
|
||||
};
|
||||
totalSize: number; // total size of the database in bytes
|
||||
totalEntries: number; // total number of entries across all tables
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { BaseApiClient } from './base';
|
||||
import { API_CONFIG } from '../config/api';
|
||||
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, OpenCandDataAvailabilityStats, PlatformStats, RandomCandidate } from './apiModels';
|
||||
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, OpenCandDataAvailabilityStats, OpenCandDatabaseStats, PlatformStats, RandomCandidate } from './apiModels';
|
||||
import type { EnrichmentResponse, StatisticsConfig, ValueSumRequest, ValueSumResponse } from './apiStatisticsModels';
|
||||
|
||||
/**
|
||||
@ -27,6 +27,13 @@ export class OpenCandApi extends BaseApiClient {
|
||||
return this.get<OpenCandDataAvailabilityStats>('/v1/stats/data-availability');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database tech stats
|
||||
*/
|
||||
async getDatabaseTechStats(): Promise<OpenCandDatabaseStats> {
|
||||
return this.get<OpenCandDatabaseStats>(`/v1/stats/tech`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for candidates by name or other attributes
|
||||
*/
|
||||
|
@ -1,13 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FaCloudDownloadAlt, FaGoogleDrive } from 'react-icons/fa';
|
||||
import { openCandApi } from '../api';
|
||||
import type { OpenCandDataAvailabilityStats } from '../api/apiModels';
|
||||
import type { OpenCandDataAvailabilityStats, OpenCandDatabaseStats } from '../api/apiModels';
|
||||
import Card from '../shared/Card';
|
||||
import ErrorPage from './ErrorPage';
|
||||
|
||||
const DataStatsPage: React.FC = () => {
|
||||
const [stats, setStats] = useState<OpenCandDataAvailabilityStats | null>(null);
|
||||
const [dbStats, setDbStats] = useState<OpenCandDatabaseStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dbLoading, setDbLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dbError, setDbError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
@ -22,8 +26,20 @@ const DataStatsPage: React.FC = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDbStats = async () => {
|
||||
try {
|
||||
setDbLoading(true);
|
||||
const dbData = await openCandApi.getDatabaseTechStats();
|
||||
setDbStats(dbData);
|
||||
} catch (err) {
|
||||
setDbError('Erro ao carregar estatísticas técnicas do banco de dados');
|
||||
console.error('Error fetching database tech stats:', err);
|
||||
} finally {
|
||||
setDbLoading(false);
|
||||
}
|
||||
};
|
||||
fetchStats();
|
||||
fetchDbStats();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
@ -68,14 +84,14 @@ const DataStatsPage: React.FC = () => {
|
||||
{ key: 'despesaCandidatos', label: 'Despesas de Candidatos', icon: '💸' },
|
||||
{ key: 'receitaCandidatos', label: 'Receitas de Candidatos', icon: '💵' },
|
||||
{ key: 'redeSocialCandidatos', label: 'Redes Sociais', icon: '📱' },
|
||||
{ key: 'fotosCandidatos', label: 'Fotos de Candidatos', icon: '📸' },
|
||||
{ key: 'fotosCandidatos', label: 'Fotos de Candidatos (API)', icon: '📸' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen py-20 px-4 hover:cursor-default">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 animate-fade-in">
|
||||
<div className="text-center mb-12 animate-slide-in-left">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white">
|
||||
Disponibilidade de Dados
|
||||
</h1>
|
||||
@ -85,6 +101,23 @@ const DataStatsPage: React.FC = () => {
|
||||
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* Download DB Dump Section */}
|
||||
<div className="flex justify-center mb-10 animate-slide-in-left backdrop-blur-xs bg-gray-800/10 rounded-xl shadow-lg hover:shadow-xl transform transition-all duration-200 overflow-hidden ring-1 ring-gray-700 hover:ring-indigo-300 px-8 py-6 flex flex-col md:flex-row items-center gap-4 max-w-xl-auto">
|
||||
<div className="flex items-center gap-3 mb-2 md:mb-0">
|
||||
<FaCloudDownloadAlt className="text-3xl text-green-400" />
|
||||
<span className="text-lg font-semibold text-white">Download do Dump do Banco de Dados</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://drive.google.com/file/d/1cfMItrsAdv8y8YUNp04D33s6pYrRbmDn/view?usp=sharing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 py-2 px-5 bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-600 hover:to-purple-700 transition-all duration-300 transform shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<FaGoogleDrive className="text-xl" />
|
||||
Google Drive
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
|
||||
@ -133,7 +166,7 @@ const DataStatsPage: React.FC = () => {
|
||||
{sortedYears.map((year, index) => (
|
||||
<th
|
||||
key={year}
|
||||
className="text-center p-4 text-white font-semibold border-b border-gray-700/30 animate-fade-in"
|
||||
className="text-center p-4 text-white font-semibold border-b border-gray-700/30 animate-slide-in-left"
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
{year}
|
||||
@ -178,6 +211,131 @@ const DataStatsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Tech Stats Section */}
|
||||
<div className="mt-20 flex justify-center">
|
||||
<div className="bg-gray-800/10 backdrop-blur-xs rounded-xl shadow-lg hover:shadow-xl transform hover:scale-[1.005] transition-all duration-200 overflow-hidden ring-1 ring-gray-700 hover:ring-indigo-300 mb-12 w-full max-w-2xl">
|
||||
<div className="p-6 border-b border-gray-700/30 bg-gray-800/10">
|
||||
<h2 className="text-2xl font-bold text-white">Dados Técnicas do Banco de Dados</h2>
|
||||
<p className="text-gray-400 mt-2">Informações sobre tabelas, views materializadas e índices</p>
|
||||
</div>
|
||||
{dbLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-4 border-indigo-600 border-t-transparent mr-4"></div>
|
||||
<span className="text-gray-300">Carregando dados do banco de dados...</span>
|
||||
</div>
|
||||
) : dbError ? (
|
||||
<div className="p-8 text-red-400">{dbError}</div>
|
||||
) : dbStats ? (
|
||||
<div className="p-6 space-y-10">
|
||||
{/* Tables */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Tabelas</h3>
|
||||
<div className="overflow-x-auto max-h-112" style={{maxHeight: '32rem'}}>
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-800/10">
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Nome</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Entradas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dbStats.tables.map((table) => {
|
||||
const name = table.name.replace(/^public\./, '');
|
||||
const sizeMB = table.totalSize / 1024 / 1024;
|
||||
const sizeDisplay = sizeMB > 1024
|
||||
? `${(sizeMB / 1024).toFixed(2)} GB`
|
||||
: `${sizeMB.toFixed(2)} MB`;
|
||||
return (
|
||||
<tr key={table.name} className="hover:bg-gray-800/10 transition-all duration-200">
|
||||
<td className="p-3 text-white">{name}</td>
|
||||
<td className="p-3 text-gray-300">{sizeDisplay}</td>
|
||||
<td className="p-3 text-gray-300">{table.entries.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{/* Total row */}
|
||||
{(() => {
|
||||
const totalSize = dbStats.tables.reduce((acc, t) => acc + t.totalSize, 0);
|
||||
const totalEntries = dbStats.tables.reduce((acc, t) => acc + t.entries, 0);
|
||||
const totalMB = totalSize / 1024 / 1024;
|
||||
const totalDisplay = totalMB > 1024
|
||||
? `${(totalMB / 1024).toFixed(2)} GB`
|
||||
: `${totalMB.toFixed(2)} MB`;
|
||||
return (
|
||||
<tr className="font-bold bg-gray-900/30">
|
||||
<td className="p-3 text-white">Total</td>
|
||||
<td className="p-3 text-gray-300">{totalDisplay}</td>
|
||||
<td className="p-3 text-gray-300">{totalEntries.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Materialized Views */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Views Materializadas</h3>
|
||||
<div className="overflow-x-auto max-h-96" style={{maxHeight: '28rem'}}>
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-800/10">
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Nome</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Entradas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dbStats.materializedViews.map((view) => {
|
||||
const name = view.name.replace(/^public\./, '');
|
||||
const sizeMB = view.totalSize / 1024 / 1024;
|
||||
const sizeDisplay = sizeMB > 1024
|
||||
? `${(sizeMB / 1024).toFixed(2)} GB`
|
||||
: `${sizeMB.toFixed(2)} MB`;
|
||||
return (
|
||||
<tr key={view.name} className="hover:bg-gray-800/10 transition-all duration-200">
|
||||
<td className="p-3 text-white">{name}</td>
|
||||
<td className="p-3 text-gray-300">{sizeDisplay}</td>
|
||||
<td className="p-3 text-gray-300">{view.entries.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Indexes */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Índices</h3>
|
||||
<div className="overflow-x-auto max-h-96 flex justify-center" style={{maxHeight: '28rem'}}>
|
||||
<table className="w-auto text-left border-collapse mx-auto">
|
||||
<thead>
|
||||
<tr className="bg-gray-800/10">
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Quantidade</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="p-3 text-gray-300">{dbStats.indexes.amount}</td>
|
||||
{(() => {
|
||||
const sizeMB = dbStats.indexes.size / 1024 / 1024;
|
||||
const sizeDisplay = sizeMB > 1024
|
||||
? `${(sizeMB / 1024).toFixed(2)} GB`
|
||||
: `${sizeMB.toFixed(2)} MB`;
|
||||
return <td className="p-3 text-gray-300">{sizeDisplay}</td>;
|
||||
})()}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -95,14 +95,14 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Patrimônio Inicial (<span className="text-xs text-gray-500">{enrichmentData.anoInicial}</span>):</span>
|
||||
<span className="text-gray-900 font-medium">
|
||||
R$ {enrichmentData.patrimonioInicial?.toLocaleString('pt-BR') || '0'}
|
||||
R$ {enrichmentData.patrimonioInicial?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Patrimônio Final (<span className="text-xs text-gray-500">{enrichmentData.anoFinal}</span>):</span>
|
||||
<span className="text-gray-900 font-medium">
|
||||
R$ {enrichmentData.patrimonioFinal?.toLocaleString('pt-BR') || '0'}
|
||||
R$ {enrichmentData.patrimonioFinal?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-3">
|
||||
@ -112,7 +112,7 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
|
||||
enrichmentData.enriquecimento >= 0 ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{enrichmentData.enriquecimento >= 0 ? '+' : ''}
|
||||
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR') || '0'}
|
||||
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -202,7 +202,7 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
|
||||
)}
|
||||
<td className="py-3 text-gray-900">{item.ano || 'N/A'}</td>
|
||||
<td className="py-3 text-right font-medium text-gray-900">
|
||||
R$ {item.valor?.toLocaleString('pt-BR') || '0'}
|
||||
R$ {item.valor?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
@ -66,6 +66,9 @@ const StatisticsPage: React.FC = () => {
|
||||
<p className="text-white/70 text-lg">
|
||||
Análise de dados e estatísticas dos candidatos e partidos brasileiros
|
||||
</p>
|
||||
<p className="text-white/70 text-lg">
|
||||
Para mais informações, acesse a <a href="https://sig.tse.jus.br/ords/dwapr/r/seai/sig-eleicao/home" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline">página de estatísticas do TSE</a> ou o <a href="https://divulgacandcontas.tse.jus.br/divulga/#/home" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline">DivulgaCand do TSE</a>.
|
||||
</p>
|
||||
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user