Feat Widgets Cards y Optimización de Consultas

This commit is contained in:
2025-09-28 19:04:09 -03:00
parent 67634ae947
commit 3b0eee25e6
71 changed files with 5415 additions and 442 deletions

View File

@@ -1,6 +1,9 @@
// src/apiService.ts
import axios from 'axios';
import type { ApiResponseRankingMunicipio, ApiResponseRankingSeccion, ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker, ApiResponseResultadosPorSeccion, PanelElectoralDto } from './types/types';
import type { ApiResponseRankingMunicipio, ApiResponseRankingSeccion,
ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple,
TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker,
ApiResponseResultadosPorSeccion, PanelElectoralDto, ResumenProvincia } from './types/types';
/**
* URL base para las llamadas a la API.
@@ -84,6 +87,32 @@ export interface ResultadoDetalleSeccion {
color: string | null;
}
export interface PartidoComposicionNacional {
id: string;
nombre: string;
nombreCorto: string | null;
color: string | null;
bancasFijos: number;
bancasGanadas: number;
bancasTotales: number;
ordenDiputadosNacionales: number | null;
ordenSenadoresNacionales: number | null;
}
export interface CamaraComposicionNacional {
camaraNombre: string;
totalBancas: number;
bancasEnJuego: number;
partidos: PartidoComposicionNacional[];
presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null;
ultimaActualizacion: string;
}
export interface ComposicionNacionalData {
diputados: CamaraComposicionNacional;
senadores: CamaraComposicionNacional;
}
export const getResumenProvincial = async (eleccionId: number): Promise<CategoriaResumen[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/provincia/02`);
return response.data;
@@ -221,4 +250,16 @@ export const getPanelElectoral = async (eleccionId: number, ambitoId: string | n
const { data } = await apiClient.get(url);
return data;
};
export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => {
const { data } = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`);
return data;
};
// 11. Endpoint para el widget de tarjetas nacionales
export const getResumenPorProvincia = async (eleccionId: number): Promise<ResumenProvincia[]> => {
// Usamos el cliente público ya que son datos de resultados
const { data } = await apiClient.get(`/elecciones/${eleccionId}/resumen-por-provincia`);
return data;
};

View File

@@ -0,0 +1,339 @@
// src/components/common/DiputadosNacionalesLayout.tsx
import React from 'react';
import type { PartidoComposicionNacional } from '../../apiService';
// --- Interfaces Actualizadas ---
interface DiputadosNacionalesLayoutProps {
partyData: PartidoComposicionNacional[];
size?: number;
presidenteBancada?: { color: string | null } | null; // <-- Nueva Prop
}
const PRESIDENTE_SEAT_INDEX = 0; // El escaño 'seat-0' es el del presidente
export const DiputadosNacionalesLayout: React.FC<DiputadosNacionalesLayoutProps> = ({
partyData,
size = 800,
presidenteBancada, // <-- Recibimos la nueva prop
}) => {
// --- ARRAY DE 257 ELEMENTOS <circle> ORDENADOS POR ID DE "seat-X" ---
const seatElements = [
<circle key="seat-0" id="seat-0" r="15.7" cy="639.5" cx="595.3" />,
<circle key="seat-1" id="seat-1" r="15.7" cy="673.1" cx="109.3" />,
<circle key="seat-2" id="seat-2" r="15.7" cy="673.1" cx="161.7" />,
<circle key="seat-3" id="seat-3" r="15.7" cy="673.5" cx="214.3" />,
<circle key="seat-4" id="seat-4" r="15.7" cy="673.2" cx="266.5" />,
<circle key="seat-5" id="seat-5" r="15.7" cy="669.5" cx="319.4" />,
<circle key="seat-6" id="seat-6" r="15.7" cy="660" cx="370.8" />,
<circle key="seat-7" id="seat-7" transform="rotate(-88.1)" r="15.7" cy="77.69" cx="-634.1" />,
<circle key="seat-8" id="seat-8" r="15.7" cy="639" cx="109.3" />,
<circle key="seat-9" id="seat-9" r="15.7" cy="639" cx="161.7" />,
<circle key="seat-10" id="seat-10" r="15.7" cy="639.2" cx="214.3" />,
<circle key="seat-11" id="seat-11" r="15.7" cy="638.8" cx="266.7" />,
<circle key="seat-12" id="seat-12" r="15.7" cy="635.1" cx="319.4" />,
<circle key="seat-13" id="seat-13" r="15.7" cy="625.7" cx="371.7" />,
<circle key="seat-14" id="seat-14" r="15.7" cy="639" cx="424.2" />,
<circle key="seat-15" id="seat-15" transform="rotate(-88.1)" r="15.7" cy="77" cx="-600.18" />,
<circle key="seat-16" id="seat-16" r="15.7" cy="600.9" cx="109.5" />,
<circle key="seat-17" id="seat-17" r="15.7" cy="603.7" cx="162.1" />,
<circle key="seat-18" id="seat-18" r="15.7" cy="598.6" cx="215" />,
<circle key="seat-19" id="seat-19" r="15.7" cy="602.6" cx="267.1" />,
<circle key="seat-20" id="seat-20" transform="rotate(-88.1)" r="15.7" cy="76.57" cx="-562.57" />,
<circle key="seat-21" id="seat-21" r="15.7" cy="566.7" cx="112.2" />,
<circle key="seat-22" id="seat-22" r="15.7" cy="570" cx="164.7" />,
<circle key="seat-23" id="seat-23" r="15.7" cy="564.5" cx="218.2" />,
<circle key="seat-24" id="seat-24" r="15.7" cy="568.6" cx="270.9" />,
<circle key="seat-25" id="seat-25" r="15.7" cy="588" cx="321.1" />,
<circle key="seat-26" id="seat-26" transform="rotate(-88.1)" r="15.7" cy="79.88" cx="-524.51" />,
<circle key="seat-27" id="seat-27" transform="rotate(-5.7)" r="15.7" cy="539.19" cx="65.05" />,
<circle key="seat-28" id="seat-28" r="15.7" cy="535.9" cx="170" />,
<circle key="seat-29" id="seat-29" transform="rotate(-88.1)" r="15.7" cy="86.87" cx="-488.2" />,
<circle key="seat-30" id="seat-30" r="15.7" cy="497.2" cx="125.2" />,
<circle key="seat-31" id="seat-31" r="15.7" cy="502.8" cx="178.2" />,
<circle key="seat-32" id="seat-32" r="15.7" cy="525.1" cx="226.3" />,
<circle key="seat-33" id="seat-33" r="15.7" cy="533.1" cx="278.4" />,
<circle key="seat-34" id="seat-34" r="15.7" cy="554.6" cx="327.1" />,
<circle key="seat-35" id="seat-35" r="15.7" cy="567.9" cx="377.9" />,
<circle key="seat-36" id="seat-36" r="15.7" cy="596.7" cx="426" />,
<circle key="seat-37" id="seat-37" r="15.7" cy="453.8" cx="79.7" />,
<circle key="seat-38" id="seat-38" r="15.7" cy="462" cx="135.7" />,
<circle key="seat-39" id="seat-39" r="15.7" cy="469.3" cx="188.9" />,
<circle key="seat-40" id="seat-40" r="15.7" cy="492.6" cx="236.4" />,
<circle key="seat-41" id="seat-41" r="15.7" cy="500.6" cx="289.8" />,
<circle key="seat-42" id="seat-42" r="15.7" cy="511.6" cx="341.5" />,
<circle key="seat-43" id="seat-43" r="15.7" cy="535" cx="388.9" />,
<circle key="seat-44" id="seat-44" r="15.7" cy="555" cx="437.3" />,
<circle key="seat-45" id="seat-45" r="15.7" cy="419.3" cx="92.8" />,
<circle key="seat-46" id="seat-46" r="15.7" cy="429.8" cx="148.1" />,
<circle key="seat-47" id="seat-47" r="15.7" cy="387.4" cx="106.8" />,
<circle key="seat-48" id="seat-48" transform="rotate(-5.7)" r="15.7" cy="364.72" cx="89.86" />,
<circle key="seat-49" id="seat-49" r="15.7" cy="395.5" cx="164.4" />,
<circle key="seat-50" id="seat-50" r="15.7" cy="437.3" cx="202.4" />,
<circle key="seat-51" id="seat-51" r="15.7" cy="455.4" cx="252.1" />,
<circle key="seat-52" id="seat-52" r="15.7" cy="325.1" cx="144.9" />,
<circle key="seat-53" id="seat-53" r="15.7" cy="365.7" cx="181.3" />,
<circle key="seat-54" id="seat-54" r="15.7" cy="405.1" cx="218.8" />,
<circle key="seat-55" id="seat-55" r="15.7" cy="425.6" cx="267.7" />,
<circle key="seat-56" id="seat-56" r="15.7" cy="464.9" cx="306.5" />,
<circle key="seat-57" id="seat-57" r="15.7" cy="292.1" cx="168.7" />,
<circle key="seat-58" id="seat-58" r="15.7" cy="334.6" cx="202.3" />,
<circle key="seat-59" id="seat-59" r="15.7" cy="376.9" cx="236.7" />,
<circle key="seat-60" id="seat-60" r="15.7" cy="265.1" cx="190.8" />,
<circle key="seat-61" id="seat-61" r="15.7" cy="307.2" cx="224" />,
<circle key="seat-62" id="seat-62" r="15.7" cy="346.9" cx="259.3" />,
<circle key="seat-63" id="seat-63" r="15.7" cy="393" cx="289.6" />,
<circle key="seat-64" id="seat-64" r="15.7" cy="435.9" cx="323.7" />,
<circle key="seat-65" id="seat-65" r="15.7" cy="480.8" cx="357.3" />,
<circle key="seat-66" id="seat-66" r="15.7" cy="236.2" cx="218.1" />,
<circle key="seat-67" id="seat-67" r="15.7" cy="278.6" cx="250" />,
<circle key="seat-68" id="seat-68" r="15.7" cy="320.2" cx="283" />,
<circle key="seat-69" id="seat-69" r="15.7" cy="362" cx="315.5" />,
<circle key="seat-70" id="seat-70" r="15.7" cy="403.8" cx="348.7" />,
<circle key="seat-71" id="seat-71" r="15.7" cy="445.9" cx="381.6" />,
<circle key="seat-72" id="seat-72" r="15.7" cy="489" cx="415.1" />,
<circle key="seat-73" id="seat-73" r="15.7" cy="515.6" cx="460.7" />,
<circle key="seat-74" id="seat-74" r="15.7" cy="485.2" cx="491" />,
<circle key="seat-75" id="seat-75" r="15.7" cy="213.6" cx="243.2" />,
<circle key="seat-76" id="seat-76" r="15.7" cy="254.9" cx="275.3" />,
<circle key="seat-77" id="seat-77" r="15.7" cy="296.4" cx="307.8" />,
<circle key="seat-78" id="seat-78" r="15.7" cy="337.6" cx="339.9" />,
<circle key="seat-79" id="seat-79" r="15.7" cy="379" cx="372.5" />,
<circle key="seat-80" id="seat-80" r="15.7" cy="420.8" cx="405.1" />,
<circle key="seat-81" id="seat-81" r="15.7" cy="462.7" cx="437.2" />,
<circle key="seat-82" id="seat-82" r="15.5" cy="181.8" cx="283.1" />,
<circle key="seat-83" id="seat-83" r="15.5" cy="223.6" cx="315.4" />,
<circle key="seat-84" id="seat-84" r="15.7" cy="262.6" cx="351" />,
<circle key="seat-85" id="seat-85" r="15.5" cy="304.5" cx="382.7" />,
<circle key="seat-86" id="seat-86" r="15.7" cy="339.1" cx="425.3" />,
<circle key="seat-87" id="seat-87" r="15.7" cy="379" cx="461" />,
<circle key="seat-88" id="seat-88" r="15.7" cy="420.4" cx="495.9" />,
<circle key="seat-89" id="seat-89" r="15.7" cy="463.5" cx="528.1" />,
<circle key="seat-90" id="seat-90" r="15.5" cy="160.4" cx="315.7" />,
<circle key="seat-91" id="seat-91" r="15.5" cy="206.2" cx="342.9" />,
<circle key="seat-92" id="seat-92" r="15.7" cy="245.1" cx="379" />,
<circle key="seat-93" id="seat-93" r="15.5" cy="287.4" cx="410.5" />,
<circle key="seat-94" id="seat-94" r="15.7" cy="323.4" cx="455.9" />,
<circle key="seat-95" id="seat-95" transform="rotate(-80.8)" r="15.7" cy="555.93" cx="-274.27" />,
<circle key="seat-96" id="seat-96" r="15.7" cy="407.6" cx="527.7" />,
<circle key="seat-97" id="seat-97" r="15.5" cy="142.7" cx="345.9" />,
<circle key="seat-98" id="seat-98" r="15.5" cy="186.8" cx="375.8" />,
<circle key="seat-99" id="seat-99" r="15.5" cy="125.9" cx="377.8" />,
<circle key="seat-100" id="seat-100" r="15.5" cy="173.7" cx="405.1" />,
<circle key="seat-101" id="seat-101" r="15.7" cy="223" cx="422.9" />,
<circle key="seat-102" id="seat-102" r="15.5" cy="270.9" cx="444.3" />,
<circle key="seat-103" id="seat-103" r="15.5" cy="112" cx="409.4" />,
<circle key="seat-104" id="seat-104" r="15.5" cy="157.7" cx="438.1" />,
<circle key="seat-105" id="seat-105" r="15.7" cy="209" cx="453.9" />,
<circle key="seat-106" id="seat-106" r="15.5" cy="259.6" cx="474.2" />,
<circle key="seat-107" id="seat-107" r="15.7" cy="306.3" cx="499.3" />,
<circle key="seat-108" id="seat-108" r="15.5" cy="100.1" cx="443.4" />,
<circle key="seat-109" id="seat-109" r="15.5" cy="146.7" cx="472.7" />,
<circle key="seat-110" id="seat-110" r="15.7" cy="197.9" cx="497" />,
<circle key="seat-111" id="seat-111" r="15.5" cy="249" cx="508.8" />,
<circle key="seat-112" id="seat-112" r="15.7" cy="298.4" cx="532.7" />,
<circle key="seat-113" id="seat-113" r="15.7" cy="350.8" cx="538.1" />,
<circle key="seat-114" id="seat-114" r="15.5" cy="92.2" cx="477" />,
<circle key="seat-115" id="seat-115" r="15.5" cy="84.4" cx="510" />,
<circle key="seat-116" id="seat-116" transform="rotate(-80.8)" r="15.5" cy="523.04" cx="-55.62" />,
<circle key="seat-117" id="seat-117" r="15.7" cy="190.1" cx="531.6" />,
<circle key="seat-118" id="seat-118" r="15.5" cy="243.4" cx="542.3" />,
<circle key="seat-119" id="seat-119" r="15.5" cy="80.7" cx="544.3" />,
<circle key="seat-120" id="seat-120" r="15.5" cy="136.1" cx="541.9" />,
<circle key="seat-121" id="seat-121" r="15.5" cy="78.5" cx="579" />,
<circle key="seat-122" id="seat-122" r="15.5" cy="135" cx="578.2" />,
<circle key="seat-123" id="seat-123" r="15.7" cy="187.6" cx="577.9" />,
<circle key="seat-124" id="seat-124" r="15.5" cy="240" cx="579" />,
<circle key="seat-125" id="seat-125" r="15.7" cy="292.6" cx="578" />,
<circle key="seat-126" id="seat-126" r="15.7" cy="345.3" cx="578" />,
<circle key="seat-127" id="seat-127" r="15.7" cy="398" cx="577.8" />,
<circle key="seat-128" id="seat-128" r="15.7" cy="451.2" cx="572.2" />,
<circle key="seat-129" id="seat-129" r="15.5" cy="78.5" cx="613.5" />,
<circle key="seat-130" id="seat-130" r="15.5" cy="135" cx="612.3" />,
<circle key="seat-131" id="seat-131" r="15.7" cy="187.6" cx="612.6" />,
<circle key="seat-132" id="seat-132" r="15.5" cy="240" cx="611.5" />,
<circle key="seat-133" id="seat-133" r="15.7" cy="292.6" cx="612.5" />,
<circle key="seat-134" id="seat-134" r="15.7" cy="345.3" cx="612.5" />,
<circle key="seat-135" id="seat-135" r="15.7" cy="398" cx="612.7" />,
<circle key="seat-136" id="seat-136" r="15.7" cy="451.2" cx="618.3" />,
<circle key="seat-137" id="seat-137" r="15.5" cy="82.6" cx="646.3" />,
<circle key="seat-138" id="seat-138" r="15.5" cy="86.4" cx="680.5" />,
<circle key="seat-139" id="seat-139" r="15.5" cy="138.4" cx="650.6" />,
<circle key="seat-140" id="seat-140" r="15.5" cy="94.2" cx="715.6" />,
<circle key="seat-141" id="seat-141" r="15.5" cy="142.6" cx="685.4" />,
<circle key="seat-142" id="seat-142" r="15.7" cy="190.1" cx="657" />,
<circle key="seat-143" id="seat-143" r="15.5" cy="243.4" cx="648.3" />,
<circle key="seat-144" id="seat-144" r="15.5" cy="104.1" cx="747.1" />,
<circle key="seat-145" id="seat-145" r="15.5" cy="150.7" cx="719.9" />,
<circle key="seat-146" id="seat-146" r="15.7" cy="197.9" cx="691.5" />,
<circle key="seat-147" id="seat-147" r="15.5" cy="248.5" cx="679.8" />,
<circle key="seat-148" id="seat-148" r="15.7" cy="298.4" cx="657.8" />,
<circle key="seat-149" id="seat-149" r="15.7" cy="350.8" cx="652.4" />,
<circle key="seat-150" id="seat-150" r="15.5" cy="116" cx="783.1" />,
<circle key="seat-151" id="seat-151" r="15.5" cy="159.7" cx="750.4" />,
<circle key="seat-152" id="seat-152" r="15.7" cy="211" cx="736.6" />,
<circle key="seat-153" id="seat-153" r="15.5" cy="259.6" cx="716.4" />,
<circle key="seat-154" id="seat-154" r="15.7" cy="306.3" cx="691.2" />,
<circle key="seat-155" id="seat-155" r="15.5" cy="127.9" cx="812.8" />,
<circle key="seat-156" id="seat-156" r="15.5" cy="173.7" cx="785.5" />,
<circle key="seat-157" id="seat-157" r="15.7" cy="223" cx="767.7" />,
<circle key="seat-158" id="seat-158" r="15.5" cy="270.9" cx="746.3" />,
<circle key="seat-159" id="seat-159" r="15.5" cy="144.7" cx="846.6" />,
<circle key="seat-160" id="seat-160" r="15.5" cy="186.8" cx="814.8" />,
<circle key="seat-161" id="seat-161" r="15.5" cy="160.4" cx="874.8" />,
<circle key="seat-162" id="seat-162" r="15.5" cy="206.2" cx="847.6" />,
<circle key="seat-163" id="seat-163" r="15.7" cy="245.1" cx="811.5" />,
<circle key="seat-164" id="seat-164" r="15.5" cy="287.4" cx="780.1" />,
<circle key="seat-165" id="seat-165" r="15.7" cy="323.4" cx="734.6" />,
<circle key="seat-166" id="seat-166" r="15.7" cy="357.8" cx="687.4" />,
<circle key="seat-167" id="seat-167" r="15.7" cy="407.6" cx="662.8" />,
<circle key="seat-168" id="seat-168" r="15.5" cy="181.8" cx="907.5" />,
<circle key="seat-169" id="seat-169" r="15.5" cy="223.6" cx="875.2" />,
<circle key="seat-170" id="seat-170" r="15.7" cy="262.6" cx="839.5" />,
<circle key="seat-171" id="seat-171" r="15.5" cy="304.3" cx="807.8" />,
<circle key="seat-172" id="seat-172" r="15.7" cy="339.1" cx="765.3" />,
<circle key="seat-173" id="seat-173" r="15.7" cy="379" cx="729.6" />,
<circle key="seat-174" id="seat-174" r="15.7" cy="420.4" cx="694.6" />,
<circle key="seat-175" id="seat-175" r="15.7" cy="463.5" cx="662.5" />,
<circle key="seat-176" id="seat-176" r="15.7" cy="485.4" cx="699.5" />,
<circle key="seat-177" id="seat-177" r="15.7" cy="213.6" cx="947.4" />,
<circle key="seat-178" id="seat-178" r="15.7" cy="254.9" cx="915.2" />,
<circle key="seat-179" id="seat-179" r="15.7" cy="296.4" cx="882.7" />,
<circle key="seat-180" id="seat-180" r="15.7" cy="337.6" cx="850.7" />,
<circle key="seat-181" id="seat-181" r="15.7" cy="379" cx="818.1" />,
<circle key="seat-182" id="seat-182" r="15.7" cy="420.8" cx="785.4" />,
<circle key="seat-183" id="seat-183" r="15.7" cy="462.7" cx="753.4" />,
<circle key="seat-184" id="seat-184" r="15.7" cy="515.4" cx="730.1" />,
<circle key="seat-185" id="seat-185" r="15.7" cy="236.2" cx="972.4" />,
<circle key="seat-186" id="seat-186" r="15.7" cy="278.6" cx="940.5" />,
<circle key="seat-187" id="seat-187" r="15.7" cy="320.2" cx="907.5" />,
<circle key="seat-188" id="seat-188" r="15.7" cy="362" cx="875.1" />,
<circle key="seat-189" id="seat-189" r="15.7" cy="403.8" cx="841.8" />,
<circle key="seat-190" id="seat-190" r="15.7" cy="445.9" cx="808.9" />,
<circle key="seat-191" id="seat-191" r="15.7" cy="489" cx="775.5" />,
<circle key="seat-192" id="seat-192" r="15.7" cy="265.1" cx="999.7" />,
<circle key="seat-193" id="seat-193" r="15.7" cy="307.2" cx="966.6" />,
<circle key="seat-194" id="seat-194" r="15.7" cy="346.9" cx="931.2" />,
<circle key="seat-195" id="seat-195" r="15.7" cy="393" cx="901" />,
<circle key="seat-196" id="seat-196" r="15.7" cy="435.9" cx="866.9" />,
<circle key="seat-197" id="seat-197" r="15.7" cy="480.8" cx="833.2" />,
<circle key="seat-198" id="seat-198" transform="rotate(-80.8)" r="15.7" cy="1055.16" cx="-124.85" />,
<circle key="seat-199" id="seat-199" r="15.7" cy="334.6" cx="988.2" />,
<circle key="seat-200" id="seat-200" r="15.7" cy="376.9" cx="953.8" />,
<circle key="seat-201" id="seat-201" r="15.7" cy="425.6" cx="922.8" />,
<circle key="seat-202" id="seat-202" r="15.7" cy="464.9" cx="884" />,
<circle key="seat-203" id="seat-203" r="15.7" cy="325.1" cx="1045.7" />,
<circle key="seat-204" id="seat-204" r="15.7" cy="365.7" cx="1009.2" />,
<circle key="seat-205" id="seat-205" r="15.7" cy="405.1" cx="971.7" />,
<circle key="seat-206" id="seat-206" r="15.7" cy="354.1" cx="1063.2" />,
<circle key="seat-207" id="seat-207" transform="rotate(-80.8)" r="15.7" cy="1075.78" cx="-226.25" />,
<circle key="seat-208" id="seat-208" r="15.7" cy="387.4" cx="1081.8" />,
<circle key="seat-209" id="seat-209" r="15.7" cy="421.3" cx="1095.7" />,
<circle key="seat-210" id="seat-210" r="15.7" cy="429.8" cx="1042.5" />,
<circle key="seat-211" id="seat-211" r="15.7" cy="437.3" cx="988.2" />,
<circle key="seat-212" id="seat-212" r="15.7" cy="455.4" cx="938.5" />,
<circle key="seat-213" id="seat-213" r="15.7" cy="455.8" cx="1108.8" />,
<circle key="seat-214" id="seat-214" r="15.7" cy="462" cx="1054.9" />,
<circle key="seat-215" id="seat-215" r="15.7" cy="469.3" cx="1001.6" />,
<circle key="seat-216" id="seat-216" r="15.7" cy="492.6" cx="954.1" />,
<circle key="seat-217" id="seat-217" r="15.7" cy="500.6" cx="900.8" />,
<circle key="seat-218" id="seat-218" r="15.7" cy="511.6" cx="849" />,
<circle key="seat-219" id="seat-219" r="15.7" cy="535" cx="801.6" />,
<circle key="seat-220" id="seat-220" r="15.7" cy="554.8" cx="753.3" />,
<circle key="seat-221" id="seat-221" r="15.7" cy="490.9" cx="1118" />,
<circle key="seat-222" id="seat-222" r="15.7" cy="497.2" cx="1065.3" />,
<circle key="seat-223" id="seat-223" r="15.7" cy="502.8" cx="1012.3" />,
<circle key="seat-224" id="seat-224" r="15.7" cy="525.1" cx="964.2" />,
<circle key="seat-225" id="seat-225" r="15.7" cy="533.1" cx="912.2" />,
<circle key="seat-226" id="seat-226" r="15.7" cy="554.6" cx="863.4" />,
<circle key="seat-227" id="seat-227" r="15.7" cy="567.9" cx="812.7" />,
<circle key="seat-228" id="seat-228" r="15.7" cy="596.7" cx="764.8" />,
<circle key="seat-229" id="seat-229" r="15.7" cy="528.9" cx="1126.1" />,
<circle key="seat-230" id="seat-230" r="15.7" cy="530.2" cx="1072.7" />,
<circle key="seat-231" id="seat-231" transform="rotate(-80.8)" r="15.7" cy="1092.81" cx="-365.69" />,
<circle key="seat-232" id="seat-232" r="15.7" cy="562.9" cx="1130.6" />,
<circle key="seat-233" id="seat-233" r="15.7" cy="566.7" cx="1078.3" />,
<circle key="seat-234" id="seat-234" transform="rotate(-80.8)" r="15.7" cy="1103.39" cx="-398.54" />,
<circle key="seat-235" id="seat-235" r="15.7" cy="564.5" cx="972.4" />,
<circle key="seat-236" id="seat-236" r="15.7" cy="568.6" cx="919.7" />,
<circle key="seat-237" id="seat-237" r="15.7" cy="588" cx="869.4" />,
<circle key="seat-238" id="seat-238" r="15.7" cy="602.5" cx="1133.5" />,
<circle key="seat-239" id="seat-239" r="15.7" cy="600.9" cx="1081" />,
<circle key="seat-240" id="seat-240" transform="rotate(-80.8)" r="15.7" cy="1111.41" cx="-431.3" />,
<circle key="seat-241" id="seat-241" r="15.7" cy="598.6" cx="975.6" />,
<circle key="seat-242" id="seat-242" r="15.7" cy="602.6" cx="923.4" />,
<circle key="seat-243" id="seat-243" r="15.7" cy="636.4" cx="1133.9" />,
<circle key="seat-244" id="seat-244" r="15.7" cy="639" cx="1081.3" />,
<circle key="seat-245" id="seat-245" transform="rotate(-80.8)" r="15.7" cy="1117.48" cx="-466.13" />,
<circle key="seat-246" id="seat-246" r="15.7" cy="639.2" cx="976.3" />,
<circle key="seat-247" id="seat-247" r="15.7" cy="638.8" cx="923.9" />,
<circle key="seat-248" id="seat-248" r="15.7" cy="635.1" cx="871.2" />,
<circle key="seat-249" id="seat-249" r="15.7" cy="625.7" cx="818.8" />,
<circle key="seat-250" id="seat-250" r="15.7" cy="639" cx="766.3" />,
<circle key="seat-251" id="seat-251" r="15.7" cy="673.1" cx="1081.3" />,
<circle key="seat-252" id="seat-252" transform="rotate(-80.8)" r="15.7" cy="1122.99" cx="-499.74" />,
<circle key="seat-253" id="seat-253" r="15.7" cy="673.5" cx="976.3" />,
<circle key="seat-254" id="seat-254" r="15.7" cy="673.2" cx="924" />,
<circle key="seat-255" id="seat-255" r="15.7" cy="669.5" cx="871.2" />,
<circle key="seat-256" id="seat-256" r="15.7" cy="660" cx="819.7" />,
];
let seatIndex = 1; // Empezamos a contar desde 1, ya que el 0 es presidencial
return (
<svg viewBox="0 0 1190.6 772.2" width={size} height={size * (772.2 / 1190.6)} style={{ display: 'block', margin: 'auto' }}>
<g>
{/* Renderizamos el escaño presidencial primero y por separado */}
{presidenteBancada && React.cloneElement(seatElements[PRESIDENTE_SEAT_INDEX], {
fill: presidenteBancada.color || '#A9A9A9',
strokeWidth: 0.5,
})}
{partyData.map(partido => {
// Por cada partido, creamos un array combinado de sus escaños
const partySeats = [
...Array(partido.bancasFijos).fill({ isNew: false }),
...Array(partido.bancasGanadas).fill({ isNew: true })
];
return (
// Envolvemos todos los escaños de un partido en un <g>
<g
key={partido.id}
className="party-block"
data-tooltip-id="party-tooltip"
data-tooltip-content={`${partido.nombreCorto || partido.nombre}: ${partido.bancasTotales} bancas`}
>
{partySeats.map((seatInfo, i) => {
// Si ya no hay más plantillas de escaños, no renderizamos nada
if (seatIndex >= seatElements.length) return null;
const template = seatElements[seatIndex];
seatIndex++; // Incrementamos el contador para el siguiente escaño
// Clonamos la plantilla con el estilo apropiado
return React.cloneElement(template, {
key: `${partido.id}-${i}`,
className: 'seat-circle',
fill: partido.color || '#808080',
fillOpacity: seatInfo.isNew ? 1 : 0.3, // Opacidad para bancas previas
stroke: partido.color || '#808080',
strokeWidth: 0.5,
});
})}
</g>
);
})}
{/* Renderizamos los escaños vacíos sobrantes */}
{seatIndex < seatElements.length &&
seatElements.slice(seatIndex).map((template, i) =>
React.cloneElement(template, {
key: `empty-${i}`,
fill: '#E0E0E0',
stroke: '#ffffff',
strokeWidth: 0.5
})
)
}
</g>
</svg>
);
};

