From 92c80f195b3ce21b26333dbb2816bf58f0b1a92c Mon Sep 17 00:00:00 2001 From: dmolinari Date: Fri, 17 Oct 2025 13:55:38 -0300 Subject: [PATCH] =?UTF-8?q?Fix=20Mapa=20Error=20(Secci=C3=B3n=20Sin=20Dato?= =?UTF-8?q?s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Elecciones-Web/frontend/src/apiService.ts | 38 ++- .../nacionales/PanelNacionalWidget.tsx | 241 ++++++++++-------- .../nacionales/components/MapaNacional.tsx | 2 +- Elecciones-Web/frontend/src/types/types.ts | 1 + 4 files changed, 161 insertions(+), 121 deletions(-) diff --git a/Elecciones-Web/frontend/src/apiService.ts b/Elecciones-Web/frontend/src/apiService.ts index da5281b..60d18df 100644 --- a/Elecciones-Web/frontend/src/apiService.ts +++ b/Elecciones-Web/frontend/src/apiService.ts @@ -252,12 +252,26 @@ export const getPanelElectoral = async (eleccionId: number, ambitoId: string | n let url = ambitoId ? `/elecciones/${eleccionId}/panel/${ambitoId}` : `/elecciones/${eleccionId}/panel`; - - // Añadimos categoriaId como un query parameter url += `?categoriaId=${categoriaId}`; - const { data } = await apiClient.get(url); - return data; + try { + const { data } = await apiClient.get(url); + return data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + console.warn(`API devolvió 404 para ${url}. Devolviendo un estado vacío.`); + + // Devolvemos el objeto vacío PERO con la nueva bandera activada + return { + ambitoNombre: 'Sin Datos', + mapaData: [], + resultadosPanel: [], + estadoRecuento: { participacionPorcentaje: 0, mesasTotalizadasPorcentaje: 0 }, + sinDatos: true, + }; + } + throw error; + } }; export const getComposicionNacional = async (eleccionId: number): Promise => { @@ -295,12 +309,12 @@ export const getMunicipiosPorDistrito = async (distritoId: string): Promise => { - const queryParams = new URLSearchParams({ - eleccionId: eleccionId.toString(), - distritoId: distritoId, - categoriaId: categoriaId.toString(), - }); - const url = `/elecciones/home-resumen?${queryParams.toString()}`; - const { data } = await apiClient.get(url); - return data; + const queryParams = new URLSearchParams({ + eleccionId: eleccionId.toString(), + distritoId: distritoId, + categoriaId: categoriaId.toString(), + }); + const url = `/elecciones/home-resumen?${queryParams.toString()}`; + const { data } = await apiClient.get(url); + return data; }; \ 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 da099fc..a19d1ee 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx @@ -13,9 +13,10 @@ import Select from 'react-select'; 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 toast, { Toaster } from 'react-hot-toast'; import { ImageWithFallback } from '../../../components/common/ImageWithFallback'; import { assetBaseUrl } from '../../../apiService'; +import { useQueryClient } from '@tanstack/react-query'; // --- COMPONENTE INTERNO PARA LA TARJETA DE RESULTADOS EN MÓVIL --- interface MobileResultsCardProps { @@ -31,25 +32,25 @@ const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',' // --- SUB-COMPONENTE PARA UNA FILA DE RESULTADO --- // 2. Todas las props 'className' ahora usan el objeto 'styles' 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')} -
+
+
+
+
+ {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 --- @@ -63,75 +64,75 @@ interface MobileResultsCardProps { setMobileView: (view: 'mapa' | 'resultados') => void; } -const MobileResultsCard = ({ - eleccionId, ambitoId, categoriaId, ambitoNombre, ambitoNivel, mobileView, setMobileView +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 [isExpanded, setIsExpanded] = useState(false); - const topResults = data.resultadosPanel.slice(0, 3); + const { data } = useSuspenseQuery({ + queryKey: ['panelElectoral', eleccionId, ambitoId, categoriaId], + queryFn: () => getPanelElectoral(eleccionId, ambitoId, categoriaId), + }); - if (topResults.length === 0 && ambitoNivel === 'pais') { - return null; - } + useEffect(() => { + setIsExpanded(ambitoNivel === 'municipio'); + }, [ambitoNivel]); - // 3. Clases condicionales también se construyen con el objeto 'styles' - const cardClasses = [ - styles.mobileResultsCardContainer, - isExpanded ? styles.expanded : '', - styles[`view-${mobileView}`] - ].join(' '); + const topResults = data.resultadosPanel.slice(0, 3); - return ( -
- {/* Sección Colapsable con Resultados */} -
-
setIsExpanded(!isExpanded)}> -
-

{ambitoNombre}

- {isExpanded ? 'Ocultar resultados' : 'Ver top 3'} -
-
- {isExpanded ? : } -
-
-
- {topResults.length > 0 ? ( - topResults.map(partido => ) - ) : ( -

No hay resultados para esta selección.

- )} -
-
+ if (topResults.length === 0 && ambitoNivel === 'pais') { + return null; + } - {/* Footer Fijo con Botones de Navegación */} -
- - -
+ // 3. Clases condicionales también se construyen con el objeto 'styles' + const cardClasses = [ + styles.mobileResultsCardContainer, + isExpanded ? styles.expanded : '', + styles[`view-${mobileView}`] + ].join(' '); + + return ( +
+ {/* Sección Colapsable con Resultados */} +
+
setIsExpanded(!isExpanded)}> +
+

{ambitoNombre}

+ {isExpanded ? 'Ocultar resultados' : 'Ver top 3'} +
+
+ {isExpanded ? : } +
- ); +
+ {topResults.length > 0 ? ( + topResults.map(partido => ) + ) : ( +

No hay resultados para esta selección.

+ )} +
+
+ + {/* Footer Fijo con Botones de Navegación */} +
+ + +
+
+ ); }; // --- WIDGET PRINCIPAL --- @@ -157,10 +158,22 @@ const PanelContenido = ({ eleccionId, ambitoActual, categoriaId }: { eleccionId: queryKey: ['panelElectoral', eleccionId, ambitoActual.id, categoriaId], queryFn: () => getPanelElectoral(eleccionId, ambitoActual.id, categoriaId), }); + // Si la API devolvió la bandera 'sinDatos', mostramos un mensaje. + if (data.sinDatos) { + return ( +
+

Sin Resultados Detallados

+

Aún no hay datos disponibles para esta selección.

+

Por favor, intente de nuevo más tarde.

+
+ ); + } + // Si no, renderizamos los resultados. return ; }; export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => { + const queryClient = useQueryClient(); const [ambitoActual, setAmbitoActual] = useState({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null }); const [categoriaId, setCategoriaId] = useState(2); const [isPanelOpen, setIsPanelOpen] = useState(true); @@ -168,6 +181,16 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => const isMobile = useMediaQuery('(max-width: 800px)'); const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => { + if (nuevoNivel === 'municipio') { + toast.promise( + queryClient.invalidateQueries({ queryKey: ['panelElectoral', eleccionId, nuevoAmbitoId, categoriaId] }), + { + loading: `Cargando datos de ${nuevoNombre}...`, + success: Datos cargados, + error: No se pudieron cargar los datos., + } + ); + } setAmbitoActual(prev => ({ id: nuevoAmbitoId, nivel: nuevoNivel, @@ -208,41 +231,43 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => return (
- +