Fix Mapa Error (Sección Sin Datos)
This commit is contained in:
		| @@ -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 }) => ( | ||||
|     <div className={styles.mobileResultRow} style={{ borderLeftColor: partido.color || '#ccc' }}> | ||||
|         <div className={styles.mobileResultLogo} style={{ backgroundColor: partido.color || '#e9ecef' }}> | ||||
|              <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} /> | ||||
|         </div> | ||||
|         <div className={styles.mobileResultInfo}> | ||||
|             {partido.nombreCandidato ? ( | ||||
|                 <> | ||||
|                     <span className={styles.mobileResultPartyName}>{partido.nombreCandidato}</span> | ||||
|                     <span className={styles.mobileResultCandidateName}>{partido.nombreCorto || partido.nombre}</span> | ||||
|                 </> | ||||
|             ) : ( | ||||
|                 <span className={styles.mobileResultPartyName}>{partido.nombreCorto || partido.nombre}</span> | ||||
|             )} | ||||
|         </div> | ||||
|         <div className={styles.mobileResultStats}> | ||||
|             <strong>{formatPercent(partido.porcentaje)}</strong> | ||||
|             <span>{partido.votos.toLocaleString('es-AR')}</span> | ||||
|         </div> | ||||
|   <div className={styles.mobileResultRow} style={{ borderLeftColor: partido.color || '#ccc' }}> | ||||
|     <div className={styles.mobileResultLogo} style={{ backgroundColor: partido.color || '#e9ecef' }}> | ||||
|       <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} /> | ||||
|     </div> | ||||
|     <div className={styles.mobileResultInfo}> | ||||
|       {partido.nombreCandidato ? ( | ||||
|         <> | ||||
|           <span className={styles.mobileResultPartyName}>{partido.nombreCandidato}</span> | ||||
|           <span className={styles.mobileResultCandidateName}>{partido.nombreCorto || partido.nombre}</span> | ||||
|         </> | ||||
|       ) : ( | ||||
|         <span className={styles.mobileResultPartyName}>{partido.nombreCorto || partido.nombre}</span> | ||||
|       )} | ||||
|     </div> | ||||
|     <div className={styles.mobileResultStats}> | ||||
|       <strong>{formatPercent(partido.porcentaje)}</strong> | ||||
|       <span>{partido.votos.toLocaleString('es-AR')}</span> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
|  | ||||
| // --- 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<PanelElectoralDto>({ | ||||
|         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<PanelElectoralDto>({ | ||||
|     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 ( | ||||
|         <div className={cardClasses}> | ||||
|             {/* Sección Colapsable con Resultados */} | ||||
|             <div className={styles.collapsibleSection}> | ||||
|                 <div className={styles.mobileResultsHeader} onClick={() => setIsExpanded(!isExpanded)}> | ||||
|                     <div className={styles.headerInfo}> | ||||
|                         <h4>{ambitoNombre}</h4> | ||||
|                         <span className={styles.headerActionText}>{isExpanded ? 'Ocultar resultados' : 'Ver top 3'}</span> | ||||
|                     </div> | ||||
|                     <div className={styles.headerToggleIcon}> | ||||
|                         {isExpanded ? <FiChevronDown /> : <FiChevronUp />} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div className={styles.mobileResultsContent}> | ||||
|                     {topResults.length > 0 ? ( | ||||
|                         topResults.map(partido => <ResultRow key={partido.id} partido={partido} />) | ||||
|                     ) : ( | ||||
|                         <p className={styles.noResultsText}>No hay resultados para esta selección.</p> | ||||
|                     )} | ||||
|                 </div> | ||||
|             </div> | ||||
|   if (topResults.length === 0 && ambitoNivel === 'pais') { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|             {/* Footer Fijo con Botones de Navegación */} | ||||
|             <div className={styles.mobileCardViewToggle}> | ||||
|                 <button | ||||
|                     className={`${styles.toggleBtn} ${mobileView === 'mapa' ? styles.active : ''}`} | ||||
|                     onClick={() => setMobileView('mapa')} | ||||
|                 > | ||||
|                     <FiMap /> | ||||
|                     <span>Mapa</span> | ||||
|                 </button> | ||||
|                 <button | ||||
|                     className={`${styles.toggleBtn} ${mobileView === 'resultados' ? styles.active : ''}`} | ||||
|                     onClick={() => setMobileView('resultados')} | ||||
|                 > | ||||
|                     <FiList /> | ||||
|                     <span>Detalles</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|   // 3. Clases condicionales también se construyen con el objeto 'styles' | ||||
|   const cardClasses = [ | ||||
|     styles.mobileResultsCardContainer, | ||||
|     isExpanded ? styles.expanded : '', | ||||
|     styles[`view-${mobileView}`] | ||||
|   ].join(' '); | ||||
|  | ||||
|   return ( | ||||
|     <div className={cardClasses}> | ||||
|       {/* Sección Colapsable con Resultados */} | ||||
|       <div className={styles.collapsibleSection}> | ||||
|         <div className={styles.mobileResultsHeader} onClick={() => setIsExpanded(!isExpanded)}> | ||||
|           <div className={styles.headerInfo}> | ||||
|             <h4>{ambitoNombre}</h4> | ||||
|             <span className={styles.headerActionText}>{isExpanded ? 'Ocultar resultados' : 'Ver top 3'}</span> | ||||
|           </div> | ||||
|           <div className={styles.headerToggleIcon}> | ||||
|             {isExpanded ? <FiChevronDown /> : <FiChevronUp />} | ||||
|           </div> | ||||
|         </div> | ||||
|     ); | ||||
|         <div className={styles.mobileResultsContent}> | ||||
|           {topResults.length > 0 ? ( | ||||
|             topResults.map(partido => <ResultRow key={partido.id} partido={partido} />) | ||||
|           ) : ( | ||||
|             <p className={styles.noResultsText}>No hay resultados para esta selección.</p> | ||||
|           )} | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       {/* Footer Fijo con Botones de Navegación */} | ||||
|       <div className={styles.mobileCardViewToggle}> | ||||
|         <button | ||||
|           className={`${styles.toggleBtn} ${mobileView === 'mapa' ? styles.active : ''}`} | ||||
|           onClick={() => setMobileView('mapa')} | ||||
|         > | ||||
|           <FiMap /> | ||||
|           <span>Mapa</span> | ||||
|         </button> | ||||
|         <button | ||||
|           className={`${styles.toggleBtn} ${mobileView === 'resultados' ? styles.active : ''}`} | ||||
|           onClick={() => setMobileView('resultados')} | ||||
|         > | ||||
|           <FiList /> | ||||
|           <span>Detalles</span> | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| // --- 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 ( | ||||
|       <div style={{ padding: '2rem', textAlign: 'center', color: '#666' }}> | ||||
|         <h4>Sin Resultados Detallados</h4> | ||||
|         <p>Aún no hay datos disponibles para esta selección.</p> | ||||
|         <p>Por favor, intente de nuevo más tarde.</p> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|   // Si no, renderizamos los resultados. | ||||
|   return <PanelResultados resultados={data.resultadosPanel} estadoRecuento={data.estadoRecuento} />; | ||||
| }; | ||||
|  | ||||
| export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => { | ||||
|   const queryClient = useQueryClient(); | ||||
|   const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null }); | ||||
|   const [categoriaId, setCategoriaId] = useState<number>(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: <b>Datos cargados</b>, | ||||
|           error: <b>No se pudieron cargar los datos.</b>, | ||||
|         } | ||||
|       ); | ||||
|     } | ||||
|     setAmbitoActual(prev => ({ | ||||
|       id: nuevoAmbitoId, | ||||
|       nivel: nuevoNivel, | ||||
| @@ -208,41 +231,43 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.panelNacionalContainer}> | ||||
|       <Toaster containerClassName={styles.widgetToasterContainer} /> | ||||
|       <Toaster | ||||
|         position="bottom-center" | ||||
|         containerClassName={styles.widgetToasterContainer} | ||||
|       /> | ||||
|       <header className={styles.panelHeader}> | ||||
|         <div className={styles.headerTopRow}> | ||||
|           <Select | ||||
|             options={CATEGORIAS_NACIONALES} | ||||
|             value={selectedCategoria} | ||||
|             onChange={(option) => option && setCategoriaId(option.value)} | ||||
|             // 4. Usamos un prefijo de clase simple que se asociará con las clases del módulo CSS | ||||
|             classNamePrefix="categoriaSelector" | ||||
|             className={styles.categoriaSelectorContainer} | ||||
|             isSearchable={false} | ||||
|           /> | ||||
|         </div> | ||||
|         <div className={styles.headerBottomRow}> | ||||
|             <Breadcrumbs | ||||
|                 nivel={ambitoActual.nivel} | ||||
|                 nombreAmbito={ambitoActual.nombre} | ||||
|                 nombreProvincia={ambitoActual.provinciaNombre} | ||||
|                 onReset={handleResetToPais} | ||||
|                 onVolverProvincia={handleVolverAProvincia} | ||||
|           <Breadcrumbs | ||||
|             nivel={ambitoActual.nivel} | ||||
|             nombreAmbito={ambitoActual.nombre} | ||||
|             nombreProvincia={ambitoActual.provinciaNombre} | ||||
|             onReset={handleResetToPais} | ||||
|             onVolverProvincia={handleVolverAProvincia} | ||||
|           /> | ||||
|           {ambitoActual.nivel === 'provincia' && ambitoActual.provinciaDistritoId && ( | ||||
|             <MunicipioSearch | ||||
|               distritoId={ambitoActual.provinciaDistritoId} | ||||
|               onMunicipioSelect={(municipioId, municipioNombre) => | ||||
|                 handleAmbitoSelect(municipioId, 'municipio', municipioNombre) | ||||
|               } | ||||
|             /> | ||||
|             {ambitoActual.nivel === 'provincia' && ambitoActual.provinciaDistritoId && ( | ||||
|                 <MunicipioSearch  | ||||
|                     distritoId={ambitoActual.provinciaDistritoId} | ||||
|                     onMunicipioSelect={(municipioId, municipioNombre) =>  | ||||
|                         handleAmbitoSelect(municipioId, 'municipio', municipioNombre) | ||||
|                     } | ||||
|                 /> | ||||
|             )} | ||||
|           )} | ||||
|         </div> | ||||
|       </header> | ||||
|       <main className={mainContentClasses}> | ||||
|         <div className={styles.mapaColumn}> | ||||
|           <button className={styles.panelToggleBtn} onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}> {isPanelOpen ? '›' : '‹'} </button> | ||||
|            | ||||
|  | ||||
|           <Suspense fallback={<div className={styles.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} /> | ||||
|           </Suspense> | ||||
| @@ -254,17 +279,17 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|         </div> | ||||
|  | ||||
|         <Suspense fallback={null}> | ||||
|             {isMobile && ( | ||||
|               <MobileResultsCard  | ||||
|                 eleccionId={eleccionId} | ||||
|                 ambitoId={ambitoActual.id} | ||||
|                 categoriaId={categoriaId} | ||||
|                 ambitoNombre={ambitoActual.nombre} | ||||
|                 ambitoNivel={ambitoActual.nivel} | ||||
|                 mobileView={mobileView} | ||||
|                 setMobileView={setMobileView} | ||||
|               /> | ||||
|             )} | ||||
|           {isMobile && ( | ||||
|             <MobileResultsCard | ||||
|               eleccionId={eleccionId} | ||||
|               ambitoId={ambitoActual.id} | ||||
|               categoriaId={categoriaId} | ||||
|               ambitoNombre={ambitoActual.nombre} | ||||
|               ambitoNivel={ambitoActual.nivel} | ||||
|               mobileView={mobileView} | ||||
|               setMobileView={setMobileView} | ||||
|             /> | ||||
|           )} | ||||
|         </Suspense> | ||||
|       </main> | ||||
|     </div> | ||||
|   | ||||
| @@ -195,7 +195,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|       if (newZoom > initialProvincePositionRef.current.zoom) { | ||||
|         toast.success('Desplazamiento Habilitado', { | ||||
|           icon: '🖐️', | ||||
|           style: { background: '#32e5f1ff', color: 'white' }, | ||||
|           style: { background: '#32e5f1ff', color: 'white', zIndex: 9999}, | ||||
|           duration: 1000, | ||||
|         }); | ||||
|       } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user