View File

@@ -0,0 +1,154 @@
// src/components/common/SenadoresNacionalesLayout.tsx
import React from 'react';
import type { PartidoComposicionNacional } from '../../apiService';
// Interfaces
interface SenadoresNacionalesLayoutProps {
partyData: PartidoComposicionNacional[];
size?: number;
presidenteBancada?: { color: string | null } | null;
}
const PRESIDENTE_SEAT_INDEX = 0;
export const SenadoresNacionalesLayout: React.FC<SenadoresNacionalesLayoutProps> = ({
partyData,
size = 800,
presidenteBancada,
}) => {
// --- ARRAY DE 73 ELEMENTOS <circle> ORDENADOS POR ID DE "seat-X" ---
// El asiento 0 es el presidencial, los 72 restantes son los senadores.
const seatElements = [
<circle key="seat-0" id="seat-0" r="7.1" cy="187" cx="168.6" />,
<circle key="seat-1" id="seat-1" r="7.1" cy="166" cx="21.8" />,
<circle key="seat-2" id="seat-2" r="7.1" cy="172" cx="51.5" />,
<circle key="seat-3" id="seat-3" r="7.1" cy="174.5" cx="82.7" />,
<circle key="seat-4" id="seat-4" r="7.1" cy="147.4" cx="21.5" />,
<circle key="seat-5" id="seat-5" r="7.1" cy="155.2" cx="51.8" />,
<circle key="seat-6" id="seat-6" r="7.1" cy="156.3" cx="83.4" />,
<circle key="seat-7" id="seat-7" r="7.1" cy="169.9" cx="120.9" />,
<circle key="seat-8" id="seat-8" r="7.1" cy="128.4" cx="22.8" />,
<circle key="seat-9" id="seat-9" r="7.1" cy="137.9" cx="53.2" />,
<circle key="seat-10" id="seat-10" r="7.1" cy="138.8" cx="85.5" />,
<circle key="seat-11" id="seat-11" r="7.1" cy="151.9" cx="120.9" />,
<circle key="seat-12" id="seat-12" r="7.1" cy="109" cx="25.6" />,
<circle key="seat-13" id="seat-13" r="7.1" cy="121.3" cx="57.2" />,
<circle key="seat-14" id="seat-14" r="7.1" cy="91.5" cx="34.2" />,
<circle key="seat-15" id="seat-15" r="7.1" cy="105.7" cx="64.8" />,
<circle key="seat-16" id="seat-16" r="7.1" cy="122.5" cx="92.9" />,
<circle key="seat-17" id="seat-17" r="7.1" cy="136.2" cx="128.2" />,
<circle key="seat-18" id="seat-18" r="7.1" cy="75.5" cx="45.3" />,
<circle key="seat-19" id="seat-19" r="7.1" cy="91.3" cx="75.7" />,
<circle key="seat-20" id="seat-20" r="7.1" cy="106.5" cx="106.3" />,
<circle key="seat-21" id="seat-21" r="7.1" cy="59.8" cx="57.9" />,
<circle key="seat-22" id="seat-22" r="7.1" cy="78.6" cx="89.5" />,
<circle key="seat-23" id="seat-23" r="7.1" cy="45.3" cx="73.2" />,
<circle key="seat-24" id="seat-24" r="7.1" cy="67.2" cx="104.6" />,
<circle key="seat-25" id="seat-25" r="7.1" cy="94.3" cx="121.6" />,
<circle key="seat-26" id="seat-26" r="7.1" cy="124.3" cx="141.1" />,
<circle key="seat-27" id="seat-27" r="7.1" cy="32.7" cx="90.8" />,
<circle key="seat-28" id="seat-28" r="7.1" cy="58.3" cx="120.9" />,
<circle key="seat-29" id="seat-29" r="7.1" cy="84.9" cx="139.1" />,
<circle key="seat-30" id="seat-30" r="7.1" cy="116.4" cx="157.2" />,
<circle key="seat-31" id="seat-31" r="7.1" cy="24.6" cx="109.5" />,
<circle key="seat-32" id="seat-32" r="7.1" cy="52.2" cx="138.6" />,
<circle key="seat-33" id="seat-33" r="7.1" cy="79.5" cx="157.8" />,
<circle key="seat-34" id="seat-34" r="7.1" cy="17.9" cx="128.8" />,
<circle key="seat-35" id="seat-35" r="7.1" cy="15.2" cx="147.7" />,
<circle key="seat-36" id="seat-36" r="7.1" cy="48.3" cx="156.9" />,
<circle key="seat-37" id="seat-37" r="7.1" cy="15.2" cx="192.5" />,
<circle key="seat-38" id="seat-38" r="7.1" cy="48.3" cx="183.3" />,
<circle key="seat-39" id="seat-39" r="7.1" cy="79.5" cx="182.4" />,
<circle key="seat-40" id="seat-40" r="7.1" cy="115.8" cx="182.2" />,
<circle key="seat-41" id="seat-41" r="7.1" cy="17.9" cx="211.4" />,
<circle key="seat-42" id="seat-42" r="7.1" cy="52.2" cx="201.6" />,
<circle key="seat-43" id="seat-43" r="7.1" cy="24.6" cx="230.7" />,
<circle key="seat-44" id="seat-44" r="7.1" cy="58.3" cx="219.3" />,
<circle key="seat-45" id="seat-45" r="7.1" cy="84.9" cx="201.1" />,
<circle key="seat-46" id="seat-46" r="7.1" cy="32.7" cx="249.4" />,
<circle key="seat-47" id="seat-47" r="7.1" cy="67.2" cx="235.6" />,
<circle key="seat-48" id="seat-48" r="7.1" cy="94.3" cx="218.6" />,
<circle key="seat-49" id="seat-49" r="7.1" cy="124.3" cx="199.1" />,
<circle key="seat-50" id="seat-50" r="7.1" cy="45.3" cx="267" />,
<circle key="seat-51" id="seat-51" r="7.1" cy="59.8" cx="282.3" />,
<circle key="seat-52" id="seat-52" r="7.1" cy="78.6" cx="250.7" />,
<circle key="seat-53" id="seat-53" r="7.1" cy="106.5" cx="234" />,
<circle key="seat-54" id="seat-54" r="7.1" cy="136.2" cx="212" />,
<circle key="seat-55" id="seat-55" r="7.1" cy="75.5" cx="294.9" />,
<circle key="seat-56" id="seat-56" r="7.1" cy="91.3" cx="264.5" />,
<circle key="seat-57" id="seat-57" r="7.1" cy="91.5" cx="306" />,
<circle key="seat-58" id="seat-58" r="7.1" cy="105.7" cx="275.4" />,
<circle key="seat-59" id="seat-59" r="7.1" cy="122.5" cx="247.3" />,
<circle key="seat-60" id="seat-60" r="7.1" cy="109" cx="313.5" />,
<circle key="seat-61" id="seat-61" r="7.1" cy="121.3" cx="283" />,
<circle key="seat-62" id="seat-62" r="7.1" cy="138.8" cx="254.7" />,
<circle key="seat-63" id="seat-63" r="7.1" cy="151.9" cx="219.3" />,
<circle key="seat-64" id="seat-64" r="7.1" cy="128.4" cx="317.4" />,
<circle key="seat-65" id="seat-65" r="7.1" cy="137.9" cx="287" />,
<circle key="seat-66" id="seat-66" r="7.1" cy="156.3" cx="256.8" />,
<circle key="seat-67" id="seat-67" r="7.1" cy="169.9" cx="219.3" />,
<circle key="seat-68" id="seat-68" r="7.1" cy="147.4" cx="318.7" />,
<circle key="seat-69" id="seat-69" r="7.1" cy="155.2" cx="288.4" />,
<circle key="seat-70" id="seat-70" r="7.1" cy="166" cx="318.4" />,
<circle key="seat-71" id="seat-71" r="7.1" cy="172" cx="288.7" />,
<circle key="seat-72" id="seat-72" r="7.1" cy="174.5" cx="257.5" />,
];
let seatIndex = 1; // Empezamos desde 1 porque el 0 es para el presidente
return (
<svg viewBox="0 0 340.2 220.5" width={size} height={size * (220.5 / 340.2)} style={{ display: 'block', margin: 'auto' }}>
<g>
{/* Renderizamos primero el escaño del presidente por separado */}
{presidenteBancada && React.cloneElement(seatElements[PRESIDENTE_SEAT_INDEX], {
fill: presidenteBancada.color || '#A9A9A9',
strokeWidth: 0.5,
})}
{/* Mapeamos los partidos para crear los bloques */}
{partyData.map(partido => {
const partySeats = [
...Array(partido.bancasFijos).fill({ isNew: false }),
...Array(partido.bancasGanadas).fill({ isNew: true })
];
return (
<g
key={partido.id}
className="party-block"
data-tooltip-id="party-tooltip"
data-tooltip-content={`${partido.nombreCorto || partido.nombre}: ${partido.bancasTotales} bancas`}
>
{partySeats.map((seatInfo, i) => {
if (seatIndex >= seatElements.length) return null;
const template = seatElements[seatIndex];
seatIndex++;
return React.cloneElement(template, {
key: `${partido.id}-${i}`,
className: 'seat-circle',
fill: partido.color || '#808080',
fillOpacity: seatInfo.isNew ? 1 : 0.3,
stroke: partido.color || '#808080',
strokeWidth: 0.5,
});
})}
</g>
);
})}
{/* Renderizamos escaños vacíos si sobran */}
{seatIndex < seatElements.length &&
seatElements.slice(seatIndex).map((template, i) =>
React.cloneElement(template, {
key: `empty-${i}`,
fill: '#E0E0E0',
stroke: '#ffffff',
strokeWidth: 0.5
})
)
}
</g>
</svg>
);
};

