Feat Widgets Carousel Selector de Porv. Fix Tablas Movil
This commit is contained in:
		| @@ -354,3 +354,8 @@ export const getResumenNacionalPorProvincia = async (eleccionId: number, categor | ||||
|   const response = await apiClient.get(`/elecciones/${eleccionId}/resumen-nacional-por-provincia?categoriaId=${categoriaId}`); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| export const getProvincias = async (): Promise<CatalogoItem[]> => { | ||||
|   const response = await apiClient.get('/catalogos/provincias'); | ||||
|   return response.data; | ||||
| }; | ||||
| @@ -9,6 +9,7 @@ import { HomeCarouselNacionalWidget } from './nacionales/HomeCarouselNacionalWid | ||||
| import { TablaConurbanoWidget } from './nacionales/TablaConurbanoWidget'; | ||||
| import { TablaSeccionesWidget } from './nacionales/TablaSeccionesWidget'; | ||||
| import { ResumenNacionalWidget } from './nacionales/ResumenNacionalWidget'; | ||||
| import { HomeCarouselProvincialWidget } from './nacionales/HomeCarouselProvincialWidget'; | ||||
|  | ||||
| // --- NUEVO COMPONENTE REUTILIZABLE PARA CONTENIDO COLAPSABLE --- | ||||
| const CollapsibleWidgetWrapper = ({ children }: { children: React.ReactNode }) => { | ||||
| @@ -93,6 +94,33 @@ export const DevAppLegislativas = () => { | ||||
|                     titulo="Senadores - Total País" mapLinkUrl={''} /> | ||||
|             </div> | ||||
|  | ||||
|             <div style={sectionStyle}> | ||||
|                 <h2>Widget: Carrusel Provincial con Selector (Home)</h2> | ||||
|                 <p style={descriptionStyle}> | ||||
|                     Categoría Diputados | ||||
|                 </p> | ||||
|                 <p style={descriptionStyle}> | ||||
|                     Uso: <code style={codeStyle}><HomeCarouselProvincialWidget eleccionId={2} categoriaId={3} titulo="Diputados" /></code> | ||||
|                 </p> | ||||
|                 <HomeCarouselProvincialWidget | ||||
|                     eleccionId={2} | ||||
|                     categoriaId={3} // 3 para Diputados, 2 para Senadores | ||||
|                     titulo="Diputados" | ||||
|                 /> | ||||
|  | ||||
|                 <p style={descriptionStyle}> | ||||
|                     Categoría Senadores | ||||
|                 </p> | ||||
|                 <p style={descriptionStyle}> | ||||
|                     Uso: <code style={codeStyle}><HomeCarouselProvincialWidget eleccionId={2} categoriaId={2} titulo="Senadores" /></code> | ||||
|                 </p> | ||||
|                 <HomeCarouselProvincialWidget | ||||
|                     eleccionId={2} | ||||
|                     categoriaId={2} // 3 para Diputados, 2 para Senadores | ||||
|                     titulo="Senadores" | ||||
|                 /> | ||||
|             </div> | ||||
|  | ||||
|             {/* --- SECCIÓN PARA EL WIDGET DE TARJETAS CON EJEMPLOS --- */} | ||||
|             <div style={sectionStyle}> | ||||
|                 <h2>Widget: Resultados por Provincia (Tarjetas)</h2> | ||||
|   | ||||
| @@ -57,7 +57,7 @@ export const HomeCarouselNacionalWidget = ({ eleccionId, categoriaId, titulo, ma | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.homeCarouselWidget}> | ||||
|       <div className={styles.widgetHeader}> | ||||
|       <div className={`${styles.widgetHeader} ${styles.headerSingleLine}`}> | ||||
|         <h2 className={styles.widgetTitle}>{titulo}</h2> | ||||
|         <a href={mapLinkUrl} className={styles.mapLinkButton}> | ||||
|           <TfiMapAlt /> | ||||
|   | ||||
| @@ -0,0 +1,201 @@ | ||||
| // src/features/legislativas/nacionales/HomeCarouselProvincialWidget.tsx | ||||
|  | ||||
| import { useState, useMemo, useEffect } from 'react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import Select, { type SingleValue } from 'react-select'; | ||||
| import { getHomeResumen, getProvincias } from '../../../apiService'; | ||||
| import type { CatalogoItem } from '../../../types/types'; | ||||
| import { ImageWithFallback } from '../../../components/common/ImageWithFallback'; | ||||
| import { assetBaseUrl } from '../../../apiService'; | ||||
| import { Swiper, SwiperSlide } from 'swiper/react'; | ||||
| import { Navigation, A11y } from 'swiper/modules'; | ||||
|  | ||||
| // @ts-ignore | ||||
| import 'swiper/css'; | ||||
| // @ts-ignore | ||||
| import 'swiper/css/navigation'; | ||||
| import styles from './HomeCarouselWidget.module.css'; | ||||
|  | ||||
| interface Props { | ||||
|   eleccionId: number; | ||||
|   categoriaId: number; | ||||
|   titulo: string; | ||||
| } | ||||
|  | ||||
| interface OptionType { | ||||
|   value: string; | ||||
|   label: string; | ||||
| } | ||||
|  | ||||
| // --- LÓGICA DE FILTRADO --- | ||||
| // 1. Definimos los IDs de distrito de las provincias que renuevan senadores. | ||||
| const PROVINCIAS_QUE_RENUEVAN_SENADORES = new Set(['01', '06', '08', '15', '16', '17', '22', '24']); | ||||
| const CATEGORIA_SENADORES = 2; | ||||
| // --- FIN LÓGICA DE FILTRADO --- | ||||
|  | ||||
| const formatPercent = (num: number | null | undefined) => `${(num || 0).toFixed(2).replace('.', ',')}%`; | ||||
| const formatNumber = (num: number) => num.toLocaleString('es-AR'); | ||||
| const formatDateTime = (dateString: string | undefined | null) => { | ||||
|   if (!dateString) return '...'; | ||||
|   try { | ||||
|     const date = new Date(dateString); | ||||
|     const day = String(date.getDate()).padStart(2, '0'); | ||||
|     const month = String(date.getMonth() + 1).padStart(2, '0'); | ||||
|     const year = date.getFullYear(); | ||||
|     const hours = String(date.getHours()).padStart(2, '0'); | ||||
|     const minutes = String(date.getMinutes()).padStart(2, '0'); | ||||
|     return `${day}/${month}/${year}, ${hours}:${minutes} hs.`; | ||||
|   } catch (e) { | ||||
|     return dateString; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const HomeCarouselProvincialWidget = ({ eleccionId, categoriaId, titulo }: Props) => { | ||||
|   // 2. Estado inicial nulo para que el useEffect lo establezca dinámicamente. | ||||
|   const [selectedProvince, setSelectedProvince] = useState<OptionType | null>(null); | ||||
|  | ||||
|   const { data: provincias = [], isLoading: isLoadingProvincias } = useQuery<CatalogoItem[]>({ | ||||
|     queryKey: ['provincias'], | ||||
|     queryFn: getProvincias, | ||||
|   }); | ||||
|  | ||||
|   // 3. Usamos useMemo para filtrar las opciones solo cuando sea necesario. | ||||
|   const provinceOptions: OptionType[] = useMemo(() => { | ||||
|     const allOptions = provincias.map(p => ({ value: p.id, label: p.nombre })); | ||||
|     if (categoriaId === CATEGORIA_SENADORES) { | ||||
|       return allOptions.filter(opt => PROVINCIAS_QUE_RENUEVAN_SENADORES.has(opt.value)); | ||||
|     } | ||||
|     return allOptions; | ||||
|   }, [provincias, categoriaId]); | ||||
|  | ||||
|   // 4. useEffect para establecer y validar la provincia por defecto. | ||||
|   useEffect(() => { | ||||
|     if (provinceOptions.length > 0) { | ||||
|       // Si no hay nada seleccionado, establece el default. | ||||
|       if (!selectedProvince) { | ||||
|         const defaultOption = provinceOptions.find(opt => opt.value === '01'); // CABA | ||||
|         setSelectedProvince(defaultOption || provinceOptions[0]); // Si CABA no está, usa la primera opción. | ||||
|       } else { | ||||
|         // Si ya hay algo seleccionado, verifica que siga siendo válido. Si no, lo resetea. | ||||
|         const isSelectedStillValid = provinceOptions.some(opt => opt.value === selectedProvince.value); | ||||
|         if (!isSelectedStillValid) { | ||||
|           setSelectedProvince(provinceOptions[0]); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, [provinceOptions, selectedProvince]); | ||||
|  | ||||
|   const { data, isLoading, error } = useQuery({ | ||||
|     queryKey: ['homeResumen', eleccionId, selectedProvince?.value, categoriaId], | ||||
|     queryFn: () => getHomeResumen(eleccionId, selectedProvince!.value, categoriaId), | ||||
|     enabled: !!selectedProvince, // La consulta solo se ejecuta si hay una provincia seleccionada | ||||
|     refetchInterval: 30000, | ||||
|   }); | ||||
|  | ||||
|   const uniqueId = `swiper-${Math.random().toString(36).substring(2, 9)}`; | ||||
|   const prevButtonClass = `prev-${uniqueId}`; | ||||
|   const nextButtonClass = `next-${uniqueId}`; | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.homeCarouselWidget}> | ||||
|       <div className={styles.widgetHeader}> | ||||
|         <h2 className={styles.widgetTitle}>{`${titulo} - ${selectedProvince?.label || '...'}`}</h2> | ||||
|         <div className={styles.provinceSelector}> | ||||
|           <Select | ||||
|             value={selectedProvince} | ||||
|             options={provinceOptions} | ||||
|             onChange={(option: SingleValue<OptionType>) => option && setSelectedProvince(option)} | ||||
|             isLoading={isLoadingProvincias} | ||||
|             isSearchable={true} | ||||
|             placeholder="Seleccionar provincia..." | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       {(isLoading || !selectedProvince) && <div>Cargando resultados...</div>} | ||||
|       {error && <div>No se pudieron cargar los datos.</div>} | ||||
|       {data && selectedProvince && ( | ||||
|         <> | ||||
|           <div className={styles.carouselContainer}> | ||||
|             <Swiper | ||||
|               modules={[Navigation, A11y]} | ||||
|               spaceBetween={16} | ||||
|               slidesPerView={1.3} | ||||
|               navigation={{ | ||||
|                 prevEl: `.${prevButtonClass}`, | ||||
|                 nextEl: `.${nextButtonClass}`, | ||||
|               }} | ||||
|               breakpoints={{ | ||||
|                 320: { slidesPerView: 1.25, spaceBetween: 10 }, | ||||
|                 430: { slidesPerView: 1.4, spaceBetween: 12 }, | ||||
|                 640: { slidesPerView: 2.5 }, | ||||
|                 1024: { slidesPerView: 3 }, | ||||
|                 1200: { slidesPerView: 3.5 } | ||||
|               }} | ||||
|             > | ||||
|               {data.resultados.map(candidato => ( | ||||
|                 <SwiperSlide key={candidato.agrupacionId}> | ||||
|                   <div className={styles.candidateCard} style={{ '--candidate-color': candidato.color || '#ccc' } as React.CSSProperties}> | ||||
|                     <div className={styles.candidatePhotoWrapper}> | ||||
|                       <ImageWithFallback | ||||
|                         src={candidato.fotoUrl ?? undefined} | ||||
|                         fallbackSrc={`${assetBaseUrl}/default-avatar.png`} | ||||
|                         alt={candidato.nombreCandidato ?? ''} | ||||
|                         className={styles.candidatePhoto} | ||||
|                       /> | ||||
|                     </div> | ||||
|                     <div className={styles.candidateDetails}> | ||||
|                       <div className={styles.candidateInfo}> | ||||
|                         {candidato.nombreCandidato ? ( | ||||
|                           <> | ||||
|                             <span className={styles.candidateName}>{candidato.nombreCandidato}</span> | ||||
|                             <span className={styles.partyName}>{candidato.nombreCortoAgrupacion || candidato.nombreAgrupacion}</span> | ||||
|                           </> | ||||
|                         ) : ( | ||||
|                           <span className={styles.candidateName}>{candidato.nombreCortoAgrupacion || candidato.nombreAgrupacion}</span> | ||||
|                         )} | ||||
|                       </div> | ||||
|                       <div className={styles.candidateResults}> | ||||
|                         <span className={styles.percentage}>{formatPercent(candidato.porcentaje)}</span> | ||||
|                         <span className={styles.votes}>{formatNumber(candidato.votos)} votos</span> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </SwiperSlide> | ||||
|               ))} | ||||
|             </Swiper> | ||||
|  | ||||
|             <div className={`${styles.navButton} ${styles.navButtonPrev} ${prevButtonClass}`}></div> | ||||
|             <div className={`${styles.navButton} ${styles.navButtonNext} ${nextButtonClass}`}></div> | ||||
|           </div> | ||||
|  | ||||
|           <div className={styles.topStatsBar}> | ||||
|             <div> | ||||
|               <span>Participación</span> | ||||
|               <strong>{formatPercent(data.estadoRecuento?.participacionPorcentaje)}</strong> | ||||
|             </div> | ||||
|             <div> | ||||
|               <span className={styles.longText}>Mesas escrutadas</span> | ||||
|               <span className={styles.shortText}>Escrutado</span> | ||||
|               <strong>{formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje)}</strong> | ||||
|             </div> | ||||
|             <div> | ||||
|               <span className={styles.longText}>Votos en blanco</span> | ||||
|               <span className={styles.shortText}>En blanco</span> | ||||
|               <strong>{formatPercent(data.votosEnBlancoPorcentaje)}</strong> | ||||
|             </div> | ||||
|             <div> | ||||
|               <span className={styles.longText}>Votos totales</span> | ||||
|               <span className={styles.shortText}>Votos</span> | ||||
|               <strong>{formatNumber(data.votosTotales)}</strong> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           <div className={styles.widgetFooter}> | ||||
|             Última actualización: {formatDateTime(data.ultimaActualizacion)} | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -49,6 +49,15 @@ | ||||
|     border: none; | ||||
|     text-align: left; | ||||
|     flex-grow: 1; | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
| } | ||||
|  | ||||
| .provinceSelector { | ||||
|     min-width: 180px; | ||||
|     flex-shrink: 0; | ||||
|     z-index: 9999; | ||||
| } | ||||
|  | ||||
| .mapLinkButton { | ||||
| @@ -57,14 +66,15 @@ | ||||
|     gap: 0.4rem; | ||||
|     background-color: #007bff; | ||||
|     color: white; | ||||
|     padding: 0.4rem 1rem; | ||||
|     padding: 0.5rem 1rem; | ||||
|     border-radius: 9999px; | ||||
|     font-size: 0.9rem; | ||||
|     font-weight: 700; | ||||
|     font-size: 0.8rem; | ||||
|     font-weight: 600; | ||||
|     text-decoration: none; | ||||
|     white-space: nowrap; | ||||
|     transition: background-color 0.2s ease, box-shadow 0.2s ease; | ||||
|     box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .mapLinkButton svg { | ||||
| @@ -94,7 +104,7 @@ | ||||
|     margin-top: 0.5rem; | ||||
| } | ||||
|  | ||||
| .topStatsBar > div {  | ||||
| .topStatsBar>div { | ||||
|     display: flex; | ||||
|     align-items: baseline; | ||||
|     gap: 0.5rem; | ||||
| @@ -103,9 +113,21 @@ | ||||
|     flex-grow: 1; | ||||
|     justify-content: center; | ||||
| } | ||||
| .topStatsBar > div:last-child { border-right: none; } | ||||
| .topStatsBar span { font-size: 0.9rem; color: var(--secondary-text); } | ||||
| .topStatsBar strong { font-size: 0.9rem; font-weight: 600; color: var(--primary-text); } | ||||
|  | ||||
| .topStatsBar>div:last-child { | ||||
|     border-right: none; | ||||
| } | ||||
|  | ||||
| .topStatsBar span { | ||||
|     font-size: 0.9rem; | ||||
|     color: var(--secondary-text); | ||||
| } | ||||
|  | ||||
| .topStatsBar strong { | ||||
|     font-size: 0.9rem; | ||||
|     font-weight: 600; | ||||
|     color: var(--primary-text); | ||||
| } | ||||
|  | ||||
| .candidateCard { | ||||
|     display: flex; | ||||
| @@ -149,13 +171,14 @@ | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
|     align-items:flex-start; | ||||
|     align-items: flex-start; | ||||
|     gap: 0.1rem; | ||||
|     min-width: 0; | ||||
|     margin-right: 0.75rem; | ||||
| } | ||||
|  | ||||
| .candidateName, .partyName { | ||||
| .candidateName, | ||||
| .partyName { | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
| @@ -165,17 +188,23 @@ | ||||
|     color: var(--primary-text); | ||||
|     text-align: left; | ||||
| } | ||||
|  | ||||
| .candidateName { | ||||
|     font-size: 0.95rem; | ||||
|     font-weight: 700; | ||||
| } | ||||
|  | ||||
| .partyName { | ||||
|     font-size: 0.8rem; | ||||
|     color: var(--secondary-text); | ||||
|     font-weight: 400; | ||||
| } | ||||
|  | ||||
| .candidateResults { text-align: right; flex-shrink: 0; } | ||||
| .candidateResults { | ||||
|     text-align: right; | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .percentage { | ||||
|     display: block; | ||||
|     font-size: 1.2rem; | ||||
| @@ -183,6 +212,7 @@ | ||||
|     color: var(--primary-text); | ||||
|     line-height: 1.1; | ||||
| } | ||||
|  | ||||
| .votes { | ||||
|     font-size: 0.75rem; | ||||
|     color: var(--secondary-text); | ||||
| @@ -208,7 +238,8 @@ | ||||
|  | ||||
| /* Usamos el pseudo-elemento ::after para mostrar el icono SVG como fondo */ | ||||
| .navButton::after { | ||||
|     content: ''; /* Es necesario para que el pseudo-elemento se muestre */ | ||||
|     content: ''; | ||||
|     /* Es necesario para que el pseudo-elemento se muestre */ | ||||
|     display: block; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
| @@ -222,6 +253,7 @@ | ||||
| .navButtonPrev { | ||||
|     left: -10px; | ||||
| } | ||||
|  | ||||
| .navButtonPrev::after { | ||||
|     /* SVG de flecha izquierda (chevron) codificado en Base64 */ | ||||
|     background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMyMTI1MjkiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWxpbmUgcG9pbnRzPSIxNSA2IDkgMTIgMTUgMTgiPjwvcG9seWxpbmU+PC9zdmc+"); | ||||
| @@ -230,6 +262,7 @@ | ||||
| .navButtonNext { | ||||
|     right: -10px; | ||||
| } | ||||
|  | ||||
| .navButtonNext::after { | ||||
|     /* SVG de flecha derecha (chevron) codificado en Base64 */ | ||||
|     background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMyMTI1MjkiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWxpbmUgcG9pbnRzPSI5IDYgMTUgMTIgOSAxOCI+PC9wb2x5bGluZT48L3N2Zz4="); | ||||
| @@ -285,8 +318,13 @@ | ||||
|     color: var(--primary-text) !important; | ||||
| } | ||||
|  | ||||
| .homeCarouselWidget :global(.swiper-button-prev) { left: 10px !important; } | ||||
| .homeCarouselWidget :global(.swiper-button-next) { right: 10px !important; } | ||||
| .homeCarouselWidget :global(.swiper-button-prev) { | ||||
|     left: 10px !important; | ||||
| } | ||||
|  | ||||
| .homeCarouselWidget :global(.swiper-button-next) { | ||||
|     right: 10px !important; | ||||
| } | ||||
|  | ||||
| .homeCarouselWidget :global(.swiper-button-disabled) { | ||||
|     opacity: 0 !important; | ||||
| @@ -306,15 +344,32 @@ | ||||
|  | ||||
| @media (max-width: 768px) { | ||||
|     .homeCarouselWidget .widgetHeader { | ||||
|         /* Comportamiento por defecto en móvil: apilado y centrado */ | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         gap: 0.75rem; | ||||
|     } | ||||
|  | ||||
|     /* NUEVA CLASE MODIFICADORA para los widgets con botón */ | ||||
|     .homeCarouselWidget .headerSingleLine { | ||||
|         flex-direction: row; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|         gap: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .widgetTitle { | ||||
|         text-align: center; | ||||
|         font-size: 1.1rem; | ||||
|     } | ||||
|  | ||||
|     /* Ajuste para que el título vuelva a la izquierda en la vista de una línea */ | ||||
|     .headerSingleLine .widgetTitle { | ||||
|         text-align: left; | ||||
|         font-size: 1rem;  | ||||
|     } | ||||
|  | ||||
|     .provinceSelector { | ||||
|         min-width: 100%; | ||||
|         z-index: 9999; | ||||
|     } | ||||
|  | ||||
|     .mapLinkButton { | ||||
| @@ -325,31 +380,83 @@ | ||||
|         display: none; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .topStatsBar { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.2rem; padding: 0.3rem; } | ||||
|     .homeCarouselWidget .topStatsBar > div { padding: 0.25rem 0.5rem; border-right: none; } | ||||
|     .homeCarouselWidget .topStatsBar > div:nth-child(odd) { border-right: 1px solid var(--border-color); } | ||||
|     .homeCarouselWidget .longText { display: none; } | ||||
|     .homeCarouselWidget .shortText { display:inline; } | ||||
|     .homeCarouselWidget .topStatsBar span { font-size: 0.8rem; text-align: left; } | ||||
|     .homeCarouselWidget .topStatsBar strong { font-size: 0.85rem; text-align: right; } | ||||
|     .homeCarouselWidget .topStatsBar { | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(2, 1fr); | ||||
|         gap: 0.2rem; | ||||
|         padding: 0.3rem; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .topStatsBar>div { | ||||
|         padding: 0.25rem 0.5rem; | ||||
|         border-right: none; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .topStatsBar>div:nth-child(odd) { | ||||
|         border-right: 1px solid var(--border-color); | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .longText { | ||||
|         display: none; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .shortText { | ||||
|         display: inline; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .topStatsBar span { | ||||
|         font-size: 0.8rem; | ||||
|         text-align: left; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .topStatsBar strong { | ||||
|         font-size: 0.85rem; | ||||
|         text-align: right; | ||||
|     } | ||||
|  | ||||
|     /* Ajustamos los botones custom en mobile */ | ||||
|     .navButton { | ||||
|         width: 32px; | ||||
|         height: 32px; | ||||
|     } | ||||
|  | ||||
|     .navButton::after { | ||||
|         line-height: 32px; | ||||
|     } | ||||
|     .navButtonPrev { left: -10px; } | ||||
|     .navButtonNext { right: -10px; } | ||||
|  | ||||
|     .homeCarouselWidget .candidateCard { gap: 0.5rem; padding: 0.5rem; } | ||||
|     .homeCarouselWidget .candidatePhotoWrapper { width: 50px; height: 50px; } | ||||
|     .homeCarouselWidget .candidateName { font-size: 0.9rem; } | ||||
|     .homeCarouselWidget .percentage { font-size: 1.1rem; } | ||||
|     .homeCarouselWidget .votes { font-size: 0.7rem; } | ||||
|     .homeCarouselWidget .widgetFooter { text-align: center; } | ||||
|     .navButtonPrev { | ||||
|         left: -10px; | ||||
|     } | ||||
|  | ||||
|     .navButtonNext { | ||||
|         right: -10px; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .candidateCard { | ||||
|         gap: 0.5rem; | ||||
|         padding: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .candidatePhotoWrapper { | ||||
|         width: 50px; | ||||
|         height: 50px; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .candidateName { | ||||
|         font-size: 0.9rem; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .percentage { | ||||
|         font-size: 1.1rem; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .votes { | ||||
|         font-size: 0.7rem; | ||||
|     } | ||||
|  | ||||
|     .homeCarouselWidget .widgetFooter { | ||||
|         text-align: center; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Mantenemos estos estilos globales por si acaso */ | ||||
|   | ||||
| @@ -58,7 +58,7 @@ export const HomeCarouselWidget = ({ eleccionId, distritoId, categoriaId, titulo | ||||
|  | ||||
|     return ( | ||||
|         <div className={styles.homeCarouselWidget}> | ||||
|             <div className={styles.widgetHeader}> | ||||
|             <div className={`${styles.widgetHeader} ${styles.headerSingleLine}`}> | ||||
|                 <h2 className={styles.widgetTitle}>{titulo}</h2> | ||||
|                 <a href={mapLinkUrl} className={styles.mapLinkButton}> | ||||
|                     <TfiMapAlt /> | ||||
|   | ||||
| @@ -1,81 +1,107 @@ | ||||
| /* src/components/widgets/ResumenNacionalWidget.module.css */ | ||||
| .widgetContainer { | ||||
|   font-family: sans-serif; | ||||
|   border: 1px solid #ccc; | ||||
|   font-family: 'Roboto', sans-serif; | ||||
|   border: 1px solid #e0e0e0; | ||||
|   border-radius: 8px; | ||||
|   padding: 1.5rem; | ||||
|   max-width: 1000px; | ||||
|   max-width: 500px; | ||||
|   margin: 2rem auto; | ||||
| } | ||||
|  | ||||
| .header { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   text-align: center; | ||||
|   padding-bottom: 1rem; | ||||
|   margin-bottom: 1rem; | ||||
|   border-bottom: 1px solid #eee; | ||||
| } | ||||
|  | ||||
| .header h3 { | ||||
|   margin: 0; | ||||
|   font-size: 1.8rem; | ||||
|   font-weight: 400; | ||||
| } | ||||
|  | ||||
| .subHeader { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| .subHeader h4 { | ||||
|   margin: 0; | ||||
|   font-size: 1.5rem; | ||||
| } | ||||
|  | ||||
| .categoriaSelector { | ||||
|   min-width: 280px; | ||||
|   min-width: 230px; | ||||
| } | ||||
|  | ||||
| .listaProvincias { | ||||
|   list-style: none; | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
| .resultsTable { | ||||
|   width: 100%; | ||||
|   border-collapse: collapse; | ||||
|   border-spacing: 0; | ||||
| } | ||||
|  | ||||
| .provinciaItem { | ||||
|   padding: 1rem 0; | ||||
|   border-bottom: 1px solid #eee; | ||||
| .resultsTable thead { | ||||
|   display: none; | ||||
| } | ||||
|  | ||||
| .provinciaItem:last-child { | ||||
|   border-bottom: none; | ||||
| .resultsTable td { | ||||
|   padding: 3px 8px; | ||||
|   text-align: left; | ||||
| } | ||||
|  | ||||
| .provinciaHeader { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: baseline; | ||||
|   margin-bottom: 0.5rem; | ||||
| .provinciaBlock { | ||||
|   border-top: 1px solid #e0e0e0; | ||||
| } | ||||
|  | ||||
| .provinciaBlock:first-child { | ||||
|   border-top: none; | ||||
| } | ||||
|  | ||||
| .provinciaNombre { | ||||
|   font-weight: bold; | ||||
|   font-size: 1.1rem; | ||||
|   font-weight: 700; | ||||
|   font-size: 1rem; | ||||
|   text-transform: uppercase; | ||||
|   color: #333; | ||||
|   padding-top: 1rem; | ||||
| } | ||||
|  | ||||
| .provinciaEscrutado { | ||||
|   font-size: 0.8rem; | ||||
|   color: #555; | ||||
|   font-weight: 500; | ||||
| } | ||||
|  | ||||
| .resultadosLista { | ||||
|   list-style: none; | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| .resultadoItem { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   padding: 0.25rem 0; | ||||
|   color: #666; | ||||
|   text-align: right; | ||||
|   white-space: nowrap; | ||||
|   padding-top: 1rem; | ||||
|   width: 1%; | ||||
| } | ||||
|  | ||||
| .partidoNombre { | ||||
|   color: #333; | ||||
|   font-size: 0.9rem; | ||||
| } | ||||
|  | ||||
| .partidoPorcentaje { | ||||
|   font-weight: bold; | ||||
|   text-align: right; | ||||
|   font-weight: 700; | ||||
|   font-size: 0.95rem; | ||||
|   width: 1%; | ||||
| } | ||||
|  | ||||
| /* --- INICIO DE ESTILOS PARA MÓVILES --- */ | ||||
| @media (max-width: 768px) { | ||||
|   .subHeader { | ||||
|     flex-direction: column; /* Apila el título y el selector */ | ||||
|     align-items: center;   /* Centra los elementos */ | ||||
|     gap: 0.75rem;          /* Añade espacio entre ellos */ | ||||
|   } | ||||
|  | ||||
|   .subHeader h4 { | ||||
|     text-align: center; | ||||
|   } | ||||
|  | ||||
|   .categoriaSelector { | ||||
|     width: 100%;           /* Hace que el selector ocupe todo el ancho */ | ||||
|     min-width: unset;      /* Elimina el ancho mínimo que interfiere */ | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| // src/components/widgets/ResumenNacionalWidget.tsx | ||||
| import { useState } from 'react'; | ||||
| import { useState, useMemo } from 'react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import Select from 'react-select'; | ||||
| import { getResumenNacionalPorProvincia } from '../../../apiService'; | ||||
| @@ -11,6 +11,35 @@ const CATEGORIAS_NACIONALES = [ | ||||
|   { value: 2, label: 'Senadores Nacionales' }, | ||||
| ]; | ||||
|  | ||||
| // 1. Mapa para definir el orden y número de cada provincia según el PDF | ||||
| const PROVINCE_ORDER_MAP: Record<string, number> = { | ||||
|   '02': 1,  // Buenos Aires | ||||
|   '03': 2,  // Catamarca | ||||
|   '06': 3,  // Chaco | ||||
|   '07': 4,  // Chubut | ||||
|   '04': 5,  // Córdoba | ||||
|   '05': 6,  // Corrientes | ||||
|   '08': 7,  // Entre Ríos | ||||
|   '09': 8,  // Formosa | ||||
|   '10': 9,  // Jujuy | ||||
|   '11': 10, // La Pampa | ||||
|   '12': 11, // La Rioja | ||||
|   '13': 12, // Mendoza | ||||
|   '14': 13, // Misiones | ||||
|   '15': 14, // Neuquén | ||||
|   '16': 15, // Río Negro | ||||
|   '17': 16, // Salta | ||||
|   '18': 17, // San Juan | ||||
|   '19': 18, // San Luis | ||||
|   '20': 19, // Santa Cruz | ||||
|   '21': 20, // Santa Fe | ||||
|   '22': 21, // Santiago del Estero | ||||
|   '23': 22, // Tierra del Fuego | ||||
|   '24': 23, // Tucumán | ||||
|   '01': 24, // CABA | ||||
| }; | ||||
|  | ||||
|  | ||||
| export const ResumenNacionalWidget = () => { | ||||
|   const [categoria, setCategoria] = useState(CATEGORIAS_NACIONALES[0]); | ||||
|  | ||||
| @@ -20,12 +49,22 @@ export const ResumenNacionalWidget = () => { | ||||
|     refetchInterval: 60000, | ||||
|   }); | ||||
|  | ||||
|   const formatPercent = (num: number) => `${num.toFixed(2)}%`; | ||||
|   // 2. Ordenar los datos de la API usando el mapa de ordenamiento | ||||
|   const sortedData = useMemo(() => { | ||||
|     if (!data) return []; | ||||
|     return [...data].sort((a, b) => { | ||||
|       const orderA = PROVINCE_ORDER_MAP[a.provinciaId] ?? 99; | ||||
|       const orderB = PROVINCE_ORDER_MAP[b.provinciaId] ?? 99; | ||||
|       return orderA - orderB; | ||||
|     }); | ||||
|   }, [data]); | ||||
|  | ||||
|   const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`; | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.widgetContainer}> | ||||
|       <div className={styles.header}> | ||||
|         <h3>{categoria.label}</h3> | ||||
|       <div className={styles.subHeader}> | ||||
|         <h4>{categoria.label}</h4> | ||||
|         <Select | ||||
|           className={styles.categoriaSelector} | ||||
|           options={CATEGORIAS_NACIONALES} | ||||
| @@ -36,25 +75,30 @@ export const ResumenNacionalWidget = () => { | ||||
|       </div> | ||||
|       {isLoading && <p>Cargando resumen nacional...</p>} | ||||
|       {error && <p style={{ color: 'red' }}>Error al cargar los datos.</p>} | ||||
|       {data && ( | ||||
|         <ul className={styles.listaProvincias}> | ||||
|           {data.map((provincia) => ( | ||||
|             <li key={provincia.provinciaId} className={styles.provinciaItem}> | ||||
|               <div className={styles.provinciaHeader}> | ||||
|                 <span className={styles.provinciaNombre}>{provincia.provinciaNombre}</span> | ||||
|                 <span className={styles.provinciaEscrutado}>ESCR. {formatPercent(provincia.porcentajeEscrutado)}</span> | ||||
|               </div> | ||||
|               <ul className={styles.resultadosLista}> | ||||
|       {sortedData && ( | ||||
|         <table className={styles.resultsTable}> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th>Concepto</th> | ||||
|               <th>Valor</th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           {sortedData.map((provincia) => ( | ||||
|             <tbody key={provincia.provinciaId} className={styles.provinciaBlock}> | ||||
|               <tr> | ||||
|                 {/* 3. Añadir el número antes del nombre */} | ||||
|                 <td className={styles.provinciaNombre}>{`${PROVINCE_ORDER_MAP[provincia.provinciaId]}- ${provincia.provinciaNombre}`}</td> | ||||
|                 <td className={styles.provinciaEscrutado}>ESCR. {formatPercent(provincia.porcentajeEscrutado)}</td> | ||||
|               </tr> | ||||
|               {provincia.resultados.map((partido, index) => ( | ||||
|                   <li key={index} className={styles.resultadoItem}> | ||||
|                     <span className={styles.partidoNombre}>{partido.nombre}</span> | ||||
|                     <span className={styles.partidoPorcentaje}>{formatPercent(partido.porcentaje)}</span> | ||||
|                   </li> | ||||
|                 <tr key={index}> | ||||
|                   <td className={styles.partidoNombre}>{partido.nombre}</td> | ||||
|                   <td className={styles.partidoPorcentaje}>{formatPercent(partido.porcentaje)}</td> | ||||
|                 </tr> | ||||
|               ))} | ||||
|               </ul> | ||||
|             </li> | ||||
|             </tbody> | ||||
|           ))} | ||||
|         </ul> | ||||
|         </table> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
|   | ||||
| @@ -4,13 +4,10 @@ import { getTablaConurbano } from '../../../apiService'; | ||||
| import styles from './TablaResultadosWidget.module.css'; | ||||
|  | ||||
| export const TablaConurbanoWidget = () => { | ||||
|   // CORRECCIÓN: Se elimina el estado y el selector de categoría. | ||||
|   const ELECCION_ID = 2; // Exclusivo para elecciones nacionales | ||||
|   const ELECCION_ID = 2; | ||||
|  | ||||
|   const { data, isLoading, error } = useQuery({ | ||||
|     // La queryKey ya no necesita la categoría | ||||
|     queryKey: ['tablaConurbano', ELECCION_ID], | ||||
|     // La llamada a la API ya no necesita la categoría | ||||
|     queryFn: () => getTablaConurbano(ELECCION_ID), | ||||
|     refetchInterval: 60000, | ||||
|   }); | ||||
| @@ -20,7 +17,7 @@ export const TablaConurbanoWidget = () => { | ||||
|   return ( | ||||
|     <div className={styles.widgetContainer}> | ||||
|       <div className={styles.header}> | ||||
|         <h3>Resultados Conurbano - Diputados Nacionales</h3> | ||||
|         <h3>Diputados Nacionales</h3> | ||||
|       </div> | ||||
|       {isLoading && <p>Cargando resultados...</p>} | ||||
|       {error && <p>Error al cargar los datos.</p>} | ||||
| @@ -38,7 +35,9 @@ export const TablaConurbanoWidget = () => { | ||||
|           <tbody> | ||||
|             {data.map((fila, index) => ( | ||||
|               <tr key={fila.ambitoId}> | ||||
|                 <td className={styles.distritoCell}><span className={styles.distritoIndex}>{index + 1}.</span>{fila.nombre}</td> | ||||
|                 <td className={styles.distritoCell}> | ||||
|                   <span className={styles.distritoIndex}>{index + 1}.</span>{fila.nombre} | ||||
|                 </td> | ||||
|                 <td className={styles.fuerzaCell}>{fila.fuerza1Display}</td> | ||||
|                 <td className={styles.porcentajeCell}>{formatPercent(fila.fuerza1Porcentaje)}</td> | ||||
|                 <td className={styles.fuerzaCell}>{fila.fuerza2Display}</td> | ||||
|   | ||||
| @@ -74,3 +74,103 @@ | ||||
|   color: #6c757d; | ||||
|   padding-right: 0.5rem; | ||||
| } | ||||
|  | ||||
| /* --- INICIO DE ESTILOS PARA MÓVILES --- */ | ||||
| @media (max-width: 768px) { | ||||
|   .widgetContainer { | ||||
|     padding: 0.5rem; | ||||
|   } | ||||
|  | ||||
|   .resultsTable thead { | ||||
|     display: none; | ||||
|   } | ||||
|  | ||||
|   .resultsTable, | ||||
|   .resultsTable tbody { | ||||
|     display: block; | ||||
|     width: 100%; | ||||
|   } | ||||
|  | ||||
|   /* 1. Cada TR es una grilla */ | ||||
|   .resultsTable tr { | ||||
|     display: grid; | ||||
|     grid-template-columns: 1fr auto; | ||||
|     /* Columna para nombres, columna para % */ | ||||
|     grid-template-rows: auto auto auto; | ||||
|     /* Fila para distrito, 1ra fuerza, 2da fuerza */ | ||||
|     gap: 4px 1rem; | ||||
|     margin-bottom: 1rem; | ||||
|     padding-bottom: 1rem; | ||||
|     border-bottom: 2px solid #e0e0e0; | ||||
|   } | ||||
|  | ||||
|   .resultsTable tr:last-child { | ||||
|     border-bottom: none; | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
|  | ||||
|   .resultsTable td { | ||||
|     padding: 0; | ||||
|     border-bottom: none; | ||||
|     text-align: left; | ||||
|   } | ||||
|  | ||||
|   /* 2. Posicionamos cada celda en la grilla */ | ||||
|   .distritoCell { | ||||
|     grid-column: 1 / -1; | ||||
|     /* Ocupa toda la primera fila */ | ||||
|     font-size: 1.1rem; | ||||
|     font-weight: 700; | ||||
|   } | ||||
|  | ||||
|   .fuerzaCell:nth-of-type(2) { | ||||
|     grid-row: 2; | ||||
|     grid-column: 1; | ||||
|   } | ||||
|  | ||||
|   .porcentajeCell:nth-of-type(3) { | ||||
|     grid-row: 2; | ||||
|     grid-column: 2; | ||||
|   } | ||||
|  | ||||
|   .fuerzaCell:nth-of-type(4) { | ||||
|     grid-row: 3; | ||||
|     grid-column: 1; | ||||
|   } | ||||
|  | ||||
|   .porcentajeCell:nth-of-type(5) { | ||||
|     grid-row: 3; | ||||
|     grid-column: 2; | ||||
|   } | ||||
|  | ||||
|   /* 3. Añadimos los labels "1ra:" y "2da:" con pseudo-elementos */ | ||||
|   .fuerzaCell::before { | ||||
|     font-weight: 500; | ||||
|     color: #6c757d; | ||||
|     margin-right: 0.5rem; | ||||
|   } | ||||
|  | ||||
|   .fuerzaCell:nth-of-type(2)::before { | ||||
|     content: '1ra:'; | ||||
|   } | ||||
|  | ||||
|   .fuerzaCell:nth-of-type(4)::before { | ||||
|     content: '2da:'; | ||||
|   } | ||||
|  | ||||
|   /* Ajustes de alineación */ | ||||
|   .fuerzaCell { | ||||
|     display: inline-flex; | ||||
|     align-items: baseline; | ||||
|     font-size: 0.9rem; | ||||
|   } | ||||
|  | ||||
|   .porcentajeCell { | ||||
|     font-size: 0.95rem; | ||||
|   } | ||||
|  | ||||
|   .seccionHeader td { | ||||
|     display: block; | ||||
|     grid-column: 1 / -1; | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| // src/features/legislativas/nacionales/TablaSeccionesWidget.tsx | ||||
| import React from 'react'; // Importar React para React.Fragment | ||||
| import React from 'react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { getTablaSecciones } from '../../../apiService'; | ||||
| import styles from './TablaResultadosWidget.module.css'; | ||||
| @@ -18,7 +18,7 @@ export const TablaSeccionesWidget = () => { | ||||
|   return ( | ||||
|     <div className={styles.widgetContainer}> | ||||
|       <div className={styles.header}> | ||||
|         <h3>Resultados por Sección - Diputados Nacionales</h3> | ||||
|         <h3>Diputados Nacionales</h3> | ||||
|       </div> | ||||
|       {isLoading && <p>Cargando resultados...</p>} | ||||
|       {error && <p>Error al cargar los datos.</p>} | ||||
| @@ -41,7 +41,9 @@ export const TablaSeccionesWidget = () => { | ||||
|                 </tr> | ||||
|                 {seccion.municipios.map((fila, index) => ( | ||||
|                   <tr key={fila.ambitoId}> | ||||
|                     <td className={styles.distritoCell}><span className={styles.distritoIndex}>{index + 1}.</span>{fila.nombre}</td> | ||||
|                     <td className={styles.distritoCell}> | ||||
|                       <span className={styles.distritoIndex}>{index + 1}.</span>{fila.nombre} | ||||
|                     </td> | ||||
|                     <td className={styles.fuerzaCell}>{fila.fuerza1Display}</td> | ||||
|                     <td className={styles.porcentajeCell}>{formatPercent(fila.fuerza1Porcentaje)}</td> | ||||
|                     <td className={styles.fuerzaCell}>{fila.fuerza2Display}</td> | ||||
|   | ||||
| @@ -32,6 +32,7 @@ import { HomeCarouselNacionalWidget } from './features/legislativas/nacionales/H | ||||
| import { TablaConurbanoWidget } from './features/legislativas/nacionales/TablaConurbanoWidget'; | ||||
| import { TablaSeccionesWidget } from './features/legislativas/nacionales/TablaSeccionesWidget'; | ||||
| import { ResumenNacionalWidget } from './features/legislativas/nacionales/ResumenNacionalWidget'; | ||||
| import { HomeCarouselProvincialWidget } from './features/legislativas/nacionales/HomeCarouselProvincialWidget'; | ||||
|  | ||||
| import { DevAppLegislativas } from './features/legislativas/DevAppLegislativas'; | ||||
|  | ||||
| @@ -68,6 +69,7 @@ const WIDGET_MAP: Record<string, React.ElementType> = { | ||||
|     'tabla-conurbano': TablaConurbanoWidget, | ||||
|     'tabla-secciones': TablaSeccionesWidget, | ||||
|     'resumen-nacional': ResumenNacionalWidget, | ||||
|     'home-carousel-provincial': HomeCarouselProvincialWidget, | ||||
| }; | ||||
|  | ||||
| // Vite establece `import.meta.env.DEV` a `true` cuando ejecutamos 'npm run dev' | ||||
|   | ||||
| @@ -16,6 +16,23 @@ public class CatalogosController : ControllerBase | ||||
|         _dbContext = dbContext; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Obtiene la lista de todas las provincias (distritos). | ||||
|     /// </summary> | ||||
|     [HttpGet("provincias")] | ||||
|     public async Task<IActionResult> GetProvincias() | ||||
|     { | ||||
|         var provincias = await _dbContext.AmbitosGeograficos | ||||
|             .AsNoTracking() | ||||
|             .Where(a => a.NivelId == 10 && !string.IsNullOrEmpty(a.DistritoId)) // Nivel 10 = Provincia | ||||
|             .OrderBy(a => a.Nombre) | ||||
|             .Select(a => new { Id = a.DistritoId, a.Nombre }) | ||||
|             .Distinct() | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         return Ok(provincias); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("municipios")] | ||||
|     public async Task<IActionResult> GetMunicipios([FromQuery] int? categoriaId) | ||||
|     { | ||||
|   | ||||
| @@ -14,7 +14,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+248171146d637f758fd8b14a6a2ef9fcb0bcd3b5")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1335b54d759c402b859b6d8338cd0c944cc70171")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","8QWUReqP8upfOnmA5lMNgBxAfYJ1z3zv/WYBUXBEiog=","1L7p1HQI/Uoosqm7RyBuYjKbRFTycFgJEtHPSdlXWhU=","ZxPpBx5gkHuilHLcg/vcjvaXswvTqiUM0YaAEwbNSLI=","zSbNtRd32h6wCMWjU5ecl5a3ECd\u002BVBstFC3etkdk4s0=","urIQ/RlknPjR8\u002BeAcCsDIPiRjQGFfUdIC\u002BoT3wYB2dU=","QgvJ\u002BjH2t7prbQ/Cu9eYOIBqysMeDcsXR6lggWr0auI=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","xGsYFCt4z/3oybSKe/TfOuJ3mQW6fLJVS9hZfmpKuPY="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","8QWUReqP8upfOnmA5lMNgBxAfYJ1z3zv/WYBUXBEiog=","1L7p1HQI/Uoosqm7RyBuYjKbRFTycFgJEtHPSdlXWhU=","ZxPpBx5gkHuilHLcg/vcjvaXswvTqiUM0YaAEwbNSLI=","zSbNtRd32h6wCMWjU5ecl5a3ECd\u002BVBstFC3etkdk4s0=","urIQ/RlknPjR8\u002BeAcCsDIPiRjQGFfUdIC\u002BoT3wYB2dU=","ytyPPQGU70eGo9tCrHq5\u002BwXF3yVuqv9Z\u002Br1Zdf0XUCI=","scnW1D7e2F059zWPpwmOsIw6KIyloYSDqXXW70WAZpQ=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","7Yl3qA5xr\u002BXUmuY\u002Bshj87a0l8dEYVlvjk253M66DWfo="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","8QWUReqP8upfOnmA5lMNgBxAfYJ1z3zv/WYBUXBEiog=","1L7p1HQI/Uoosqm7RyBuYjKbRFTycFgJEtHPSdlXWhU=","ZxPpBx5gkHuilHLcg/vcjvaXswvTqiUM0YaAEwbNSLI=","zSbNtRd32h6wCMWjU5ecl5a3ECd\u002BVBstFC3etkdk4s0=","urIQ/RlknPjR8\u002BeAcCsDIPiRjQGFfUdIC\u002BoT3wYB2dU=","QgvJ\u002BjH2t7prbQ/Cu9eYOIBqysMeDcsXR6lggWr0auI=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","xGsYFCt4z/3oybSKe/TfOuJ3mQW6fLJVS9hZfmpKuPY="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","8QWUReqP8upfOnmA5lMNgBxAfYJ1z3zv/WYBUXBEiog=","1L7p1HQI/Uoosqm7RyBuYjKbRFTycFgJEtHPSdlXWhU=","ZxPpBx5gkHuilHLcg/vcjvaXswvTqiUM0YaAEwbNSLI=","zSbNtRd32h6wCMWjU5ecl5a3ECd\u002BVBstFC3etkdk4s0=","urIQ/RlknPjR8\u002BeAcCsDIPiRjQGFfUdIC\u002BoT3wYB2dU=","ytyPPQGU70eGo9tCrHq5\u002BwXF3yVuqv9Z\u002Br1Zdf0XUCI=","scnW1D7e2F059zWPpwmOsIw6KIyloYSDqXXW70WAZpQ=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","7Yl3qA5xr\u002BXUmuY\u002Bshj87a0l8dEYVlvjk253M66DWfo="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| @@ -13,7 +13,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+248171146d637f758fd8b14a6a2ef9fcb0bcd3b5")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1335b54d759c402b859b6d8338cd0c944cc70171")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -13,7 +13,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+248171146d637f758fd8b14a6a2ef9fcb0bcd3b5")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1335b54d759c402b859b6d8338cd0c944cc70171")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -13,7 +13,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+248171146d637f758fd8b14a6a2ef9fcb0bcd3b5")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1335b54d759c402b859b6d8338cd0c944cc70171")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user