diff --git a/src/api/apiModels.ts b/src/api/apiModels.ts index 643c20a..3d5449b 100644 --- a/src/api/apiModels.ts +++ b/src/api/apiModels.ts @@ -144,4 +144,23 @@ export interface OpenCandDataAvailabilityStats { receitaCandidatos: number[]; 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 } \ No newline at end of file diff --git a/src/api/openCandApi.ts b/src/api/openCandApi.ts index 81b00ad..2b3b5b3 100644 --- a/src/api/openCandApi.ts +++ b/src/api/openCandApi.ts @@ -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('/v1/stats/data-availability'); } + /** + * Get the database tech stats + */ + async getDatabaseTechStats(): Promise { + return this.get(`/v1/stats/tech`); + } + /** * Search for candidates by name or other attributes */ diff --git a/src/components/DataStatsPage.tsx b/src/components/DataStatsPage.tsx index 9e47ea5..091cb17 100644 --- a/src/components/DataStatsPage.tsx +++ b/src/components/DataStatsPage.tsx @@ -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(null); + const [dbStats, setDbStats] = useState(null); const [loading, setLoading] = useState(true); + const [dbLoading, setDbLoading] = useState(true); const [error, setError] = useState(null); + const [dbError, setDbError] = useState(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 (
{/* Header */} -
+

Disponibilidade de Dados

@@ -85,6 +101,23 @@ const DataStatsPage: React.FC = () => {
+ {/* Download DB Dump Section */} +
+
+ + Download do Dump do Banco de Dados +
+ + + Google Drive + +
+ {/* Stats Cards */}
@@ -133,7 +166,7 @@ const DataStatsPage: React.FC = () => { {sortedYears.map((year, index) => ( {year} @@ -178,6 +211,131 @@ const DataStatsPage: React.FC = () => {
+ + {/* Database Tech Stats Section */} +
+
+
+

Dados Técnicas do Banco de Dados

+

Informações sobre tabelas, views materializadas e índices

+
+ {dbLoading ? ( +
+
+ Carregando dados do banco de dados... +
+ ) : dbError ? ( +
{dbError}
+ ) : dbStats ? ( +
+ {/* Tables */} +
+

Tabelas

+
+ + + + + + + + + + {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 ( + + + + + + ); + })} + {/* 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 ( + + + + + + ); + })()} + +
NomeTamanho TotalEntradas
{name}{sizeDisplay}{table.entries.toLocaleString()}
Total{totalDisplay}{totalEntries.toLocaleString()}
+
+
+ {/* Materialized Views */} +
+

Views Materializadas

+
+ + + + + + + + + + {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 ( + + + + + + ); + })} + +
NomeTamanho TotalEntradas
{name}{sizeDisplay}{view.entries.toLocaleString()}
+
+
+ {/* Indexes */} +
+

Índices

+
+ + + + + + + + + + + {(() => { + const sizeMB = dbStats.indexes.size / 1024 / 1024; + const sizeDisplay = sizeMB > 1024 + ? `${(sizeMB / 1024).toFixed(2)} GB` + : `${sizeMB.toFixed(2)} MB`; + return ; + })()} + + +
QuantidadeTamanho Total
{dbStats.indexes.amount}{sizeDisplay}
+
+
+
+ ) : null} +
+
); }; diff --git a/src/components/StatisticsPage/StatisticsGraphs.tsx b/src/components/StatisticsPage/StatisticsGraphs.tsx index 71913a5..6a70a77 100644 --- a/src/components/StatisticsPage/StatisticsGraphs.tsx +++ b/src/components/StatisticsPage/StatisticsGraphs.tsx @@ -95,14 +95,14 @@ const StatisticsGraphs: React.FC = ({
Patrimônio Inicial ({enrichmentData.anoInicial}): - R$ {enrichmentData.patrimonioInicial?.toLocaleString('pt-BR') || '0'} + R$ {enrichmentData.patrimonioInicial?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
Patrimônio Final ({enrichmentData.anoFinal}): - R$ {enrichmentData.patrimonioFinal?.toLocaleString('pt-BR') || '0'} + R$ {enrichmentData.patrimonioFinal?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
@@ -112,7 +112,7 @@ const StatisticsGraphs: React.FC = ({ 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'}
@@ -202,7 +202,7 @@ const StatisticsGraphs: React.FC = ({ )} {item.ano || 'N/A'} - R$ {item.valor?.toLocaleString('pt-BR') || '0'} + R$ {item.valor?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'} ))} diff --git a/src/components/StatisticsPage/StatisticsPage.tsx b/src/components/StatisticsPage/StatisticsPage.tsx index add7971..3b34802 100644 --- a/src/components/StatisticsPage/StatisticsPage.tsx +++ b/src/components/StatisticsPage/StatisticsPage.tsx @@ -66,6 +66,9 @@ const StatisticsPage: React.FC = () => {

Análise de dados e estatísticas dos candidatos e partidos brasileiros

+

+ Para mais informações, acesse a página de estatísticas do TSE ou o DivulgaCand do TSE. +