View File

@@ -1,4 +1,6 @@
// src/features/legislativas/rovinciales/DevAppLegislativas.tsx
import { ResultadosNacionalesCardsWidget } from './nacionales/ResultadosNacionalesCardsWidget';
import { CongresoNacionalWidget } from './nacionales/CongresoNacionalWidget';
import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget';
import './DevAppStyle.css'
@@ -6,9 +8,8 @@ export const DevAppLegislativas = () => {
return (
<div className="container">
<h1>Visor de Widgets</h1>
{/* Le pasamos el ID de la elección que queremos visualizar.
Para tus datos de prueba provinciales, este ID es 1. */}
<ResultadosNacionalesCardsWidget eleccionId={2} />
<CongresoNacionalWidget eleccionId={2} />
<PanelNacionalWidget eleccionId={2} />
</div>
);

View File

@@ -0,0 +1,162 @@
// src/features/legislativas/nacionales/CongresoNacionalWidget.tsx
import { useState, Suspense, useMemo } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';
import { Tooltip } from 'react-tooltip';
import { DiputadosNacionalesLayout } from '../../../components/common/DiputadosNacionalesLayout';
import { SenadoresNacionalesLayout } from '../../../components/common/SenadoresNacionalesLayout';
import { getComposicionNacional, type ComposicionNacionalData, type PartidoComposicionNacional } from '../../../apiService';
import '../provinciales/CongresoWidget.css';
interface CongresoNacionalWidgetProps {
eleccionId: number;
}
const formatTimestamp = (dateString: string) => {
if (!dateString) return '...';
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
const WidgetContent = ({ eleccionId }: CongresoNacionalWidgetProps) => {
const [camaraActiva, setCamaraActiva] = useState<'diputados' | 'senadores'>('diputados');
const [isHovering, setIsHovering] = useState(false);
const { data } = useSuspenseQuery<ComposicionNacionalData>({
queryKey: ['composicionNacional', eleccionId],
queryFn: () => getComposicionNacional(eleccionId),
refetchInterval: 30000,
});
const datosCamaraActual = data[camaraActiva];
const partidosOrdenados = useMemo(() => {
if (!datosCamaraActual?.partidos) return [];
const partidosACopiar = [...datosCamaraActual.partidos];
partidosACopiar.sort((a, b) => {
const ordenA = camaraActiva === 'diputados' ? a.ordenDiputadosNacionales : a.ordenSenadoresNacionales;
const ordenB = camaraActiva === 'diputados' ? b.ordenDiputadosNacionales : b.ordenSenadoresNacionales;
return (ordenA ?? 999) - (ordenB ?? 999);
});
return partidosACopiar;
}, [datosCamaraActual, camaraActiva]);
const partyDataParaLayout = useMemo(() => {
if (camaraActiva === 'senadores') return partidosOrdenados;
if (!partidosOrdenados || !datosCamaraActual.presidenteBancada?.color) return partidosOrdenados;
const partidoPresidente = partidosOrdenados.find(p => p.color === datosCamaraActual.presidenteBancada!.color);
if (!partidoPresidente) return partidosOrdenados;
const adjustedPartyData = JSON.parse(JSON.stringify(partidosOrdenados));
const partidoAjustar = adjustedPartyData.find((p: PartidoComposicionNacional) => p.id === partidoPresidente.id);
if (partidoAjustar) {
const tipoBanca = datosCamaraActual.presidenteBancada.tipoBanca;
if (tipoBanca === 'ganada' && partidoAjustar.bancasGanadas > 0) {
partidoAjustar.bancasGanadas -= 1;
} else if (tipoBanca === 'previa' && partidoAjustar.bancasFijos > 0) {
partidoAjustar.bancasFijos -= 1;
} else {
if (partidoAjustar.bancasGanadas > 0) {
partidoAjustar.bancasGanadas -= 1;
} else if (partidoAjustar.bancasFijos > 0) {
partidoAjustar.bancasFijos -= 1;
}
}
}
return adjustedPartyData;
}, [partidosOrdenados, datosCamaraActual.presidenteBancada, camaraActiva]);
return (
<div className="congreso-container">
<div className="congreso-grafico">
<div
className={`congreso-hemiciclo-wrapper ${isHovering ? 'is-hovering' : ''}`}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{camaraActiva === 'diputados' ?
<DiputadosNacionalesLayout
partyData={partyDataParaLayout}
presidenteBancada={datosCamaraActual.presidenteBancada || null}
size={700}
/> :
<SenadoresNacionalesLayout
partyData={partyDataParaLayout}
presidenteBancada={datosCamaraActual.presidenteBancada || null}
size={700}
/>
}
</div>
<div className="congreso-footer">
<div className="footer-legend">
<div className="footer-legend-item">
{/* Usamos la nueva clase CSS para el círculo sólido */}
<span className="legend-icon legend-icon--solid"></span>
<span>Bancas en juego</span>
</div>
<div className="footer-legend-item">
{/* Reemplazamos el SVG por un span con la nueva clase para el anillo */}
<span className="legend-icon legend-icon--ring"></span>
<span>Bancas previas</span>
</div>
</div>
<div className="footer-timestamp">
Última Actualización: {formatTimestamp(datosCamaraActual.ultimaActualizacion)}
</div>
</div>
</div>
<div className="congreso-summary">
<div className="chamber-tabs">
<button className={camaraActiva === 'diputados' ? 'active' : ''} onClick={() => setCamaraActiva('diputados')}>
Diputados
</button>
<button className={camaraActiva === 'senadores' ? 'active' : ''} onClick={() => setCamaraActiva('senadores')}>
Senadores
</button>
</div>
<h3>{datosCamaraActual.camaraNombre}</h3>
<div className="summary-metric">
<span>Total de Bancas</span>
<strong>{datosCamaraActual.totalBancas}</strong>
</div>
<div className="summary-metric">
<span>Bancas en Juego</span>
<strong>{datosCamaraActual.bancasEnJuego}</strong>
</div>
<hr />
<div className="partido-lista-container">
<ul className="partido-lista">
{partidosOrdenados
.filter(p => p.bancasTotales > 0)
.map((partido: PartidoComposicionNacional) => (
<li key={partido.id}>
<span className="partido-color-box" style={{ backgroundColor: partido.color || '#808080' }}></span>
<span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span>
<strong
className="partido-bancas"
title={`${partido.bancasFijos} bancas previas + ${partido.bancasGanadas} ganadas`}
>
{partido.bancasTotales}
</strong>
</li>
))}
</ul>
</div>
</div>
<Tooltip id="party-tooltip" />
</div>
);
};
export const CongresoNacionalWidget = ({ eleccionId }: CongresoNacionalWidgetProps) => {
return (
<Suspense fallback={<div className="congreso-container loading" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '400px' }}>Cargando composición del congreso...</div>}>
<WidgetContent eleccionId={eleccionId} />
</Suspense>
);
};

