diff --git a/Elecciones-Web/frontend/src/apiService.ts b/Elecciones-Web/frontend/src/apiService.ts index 8875ec3..da5281b 100644 --- a/Elecciones-Web/frontend/src/apiService.ts +++ b/Elecciones-Web/frontend/src/apiService.ts @@ -289,6 +289,11 @@ export const getResumenPorProvincia = async (eleccionId: number, params: Resumen return data; }; +export const getMunicipiosPorDistrito = async (distritoId: string): Promise => { + const response = await apiClient.get(`/catalogos/municipios-por-distrito/${distritoId}`); + return response.data; +}; + export const getHomeResumen = async (eleccionId: number, distritoId: string, categoriaId: number): Promise => { const queryParams = new URLSearchParams({ eleccionId: eleccionId.toString(), diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.css b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.css index 46cfbc4..287d455 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.css +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.css @@ -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 */ } } \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx index 6be3407..3f43eb5 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx @@ -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 }) => ( +
+
+ +
+
+ {partido.nombreCandidato ? ( + <> + {partido.nombreCandidato} + {partido.nombreCorto || partido.nombre} + + ) : ( + {partido.nombreCorto || partido.nombre} + )} +
+
+ {formatPercent(partido.porcentaje)} + {partido.votos.toLocaleString('es-AR')} +
+
+); + +// --- 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({ + 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 ( +
+ {/* Sección Colapsable con Resultados */} +
+
setIsExpanded(!isExpanded)}> +
+

{ambitoNombre}

+ {/* Se añade una clase para estilizar este texto específicamente */} + {isExpanded ? 'Ocultar resultados' : 'Ver top 4'} +
+
+ {isExpanded ? : } +
+
+
+ {topResults.length > 0 ? ( + topResults.map(partido => ) + ) : ( +

No hay resultados para esta selección.

+ )} +
+
+ + {/* Footer Fijo con Botones de Navegación */} +
+ + +
+
+ ); +}; + +// --- WIDGET PRINCIPAL --- interface PanelNacionalWidgetProps { eleccionId: number; } @@ -42,7 +155,6 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => const [categoriaId, setCategoriaId] = useState(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} /> - + + {/* --- 2. NUEVO CONTENEDOR PARA BREADCRUMBS Y BUSCADOR --- */} +
+ + {/* --- 3. RENDERIZADO CONDICIONAL DEL BUSCADOR --- */} + {ambitoActual.nivel === 'provincia' && ambitoActual.provinciaDistritoId && ( + + handleAmbitoSelect(municipioId, 'municipio', municipioNombre) + } + /> + )}
- + + }> - +
}> - +
-
- {/* --- NUEVO CONTROLADOR DE VISTA PARA MÓVIL --- */} -
- - -
+ + {isMobile && ( + + )} + + ); }; \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.css b/Elecciones-Web/frontend/src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.css index 48a6a62..5573059 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.css +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.css @@ -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 { diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/components/MapaNacional.tsx b/Elecciones-Web/frontend/src/features/legislativas/nacionales/components/MapaNacional.tsx index a695354..2803e43 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/components/MapaNacional.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/components/MapaNacional.tsx @@ -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 = { - "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 = { + "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(null); const containerRef = useRef(null); const lupaRef = useRef(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(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
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>(null); + + const { data: municipios = [], isLoading } = useQuery({ + queryKey: ['municipiosPorDistrito', distritoId], + queryFn: () => getMunicipiosPorDistrito(distritoId), + enabled: !!distritoId, + }); + + const options: OptionType[] = municipios.map(m => ({ + value: m.id, + label: m.nombre + })); + + const handleChange = (selected: SingleValue) => { + if (selected) { + onMunicipioSelect(selected.value, selected.label); + setSelectedOption(null); // Resetea el selector para que muestre el placeholder de nuevo + } + }; + + return ( +
+