Feat Widgets Controles y Estilos
This commit is contained in:
		| @@ -144,7 +144,6 @@ | ||||
|   font-size: 1.2rem; | ||||
| } | ||||
|  | ||||
|  | ||||
| .panel-main-content { | ||||
|   display: flex; | ||||
|   height: 75vh; | ||||
| @@ -180,15 +179,16 @@ | ||||
|  | ||||
| .partido-logo { | ||||
|   flex-shrink: 0; | ||||
|   width: 65px;  /* ANTES: 75px */ | ||||
|   height: 65px; /* ANTES: 75px */ | ||||
|   width: 65px; | ||||
|   height: 65px; | ||||
|   border-radius: 12px; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| .partido-logo img { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   object-fit: contain; | ||||
|   border-radius: 10%; | ||||
|   border-radius: 12px; | ||||
| } | ||||
|  | ||||
| .partido-main-content { | ||||
| @@ -279,7 +279,6 @@ | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* --- MAPA Y ELEMENTOS ASOCIADOS --- */ | ||||
| .mapa-componente-container { | ||||
|   width: 100%; | ||||
| @@ -313,7 +312,7 @@ | ||||
|   transition: transform 0.75s ease-in-out; | ||||
| } | ||||
|  | ||||
| /* AÑADIDO: Desactivar la transición durante el arrastre */ | ||||
| /* Desactivar la transición durante el arrastre */ | ||||
| .rsm-zoomable-group.panning { | ||||
|   transition: none; | ||||
| } | ||||
| @@ -475,52 +474,12 @@ | ||||
|   margin-top: 4px; | ||||
| } | ||||
|  | ||||
| /* --- NUEVOS ESTILOS PARA EL TOGGLE MÓVIL --- */ | ||||
| .mobile-view-toggle { | ||||
|   display: none; | ||||
|   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; | ||||
|   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||||
|   gap: 5px; | ||||
|   backdrop-filter: blur(5px); | ||||
|   -webkit-backdrop-filter: blur(5px); | ||||
| } | ||||
|  | ||||
| .mobile-view-toggle .toggle-btn { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 8px; | ||||
|   padding: 10px 20px; | ||||
|   border: none; | ||||
|   background-color: transparent; | ||||
|   border-radius: 25px; | ||||
|   cursor: pointer; | ||||
|   font-size: 1rem; | ||||
|   font-weight: 500; | ||||
|   color: #555; | ||||
|   transition: all 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .mobile-view-toggle .toggle-btn.active { | ||||
|   background-color: #007bff; | ||||
|   color: white; | ||||
| } | ||||
|  | ||||
| /* --- ESTILOS PARA LOS BOTONES DE ZOOM DEL MAPA --- */ | ||||
| .zoom-controls-container { | ||||
|   position: absolute; | ||||
|   top: 5px; | ||||
|   right: 10px; | ||||
|   z-index: 30; | ||||
|   /* Debe ser MAYOR que el z-index del header (20) */ | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 5px; | ||||
| @@ -542,15 +501,12 @@ | ||||
| } | ||||
|  | ||||
| .zoom-icon-wrapper { | ||||
|   /* Contenedor del icono */ | ||||
|   display: flex; | ||||
|   /* Necesario para que el SVG interno se alinee */ | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| .zoom-icon-wrapper svg { | ||||
|   /* Apunta directamente al SVG del icono */ | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|   color: #333; | ||||
| @@ -558,9 +514,7 @@ | ||||
|  | ||||
| .zoom-btn.disabled { | ||||
|   opacity: 0.5; | ||||
|   /* Lo hace semitransparente */ | ||||
|   cursor: not-allowed; | ||||
|   /* Muestra el cursor de "no permitido" */ | ||||
| } | ||||
|  | ||||
| .zoom-btn:hover { | ||||
| @@ -576,22 +530,41 @@ | ||||
|   cursor: grab; | ||||
| } | ||||
|  | ||||
| /* El cursor 'grabbing' se aplica automáticamente por el navegador durante el arrastre */ | ||||
| .header-bottom-row { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   margin-top: 1rem; | ||||
|   gap: 1rem; | ||||
| } | ||||
|  | ||||
| .municipio-search-container { | ||||
|   min-width: 280px; /* Ancho mínimo para el buscador en desktop */ | ||||
| } | ||||
|  | ||||
| /* --- MEDIA QUERY PARA RESPONSIVE (ENFOQUE FINAL CON CAPAS) --- */ | ||||
| /* --- MEDIA QUERY PARA RESPONSIVE (REFACTORIZADA) --- */ | ||||
| @media (max-width: 800px) { | ||||
|  | ||||
|   /* --- CONFIGURACIÓN GENERAL --- */ | ||||
|   html, | ||||
|   body { | ||||
|     width: 100%; | ||||
|     overflow-x: hidden; | ||||
|   .panel-nacional-container { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       height: 100vh; | ||||
|       padding: 0; | ||||
|       border: none; | ||||
|       border-radius: 0; | ||||
|   } | ||||
|  | ||||
|   /* Controles de vista y header (sin cambios) */ | ||||
|   .mobile-view-toggle { | ||||
|     display: flex; | ||||
|   .panel-header { | ||||
|       flex-shrink: 0; | ||||
|       padding: 1rem; | ||||
|       border-radius: 0; | ||||
|   } | ||||
|  | ||||
|   .panel-main-content { | ||||
|     flex-grow: 1; | ||||
|     position: relative; | ||||
|     height: auto; | ||||
|     min-height: 0; | ||||
|   } | ||||
|  | ||||
|   .panel-toggle-btn { | ||||
| @@ -608,21 +581,10 @@ | ||||
|     width: 100%; | ||||
|   } | ||||
|  | ||||
|   /* --- NUEVO LAYOUT DE CAPAS SUPERPUESTAS --- */ | ||||
|  | ||||
|   /* 1. El contenedor principal ahora es un ancla de posicionamiento */ | ||||
|   .panel-main-content { | ||||
|     position: relative; | ||||
|     /* Clave para que los hijos se posicionen dentro de él */ | ||||
|     height: calc(100vh - 200px); | ||||
|     /* Le damos una altura fija y predecible */ | ||||
|     min-height: 450px; | ||||
|   } | ||||
|  | ||||
|   /* 2. Ambas columnas son capas que ocupan el 100% del espacio del padre */ | ||||
|   .mapa-column, | ||||
|   .resultados-column { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
| @@ -630,30 +592,20 @@ | ||||
|     transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out; | ||||
|   } | ||||
|  | ||||
|   /* Le damos un estilo específico a la columna del mapa para subirla */ | ||||
|   .mapa-column { | ||||
|     top: -50px; | ||||
|     left: -10px; | ||||
|     z-index: 10; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /* Hacemos que la columna de resultados pueda tener su propio scroll... */ | ||||
|   .resultados-column { | ||||
|     top: 0; | ||||
|     /* Aseguramos que los resultados se queden en su sitio */ | ||||
|     padding: 1rem; | ||||
|     overflow-y: auto; | ||||
|     z-index: 15; | ||||
|   } | ||||
|  | ||||
|   /* 3. Lógica de visibilidad: controlamos qué capa está "arriba" */ | ||||
|   .panel-main-content.mobile-view-mapa .resultados-column { | ||||
|     opacity: 0; | ||||
|     visibility: hidden; | ||||
|     /* Esta es la propiedad clave que ya tenías, pero es importante verificarla */ | ||||
|     pointer-events: none; | ||||
|     /* Asegura que la capa oculta no bloquee el mapa */ | ||||
|   } | ||||
|  | ||||
|   .panel-main-content.mobile-view-resultados .mapa-column { | ||||
| @@ -662,85 +614,35 @@ | ||||
|     pointer-events: none; | ||||
|   } | ||||
|  | ||||
|   /* Hacemos que la columna de resultados pueda tener su propio scroll si el contenido es largo */ | ||||
|   .resultados-column { | ||||
|     padding: 1rem; | ||||
|     overflow-y: auto; | ||||
|   } | ||||
|  | ||||
|   /* 4. Estilos de los resultados (ya estaban bien, se mantienen) */ | ||||
|   .partido-fila { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 1rem; | ||||
|     padding: 1rem 0; | ||||
|     border-bottom: 1px solid #f0f0f0; | ||||
|     border-left: 5px solid; | ||||
|     /* Grosor del borde */ | ||||
|     border-radius: 12px; | ||||
|     /* Redondeamos las esquinas */ | ||||
|     padding-left: 1rem; | ||||
|     /* Espacio a la izquierda */ | ||||
|   } | ||||
|  | ||||
|   .partido-logo { | ||||
|     width: 60px; | ||||
|     height: 60px; | ||||
|     flex-shrink: 0; | ||||
|   } | ||||
|  | ||||
|   .partido-main-content { | ||||
|     flex-grow: 1; | ||||
|     min-width: 0; | ||||
|   } | ||||
|  | ||||
|   .partido-top-row { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: flex-start; | ||||
|   } | ||||
|  | ||||
|   .partido-info-wrapper { | ||||
|     min-width: 0; | ||||
|   } | ||||
|  | ||||
|   .partido-nombre { | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|   } | ||||
|  | ||||
|   .partido-stats { | ||||
|     text-align: right; | ||||
|     flex-shrink: 0; | ||||
|     padding-left: 0.5rem; | ||||
|   } | ||||
|  | ||||
|   /* --- AJUSTE DE TAMAÑO DEL CONTENEDOR INTERNO DEL MAPA --- */ | ||||
|   .mapa-column .mapa-componente-container, | ||||
|   .mapa-column .mapa-render-area { | ||||
|     height: 100%; | ||||
|   } | ||||
|  | ||||
|   /* Margen de seguridad para el último elemento de la lista de resultados */ | ||||
|   .panel-partidos-container .partido-fila:last-child { | ||||
|     margin-bottom: 90px; | ||||
|   } | ||||
|  | ||||
|   .zoom-controls-container { | ||||
|     top: 55px; | ||||
|   .zoom-controls-container, .mapa-volver-btn { | ||||
|     top: 15px; | ||||
|   } | ||||
|  | ||||
|   .mapa-volver-btn { | ||||
|     top: 55px; | ||||
|     left: 12px; | ||||
|   .header-bottom-row { | ||||
|     flex-direction: column; | ||||
|     align-items: stretch; /* Para que ambos elementos ocupen el ancho completo */ | ||||
|     gap: 1rem; | ||||
|   } | ||||
|    | ||||
|   .municipio-search-container { | ||||
|     min-width: 100%; /* El buscador ocupa todo el ancho en móvil */ | ||||
|   } | ||||
|  | ||||
|   /* --- MEDIA QUERY ADICIONAL PARA MÓVIL EN HORIZONTAL --- */ | ||||
|   /* Se activa cuando la pantalla es ancha pero no muy alta, como un teléfono en landscape */ | ||||
|   @media (max-width: 900px) and (orientation: landscape) { | ||||
|  | ||||
|     /* Layout flexible de dos columnas */ | ||||
|     .panel-main-content { | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
| @@ -748,11 +650,8 @@ | ||||
|       height: 85vh; | ||||
|       min-height: 400px; | ||||
|     } | ||||
|  | ||||
|     .mapa-column, | ||||
|     .resultados-column { | ||||
|     .mapa-column, .resultados-column { | ||||
|       position: static; | ||||
|       /* Desactivamos el posicionamiento absoluto */ | ||||
|       height: auto; | ||||
|       width: auto; | ||||
|       opacity: 1; | ||||
| @@ -760,23 +659,177 @@ | ||||
|       pointer-events: auto; | ||||
|       flex: 3; | ||||
|       overflow-y: auto; | ||||
|       /* Permitimos que la columna de resultados tenga su propio scroll */ | ||||
|     } | ||||
|  | ||||
|     .resultados-column { | ||||
|       flex: 2; | ||||
|       min-width: 300px; | ||||
|       /* Un mínimo para que no se comprima */ | ||||
|     } | ||||
|     .mobile-results-card-container { display: none; } | ||||
|     .panel-toggle-btn { display: flex; } | ||||
|   } | ||||
| } | ||||
|  | ||||
|     /* 3. Ocultamos los botones de cambio de vista móvil, ya que ambas se ven */ | ||||
|     .mobile-view-toggle { | ||||
|       display: none; | ||||
|     } | ||||
| /* --- ESTILOS PARA LA TARJETA DE RESULTADOS EN MÓVIL (ACTUALIZADOS) --- */ | ||||
| .mobile-results-card-container { | ||||
|     position: absolute; | ||||
|     bottom: 10px; | ||||
|     left: 50%; | ||||
|     transform: translateX(-50%); | ||||
|     z-index: 40; | ||||
|     width: 95%; | ||||
|     max-width: 450px; | ||||
|     background-color: rgba(255, 255, 255, 0.95); | ||||
|     border-radius: 12px; | ||||
|     box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); | ||||
|     backdrop-filter: blur(8px); | ||||
|     -webkit-backdrop-filter: blur(8px); | ||||
|     border: 1px solid rgba(0, 0, 0, 0.1); | ||||
|     transition: all 0.3s ease-in-out; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
| } | ||||
|  | ||||
|     /* 4. Mostramos de nuevo el botón lateral para colapsar el panel de resultados */ | ||||
|     .panel-toggle-btn { | ||||
|       display: flex; | ||||
| .mobile-results-card-container.view-resultados .collapsible-section { | ||||
|     display: none; | ||||
| } | ||||
| .mobile-results-card-container.view-resultados .mobile-card-view-toggle { | ||||
|     border-top: none; | ||||
| } | ||||
|  | ||||
|  | ||||
| .collapsible-section { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
| } | ||||
|  | ||||
| .mobile-results-header { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     padding: 12px 18px; /* REDUCIDO */ | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .mobile-results-header .header-info { | ||||
|     display: flex;       /* AÑADIDO */ | ||||
|     align-items: baseline; /* AÑADIDO */ | ||||
|     gap: 12px;           /* AÑADIDO */ | ||||
| } | ||||
|  | ||||
| .mobile-results-header .header-info h4 { | ||||
|     margin: 0; | ||||
|     font-size: 1.2rem; | ||||
|     font-weight: 700; | ||||
| } | ||||
|  | ||||
| /* SELECTOR ESPECÍFICO PARA EL TEXTO DE ACCIÓN */ | ||||
| .mobile-results-header .header-info .header-action-text { | ||||
|     font-size: 0.8rem; | ||||
|     color: #6c757d; | ||||
|     font-weight: 500; | ||||
|     text-transform: uppercase; | ||||
| } | ||||
|  | ||||
| .mobile-results-header .header-toggle-icon { | ||||
|     font-size: 1.5rem; | ||||
|     color: #007bff; | ||||
|     transition: transform 0.3s; | ||||
| } | ||||
|  | ||||
| .mobile-results-content { | ||||
|     max-height: 0; | ||||
|     opacity: 0; | ||||
|     overflow: hidden; | ||||
|     transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out, padding 0.3s ease-in-out; | ||||
|     padding: 0 15px; | ||||
|     border-top: 1px solid transparent; | ||||
| } | ||||
|  | ||||
| .mobile-results-card-container.expanded .mobile-results-content { | ||||
|     max-height: 500px; | ||||
|     opacity: 1; | ||||
|     padding: 5px 15px 15px 15px; | ||||
|     border-top-color: #e0e0e0; | ||||
| } | ||||
|  | ||||
| .mobile-result-row { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     padding: 8px 0; | ||||
|     border-bottom: 1px solid #f0f0f0; | ||||
|     border-left: 4px solid; | ||||
|     padding-left: 8px; | ||||
| } | ||||
| .mobile-result-row:last-child { border-bottom: none; } | ||||
|  | ||||
| .mobile-result-logo { | ||||
|         flex-shrink: 0; | ||||
|         width: 40px; | ||||
|         height: 40px; | ||||
|         border-radius: 8px; | ||||
|         box-sizing: border-box; | ||||
|     } | ||||
|     .mobile-result-logo img { | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         border-radius: 8px; | ||||
|     } | ||||
| .mobile-result-info { flex-grow: 1; min-width: 0; } | ||||
| .mobile-result-party-name { display: block; font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | ||||
| .mobile-result-candidate-name { display: block; font-size: 0.75rem; color: #6c757d; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | ||||
| .mobile-result-stats { display: flex; flex-direction: column; align-items: flex-end; flex-shrink: 0; } | ||||
| .mobile-result-stats strong { font-size: 0.95rem; font-weight: 700; } | ||||
| .mobile-result-stats span { font-size: 0.7rem; color: #6c757d; } | ||||
| .no-results-text { padding: 1rem; text-align: center; color: #6c757d; font-size: 0.9rem; } | ||||
|  | ||||
| .mobile-card-view-toggle { | ||||
|     display: flex; | ||||
|     padding: 5px; | ||||
|     background-color: rgba(230, 230, 230, 0.6); | ||||
|     border-top: 1px solid rgba(0, 0, 0, 0.08); | ||||
| } | ||||
|  | ||||
| .mobile-card-view-toggle .toggle-btn { | ||||
|     flex: 1; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 8px; | ||||
|     padding: 10px 15px; /* Aumentado para pantallas más grandes */ | ||||
|     border: none; | ||||
|     background-color: transparent; | ||||
|     border-radius: 25px; | ||||
|     cursor: pointer; | ||||
|     font-size: 1rem; /* Mantenido para pantallas más grandes */ | ||||
|     font-weight: 500; | ||||
|     color: #555; | ||||
|     transition: all 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .mobile-card-view-toggle .toggle-btn.active { | ||||
|     background-color: #007bff; | ||||
|     color: white; | ||||
|     box-shadow: 0 2px 5px rgba(0, 123, 255, 0.2); | ||||
| } | ||||
|  | ||||
| /* Ajustes para pantallas pequeñas como el iPhone SE */ | ||||
| @media (max-width: 380px) { | ||||
|   .mobile-results-header { | ||||
|     padding: 4px 10px; | ||||
|   } | ||||
|  | ||||
|   .mobile-results-header .header-info h4 { | ||||
|     font-size: 0.75rem; | ||||
|     text-transform: uppercase; /* Se achica el título */ | ||||
|   } | ||||
|  | ||||
|   .mobile-results-header .header-info .header-action-text { | ||||
|     font-size: 0.7rem; /* Se achica el texto de acción */ | ||||
|   } | ||||
|  | ||||
|   .mobile-card-view-toggle .toggle-btn { | ||||
|     padding: 6px 10px; /* Se reduce el padding de los botones */ | ||||
|     font-size: 0.8rem; /* Se achica la fuente de los botones */ | ||||
|   } | ||||
| } | ||||
| @@ -1,17 +1,130 @@ | ||||
| // src/features/legislativas/nacionales/PanelNacionalWidget.tsx | ||||
| import { useMemo, useState, Suspense } from 'react'; | ||||
| import { useMemo, useState, Suspense, useEffect } from 'react'; | ||||
| import { useSuspenseQuery } from '@tanstack/react-query'; | ||||
| import { getPanelElectoral } from '../../../apiService'; | ||||
| import { MapaNacional } from './components/MapaNacional'; | ||||
| import { PanelResultados } from './components/PanelResultados'; | ||||
| import { Breadcrumbs } from './components/Breadcrumbs'; | ||||
| import { MunicipioSearch } from './components/MunicipioSearch'; | ||||
| import './PanelNacional.css'; | ||||
| import Select from 'react-select'; | ||||
| import type { PanelElectoralDto } from '../../../types/types'; | ||||
| import { FiMap, FiList } from 'react-icons/fi'; | ||||
| import type { PanelElectoralDto, ResultadoTicker } from '../../../types/types'; | ||||
| import { FiMap, FiList, FiChevronDown, FiChevronUp } from 'react-icons/fi'; | ||||
| import { useMediaQuery } from './hooks/useMediaQuery'; | ||||
| import { Toaster } from 'react-hot-toast'; | ||||
| import { ImageWithFallback } from '../../../components/common/ImageWithFallback'; | ||||
| import { assetBaseUrl } from '../../../apiService'; | ||||
|  | ||||
| // --- COMPONENTE INTERNO PARA LA TARJETA DE RESULTADOS EN MÓVIL --- | ||||
| interface MobileResultsCardProps { | ||||
|   eleccionId: number; | ||||
|   ambitoId: string | null; | ||||
|   categoriaId: number; | ||||
|   ambitoNombre: string; | ||||
|   ambitoNivel: 'pais' | 'provincia' | 'municipio'; | ||||
| } | ||||
|  | ||||
| const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; | ||||
|  | ||||
| // --- SUB-COMPONENTE PARA UNA FILA DE RESULTADO --- | ||||
| const ResultRow = ({ partido }: { partido: ResultadoTicker }) => ( | ||||
|     <div className="mobile-result-row" style={{ borderLeftColor: partido.color || '#ccc' }}> | ||||
|         <div className="mobile-result-logo" style={{ backgroundColor: partido.color || '#e9ecef' }}> | ||||
|              <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} /> | ||||
|         </div> | ||||
|         <div className="mobile-result-info"> | ||||
|             {partido.nombreCandidato ? ( | ||||
|                 <> | ||||
|                     <span className="mobile-result-party-name">{partido.nombreCandidato}</span> | ||||
|                     <span className="mobile-result-candidate-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|                 </> | ||||
|             ) : ( | ||||
|                 <span className="mobile-result-party-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|             )} | ||||
|         </div> | ||||
|         <div className="mobile-result-stats"> | ||||
|             <strong>{formatPercent(partido.porcentaje)}</strong> | ||||
|             <span>{partido.votos.toLocaleString('es-AR')}</span> | ||||
|         </div> | ||||
|     </div> | ||||
| ); | ||||
|  | ||||
| // --- COMPONENTE REFACTORIZADO PARA LA TARJETA MÓVIL --- | ||||
| interface MobileResultsCardProps { | ||||
|   eleccionId: number; | ||||
|   ambitoId: string | null; | ||||
|   categoriaId: number; | ||||
|   ambitoNombre: string; | ||||
|   ambitoNivel: 'pais' | 'provincia' | 'municipio'; | ||||
|   mobileView: 'mapa' | 'resultados'; | ||||
|   setMobileView: (view: 'mapa' | 'resultados') => void; | ||||
| } | ||||
|  | ||||
| const MobileResultsCard = ({  | ||||
|     eleccionId, ambitoId, categoriaId, ambitoNombre, ambitoNivel, mobileView, setMobileView  | ||||
| }: MobileResultsCardProps) => { | ||||
|      | ||||
|     const [isExpanded, setIsExpanded] = useState(false); | ||||
|  | ||||
|     const { data } = useSuspenseQuery<PanelElectoralDto>({ | ||||
|         queryKey: ['panelElectoral', eleccionId, ambitoId, categoriaId], | ||||
|         queryFn: () => getPanelElectoral(eleccionId, ambitoId, categoriaId), | ||||
|     }); | ||||
|      | ||||
|     useEffect(() => { | ||||
|         setIsExpanded(ambitoNivel === 'municipio'); | ||||
|     }, [ambitoNivel]); | ||||
|  | ||||
|     const topResults = data.resultadosPanel.slice(0, 4); | ||||
|  | ||||
|     if (topResults.length === 0 && ambitoNivel === 'pais') { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div className={`mobile-results-card-container ${isExpanded ? 'expanded' : ''} view-${mobileView}`}> | ||||
|             {/* Sección Colapsable con Resultados */} | ||||
|             <div className="collapsible-section"> | ||||
|                 <div className="mobile-results-header" onClick={() => setIsExpanded(!isExpanded)}> | ||||
|                     <div className="header-info"> | ||||
|                         <h4>{ambitoNombre}</h4> | ||||
|                         {/* Se añade una clase para estilizar este texto específicamente */} | ||||
|                         <span className="header-action-text">{isExpanded ? 'Ocultar resultados' : 'Ver top 4'}</span> | ||||
|                     </div> | ||||
|                     <div className="header-toggle-icon"> | ||||
|                         {isExpanded ? <FiChevronUp /> : <FiChevronDown />} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div className="mobile-results-content"> | ||||
|                     {topResults.length > 0 ? ( | ||||
|                         topResults.map(partido => <ResultRow key={partido.id} partido={partido} />) | ||||
|                     ) : ( | ||||
|                         <p className="no-results-text">No hay resultados para esta selección.</p> | ||||
|                     )} | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             {/* Footer Fijo con Botones de Navegación */} | ||||
|             <div className="mobile-card-view-toggle"> | ||||
|                 <button | ||||
|                     className={`toggle-btn ${mobileView === 'mapa' ? 'active' : ''}`} | ||||
|                     onClick={() => setMobileView('mapa')} | ||||
|                 > | ||||
|                     <FiMap /> | ||||
|                     <span>Mapa</span> | ||||
|                 </button> | ||||
|                 <button | ||||
|                     className={`toggle-btn ${mobileView === 'resultados' ? 'active' : ''}`} | ||||
|                     onClick={() => setMobileView('resultados')} | ||||
|                 > | ||||
|                     <FiList /> | ||||
|                     <span>Resultados</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| // --- WIDGET PRINCIPAL --- | ||||
| interface PanelNacionalWidgetProps { | ||||
|   eleccionId: number; | ||||
| } | ||||
| @@ -42,7 +155,6 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|   const [categoriaId, setCategoriaId] = useState<number>(2); | ||||
|   const [isPanelOpen, setIsPanelOpen] = useState(true); | ||||
|   const [mobileView, setMobileView] = useState<'mapa' | 'resultados'>('mapa'); | ||||
|   // --- DETECCIÓN DE VISTA MÓVIL --- | ||||
|   const isMobile = useMediaQuery('(max-width: 800px)'); | ||||
|  | ||||
|   const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => { | ||||
| @@ -91,62 +203,55 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|             classNamePrefix="categoria-selector" | ||||
|             isSearchable={false} | ||||
|           /> | ||||
|           <Breadcrumbs | ||||
|             nivel={ambitoActual.nivel} | ||||
|             nombreAmbito={ambitoActual.nombre} | ||||
|             nombreProvincia={ambitoActual.provinciaNombre} | ||||
|             onReset={handleResetToPais} | ||||
|             onVolverProvincia={handleVolverAProvincia} | ||||
|           /> | ||||
|         </div> | ||||
|         {/* --- 2. NUEVO CONTENEDOR PARA BREADCRUMBS Y BUSCADOR --- */} | ||||
|         <div className="header-bottom-row"> | ||||
|             <Breadcrumbs | ||||
|                 nivel={ambitoActual.nivel} | ||||
|                 nombreAmbito={ambitoActual.nombre} | ||||
|                 nombreProvincia={ambitoActual.provinciaNombre} | ||||
|                 onReset={handleResetToPais} | ||||
|                 onVolverProvincia={handleVolverAProvincia} | ||||
|             /> | ||||
|             {/* --- 3. RENDERIZADO CONDICIONAL DEL BUSCADOR --- */} | ||||
|             {ambitoActual.nivel === 'provincia' && ambitoActual.provinciaDistritoId && ( | ||||
|                 <MunicipioSearch  | ||||
|                     distritoId={ambitoActual.provinciaDistritoId} | ||||
|                     onMunicipioSelect={(municipioId, municipioNombre) =>  | ||||
|                         handleAmbitoSelect(municipioId, 'municipio', municipioNombre) | ||||
|                     } | ||||
|                 /> | ||||
|             )} | ||||
|         </div> | ||||
|       </header> | ||||
|       <main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''} ${isMobile ? `mobile-view-${mobileView}` : ''}`}> | ||||
|         <div className="mapa-column"> | ||||
|           <button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}> | ||||
|             {isPanelOpen ? '›' : '‹'} | ||||
|           </button> | ||||
|           <button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}> {isPanelOpen ? '›' : '‹'} </button> | ||||
|            | ||||
|           <Suspense fallback={<div className="spinner" />}> | ||||
|             <MapaNacional | ||||
|               eleccionId={eleccionId} | ||||
|               categoriaId={categoriaId} | ||||
|               nivel={ambitoActual.nivel} | ||||
|               nombreAmbito={ambitoActual.nombre} | ||||
|               nombreProvinciaActiva={ambitoActual.provinciaNombre} | ||||
|               provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null} | ||||
|               onAmbitoSelect={handleAmbitoSelect} | ||||
|               onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais} | ||||
|               isMobileView={isMobile} | ||||
|             /> | ||||
|             <MapaNacional eleccionId={eleccionId} categoriaId={categoriaId} nivel={ambitoActual.nivel} nombreAmbito={ambitoActual.nombre} nombreProvinciaActiva={ambitoActual.provinciaNombre} provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null} onAmbitoSelect={handleAmbitoSelect} onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais} isMobileView={isMobile} /> | ||||
|           </Suspense> | ||||
|         </div> | ||||
|         <div className="resultados-column"> | ||||
|           <Suspense fallback={<div className="spinner" />}> | ||||
|             <PanelContenido | ||||
|               eleccionId={eleccionId} | ||||
|               ambitoActual={ambitoActual} | ||||
|               categoriaId={categoriaId} | ||||
|             /> | ||||
|             <PanelContenido eleccionId={eleccionId} ambitoActual={ambitoActual} categoriaId={categoriaId} /> | ||||
|           </Suspense> | ||||
|         </div> | ||||
|       </main> | ||||
|  | ||||
|       {/* --- NUEVO CONTROLADOR DE VISTA PARA MÓVIL --- */} | ||||
|       <div className="mobile-view-toggle"> | ||||
|         <button | ||||
|           className={`toggle-btn ${mobileView === 'mapa' ? 'active' : ''}`} | ||||
|           onClick={() => setMobileView('mapa')} | ||||
|         > | ||||
|           <FiMap /> | ||||
|           <span>Mapa</span> | ||||
|         </button> | ||||
|         <button | ||||
|           className={`toggle-btn ${mobileView === 'resultados' ? 'active' : ''}`} | ||||
|           onClick={() => setMobileView('resultados')} | ||||
|         > | ||||
|           <FiList /> | ||||
|           <span>Resultados</span> | ||||
|         </button> | ||||
|       </div> | ||||
|         <Suspense fallback={null}> | ||||
|             {isMobile && ( | ||||
|               <MobileResultsCard  | ||||
|                 eleccionId={eleccionId} | ||||
|                 ambitoId={ambitoActual.id} | ||||
|                 categoriaId={categoriaId} | ||||
|                 ambitoNombre={ambitoActual.nombre} | ||||
|                 ambitoNivel={ambitoActual.nivel} | ||||
|                 mobileView={mobileView} | ||||
|                 setMobileView={setMobileView} | ||||
|               /> | ||||
|             )} | ||||
|         </Suspense> | ||||
|       </main> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -106,6 +106,10 @@ | ||||
|     padding: 0.5rem 1rem; | ||||
| } | ||||
|  | ||||
| .candidato-row:last-child { | ||||
|     border-bottom: none; | ||||
| } | ||||
|  | ||||
| .candidato-row { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
| @@ -114,19 +118,24 @@ | ||||
|     border-bottom: 1px solid #f0f0f0; | ||||
|     border-left: 5px solid; /* Grosor del borde */ | ||||
|     border-radius: 12px;     /* Redondeamos las esquinas para un look más suave */ | ||||
|     padding-left: 1rem;      /* Añadimos un poco de espacio a la izquierda */ | ||||
|     padding-left: 0.75rem;      /* Añadimos un poco de espacio a la izquierda */ | ||||
| } | ||||
|  | ||||
| .candidato-row:last-child { | ||||
|     border-bottom: none; | ||||
| } | ||||
|  | ||||
| .candidato-foto { | ||||
| /* Nuevo contenedor para el logo con fondo de color */ | ||||
| .candidato-foto-wrapper { | ||||
|     width: 60px; | ||||
|     height: 60px; | ||||
|     border-radius: 5%; | ||||
|     object-fit: cover; | ||||
|     border-radius: 12px; | ||||
|     flex-shrink: 0; | ||||
|     box-sizing: border-box; | ||||
|     background-color: #e9ecef; /* Color de fallback */ | ||||
| } | ||||
|  | ||||
| /* La imagen ahora llena su nuevo contenedor */ | ||||
| .candidato-foto { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     border-radius: 12px; | ||||
| } | ||||
|  | ||||
| .candidato-data { | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| // src/features/legislativas/nacionales/components/MapaNacional.tsx | ||||
| import axios from 'axios'; | ||||
| import { Suspense, useState, useEffect, useCallback, useRef } from 'react'; | ||||
| import { useSuspenseQuery } from '@tanstack/react-query'; | ||||
| @@ -12,6 +11,7 @@ import { MapaProvincial } from './MapaProvincial'; | ||||
| import { CabaLupa } from './CabaLupa'; | ||||
| import { BiZoomIn, BiZoomOut } from "react-icons/bi"; | ||||
| import toast from 'react-hot-toast'; | ||||
| import { useMediaQuery } from '../hooks/useMediaQuery'; | ||||
|  | ||||
| const DEFAULT_MAP_COLOR = '#E0E0E0'; | ||||
| const FADED_BACKGROUND_COLOR = '#F0F0F0'; | ||||
| @@ -19,15 +19,21 @@ const normalizarTexto = (texto: string = '') => texto.trim().toUpperCase().norma | ||||
|  | ||||
| type PointTuple = [number, number]; | ||||
|  | ||||
| const PROVINCE_VIEW_CONFIG: Record<string, { center: PointTuple; zoom: number }> = { | ||||
|   "BUENOS AIRES": { center: [-60.5, -37.3], zoom: 5 }, | ||||
|   "SANTA CRUZ": { center: [-69.5, -49.3], zoom: 5 }, | ||||
|   "CIUDAD AUTONOMA DE BUENOS AIRES": { center: [-58.45, -34.6], zoom: 85 }, | ||||
|   "CHUBUT": { center: [-68.5, -44.5], zoom: 5.5 }, | ||||
|   "SANTA FE": { center: [-61, -31.2], zoom: 6 }, | ||||
|   "CORRIENTES": { center: [-58, -29], zoom: 7 }, | ||||
|   "RIO NEGRO": { center: [-67.5, -40], zoom: 5.5 }, | ||||
|   "TIERRA DEL FUEGO": { center: [-66.5, -54.2], zoom: 7 }, | ||||
| interface ViewConfig { | ||||
|   center: PointTuple; | ||||
|   zoom: number; | ||||
| } | ||||
|  | ||||
| const PROVINCE_VIEW_CONFIG: Record<string, { desktop: ViewConfig; mobile?: ViewConfig }> = { | ||||
|   "BUENOS AIRES": { desktop: { center: [-60.5, -37.3], zoom: 5 }, mobile: { center: [-60, -38], zoom: 5.5 } }, | ||||
|   "SANTA CRUZ": { desktop: { center: [-69.5, -49.3], zoom: 5 }, mobile: { center: [-69.5, -50], zoom: 4 } }, | ||||
|   "CIUDAD AUTONOMA DE BUENOS AIRES": { desktop: { center: [-58.44, -34.65], zoom: 150 } }, | ||||
|   "CHUBUT": { desktop: { center: [-68.5, -44.5], zoom: 5.5 }, mobile: { center: [-68, -44.5], zoom: 4.5 } }, | ||||
|   "SANTA FE": { desktop: { center: [-61, -31.2], zoom: 6 }, mobile: { center: [-61, -31.5], zoom: 7.5 } }, | ||||
|   "CORRIENTES": { desktop: { center: [-58, -29], zoom: 7 }, mobile: { center: [-57.5, -28.8], zoom: 9 } }, | ||||
|   "RIO NEGRO": { desktop: { center: [-67.5, -40], zoom: 5.5 }, mobile: { center: [-67.5, -40], zoom: 4.3 } }, | ||||
|   "SALTA": { desktop: { center: [-64.5, -24], zoom: 7 }, mobile: { center: [-65.5, -24.5], zoom: 6 } }, | ||||
|   "TIERRA DEL FUEGO": { desktop: { center: [-66.5, -54.2], zoom: 7 }, mobile: { center: [-66, -54], zoom: 7.5 } }, | ||||
| }; | ||||
|  | ||||
| const LUPA_SIZE_RATIO = 0.2; | ||||
| @@ -48,15 +54,20 @@ interface MapaNacionalProps { | ||||
|  | ||||
| // --- CONFIGURACIONES DEL MAPA --- | ||||
| const desktopProjectionConfig = { scale: 700, center: [-65, -40] as [number, number] }; | ||||
| const mobileProjectionConfig = { scale: 1100, center: [-64, -41] as [number, number] }; | ||||
| const mobileProjectionConfig = { scale: 1100, center: [-64, -42.5] as [number, number] }; | ||||
| // --- LÍNEA A CALIBRAR --- | ||||
| const mobileSmallProjectionConfig = { scale: 900, center: [-64, -43] as [number, number] }; | ||||
|  | ||||
|  | ||||
| export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nombreProvinciaActiva, provinciaDistritoId, onAmbitoSelect, onVolver, isMobileView }: MapaNacionalProps) => { | ||||
|   const isMobileSmall = useMediaQuery('(max-width: 380px)'); | ||||
|  | ||||
|   const [position, setPosition] = useState({ | ||||
|     zoom: isMobileView ? 1.5 : 1.05, // 1.5 para móvil, 1.05 para desktop | ||||
|     center: [-65, -40] as PointTuple | ||||
|     zoom: isMobileView ? 1.5 : 1.05, | ||||
|     center: isMobileView ? mobileProjectionConfig.center : desktopProjectionConfig.center as PointTuple | ||||
|   }); | ||||
|   const [isPanning, setIsPanning] = useState(false); | ||||
|   const initialProvincePositionRef = useRef<{ zoom: number, center: PointTuple } | null>(null); | ||||
|   const initialProvincePositionRef = useRef<ViewConfig | null>(null); | ||||
|  | ||||
|   const containerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const lupaRef = useRef<HTMLDivElement | null>(null); | ||||
| @@ -82,32 +93,38 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (nivel === 'pais') { | ||||
|       const currentMobileConfig = isMobileSmall ? mobileSmallProjectionConfig : mobileProjectionConfig; | ||||
|       const currentMobileZoom = isMobileSmall ? 1.4 : 1.5; | ||||
|  | ||||
|       setPosition({ | ||||
|         zoom: isMobileView ? 1.4 : 1.05, | ||||
|         center: [-65, -40] | ||||
|         zoom: isMobileView ? currentMobileZoom : 1.05, | ||||
|         center: isMobileView ? currentMobileConfig.center : desktopProjectionConfig.center | ||||
|       }); | ||||
|       initialProvincePositionRef.current = null; | ||||
|     } else if (nivel === 'provincia') { | ||||
|       const nombreNormalizado = normalizarTexto(nombreAmbito); | ||||
|       const manualConfig = PROVINCE_VIEW_CONFIG[nombreNormalizado]; | ||||
|  | ||||
|       let provinceConfig = { zoom: 7, center: [-65, -40] as PointTuple }; | ||||
|        | ||||
|       let provinceConfig: ViewConfig | undefined; | ||||
|  | ||||
|       if (manualConfig) { | ||||
|         provinceConfig = manualConfig; | ||||
|         provinceConfig = (isMobileView && manualConfig.mobile) ? manualConfig.mobile : manualConfig.desktop; | ||||
|       } else { | ||||
|         const provinciaGeo = geoDataNacional.objects.provincias.geometries.find((g: any) => normalizarTexto(g.properties.nombre) === nombreNormalizado); | ||||
|         if (provinciaGeo) { | ||||
|           const provinciaFeature = feature(geoDataNacional, provinciaGeo); | ||||
|           const centroid = geoCentroid(provinciaFeature); | ||||
|           provinceConfig = { zoom: 7, center: centroid as PointTuple }; | ||||
|           provinceConfig = { zoom: isMobileView ? 8 : 7, center: centroid as PointTuple }; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       setPosition(provinceConfig); | ||||
|       initialProvincePositionRef.current = provinceConfig; | ||||
|       if (provinceConfig) { | ||||
|           setPosition(provinceConfig); | ||||
|           initialProvincePositionRef.current = provinceConfig; | ||||
|       } | ||||
|     } | ||||
|   }, [nivel, nombreAmbito, geoDataNacional, isMobileView]); | ||||
|   }, [nivel, nombreAmbito, geoDataNacional, isMobileView, isMobileSmall]); | ||||
|  | ||||
|  | ||||
|   const resultadosNacionalesPorNombre = new Map<string, ResultadoMapaDto>(mapaDataNacional.map(d => [normalizarTexto(d.ambitoNombre), d])); | ||||
|   const nombreMunicipioSeleccionado = nivel === 'municipio' ? nombreAmbito : null; | ||||
| @@ -173,14 +190,9 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|     position.zoom > initialProvincePositionRef.current.zoom && | ||||
|     !nombreMunicipioSeleccionado; | ||||
|  | ||||
|   // --- INICIO DE LA CORRECCIÓN --- | ||||
|  | ||||
|   const handleZoomIn = () => { | ||||
|     // Solo mostramos la notificación si el paneo NO está ya habilitado | ||||
|     if (!panEnabled && initialProvincePositionRef.current) { | ||||
|       // Calculamos cuál será el nuevo nivel de zoom | ||||
|       const newZoom = position.zoom * 1.8; | ||||
|       // Si el nuevo zoom supera el umbral inicial, activamos la notificación | ||||
|       if (newZoom > initialProvincePositionRef.current.zoom) { | ||||
|         toast.success('Desplazamiento Habilitado', { | ||||
|           icon: '🖐️', | ||||
| @@ -193,10 +205,8 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|   }; | ||||
|  | ||||
|   const handleZoomOut = () => { | ||||
|     // Solo mostramos la notificación si el paneo SÍ está habilitado actualmente | ||||
|     if (panEnabled && initialProvincePositionRef.current) { | ||||
|       const newZoom = position.zoom / 1.8; | ||||
|       // Si el nuevo zoom es igual o menor al umbral, desactivamos | ||||
|       if (newZoom <= initialProvincePositionRef.current.zoom) { | ||||
|         toast.error('Desplazamiento Deshabilitado', { | ||||
|           icon: '🔒', | ||||
| @@ -205,7 +215,6 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|     // La lógica para actualizar la posición no cambia | ||||
|     setPosition(prev => { | ||||
|       const newZoom = Math.max(prev.zoom / 1.8, 1); | ||||
|       const initialPos = initialProvincePositionRef.current; | ||||
| @@ -254,7 +263,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|       <div className="mapa-render-area"> | ||||
|         <ComposableMap | ||||
|           projection="geoMercator" | ||||
|           projectionConfig={isMobileView ? mobileProjectionConfig : desktopProjectionConfig} | ||||
|           projectionConfig={isMobileSmall ? mobileSmallProjectionConfig : (isMobileView ? mobileProjectionConfig : desktopProjectionConfig)} | ||||
|           style={{ width: "100%", height: "100%" }} | ||||
|         > | ||||
|           <ZoomableGroup | ||||
|   | ||||
| @@ -0,0 +1,69 @@ | ||||
| // src/features/legislativas/nacionales/components/MunicipioSearch.tsx | ||||
| import { useState } from 'react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import Select, { type SingleValue } from 'react-select'; | ||||
| import { getMunicipiosPorDistrito } from '../../../../apiService'; | ||||
| import type { CatalogoItem } from '../../../../types/types'; | ||||
|  | ||||
| interface MunicipioSearchProps { | ||||
|     distritoId: string; | ||||
|     onMunicipioSelect: (municipioId: string, municipioNombre: string) => void; | ||||
| } | ||||
|  | ||||
| interface OptionType { | ||||
|     value: string; | ||||
|     label: string; | ||||
| } | ||||
|  | ||||
| const customSelectStyles = { | ||||
|     control: (base: any) => ({ | ||||
|         ...base, | ||||
|         borderRadius: '8px', | ||||
|         borderColor: '#e0e0e0', | ||||
|         boxShadow: 'none', | ||||
|         '&:hover': { borderColor: '#007bff' } | ||||
|     }), | ||||
|     menu: (base: any) => ({ | ||||
|         ...base, | ||||
|         borderRadius: '8px', | ||||
|         zIndex: 30 | ||||
|     }) | ||||
| }; | ||||
|  | ||||
| export const MunicipioSearch = ({ distritoId, onMunicipioSelect }: MunicipioSearchProps) => { | ||||
|     const [selectedOption, setSelectedOption] = useState<SingleValue<OptionType>>(null); | ||||
|  | ||||
|     const { data: municipios = [], isLoading } = useQuery<CatalogoItem[]>({ | ||||
|         queryKey: ['municipiosPorDistrito', distritoId], | ||||
|         queryFn: () => getMunicipiosPorDistrito(distritoId), | ||||
|         enabled: !!distritoId, | ||||
|     }); | ||||
|  | ||||
|     const options: OptionType[] = municipios.map(m => ({ | ||||
|         value: m.id, | ||||
|         label: m.nombre | ||||
|     })); | ||||
|  | ||||
|     const handleChange = (selected: SingleValue<OptionType>) => { | ||||
|         if (selected) { | ||||
|             onMunicipioSelect(selected.value, selected.label); | ||||
|             setSelectedOption(null); // Resetea el selector para que muestre el placeholder de nuevo | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div className="municipio-search-container"> | ||||
|             <Select | ||||
|                 options={options} | ||||
|                 onChange={handleChange} | ||||
|                 value={selectedOption} | ||||
|                 isLoading={isLoading} | ||||
|                 placeholder="Buscar municipio..." | ||||
|                 isClearable | ||||
|                 isSearchable | ||||
|                 styles={customSelectStyles} | ||||
|                 noOptionsMessage={() => 'No se encontraron municipios'} | ||||
|             /> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -12,7 +12,6 @@ const formatVotes = (num: number) => Math.round(num).toLocaleString('es-AR'); | ||||
| const SvgDefs = () => ( | ||||
|   <svg style={{ height: 0, width: 0, position: 'absolute' }}> | ||||
|     <defs> | ||||
|       {/* El gradiente ahora se define para que el color oscuro se mantenga en la segunda mitad del recorrido vertical */} | ||||
|       <linearGradient id="participationGradient" gradientTransform="rotate(90)"> | ||||
|         <stop offset="0%" stopColor="#e0f3ffff" /> | ||||
|         <stop offset="100%" stopColor="#007bff" /> | ||||
| @@ -40,13 +39,13 @@ export const PanelResultados = ({ resultados, estadoRecuento }: PanelResultadosP | ||||
|             value={estadoRecuento.participacionPorcentaje} | ||||
|             text={formatPercent(estadoRecuento.participacionPorcentaje)} | ||||
|             strokeWidth={12} | ||||
|             circleRatio={0.75} /* Se convierte en un arco de 270 grados */ | ||||
|             circleRatio={0.75} | ||||
|             styles={buildStyles({ | ||||
|               textColor: '#333', | ||||
|               pathColor: 'url(#participationGradient)', | ||||
|               trailColor: '#e9ecef', | ||||
|               textSize: '22px', | ||||
|               rotation: 0.625, /* Rota el inicio para que la apertura quede abajo */ | ||||
|               rotation: 0.625, | ||||
|             })} | ||||
|           /> | ||||
|           <span>Participación</span> | ||||
| @@ -56,13 +55,13 @@ export const PanelResultados = ({ resultados, estadoRecuento }: PanelResultadosP | ||||
|             value={estadoRecuento.mesasTotalizadasPorcentaje} | ||||
|             text={formatPercent(estadoRecuento.mesasTotalizadasPorcentaje)} | ||||
|             strokeWidth={12} | ||||
|             circleRatio={0.75} /* Se convierte en un arco de 270 grados */ | ||||
|             circleRatio={0.75} | ||||
|             styles={buildStyles({ | ||||
|               textColor: '#333', | ||||
|               pathColor: 'url(#scrutinizedGradient)', | ||||
|               trailColor: '#e9ecef', | ||||
|               textSize: '22px', | ||||
|               rotation: 0.625, /* Rota el inicio para que la apertura quede abajo */ | ||||
|               rotation: 0.625, | ||||
|             })} | ||||
|           /> | ||||
|           <span>Escrutado</span> | ||||
| @@ -76,7 +75,7 @@ export const PanelResultados = ({ resultados, estadoRecuento }: PanelResultadosP | ||||
|             className="partido-fila" | ||||
|             style={{ borderLeftColor: partido.color || '#ccc' }} | ||||
|           > | ||||
|             <div className="partido-logo"> | ||||
|             <div className="partido-logo" style={{ backgroundColor: partido.color || '#e9ecef' }}> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} /> | ||||
|             </div> | ||||
|             <div className="partido-main-content"> | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import { MiniMapaSvg } from './MiniMapaSvg'; | ||||
| import { ImageWithFallback } from '../../../../components/common/ImageWithFallback'; | ||||
| import { assetBaseUrl } from '../../../../apiService'; | ||||
|  | ||||
| // --- 1. AÑADIR LA PROP A AMBAS INTERFACES --- | ||||
| interface CategoriaDisplayProps { | ||||
|     categoria: CategoriaResumen; | ||||
|     mostrarBancas?: boolean; | ||||
| @@ -18,7 +17,6 @@ interface ProvinciaCardProps { | ||||
| const formatNumber = (num: number) => num.toLocaleString('es-AR'); | ||||
| const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`; | ||||
|  | ||||
| // --- 2. RECIBIR Y USAR LA PROP EN EL SUB-COMPONENTE --- | ||||
| const CategoriaDisplay = ({ categoria, mostrarBancas }: CategoriaDisplayProps) => { | ||||
|     return ( | ||||
|         <div className="categoria-bloque"> | ||||
| @@ -30,20 +28,26 @@ const CategoriaDisplay = ({ categoria, mostrarBancas }: CategoriaDisplayProps) = | ||||
|                     className="candidato-row" | ||||
|                     style={{ borderLeftColor: res.color || '#ccc' }} | ||||
|                 > | ||||
|                     <ImageWithFallback | ||||
|                         src={res.fotoUrl ?? undefined} | ||||
|                         fallbackSrc={`${assetBaseUrl}/default-avatar.png`} | ||||
|                         alt={res.nombreCandidato ?? res.nombreAgrupacion} | ||||
|                         className="candidato-foto" | ||||
|                     /> | ||||
|                     {/* --- INICIO DE LA MODIFICACIÓN --- */} | ||||
|                     <div className="candidato-foto-wrapper" style={{ backgroundColor: res.color || '#e9ecef' }}> | ||||
|                         <ImageWithFallback | ||||
|                             src={res.fotoUrl ?? undefined} | ||||
|                             fallbackSrc={`${assetBaseUrl}/default-avatar.png`} | ||||
|                             alt={res.nombreCandidato ?? res.nombreAgrupacion} | ||||
|                             className="candidato-foto" | ||||
|                         /> | ||||
|                     </div> | ||||
|                     {/* --- FIN DE LA MODIFICACIÓN --- */} | ||||
|  | ||||
|                     <div className="candidato-data"> | ||||
|                         {res.nombreCandidato && ( | ||||
|                             <span className="candidato-nombre">{res.nombreCandidato}</span> | ||||
|                         {res.nombreCandidato ? ( | ||||
|                             <> | ||||
|                                 <span className="candidato-nombre">{res.nombreCandidato}</span> | ||||
|                                 <span className="candidato-partido">{res.nombreCortoAgrupacion || res.nombreAgrupacion}</span> | ||||
|                             </> | ||||
|                         ) : ( | ||||
|                             <span className="candidato-nombre">{res.nombreCortoAgrupacion || res.nombreAgrupacion}</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> | ||||
| @@ -53,8 +57,6 @@ const CategoriaDisplay = ({ categoria, mostrarBancas }: CategoriaDisplayProps) = | ||||
|                         <span className="stats-votos">{formatNumber(res.votos)} votos</span> | ||||
|                     </div> | ||||
|  | ||||
|                     {/* --- 3. RENDERIZADO CONDICIONAL DEL CUADRO DE BANCAS --- */} | ||||
|                     {/* Este div solo se renderizará si mostrarBancas es true */} | ||||
|                     {mostrarBancas && ( | ||||
|                         <div className="stats-bancas"> | ||||
|                             +{res.bancasObtenidas} | ||||
| @@ -82,7 +84,6 @@ const CategoriaDisplay = ({ categoria, mostrarBancas }: CategoriaDisplayProps) = | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| // --- 4. RECIBIR Y PASAR LA PROP EN EL COMPONENTE PRINCIPAL --- | ||||
| export const ProvinciaCard = ({ data, mostrarBancas }: ProvinciaCardProps) => { | ||||
|     const colorGanador = data.categorias[0]?.resultados[0]?.color || '#d1d1d1'; | ||||
|  | ||||
| @@ -101,7 +102,7 @@ export const ProvinciaCard = ({ data, mostrarBancas }: ProvinciaCardProps) => { | ||||
|                     <CategoriaDisplay | ||||
|                         key={categoria.categoriaId} | ||||
|                         categoria={categoria} | ||||
|                         mostrarBancas={mostrarBancas} // Pasar la prop hacia abajo | ||||
|                         mostrarBancas={mostrarBancas} | ||||
|                     /> | ||||
|                 ))} | ||||
|             </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user