View File

@@ -1,10 +1,11 @@
/* src/features/legislativas/nacionales/PanelNaciona.css */
/* src/features/legislativas/nacionales/PanelNacional.css */
.panel-nacional-container {
font-family: 'Roboto', sans-serif;
max-width: 1200px;
margin: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
position: relative;
}
.panel-header {
@@ -491,13 +492,11 @@
/* --- NUEVOS ESTILOS PARA EL TOGGLE MÓVIL --- */
.mobile-view-toggle {
display: none;
/* Oculto por defecto */
position: fixed;
bottom: 20px;
position: absolute; /* <-- CAMBIO: De 'fixed' a 'absolute' */
bottom: 10px; /* <-- AJUSTE: Menos espacio desde abajo */
left: 50%;
transform: translateX(-50%);
z-index: 100;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 30px;
padding: 5px;

View File

@@ -0,0 +1,259 @@
/* src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.css */
/* --- Variables de Diseño --- */
:root {
--card-border-color: #e0e0e0;
--card-bg-color: #ffffff;
--card-header-bg-color: #f8f9fa;
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--text-primary: #212529;
--text-secondary: #6c757d;
--font-family: "Public Sans", system-ui, sans-serif;
--primary-accent-color: #007bff;
}
/* --- Contenedor Principal del Widget --- */
.cards-widget-container {
font-family: var(--font-family);
width: 100%;
max-width: 1200px;
margin: 2rem auto;
}
.cards-widget-container h2 {
font-size: 1.75rem;
color: var(--text-primary);
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--card-border-color);
}
/* --- Grilla de Tarjetas --- */
.cards-grid {
display: grid;
/* Crea columnas flexibles que se ajustan al espacio disponible */
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 1.5rem;
}
/* --- Tarjeta Individual --- */
.provincia-card {
background-color: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 8px;
box-shadow: var(--card-shadow);
display: flex;
flex-direction: column;
overflow: hidden; /* Asegura que los bordes redondeados se apliquen al contenido */
}
/* --- Cabecera de la Tarjeta --- */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--card-header-bg-color);
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--card-border-color);
}
.header-info h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 700;
color: var(--text-primary);
}
.header-info span {
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
}
.header-map {
width: 90px;
height: 90px;
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
background-color: #e9ecef;
padding: 0.25rem;
box-sizing: border-box; /* Para que el padding no aumente el tamaño total */
}
/* Contenedor del SVG para asegurar que se ajuste al espacio */
.map-svg-container, .map-placeholder {
width: 100%;
height: 100%;
}
/* Estilo para el SVG renderizado */
.map-svg-container svg {
width: 100%;
height: 100%;
object-fit: contain; /* Asegura que el mapa no se deforme */
}
/* Placeholder para cuando el mapa no carga */
.map-placeholder.error {
background-color: #f8d7da; /* Un color de fondo rojizo para indicar un error */
}
/* --- Cuerpo de la Tarjeta --- */
.card-body {
padding: 0.5rem 1rem;
}
.candidato-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
border-bottom: 1px solid #f0f0f0;
}
.candidato-row:last-child {
border-bottom: none;
}
.candidato-foto {
width: 45px;
height: 45px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.candidato-data {
flex-grow: 1;
min-width: 0; /* Permite que el texto se trunque si es necesario */
margin-right: 0.5rem;
}
.candidato-nombre {
font-weight: 700;
font-size: 0.95rem;
color: var(--text-primary);
display: block;
}
.candidato-partido {
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
display: block;
margin-bottom: 0.3rem;
}
.progress-bar-container {
height: 6px;
background-color: #e9ecef;
border-radius: 3px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease-out;
}
.candidato-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
text-align: right;
flex-shrink: 0;
padding-left: 0.5rem;
}
.stats-percent {
font-weight: 700;
font-size: 1.1rem;
color: var(--text-primary);
}
.stats-votos {
font-size: 0.8rem;
color: var(--text-secondary);
}
.stats-bancas {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1px solid var(--card-border-color);
border-radius: 6px;
padding: 0.25rem 0.5rem;
margin-left: 0.75rem;
font-weight: 700;
font-size: 1.2rem;
color: var(--primary-accent-color);
min-width: 50px;
}
.stats-bancas span {
font-size: 0.65rem;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
margin-top: -4px;
}
/* --- Pie de la Tarjeta --- */
.card-footer {
display: grid;
grid-template-columns: repeat(3, 1fr);
background-color: var(--card-header-bg-color);
border-top: 1px solid var(--card-border-color);
padding: 0.75rem 0;
text-align: center;
}
.card-footer div {
border-right: 1px solid var(--card-border-color);
}
.card-footer div:last-child {
border-right: none;
}
.card-footer span {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
}
.card-footer strong {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
}
/* --- Media Query para Móvil --- */
@media (max-width: 480px) {
.cards-grid {
/* En pantallas muy pequeñas, forzamos una sola columna */
grid-template-columns: 1fr;
}
.card-header {
padding: 0.5rem;
}
.header-info h3 {
font-size: 1rem;
}
}
/* --- NUEVOS ESTILOS PARA EL NOMBRE DEL PARTIDO CUANDO ES EL TÍTULO PRINCIPAL --- */
.candidato-partido.main-title {
font-size: 0.95rem; /* Hacemos la fuente más grande */
font-weight: 700; /* La ponemos en negrita, como el nombre del candidato */
color: var(--text-primary); /* Usamos el color de texto principal */
text-transform: none; /* Quitamos el 'uppercase' para que se lea mejor */
margin-bottom: 0.3rem;
}

