This commit is contained in:
parent
ceaffc088e
commit
46e76acad3
@ -144,4 +144,23 @@ export interface OpenCandDataAvailabilityStats {
|
|||||||
receitaCandidatos: number[];
|
receitaCandidatos: number[];
|
||||||
redeSocialCandidatos: number[];
|
redeSocialCandidatos: number[];
|
||||||
fotosCandidatos: 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 { BaseApiClient } from './base';
|
||||||
import { API_CONFIG } from '../config/api';
|
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';
|
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');
|
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
|
* Search for candidates by name or other attributes
|
||||||
*/
|
*/
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { FaCloudDownloadAlt, FaGoogleDrive } from 'react-icons/fa';
|
||||||
import { openCandApi } from '../api';
|
import { openCandApi } from '../api';
|
||||||
import type { OpenCandDataAvailabilityStats } from '../api/apiModels';
|
import type { OpenCandDataAvailabilityStats, OpenCandDatabaseStats } from '../api/apiModels';
|
||||||
import Card from '../shared/Card';
|
import Card from '../shared/Card';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
|
|
||||||
const DataStatsPage: React.FC = () => {
|
const DataStatsPage: React.FC = () => {
|
||||||
const [stats, setStats] = useState<OpenCandDataAvailabilityStats | null>(null);
|
const [stats, setStats] = useState<OpenCandDataAvailabilityStats | null>(null);
|
||||||
|
const [dbStats, setDbStats] = useState<OpenCandDatabaseStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [dbLoading, setDbLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [dbError, setDbError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
@ -22,8 +26,20 @@ const DataStatsPage: React.FC = () => {
|
|||||||
setLoading(false);
|
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();
|
fetchStats();
|
||||||
|
fetchDbStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -68,14 +84,14 @@ const DataStatsPage: React.FC = () => {
|
|||||||
{ key: 'despesaCandidatos', label: 'Despesas de Candidatos', icon: '💸' },
|
{ key: 'despesaCandidatos', label: 'Despesas de Candidatos', icon: '💸' },
|
||||||
{ key: 'receitaCandidatos', label: 'Receitas de Candidatos', icon: '💵' },
|
{ key: 'receitaCandidatos', label: 'Receitas de Candidatos', icon: '💵' },
|
||||||
{ key: 'redeSocialCandidatos', label: 'Redes Sociais', icon: '📱' },
|
{ key: 'redeSocialCandidatos', label: 'Redes Sociais', icon: '📱' },
|
||||||
{ key: 'fotosCandidatos', label: 'Fotos de Candidatos', icon: '📸' },
|
{ key: 'fotosCandidatos', label: 'Fotos de Candidatos (API)', icon: '📸' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen py-20 px-4 hover:cursor-default">
|
<div className="min-h-screen py-20 px-4 hover:cursor-default">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* 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">
|
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white">
|
||||||
Disponibilidade de Dados
|
Disponibilidade de Dados
|
||||||
</h1>
|
</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 className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||||
</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 */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||||
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
|
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
|
||||||
@ -133,7 +166,7 @@ const DataStatsPage: React.FC = () => {
|
|||||||
{sortedYears.map((year, index) => (
|
{sortedYears.map((year, index) => (
|
||||||
<th
|
<th
|
||||||
key={year}
|
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` }}
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
>
|
>
|
||||||
{year}
|
{year}
|
||||||
@ -178,6 +211,131 @@ const DataStatsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -95,14 +95,14 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
|
|||||||
<div className="flex justify-between items-center">
|
<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-600">Patrimônio Inicial (<span className="text-xs text-gray-500">{enrichmentData.anoInicial}</span>):</span>
|
||||||
<span className="text-gray-900 font-medium">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<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-600">Patrimônio Final (<span className="text-xs text-gray-500">{enrichmentData.anoFinal}</span>):</span>
|
||||||
<span className="text-gray-900 font-medium">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-200 pt-3">
|
<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 ? 'text-green-600' : 'text-red-600'
|
||||||
}`}>
|
}`}>
|
||||||
{enrichmentData.enriquecimento >= 0 ? '+' : ''}
|
{enrichmentData.enriquecimento >= 0 ? '+' : ''}
|
||||||
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR') || '0'}
|
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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-gray-900">{item.ano || 'N/A'}</td>
|
||||||
<td className="py-3 text-right font-medium text-gray-900">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
@ -66,6 +66,9 @@ const StatisticsPage: React.FC = () => {
|
|||||||
<p className="text-white/70 text-lg">
|
<p className="text-white/70 text-lg">
|
||||||
Análise de dados e estatísticas dos candidatos e partidos brasileiros
|
Análise de dados e estatísticas dos candidatos e partidos brasileiros
|
||||||
</p>
|
</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 className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user