View File

@@ -0,0 +1,30 @@
// src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.tsx
import { useQuery } from '@tanstack/react-query';
import { getResumenPorProvincia } from '../../../apiService';
import { ProvinciaCard } from './components/ProvinciaCard';
import './ResultadosNacionalesCardsWidget.css';
interface Props {
eleccionId: number;
}
export const ResultadosNacionalesCardsWidget = ({ eleccionId }: Props) => {
const { data, isLoading, error } = useQuery({
queryKey: ['resumenPorProvincia', eleccionId],
queryFn: () => getResumenPorProvincia(eleccionId),
});
if (isLoading) return <div>Cargando resultados por provincia...</div>;
if (error) return <div>Error al cargar los datos.</div>;
return (
<section className="cards-widget-container">
<h2>Resultados elecciones nacionales 2025</h2>
<div className="cards-grid">
{data?.map(provinciaData => (
<ProvinciaCard key={provinciaData.provinciaId} data={provinciaData} />
))}
</div>
</section>
);
};

View File

@@ -0,0 +1,64 @@
// src/features/legislativas/nacionales/components/MiniMapaSvg.tsx
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { useMemo } from 'react';
import { assetBaseUrl } from '../../../../apiService';
interface MiniMapaSvgProps {
provinciaNombre: string;
fillColor: string;
}
// Función para normalizar el nombre de la provincia y que coincida con el nombre del archivo SVG
const normalizarNombreParaUrl = (nombre: string) =>
nombre
.toLowerCase()
.replace(/ /g, '_') // Reemplaza espacios con guiones bajos
.normalize("NFD") // Descompone acentos para eliminarlos en el siguiente paso
.replace(/[\u0300-\u036f]/g, ""); // Elimina los acentos
export const MiniMapaSvg = ({ provinciaNombre, fillColor }: MiniMapaSvgProps) => {
const nombreNormalizado = normalizarNombreParaUrl(provinciaNombre);
// Asumimos que los SVGs están en /public/maps/provincias-svg/
const mapFileUrl = `${assetBaseUrl}/maps/provincias-svg/${nombreNormalizado}.svg`;
// Usamos React Query para fetchear el contenido del SVG como texto
const { data: svgContent, isLoading, isError } = useQuery<string>({
queryKey: ['svgMapa', nombreNormalizado],
queryFn: async () => {
const response = await axios.get(mapFileUrl, { responseType: 'text' });
return response.data;
},
staleTime: Infinity, // Estos archivos son estáticos y no cambian
gcTime: Infinity,
retry: false, // No reintentar si el archivo no existe
});
// Usamos useMemo para modificar el SVG solo cuando el contenido o el color cambian
const modifiedSvg = useMemo(() => {
if (!svgContent) return '';
// Usamos una expresión regular para encontrar todas las etiquetas <path>
// y añadirles el atributo de relleno con el color del ganador.
// Esto sobrescribirá cualquier 'fill' que ya exista en la etiqueta.
return svgContent.replace(/<path/g, `<path fill="${fillColor}"`);
}, [svgContent, fillColor]);
if (isLoading) {
return <div className="map-placeholder" />;
}
if (isError || !modifiedSvg) {
// Muestra un placeholder si el SVG no se encontró o está vacío
return <div className="map-placeholder error" />;
}
// Renderizamos el SVG modificado. dangerouslySetInnerHTML es seguro aquí
// porque el contenido proviene de nuestros propios archivos SVG estáticos.
return (
<div
className="map-svg-container"
dangerouslySetInnerHTML={{ __html: modifiedSvg }}
/>
);
};

View File

@@ -0,0 +1,78 @@
// src/features/legislativas/nacionales/components/ProvinciaCard.tsx
import type { ResumenProvincia } from '../../../../types/types';
import { MiniMapaSvg } from './MiniMapaSvg';
import { ImageWithFallback } from '../../../../components/common/ImageWithFallback';
import { assetBaseUrl } from '../../../../apiService';
interface ProvinciaCardProps {
data: ResumenProvincia;
}
const formatNumber = (num: number) => num.toLocaleString('es-AR');
const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`;
export const ProvinciaCard = ({ data }: ProvinciaCardProps) => {
// Determinamos el color del ganador para pasárselo al mapa.
// Si no hay ganador, usamos un color gris por defecto.
const colorGanador = data.resultados[0]?.color || '#d1d1d1';
return (
<div className="provincia-card">
<header className="card-header">
<div className="header-info">
<h3 style={{ whiteSpace: 'normal' }}>{data.provinciaNombre}</h3>
<span>DIPUTADOS NACIONALES</span>
</div>
<div className="header-map">
<MiniMapaSvg provinciaNombre={data.provinciaNombre} fillColor={colorGanador} />
</div>
</header>
<div className="card-body">
{data.resultados.map(res => (
<div key={res.agrupacionId} className="candidato-row">
<ImageWithFallback src={res.fotoUrl ?? undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={res.nombreCandidato ?? res.nombreAgrupacion} className="candidato-foto" />
<div className="candidato-data">
{res.nombreCandidato && (
<span className="candidato-nombre">{res.nombreCandidato}</span>
)}
<span className={`candidato-partido ${!res.nombreCandidato ? 'main-title' : ''}`}>
{res.nombreAgrupacion}
</span>
<div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${res.porcentaje}%`, backgroundColor: res.color || '#ccc' }} />
</div>
</div>
<div className="candidato-stats">
<span className="stats-percent">{formatPercent(res.porcentaje)}</span>
<span className="stats-votos">{formatNumber(res.votos)} votos</span>
</div>
<div className="stats-bancas">
+{res.bancasObtenidas}
<span>Bancas</span>
</div>
</div>
))}
</div>
<footer className="card-footer">
<div>
<span>Participación</span>
{/* Usamos los datos reales del estado de recuento */}
<strong>{formatPercent(data.estadoRecuento?.participacionPorcentaje ?? 0)}</strong>
</div>
<div>
<span>Mesas escrutadas</span>
<strong>{formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje ?? 0)}</strong>
</div>
<div>
<span>Votos totales</span>
{/* Usamos el nuevo campo cantidadVotantes */}
<strong>{formatNumber(data.estadoRecuento?.cantidadVotantes ?? 0)}</strong>
</div>
</footer>
</div>
);
};

View File

@@ -1,27 +1,35 @@
/* src/features/legislativas/provinciales/CongresoWidget.css */
.congreso-container {
display: flex;
/* Se reduce ligeramente el espacio entre el gráfico y el panel */
gap: 1rem;
gap: 1.5rem;
background-color: #ffffff;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
padding: 1rem;
border-radius: 8px;
max-width: 800px;
max-width: 900px;
margin: 20px auto;
font-family: "Public Sans", system-ui, sans-serif;
color: #333333;
align-items: center;
}
.congreso-grafico {
/* --- CAMBIO PRINCIPAL: Se aumenta la proporción del gráfico --- */
flex: 1 1 65%;
flex: 2;
min-width: 300px;
display: flex;
flex-direction: column;
}
.congreso-hemiciclo-wrapper {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.congreso-hemiciclo-wrapper.is-hovering .party-block:not(:hover) {
opacity: 0.4;
}
.congreso-grafico svg {
@@ -30,35 +38,139 @@
animation: fadeIn 0.8s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
/* --- NUEVOS ESTILOS PARA EL FOOTER DEL GRÁFICO --- */
.congreso-footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem 0 1rem;
margin-top: auto; /* Empuja el footer a la parte inferior del contenedor flex */
font-size: 0.8em;
color: #666;
border-top: 1px solid #eee;
}
to {
opacity: 1;
transform: scale(1);
}
.footer-legend {
display: flex;
gap: 1.5rem; /* Espacio entre los items de la leyenda */
}
.footer-legend-item {
display: flex;
align-items: center;
gap: 0.5rem; /* Espacio entre el icono y el texto */
}
.footer-timestamp {
font-weight: 500;
}
/* --- ESTILOS PARA HOVER --- */
/* Estilo base para cada círculo de escaño */
.seat-circle {
transition: all 0.2s ease-in-out;
}
.party-block {
cursor: pointer;
transition: opacity 0.2s ease-in-out;
}
.party-block:hover .seat-circle {
stroke: #333 !important; /* Borde oscuro para resaltar */
stroke-width: 1.5px !important;
stroke-opacity: 1;
filter: brightness(1.1);
}
/* CORRECCIÓN: El selector ahora apunta al wrapper correcto */
.congreso-hemiciclo-wrapper.is-hovering .party-block:not(:hover) {
opacity: 0.3; /* Hacemos el desvanecimiento más pronunciado */
}
.congreso-grafico svg {
width: 100%;
height: auto;
animation: fadeIn 0.8s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
/* --- INICIO DE NUEVOS ESTILOS PARA EL FOOTER DEL GRÁFICO --- */
.congreso-footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0.5rem 0 0.5rem;
margin-top: auto;
font-size: 0.8em;
color: #666;
border-top: 1px solid #eee;
}
.footer-legend {
display: flex;
gap: 1.25rem;
align-items: center;
}
.footer-legend-item {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1.1em;
}
/* Creamos una clase base para ambos iconos para compartir tamaño */
.legend-icon {
display: inline-block;
width: 14px; /* Tamaño base para ambos iconos */
height: 14px;
border-radius: 50%;
box-sizing: border-box;
}
/* Estilo para el icono de "Bancas en juego" (círculo sólido) */
.legend-icon--solid {
background-color: #888;
border: 1px solid #777;
}
/* Estilo para el icono de "Bancas previas" (anillo translúcido) */
.legend-icon--ring {
background-color: rgba(136, 136, 136, 0.3); /* #888 con opacidad */
border: 1px solid #888; /* Borde sólido del mismo color */
}
.footer-timestamp {
font-weight: 500;
font-size: 0.75em;
}
.congreso-summary {
/* --- CAMBIO PRINCIPAL: Se reduce la proporción del panel de datos --- */
flex: 1 1 35%;
flex: 1;
border-left: 1px solid #e0e0e0;
/* Se reduce el padding para dar aún más espacio al gráfico */
padding-left: 1rem;
padding-left: 1.25rem; /* Un poco más de padding */
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.congreso-summary h3 {
margin-top: 0;
margin-bottom: 0.75rem; /* Margen inferior reducido */
font-size: 1.4em;
color: #212529;
}
.chamber-tabs {
display: flex;
margin-bottom: 1.5rem;
margin-bottom: 1rem; /* Margen inferior reducido */
border: 1px solid #dee2e6;
border-radius: 6px;
overflow: hidden;
@@ -66,7 +178,7 @@
.chamber-tabs button {
flex: 1;
padding: 0.75rem 0.5rem;
padding: 0.5rem 0.5rem;
border: none;
background-color: #f8f9fa;
color: #6c757d;
@@ -94,7 +206,7 @@
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.5rem;
margin-bottom: 0.25rem; /* Margen inferior muy reducido */
font-size: 1.1em;
}
@@ -107,7 +219,15 @@
.congreso-summary hr {
border: none;
border-top: 1px solid #e0e0e0;
margin: 1.5rem 0;
margin: 1rem 0; /* Margen vertical reducido */
}
/* Contenedor de la lista de partidos para aplicar el scroll */
.partido-lista-container {
flex-grow: 1; /* Ocupa el espacio vertical disponible */
overflow-y: auto; /* Muestra el scrollbar si es necesario */
min-height: 0; /* Truco de Flexbox para que el scroll funcione */
padding-right: 8px; /* Espacio para el scrollbar */
}
.partido-lista {
@@ -119,14 +239,14 @@
.partido-lista li {
display: flex;
align-items: center;
margin-bottom: 0.75rem;
margin-bottom: 0.85rem; /* Un poco más de espacio entre items */
}
.partido-color-box {
width: 14px;
height: 14px;
border-radius: 3px;
margin-right: 10px;
width: 16px; /* Cuadro de color más grande */
height: 16px;
border-radius: 4px; /* Un poco más cuadrado */
margin-right: 12px;
flex-shrink: 0;
}
@@ -139,19 +259,54 @@
font-size: 1.1em;
}
/* --- Media Query para Responsividad Móvil --- */
/* --- Media Query para Responsividad Móvil (HASTA 768px) --- */
@media (max-width: 768px) {
.congreso-container {
flex-direction: column;
padding: 1.5rem;
padding: 0.5rem;
height: auto;
max-height: none;
}
.congreso-summary {
border-left: none;
padding-left: 0;
margin-top: 2rem;
border-top: 1px solid #e0e0e0;
padding-top: 1.5rem;
}
.partido-lista-container {
overflow-y: visible;
max-height: none;
}
.congreso-footer {
flex-direction: column; /* Apila la leyenda y el timestamp verticalmente */
align-items: flex-start; /* Alinea todo a la izquierda */
gap: 0.5rem; /* Añade un pequeño espacio entre la leyenda y el timestamp */
padding: 0.75rem 0rem; /* Ajusta el padding para móvil */
align-items: center;
}
.footer-legend {
gap: 0.75rem; /* Reduce el espacio entre los items de la leyenda */
}
.footer-legend-item{
font-size: 1em;
}
.footer-timestamp {
font-size: 0.75em; /* Reduce el tamaño de la fuente para que quepa mejor */
}
}
/* --- Media Query para Escritorio (DESDE 769px en adelante) --- */
@media (min-width: 769px) {
.congreso-container {
flex-direction: row;
align-items: stretch;
height: 500px;
}
}

View File

@@ -245,4 +245,29 @@ export interface PanelElectoralDto {
mapaData: ResultadoMapaDto[];
resultadosPanel: ResultadoTicker[]; // Reutilizamos el tipo que ya tienes
estadoRecuento: EstadoRecuentoTicker; // Reutilizamos el tipo que ya tienes
}
// --- TIPOS PARA EL WIDGET DE TARJETAS ---
export interface EstadoRecuentoDto {
participacionPorcentaje: number;
mesasTotalizadasPorcentaje: number;
cantidadVotantes: number;
}
export interface ResultadoCandidato {
agrupacionId: string;
nombreCandidato: string | null;
nombreAgrupacion: string;
fotoUrl: string | null;
color: string | null;
porcentaje: number;
votos: number;
bancasObtenidas: number;
}
export interface ResumenProvincia {
provinciaId: string;
provinciaNombre: string;
estadoRecuento: EstadoRecuentoDto | null;
resultados: ResultadoCandidato[];
}