Feat Widgets Cards y Optimización de Consultas
| @@ -36,6 +36,23 @@ td button { | ||||
|     margin-right: 5px; | ||||
| } | ||||
|  | ||||
| .table-container { | ||||
|   max-height: 500px; /* Altura máxima antes de que aparezca el scroll */ | ||||
|   overflow-y: auto;  /* Habilita el scroll vertical cuando es necesario */ | ||||
|   border: 1px solid #ddd; | ||||
|   border-radius: 4px; | ||||
|   position: relative; /* Necesario para que 'sticky' funcione correctamente */ | ||||
| } | ||||
|  | ||||
| /* Hacemos que la cabecera de la tabla se quede fija en la parte superior */ | ||||
| .table-container thead th { | ||||
|   position: sticky; | ||||
|   top: 0; | ||||
|   z-index: 1; | ||||
|   /* El color de fondo es crucial para que no se vea el contenido que pasa por debajo */ | ||||
|   background-color: #f2f2f2;  | ||||
| } | ||||
|  | ||||
| .sortable-list-horizontal { | ||||
|   list-style: none; | ||||
|   padding: 8px; | ||||
|   | ||||
| @@ -1,46 +1,52 @@ | ||||
| // src/components/AgrupacionesManager.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import Select from 'react-select'; // Importamos Select | ||||
| import { getAgrupaciones, updateAgrupacion, getLogos, updateLogos } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica, LogoAgrupacionCategoria } from '../types'; | ||||
| import './AgrupacionesManager.css'; | ||||
|  | ||||
| // Constantes para los IDs de categoría | ||||
| const SENADORES_ID = 5; | ||||
| const DIPUTADOS_ID = 6; | ||||
| const CONCEJALES_ID = 7; | ||||
| const SENADORES_NAC_ID = 1; | ||||
| const DIPUTADOS_NAC_ID = 2; | ||||
|  | ||||
| // Opciones para el nuevo selector de Elección | ||||
| const ELECCION_OPTIONS = [ | ||||
|     { value: 2, label: 'Elecciones Nacionales' }, | ||||
|     { value: 1, label: 'Elecciones Provinciales' }     | ||||
| ]; | ||||
|  | ||||
| // Esta función limpia cualquier carácter no válido de un string de color. | ||||
| const sanitizeColor = (color: string | null | undefined): string => { | ||||
|     if (!color) return '#000000'; // Devuelve un color válido por defecto si es nulo | ||||
|     // Usa una expresión regular para eliminar todo lo que no sea un '#' o un carácter hexadecimal | ||||
|     if (!color) return '#000000'; | ||||
|     const sanitized = color.replace(/[^#0-9a-fA-F]/g, ''); | ||||
|     return sanitized.startsWith('#') ? sanitized : `#${sanitized}`; | ||||
| }; | ||||
|  | ||||
| export const AgrupacionesManager = () => { | ||||
|     const queryClient = useQueryClient(); | ||||
|      | ||||
|     // --- NUEVO ESTADO PARA LA ELECCIÓN SELECCIONADA --- | ||||
|     const [selectedEleccion, setSelectedEleccion] = useState(ELECCION_OPTIONS[0]); | ||||
|  | ||||
|     const [editedAgrupaciones, setEditedAgrupaciones] = useState<Record<string, Partial<AgrupacionPolitica>>>({}); | ||||
|     const [editedLogos, setEditedLogos] = useState<LogoAgrupacionCategoria[]>([]); | ||||
|  | ||||
|     // Query 1: Obtener agrupaciones | ||||
|     const { data: agrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({ | ||||
|         queryKey: ['agrupaciones'], | ||||
|         queryFn: getAgrupaciones, | ||||
|     }); | ||||
|  | ||||
|     // Query 2: Obtener logos | ||||
|     // --- CORRECCIÓN: La query de logos ahora depende del ID de la elección --- | ||||
|     const { data: logos = [], isLoading: isLoadingLogos } = useQuery<LogoAgrupacionCategoria[]>({ | ||||
|         queryKey: ['logos'], | ||||
|         queryFn: getLogos, | ||||
|         queryKey: ['logos', selectedEleccion.value], | ||||
|         queryFn: () => getLogos(selectedEleccion.value), // Pasamos el valor numérico | ||||
|     }); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         // Solo procedemos si los datos de agrupaciones están disponibles | ||||
|         if (agrupaciones && agrupaciones.length > 0) { | ||||
|             // Inicializamos el estado de 'editedAgrupaciones' una sola vez. | ||||
|             // Usamos una función en setState para asegurarnos de que solo se ejecute | ||||
|             // si el estado está vacío, evitando sobreescribir ediciones del usuario. | ||||
|             setEditedAgrupaciones(prev => { | ||||
|                 if (Object.keys(prev).length === 0) { | ||||
|                     return Object.fromEntries(agrupaciones.map(a => [a.id, {}])); | ||||
| @@ -48,20 +54,14 @@ export const AgrupacionesManager = () => { | ||||
|                 return prev; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // Hacemos lo mismo para los logos | ||||
|         if (logos && logos.length > 0) { | ||||
|             setEditedLogos(prev => { | ||||
|                 if (prev.length === 0) { | ||||
|                     // Creamos una copia profunda para evitar mutaciones accidentales | ||||
|                     return JSON.parse(JSON.stringify(logos)); | ||||
|                 } | ||||
|                 return prev; | ||||
|             }); | ||||
|     }, [agrupaciones]); | ||||
|      | ||||
|     // Este useEffect ahora también depende de 'logos' para reinicializarse | ||||
|     useEffect(() => { | ||||
|         if (logos) { | ||||
|             setEditedLogos(JSON.parse(JSON.stringify(logos))); | ||||
|         } | ||||
|         // La dependencia ahora es el estado de carga. El hook se ejecutará cuando | ||||
|         // isLoadingAgrupaciones o isLoadingLogos cambien de true a false. | ||||
|     }, [agrupaciones, logos]); | ||||
|     }, [logos]); | ||||
|  | ||||
|     const handleInputChange = (id: string, field: 'nombreCorto' | 'color', value: string) => { | ||||
|         setEditedAgrupaciones(prev => ({ | ||||
| @@ -74,6 +74,7 @@ export const AgrupacionesManager = () => { | ||||
|         setEditedLogos(prev => { | ||||
|             const newLogos = [...prev]; | ||||
|             const existing = newLogos.find(l => | ||||
|                 l.eleccionId === selectedEleccion.value && | ||||
|                 l.agrupacionPoliticaId === agrupacionId && | ||||
|                 l.categoriaId === categoriaId && | ||||
|                 l.ambitoGeograficoId == null | ||||
| @@ -84,6 +85,7 @@ export const AgrupacionesManager = () => { | ||||
|             } else { | ||||
|                 newLogos.push({ | ||||
|                     id: 0, | ||||
|                     eleccionId: selectedEleccion.value, // Añadimos el ID de la elección | ||||
|                     agrupacionPoliticaId: agrupacionId, | ||||
|                     categoriaId, | ||||
|                     logoUrl: value, | ||||
| @@ -99,7 +101,7 @@ export const AgrupacionesManager = () => { | ||||
|             const agrupacionPromises = Object.entries(editedAgrupaciones).map(([id, changes]) => { | ||||
|                 if (Object.keys(changes).length > 0) { | ||||
|                     const original = agrupaciones.find(a => a.id === id); | ||||
|                     if (original) { // Chequeo de seguridad | ||||
|                     if (original) { | ||||
|                         return updateAgrupacion(id, { ...original, ...changes }); | ||||
|                     } | ||||
|                 } | ||||
| @@ -111,7 +113,7 @@ export const AgrupacionesManager = () => { | ||||
|             await Promise.all([...agrupacionPromises, logoPromise]); | ||||
|  | ||||
|             queryClient.invalidateQueries({ queryKey: ['agrupaciones'] }); | ||||
|             queryClient.invalidateQueries({ queryKey: ['logos'] }); | ||||
|             queryClient.invalidateQueries({ queryKey: ['logos', selectedEleccion.value] }); // Invalidamos la query correcta | ||||
|  | ||||
|             alert('¡Todos los cambios han sido guardados!'); | ||||
|         } catch (err) { | ||||
| @@ -124,6 +126,7 @@ export const AgrupacionesManager = () => { | ||||
|  | ||||
|     const getLogoUrl = (agrupacionId: string, categoriaId: number) => { | ||||
|         return editedLogos.find(l => | ||||
|             l.eleccionId === selectedEleccion.value && | ||||
|             l.agrupacionPoliticaId === agrupacionId && | ||||
|             l.categoriaId === categoriaId && | ||||
|             l.ambitoGeograficoId == null | ||||
| @@ -132,40 +135,67 @@ export const AgrupacionesManager = () => { | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Gestión de Agrupaciones y Logos</h3> | ||||
|             <div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}> | ||||
|                 <h3>Gestión de Agrupaciones y Logos Generales</h3> | ||||
|                 <div style={{width: '250px', zIndex: 100 }}> | ||||
|                     <Select | ||||
|                         options={ELECCION_OPTIONS} | ||||
|                         value={selectedEleccion} | ||||
|                         onChange={(opt) => setSelectedEleccion(opt!)} | ||||
|                     /> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             {isLoading ? <p>Cargando...</p> : ( | ||||
|                 <> | ||||
|                     <table> | ||||
|                         <thead> | ||||
|                             <tr> | ||||
|                                 <th>Nombre</th> | ||||
|                                 <th>Nombre Corto</th> | ||||
|                                 <th>Color</th> | ||||
|                                 <th>Logo Senadores</th> | ||||
|                                 <th>Logo Diputados</th> | ||||
|                                 <th>Logo Concejales</th> | ||||
|                             </tr> | ||||
|                         </thead> | ||||
|                         <tbody> | ||||
|                             {agrupaciones.map(agrupacion => ( | ||||
|                                 <tr key={agrupacion.id}> | ||||
|                                     <td>{agrupacion.nombre}</td> | ||||
|                                     <td><input type="text" value={editedAgrupaciones[agrupacion.id]?.nombreCorto ?? agrupacion.nombreCorto ?? ''} onChange={(e) => handleInputChange(agrupacion.id, 'nombreCorto', e.target.value)} /></td> | ||||
|                                     <td> | ||||
|                                         <input  | ||||
|                                             type="color"  | ||||
|                                             // Usamos la función sanitizeColor para asegurarnos de que el valor sea siempre válido | ||||
|                                             value={sanitizeColor(editedAgrupaciones[agrupacion.id]?.color ?? agrupacion.color)}  | ||||
|                                             onChange={(e) => handleInputChange(agrupacion.id, 'color', e.target.value)}  | ||||
|                                         /> | ||||
|                                     </td> | ||||
|                                     <td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, SENADORES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, SENADORES_ID, e.target.value)} /></td> | ||||
|                                     <td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, DIPUTADOS_ID)} onChange={(e) => handleLogoChange(agrupacion.id, DIPUTADOS_ID, e.target.value)} /></td> | ||||
|                                     <td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, CONCEJALES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, CONCEJALES_ID, e.target.value)} /></td> | ||||
|                     <div className="table-container"> | ||||
|                         <table> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th>Nombre</th> | ||||
|                                     <th>Nombre Corto</th> | ||||
|                                     <th>Color</th> | ||||
|                                     {/* --- CABECERAS CONDICIONALES --- */} | ||||
|                                     {selectedEleccion.value === 2 ? ( | ||||
|                                         <> | ||||
|                                             <th>Logo Senadores Nac.</th> | ||||
|                                             <th>Logo Diputados Nac.</th> | ||||
|                                         </> | ||||
|                                     ) : ( | ||||
|                                         <> | ||||
|                                             <th>Logo Senadores Prov.</th> | ||||
|                                             <th>Logo Diputados Prov.</th> | ||||
|                                             <th>Logo Concejales</th> | ||||
|                                         </> | ||||
|                                     )} | ||||
|                                 </tr> | ||||
|                             ))} | ||||
|                         </tbody> | ||||
|                     </table> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 {agrupaciones.map(agrupacion => ( | ||||
|                                     <tr key={agrupacion.id}> | ||||
|                                         <td>{agrupacion.nombre}</td> | ||||
|                                         <td><input type="text" value={editedAgrupaciones[agrupacion.id]?.nombreCorto ?? agrupacion.nombreCorto ?? ''} onChange={(e) => handleInputChange(agrupacion.id, 'nombreCorto', e.target.value)} /></td> | ||||
|                                         <td> | ||||
|                                             <input type="color" value={sanitizeColor(editedAgrupaciones[agrupacion.id]?.color ?? agrupacion.color)} onChange={(e) => handleInputChange(agrupacion.id, 'color', e.target.value)} /> | ||||
|                                         </td> | ||||
|                                         {/* --- CELDAS CONDICIONALES --- */} | ||||
|                                         {selectedEleccion.value === 2 ? ( | ||||
|                                             <> | ||||
|                                                 <td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, SENADORES_NAC_ID)} onChange={(e) => handleLogoChange(agrupacion.id, SENADORES_NAC_ID, e.target.value)} /></td> | ||||
|                                                 <td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, DIPUTADOS_NAC_ID)} onChange={(e) => handleLogoChange(agrupacion.id, DIPUTADOS_NAC_ID, e.target.value)} /></td> | ||||
|                                             </> | ||||
|                                         ) : ( | ||||
|                                             <> | ||||
|                                                 <td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, SENADORES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, SENADORES_ID, e.target.value)} /></td> | ||||
|                                                 <td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, DIPUTADOS_ID)} onChange={(e) => handleLogoChange(agrupacion.id, DIPUTADOS_ID, e.target.value)} /></td> | ||||
|                                                 <td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, CONCEJALES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, CONCEJALES_ID, e.target.value)} /></td> | ||||
|                                             </> | ||||
|                                         )} | ||||
|                                     </tr> | ||||
|                                 ))} | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
|                     <button onClick={handleSaveAll} style={{ marginTop: '1rem' }}> | ||||
|                         Guardar Todos los Cambios | ||||
|                     </button> | ||||
|   | ||||
| @@ -0,0 +1,117 @@ | ||||
| // src/components/BancasNacionalesManager.tsx | ||||
| import { useState } from 'react'; | ||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { getBancadas, getAgrupaciones, updateBancada, type UpdateBancadaData } from '../services/apiService'; | ||||
| import type { Bancada, AgrupacionPolitica } from '../types'; | ||||
| import { OcupantesModal } from './OcupantesModal'; | ||||
| import './AgrupacionesManager.css'; | ||||
|  | ||||
| const ELECCION_ID_NACIONAL = 2; | ||||
| const camaras = ['diputados', 'senadores'] as const; | ||||
|  | ||||
| export const BancasNacionalesManager = () => { | ||||
|   const [activeTab, setActiveTab] = useState<'diputados' | 'senadores'>('diputados'); | ||||
|   const [modalVisible, setModalVisible] = useState(false); | ||||
|   const [bancadaSeleccionada, setBancadaSeleccionada] = useState<Bancada | null>(null); | ||||
|   const queryClient = useQueryClient(); | ||||
|  | ||||
|   const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ | ||||
|     queryKey: ['agrupaciones'], | ||||
|     queryFn: getAgrupaciones | ||||
|   }); | ||||
|  | ||||
|   const { data: bancadas = [], isLoading, error } = useQuery<Bancada[]>({ | ||||
|     queryKey: ['bancadas', activeTab, ELECCION_ID_NACIONAL], | ||||
|     queryFn: () => getBancadas(activeTab, ELECCION_ID_NACIONAL), | ||||
|   }); | ||||
|  | ||||
|   const handleAgrupacionChange = async (bancadaId: number, nuevaAgrupacionId: string | null) => { | ||||
|     const bancadaActual = bancadas.find(b => b.id === bancadaId); | ||||
|     if (!bancadaActual) return; | ||||
|  | ||||
|     const payload: UpdateBancadaData = { | ||||
|       agrupacionPoliticaId: nuevaAgrupacionId, | ||||
|       nombreOcupante: nuevaAgrupacionId ? (bancadaActual.ocupante?.nombreOcupante ?? null) : null, | ||||
|       fotoUrl: nuevaAgrupacionId ? (bancadaActual.ocupante?.fotoUrl ?? null) : null, | ||||
|       periodo: nuevaAgrupacionId ? (bancadaActual.ocupante?.periodo ?? null) : null, | ||||
|     }; | ||||
|  | ||||
|     try { | ||||
|       await updateBancada(bancadaId, payload); | ||||
|       queryClient.invalidateQueries({ queryKey: ['bancadas', activeTab, ELECCION_ID_NACIONAL] }); | ||||
|     } catch (err) { | ||||
|       alert("Error al guardar el cambio de agrupación."); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleOpenModal = (bancada: Bancada) => { | ||||
|     setBancadaSeleccionada(bancada); | ||||
|     setModalVisible(true); | ||||
|   }; | ||||
|  | ||||
|   if (error) return <p style={{ color: 'red' }}>Error al cargar las bancas nacionales.</p>; | ||||
|  | ||||
|   return ( | ||||
|     <div className="admin-module"> | ||||
|       <h3>Gestión de Bancas (Nacionales)</h3> | ||||
|       <p>Asigne partidos y ocupantes a las bancas del Congreso de la Nación.</p> | ||||
|  | ||||
|       <div className="chamber-tabs"> | ||||
|         {camaras.map(camara => ( | ||||
|           <button | ||||
|             key={camara} | ||||
|             className={activeTab === camara ? 'active' : ''} | ||||
|             onClick={() => setActiveTab(camara)} | ||||
|           > | ||||
|             {camara === 'diputados' ? 'Diputados Nacionales (257)' : 'Senadores Nacionales (72)'} | ||||
|           </button> | ||||
|         ))} | ||||
|       </div> | ||||
|  | ||||
|       {isLoading ? <p>Cargando bancas...</p> : ( | ||||
|         <div className="table-container"> | ||||
|           <table> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th style={{ width: '15%' }}>Banca #</th> | ||||
|                 <th style={{ width: '35%' }}>Partido Asignado</th> | ||||
|                 <th style={{ width: '30%' }}>Ocupante Actual</th> | ||||
|                 <th style={{ width: '20%' }}>Acciones</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               {bancadas.map((bancada) => ( | ||||
|                 <tr key={bancada.id}> | ||||
|                   <td>{`${activeTab.charAt(0).toUpperCase()}-${bancada.numeroBanca}`}</td> | ||||
|                   <td> | ||||
|                     <select | ||||
|                       value={bancada.agrupacionPoliticaId || ''} | ||||
|                       onChange={(e) => handleAgrupacionChange(bancada.id, e.target.value || null)} | ||||
|                     > | ||||
|                       <option value="">-- Vacante --</option> | ||||
|                       {agrupaciones.map(a => <option key={a.id} value={a.id}>{a.nombre}</option>)} | ||||
|                     </select> | ||||
|                   </td> | ||||
|                   <td>{bancada.ocupante?.nombreOcupante || 'Sin asignar'}</td> | ||||
|                   <td> | ||||
|                     <button disabled={!bancada.agrupacionPoliticaId} onClick={() => handleOpenModal(bancada)}> | ||||
|                       Editar Ocupante | ||||
|                     </button> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               ))} | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       {modalVisible && bancadaSeleccionada && ( | ||||
|         <OcupantesModal | ||||
|           bancada={bancadaSeleccionada} | ||||
|           onClose={() => setModalVisible(false)} | ||||
|           activeTab={activeTab} | ||||
|         /> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,125 @@ | ||||
| // src/components/BancasPreviasManager.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { getBancasPrevias, updateBancasPrevias, getAgrupaciones } from '../services/apiService'; | ||||
| import type { BancaPrevia, AgrupacionPolitica } from '../types'; | ||||
| import { TipoCamara } from '../types'; | ||||
|  | ||||
| const ELECCION_ID_NACIONAL = 2; | ||||
|  | ||||
| export const BancasPreviasManager = () => { | ||||
|     const queryClient = useQueryClient(); | ||||
|     const [editedBancas, setEditedBancas] = useState<Record<string, Partial<BancaPrevia>>>({}); | ||||
|  | ||||
|     const { data: agrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({ | ||||
|         queryKey: ['agrupaciones'], | ||||
|         queryFn: getAgrupaciones, | ||||
|     }); | ||||
|  | ||||
|     const { data: bancasPrevias = [], isLoading: isLoadingBancas } = useQuery<BancaPrevia[]>({ | ||||
|         queryKey: ['bancasPrevias', ELECCION_ID_NACIONAL], | ||||
|         queryFn: () => getBancasPrevias(ELECCION_ID_NACIONAL), | ||||
|     }); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (agrupaciones.length > 0) { | ||||
|             const initialData: Record<string, Partial<BancaPrevia>> = {}; | ||||
|             agrupaciones.forEach(agrupacion => { | ||||
|                 // Para Diputados | ||||
|                 const keyDip = `${agrupacion.id}-${TipoCamara.Diputados}`; | ||||
|                 const existingDip = bancasPrevias.find(b => b.agrupacionPoliticaId === agrupacion.id && b.camara === TipoCamara.Diputados); | ||||
|                 initialData[keyDip] = { cantidad: existingDip?.cantidad || 0 }; | ||||
|  | ||||
|                 // Para Senadores | ||||
|                 const keySen = `${agrupacion.id}-${TipoCamara.Senadores}`; | ||||
|                 const existingSen = bancasPrevias.find(b => b.agrupacionPoliticaId === agrupacion.id && b.camara === TipoCamara.Senadores); | ||||
|                 initialData[keySen] = { cantidad: existingSen?.cantidad || 0 }; | ||||
|             }); | ||||
|             setEditedBancas(initialData); | ||||
|         } | ||||
|     }, [agrupaciones, bancasPrevias]); | ||||
|  | ||||
|     const handleInputChange = (agrupacionId: string, camara: typeof TipoCamara.Diputados | typeof TipoCamara.Senadores, value: string) => { | ||||
|         const key = `${agrupacionId}-${camara}`; | ||||
|         const cantidad = parseInt(value, 10); | ||||
|         setEditedBancas(prev => ({ | ||||
|             ...prev, | ||||
|             [key]: { ...prev[key], cantidad: isNaN(cantidad) ? 0 : cantidad } | ||||
|         })); | ||||
|     }; | ||||
|  | ||||
|     const handleSave = async () => { | ||||
|         const payload: BancaPrevia[] = Object.entries(editedBancas) | ||||
|             .map(([key, value]) => { | ||||
|                 const [agrupacionPoliticaId, camara] = key.split('-'); | ||||
|                 return { | ||||
|                     id: 0, | ||||
|                     eleccionId: ELECCION_ID_NACIONAL, | ||||
|                     agrupacionPoliticaId, | ||||
|                     camara: parseInt(camara) as typeof TipoCamara.Diputados | typeof TipoCamara.Senadores, | ||||
|                     cantidad: value.cantidad || 0, | ||||
|                 }; | ||||
|             }) | ||||
|             .filter(b => b.cantidad > 0); | ||||
|  | ||||
|         try { | ||||
|             await updateBancasPrevias(ELECCION_ID_NACIONAL, payload); | ||||
|             queryClient.invalidateQueries({ queryKey: ['bancasPrevias', ELECCION_ID_NACIONAL] }); | ||||
|             alert('Bancas previas guardadas con éxito.'); | ||||
|         } catch (error) { | ||||
|             console.error(error); | ||||
|             alert('Error al guardar las bancas previas.'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const totalDiputados = Object.entries(editedBancas).reduce((sum, [key, value]) => key.endsWith(`-${TipoCamara.Diputados}`) ? sum + (value.cantidad || 0) : sum, 0); | ||||
|     const totalSenadores = Object.entries(editedBancas).reduce((sum, [key, value]) => key.endsWith(`-${TipoCamara.Senadores}`) ? sum + (value.cantidad || 0) : sum, 0); | ||||
|  | ||||
|     const isLoading = isLoadingAgrupaciones || isLoadingBancas; | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Gestión de Bancas Previas (Composición Nacional)</h3> | ||||
|             <p>Define cuántas bancas retiene cada partido antes de la elección. Estos son los escaños que **no** están en juego.</p> | ||||
|             {isLoading ? <p>Cargando...</p> : ( | ||||
|                 <> | ||||
|                     <div className="table-container"> | ||||
|                         <table> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th>Agrupación Política</th> | ||||
|                                     <th>Bancas Previas Diputados (Total: {totalDiputados} / 130)</th> | ||||
|                                     <th>Bancas Previas Senadores (Total: {totalSenadores} / 48)</th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 {agrupaciones.map(agrupacion => ( | ||||
|                                     <tr key={agrupacion.id}> | ||||
|                                         <td>{agrupacion.nombre}</td> | ||||
|                                         <td> | ||||
|                                             <input | ||||
|                                                 type="number" | ||||
|                                                 min="0" | ||||
|                                                 value={editedBancas[`${agrupacion.id}-${TipoCamara.Diputados}`]?.cantidad || 0} | ||||
|                                                 onChange={e => handleInputChange(agrupacion.id, TipoCamara.Diputados, e.target.value)} | ||||
|                                             /> | ||||
|                                         </td> | ||||
|                                         <td> | ||||
|                                             <input | ||||
|                                                 type="number" | ||||
|                                                 min="0" | ||||
|                                                 value={editedBancas[`${agrupacion.id}-${TipoCamara.Senadores}`]?.cantidad || 0} | ||||
|                                                 onChange={e => handleInputChange(agrupacion.id, TipoCamara.Senadores, e.target.value)} | ||||
|                                             /> | ||||
|                                         </td> | ||||
|                                     </tr> | ||||
|                                 ))} | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
|                     <button onClick={handleSave} style={{ marginTop: '1rem' }}>Guardar Bancas Previas</button> | ||||
|                 </> | ||||
|             )} | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -1,4 +1,4 @@ | ||||
| // src/components/BancasManager.tsx
 | ||||
| // src/components/BancasProvincialesManager.tsx
 | ||||
| import { useState } from 'react'; | ||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { getBancadas, getAgrupaciones, updateBancada, type UpdateBancadaData } from '../services/apiService'; | ||||
| @@ -6,9 +6,10 @@ import type { Bancada, AgrupacionPolitica } from '../types'; | ||||
| import { OcupantesModal } from './OcupantesModal'; | ||||
| import './AgrupacionesManager.css'; | ||||
| 
 | ||||
| const ELECCION_ID_PROVINCIAL = 1; | ||||
| const camaras = ['diputados', 'senadores'] as const; | ||||
| 
 | ||||
| export const BancasManager = () => { | ||||
| export const BancasProvincialesManager = () => { | ||||
|   const [activeTab, setActiveTab] = useState<'diputados' | 'senadores'>('diputados'); | ||||
|   const [modalVisible, setModalVisible] = useState(false); | ||||
|   const [bancadaSeleccionada, setBancadaSeleccionada] = useState<Bancada | null>(null); | ||||
| @@ -19,16 +20,18 @@ export const BancasManager = () => { | ||||
|     queryFn: getAgrupaciones | ||||
|   }); | ||||
| 
 | ||||
|   // --- CORRECCIÓN CLAVE ---
 | ||||
|   // 1. La queryKey ahora incluye el eleccionId para ser única.
 | ||||
|   // 2. La función queryFn ahora pasa el ELECCION_ID_PROVINCIAL a getBancadas.
 | ||||
|   const { data: bancadas = [], isLoading, error } = useQuery<Bancada[]>({ | ||||
|     queryKey: ['bancadas', activeTab], | ||||
|     queryFn: () => getBancadas(activeTab), | ||||
|     queryKey: ['bancadas', activeTab, ELECCION_ID_PROVINCIAL], | ||||
|     queryFn: () => getBancadas(activeTab, ELECCION_ID_PROVINCIAL), | ||||
|   }); | ||||
| 
 | ||||
|   const handleAgrupacionChange = async (bancadaId: number, nuevaAgrupacionId: string | null) => { | ||||
|     const bancadaActual = bancadas.find(b => b.id === bancadaId); | ||||
|     if (!bancadaActual) return; | ||||
| 
 | ||||
|     // Si se desasigna el partido (vacante), también se limpia el ocupante
 | ||||
|     const payload: UpdateBancadaData = { | ||||
|       agrupacionPoliticaId: nuevaAgrupacionId, | ||||
|       nombreOcupante: nuevaAgrupacionId ? (bancadaActual.ocupante?.nombreOcupante ?? null) : null, | ||||
| @@ -38,7 +41,7 @@ export const BancasManager = () => { | ||||
| 
 | ||||
|     try { | ||||
|       await updateBancada(bancadaId, payload); | ||||
|       queryClient.invalidateQueries({ queryKey: ['bancadas', activeTab] }); | ||||
|       queryClient.invalidateQueries({ queryKey: ['bancadas', activeTab, ELECCION_ID_PROVINCIAL] }); | ||||
|     } catch (err) { | ||||
|       alert("Error al guardar el cambio de agrupación."); | ||||
|     } | ||||
| @@ -49,12 +52,12 @@ export const BancasManager = () => { | ||||
|     setModalVisible(true); | ||||
|   }; | ||||
| 
 | ||||
|   if (error) return <p style={{ color: 'red' }}>Error al cargar las bancas.</p>; | ||||
|   if (error) return <p style={{ color: 'red' }}>Error al cargar las bancas provinciales.</p>; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="admin-module"> | ||||
|       <h2>Gestión de Ocupación de Bancas</h2> | ||||
|       <p>Asigne a cada banca física un partido político y, opcionalmente, los datos de la persona que la ocupa.</p> | ||||
|       <h3>Gestión de Bancas (Provinciales)</h3> | ||||
|       <p>Asigne partidos y ocupantes a las bancas de la legislatura provincial.</p> | ||||
| 
 | ||||
|       <div className="chamber-tabs"> | ||||
|         {camaras.map(camara => ( | ||||
| @@ -63,7 +66,7 @@ export const BancasManager = () => { | ||||
|             className={activeTab === camara ? 'active' : ''} | ||||
|             onClick={() => setActiveTab(camara)} | ||||
|           > | ||||
|             {camara === 'diputados' ? 'Cámara de Diputados' : 'Cámara de Senadores'} | ||||
|             {camara === 'diputados' ? 'Diputados Provinciales (92)' : 'Senadores Provinciales (46)'} | ||||
|           </button> | ||||
|         ))} | ||||
|       </div> | ||||
| @@ -81,16 +84,7 @@ export const BancasManager = () => { | ||||
|           <tbody> | ||||
|             {bancadas.map((bancada) => ( | ||||
|               <tr key={bancada.id}> | ||||
|                 {/* Usamos el NumeroBanca para la etiqueta visual */} | ||||
|                 <td> | ||||
|                   {`${activeTab.charAt(0).toUpperCase()}-${bancada.numeroBanca}`} | ||||
|                   {((activeTab === 'diputados' && bancada.numeroBanca === 92) || | ||||
|                     (activeTab === 'senadores' && bancada.numeroBanca === 46)) && ( | ||||
|                       <span style={{ marginLeft: '8px', fontSize: '0.8em', color: '#666', fontStyle: 'italic' }}> | ||||
|                         (Presidencia) | ||||
|                       </span> | ||||
|                     )} | ||||
|                 </td> | ||||
|                 <td>{`${activeTab.charAt(0).toUpperCase()}-${bancada.numeroBanca}`}</td> | ||||
|                 <td> | ||||
|                   <select | ||||
|                     value={bancada.agrupacionPoliticaId || ''} | ||||
| @@ -102,11 +96,7 @@ export const BancasManager = () => { | ||||
|                 </td> | ||||
|                 <td>{bancada.ocupante?.nombreOcupante || 'Sin asignar'}</td> | ||||
|                 <td> | ||||
|                   <button | ||||
|                     // El botón se habilita solo si hay un partido asignado a la banca
 | ||||
|                     disabled={!bancada.agrupacionPoliticaId} | ||||
|                     onClick={() => handleOpenModal(bancada)} | ||||
|                   > | ||||
|                   <button disabled={!bancada.agrupacionPoliticaId} onClick={() => handleOpenModal(bancada)}> | ||||
|                     Editar Ocupante | ||||
|                   </button> | ||||
|                 </td> | ||||
| @@ -1,96 +1,101 @@ | ||||
| // src/components/CandidatoOverridesManager.tsx | ||||
|  | ||||
| import { useState, useMemo, useEffect } from 'react'; | ||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import Select from 'react-select'; | ||||
| import { getMunicipiosForAdmin, getAgrupaciones, getCandidatos, updateCandidatos } from '../services/apiService'; | ||||
| import type { MunicipioSimple, AgrupacionPolitica, CandidatoOverride } from '../types'; | ||||
| import { getProvinciasForAdmin, getMunicipiosForAdmin, getAgrupaciones, getCandidatos, updateCandidatos } from '../services/apiService'; | ||||
| import type { MunicipioSimple, AgrupacionPolitica, CandidatoOverride, ProvinciaSimple } from '../types'; | ||||
| import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias'; | ||||
|  | ||||
| const CATEGORIAS_OPTIONS = [ | ||||
|     { value: 5, label: 'Senadores' }, | ||||
|     { value: 6, label: 'Diputados' }, | ||||
|     { value: 7, label: 'Concejales' } | ||||
| const ELECCION_OPTIONS = [     | ||||
|     { value: 2, label: 'Elecciones Nacionales' }, | ||||
|     { value: 1, label: 'Elecciones Provinciales' } | ||||
| ]; | ||||
|  | ||||
| const AMBITO_LEVEL_OPTIONS = [ | ||||
|     { value: 'general', label: 'General (Toda la elección)' }, | ||||
|     { value: 'provincia', label: 'Por Provincia' }, | ||||
|     { value: 'municipio', label: 'Por Municipio' } | ||||
| ]; | ||||
|  | ||||
| export const CandidatoOverridesManager = () => { | ||||
|     const queryClient = useQueryClient(); | ||||
|     const { data: municipios = [] } = useQuery<MunicipioSimple[]>({ queryKey: ['municipiosForAdmin'], queryFn: getMunicipiosForAdmin }); | ||||
|     const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ queryKey: ['agrupaciones'], queryFn: getAgrupaciones }); | ||||
|     const { data: candidatos = [] } = useQuery<CandidatoOverride[]>({ queryKey: ['candidatos'], queryFn: getCandidatos }); | ||||
|  | ||||
|     const [selectedEleccion, setSelectedEleccion] = useState(ELECCION_OPTIONS[0]); | ||||
|     const [selectedAmbitoLevel, setSelectedAmbitoLevel] = useState(AMBITO_LEVEL_OPTIONS[0]); | ||||
|     const [selectedProvincia, setSelectedProvincia] = useState<ProvinciaSimple | null>(null); | ||||
|     const [selectedMunicipio, setSelectedMunicipio] = useState<MunicipioSimple | null>(null); | ||||
|     const [selectedCategoria, setSelectedCategoria] = useState<{ value: number; label: string } | null>(null); | ||||
|     const [selectedMunicipio, setSelectedMunicipio] = useState<{ value: string; label: string } | null>(null); | ||||
|     const [selectedAgrupacion, setSelectedAgrupacion] = useState<{ value: string; label: string } | null>(null); | ||||
|     const [selectedAgrupacion, setSelectedAgrupacion] = useState<AgrupacionPolitica | null>(null); | ||||
|     const [nombreCandidato, setNombreCandidato] = useState(''); | ||||
|  | ||||
|     const municipioOptions = useMemo(() =>  | ||||
|         // Añadimos la opción "General" que representará un ámbito nulo | ||||
|         [{ value: 'general', label: 'General (Todos los Municipios)' }, ...municipios.map(m => ({ value: m.id, label: m.nombre }))] | ||||
|     , [municipios]); | ||||
|      | ||||
|     const agrupacionOptions = useMemo(() => agrupaciones.map(a => ({ value: a.id, label: a.nombre })), [agrupaciones]); | ||||
|     const { data: provincias = [] } = useQuery<ProvinciaSimple[]>({ queryKey: ['provinciasForAdmin'], queryFn: getProvinciasForAdmin }); | ||||
|     const { data: municipios = [] } = useQuery<MunicipioSimple[]>({ queryKey: ['municipiosForAdmin'], queryFn: getMunicipiosForAdmin }); | ||||
|     const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ queryKey: ['agrupaciones'], queryFn: getAgrupaciones }); | ||||
|     const { data: candidatos = [] } = useQuery<CandidatoOverride[]>({ | ||||
|         queryKey: ['candidatos', selectedEleccion.value], | ||||
|         queryFn: () => getCandidatos(selectedEleccion.value), | ||||
|     }); | ||||
|  | ||||
|     const categoriaOptions = selectedEleccion.value === 2 ? CATEGORIAS_NACIONALES_OPTIONS : CATEGORIAS_PROVINCIALES_OPTIONS; | ||||
|  | ||||
|     const getAmbitoId = () => { | ||||
|         if (selectedAmbitoLevel.value === 'municipio' && selectedMunicipio) return parseInt(selectedMunicipio.id); | ||||
|         if (selectedAmbitoLevel.value === 'provincia' && selectedProvincia) return parseInt(selectedProvincia.id); | ||||
|         return null; | ||||
|     }; | ||||
|  | ||||
|     const currentCandidato = useMemo(() => { | ||||
|         if (!selectedAgrupacion || !selectedCategoria) return ''; | ||||
|          | ||||
|         // Determina si estamos buscando un override general (null) o específico (ID numérico) | ||||
|         const ambitoIdBuscado = selectedMunicipio?.value === 'general' ? null : (selectedMunicipio ? parseInt(selectedMunicipio.value) : undefined); | ||||
|  | ||||
|         // Si no se ha seleccionado un municipio, no buscamos nada | ||||
|         if (ambitoIdBuscado === undefined) return ''; | ||||
|  | ||||
|         return candidatos.find(c =>  | ||||
|             c.ambitoGeograficoId === ambitoIdBuscado &&  | ||||
|             c.agrupacionPoliticaId === selectedAgrupacion.value && | ||||
|         const ambitoId = getAmbitoId(); | ||||
|         return candidatos.find(c => | ||||
|             c.ambitoGeograficoId === ambitoId && | ||||
|             c.agrupacionPoliticaId === selectedAgrupacion.id && | ||||
|             c.categoriaId === selectedCategoria.value | ||||
|         )?.nombreCandidato || ''; | ||||
|     }, [candidatos, selectedMunicipio, selectedAgrupacion, selectedCategoria]); | ||||
|      | ||||
|     useEffect(() => { setNombreCandidato(currentCandidato) }, [currentCandidato]); | ||||
|     }, [candidatos, selectedAmbitoLevel, selectedProvincia, selectedMunicipio, selectedAgrupacion, selectedCategoria]); | ||||
|  | ||||
|     useEffect(() => { setNombreCandidato(currentCandidato || ''); }, [currentCandidato]); | ||||
|  | ||||
|     const handleSave = async () => { | ||||
|         if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return; | ||||
|  | ||||
|         const ambitoIdParaEnviar = selectedMunicipio.value === 'general'  | ||||
|             ? null  | ||||
|             : parseInt(selectedMunicipio.value); | ||||
|  | ||||
|         if (!selectedAgrupacion || !selectedCategoria) return; | ||||
|         const newCandidatoEntry: CandidatoOverride = { | ||||
|             id: 0, // El backend no lo necesita para el upsert | ||||
|             agrupacionPoliticaId: selectedAgrupacion.value, | ||||
|             id: 0, | ||||
|             eleccionId: selectedEleccion.value, | ||||
|             agrupacionPoliticaId: selectedAgrupacion.id, | ||||
|             categoriaId: selectedCategoria.value, | ||||
|             ambitoGeograficoId: ambitoIdParaEnviar, | ||||
|             ambitoGeograficoId: getAmbitoId(), | ||||
|             nombreCandidato: nombreCandidato || null | ||||
|         }; | ||||
|  | ||||
|         try { | ||||
|             await updateCandidatos([newCandidatoEntry]); | ||||
|             queryClient.invalidateQueries({ queryKey: ['candidatos'] }); | ||||
|             queryClient.invalidateQueries({ queryKey: ['candidatos', selectedEleccion.value] }); | ||||
|             alert('Override de candidato guardado.'); | ||||
|         } catch (error) { | ||||
|             console.error(error); | ||||
|             alert('Error al guardar el override del candidato.'); | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Overrides de Nombres de Candidatos</h3> | ||||
|             <p>Configure un nombre de candidato específico para un partido, categoría y municipio (o general).</p> | ||||
|             <div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end', flexWrap: 'wrap' }}> | ||||
|             <p>Configure un nombre de candidato específico para un partido en un contexto determinado.</p> | ||||
|             <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', alignItems: 'flex-end' }}> | ||||
|                 <Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => { setSelectedEleccion(opt!); setSelectedCategoria(null); }} /> | ||||
|                 <Select options={categoriaOptions} value={selectedCategoria} onChange={setSelectedCategoria} placeholder="Seleccione Categoría..." isDisabled={!selectedEleccion} /> | ||||
|                 <Select options={agrupaciones.map(a => ({ value: a.id, label: a.nombre, ...a }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedAgrupacion} onChange={setSelectedAgrupacion} placeholder="Seleccione Agrupación..." /> | ||||
|                 <Select options={AMBITO_LEVEL_OPTIONS} value={selectedAmbitoLevel} onChange={(opt) => { setSelectedAmbitoLevel(opt!); setSelectedProvincia(null); setSelectedMunicipio(null); }} /> | ||||
|  | ||||
|                 {selectedAmbitoLevel.value === 'provincia' || selectedAmbitoLevel.value === 'municipio' ? ( | ||||
|                     <Select options={provincias.map(p => ({ value: p.id, label: p.nombre, ...p }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedProvincia} onChange={setSelectedProvincia} placeholder="Seleccione Provincia..." /> | ||||
|                 ) : <div />} | ||||
|  | ||||
|                 {selectedAmbitoLevel.value === 'municipio' ? ( | ||||
|                     <Select options={municipios.map(m => ({ value: m.id, label: m.nombre, ...m }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedMunicipio} onChange={setSelectedMunicipio} placeholder="Seleccione Municipio..." isDisabled={!selectedProvincia} /> | ||||
|                 ) : <div />} | ||||
|             </div> | ||||
|             <div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end', marginTop: '1rem' }}> | ||||
|                 <div style={{ flex: 1 }}> | ||||
|                     <label>Categoría</label> | ||||
|                     <Select options={CATEGORIAS_OPTIONS} value={selectedCategoria} onChange={setSelectedCategoria} isClearable placeholder="Seleccione..."/> | ||||
|                 </div> | ||||
|                 <div style={{ flex: 1 }}> | ||||
|                     <label>Municipio (Opcional)</label> | ||||
|                     <Select options={municipioOptions} value={selectedMunicipio} onChange={setSelectedMunicipio} isClearable placeholder="General..."/> | ||||
|                 </div> | ||||
|                 <div style={{ flex: 1 }}> | ||||
|                     <label>Agrupación</label> | ||||
|                     <Select options={agrupacionOptions} value={selectedAgrupacion} onChange={setSelectedAgrupacion} isClearable placeholder="Seleccione..."/> | ||||
|                 </div> | ||||
|                 <div style={{ flex: 2 }}> | ||||
|                     <label>Nombre del Candidato</label> | ||||
|                     <input type="text" value={nombreCandidato} onChange={e => setNombreCandidato(e.target.value)} style={{ width: '100%' }} disabled={!selectedAgrupacion || !selectedCategoria} /> | ||||
|                 </div> | ||||
|   | ||||
| @@ -0,0 +1,110 @@ | ||||
| // src/components/ConfiguracionNacional.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { useQueryClient } from '@tanstack/react-query'; | ||||
| import { getAgrupaciones, getConfiguracion, updateConfiguracion } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica } from '../types'; | ||||
| import './AgrupacionesManager.css'; | ||||
|  | ||||
| export const ConfiguracionNacional = () => { | ||||
|     const queryClient = useQueryClient(); | ||||
|     const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); | ||||
|     const [loading, setLoading] = useState(true); | ||||
|      | ||||
|     const [presidenciaDiputadosId, setPresidenciaDiputadosId] = useState<string>(''); | ||||
|     const [presidenciaSenadoId, setPresidenciaSenadoId] = useState<string>(''); | ||||
|     const [modoOficialActivo, setModoOficialActivo] = useState(false); | ||||
|     const [diputadosTipoBanca, setDiputadosTipoBanca] = useState<'ganada' | 'previa'>('ganada'); | ||||
|     // El estado para el tipo de banca del senado ya no es necesario para la UI,  | ||||
|     // pero lo mantenemos para no romper el handleSave. | ||||
|     const [senadoTipoBanca, setSenadoTipoBanca] = useState<'ganada' | 'previa'>('ganada'); | ||||
|  | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const loadInitialData = async () => { | ||||
|             try { | ||||
|                 setLoading(true); | ||||
|                 const [agrupacionesData, configData] = await Promise.all([getAgrupaciones(), getConfiguracion()]); | ||||
|                 setAgrupaciones(agrupacionesData); | ||||
|                 setPresidenciaDiputadosId(configData.PresidenciaDiputadosNacional || ''); | ||||
|                 setPresidenciaSenadoId(configData.PresidenciaSenadoNacional || ''); | ||||
|                 setModoOficialActivo(configData.UsarDatosOficialesNacionales === 'true'); | ||||
|                 setDiputadosTipoBanca(configData.PresidenciaDiputadosNacional_TipoBanca === 'previa' ? 'previa' : 'ganada'); | ||||
|                 setSenadoTipoBanca(configData.PresidenciaSenadoNacional_TipoBanca === 'previa' ? 'previa' : 'ganada'); | ||||
|             } catch (err) { console.error("Error al cargar datos de configuración nacional:", err); }  | ||||
|             finally { setLoading(false); } | ||||
|         }; | ||||
|         loadInitialData(); | ||||
|     }, []); | ||||
|  | ||||
|     const handleSave = async () => { | ||||
|         try { | ||||
|             await updateConfiguracion({ | ||||
|                 "PresidenciaDiputadosNacional": presidenciaDiputadosId, | ||||
|                 "PresidenciaSenadoNacional": presidenciaSenadoId, | ||||
|                 "UsarDatosOficialesNacionales": modoOficialActivo.toString(), | ||||
|                 "PresidenciaDiputadosNacional_TipoBanca": diputadosTipoBanca, | ||||
|                 // Aunque no se muestre, guardamos el valor para consistencia | ||||
|                 "PresidenciaSenadoNacional_TipoBanca": senadoTipoBanca, | ||||
|             }); | ||||
|             queryClient.invalidateQueries({ queryKey: ['composicionNacional'] }); | ||||
|             alert('Configuración nacional guardada.'); | ||||
|         } catch { alert('Error al guardar.'); } | ||||
|     }; | ||||
|  | ||||
|     if (loading) return <div className="admin-module"><p>Cargando...</p></div>; | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Configuración de Widgets Nacionales</h3> | ||||
|             {/*<div className="form-group"> | ||||
|                 <label> | ||||
|                     <input type="checkbox" checked={modoOficialActivo} onChange={e => setModoOficialActivo(e.target.checked)} /> | ||||
|                     **Activar Modo "Resultados Oficiales" para Widgets Nacionales** | ||||
|                 </label> | ||||
|                 <p style={{ fontSize: '0.8rem', color: '#666' }}> | ||||
|                     Si está activo, los widgets nacionales usarán la composición manual de bancas. Si no, usarán la proyección en tiempo real. | ||||
|                 </p> | ||||
|             </div>*/} | ||||
|              | ||||
|             <div style={{ display: 'flex', gap: '2rem', marginTop: '1rem' }}> | ||||
|                 {/* Columna Diputados */} | ||||
|                 <div style={{ flex: 1, borderRight: '1px solid #ccc', paddingRight: '1rem' }}> | ||||
|                     <label htmlFor="presidencia-diputados-nacional" style={{ display: 'block', fontWeight: 'bold', marginBottom: '0.5rem' }}> | ||||
|                         Presidencia Cámara de Diputados | ||||
|                     </label> | ||||
|                     <p style={{ fontSize: '0.8rem', color: '#666', margin: '0.5rem 0 0 0' }}> | ||||
|                         Este escaño es parte de los 257 diputados y se descuenta del total del partido. | ||||
|                     </p> | ||||
|                     <select id="presidencia-diputados-nacional" value={presidenciaDiputadosId} onChange={e => setPresidenciaDiputadosId(e.target.value)} style={{ width: '100%', padding: '8px', marginBottom: '0.5rem' }}> | ||||
|                         <option value="">-- No Asignado --</option> | ||||
|                         {agrupaciones.map(a => (<option key={a.id} value={a.id}>{a.nombre}</option>))} | ||||
|                     </select> | ||||
|                     {presidenciaDiputadosId && ( | ||||
|                         <div> | ||||
|                             <label><input type="radio" value="ganada" checked={diputadosTipoBanca === 'ganada'} onChange={() => setDiputadosTipoBanca('ganada')} /> Descontar de Banca Ganada</label> | ||||
|                             <label style={{marginLeft: '1rem'}}><input type="radio" value="previa" checked={diputadosTipoBanca === 'previa'} onChange={() => setDiputadosTipoBanca('previa')} /> Descontar de Banca Previa</label> | ||||
|                         </div> | ||||
|                     )}                     | ||||
|                 </div> | ||||
|  | ||||
|                 {/* Columna Senadores */} | ||||
|                 <div style={{ flex: 1 }}> | ||||
|                     <label htmlFor="presidencia-senado-nacional" style={{ display: 'block', fontWeight: 'bold', marginBottom: '0.5rem' }}> | ||||
|                         Presidencia Senado (Vicepresidente) | ||||
|                     </label> | ||||
|                     <p style={{ fontSize: '0.8rem', color: '#666', margin: '0.5rem 0 0 0' }}> | ||||
|                         Este escaño es adicional a los 72 senadores y no se descuenta del total del partido. | ||||
|                     </p> | ||||
|                     <select id="presidencia-senado-nacional" value={presidenciaSenadoId} onChange={e => setPresidenciaSenadoId(e.target.value)} style={{ width: '100%', padding: '8px' }}> | ||||
|                         <option value="">-- No Asignado --</option> | ||||
|                         {agrupaciones.map(a => (<option key={a.id} value={a.id}>{a.nombre}</option>))} | ||||
|                     </select>                     | ||||
|                 </div> | ||||
|             </div> | ||||
|              | ||||
|             <button onClick={handleSave} style={{ marginTop: '1.5rem' }}> | ||||
|                 Guardar Configuración | ||||
|             </button> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -1,43 +1,89 @@ | ||||
| // src/components/DashboardPage.tsx | ||||
| import { useAuth } from '../context/AuthContext'; | ||||
| import { AgrupacionesManager } from './AgrupacionesManager'; | ||||
| import { OrdenDiputadosManager } from './OrdenDiputadosManager'; | ||||
| import { OrdenSenadoresManager } from './OrdenSenadoresManager'; | ||||
| import { ConfiguracionGeneral } from './ConfiguracionGeneral'; | ||||
| import { BancasManager } from './BancasManager'; | ||||
| //import { OrdenDiputadosManager } from './OrdenDiputadosManager'; | ||||
| //import { OrdenSenadoresManager } from './OrdenSenadoresManager'; | ||||
| //import { ConfiguracionGeneral } from './ConfiguracionGeneral'; | ||||
| import { LogoOverridesManager } from './LogoOverridesManager'; | ||||
| import { CandidatoOverridesManager } from './CandidatoOverridesManager'; | ||||
| import { WorkerManager } from './WorkerManager'; | ||||
| import { ConfiguracionNacional } from './ConfiguracionNacional'; | ||||
| import { BancasPreviasManager } from './BancasPreviasManager'; | ||||
| import { OrdenDiputadosNacionalesManager } from './OrdenDiputadosNacionalesManager'; | ||||
| import { OrdenSenadoresNacionalesManager } from './OrdenSenadoresNacionalesManager'; | ||||
| //import { BancasProvincialesManager } from './BancasProvincialesManager'; | ||||
| //import { BancasNacionalesManager } from './BancasNacionalesManager'; | ||||
|  | ||||
|  | ||||
| export const DashboardPage = () => { | ||||
|     const { logout } = useAuth(); | ||||
|  | ||||
|     const sectionStyle = { | ||||
|         border: '1px solid #dee2e6', | ||||
|         borderRadius: '8px', | ||||
|         padding: '1.5rem', | ||||
|         marginBottom: '2rem', | ||||
|         backgroundColor: '#f8f9fa' | ||||
|     }; | ||||
|  | ||||
|     const sectionTitleStyle = { | ||||
|         marginTop: 0, | ||||
|         borderBottom: '2px solid #007bff', | ||||
|         paddingBottom: '0.5rem', | ||||
|         marginBottom: '1.5rem', | ||||
|         color: '#007bff' | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div style={{ padding: '1rem 2rem' }}> | ||||
|             <header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '2px solid #eee', paddingBottom: '1rem' }}> | ||||
|             <header style={{ /* ... */ }}> | ||||
|                 <h1>Panel de Administración Electoral</h1> | ||||
|                 <button onClick={logout}>Cerrar Sesión</button> | ||||
|             </header> | ||||
|              | ||||
|             <main style={{ marginTop: '2rem' }}> | ||||
|                 <AgrupacionesManager /> | ||||
|                     <div style={{ flex: '1 1 800px' }}> | ||||
|                         <LogoOverridesManager /> | ||||
|                     </div> | ||||
|                     <div style={{ flex: '1 1 800px' }}> | ||||
|                         <CandidatoOverridesManager /> | ||||
|                     </div> | ||||
|                 <div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', marginTop: '2rem' }}> | ||||
|                     <div style={{ flex: '1 1 400px' }}> | ||||
|                         <OrdenDiputadosManager /> | ||||
|                     </div> | ||||
|                     <div style={{ flex: '1 1 400px' }}> | ||||
|                         <OrdenSenadoresManager /> | ||||
|                     </div> | ||||
|  | ||||
|                 <div style={sectionStyle}> | ||||
|                     <h2 style={sectionTitleStyle}>Configuración Global</h2> | ||||
|                     <AgrupacionesManager /> | ||||
|                     <LogoOverridesManager /> | ||||
|                     <CandidatoOverridesManager /> | ||||
|                 </div> | ||||
|                  | ||||
|                 <div style={sectionStyle}> | ||||
|                     <h2 style={sectionTitleStyle}>Gestión de Elecciones Nacionales</h2> | ||||
|                     <ConfiguracionNacional /> | ||||
|                     <BancasPreviasManager /> | ||||
|                     <div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', marginTop: '2rem' }}> | ||||
|                         <div style={{ flex: '1 1 400px' }}> | ||||
|                             <OrdenDiputadosNacionalesManager /> | ||||
|                         </div> | ||||
|                         <div style={{ flex: '1 1 400px' }}> | ||||
|                             <OrdenSenadoresNacionalesManager /> | ||||
|                         </div> | ||||
|                     </div>                     | ||||
|                    {/* <BancasNacionalesManager /> */} | ||||
|                 </div> | ||||
|                  | ||||
|                 {/* | ||||
|                 <div style={sectionStyle}> | ||||
|                     <h2 style={sectionTitleStyle}>Gestión de Elecciones Provinciales</h2> | ||||
|                     <ConfiguracionGeneral /> | ||||
|                     <BancasProvincialesManager /> | ||||
|                     <div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', marginTop: '2rem' }}> | ||||
|                         <div style={{ flex: '1 1 400px' }}> | ||||
|                             <OrdenDiputadosManager /> | ||||
|                         </div> | ||||
|                         <div style={{ flex: '1 1 400px' }}> | ||||
|                             <OrdenSenadoresManager /> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div>*/} | ||||
|  | ||||
|                 <div style={sectionStyle}> | ||||
|                     <h2 style={sectionTitleStyle}>Gestión de Workers y Sistema</h2> | ||||
|                     <WorkerManager /> | ||||
|                 </div> | ||||
|                 <ConfiguracionGeneral /> | ||||
|                 <BancasManager /> | ||||
|                 <hr style={{ margin: '2rem 0' }}/> | ||||
|                 <WorkerManager /> | ||||
|             </main> | ||||
|         </div> | ||||
|     ); | ||||
|   | ||||
| @@ -2,83 +2,104 @@ | ||||
| import { useState, useMemo, useEffect } from 'react'; | ||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import Select from 'react-select'; | ||||
| import { getMunicipiosForAdmin, getAgrupaciones, getLogos, updateLogos } from '../services/apiService'; | ||||
| import type { MunicipioSimple, AgrupacionPolitica, LogoAgrupacionCategoria } from '../types'; | ||||
| import { getProvinciasForAdmin, getMunicipiosForAdmin, getAgrupaciones, getLogos, updateLogos } from '../services/apiService'; | ||||
| import type { MunicipioSimple, AgrupacionPolitica, LogoAgrupacionCategoria, ProvinciaSimple } from '../types'; | ||||
| import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias'; | ||||
|  | ||||
| // --- AÑADIMOS LAS CATEGORÍAS PARA EL SELECTOR --- | ||||
| const CATEGORIAS_OPTIONS = [ | ||||
|     { value: 5, label: 'Senadores' }, | ||||
|     { value: 6, label: 'Diputados' }, | ||||
|     { value: 7, label: 'Concejales' } | ||||
| const ELECCION_OPTIONS = [ | ||||
|     { value: 2, label: 'Elecciones Nacionales' }, | ||||
|     { value: 1, label: 'Elecciones Provinciales' } | ||||
| ]; | ||||
|  | ||||
| const AMBITO_LEVEL_OPTIONS = [ | ||||
|     { value: 'general', label: 'General (Toda la elección)' }, | ||||
|     { value: 'provincia', label: 'Por Provincia' }, | ||||
|     { value: 'municipio', label: 'Por Municipio' } | ||||
| ]; | ||||
|  | ||||
| export const LogoOverridesManager = () => { | ||||
|     const queryClient = useQueryClient(); | ||||
|     const { data: municipios = [] } = useQuery<MunicipioSimple[]>({ queryKey: ['municipiosForAdmin'], queryFn: getMunicipiosForAdmin }); | ||||
|     const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ queryKey: ['agrupaciones'], queryFn: getAgrupaciones }); | ||||
|     const { data: logos = [] } = useQuery<LogoAgrupacionCategoria[]>({ queryKey: ['logos'], queryFn: getLogos }); | ||||
|  | ||||
|     // --- NUEVO ESTADO PARA LA CATEGORÍA --- | ||||
|     // --- ESTADOS --- | ||||
|     const [selectedEleccion, setSelectedEleccion] = useState(ELECCION_OPTIONS[0]); | ||||
|     const [selectedAmbitoLevel, setSelectedAmbitoLevel] = useState(AMBITO_LEVEL_OPTIONS[0]); | ||||
|     const [selectedProvincia, setSelectedProvincia] = useState<ProvinciaSimple | null>(null); | ||||
|     const [selectedMunicipio, setSelectedMunicipio] = useState<MunicipioSimple | null>(null); | ||||
|     const [selectedCategoria, setSelectedCategoria] = useState<{ value: number; label: string } | null>(null); | ||||
|     const [selectedMunicipio, setSelectedMunicipio] = useState<{ value: string; label: string } | null>(null); | ||||
|     const [selectedAgrupacion, setSelectedAgrupacion] = useState<{ value: string; label: string } | null>(null); | ||||
|     const [selectedAgrupacion, setSelectedAgrupacion] = useState<AgrupacionPolitica | null>(null); | ||||
|     const [logoUrl, setLogoUrl] = useState(''); | ||||
|  | ||||
|     const municipioOptions = useMemo(() =>  | ||||
|         [{ value: 'general', label: 'General (Todas las secciones)' }, ...municipios.map(m => ({ value: m.id, label: m.nombre }))] | ||||
|     , [municipios]); | ||||
|     const agrupacionOptions = useMemo(() => agrupaciones.map(a => ({ value: a.id, label: a.nombre })), [agrupaciones]); | ||||
|     // --- QUERIES --- | ||||
|     const { data: provincias = [] } = useQuery<ProvinciaSimple[]>({ queryKey: ['provinciasForAdmin'], queryFn: getProvinciasForAdmin }); | ||||
|     const { data: municipios = [] } = useQuery<MunicipioSimple[]>({ queryKey: ['municipiosForAdmin'], queryFn: getMunicipiosForAdmin }); | ||||
|     const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ queryKey: ['agrupaciones'], queryFn: getAgrupaciones }); | ||||
|     const { data: logos = [] } = useQuery<LogoAgrupacionCategoria[]>({ | ||||
|         queryKey: ['logos', selectedEleccion.value], | ||||
|         queryFn: () => getLogos(selectedEleccion.value) | ||||
|     }); | ||||
|  | ||||
|     // --- LÓGICA DE SELECTORES DINÁMICOS --- | ||||
|     const categoriaOptions = selectedEleccion.value === 2 ? CATEGORIAS_NACIONALES_OPTIONS : CATEGORIAS_PROVINCIALES_OPTIONS; | ||||
|  | ||||
|     const getAmbitoId = () => { | ||||
|         if (selectedAmbitoLevel.value === 'municipio' && selectedMunicipio) return parseInt(selectedMunicipio.id); | ||||
|         if (selectedAmbitoLevel.value === 'provincia' && selectedProvincia) return parseInt(selectedProvincia.id); | ||||
|         return null; | ||||
|     }; | ||||
|  | ||||
|     const currentLogo = useMemo(() => { | ||||
|         // La búsqueda ahora depende de los 3 selectores | ||||
|         if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return ''; | ||||
|         return logos.find(l =>  | ||||
|             l.ambitoGeograficoId === parseInt(selectedMunicipio.value) &&  | ||||
|             l.agrupacionPoliticaId === selectedAgrupacion.value && | ||||
|         if (!selectedAgrupacion || !selectedCategoria) return ''; | ||||
|         const ambitoId = getAmbitoId(); | ||||
|         return logos.find(l => | ||||
|             l.ambitoGeograficoId === ambitoId && | ||||
|             l.agrupacionPoliticaId === selectedAgrupacion.id && | ||||
|             l.categoriaId === selectedCategoria.value | ||||
|         )?.logoUrl || ''; | ||||
|     }, [logos, selectedMunicipio, selectedAgrupacion, selectedCategoria]); | ||||
|      | ||||
|     useEffect(() => { setLogoUrl(currentLogo) }, [currentLogo]); | ||||
|     }, [logos, selectedAmbitoLevel, selectedProvincia, selectedMunicipio, selectedAgrupacion, selectedCategoria]); | ||||
|  | ||||
|     useEffect(() => { setLogoUrl(currentLogo || ''); }, [currentLogo]); | ||||
|  | ||||
|     const handleSave = async () => { | ||||
|         if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return; | ||||
|         if (!selectedAgrupacion || !selectedCategoria) return; | ||||
|         const newLogoEntry: LogoAgrupacionCategoria = { | ||||
|             id: 0, | ||||
|             agrupacionPoliticaId: selectedAgrupacion.value, | ||||
|             eleccionId: selectedEleccion.value, | ||||
|             agrupacionPoliticaId: selectedAgrupacion.id, | ||||
|             categoriaId: selectedCategoria.value, | ||||
|             ambitoGeograficoId: parseInt(selectedMunicipio.value), | ||||
|             ambitoGeograficoId: getAmbitoId(), | ||||
|             logoUrl: logoUrl || null | ||||
|         }; | ||||
|         try { | ||||
|             await updateLogos([newLogoEntry]); | ||||
|             queryClient.invalidateQueries({ queryKey: ['logos'] }); | ||||
|             queryClient.invalidateQueries({ queryKey: ['logos', selectedEleccion.value] }); | ||||
|             alert('Override de logo guardado.'); | ||||
|         } catch { alert('Error al guardar.'); } | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Overrides de Logos por Municipio y Categoría</h3> | ||||
|             <p>Configure una imagen específica para un partido en un municipio y categoría determinados.</p> | ||||
|             <div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end' }}> | ||||
|             <h3>Overrides de Logos</h3> | ||||
|             <p>Configure una imagen específica para un partido en un contexto determinado.</p> | ||||
|             <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', alignItems: 'flex-end' }}> | ||||
|                 <Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => { setSelectedEleccion(opt!); setSelectedCategoria(null); }} /> | ||||
|                 <Select options={categoriaOptions} value={selectedCategoria} onChange={setSelectedCategoria} placeholder="Seleccione Categoría..." isDisabled={!selectedEleccion} /> | ||||
|                 <Select options={agrupaciones.map(a => ({ value: a.id, label: a.nombre, ...a }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedAgrupacion} onChange={setSelectedAgrupacion} placeholder="Seleccione Agrupación..." /> | ||||
|                 <Select options={AMBITO_LEVEL_OPTIONS} value={selectedAmbitoLevel} onChange={(opt) => { setSelectedAmbitoLevel(opt!); setSelectedProvincia(null); setSelectedMunicipio(null); }} /> | ||||
|  | ||||
|                 {selectedAmbitoLevel.value === 'provincia' || selectedAmbitoLevel.value === 'municipio' ? ( | ||||
|                     <Select options={provincias.map(p => ({ value: p.id, label: p.nombre, ...p }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedProvincia} onChange={setSelectedProvincia} placeholder="Seleccione Provincia..." /> | ||||
|                 ) : <div />} | ||||
|  | ||||
|                 {selectedAmbitoLevel.value === 'municipio' ? ( | ||||
|                     <Select options={municipios.map(m => ({ value: m.id, label: m.nombre, ...m }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedMunicipio} onChange={setSelectedMunicipio} placeholder="Seleccione Municipio..." isDisabled={!selectedProvincia} /> | ||||
|                 ) : <div />} | ||||
|             </div> | ||||
|             <div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end', marginTop: '1rem' }}> | ||||
|                 <div style={{ flex: 1 }}> | ||||
|                     <label>Categoría</label> | ||||
|                     <Select options={CATEGORIAS_OPTIONS} value={selectedCategoria} onChange={setSelectedCategoria} isClearable placeholder="Seleccione..."/> | ||||
|                 </div> | ||||
|                 <div style={{ flex: 1 }}> | ||||
|                     <label>Municipio</label> | ||||
|                     <Select options={municipioOptions} value={selectedMunicipio} onChange={setSelectedMunicipio} isClearable placeholder="Seleccione..."/> | ||||
|                 </div> | ||||
|                 <div style={{ flex: 1 }}> | ||||
|                     <label>Agrupación</label> | ||||
|                     <Select options={agrupacionOptions} value={selectedAgrupacion} onChange={setSelectedAgrupacion} isClearable placeholder="Seleccione..."/> | ||||
|                 </div> | ||||
|                 <div style={{ flex: 2 }}> | ||||
|                     <label>URL del Logo Específico</label> | ||||
|                     <input type="text" value={logoUrl} onChange={e => setLogoUrl(e.target.value)} style={{ width: '100%' }} disabled={!selectedMunicipio || !selectedAgrupacion || !selectedCategoria} /> | ||||
|                     <input type="text" value={logoUrl} onChange={e => setLogoUrl(e.target.value)} style={{ width: '100%' }} disabled={!selectedAgrupacion || !selectedCategoria} /> | ||||
|                 </div> | ||||
|                 <button onClick={handleSave} disabled={!selectedMunicipio || !selectedAgrupacion || !selectedCategoria}>Guardar</button> | ||||
|                 <button onClick={handleSave} disabled={!selectedAgrupacion || !selectedCategoria}>Guardar</button> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
|   | ||||
| @@ -0,0 +1,102 @@ | ||||
| // src/components/OrdenDiputadosNacionalesManager.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; | ||||
| import { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy } from '@dnd-kit/sortable'; | ||||
| import { getAgrupaciones, updateOrden, getComposicionNacional } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica } from '../types'; | ||||
| import { SortableItem } from './SortableItem'; | ||||
| import './AgrupacionesManager.css'; | ||||
|  | ||||
| const ELECCION_ID_NACIONAL = 2; | ||||
|  | ||||
| export const OrdenDiputadosNacionalesManager = () => { | ||||
|     // Estado para la lista que el usuario puede ordenar | ||||
|     const [agrupacionesOrdenadas, setAgrupacionesOrdenadas] = useState<AgrupacionPolitica[]>([]); | ||||
|      | ||||
|     // Query 1: Obtener TODAS las agrupaciones para tener sus datos completos (nombre, etc.) | ||||
|     const { data: todasAgrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({ | ||||
|         queryKey: ['agrupaciones'], | ||||
|         queryFn: getAgrupaciones, | ||||
|     }); | ||||
|      | ||||
|     // Query 2: Obtener los datos de composición para saber qué partidos tienen bancas | ||||
|     const { data: composicionData, isLoading: isLoadingComposicion } = useQuery({ | ||||
|         queryKey: ['composicionNacional', ELECCION_ID_NACIONAL], | ||||
|         queryFn: () => getComposicionNacional(ELECCION_ID_NACIONAL), | ||||
|     }); | ||||
|  | ||||
|     // Este efecto se ejecuta cuando los datos de las queries estén disponibles | ||||
|     useEffect(() => { | ||||
|         // No hacemos nada hasta que ambas queries hayan cargado sus datos | ||||
|         if (!composicionData || !todasAgrupaciones || todasAgrupaciones.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Creamos un Set con los IDs de los partidos que tienen al menos una banca de diputado | ||||
|         const partidosConBancasIds = new Set( | ||||
|             composicionData.diputados.partidos | ||||
|                 .filter(p => p.bancasTotales > 0) | ||||
|                 .map(p => p.id) | ||||
|         ); | ||||
|  | ||||
|         // Filtramos la lista completa de agrupaciones, quedándonos solo con las relevantes | ||||
|         const agrupacionesFiltradas = todasAgrupaciones.filter(a => partidosConBancasIds.has(a.id)); | ||||
|  | ||||
|         // Ordenamos la lista filtrada según el orden guardado en la BD | ||||
|         agrupacionesFiltradas.sort((a, b) => (a.ordenDiputadosNacionales || 999) - (b.ordenDiputadosNacionales || 999)); | ||||
|          | ||||
|         // Actualizamos el estado que se renderiza y que el usuario puede ordenar | ||||
|         setAgrupacionesOrdenadas(agrupacionesFiltradas); | ||||
|  | ||||
|     }, [todasAgrupaciones, composicionData]); // Dependencias: se re-ejecuta si los datos cambian | ||||
|  | ||||
|     const sensors = useSensors( | ||||
|       useSensor(PointerSensor), | ||||
|       useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) | ||||
|     ); | ||||
|  | ||||
|     const handleDragEnd = (event: DragEndEvent) => { | ||||
|       const { active, over } = event; | ||||
|       if (over && active.id !== over.id) { | ||||
|         setAgrupacionesOrdenadas((items) => { | ||||
|           const oldIndex = items.findIndex((item) => item.id === active.id); | ||||
|           const newIndex = items.findIndex((item) => item.id === over.id); | ||||
|           return arrayMove(items, oldIndex, newIndex); | ||||
|         }); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     const handleSaveOrder = async () => { | ||||
|         const idsOrdenados = agrupacionesOrdenadas.map(a => a.id); | ||||
|         try { | ||||
|             await updateOrden('diputados-nacionales', idsOrdenados); | ||||
|             alert('Orden de Diputados Nacionales guardado con éxito!'); | ||||
|         } catch (error) { | ||||
|             alert('Error al guardar el orden de Diputados Nacionales.'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const isLoading = isLoadingAgrupaciones || isLoadingComposicion; | ||||
|     if (isLoading) return <p>Cargando orden de Diputados Nacionales...</p>; | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Ordenar Agrupaciones (Diputados Nacionales)</h3> | ||||
|             <p>Arrastre para reordenar. Solo se muestran los partidos con bancas.</p> | ||||
|             <p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p> | ||||
|             <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> | ||||
|               <SortableContext items={agrupacionesOrdenadas.map(a => a.id)} strategy={horizontalListSortingStrategy}> | ||||
|                 <ul className="sortable-list-horizontal"> | ||||
|                   {agrupacionesOrdenadas.map(agrupacion => ( | ||||
|                     <SortableItem key={agrupacion.id} id={agrupacion.id}> | ||||
|                       {agrupacion.nombreCorto || agrupacion.nombre} | ||||
|                     </SortableItem> | ||||
|                   ))} | ||||
|                 </ul> | ||||
|               </SortableContext> | ||||
|             </DndContext> | ||||
|             <button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden</button> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -0,0 +1,94 @@ | ||||
| // src/components/OrdenSenadoresNacionalesManager.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; | ||||
| import { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy } from '@dnd-kit/sortable'; | ||||
| import { getAgrupaciones, updateOrden, getComposicionNacional } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica } from '../types'; | ||||
| import { SortableItem } from './SortableItem'; | ||||
| import './AgrupacionesManager.css'; | ||||
|  | ||||
| const ELECCION_ID_NACIONAL = 2; | ||||
|  | ||||
| export const OrdenSenadoresNacionalesManager = () => { | ||||
|     const [agrupacionesOrdenadas, setAgrupacionesOrdenadas] = useState<AgrupacionPolitica[]>([]); | ||||
|      | ||||
|     const { data: todasAgrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({ | ||||
|         queryKey: ['agrupaciones'], | ||||
|         queryFn: getAgrupaciones, | ||||
|     }); | ||||
|      | ||||
|     const { data: composicionData, isLoading: isLoadingComposicion } = useQuery({ | ||||
|         queryKey: ['composicionNacional', ELECCION_ID_NACIONAL], | ||||
|         queryFn: () => getComposicionNacional(ELECCION_ID_NACIONAL), | ||||
|     }); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!composicionData || !todasAgrupaciones || todasAgrupaciones.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Creamos un Set con los IDs de los partidos que tienen al menos una banca de senador | ||||
|         const partidosConBancasIds = new Set( | ||||
|             composicionData.senadores.partidos | ||||
|                 .filter(p => p.bancasTotales > 0) | ||||
|                 .map(p => p.id) | ||||
|         ); | ||||
|  | ||||
|         const agrupacionesFiltradas = todasAgrupaciones.filter(a => partidosConBancasIds.has(a.id)); | ||||
|  | ||||
|         agrupacionesFiltradas.sort((a, b) => (a.ordenSenadoresNacionales || 999) - (b.ordenSenadoresNacionales || 999)); | ||||
|          | ||||
|         setAgrupacionesOrdenadas(agrupacionesFiltradas); | ||||
|  | ||||
|     }, [todasAgrupaciones, composicionData]); | ||||
|  | ||||
|     const sensors = useSensors( | ||||
|       useSensor(PointerSensor), | ||||
|       useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) | ||||
|     ); | ||||
|  | ||||
|     const handleDragEnd = (event: DragEndEvent) => { | ||||
|       const { active, over } = event; | ||||
|       if (over && active.id !== over.id) { | ||||
|         setAgrupacionesOrdenadas((items) => { | ||||
|           const oldIndex = items.findIndex((item) => item.id === active.id); | ||||
|           const newIndex = items.findIndex((item) => item.id === over.id); | ||||
|           return arrayMove(items, oldIndex, newIndex); | ||||
|         }); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     const handleSaveOrder = async () => { | ||||
|         const idsOrdenados = agrupacionesOrdenadas.map(a => a.id); | ||||
|         try { | ||||
|             await updateOrden('senadores-nacionales', idsOrdenados); | ||||
|             alert('Orden de Senadores Nacionales guardado con éxito!'); | ||||
|         } catch (error) { | ||||
|             alert('Error al guardar el orden de Senadores Nacionales.'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const isLoading = isLoadingAgrupaciones || isLoadingComposicion; | ||||
|     if (isLoading) return <p>Cargando orden de Senadores Nacionales...</p>; | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Ordenar Agrupaciones (Senado de la Nación)</h3> | ||||
|             <p>Arrastre para reordenar. Solo se muestran los partidos con bancas.</p> | ||||
|             <p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p> | ||||
|             <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> | ||||
|               <SortableContext items={agrupacionesOrdenadas.map(a => a.id)} strategy={horizontalListSortingStrategy}> | ||||
|                 <ul className="sortable-list-horizontal"> | ||||
|                   {agrupacionesOrdenadas.map(agrupacion => ( | ||||
|                     <SortableItem key={agrupacion.id} id={agrupacion.id}> | ||||
|                       {agrupacion.nombreCorto || agrupacion.nombre} | ||||
|                     </SortableItem> | ||||
|                   ))} | ||||
|                 </ul> | ||||
|               </SortableContext> | ||||
|             </DndContext> | ||||
|             <button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden</button> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										23
									
								
								Elecciones-Web/frontend-admin/src/constants/categorias.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| // src/constants/categorias.ts | ||||
|  | ||||
| // Opciones para los selectores en el panel de administración | ||||
| export const CATEGORIAS_ADMIN_OPTIONS = [ | ||||
|     // Nacionales | ||||
|     { value: 1, label: 'Senadores Nacionales' }, | ||||
|     { value: 2, label: 'Diputados Nacionales' }, | ||||
|     // Provinciales | ||||
|     { value: 5, label: 'Senadores Provinciales' }, | ||||
|     { value: 6, label: 'Diputados Provinciales' }, | ||||
|     { value: 7, label: 'Concejales' }, | ||||
| ]; | ||||
|  | ||||
| export const CATEGORIAS_NACIONALES_OPTIONS = [ | ||||
|     { value: 1, label: 'Senadores Nacionales' }, | ||||
|     { value: 2, label: 'Diputados Nacionales' }, | ||||
| ]; | ||||
|  | ||||
| export const CATEGORIAS_PROVINCIALES_OPTIONS = [ | ||||
|     { value: 5, label: 'Senadores Provinciales' }, | ||||
|     { value: 6, label: 'Diputados Provinciales' }, | ||||
|     { value: 7, label: 'Concejales' }, | ||||
| ]; | ||||
| @@ -1,11 +1,12 @@ | ||||
| // src/services/apiService.ts | ||||
| import axios from 'axios'; | ||||
| import { triggerLogout } from '../context/authUtils'; | ||||
| import type { CandidatoOverride, AgrupacionPolitica, UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria, MunicipioSimple } from '../types'; | ||||
| import type { CandidatoOverride, AgrupacionPolitica, | ||||
|   UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria, | ||||
|   MunicipioSimple, BancaPrevia, ProvinciaSimple } from '../types'; | ||||
|  | ||||
| /** | ||||
|  * URL base para las llamadas a la API. | ||||
|  * Se usa para construir las URLs más específicas. | ||||
|  */ | ||||
| const API_URL_BASE = import.meta.env.DEV | ||||
|   ? 'http://localhost:5217/api' | ||||
| @@ -21,13 +22,19 @@ export const AUTH_API_URL = `${API_URL_BASE}/auth`; | ||||
|  */ | ||||
| export const ADMIN_API_URL = `${API_URL_BASE}/admin`; | ||||
|  | ||||
| // Cliente de API para endpoints de administración (requiere token) | ||||
| const adminApiClient = axios.create({ | ||||
|   baseURL: ADMIN_API_URL, | ||||
| }); | ||||
|  | ||||
| // --- INTERCEPTORES --- | ||||
| // Cliente de API para endpoints públicos (no envía token) | ||||
| const apiClient = axios.create({ | ||||
|     baseURL: API_URL_BASE, | ||||
|     headers: { 'Content-Type': 'application/json' }, | ||||
| }); | ||||
|  | ||||
| // Interceptor de Peticiones: Añade el token JWT a cada llamada | ||||
|  | ||||
| // --- INTERCEPTORES (Solo para el cliente de admin) --- | ||||
| adminApiClient.interceptors.request.use( | ||||
|   (config) => { | ||||
|     const token = localStorage.getItem('admin-jwt-token'); | ||||
| @@ -39,7 +46,6 @@ adminApiClient.interceptors.request.use( | ||||
|   (error) => Promise.reject(error) | ||||
| ); | ||||
|  | ||||
| // Interceptor de Respuestas: Maneja la expiración del token (error 401) | ||||
| adminApiClient.interceptors.response.use( | ||||
|   (response) => response, | ||||
|   (error) => { | ||||
| @@ -51,6 +57,32 @@ adminApiClient.interceptors.response.use( | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| // --- INTERFACES PARA COMPOSICIÓN NACIONAL (NECESARIAS PARA EL NUEVO MÉTODO) --- | ||||
| export interface PartidoComposicionNacional { | ||||
|     id: string; | ||||
|     nombre: string; | ||||
|     nombreCorto: string | null; | ||||
|     color: string | null; | ||||
|     bancasFijos: number; | ||||
|     bancasGanadas: number; | ||||
|     bancasTotales: number; | ||||
|     ordenDiputadosNacionales: number | null; | ||||
|     ordenSenadoresNacionales: number | null; | ||||
| } | ||||
| export interface CamaraComposicionNacional { | ||||
|     camaraNombre: string; | ||||
|     totalBancas: number; | ||||
|     bancasEnJuego: number; | ||||
|     partidos: PartidoComposicionNacional[]; | ||||
|     presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null; | ||||
| } | ||||
| export interface ComposicionNacionalData { | ||||
|     diputados: CamaraComposicionNacional; | ||||
|     senadores: CamaraComposicionNacional; | ||||
| } | ||||
|  | ||||
|  | ||||
| // --- SERVICIOS DE API --- | ||||
|  | ||||
| // 1. Autenticación | ||||
| @@ -66,7 +98,7 @@ export const loginUser = async (credentials: LoginCredentials): Promise<string | | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // 2. Agrupaciones Políticas | ||||
| // 2. Agrupaciones | ||||
| export const getAgrupaciones = async (): Promise<AgrupacionPolitica[]> => { | ||||
|   const response = await adminApiClient.get('/agrupaciones'); | ||||
|   return response.data; | ||||
| @@ -77,14 +109,14 @@ export const updateAgrupacion = async (id: string, data: UpdateAgrupacionData): | ||||
| }; | ||||
|  | ||||
| // 3. Ordenamiento de Agrupaciones | ||||
| export const updateOrden = async (camara: 'diputados' | 'senadores', ids: string[]): Promise<void> => { | ||||
| export const updateOrden = async (camara: 'diputados' | 'senadores' | 'diputados-nacionales' | 'senadores-nacionales', ids: string[]): Promise<void> => { | ||||
|   await adminApiClient.put(`/agrupaciones/orden-${camara}`, ids); | ||||
| }; | ||||
|  | ||||
| // 4. Gestión de Bancas y Ocupantes | ||||
| export const getBancadas = async (camara: 'diputados' | 'senadores'): Promise<Bancada[]> => { | ||||
|   const camaraId = camara === 'diputados' ? 0 : 1; | ||||
|   const response = await adminApiClient.get(`/bancadas/${camaraId}`); | ||||
| // 4. Gestión de Bancas | ||||
| export const getBancadas = async (camara: 'diputados' | 'senadores', eleccionId: number): Promise<Bancada[]> => { | ||||
|   const camaraId = (camara === 'diputados') ? 0 : 1; | ||||
|   const response = await adminApiClient.get(`/bancadas/${camaraId}?eleccionId=${eleccionId}`); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| @@ -111,38 +143,52 @@ export const updateConfiguracion = async (data: Record<string, string>): Promise | ||||
|   await adminApiClient.put('/configuracion', data); | ||||
| }; | ||||
|  | ||||
| export const getLogos = async (): Promise<LogoAgrupacionCategoria[]> => { | ||||
|   const response = await adminApiClient.get('/logos'); | ||||
| // 6. Logos y Candidatos | ||||
| export const getLogos = async (eleccionId: number): Promise<LogoAgrupacionCategoria[]> => { | ||||
|   const response = await adminApiClient.get(`/logos?eleccionId=${eleccionId}`); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| export const updateLogos = async (data: LogoAgrupacionCategoria[]): Promise<void> => { | ||||
|   await adminApiClient.put('/logos', data); | ||||
| }; | ||||
|  | ||||
| export const getMunicipiosForAdmin = async (): Promise<MunicipioSimple[]> => { | ||||
|   // Ahora usa adminApiClient, que apunta a /api/admin/ | ||||
|   // La URL final será /api/admin/catalogos/municipios | ||||
|   const response = await adminApiClient.get('/catalogos/municipios'); | ||||
| export const getCandidatos = async (eleccionId: number): Promise<CandidatoOverride[]> => { | ||||
|   const response = await adminApiClient.get(`/candidatos?eleccionId=${eleccionId}`); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| // 6. Overrides de Candidatos | ||||
| export const getCandidatos = async (): Promise<CandidatoOverride[]> => { | ||||
|   const response = await adminApiClient.get('/candidatos'); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| export const updateCandidatos = async (data: CandidatoOverride[]): Promise<void> => { | ||||
|   await adminApiClient.put('/candidatos', data); | ||||
| }; | ||||
|  | ||||
| // 7. Gestión de Logging | ||||
| export interface UpdateLoggingLevelData { | ||||
|   level: string; | ||||
| } | ||||
| // 7. Catálogos | ||||
| export const getMunicipiosForAdmin = async (): Promise<MunicipioSimple[]> => { | ||||
|   const response = await adminApiClient.get('/catalogos/municipios'); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| // 8. Logging | ||||
| export interface UpdateLoggingLevelData { level: string; } | ||||
| export const updateLoggingLevel = async (data: UpdateLoggingLevelData): Promise<void> => { | ||||
|   // Este endpoint es específico, no es parte de la configuración general | ||||
|   await adminApiClient.put(`/logging-level`, data); | ||||
| }; | ||||
|  | ||||
| // 9. Bancas Previas | ||||
| export const getBancasPrevias = async (eleccionId: number): Promise<BancaPrevia[]> => { | ||||
|     const response = await adminApiClient.get(`/bancas-previas/${eleccionId}`); | ||||
|     return response.data; | ||||
| }; | ||||
| export const updateBancasPrevias = async (eleccionId: number, data: BancaPrevia[]): Promise<void> => { | ||||
|     await adminApiClient.put(`/bancas-previas/${eleccionId}`, data); | ||||
| }; | ||||
|  | ||||
| // 10. Obtener Composición Nacional (Endpoint Público) | ||||
| export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => { | ||||
|     // Este es un endpoint público, por lo que usamos el cliente sin token de admin. | ||||
|     const response = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| // Obtenemos las provincias para el selector de ámbito | ||||
| export const getProvinciasForAdmin = async (): Promise<ProvinciaSimple[]> => { | ||||
| const response = await adminApiClient.get('/catalogos/provincias'); | ||||
| return response.data; | ||||
| }; | ||||
| @@ -8,6 +8,9 @@ export interface AgrupacionPolitica { | ||||
|   color: string | null; | ||||
|   ordenDiputados: number | null; | ||||
|   ordenSenadores: number | null; | ||||
|   // Añadimos los nuevos campos para el ordenamiento nacional | ||||
|   ordenDiputadosNacionales: number | null; | ||||
|   ordenSenadoresNacionales: number | null; | ||||
| } | ||||
|  | ||||
| export interface UpdateAgrupacionData { | ||||
| @@ -30,9 +33,9 @@ export interface OcupanteBanca { | ||||
|   periodo: string | null; | ||||
| } | ||||
|  | ||||
| // Nueva interfaz para la Bancada | ||||
| export interface Bancada { | ||||
|   id: number; | ||||
|   eleccionId: number; // Clave para diferenciar provinciales de nacionales | ||||
|   camara: TipoCamaraValue; | ||||
|   numeroBanca: number; | ||||
|   agrupacionPoliticaId: string | null; | ||||
| @@ -40,8 +43,20 @@ export interface Bancada { | ||||
|   ocupante: OcupanteBanca | null; | ||||
| } | ||||
|  | ||||
| // Nueva interfaz para Bancas Previas | ||||
| export interface BancaPrevia { | ||||
|     id: number; | ||||
|     eleccionId: number; | ||||
|     camara: TipoCamaraValue; | ||||
|     agrupacionPoliticaId: string; | ||||
|     agrupacionPolitica?: AgrupacionPolitica; // Opcional para la UI | ||||
|     cantidad: number; | ||||
| } | ||||
|  | ||||
|  | ||||
| export interface LogoAgrupacionCategoria { | ||||
|     id: number; | ||||
|     eleccionId: number; // Clave para diferenciar | ||||
|     agrupacionPoliticaId: string; | ||||
|     categoriaId: number; | ||||
|     logoUrl: string | null; | ||||
| @@ -50,8 +65,11 @@ export interface LogoAgrupacionCategoria { | ||||
|  | ||||
| export interface MunicipioSimple { id: string; nombre: string; } | ||||
|  | ||||
| export interface ProvinciaSimple { id: string; nombre: string; } | ||||
|  | ||||
| export interface CandidatoOverride { | ||||
|   id: number; | ||||
|   eleccionId: number; // Clave para diferenciar | ||||
|   agrupacionPoliticaId: string; | ||||
|   categoriaId: number; | ||||
|   ambitoGeograficoId: number | null; | ||||
|   | ||||
							
								
								
									
										482
									
								
								Elecciones-Web/frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -18,6 +18,8 @@ | ||||
|     "axios": "^1.11.0", | ||||
|     "d3-geo": "^3.1.1", | ||||
|     "d3-shape": "^3.2.0", | ||||
|     "highcharts": "^12.4.0", | ||||
|     "highcharts-react-official": "^3.2.2", | ||||
|     "react": "^19.1.1", | ||||
|     "react-circular-progressbar": "^2.2.0", | ||||
|     "react-dom": "^19.1.1", | ||||
| @@ -26,7 +28,8 @@ | ||||
|     "react-select": "^5.10.2", | ||||
|     "react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support", | ||||
|     "react-tooltip": "^5.29.1", | ||||
|     "topojson-client": "^3.1.0" | ||||
|     "topojson-client": "^3.1.0", | ||||
|     "vite-plugin-svgr": "^4.5.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@eslint/js": "^9.33.0", | ||||
|   | ||||
| After Width: | Height: | Size: 9.8 KiB | 
| After Width: | Height: | Size: 7.7 KiB | 
							
								
								
									
										23
									
								
								Elecciones-Web/frontend/public/maps/provincias-svg/chaco.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.0 KiB | 
| After Width: | Height: | Size: 12 KiB | 
| @@ -0,0 +1,23 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
|  | ||||
| <svg | ||||
|    width="1.3493201mm" | ||||
|    height="1.6933239mm" | ||||
|    viewBox="0 0 1.3493201 1.6933238" | ||||
|    version="1.1" | ||||
|    id="svg1" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg"> | ||||
|   <defs | ||||
|      id="defs1" /> | ||||
|   <g | ||||
|      id="layer1" | ||||
|      transform="translate(-103.9813,-147.63758)"> | ||||
|     <path | ||||
|        d="m 105.33062,148.53708 -0.0264,0.0794 -0.1323,0.13229 -0.1852,0.0265 -0.15875,0.15875 -0.21167,0.39687 -0.52917,-0.44979 -0.10583,-0.37042 0.13229,-0.58208 0.34396,-0.29104 0.13229,0.0794 0.10583,0.0529 0.10584,0.0794 0.18521,0.13229 0.0794,0.0794 0.10583,0.15875 0.0794,0.15875 z" | ||||
|        id="ARC" | ||||
|        name="Ciudad de Buenos Aires" | ||||
|        style="stroke-width:0.264583" /> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 821 B | 
| @@ -0,0 +1,23 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
|  | ||||
| <svg | ||||
|    width="22.092972mm" | ||||
|    height="36.143562mm" | ||||
|    viewBox="0 0 22.092972 36.143562" | ||||
|    version="1.1" | ||||
|    id="svg1" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg"> | ||||
|   <defs | ||||
|      id="defs1" /> | ||||
|   <g | ||||
|      id="layer1" | ||||
|      transform="translate(-93.662753,-130.4396)"> | ||||
|     <path | ||||
|        d="m 95.646872,134.9375 1.190625,-0.3175 0.264583,-0.15875 0.47625,-0.50271 0.582083,-0.66146 0.05292,-0.10583 0.02646,-0.13229 -0.15875,-1.45521 0.05292,-0.21167 0.370417,-0.13229 1.5875,-0.39687 1.03188,-0.29105 0.60854,-0.13229 h 0.21167 l 1.08479,0.26459 0.0529,0.0264 v 0.0265 l 0.0265,0.0794 -0.0265,0.15875 v 0.0794 l 0.0265,0.0529 0.0794,0.0529 0.39687,0.23812 0.0265,0.0265 0.0529,0.0794 0.0529,0.0529 0.0265,0.0265 0.34396,0.0529 0.0529,0.0265 0.0265,0.0265 0.0794,0.0529 0.0529,0.0529 0.0529,0.0265 h 0.0529 l 0.3175,0.0265 h 0.10583 l 0.635,-0.10583 0.0794,0.0265 0.39687,0.10583 0.0529,0.0265 0.21167,-0.0529 h 0.0529 l 0.0265,0.0265 0.0265,0.0265 v 0.0265 0.0265 0.0265 l -0.0265,0.18521 v 0.0265 l 0.0265,0.0529 h 0.0529 l 0.1852,0.0265 0.0529,0.0264 0.0529,0.0265 v 0.0265 0.0529 0.10583 l 0.0265,0.0265 0.0264,0.0529 0.1323,0.0264 h 3.12208 l 1.85208,-0.0264 0.89959,0.0529 0.26458,0.15875 0.84667,2.2225 -0.26459,1.66688 0.0265,0.15875 0.0265,0.10583 1.21708,1.34938 0.26459,0.37041 -0.0529,0.37042 -1.40229,5.26521 -0.10584,0.21166 -0.23812,0.21167 -0.21167,0.21167 -0.0794,0.0794 v 0.0794 0.0265 l 0.0794,0.15875 0.0265,0.0265 v 0.0265 0.0529 0.0794 l -0.0265,0.29105 v 0.0794 l 0.0265,0.39688 0.0265,0.0264 v 0.0265 0.0265 l 0.0794,0.13229 0.0529,0.0529 0.0265,0.0265 v 0.0529 0.0529 l -0.0794,0.18521 -0.0529,0.10583 -0.0265,0.10584 -0.0265,0.15875 v 0.52916 l 0.13229,0.15875 0.13229,0.10584 0.15875,0.21166 0.0529,0.0265 0.15875,0.0794 0.10583,0.0794 0.0265,0.0265 0.0265,0.0529 0.15875,0.55562 0.0265,0.10583 0.13229,0.21167 0.39688,0.29104 0.0529,0.0794 0.0794,0.0794 0.0265,0.0529 0.0264,0.13229 0.0529,0.21167 v 0.21166 l 0.0265,0.0265 0.0265,0.0529 0.0265,0.0265 0.0529,0.0529 0.0265,0.0265 0.0265,0.0529 v 0.0794 l -0.0265,0.0529 v 0.0265 0.0529 l -0.0265,0.0529 -0.0265,0.0265 -0.0794,0.10583 v 0.0265 0.0529 l 0.0265,0.0265 h 0.0529 l 0.15875,0.0265 0.0265,0.0265 H 115.2 l 0.0265,0.0265 v 0.0265 l 0.15875,0.29104 0.10583,0.21167 v 0.0529 l 0.0265,0.0265 0.15875,0.10584 0.0265,0.0265 v 0.0529 l 0.0529,0.0794 v 0.0529 0.0529 0.0529 l -0.0264,0.10583 -0.0794,0.15875 -0.0265,0.13229 v 0.39688 0.15875 l -0.0265,0.13229 -0.0265,0.0265 -0.0794,0.15875 -0.18521,0.21166 -0.0794,0.0794 -0.0794,0.0265 -0.0265,0.0265 h -0.0264 l -0.0794,0.0264 h -0.0529 l -0.0794,-0.0264 -0.13229,0.21166 -1.24354,2.01084 -1.66688,2.7252 -2.2225,3.6248 -2.67229,-0.0265 -0.18521,0.23812 -0.0265,1.08479 -0.0265,2.83105 h -3.12208 -2.91041 -3.28084 v -0.0265 -6.87917 l -0.264584,-4.97417 0.02646,-0.0529 0.02646,-0.0794 0.15875,-0.29104 0.02646,-0.0265 v -0.0265 l 0.02646,-0.0264 h 0.05292 l 0.132292,-0.0265 0.02646,-0.0265 0.05292,-0.0265 0.02646,-0.0529 v -0.0264 -0.0529 l -0.05292,-0.13229 v -0.10584 l 0.02646,-0.0794 0.132292,-0.29104 0.02646,-0.0794 v -0.0529 l -0.02646,-0.10584 -0.02646,-0.0794 0.02646,-0.0529 v -0.0529 l 0.211666,-0.39687 0.02646,-0.0529 v -0.0529 -0.23813 l 0.02646,-0.0529 v -0.0529 l 0.07937,-0.13229 0.02646,-0.0529 v -0.0265 -0.10583 -0.0794 -0.0265 l 0.02646,-0.0529 0.07938,-0.0794 0.02646,-0.0265 0.02646,-0.0529 0.02646,-0.0529 0.02646,-0.26458 0.132292,-0.29104 0.02646,-0.18521 0.02646,-0.21167 v -0.0529 l -0.02646,-0.0529 -0.02646,-0.13229 -0.02646,-0.10584 -0.02646,-0.13229 -0.132292,-0.23812 -0.02646,-0.0794 v -0.0529 -0.15875 l 0.02646,-0.15875 v -0.1852 -0.18521 l -0.02646,-0.0794 v -0.0794 h -0.02646 l -0.02646,-0.0265 -0.05292,-0.0265 h -0.02646 -0.02646 l -0.105833,0.0265 -0.555625,0.15875 -0.582084,0.0794 -0.07937,-0.0265 -0.02646,-0.0265 -0.02646,-0.0265 v -0.0529 -0.0529 -0.26459 l -0.02646,-0.0794 v -0.0265 l -0.07937,-0.13229 -0.05292,-0.10584 -0.05292,-0.13229 v -0.0794 -0.0794 -0.10583 -0.0265 l -0.02646,-0.10583 -0.02646,-0.0529 -0.02646,-0.0529 -0.15875,-0.13229 -0.15875,-0.18521 -1.534583,-0.9525 -0.264584,-0.13229 -0.185208,-0.0529 h -0.47625 v -1.69334 l -0.02646,-2.24896 v -1.11125 l 0.05292,-0.39687 0.370417,-1.08479 0.07937,-0.21167 0.105833,-0.3175 0.449792,-1.34937 v -0.10584 l 0.05292,-0.18521 0.132291,-0.44979 0.07937,-0.23812 0.02646,-0.0794 0.105833,-0.18521 0.07937,-0.1852 z" | ||||
|        id="ARX" | ||||
|        name="Córdoba" | ||||
|        style="stroke-width:0.264583" /> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 4.5 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 5.6 KiB | 
| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										23
									
								
								Elecciones-Web/frontend/public/maps/provincias-svg/jujuy.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.5 KiB | 
| @@ -0,0 +1,23 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
|  | ||||
| <svg | ||||
|    width="26.908112mm" | ||||
|    height="29.739168mm" | ||||
|    viewBox="0 0 26.908112 29.739168" | ||||
|    version="1.1" | ||||
|    id="svg1" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg"> | ||||
|   <defs | ||||
|      id="defs1" /> | ||||
|   <g | ||||
|      id="layer1" | ||||
|      transform="translate(-91.281242,-133.61455)"> | ||||
|     <path | ||||
|        d="m 91.545834,150.99771 v -5.00063 l -0.05292,-3.78354 -0.15875,-0.82021 -0.02646,-0.0794 -0.02646,-0.10584 0.02646,-0.29104 0.05292,-0.44979 0.07937,-0.0794 0.105834,-0.0529 h 6.085416 0.423334 2.434162 3.01625 0.21167 4.89479 0.21167 0.0264 v -1.05833 l 0.0265,-5.66209 h 3.28084 2.91041 3.12208 v 1.82563 1.50812 l -0.0264,3.04271 v 0.23813 2.83104 1.53458 1.53458 2.9898 l 0.0264,3.09562 -0.0264,0.3175 0.0264,3.41313 v 3.88937 l -0.0264,3.51896 -0.47625,-0.0529 -0.21167,-0.0794 -0.15875,-0.15875 -0.71437,-0.50271 -0.18521,-0.0794 -0.10584,-0.13229 -0.13229,-0.23812 -0.0794,-0.0794 -0.0794,-0.0529 -0.21166,-0.1323 -0.37042,-0.0794 -0.10583,-0.0529 -0.47625,-0.58208 -0.0794,-0.0794 -0.26458,-0.0265 -0.10584,-0.0529 -0.39687,-0.26458 -1.77271,-0.68792 h -0.39687 l -1.50813,-0.29104 -0.10583,0.0529 -0.18521,-0.0529 -0.39688,0.0529 -0.13229,-0.10583 h -0.0529 -0.74084 l -0.82021,0.15875 -0.26458,-0.0265 -0.23812,0.10584 h -0.0794 l -0.0794,-0.0265 -0.21167,-0.15875 -0.47625,-0.18521 -0.29104,-0.0529 h -0.15875 l -0.10584,0.0794 -0.0794,0.0794 -0.13229,0.0794 -0.10584,0.0529 h -0.10583 l -0.0529,-0.0265 -0.0529,-0.0529 -0.0529,-0.0529 -0.0794,-0.0265 h -0.23813 -0.0529 l -0.10583,-0.10583 -0.0529,-0.0265 -0.0794,-0.0265 -0.21167,-0.0265 -0.18521,-0.0794 -0.15875,-0.0265 -0.10583,-0.0794 h -0.0529 l -1.40229,-0.18521 -0.74083,0.13229 h -0.23813 l -0.635,-0.18521 h -0.15875 l -0.13229,-0.0529 -0.0529,-0.10584 -0.0529,-0.34396 -0.0529,-0.21166 -0.13229,-0.18521 -0.18521,-0.15875 -0.18521,-0.10583 -0.238117,-0.0529 -0.05292,-0.0265 -0.105833,-0.0794 -0.02646,-0.0265 -0.05292,-0.0265 -0.211667,-0.13229 -0.264583,-0.0265 -0.105834,-0.0529 -0.185208,-0.13229 -0.238125,-0.0529 -0.555625,-0.29104 h -0.05292 l -0.02646,-0.0529 -0.07937,-0.10584 -0.07937,-0.0794 -0.211667,-0.29104 -0.132291,-0.37042 v -0.0794 -0.15875 l -0.02646,-0.10583 -0.05292,-0.10583 -0.132292,-0.0529 h -0.105833 l -0.47625,0.0794 -0.238125,0.15875 -0.15875,0.0529 -0.396875,-0.0529 -0.15875,0.0794 -0.396875,0.0265 -0.211667,-0.10583 -0.185208,-0.18521 -0.15875,-0.21167 -0.291042,-0.635 -0.15875,-0.15875 -0.211667,-0.10583 h -0.05292 l -0.15875,0.0529 h -0.05292 l -0.07937,-0.0794 h -0.05292 l -0.05292,-0.0529 -0.05292,-0.15875 -0.02646,-0.13229 -0.02646,-0.13229 0.02646,-0.34396 0.132291,-0.23812 0.15875,-0.15875 0.370417,-0.26459 0.132292,-0.18521 0.07937,-0.26458 v -0.3175 l -0.07937,-0.21167 -0.132292,-0.23812 -0.15875,-0.21167 -0.15875,-0.13229 -0.238125,-0.13229 z" | ||||
|        id="ARL" | ||||
|        name="La Pampa" | ||||
|        style="stroke-width:0.264583" /> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 2.9 KiB | 
| After Width: | Height: | Size: 7.2 KiB | 
| After Width: | Height: | Size: 8.4 KiB | 
| After Width: | Height: | Size: 7.4 KiB | 
| After Width: | Height: | Size: 9.1 KiB | 
| After Width: | Height: | Size: 7.6 KiB | 
							
								
								
									
										23
									
								
								Elecciones-Web/frontend/public/maps/provincias-svg/salta.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
| After Width: | Height: | Size: 8.7 KiB | 
| @@ -0,0 +1,23 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
|  | ||||
| <svg | ||||
|    width="13.838294mm" | ||||
|    height="27.438555mm" | ||||
|    viewBox="0 0 13.838294 27.438555" | ||||
|    version="1.1" | ||||
|    id="svg1" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg"> | ||||
|   <defs | ||||
|      id="defs1" /> | ||||
|   <g | ||||
|      id="layer1" | ||||
|      transform="translate(-97.895573,-134.6725)"> | ||||
|     <path | ||||
|        d="m 101.54708,134.85812 0.37042,0.0794 0.13229,0.0529 0.15875,0.0794 0.0529,0.0265 h 0.0265 0.0529 0.10584 l 0.10583,-0.0529 0.0529,-0.0265 0.0265,0.0265 0.0529,0.0265 0.13229,0.0794 h 0.0265 l 0.29104,-0.0529 h 0.0529 l 0.0529,0.0265 0.0794,0.0265 0.13229,0.0794 h 0.0529 0.0529 l 0.1852,-0.0529 h 0.0794 l 0.26458,0.0265 0.26459,-0.0265 0.29104,-0.10583 0.21166,-0.13229 0.1323,-0.0529 0.26458,-0.0529 h 0.21167 l 0.26458,0.0529 h 0.0529 l 0.0264,0.0265 0.0794,0.0529 0.0529,0.0265 h 0.0264 l 0.10584,0.0265 h 0.26458 l 0.52917,-0.10583 h 0.47625 l 0.18521,0.0529 0.26458,0.13229 1.53458,0.9525 0.15875,0.18521 0.15875,0.13229 0.0265,0.0529 0.0265,0.0529 0.0265,0.10584 v 0.0264 0.10584 0.0794 0.0794 l 0.0529,0.13229 0.0529,0.10583 0.0794,0.13229 v 0.0265 l 0.0264,0.0794 v 0.26458 0.0529 0.0529 l 0.0265,0.0265 0.0265,0.0265 0.0794,0.0265 0.58208,-0.0794 0.55562,-0.15875 0.10584,-0.0265 h 0.0265 0.0264 l 0.0529,0.0265 0.0265,0.0265 h 0.0265 v 0.0794 l 0.0265,0.0794 v 0.18521 0.18521 l -0.0265,0.15875 v 0.15875 0.0529 l 0.0265,0.0794 0.13229,0.23813 0.0265,0.13229 0.0265,0.10583 0.0265,0.13229 0.0265,0.0529 v 0.0529 l -0.0265,0.21166 -0.0265,0.18521 -0.13229,0.29104 -0.0265,0.26459 -0.0265,0.0529 -0.0265,0.0529 -0.0265,0.0265 -0.0794,0.0794 -0.0264,0.0529 v 0.0265 0.0794 0.10584 0.0264 l -0.0265,0.0529 -0.0794,0.13229 v 0.0529 l -0.0265,0.0529 v 0.23812 0.0529 l -0.0264,0.0529 -0.21167,0.39688 v 0.0529 l -0.0265,0.0529 0.0265,0.0794 0.0265,0.10583 v 0.0529 l -0.0265,0.0794 -0.13229,0.29104 -0.0265,0.0794 v 0.10583 l 0.0529,0.13229 v 0.0529 0.0265 l -0.0265,0.0529 -0.0529,0.0265 -0.0265,0.0265 -0.13229,0.0265 h -0.0529 l -0.0265,0.0265 v 0.0265 l -0.0265,0.0265 -0.15875,0.29104 -0.0265,0.0794 -0.0265,0.0529 0.26458,4.97417 v 6.87916 0.0265 l -0.0265,5.66208 v 1.05834 h -0.0265 -0.21167 -4.89479 -0.21167 -3.01625 l -0.0529,-0.0265 -0.0265,-0.21167 v -0.10583 l 0.0794,-0.3175 0.0265,-0.50271 0.23813,-1.05833 0.0529,-0.68792 0.18521,-0.37042 0.0265,-0.13229 v -0.60854 l 0.0794,-0.42333 v -0.10584 l -0.0794,-0.42333 0.0265,-0.0265 0.0265,-0.0529 0.0265,-0.0794 v -0.0794 l -0.0265,-0.0529 -0.0265,-0.0529 -0.0529,-0.0794 0.10583,-0.39688 V 156.21 l -0.0265,-0.10583 -0.10584,-0.21167 -0.0265,-0.0794 -0.0265,-0.10583 -0.0794,-0.26458 v -0.0794 l 0.0529,-0.34396 -0.0265,-0.13229 -0.13229,-0.50271 -0.66146,-1.29645 -0.23813,-0.29105 -0.0529,-0.10583 -0.0265,-0.13229 -0.13229,-0.42333 -0.0265,-0.58209 -0.0529,-0.13229 -0.0794,-0.0529 -0.0529,-0.10583 0.0794,-0.13229 -0.0794,-0.13229 V 150.495 l -0.0529,-0.15875 v -0.0794 l 0.0794,-0.13229 0.0265,-0.10583 0.0265,-0.0265 h 0.0264 l 0.0794,0.0529 h 0.0265 l 0.0794,-0.10584 0.0265,-0.13229 0.0265,-0.34396 0.0265,-0.21166 v -0.10584 l -0.10584,-0.15875 -0.0264,-0.21166 -0.0529,-0.10584 -0.0794,-0.0529 -0.0794,-0.0529 -0.0794,-0.10583 -0.0529,-0.29104 -0.0529,-0.1323 -0.10583,-0.0794 -0.0794,-0.0794 -0.18521,-0.13229 -0.10583,-0.0794 -0.0529,-0.0794 v -0.13229 l -0.13229,-0.44979 -0.13229,-0.21167 -0.0794,-0.29104 -0.0265,-0.0794 -0.07938,-0.21167 -0.02646,-0.0794 -0.105833,-0.10583 -0.529167,-0.89958 -0.05292,-0.15875 v -0.13229 -0.29105 -0.10583 l -0.132291,-0.3175 v -0.23812 l -0.05292,-0.0529 -0.02646,-0.18521 0.02646,-0.50271 -0.02646,-0.60854 -0.05292,-0.13229 -0.02646,-0.0529 v -0.0794 l 0.02646,-0.13229 v -0.0794 l -0.105833,-0.26458 -0.05292,-0.18521 0.02646,-0.0794 0.105834,-0.0794 v -0.13229 l -0.07937,-0.29104 0.02646,-0.0529 0.02646,-0.18521 0.105834,-0.18521 -0.02646,-0.0794 -0.05292,-0.0794 -0.02646,-0.10583 -0.02646,-0.10583 -0.05292,-0.0529 -0.05292,-0.0529 -0.05292,-0.0794 -0.05292,-0.0529 -0.02646,-0.10583 -0.105833,-0.52917 -0.02646,-0.0794 v -0.10584 l -0.07937,-0.18521 -0.02646,-0.1852 -0.02646,-0.0529 -0.02646,-0.0529 -0.02646,-0.0529 -0.02646,-0.0265 v -0.10584 -0.0529 l -0.05292,-0.0794 -0.05292,-0.0529 -0.132291,-0.10584 -0.132292,-0.58208 -0.02646,-0.29104 v -0.42334 -0.21166 -0.39688 -0.10583 l -0.02646,-0.13229 0.02646,-0.0529 v -0.0794 l 0.07937,-0.29105 0.02646,-0.0265 v -0.0529 l -0.02646,-0.0794 v -0.0529 l -0.02646,-0.0265 v -0.0265 l 0.02646,-0.10583 -0.02646,-0.0529 -0.02646,-0.15875 v -0.0794 -0.0265 -0.0529 l 0.02646,-0.0529 0.02646,-0.0529 0.05292,-0.0529 0.132292,-0.0794 0.238125,0.0529 0.15875,0.0529 h 0.07937 l 0.132292,0.0265 0.449791,-0.0529 0.185209,-0.0529 h 0.05292 0.07937 l 0.105834,0.0265 0.07937,0.0265 0.05292,0.0265 0.05292,0.0794 h 0.02646 l 0.02646,0.0265 h 0.05292 l 0.185209,0.0529 z" | ||||
|        id="ARD" | ||||
|        name="San Luis" | ||||
|        style="stroke-width:0.264583" /> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 4.9 KiB | 
| After Width: | Height: | Size: 12 KiB | 
| After Width: | Height: | Size: 5.8 KiB | 
| @@ -0,0 +1,23 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
|  | ||||
| <svg | ||||
|    width="19.023661mm" | ||||
|    height="28.363354mm" | ||||
|    viewBox="0 0 19.023661 28.363354" | ||||
|    version="1.1" | ||||
|    id="svg1" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg"> | ||||
|   <defs | ||||
|      id="defs1" /> | ||||
|   <g | ||||
|      id="layer1" | ||||
|      transform="translate(-95.24991,-134.40833)"> | ||||
|     <path | ||||
|        d="m 99.059995,138.29771 0.02646,-0.21167 0.02646,-0.0265 0.185208,-0.39687 0.05292,-0.10584 v -0.0529 -0.21167 l 0.02646,-0.0529 0.02646,-0.10583 1.164167,-2.59292 0.10583,-0.13229 0.21167,0.0265 1.05833,0.34396 0.18521,0.0529 2.88396,0.0529 h 9.02229 l 0.15875,0.50271 0.0794,2.46063 v 0.68791 3.28084 6.27062 1.16417 l -0.3175,2.2225 -0.52917,3.4925 -0.47625,3.04271 -0.26458,1.66687 -0.47625,3.09563 -0.84667,-2.2225 -0.26458,-0.15875 -0.89959,-0.0529 -1.85208,0.0265 h -3.12208 l -0.1323,-0.0265 -0.0264,-0.0529 -0.0265,-0.0265 v -0.10583 -0.0529 -0.0264 l -0.0529,-0.0265 -0.0529,-0.0265 -0.1852,-0.0265 h -0.0529 l -0.0265,-0.0529 v -0.0264 l 0.0265,-0.18521 v -0.0265 -0.0265 -0.0265 l -0.0265,-0.0265 -0.0265,-0.0265 h -0.0529 l -0.21167,0.0529 -0.0529,-0.0265 -0.39687,-0.10583 -0.0794,-0.0265 -0.635,0.10584 h -0.10583 l -0.3175,-0.0265 h -0.0529 l -0.0529,-0.0265 -0.0529,-0.0529 -0.0794,-0.0529 -0.0265,-0.0265 -0.0529,-0.0265 -0.34396,-0.0529 -0.0265,-0.0265 -0.0529,-0.0529 -0.0529,-0.0794 -0.0265,-0.0265 -0.39687,-0.23812 -0.0794,-0.0529 -0.0265,-0.0529 v -0.0794 l 0.0265,-0.15875 -0.0265,-0.0794 v -0.0265 l -0.0529,-0.0265 -1.08479,-0.26458 h -0.21167 l -0.608543,0.13229 -1.031875,0.29104 -1.5875,0.39687 -0.132292,-0.15875 -0.05292,-0.13229 V 158.67 l -0.05292,-0.13229 -0.370417,-0.635 -0.238125,-0.58208 -0.238125,-1.82563 0.02646,-0.52916 -0.105833,-1.21709 v -0.10583 l -0.02646,-0.0265 -0.02646,-0.0265 -0.105833,-0.10583 -0.211667,-0.13229 h -0.05292 l -0.02646,-0.0265 v -0.0265 l -0.02646,-0.0265 v -0.21166 -0.0529 l 0.02646,-0.0265 0.105834,-0.0529 0.02646,-0.0265 0.05292,-0.13229 0.07937,-0.1323 0.132291,-0.13229 0.07937,-0.10583 0.05292,-0.15875 0.05292,-0.34396 -0.05292,-0.92604 -0.47625,-2.27542 0.05292,-0.0529 0.07937,-0.0265 h 0.02646 0.05292 l 0.07937,0.0265 h 0.05292 0.07937 l 0.05292,-0.0265 0.05292,-0.0529 0.05292,-0.10583 0.15875,-0.50271 0.05292,-0.0529 0.02646,-0.0265 h 0.07937 0.02646 l -0.02646,-0.0794 -0.132291,-0.3175 -0.238125,-0.55563 v -0.13229 l 0.05292,-0.18521 h 0.05292 l 0.02646,-0.0264 h 0.105834 0.05292 0.02646 0.02646 l 0.02646,-0.0265 0.05292,-0.0265 0.02646,-0.0265 0.02646,-0.0529 0.05292,-0.10583 0.02646,-0.0529 v -0.0529 h -0.02646 l -0.105833,-0.0794 -0.105833,-0.0529 h -0.07937 l -0.105834,-0.0265 -0.07937,-0.0529 -0.02646,-0.0265 0.343958,-0.39687 0.07937,-0.0794 0.264584,-0.3175 0.07937,-0.0794 0.05292,-0.0265 0.132292,-0.0265 0.02646,-0.0265 0.02646,-0.0529 0.132291,-0.37042 0.02646,-0.0265 0.105833,-0.13229 0.15875,-0.44979 0.05292,-0.26459 0.07937,-0.34395 0.02646,-0.0529 0.02646,-0.0529 0.05292,-0.0529 0.02646,-0.0529 h 0.05292 l 0.02646,-0.0265 0.07937,-0.15875 0.185209,-0.76729 0.07937,-0.26458 0.02646,-0.10584 H 98.081 l 0.02646,-0.0265 h 0.07937 0.07937 0.02646 l 0.02646,-0.0529 0.02646,-0.0529 0.15875,-0.58208 0.02646,-0.0529 0.07937,-0.0265 0.343959,0.0529 0.02646,-0.0265 v -0.10583 -0.23813 l -0.02646,-0.44979 -0.105834,-0.635 v -0.0529 l 0.02646,-0.0529 0.185208,-0.66146 0.02646,-0.18521 z" | ||||
|        id="ARG" | ||||
|        name="Santiago del Estero" | ||||
|        style="stroke-width:0.264583" /> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 3.4 KiB | 
| After Width: | Height: | Size: 5.7 KiB | 
| @@ -0,0 +1,23 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
|  | ||||
| <svg | ||||
|    width="9.3935156mm" | ||||
|    height="12.197932mm" | ||||
|    viewBox="0 0 9.3935156 12.197932" | ||||
|    version="1.1" | ||||
|    id="svg1" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg"> | ||||
|   <defs | ||||
|      id="defs1" /> | ||||
|   <g | ||||
|      id="layer1" | ||||
|      transform="translate(-100.01234,-142.34548)"> | ||||
|     <path | ||||
|        d="m 100.77979,143.45708 1.61396,0.26459 0.1852,0.0265 0.0529,-0.0265 0.0265,-0.0265 0.0265,-0.0265 0.0265,-0.0265 0.0264,-0.0529 v -0.0265 -0.0265 -0.0529 -0.21167 -0.0794 l 0.0265,-0.13229 0.0529,-0.10584 v -0.13229 -0.23812 -0.0794 l 0.0265,-0.0794 v -0.0265 l 0.0265,-0.0265 0.0529,-0.0265 h 0.0265 l 0.0265,0.0265 0.13229,0.0529 0.0265,0.0265 0.26458,0.0794 0.13229,-0.0265 h 0.0794 l 0.52917,0.13229 h 0.0794 0.0265 l 0.0265,-0.0264 0.29104,-0.15875 0.15875,-0.0529 0.0529,-0.0265 h 0.0529 l 0.0265,0.0265 h 0.0265 l 0.0265,0.0265 0.0794,0.15875 0.0265,0.15875 0.0529,0.0794 0.0265,0.0794 0.0794,0.0529 1.05833,0.42334 0.0794,-0.0265 h 0.0529 l 0.10583,0.0265 0.10583,0.0794 0.18521,0.0794 0.10583,0.0265 0.0794,0.0265 0.0265,-0.0265 h 0.0794 l 0.0529,-0.0265 h 0.0265 l 0.10583,-0.0794 0.10583,-0.10583 0.0529,-0.0265 0.26458,-0.10583 0.18521,-0.0265 0.29104,0.0794 h 0.0794 l 1.16417,-0.0265 0.0264,0.42333 -0.0264,0.18521 -0.18521,0.66146 -0.0265,0.0529 v 0.0529 l 0.10583,0.635 0.0265,0.44979 v 0.23813 0.10583 l -0.0265,0.0265 -0.34396,-0.0529 -0.0794,0.0265 -0.0265,0.0529 -0.15875,0.58208 -0.0265,0.0529 -0.0265,0.0529 h -0.0265 -0.0794 -0.0794 l -0.0265,0.0265 h -0.0529 l -0.0265,0.10583 -0.0794,0.26459 -0.18521,0.76729 -0.0794,0.15875 -0.0264,0.0265 h -0.0529 l -0.0265,0.0529 -0.0529,0.0529 -0.0264,0.0529 -0.0265,0.0529 -0.0794,0.34396 -0.0529,0.26459 -0.15875,0.44979 -0.10584,0.13229 -0.0265,0.0265 -0.13229,0.37041 -0.0265,0.0529 -0.0264,0.0265 -0.1323,0.0265 -0.0529,0.0265 -0.0794,0.0794 -0.26458,0.3175 -0.0794,0.0794 -0.34395,0.39687 0.0264,0.0265 0.0794,0.0529 0.10583,0.0265 h 0.0794 l 0.10583,0.0529 0.10583,0.0794 h 0.0265 v 0.0529 l -0.0265,0.0529 -0.0529,0.10583 -0.0265,0.0529 -0.0265,0.0265 -0.0529,0.0265 -0.0265,0.0265 h -0.0265 -0.0265 -0.0529 -0.10583 l -0.0265,0.0265 h -0.0529 l -0.0529,0.18521 v 0.13229 l 0.23812,0.55563 0.13229,0.3175 0.0265,0.0794 h -0.0265 -0.0794 l -0.0265,0.0265 -0.0529,0.0529 -0.15875,0.50271 -0.0529,0.10583 -0.0529,0.0529 -0.0529,0.0265 h -0.0794 -0.0529 l -0.0794,-0.0265 h -0.0529 -0.0265 l -0.0794,0.0265 -0.0529,0.0529 -0.15875,0.0794 h -0.10583 -0.0794 l -0.0794,-0.0265 -0.10584,-0.0529 -0.10583,-0.0794 -0.10583,-0.10584 -0.0794,-0.0529 -0.0794,-0.0529 h -0.0529 -0.0529 l -0.44979,0.29104 -0.15875,0.18521 -0.18521,0.13229 -0.0529,0.0265 -0.13229,0.21167 -0.1323,0.29104 -0.0265,0.0529 -0.0794,-0.0264 -0.44979,-0.58209 -0.0529,-0.0794 -0.0794,-0.23813 -0.0529,-0.3175 -0.0529,-0.18521 -0.0264,-0.0265 -0.0529,-0.0529 -0.23813,-0.13229 h -0.10583 l -0.0529,0.0265 -0.0529,0.0265 -0.13229,0.0529 -0.10584,-0.0794 -0.0265,-0.0265 -0.0529,-0.0529 -0.0265,-0.0794 v -0.1323 l -0.0794,-0.23812 -0.0264,-0.0794 -0.0265,-0.0264 -0.0794,-0.0529 -0.0529,-0.0265 -0.0794,-0.0794 -0.0265,-0.0265 -0.0265,-0.0529 -0.0264,-0.0529 -0.0265,-0.26458 -0.0794,-0.15875 -0.18521,-0.89958 0.0529,-0.15875 v -0.0265 l -0.0265,-0.0529 h -0.0265 -0.0264 l -0.15875,-0.0265 -0.39688,-0.23812 -0.0794,-0.0265 -0.18521,-0.0265 -0.10584,-0.0265 -0.10583,-0.0264 -0.0529,-0.0529 -0.0264,-0.0265 v -0.0265 l 0.0264,-0.0265 0.1323,-0.21166 0.10583,-0.23813 0.15875,-0.18521 0.13229,-0.10583 0.10583,-0.13229 0.0265,-0.0529 0.0265,-0.0265 0.0265,-0.0529 0.0265,-0.0794 0.10583,-0.18521 0.13229,-0.1852 0.18521,-0.18521 0.37042,-0.39688 0.15875,-0.23812 0.0529,-0.15875 0.0265,-0.0794 v -0.0794 -0.34396 -0.0794 l 0.0794,-0.37041 v -0.0529 -0.0794 -0.0529 -0.0529 l -0.0265,-0.0265 v -0.0264 l -0.0265,-0.0529 h -0.0265 l -0.0265,-0.0265 -0.23812,-0.13229 -0.37042,-0.29104 -0.18521,-0.18521 -0.0794,-0.0529 -0.13229,-0.0529 -0.13229,-0.0529 h -0.10584 l -0.13229,-0.0529 -0.0794,-0.0529 -0.0794,-0.1323 -0.0265,-0.0794 v -0.0529 -0.10583 l 0.10584,-0.29104 0.13229,-0.26459 0.0265,-0.15875 0.0264,-0.13229 v -0.10583 l 0.0265,-0.13229 0.0265,-0.0794 0.0794,-0.0794 0.0529,-0.0794 0.0529,-0.0265 z" | ||||
|        id="ART" | ||||
|        name="Tucumán" | ||||
|        style="stroke-width:0.264583" /> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 4.3 KiB | 
| @@ -1,6 +1,9 @@ | ||||
| // src/apiService.ts | ||||
| import axios from 'axios'; | ||||
| import type { ApiResponseRankingMunicipio, ApiResponseRankingSeccion, ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker, ApiResponseResultadosPorSeccion, PanelElectoralDto } from './types/types'; | ||||
| import type { ApiResponseRankingMunicipio, ApiResponseRankingSeccion,  | ||||
|   ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple,  | ||||
|   TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker,  | ||||
|   ApiResponseResultadosPorSeccion, PanelElectoralDto, ResumenProvincia } from './types/types'; | ||||
|  | ||||
| /** | ||||
|  * URL base para las llamadas a la API. | ||||
| @@ -84,6 +87,32 @@ export interface ResultadoDetalleSeccion { | ||||
|   color: string | null; | ||||
| } | ||||
|  | ||||
| export interface PartidoComposicionNacional { | ||||
|     id: string; | ||||
|     nombre: string; | ||||
|     nombreCorto: string | null; | ||||
|     color: string | null; | ||||
|     bancasFijos: number; | ||||
|     bancasGanadas: number; | ||||
|     bancasTotales: number; | ||||
|     ordenDiputadosNacionales: number | null; | ||||
|     ordenSenadoresNacionales: number | null; | ||||
| } | ||||
|  | ||||
| export interface CamaraComposicionNacional { | ||||
|     camaraNombre: string; | ||||
|     totalBancas: number; | ||||
|     bancasEnJuego: number; | ||||
|     partidos: PartidoComposicionNacional[]; | ||||
|     presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null; | ||||
|     ultimaActualizacion: string; | ||||
| } | ||||
|  | ||||
| export interface ComposicionNacionalData { | ||||
|     diputados: CamaraComposicionNacional; | ||||
|     senadores: CamaraComposicionNacional; | ||||
| } | ||||
|  | ||||
| export const getResumenProvincial = async (eleccionId: number): Promise<CategoriaResumen[]> => { | ||||
|   const response = await apiClient.get(`/elecciones/${eleccionId}/provincia/02`); | ||||
|   return response.data; | ||||
| @@ -221,4 +250,16 @@ export const getPanelElectoral = async (eleccionId: number, ambitoId: string | n | ||||
|          | ||||
|     const { data } = await apiClient.get(url); | ||||
|     return data; | ||||
| }; | ||||
|  | ||||
| export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => { | ||||
|     const { data } = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`); | ||||
|     return data; | ||||
| }; | ||||
|  | ||||
| // 11. Endpoint para el widget de tarjetas nacionales | ||||
| export const getResumenPorProvincia = async (eleccionId: number): Promise<ResumenProvincia[]> => { | ||||
|     // Usamos el cliente público ya que son datos de resultados | ||||
|     const { data } = await apiClient.get(`/elecciones/${eleccionId}/resumen-por-provincia`); | ||||
|     return data; | ||||
| }; | ||||
| @@ -0,0 +1,339 @@ | ||||
| // src/components/common/DiputadosNacionalesLayout.tsx | ||||
| import React from 'react'; | ||||
| import type { PartidoComposicionNacional } from '../../apiService'; | ||||
|  | ||||
| // --- Interfaces Actualizadas --- | ||||
| interface DiputadosNacionalesLayoutProps { | ||||
|   partyData: PartidoComposicionNacional[]; | ||||
|   size?: number; | ||||
|   presidenteBancada?: { color: string | null } | null; // <-- Nueva Prop | ||||
| } | ||||
|  | ||||
| const PRESIDENTE_SEAT_INDEX = 0; // El escaño 'seat-0' es el del presidente | ||||
|  | ||||
| export const DiputadosNacionalesLayout: React.FC<DiputadosNacionalesLayoutProps> = ({ | ||||
|   partyData, | ||||
|   size = 800, | ||||
|   presidenteBancada, // <-- Recibimos la nueva prop | ||||
| }) => { | ||||
|   // --- ARRAY DE 257 ELEMENTOS <circle> ORDENADOS POR ID DE "seat-X" --- | ||||
|   const seatElements = [ | ||||
|     <circle key="seat-0" id="seat-0" r="15.7" cy="639.5" cx="595.3" />, | ||||
|     <circle key="seat-1" id="seat-1" r="15.7" cy="673.1" cx="109.3" />, | ||||
|     <circle key="seat-2" id="seat-2" r="15.7" cy="673.1" cx="161.7" />, | ||||
|     <circle key="seat-3" id="seat-3" r="15.7" cy="673.5" cx="214.3" />, | ||||
|     <circle key="seat-4" id="seat-4" r="15.7" cy="673.2" cx="266.5" />, | ||||
|     <circle key="seat-5" id="seat-5" r="15.7" cy="669.5" cx="319.4" />, | ||||
|     <circle key="seat-6" id="seat-6" r="15.7" cy="660" cx="370.8" />, | ||||
|     <circle key="seat-7" id="seat-7" transform="rotate(-88.1)" r="15.7" cy="77.69" cx="-634.1" />, | ||||
|     <circle key="seat-8" id="seat-8" r="15.7" cy="639" cx="109.3" />, | ||||
|     <circle key="seat-9" id="seat-9" r="15.7" cy="639" cx="161.7" />, | ||||
|     <circle key="seat-10" id="seat-10" r="15.7" cy="639.2" cx="214.3" />, | ||||
|     <circle key="seat-11" id="seat-11" r="15.7" cy="638.8" cx="266.7" />, | ||||
|     <circle key="seat-12" id="seat-12" r="15.7" cy="635.1" cx="319.4" />, | ||||
|     <circle key="seat-13" id="seat-13" r="15.7" cy="625.7" cx="371.7" />, | ||||
|     <circle key="seat-14" id="seat-14" r="15.7" cy="639" cx="424.2" />, | ||||
|     <circle key="seat-15" id="seat-15" transform="rotate(-88.1)" r="15.7" cy="77" cx="-600.18" />, | ||||
|     <circle key="seat-16" id="seat-16" r="15.7" cy="600.9" cx="109.5" />, | ||||
|     <circle key="seat-17" id="seat-17" r="15.7" cy="603.7" cx="162.1" />, | ||||
|     <circle key="seat-18" id="seat-18" r="15.7" cy="598.6" cx="215" />, | ||||
|     <circle key="seat-19" id="seat-19" r="15.7" cy="602.6" cx="267.1" />, | ||||
|     <circle key="seat-20" id="seat-20" transform="rotate(-88.1)" r="15.7" cy="76.57" cx="-562.57" />, | ||||
|     <circle key="seat-21" id="seat-21" r="15.7" cy="566.7" cx="112.2" />, | ||||
|     <circle key="seat-22" id="seat-22" r="15.7" cy="570" cx="164.7" />, | ||||
|     <circle key="seat-23" id="seat-23" r="15.7" cy="564.5" cx="218.2" />, | ||||
|     <circle key="seat-24" id="seat-24" r="15.7" cy="568.6" cx="270.9" />, | ||||
|     <circle key="seat-25" id="seat-25" r="15.7" cy="588" cx="321.1" />, | ||||
|     <circle key="seat-26" id="seat-26" transform="rotate(-88.1)" r="15.7" cy="79.88" cx="-524.51" />, | ||||
|     <circle key="seat-27" id="seat-27" transform="rotate(-5.7)" r="15.7" cy="539.19" cx="65.05" />, | ||||
|     <circle key="seat-28" id="seat-28" r="15.7" cy="535.9" cx="170" />, | ||||
|     <circle key="seat-29" id="seat-29" transform="rotate(-88.1)" r="15.7" cy="86.87" cx="-488.2" />, | ||||
|     <circle key="seat-30" id="seat-30" r="15.7" cy="497.2" cx="125.2" />, | ||||
|     <circle key="seat-31" id="seat-31" r="15.7" cy="502.8" cx="178.2" />, | ||||
|     <circle key="seat-32" id="seat-32" r="15.7" cy="525.1" cx="226.3" />, | ||||
|     <circle key="seat-33" id="seat-33" r="15.7" cy="533.1" cx="278.4" />, | ||||
|     <circle key="seat-34" id="seat-34" r="15.7" cy="554.6" cx="327.1" />, | ||||
|     <circle key="seat-35" id="seat-35" r="15.7" cy="567.9" cx="377.9" />, | ||||
|     <circle key="seat-36" id="seat-36" r="15.7" cy="596.7" cx="426" />, | ||||
|     <circle key="seat-37" id="seat-37" r="15.7" cy="453.8" cx="79.7" />, | ||||
|     <circle key="seat-38" id="seat-38" r="15.7" cy="462" cx="135.7" />, | ||||
|     <circle key="seat-39" id="seat-39" r="15.7" cy="469.3" cx="188.9" />, | ||||
|     <circle key="seat-40" id="seat-40" r="15.7" cy="492.6" cx="236.4" />, | ||||
|     <circle key="seat-41" id="seat-41" r="15.7" cy="500.6" cx="289.8" />, | ||||
|     <circle key="seat-42" id="seat-42" r="15.7" cy="511.6" cx="341.5" />, | ||||
|     <circle key="seat-43" id="seat-43" r="15.7" cy="535" cx="388.9" />, | ||||
|     <circle key="seat-44" id="seat-44" r="15.7" cy="555" cx="437.3" />, | ||||
|     <circle key="seat-45" id="seat-45" r="15.7" cy="419.3" cx="92.8" />, | ||||
|     <circle key="seat-46" id="seat-46" r="15.7" cy="429.8" cx="148.1" />, | ||||
|     <circle key="seat-47" id="seat-47" r="15.7" cy="387.4" cx="106.8" />, | ||||
|     <circle key="seat-48" id="seat-48" transform="rotate(-5.7)" r="15.7" cy="364.72" cx="89.86" />, | ||||
|     <circle key="seat-49" id="seat-49" r="15.7" cy="395.5" cx="164.4" />, | ||||
|     <circle key="seat-50" id="seat-50" r="15.7" cy="437.3" cx="202.4" />, | ||||
|     <circle key="seat-51" id="seat-51" r="15.7" cy="455.4" cx="252.1" />, | ||||
|     <circle key="seat-52" id="seat-52" r="15.7" cy="325.1" cx="144.9" />, | ||||
|     <circle key="seat-53" id="seat-53" r="15.7" cy="365.7" cx="181.3" />, | ||||
|     <circle key="seat-54" id="seat-54" r="15.7" cy="405.1" cx="218.8" />, | ||||
|     <circle key="seat-55" id="seat-55" r="15.7" cy="425.6" cx="267.7" />, | ||||
|     <circle key="seat-56" id="seat-56" r="15.7" cy="464.9" cx="306.5" />, | ||||
|     <circle key="seat-57" id="seat-57" r="15.7" cy="292.1" cx="168.7" />, | ||||
|     <circle key="seat-58" id="seat-58" r="15.7" cy="334.6" cx="202.3" />, | ||||
|     <circle key="seat-59" id="seat-59" r="15.7" cy="376.9" cx="236.7" />, | ||||
|     <circle key="seat-60" id="seat-60" r="15.7" cy="265.1" cx="190.8" />, | ||||
|     <circle key="seat-61" id="seat-61" r="15.7" cy="307.2" cx="224" />, | ||||
|     <circle key="seat-62" id="seat-62" r="15.7" cy="346.9" cx="259.3" />, | ||||
|     <circle key="seat-63" id="seat-63" r="15.7" cy="393" cx="289.6" />, | ||||
|     <circle key="seat-64" id="seat-64" r="15.7" cy="435.9" cx="323.7" />, | ||||
|     <circle key="seat-65" id="seat-65" r="15.7" cy="480.8" cx="357.3" />, | ||||
|     <circle key="seat-66" id="seat-66" r="15.7" cy="236.2" cx="218.1" />, | ||||
|     <circle key="seat-67" id="seat-67" r="15.7" cy="278.6" cx="250" />, | ||||
|     <circle key="seat-68" id="seat-68" r="15.7" cy="320.2" cx="283" />, | ||||
|     <circle key="seat-69" id="seat-69" r="15.7" cy="362" cx="315.5" />, | ||||
|     <circle key="seat-70" id="seat-70" r="15.7" cy="403.8" cx="348.7" />, | ||||
|     <circle key="seat-71" id="seat-71" r="15.7" cy="445.9" cx="381.6" />, | ||||
|     <circle key="seat-72" id="seat-72" r="15.7" cy="489" cx="415.1" />, | ||||
|     <circle key="seat-73" id="seat-73" r="15.7" cy="515.6" cx="460.7" />, | ||||
|     <circle key="seat-74" id="seat-74" r="15.7" cy="485.2" cx="491" />, | ||||
|     <circle key="seat-75" id="seat-75" r="15.7" cy="213.6" cx="243.2" />, | ||||
|     <circle key="seat-76" id="seat-76" r="15.7" cy="254.9" cx="275.3" />, | ||||
|     <circle key="seat-77" id="seat-77" r="15.7" cy="296.4" cx="307.8" />, | ||||
|     <circle key="seat-78" id="seat-78" r="15.7" cy="337.6" cx="339.9" />, | ||||
|     <circle key="seat-79" id="seat-79" r="15.7" cy="379" cx="372.5" />, | ||||
|     <circle key="seat-80" id="seat-80" r="15.7" cy="420.8" cx="405.1" />, | ||||
|     <circle key="seat-81" id="seat-81" r="15.7" cy="462.7" cx="437.2" />, | ||||
|     <circle key="seat-82" id="seat-82" r="15.5" cy="181.8" cx="283.1" />, | ||||
|     <circle key="seat-83" id="seat-83" r="15.5" cy="223.6" cx="315.4" />, | ||||
|     <circle key="seat-84" id="seat-84" r="15.7" cy="262.6" cx="351" />, | ||||
|     <circle key="seat-85" id="seat-85" r="15.5" cy="304.5" cx="382.7" />, | ||||
|     <circle key="seat-86" id="seat-86" r="15.7" cy="339.1" cx="425.3" />, | ||||
|     <circle key="seat-87" id="seat-87" r="15.7" cy="379" cx="461" />, | ||||
|     <circle key="seat-88" id="seat-88" r="15.7" cy="420.4" cx="495.9" />, | ||||
|     <circle key="seat-89" id="seat-89" r="15.7" cy="463.5" cx="528.1" />, | ||||
|     <circle key="seat-90" id="seat-90" r="15.5" cy="160.4" cx="315.7" />, | ||||
|     <circle key="seat-91" id="seat-91" r="15.5" cy="206.2" cx="342.9" />, | ||||
|     <circle key="seat-92" id="seat-92" r="15.7" cy="245.1" cx="379" />, | ||||
|     <circle key="seat-93" id="seat-93" r="15.5" cy="287.4" cx="410.5" />, | ||||
|     <circle key="seat-94" id="seat-94" r="15.7" cy="323.4" cx="455.9" />, | ||||
|     <circle key="seat-95" id="seat-95" transform="rotate(-80.8)" r="15.7" cy="555.93" cx="-274.27" />, | ||||
|     <circle key="seat-96" id="seat-96" r="15.7" cy="407.6" cx="527.7" />, | ||||
|     <circle key="seat-97" id="seat-97" r="15.5" cy="142.7" cx="345.9" />, | ||||
|     <circle key="seat-98" id="seat-98" r="15.5" cy="186.8" cx="375.8" />, | ||||
|     <circle key="seat-99" id="seat-99" r="15.5" cy="125.9" cx="377.8" />, | ||||
|     <circle key="seat-100" id="seat-100" r="15.5" cy="173.7" cx="405.1" />, | ||||
|     <circle key="seat-101" id="seat-101" r="15.7" cy="223" cx="422.9" />, | ||||
|     <circle key="seat-102" id="seat-102" r="15.5" cy="270.9" cx="444.3" />, | ||||
|     <circle key="seat-103" id="seat-103" r="15.5" cy="112" cx="409.4" />, | ||||
|     <circle key="seat-104" id="seat-104" r="15.5" cy="157.7" cx="438.1" />, | ||||
|     <circle key="seat-105" id="seat-105" r="15.7" cy="209" cx="453.9" />, | ||||
|     <circle key="seat-106" id="seat-106" r="15.5" cy="259.6" cx="474.2" />, | ||||
|     <circle key="seat-107" id="seat-107" r="15.7" cy="306.3" cx="499.3" />, | ||||
|     <circle key="seat-108" id="seat-108" r="15.5" cy="100.1" cx="443.4" />, | ||||
|     <circle key="seat-109" id="seat-109" r="15.5" cy="146.7" cx="472.7" />, | ||||
|     <circle key="seat-110" id="seat-110" r="15.7" cy="197.9" cx="497" />, | ||||
|     <circle key="seat-111" id="seat-111" r="15.5" cy="249" cx="508.8" />, | ||||
|     <circle key="seat-112" id="seat-112" r="15.7" cy="298.4" cx="532.7" />, | ||||
|     <circle key="seat-113" id="seat-113" r="15.7" cy="350.8" cx="538.1" />, | ||||
|     <circle key="seat-114" id="seat-114" r="15.5" cy="92.2" cx="477" />, | ||||
|     <circle key="seat-115" id="seat-115" r="15.5" cy="84.4" cx="510" />, | ||||
|     <circle key="seat-116" id="seat-116" transform="rotate(-80.8)" r="15.5" cy="523.04" cx="-55.62" />, | ||||
|     <circle key="seat-117" id="seat-117" r="15.7" cy="190.1" cx="531.6" />, | ||||
|     <circle key="seat-118" id="seat-118" r="15.5" cy="243.4" cx="542.3" />, | ||||
|     <circle key="seat-119" id="seat-119" r="15.5" cy="80.7" cx="544.3" />, | ||||
|     <circle key="seat-120" id="seat-120" r="15.5" cy="136.1" cx="541.9" />, | ||||
|     <circle key="seat-121" id="seat-121" r="15.5" cy="78.5" cx="579" />, | ||||
|     <circle key="seat-122" id="seat-122" r="15.5" cy="135" cx="578.2" />, | ||||
|     <circle key="seat-123" id="seat-123" r="15.7" cy="187.6" cx="577.9" />, | ||||
|     <circle key="seat-124" id="seat-124" r="15.5" cy="240" cx="579" />, | ||||
|     <circle key="seat-125" id="seat-125" r="15.7" cy="292.6" cx="578" />, | ||||
|     <circle key="seat-126" id="seat-126" r="15.7" cy="345.3" cx="578" />, | ||||
|     <circle key="seat-127" id="seat-127" r="15.7" cy="398" cx="577.8" />, | ||||
|     <circle key="seat-128" id="seat-128" r="15.7" cy="451.2" cx="572.2" />, | ||||
|     <circle key="seat-129" id="seat-129" r="15.5" cy="78.5" cx="613.5" />, | ||||
|     <circle key="seat-130" id="seat-130" r="15.5" cy="135" cx="612.3" />, | ||||
|     <circle key="seat-131" id="seat-131" r="15.7" cy="187.6" cx="612.6" />, | ||||
|     <circle key="seat-132" id="seat-132" r="15.5" cy="240" cx="611.5" />, | ||||
|     <circle key="seat-133" id="seat-133" r="15.7" cy="292.6" cx="612.5" />, | ||||
|     <circle key="seat-134" id="seat-134" r="15.7" cy="345.3" cx="612.5" />, | ||||
|     <circle key="seat-135" id="seat-135" r="15.7" cy="398" cx="612.7" />, | ||||
|     <circle key="seat-136" id="seat-136" r="15.7" cy="451.2" cx="618.3" />, | ||||
|     <circle key="seat-137" id="seat-137" r="15.5" cy="82.6" cx="646.3" />, | ||||
|     <circle key="seat-138" id="seat-138" r="15.5" cy="86.4" cx="680.5" />, | ||||
|     <circle key="seat-139" id="seat-139" r="15.5" cy="138.4" cx="650.6" />, | ||||
|     <circle key="seat-140" id="seat-140" r="15.5" cy="94.2" cx="715.6" />, | ||||
|     <circle key="seat-141" id="seat-141" r="15.5" cy="142.6" cx="685.4" />, | ||||
|     <circle key="seat-142" id="seat-142" r="15.7" cy="190.1" cx="657" />, | ||||
|     <circle key="seat-143" id="seat-143" r="15.5" cy="243.4" cx="648.3" />, | ||||
|     <circle key="seat-144" id="seat-144" r="15.5" cy="104.1" cx="747.1" />, | ||||
|     <circle key="seat-145" id="seat-145" r="15.5" cy="150.7" cx="719.9" />, | ||||
|     <circle key="seat-146" id="seat-146" r="15.7" cy="197.9" cx="691.5" />, | ||||
|     <circle key="seat-147" id="seat-147" r="15.5" cy="248.5" cx="679.8" />, | ||||
|     <circle key="seat-148" id="seat-148" r="15.7" cy="298.4" cx="657.8" />, | ||||
|     <circle key="seat-149" id="seat-149" r="15.7" cy="350.8" cx="652.4" />, | ||||
|     <circle key="seat-150" id="seat-150" r="15.5" cy="116" cx="783.1" />, | ||||
|     <circle key="seat-151" id="seat-151" r="15.5" cy="159.7" cx="750.4" />, | ||||
|     <circle key="seat-152" id="seat-152" r="15.7" cy="211" cx="736.6" />, | ||||
|     <circle key="seat-153" id="seat-153" r="15.5" cy="259.6" cx="716.4" />, | ||||
|     <circle key="seat-154" id="seat-154" r="15.7" cy="306.3" cx="691.2" />, | ||||
|     <circle key="seat-155" id="seat-155" r="15.5" cy="127.9" cx="812.8" />, | ||||
|     <circle key="seat-156" id="seat-156" r="15.5" cy="173.7" cx="785.5" />, | ||||
|     <circle key="seat-157" id="seat-157" r="15.7" cy="223" cx="767.7" />, | ||||
|     <circle key="seat-158" id="seat-158" r="15.5" cy="270.9" cx="746.3" />, | ||||
|     <circle key="seat-159" id="seat-159" r="15.5" cy="144.7" cx="846.6" />, | ||||
|     <circle key="seat-160" id="seat-160" r="15.5" cy="186.8" cx="814.8" />, | ||||
|     <circle key="seat-161" id="seat-161" r="15.5" cy="160.4" cx="874.8" />, | ||||
|     <circle key="seat-162" id="seat-162" r="15.5" cy="206.2" cx="847.6" />, | ||||
|     <circle key="seat-163" id="seat-163" r="15.7" cy="245.1" cx="811.5" />, | ||||
|     <circle key="seat-164" id="seat-164" r="15.5" cy="287.4" cx="780.1" />, | ||||
|     <circle key="seat-165" id="seat-165" r="15.7" cy="323.4" cx="734.6" />, | ||||
|     <circle key="seat-166" id="seat-166" r="15.7" cy="357.8" cx="687.4" />, | ||||
|     <circle key="seat-167" id="seat-167" r="15.7" cy="407.6" cx="662.8" />, | ||||
|     <circle key="seat-168" id="seat-168" r="15.5" cy="181.8" cx="907.5" />, | ||||
|     <circle key="seat-169" id="seat-169" r="15.5" cy="223.6" cx="875.2" />, | ||||
|     <circle key="seat-170" id="seat-170" r="15.7" cy="262.6" cx="839.5" />, | ||||
|     <circle key="seat-171" id="seat-171" r="15.5" cy="304.3" cx="807.8" />, | ||||
|     <circle key="seat-172" id="seat-172" r="15.7" cy="339.1" cx="765.3" />, | ||||
|     <circle key="seat-173" id="seat-173" r="15.7" cy="379" cx="729.6" />, | ||||
|     <circle key="seat-174" id="seat-174" r="15.7" cy="420.4" cx="694.6" />, | ||||
|     <circle key="seat-175" id="seat-175" r="15.7" cy="463.5" cx="662.5" />, | ||||
|     <circle key="seat-176" id="seat-176" r="15.7" cy="485.4" cx="699.5" />, | ||||
|     <circle key="seat-177" id="seat-177" r="15.7" cy="213.6" cx="947.4" />, | ||||
|     <circle key="seat-178" id="seat-178" r="15.7" cy="254.9" cx="915.2" />, | ||||
|     <circle key="seat-179" id="seat-179" r="15.7" cy="296.4" cx="882.7" />, | ||||
|     <circle key="seat-180" id="seat-180" r="15.7" cy="337.6" cx="850.7" />, | ||||
|     <circle key="seat-181" id="seat-181" r="15.7" cy="379" cx="818.1" />, | ||||
|     <circle key="seat-182" id="seat-182" r="15.7" cy="420.8" cx="785.4" />, | ||||
|     <circle key="seat-183" id="seat-183" r="15.7" cy="462.7" cx="753.4" />, | ||||
|     <circle key="seat-184" id="seat-184" r="15.7" cy="515.4" cx="730.1" />, | ||||
|     <circle key="seat-185" id="seat-185" r="15.7" cy="236.2" cx="972.4" />, | ||||
|     <circle key="seat-186" id="seat-186" r="15.7" cy="278.6" cx="940.5" />, | ||||
|     <circle key="seat-187" id="seat-187" r="15.7" cy="320.2" cx="907.5" />, | ||||
|     <circle key="seat-188" id="seat-188" r="15.7" cy="362" cx="875.1" />, | ||||
|     <circle key="seat-189" id="seat-189" r="15.7" cy="403.8" cx="841.8" />, | ||||
|     <circle key="seat-190" id="seat-190" r="15.7" cy="445.9" cx="808.9" />, | ||||
|     <circle key="seat-191" id="seat-191" r="15.7" cy="489" cx="775.5" />, | ||||
|     <circle key="seat-192" id="seat-192" r="15.7" cy="265.1" cx="999.7" />, | ||||
|     <circle key="seat-193" id="seat-193" r="15.7" cy="307.2" cx="966.6" />, | ||||
|     <circle key="seat-194" id="seat-194" r="15.7" cy="346.9" cx="931.2" />, | ||||
|     <circle key="seat-195" id="seat-195" r="15.7" cy="393" cx="901" />, | ||||
|     <circle key="seat-196" id="seat-196" r="15.7" cy="435.9" cx="866.9" />, | ||||
|     <circle key="seat-197" id="seat-197" r="15.7" cy="480.8" cx="833.2" />, | ||||
|     <circle key="seat-198" id="seat-198" transform="rotate(-80.8)" r="15.7" cy="1055.16" cx="-124.85" />, | ||||
|     <circle key="seat-199" id="seat-199" r="15.7" cy="334.6" cx="988.2" />, | ||||
|     <circle key="seat-200" id="seat-200" r="15.7" cy="376.9" cx="953.8" />, | ||||
|     <circle key="seat-201" id="seat-201" r="15.7" cy="425.6" cx="922.8" />, | ||||
|     <circle key="seat-202" id="seat-202" r="15.7" cy="464.9" cx="884" />, | ||||
|     <circle key="seat-203" id="seat-203" r="15.7" cy="325.1" cx="1045.7" />, | ||||
|     <circle key="seat-204" id="seat-204" r="15.7" cy="365.7" cx="1009.2" />, | ||||
|     <circle key="seat-205" id="seat-205" r="15.7" cy="405.1" cx="971.7" />, | ||||
|     <circle key="seat-206" id="seat-206" r="15.7" cy="354.1" cx="1063.2" />, | ||||
|     <circle key="seat-207" id="seat-207" transform="rotate(-80.8)" r="15.7" cy="1075.78" cx="-226.25" />, | ||||
|     <circle key="seat-208" id="seat-208" r="15.7" cy="387.4" cx="1081.8" />, | ||||
|     <circle key="seat-209" id="seat-209" r="15.7" cy="421.3" cx="1095.7" />, | ||||
|     <circle key="seat-210" id="seat-210" r="15.7" cy="429.8" cx="1042.5" />, | ||||
|     <circle key="seat-211" id="seat-211" r="15.7" cy="437.3" cx="988.2" />, | ||||
|     <circle key="seat-212" id="seat-212" r="15.7" cy="455.4" cx="938.5" />, | ||||
|     <circle key="seat-213" id="seat-213" r="15.7" cy="455.8" cx="1108.8" />, | ||||
|     <circle key="seat-214" id="seat-214" r="15.7" cy="462" cx="1054.9" />, | ||||
|     <circle key="seat-215" id="seat-215" r="15.7" cy="469.3" cx="1001.6" />, | ||||
|     <circle key="seat-216" id="seat-216" r="15.7" cy="492.6" cx="954.1" />, | ||||
|     <circle key="seat-217" id="seat-217" r="15.7" cy="500.6" cx="900.8" />, | ||||
|     <circle key="seat-218" id="seat-218" r="15.7" cy="511.6" cx="849" />, | ||||
|     <circle key="seat-219" id="seat-219" r="15.7" cy="535" cx="801.6" />, | ||||
|     <circle key="seat-220" id="seat-220" r="15.7" cy="554.8" cx="753.3" />, | ||||
|     <circle key="seat-221" id="seat-221" r="15.7" cy="490.9" cx="1118" />, | ||||
|     <circle key="seat-222" id="seat-222" r="15.7" cy="497.2" cx="1065.3" />, | ||||
|     <circle key="seat-223" id="seat-223" r="15.7" cy="502.8" cx="1012.3" />, | ||||
|     <circle key="seat-224" id="seat-224" r="15.7" cy="525.1" cx="964.2" />, | ||||
|     <circle key="seat-225" id="seat-225" r="15.7" cy="533.1" cx="912.2" />, | ||||
|     <circle key="seat-226" id="seat-226" r="15.7" cy="554.6" cx="863.4" />, | ||||
|     <circle key="seat-227" id="seat-227" r="15.7" cy="567.9" cx="812.7" />, | ||||
|     <circle key="seat-228" id="seat-228" r="15.7" cy="596.7" cx="764.8" />, | ||||
|     <circle key="seat-229" id="seat-229" r="15.7" cy="528.9" cx="1126.1" />, | ||||
|     <circle key="seat-230" id="seat-230" r="15.7" cy="530.2" cx="1072.7" />, | ||||
|     <circle key="seat-231" id="seat-231" transform="rotate(-80.8)" r="15.7" cy="1092.81" cx="-365.69" />, | ||||
|     <circle key="seat-232" id="seat-232" r="15.7" cy="562.9" cx="1130.6" />, | ||||
|     <circle key="seat-233" id="seat-233" r="15.7" cy="566.7" cx="1078.3" />, | ||||
|     <circle key="seat-234" id="seat-234" transform="rotate(-80.8)" r="15.7" cy="1103.39" cx="-398.54" />, | ||||
|     <circle key="seat-235" id="seat-235" r="15.7" cy="564.5" cx="972.4" />, | ||||
|     <circle key="seat-236" id="seat-236" r="15.7" cy="568.6" cx="919.7" />, | ||||
|     <circle key="seat-237" id="seat-237" r="15.7" cy="588" cx="869.4" />, | ||||
|     <circle key="seat-238" id="seat-238" r="15.7" cy="602.5" cx="1133.5" />, | ||||
|     <circle key="seat-239" id="seat-239" r="15.7" cy="600.9" cx="1081" />, | ||||
|     <circle key="seat-240" id="seat-240" transform="rotate(-80.8)" r="15.7" cy="1111.41" cx="-431.3" />, | ||||
|     <circle key="seat-241" id="seat-241" r="15.7" cy="598.6" cx="975.6" />, | ||||
|     <circle key="seat-242" id="seat-242" r="15.7" cy="602.6" cx="923.4" />, | ||||
|     <circle key="seat-243" id="seat-243" r="15.7" cy="636.4" cx="1133.9" />, | ||||
|     <circle key="seat-244" id="seat-244" r="15.7" cy="639" cx="1081.3" />, | ||||
|     <circle key="seat-245" id="seat-245" transform="rotate(-80.8)" r="15.7" cy="1117.48" cx="-466.13" />, | ||||
|     <circle key="seat-246" id="seat-246" r="15.7" cy="639.2" cx="976.3" />, | ||||
|     <circle key="seat-247" id="seat-247" r="15.7" cy="638.8" cx="923.9" />, | ||||
|     <circle key="seat-248" id="seat-248" r="15.7" cy="635.1" cx="871.2" />, | ||||
|     <circle key="seat-249" id="seat-249" r="15.7" cy="625.7" cx="818.8" />, | ||||
|     <circle key="seat-250" id="seat-250" r="15.7" cy="639" cx="766.3" />, | ||||
|     <circle key="seat-251" id="seat-251" r="15.7" cy="673.1" cx="1081.3" />, | ||||
|     <circle key="seat-252" id="seat-252" transform="rotate(-80.8)" r="15.7" cy="1122.99" cx="-499.74" />, | ||||
|     <circle key="seat-253" id="seat-253" r="15.7" cy="673.5" cx="976.3" />, | ||||
|     <circle key="seat-254" id="seat-254" r="15.7" cy="673.2" cx="924" />, | ||||
|     <circle key="seat-255" id="seat-255" r="15.7" cy="669.5" cx="871.2" />, | ||||
|     <circle key="seat-256" id="seat-256" r="15.7" cy="660" cx="819.7" />, | ||||
|   ]; | ||||
|  | ||||
|   let seatIndex = 1; // Empezamos a contar desde 1, ya que el 0 es presidencial | ||||
|  | ||||
|   return ( | ||||
|     <svg viewBox="0 0 1190.6 772.2" width={size} height={size * (772.2 / 1190.6)} style={{ display: 'block', margin: 'auto' }}> | ||||
|       <g> | ||||
|         {/* Renderizamos el escaño presidencial primero y por separado */} | ||||
|         {presidenteBancada && React.cloneElement(seatElements[PRESIDENTE_SEAT_INDEX], { | ||||
|           fill: presidenteBancada.color || '#A9A9A9', | ||||
|           strokeWidth: 0.5, | ||||
|         })} | ||||
|         {partyData.map(partido => { | ||||
|           // Por cada partido, creamos un array combinado de sus escaños | ||||
|           const partySeats = [ | ||||
|             ...Array(partido.bancasFijos).fill({ isNew: false }), | ||||
|             ...Array(partido.bancasGanadas).fill({ isNew: true }) | ||||
|           ]; | ||||
|  | ||||
|           return ( | ||||
|             // Envolvemos todos los escaños de un partido en un <g> | ||||
|             <g | ||||
|               key={partido.id} | ||||
|               className="party-block" | ||||
|               data-tooltip-id="party-tooltip" | ||||
|               data-tooltip-content={`${partido.nombreCorto || partido.nombre}: ${partido.bancasTotales} bancas`} | ||||
|             > | ||||
|               {partySeats.map((seatInfo, i) => { | ||||
|                 // Si ya no hay más plantillas de escaños, no renderizamos nada | ||||
|                 if (seatIndex >= seatElements.length) return null; | ||||
|  | ||||
|                 const template = seatElements[seatIndex]; | ||||
|                 seatIndex++; // Incrementamos el contador para el siguiente escaño | ||||
|  | ||||
|                 // Clonamos la plantilla con el estilo apropiado | ||||
|                 return React.cloneElement(template, { | ||||
|                   key: `${partido.id}-${i}`, | ||||
|                   className: 'seat-circle', | ||||
|                   fill: partido.color || '#808080', | ||||
|                   fillOpacity: seatInfo.isNew ? 1 : 0.3, // Opacidad para bancas previas | ||||
|                   stroke: partido.color || '#808080', | ||||
|                   strokeWidth: 0.5, | ||||
|                 }); | ||||
|               })} | ||||
|             </g> | ||||
|           ); | ||||
|         })} | ||||
|         {/* Renderizamos los escaños vacíos sobrantes */} | ||||
|         {seatIndex < seatElements.length && | ||||
|           seatElements.slice(seatIndex).map((template, i) => | ||||
|             React.cloneElement(template, { | ||||
|               key: `empty-${i}`, | ||||
|               fill: '#E0E0E0', | ||||
|               stroke: '#ffffff', | ||||
|               strokeWidth: 0.5 | ||||
|             }) | ||||
|           ) | ||||
|         } | ||||
|       </g> | ||||
|     </svg> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,154 @@ | ||||
| // src/components/common/SenadoresNacionalesLayout.tsx | ||||
| import React from 'react'; | ||||
| import type { PartidoComposicionNacional } from '../../apiService'; | ||||
|  | ||||
| // Interfaces | ||||
| interface SenadoresNacionalesLayoutProps { | ||||
|   partyData: PartidoComposicionNacional[]; | ||||
|   size?: number; | ||||
|   presidenteBancada?: { color: string | null } | null; | ||||
| } | ||||
|  | ||||
| const PRESIDENTE_SEAT_INDEX = 0; | ||||
|  | ||||
| export const SenadoresNacionalesLayout: React.FC<SenadoresNacionalesLayoutProps> = ({ | ||||
|   partyData, | ||||
|   size = 800, | ||||
|   presidenteBancada, | ||||
| }) => { | ||||
|   // --- ARRAY DE 73 ELEMENTOS <circle> ORDENADOS POR ID DE "seat-X" --- | ||||
|   // El asiento 0 es el presidencial, los 72 restantes son los senadores. | ||||
|   const seatElements = [ | ||||
|     <circle key="seat-0" id="seat-0" r="7.1" cy="187" cx="168.6" />, | ||||
|     <circle key="seat-1" id="seat-1" r="7.1" cy="166" cx="21.8" />, | ||||
|     <circle key="seat-2" id="seat-2" r="7.1" cy="172" cx="51.5" />, | ||||
|     <circle key="seat-3" id="seat-3" r="7.1" cy="174.5" cx="82.7" />, | ||||
|     <circle key="seat-4" id="seat-4" r="7.1" cy="147.4" cx="21.5" />, | ||||
|     <circle key="seat-5" id="seat-5" r="7.1" cy="155.2" cx="51.8" />, | ||||
|     <circle key="seat-6" id="seat-6" r="7.1" cy="156.3" cx="83.4" />, | ||||
|     <circle key="seat-7" id="seat-7" r="7.1" cy="169.9" cx="120.9" />, | ||||
|     <circle key="seat-8" id="seat-8" r="7.1" cy="128.4" cx="22.8" />, | ||||
|     <circle key="seat-9" id="seat-9" r="7.1" cy="137.9" cx="53.2" />, | ||||
|     <circle key="seat-10" id="seat-10" r="7.1" cy="138.8" cx="85.5" />, | ||||
|     <circle key="seat-11" id="seat-11" r="7.1" cy="151.9" cx="120.9" />, | ||||
|     <circle key="seat-12" id="seat-12" r="7.1" cy="109" cx="25.6" />, | ||||
|     <circle key="seat-13" id="seat-13" r="7.1" cy="121.3" cx="57.2" />, | ||||
|     <circle key="seat-14" id="seat-14" r="7.1" cy="91.5" cx="34.2" />, | ||||
|     <circle key="seat-15" id="seat-15" r="7.1" cy="105.7" cx="64.8" />, | ||||
|     <circle key="seat-16" id="seat-16" r="7.1" cy="122.5" cx="92.9" />, | ||||
|     <circle key="seat-17" id="seat-17" r="7.1" cy="136.2" cx="128.2" />, | ||||
|     <circle key="seat-18" id="seat-18" r="7.1" cy="75.5" cx="45.3" />, | ||||
|     <circle key="seat-19" id="seat-19" r="7.1" cy="91.3" cx="75.7" />, | ||||
|     <circle key="seat-20" id="seat-20" r="7.1" cy="106.5" cx="106.3" />, | ||||
|     <circle key="seat-21" id="seat-21" r="7.1" cy="59.8" cx="57.9" />, | ||||
|     <circle key="seat-22" id="seat-22" r="7.1" cy="78.6" cx="89.5" />, | ||||
|     <circle key="seat-23" id="seat-23" r="7.1" cy="45.3" cx="73.2" />, | ||||
|     <circle key="seat-24" id="seat-24" r="7.1" cy="67.2" cx="104.6" />, | ||||
|     <circle key="seat-25" id="seat-25" r="7.1" cy="94.3" cx="121.6" />, | ||||
|     <circle key="seat-26" id="seat-26" r="7.1" cy="124.3" cx="141.1" />, | ||||
|     <circle key="seat-27" id="seat-27" r="7.1" cy="32.7" cx="90.8" />, | ||||
|     <circle key="seat-28" id="seat-28" r="7.1" cy="58.3" cx="120.9" />, | ||||
|     <circle key="seat-29" id="seat-29" r="7.1" cy="84.9" cx="139.1" />, | ||||
|     <circle key="seat-30" id="seat-30" r="7.1" cy="116.4" cx="157.2" />, | ||||
|     <circle key="seat-31" id="seat-31" r="7.1" cy="24.6" cx="109.5" />, | ||||
|     <circle key="seat-32" id="seat-32" r="7.1" cy="52.2" cx="138.6" />, | ||||
|     <circle key="seat-33" id="seat-33" r="7.1" cy="79.5" cx="157.8" />, | ||||
|     <circle key="seat-34" id="seat-34" r="7.1" cy="17.9" cx="128.8" />, | ||||
|     <circle key="seat-35" id="seat-35" r="7.1" cy="15.2" cx="147.7" />, | ||||
|     <circle key="seat-36" id="seat-36" r="7.1" cy="48.3" cx="156.9" />, | ||||
|     <circle key="seat-37" id="seat-37" r="7.1" cy="15.2" cx="192.5" />, | ||||
|     <circle key="seat-38" id="seat-38" r="7.1" cy="48.3" cx="183.3" />, | ||||
|     <circle key="seat-39" id="seat-39" r="7.1" cy="79.5" cx="182.4" />, | ||||
|     <circle key="seat-40" id="seat-40" r="7.1" cy="115.8" cx="182.2" />, | ||||
|     <circle key="seat-41" id="seat-41" r="7.1" cy="17.9" cx="211.4" />, | ||||
|     <circle key="seat-42" id="seat-42" r="7.1" cy="52.2" cx="201.6" />, | ||||
|     <circle key="seat-43" id="seat-43" r="7.1" cy="24.6" cx="230.7" />, | ||||
|     <circle key="seat-44" id="seat-44" r="7.1" cy="58.3" cx="219.3" />, | ||||
|     <circle key="seat-45" id="seat-45" r="7.1" cy="84.9" cx="201.1" />, | ||||
|     <circle key="seat-46" id="seat-46" r="7.1" cy="32.7" cx="249.4" />, | ||||
|     <circle key="seat-47" id="seat-47" r="7.1" cy="67.2" cx="235.6" />, | ||||
|     <circle key="seat-48" id="seat-48" r="7.1" cy="94.3" cx="218.6" />, | ||||
|     <circle key="seat-49" id="seat-49" r="7.1" cy="124.3" cx="199.1" />, | ||||
|     <circle key="seat-50" id="seat-50" r="7.1" cy="45.3" cx="267" />, | ||||
|     <circle key="seat-51" id="seat-51" r="7.1" cy="59.8" cx="282.3" />, | ||||
|     <circle key="seat-52" id="seat-52" r="7.1" cy="78.6" cx="250.7" />, | ||||
|     <circle key="seat-53" id="seat-53" r="7.1" cy="106.5" cx="234" />, | ||||
|     <circle key="seat-54" id="seat-54" r="7.1" cy="136.2" cx="212" />, | ||||
|     <circle key="seat-55" id="seat-55" r="7.1" cy="75.5" cx="294.9" />, | ||||
|     <circle key="seat-56" id="seat-56" r="7.1" cy="91.3" cx="264.5" />, | ||||
|     <circle key="seat-57" id="seat-57" r="7.1" cy="91.5" cx="306" />, | ||||
|     <circle key="seat-58" id="seat-58" r="7.1" cy="105.7" cx="275.4" />, | ||||
|     <circle key="seat-59" id="seat-59" r="7.1" cy="122.5" cx="247.3" />, | ||||
|     <circle key="seat-60" id="seat-60" r="7.1" cy="109" cx="313.5" />, | ||||
|     <circle key="seat-61" id="seat-61" r="7.1" cy="121.3" cx="283" />, | ||||
|     <circle key="seat-62" id="seat-62" r="7.1" cy="138.8" cx="254.7" />, | ||||
|     <circle key="seat-63" id="seat-63" r="7.1" cy="151.9" cx="219.3" />, | ||||
|     <circle key="seat-64" id="seat-64" r="7.1" cy="128.4" cx="317.4" />, | ||||
|     <circle key="seat-65" id="seat-65" r="7.1" cy="137.9" cx="287" />, | ||||
|     <circle key="seat-66" id="seat-66" r="7.1" cy="156.3" cx="256.8" />, | ||||
|     <circle key="seat-67" id="seat-67" r="7.1" cy="169.9" cx="219.3" />, | ||||
|     <circle key="seat-68" id="seat-68" r="7.1" cy="147.4" cx="318.7" />, | ||||
|     <circle key="seat-69" id="seat-69" r="7.1" cy="155.2" cx="288.4" />, | ||||
|     <circle key="seat-70" id="seat-70" r="7.1" cy="166" cx="318.4" />, | ||||
|     <circle key="seat-71" id="seat-71" r="7.1" cy="172" cx="288.7" />, | ||||
|     <circle key="seat-72" id="seat-72" r="7.1" cy="174.5" cx="257.5" />, | ||||
|   ]; | ||||
|  | ||||
|   let seatIndex = 1; // Empezamos desde 1 porque el 0 es para el presidente | ||||
|  | ||||
|   return ( | ||||
|     <svg viewBox="0 0 340.2 220.5" width={size} height={size * (220.5 / 340.2)} style={{ display: 'block', margin: 'auto' }}> | ||||
|       <g> | ||||
|         {/* Renderizamos primero el escaño del presidente por separado */} | ||||
|         {presidenteBancada && React.cloneElement(seatElements[PRESIDENTE_SEAT_INDEX], { | ||||
|           fill: presidenteBancada.color || '#A9A9A9', | ||||
|           strokeWidth: 0.5, | ||||
|         })} | ||||
|  | ||||
|         {/* Mapeamos los partidos para crear los bloques */} | ||||
|         {partyData.map(partido => { | ||||
|           const partySeats = [ | ||||
|             ...Array(partido.bancasFijos).fill({ isNew: false }), | ||||
|             ...Array(partido.bancasGanadas).fill({ isNew: true }) | ||||
|           ]; | ||||
|            | ||||
|           return ( | ||||
|             <g | ||||
|               key={partido.id} | ||||
|               className="party-block" | ||||
|               data-tooltip-id="party-tooltip" | ||||
|               data-tooltip-content={`${partido.nombreCorto || partido.nombre}: ${partido.bancasTotales} bancas`} | ||||
|             > | ||||
|               {partySeats.map((seatInfo, i) => { | ||||
|                 if (seatIndex >= seatElements.length) return null; | ||||
|  | ||||
|                 const template = seatElements[seatIndex]; | ||||
|                 seatIndex++; | ||||
|  | ||||
|                 return React.cloneElement(template, { | ||||
|                   key: `${partido.id}-${i}`, | ||||
|                   className: 'seat-circle', | ||||
|                   fill: partido.color || '#808080', | ||||
|                   fillOpacity: seatInfo.isNew ? 1 : 0.3, | ||||
|                   stroke: partido.color || '#808080', | ||||
|                   strokeWidth: 0.5, | ||||
|                 }); | ||||
|               })} | ||||
|             </g> | ||||
|           ); | ||||
|         })} | ||||
|         {/* Renderizamos escaños vacíos si sobran */} | ||||
|         {seatIndex < seatElements.length && | ||||
|           seatElements.slice(seatIndex).map((template, i) =>  | ||||
|             React.cloneElement(template, { | ||||
|               key: `empty-${i}`, | ||||
|               fill: '#E0E0E0', | ||||
|               stroke: '#ffffff', | ||||
|               strokeWidth: 0.5 | ||||
|             }) | ||||
|           ) | ||||
|         } | ||||
|       </g> | ||||
|     </svg> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,4 +1,6 @@ | ||||
| // src/features/legislativas/rovinciales/DevAppLegislativas.tsx | ||||
| import { ResultadosNacionalesCardsWidget } from './nacionales/ResultadosNacionalesCardsWidget'; | ||||
| import { CongresoNacionalWidget } from './nacionales/CongresoNacionalWidget'; | ||||
| import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget'; | ||||
| import './DevAppStyle.css' | ||||
|  | ||||
| @@ -6,9 +8,8 @@ export const DevAppLegislativas = () => { | ||||
|     return ( | ||||
|         <div className="container"> | ||||
|             <h1>Visor de Widgets</h1> | ||||
|              | ||||
|             {/* Le pasamos el ID de la elección que queremos visualizar. | ||||
|                 Para tus datos de prueba provinciales, este ID es 1. */} | ||||
|             <ResultadosNacionalesCardsWidget eleccionId={2} /> | ||||
|             <CongresoNacionalWidget eleccionId={2} /> | ||||
|             <PanelNacionalWidget eleccionId={2} /> | ||||
|         </div> | ||||
|     ); | ||||
|   | ||||
| @@ -0,0 +1,162 @@ | ||||
| // src/features/legislativas/nacionales/CongresoNacionalWidget.tsx | ||||
| import { useState, Suspense, useMemo } from 'react'; | ||||
| import { useSuspenseQuery } from '@tanstack/react-query'; | ||||
| import { Tooltip } from 'react-tooltip'; | ||||
| import { DiputadosNacionalesLayout } from '../../../components/common/DiputadosNacionalesLayout'; | ||||
| import { SenadoresNacionalesLayout } from '../../../components/common/SenadoresNacionalesLayout'; | ||||
| import { getComposicionNacional, type ComposicionNacionalData, type PartidoComposicionNacional } from '../../../apiService'; | ||||
| import '../provinciales/CongresoWidget.css'; | ||||
|  | ||||
| interface CongresoNacionalWidgetProps { | ||||
|   eleccionId: number; | ||||
| } | ||||
|  | ||||
| const formatTimestamp = (dateString: string) => { | ||||
|   if (!dateString) return '...'; | ||||
|   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}`; | ||||
| }; | ||||
|  | ||||
| const WidgetContent = ({ eleccionId }: CongresoNacionalWidgetProps) => { | ||||
|   const [camaraActiva, setCamaraActiva] = useState<'diputados' | 'senadores'>('diputados'); | ||||
|   const [isHovering, setIsHovering] = useState(false); | ||||
|  | ||||
|   const { data } = useSuspenseQuery<ComposicionNacionalData>({ | ||||
|     queryKey: ['composicionNacional', eleccionId], | ||||
|     queryFn: () => getComposicionNacional(eleccionId), | ||||
|     refetchInterval: 30000, | ||||
|   }); | ||||
|  | ||||
|   const datosCamaraActual = data[camaraActiva]; | ||||
|  | ||||
|   const partidosOrdenados = useMemo(() => { | ||||
|     if (!datosCamaraActual?.partidos) return []; | ||||
|     const partidosACopiar = [...datosCamaraActual.partidos]; | ||||
|     partidosACopiar.sort((a, b) => { | ||||
|       const ordenA = camaraActiva === 'diputados' ? a.ordenDiputadosNacionales : a.ordenSenadoresNacionales; | ||||
|       const ordenB = camaraActiva === 'diputados' ? b.ordenDiputadosNacionales : b.ordenSenadoresNacionales; | ||||
|       return (ordenA ?? 999) - (ordenB ?? 999); | ||||
|     }); | ||||
|     return partidosACopiar; | ||||
|   }, [datosCamaraActual, camaraActiva]); | ||||
|  | ||||
|   const partyDataParaLayout = useMemo(() => { | ||||
|     if (camaraActiva === 'senadores') return partidosOrdenados; | ||||
|     if (!partidosOrdenados || !datosCamaraActual.presidenteBancada?.color) return partidosOrdenados; | ||||
|     const partidoPresidente = partidosOrdenados.find(p => p.color === datosCamaraActual.presidenteBancada!.color); | ||||
|     if (!partidoPresidente) return partidosOrdenados; | ||||
|  | ||||
|     const adjustedPartyData = JSON.parse(JSON.stringify(partidosOrdenados)); | ||||
|     const partidoAjustar = adjustedPartyData.find((p: PartidoComposicionNacional) => p.id === partidoPresidente.id); | ||||
|  | ||||
|     if (partidoAjustar) { | ||||
|       const tipoBanca = datosCamaraActual.presidenteBancada.tipoBanca; | ||||
|       if (tipoBanca === 'ganada' && partidoAjustar.bancasGanadas > 0) { | ||||
|         partidoAjustar.bancasGanadas -= 1; | ||||
|       } else if (tipoBanca === 'previa' && partidoAjustar.bancasFijos > 0) { | ||||
|         partidoAjustar.bancasFijos -= 1; | ||||
|       } else { | ||||
|         if (partidoAjustar.bancasGanadas > 0) { | ||||
|           partidoAjustar.bancasGanadas -= 1; | ||||
|         } else if (partidoAjustar.bancasFijos > 0) { | ||||
|           partidoAjustar.bancasFijos -= 1; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return adjustedPartyData; | ||||
|   }, [partidosOrdenados, datosCamaraActual.presidenteBancada, camaraActiva]); | ||||
|  | ||||
|   return ( | ||||
|     <div className="congreso-container"> | ||||
|       <div className="congreso-grafico"> | ||||
|         <div | ||||
|           className={`congreso-hemiciclo-wrapper ${isHovering ? 'is-hovering' : ''}`} | ||||
|           onMouseEnter={() => setIsHovering(true)} | ||||
|           onMouseLeave={() => setIsHovering(false)} | ||||
|         > | ||||
|           {camaraActiva === 'diputados' ? | ||||
|             <DiputadosNacionalesLayout | ||||
|               partyData={partyDataParaLayout} | ||||
|               presidenteBancada={datosCamaraActual.presidenteBancada || null} | ||||
|               size={700} | ||||
|             /> : | ||||
|             <SenadoresNacionalesLayout | ||||
|               partyData={partyDataParaLayout} | ||||
|               presidenteBancada={datosCamaraActual.presidenteBancada || null} | ||||
|               size={700} | ||||
|             /> | ||||
|           } | ||||
|         </div> | ||||
|         <div className="congreso-footer"> | ||||
|           <div className="footer-legend"> | ||||
|             <div className="footer-legend-item"> | ||||
|               {/* Usamos la nueva clase CSS para el círculo sólido */} | ||||
|               <span className="legend-icon legend-icon--solid"></span> | ||||
|               <span>Bancas en juego</span> | ||||
|             </div> | ||||
|             <div className="footer-legend-item"> | ||||
|               {/* Reemplazamos el SVG por un span con la nueva clase para el anillo */} | ||||
|               <span className="legend-icon legend-icon--ring"></span> | ||||
|               <span>Bancas previas</span> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="footer-timestamp"> | ||||
|             Última Actualización: {formatTimestamp(datosCamaraActual.ultimaActualizacion)} | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="congreso-summary"> | ||||
|         <div className="chamber-tabs"> | ||||
|           <button className={camaraActiva === 'diputados' ? 'active' : ''} onClick={() => setCamaraActiva('diputados')}> | ||||
|             Diputados | ||||
|           </button> | ||||
|           <button className={camaraActiva === 'senadores' ? 'active' : ''} onClick={() => setCamaraActiva('senadores')}> | ||||
|             Senadores | ||||
|           </button> | ||||
|         </div> | ||||
|         <h3>{datosCamaraActual.camaraNombre}</h3> | ||||
|         <div className="summary-metric"> | ||||
|           <span>Total de Bancas</span> | ||||
|           <strong>{datosCamaraActual.totalBancas}</strong> | ||||
|         </div> | ||||
|         <div className="summary-metric"> | ||||
|           <span>Bancas en Juego</span> | ||||
|           <strong>{datosCamaraActual.bancasEnJuego}</strong> | ||||
|         </div> | ||||
|         <hr /> | ||||
|         <div className="partido-lista-container"> | ||||
|           <ul className="partido-lista"> | ||||
|             {partidosOrdenados | ||||
|               .filter(p => p.bancasTotales > 0) | ||||
|               .map((partido: PartidoComposicionNacional) => ( | ||||
|                 <li key={partido.id}> | ||||
|                   <span className="partido-color-box" style={{ backgroundColor: partido.color || '#808080' }}></span> | ||||
|                   <span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span> | ||||
|                   <strong | ||||
|                     className="partido-bancas" | ||||
|                     title={`${partido.bancasFijos} bancas previas + ${partido.bancasGanadas} ganadas`} | ||||
|                   > | ||||
|                     {partido.bancasTotales} | ||||
|                   </strong> | ||||
|                 </li> | ||||
|               ))} | ||||
|           </ul> | ||||
|         </div> | ||||
|       </div> | ||||
|       <Tooltip id="party-tooltip" /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const CongresoNacionalWidget = ({ eleccionId }: CongresoNacionalWidgetProps) => { | ||||
|   return ( | ||||
|     <Suspense fallback={<div className="congreso-container loading" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '400px' }}>Cargando composición del congreso...</div>}> | ||||
|       <WidgetContent eleccionId={eleccionId} /> | ||||
|     </Suspense> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,10 +1,11 @@ | ||||
| /* src/features/legislativas/nacionales/PanelNaciona.css */ | ||||
| /* src/features/legislativas/nacionales/PanelNacional.css */ | ||||
| .panel-nacional-container { | ||||
|   font-family: 'Roboto', sans-serif; | ||||
|   max-width: 1200px; | ||||
|   margin: auto; | ||||
|   border: 1px solid #e0e0e0; | ||||
|   border-radius: 8px; | ||||
|   position: relative; | ||||
| } | ||||
|  | ||||
| .panel-header { | ||||
| @@ -491,13 +492,11 @@ | ||||
| /* --- NUEVOS ESTILOS PARA EL TOGGLE MÓVIL --- */ | ||||
| .mobile-view-toggle { | ||||
|   display: none; | ||||
|   /* Oculto por defecto */ | ||||
|   position: fixed; | ||||
|   bottom: 20px; | ||||
|   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; | ||||
|   | ||||
| @@ -0,0 +1,259 @@ | ||||
| /* src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.css */ | ||||
|  | ||||
| /* --- Variables de Diseño --- */ | ||||
| :root { | ||||
|     --card-border-color: #e0e0e0; | ||||
|     --card-bg-color: #ffffff; | ||||
|     --card-header-bg-color: #f8f9fa; | ||||
|     --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); | ||||
|     --text-primary: #212529; | ||||
|     --text-secondary: #6c757d; | ||||
|     --font-family: "Public Sans", system-ui, sans-serif; | ||||
|     --primary-accent-color: #007bff; | ||||
| } | ||||
|  | ||||
| /* --- Contenedor Principal del Widget --- */ | ||||
| .cards-widget-container { | ||||
|     font-family: var(--font-family); | ||||
|     width: 100%; | ||||
|     max-width: 1200px; | ||||
|     margin: 2rem auto; | ||||
| } | ||||
|  | ||||
| .cards-widget-container h2 { | ||||
|     font-size: 1.75rem; | ||||
|     color: var(--text-primary); | ||||
|     margin-bottom: 1.5rem; | ||||
|     padding-bottom: 0.5rem; | ||||
|     border-bottom: 1px solid var(--card-border-color); | ||||
| } | ||||
|  | ||||
| /* --- Grilla de Tarjetas --- */ | ||||
| .cards-grid { | ||||
|     display: grid; | ||||
|     /* Crea columnas flexibles que se ajustan al espacio disponible */ | ||||
|     grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); | ||||
|     gap: 1.5rem; | ||||
| } | ||||
|  | ||||
| /* --- Tarjeta Individual --- */ | ||||
| .provincia-card { | ||||
|     background-color: var(--card-bg-color); | ||||
|     border: 1px solid var(--card-border-color); | ||||
|     border-radius: 8px; | ||||
|     box-shadow: var(--card-shadow); | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     overflow: hidden; /* Asegura que los bordes redondeados se apliquen al contenido */ | ||||
| } | ||||
|  | ||||
| /* --- Cabecera de la Tarjeta --- */ | ||||
| .card-header { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     background-color: var(--card-header-bg-color); | ||||
|     padding: 0.75rem 1rem; | ||||
|     border-bottom: 1px solid var(--card-border-color); | ||||
| } | ||||
|  | ||||
| .header-info h3 { | ||||
|     margin: 0; | ||||
|     font-size: 1.2rem; | ||||
|     font-weight: 700; | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| .header-info span { | ||||
|     font-size: 0.8rem; | ||||
|     color: var(--text-secondary); | ||||
|     text-transform: uppercase; | ||||
| } | ||||
|  | ||||
| .header-map { | ||||
|     width: 90px; | ||||
|     height: 90px; | ||||
|     flex-shrink: 0; | ||||
|     border-radius: 4px; | ||||
|     overflow: hidden; | ||||
|     background-color: #e9ecef; | ||||
|     padding: 0.25rem; | ||||
|     box-sizing: border-box; /* Para que el padding no aumente el tamaño total */ | ||||
| } | ||||
|  | ||||
| /* Contenedor del SVG para asegurar que se ajuste al espacio */ | ||||
| .map-svg-container, .map-placeholder { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
| } | ||||
|  | ||||
| /* Estilo para el SVG renderizado */ | ||||
| .map-svg-container svg { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     object-fit: contain; /* Asegura que el mapa no se deforme */ | ||||
| } | ||||
|  | ||||
| /* Placeholder para cuando el mapa no carga */ | ||||
| .map-placeholder.error { | ||||
|     background-color: #f8d7da; /* Un color de fondo rojizo para indicar un error */ | ||||
| } | ||||
|  | ||||
| /* --- Cuerpo de la Tarjeta --- */ | ||||
| .card-body { | ||||
|     padding: 0.5rem 1rem; | ||||
| } | ||||
|  | ||||
| .candidato-row { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 0.75rem; | ||||
|     padding: 0.75rem 0; | ||||
|     border-bottom: 1px solid #f0f0f0; | ||||
| } | ||||
|  | ||||
| .candidato-row:last-child { | ||||
|     border-bottom: none; | ||||
| } | ||||
|  | ||||
| .candidato-foto { | ||||
|     width: 45px; | ||||
|     height: 45px; | ||||
|     border-radius: 50%; | ||||
|     object-fit: cover; | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .candidato-data { | ||||
|     flex-grow: 1; | ||||
|     min-width: 0; /* Permite que el texto se trunque si es necesario */ | ||||
|     margin-right: 0.5rem; | ||||
| } | ||||
|  | ||||
| .candidato-nombre { | ||||
|     font-weight: 700; | ||||
|     font-size: 0.95rem; | ||||
|     color: var(--text-primary); | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .candidato-partido { | ||||
|     font-size: 0.75rem; | ||||
|     color: var(--text-secondary); | ||||
|     text-transform: uppercase; | ||||
|     display: block; | ||||
|     margin-bottom: 0.3rem; | ||||
| } | ||||
|  | ||||
| .progress-bar-container { | ||||
|     height: 6px; | ||||
|     background-color: #e9ecef; | ||||
|     border-radius: 3px; | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| .progress-bar { | ||||
|     height: 100%; | ||||
|     border-radius: 3px; | ||||
|     transition: width 0.5s ease-out; | ||||
| } | ||||
|  | ||||
| .candidato-stats { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: flex-end; | ||||
|     text-align: right; | ||||
|     flex-shrink: 0; | ||||
|     padding-left: 0.5rem; | ||||
| } | ||||
|  | ||||
| .stats-percent { | ||||
|     font-weight: 700; | ||||
|     font-size: 1.1rem; | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| .stats-votos { | ||||
|     font-size: 0.8rem; | ||||
|     color: var(--text-secondary); | ||||
| } | ||||
|  | ||||
| .stats-bancas { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     flex-shrink: 0; | ||||
|     border: 1px solid var(--card-border-color); | ||||
|     border-radius: 6px; | ||||
|     padding: 0.25rem 0.5rem; | ||||
|     margin-left: 0.75rem; | ||||
|     font-weight: 700; | ||||
|     font-size: 1.2rem; | ||||
|     color: var(--primary-accent-color); | ||||
|     min-width: 50px; | ||||
| } | ||||
|  | ||||
| .stats-bancas span { | ||||
|     font-size: 0.65rem; | ||||
|     font-weight: 500; | ||||
|     color: var(--text-secondary); | ||||
|     text-transform: uppercase; | ||||
|     margin-top: -4px; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* --- Pie de la Tarjeta --- */ | ||||
| .card-footer { | ||||
|     display: grid; | ||||
|     grid-template-columns: repeat(3, 1fr); | ||||
|     background-color: var(--card-header-bg-color); | ||||
|     border-top: 1px solid var(--card-border-color); | ||||
|     padding: 0.75rem 0; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .card-footer div { | ||||
|     border-right: 1px solid var(--card-border-color); | ||||
| } | ||||
|  | ||||
| .card-footer div:last-child { | ||||
|     border-right: none; | ||||
| } | ||||
|  | ||||
| .card-footer span { | ||||
|     display: block; | ||||
|     font-size: 0.75rem; | ||||
|     color: var(--text-secondary); | ||||
| } | ||||
|  | ||||
| .card-footer strong { | ||||
|     font-size: 1rem; | ||||
|     font-weight: 700; | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| /* --- Media Query para Móvil --- */ | ||||
| @media (max-width: 480px) { | ||||
|     .cards-grid { | ||||
|         /* En pantallas muy pequeñas, forzamos una sola columna */ | ||||
|         grid-template-columns: 1fr; | ||||
|     } | ||||
|  | ||||
|     .card-header { | ||||
|         padding: 0.5rem; | ||||
|     } | ||||
|      | ||||
|     .header-info h3 { | ||||
|         font-size: 1rem; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* --- NUEVOS ESTILOS PARA EL NOMBRE DEL PARTIDO CUANDO ES EL TÍTULO PRINCIPAL --- */ | ||||
| .candidato-partido.main-title { | ||||
|     font-size: 0.95rem;      /* Hacemos la fuente más grande */ | ||||
|     font-weight: 700;        /* La ponemos en negrita, como el nombre del candidato */ | ||||
|     color: var(--text-primary); /* Usamos el color de texto principal */ | ||||
|     text-transform: none;    /* Quitamos el 'uppercase' para que se lea mejor */ | ||||
|     margin-bottom: 0.3rem; | ||||
| } | ||||
| @@ -0,0 +1,30 @@ | ||||
| // src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.tsx | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { getResumenPorProvincia } from '../../../apiService'; | ||||
| import { ProvinciaCard } from './components/ProvinciaCard'; | ||||
| import './ResultadosNacionalesCardsWidget.css'; | ||||
|  | ||||
| interface Props { | ||||
|     eleccionId: number; | ||||
| } | ||||
|  | ||||
| export const ResultadosNacionalesCardsWidget = ({ eleccionId }: Props) => { | ||||
|     const { data, isLoading, error } = useQuery({ | ||||
|         queryKey: ['resumenPorProvincia', eleccionId], | ||||
|         queryFn: () => getResumenPorProvincia(eleccionId), | ||||
|     }); | ||||
|  | ||||
|     if (isLoading) return <div>Cargando resultados por provincia...</div>; | ||||
|     if (error) return <div>Error al cargar los datos.</div>; | ||||
|  | ||||
|     return ( | ||||
|         <section className="cards-widget-container"> | ||||
|             <h2>Resultados elecciones nacionales 2025</h2> | ||||
|             <div className="cards-grid"> | ||||
|                 {data?.map(provinciaData => ( | ||||
|                     <ProvinciaCard key={provinciaData.provinciaId} data={provinciaData} /> | ||||
|                 ))} | ||||
|             </div> | ||||
|         </section> | ||||
|     ); | ||||
| }; | ||||
| @@ -0,0 +1,64 @@ | ||||
| // src/features/legislativas/nacionales/components/MiniMapaSvg.tsx | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import axios from 'axios'; | ||||
| import { useMemo } from 'react'; | ||||
| import { assetBaseUrl } from '../../../../apiService'; | ||||
|  | ||||
| interface MiniMapaSvgProps { | ||||
|     provinciaNombre: string; | ||||
|     fillColor: string; | ||||
| } | ||||
|  | ||||
| // Función para normalizar el nombre de la provincia y que coincida con el nombre del archivo SVG | ||||
| const normalizarNombreParaUrl = (nombre: string) =>  | ||||
|     nombre | ||||
|         .toLowerCase() | ||||
|         .replace(/ /g, '_') // Reemplaza espacios con guiones bajos | ||||
|         .normalize("NFD")    // Descompone acentos para eliminarlos en el siguiente paso | ||||
|         .replace(/[\u0300-\u036f]/g, ""); // Elimina los acentos | ||||
|  | ||||
| export const MiniMapaSvg = ({ provinciaNombre, fillColor }: MiniMapaSvgProps) => { | ||||
|     const nombreNormalizado = normalizarNombreParaUrl(provinciaNombre); | ||||
|     // Asumimos que los SVGs están en /public/maps/provincias-svg/ | ||||
|     const mapFileUrl = `${assetBaseUrl}/maps/provincias-svg/${nombreNormalizado}.svg`; | ||||
|  | ||||
|     // Usamos React Query para fetchear el contenido del SVG como texto | ||||
|     const { data: svgContent, isLoading, isError } = useQuery<string>({ | ||||
|         queryKey: ['svgMapa', nombreNormalizado], | ||||
|         queryFn: async () => { | ||||
|             const response = await axios.get(mapFileUrl, { responseType: 'text' }); | ||||
|             return response.data; | ||||
|         }, | ||||
|         staleTime: Infinity, // Estos archivos son estáticos y no cambian | ||||
|         gcTime: Infinity, | ||||
|         retry: false, // No reintentar si el archivo no existe | ||||
|     }); | ||||
|  | ||||
|     // Usamos useMemo para modificar el SVG solo cuando el contenido o el color cambian | ||||
|     const modifiedSvg = useMemo(() => { | ||||
|         if (!svgContent) return ''; | ||||
|  | ||||
|         // Usamos una expresión regular para encontrar todas las etiquetas <path> | ||||
|         // y añadirles el atributo de relleno con el color del ganador. | ||||
|         // Esto sobrescribirá cualquier 'fill' que ya exista en la etiqueta. | ||||
|         return svgContent.replace(/<path/g, `<path fill="${fillColor}"`); | ||||
|     }, [svgContent, fillColor]); | ||||
|  | ||||
|     if (isLoading) { | ||||
|         return <div className="map-placeholder" />; | ||||
|     } | ||||
|  | ||||
|     if (isError || !modifiedSvg) { | ||||
|         // Muestra un placeholder si el SVG no se encontró o está vacío | ||||
|         return <div className="map-placeholder error" />; | ||||
|     } | ||||
|  | ||||
|     // Renderizamos el SVG modificado. dangerouslySetInnerHTML es seguro aquí | ||||
|     // porque el contenido proviene de nuestros propios archivos SVG estáticos. | ||||
|     return ( | ||||
|         <div  | ||||
|             className="map-svg-container"  | ||||
|             dangerouslySetInnerHTML={{ __html: modifiedSvg }}  | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @@ -0,0 +1,78 @@ | ||||
| // src/features/legislativas/nacionales/components/ProvinciaCard.tsx | ||||
| import type { ResumenProvincia } from '../../../../types/types'; | ||||
| import { MiniMapaSvg } from './MiniMapaSvg'; | ||||
| import { ImageWithFallback } from '../../../../components/common/ImageWithFallback'; | ||||
| import { assetBaseUrl } from '../../../../apiService'; | ||||
|  | ||||
| interface ProvinciaCardProps { | ||||
|     data: ResumenProvincia; | ||||
| } | ||||
|  | ||||
| const formatNumber = (num: number) => num.toLocaleString('es-AR'); | ||||
| const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`; | ||||
|  | ||||
| export const ProvinciaCard = ({ data }: ProvinciaCardProps) => { | ||||
|     // Determinamos el color del ganador para pasárselo al mapa. | ||||
|     // Si no hay ganador, usamos un color gris por defecto. | ||||
|     const colorGanador = data.resultados[0]?.color || '#d1d1d1'; | ||||
|  | ||||
|     return ( | ||||
|         <div className="provincia-card"> | ||||
|             <header className="card-header"> | ||||
|                 <div className="header-info"> | ||||
|                     <h3 style={{ whiteSpace: 'normal' }}>{data.provinciaNombre}</h3> | ||||
|                     <span>DIPUTADOS NACIONALES</span> | ||||
|                 </div> | ||||
|                 <div className="header-map"> | ||||
|                     <MiniMapaSvg provinciaNombre={data.provinciaNombre} fillColor={colorGanador} /> | ||||
|                 </div> | ||||
|             </header> | ||||
|             <div className="card-body"> | ||||
|                 {data.resultados.map(res => ( | ||||
|                     <div key={res.agrupacionId} className="candidato-row"> | ||||
|                         <ImageWithFallback src={res.fotoUrl ?? undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={res.nombreCandidato ?? res.nombreAgrupacion} className="candidato-foto" /> | ||||
|  | ||||
|                         <div className="candidato-data"> | ||||
|                             {res.nombreCandidato && ( | ||||
|                                 <span className="candidato-nombre">{res.nombreCandidato}</span> | ||||
|                             )} | ||||
|  | ||||
|                             <span className={`candidato-partido ${!res.nombreCandidato ? 'main-title' : ''}`}> | ||||
|                                 {res.nombreAgrupacion} | ||||
|                             </span> | ||||
|  | ||||
|                             <div className="progress-bar-container"> | ||||
|                                 <div className="progress-bar" style={{ width: `${res.porcentaje}%`, backgroundColor: res.color || '#ccc' }} /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <div className="candidato-stats"> | ||||
|                             <span className="stats-percent">{formatPercent(res.porcentaje)}</span> | ||||
|                             <span className="stats-votos">{formatNumber(res.votos)} votos</span> | ||||
|                         </div> | ||||
|                         <div className="stats-bancas"> | ||||
|                             +{res.bancasObtenidas} | ||||
|                             <span>Bancas</span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 ))} | ||||
|             </div> | ||||
|             <footer className="card-footer"> | ||||
|                 <div> | ||||
|                     <span>Participación</span> | ||||
|                     {/* Usamos los datos reales del estado de recuento */} | ||||
|                     <strong>{formatPercent(data.estadoRecuento?.participacionPorcentaje ?? 0)}</strong> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <span>Mesas escrutadas</span> | ||||
|                     <strong>{formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje ?? 0)}</strong> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <span>Votos totales</span> | ||||
|                     {/* Usamos el nuevo campo cantidadVotantes */} | ||||
|                     <strong>{formatNumber(data.estadoRecuento?.cantidadVotantes ?? 0)}</strong> | ||||
|                 </div> | ||||
|             </footer> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -1,27 +1,35 @@ | ||||
| /* src/features/legislativas/provinciales/CongresoWidget.css */ | ||||
| .congreso-container { | ||||
|   display: flex; | ||||
|   /* Se reduce ligeramente el espacio entre el gráfico y el panel */ | ||||
|   gap: 1rem; | ||||
|   gap: 1.5rem; | ||||
|   background-color: #ffffff; | ||||
|   border: 1px solid #e0e0e0; | ||||
|   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | ||||
|   padding: 1rem; | ||||
|   border-radius: 8px; | ||||
|   max-width: 800px; | ||||
|   max-width: 900px; | ||||
|   margin: 20px auto; | ||||
|   font-family: "Public Sans", system-ui, sans-serif; | ||||
|   color: #333333; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .congreso-grafico { | ||||
|   /* --- CAMBIO PRINCIPAL: Se aumenta la proporción del gráfico --- */ | ||||
|   flex: 1 1 65%; | ||||
|   flex: 2;  | ||||
|   min-width: 300px; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
|  | ||||
| .congreso-hemiciclo-wrapper { | ||||
|   flex-grow: 1; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .congreso-hemiciclo-wrapper.is-hovering .party-block:not(:hover) { | ||||
|   opacity: 0.4; | ||||
| } | ||||
|  | ||||
| .congreso-grafico svg { | ||||
| @@ -30,35 +38,139 @@ | ||||
|   animation: fadeIn 0.8s ease-in-out; | ||||
| } | ||||
|  | ||||
| @keyframes fadeIn { | ||||
|   from { | ||||
|     opacity: 0; | ||||
|     transform: scale(0.9); | ||||
|   } | ||||
| /* --- NUEVOS ESTILOS PARA EL FOOTER DEL GRÁFICO --- */ | ||||
| .congreso-footer { | ||||
|   width: 100%; | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   padding: 0.5rem 1rem 0 1rem; | ||||
|   margin-top: auto; /* Empuja el footer a la parte inferior del contenedor flex */ | ||||
|   font-size: 0.8em; | ||||
|   color: #666; | ||||
|   border-top: 1px solid #eee; | ||||
| } | ||||
|  | ||||
|   to { | ||||
|     opacity: 1; | ||||
|     transform: scale(1); | ||||
|   } | ||||
| .footer-legend { | ||||
|   display: flex; | ||||
|   gap: 1.5rem; /* Espacio entre los items de la leyenda */ | ||||
| } | ||||
|  | ||||
| .footer-legend-item { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 0.5rem; /* Espacio entre el icono y el texto */ | ||||
| } | ||||
|  | ||||
| .footer-timestamp { | ||||
|   font-weight: 500; | ||||
| } | ||||
|  | ||||
| /* --- ESTILOS PARA HOVER --- */ | ||||
|  | ||||
| /* Estilo base para cada círculo de escaño */ | ||||
| .seat-circle { | ||||
|   transition: all 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .party-block { | ||||
|   cursor: pointer; | ||||
|   transition: opacity 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .party-block:hover .seat-circle { | ||||
|   stroke: #333 !important; /* Borde oscuro para resaltar */ | ||||
|   stroke-width: 1.5px !important; | ||||
|   stroke-opacity: 1; | ||||
|   filter: brightness(1.1); | ||||
| } | ||||
|  | ||||
| /* CORRECCIÓN: El selector ahora apunta al wrapper correcto */ | ||||
| .congreso-hemiciclo-wrapper.is-hovering .party-block:not(:hover) { | ||||
|   opacity: 0.3; /* Hacemos el desvanecimiento más pronunciado */ | ||||
| } | ||||
| .congreso-grafico svg { | ||||
|   width: 100%; | ||||
|   height: auto; | ||||
|   animation: fadeIn 0.8s ease-in-out; | ||||
| } | ||||
|  | ||||
| @keyframes fadeIn { | ||||
|   from { opacity: 0; transform: scale(0.9); } | ||||
|   to { opacity: 1; transform: scale(1); } | ||||
| } | ||||
|  | ||||
| /* --- INICIO DE NUEVOS ESTILOS PARA EL FOOTER DEL GRÁFICO --- */ | ||||
| .congreso-footer { | ||||
|   width: 100%; | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   padding: 0.75rem 0.5rem 0 0.5rem; | ||||
|   margin-top: auto; | ||||
|   font-size: 0.8em; | ||||
|   color: #666; | ||||
|   border-top: 1px solid #eee; | ||||
| } | ||||
|  | ||||
| .footer-legend { | ||||
|   display: flex; | ||||
|   gap: 1.25rem; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .footer-legend-item { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 0.6rem; | ||||
|   font-size: 1.1em; | ||||
| } | ||||
|  | ||||
| /* Creamos una clase base para ambos iconos para compartir tamaño */ | ||||
| .legend-icon { | ||||
|   display: inline-block; | ||||
|   width: 14px;   /* Tamaño base para ambos iconos */ | ||||
|   height: 14px; | ||||
|   border-radius: 50%; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| /* Estilo para el icono de "Bancas en juego" (círculo sólido) */ | ||||
| .legend-icon--solid { | ||||
|   background-color: #888; | ||||
|   border: 1px solid #777; | ||||
| } | ||||
|  | ||||
| /* Estilo para el icono de "Bancas previas" (anillo translúcido) */ | ||||
| .legend-icon--ring { | ||||
|   background-color: rgba(136, 136, 136, 0.3); /* #888 con opacidad */ | ||||
|   border: 1px solid #888; /* Borde sólido del mismo color */ | ||||
| } | ||||
|  | ||||
| .footer-timestamp { | ||||
|   font-weight: 500; | ||||
|   font-size: 0.75em; | ||||
| } | ||||
|  | ||||
| .congreso-summary { | ||||
|   /* --- CAMBIO PRINCIPAL: Se reduce la proporción del panel de datos --- */ | ||||
|   flex: 1 1 35%; | ||||
|   flex: 1;  | ||||
|   border-left: 1px solid #e0e0e0; | ||||
|   /* Se reduce el padding para dar aún más espacio al gráfico */ | ||||
|   padding-left: 1rem; | ||||
|   padding-left: 1.25rem; /* Un poco más de padding */ | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   justify-content: flex-start;  | ||||
| } | ||||
|  | ||||
| .congreso-summary h3 { | ||||
|   margin-top: 0; | ||||
|   margin-bottom: 0.75rem; /* Margen inferior reducido */ | ||||
|   font-size: 1.4em; | ||||
|   color: #212529; | ||||
| } | ||||
|  | ||||
| .chamber-tabs { | ||||
|   display: flex; | ||||
|   margin-bottom: 1.5rem; | ||||
|   margin-bottom: 1rem; /* Margen inferior reducido */ | ||||
|   border: 1px solid #dee2e6; | ||||
|   border-radius: 6px; | ||||
|   overflow: hidden; | ||||
| @@ -66,7 +178,7 @@ | ||||
|  | ||||
| .chamber-tabs button { | ||||
|   flex: 1; | ||||
|   padding: 0.75rem 0.5rem; | ||||
|   padding: 0.5rem 0.5rem; | ||||
|   border: none; | ||||
|   background-color: #f8f9fa; | ||||
|   color: #6c757d; | ||||
| @@ -94,7 +206,7 @@ | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: baseline; | ||||
|   margin-bottom: 0.5rem; | ||||
|   margin-bottom: 0.25rem; /* Margen inferior muy reducido */ | ||||
|   font-size: 1.1em; | ||||
| } | ||||
|  | ||||
| @@ -107,7 +219,15 @@ | ||||
| .congreso-summary hr { | ||||
|   border: none; | ||||
|   border-top: 1px solid #e0e0e0; | ||||
|   margin: 1.5rem 0; | ||||
|   margin: 1rem 0; /* Margen vertical reducido */ | ||||
| } | ||||
|  | ||||
| /* Contenedor de la lista de partidos para aplicar el scroll */ | ||||
| .partido-lista-container { | ||||
|     flex-grow: 1; /* Ocupa el espacio vertical disponible */ | ||||
|     overflow-y: auto; /* Muestra el scrollbar si es necesario */ | ||||
|     min-height: 0; /* Truco de Flexbox para que el scroll funcione */ | ||||
|     padding-right: 8px; /* Espacio para el scrollbar */ | ||||
| } | ||||
|  | ||||
| .partido-lista { | ||||
| @@ -119,14 +239,14 @@ | ||||
| .partido-lista li { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-bottom: 0.75rem; | ||||
|   margin-bottom: 0.85rem; /* Un poco más de espacio entre items */ | ||||
| } | ||||
|  | ||||
| .partido-color-box { | ||||
|   width: 14px; | ||||
|   height: 14px; | ||||
|   border-radius: 3px; | ||||
|   margin-right: 10px; | ||||
|   width: 16px;  /* Cuadro de color más grande */ | ||||
|   height: 16px; | ||||
|   border-radius: 4px; /* Un poco más cuadrado */ | ||||
|   margin-right: 12px; | ||||
|   flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| @@ -139,19 +259,54 @@ | ||||
|   font-size: 1.1em; | ||||
| } | ||||
|  | ||||
| /* --- Media Query para Responsividad Móvil --- */ | ||||
| /* --- Media Query para Responsividad Móvil (HASTA 768px) --- */ | ||||
| @media (max-width: 768px) { | ||||
|   .congreso-container { | ||||
|     flex-direction: column; | ||||
|     padding: 1.5rem; | ||||
|     padding: 0.5rem; | ||||
|     height: auto; | ||||
|     max-height: none; | ||||
|   } | ||||
|  | ||||
|   .congreso-summary { | ||||
|     border-left: none; | ||||
|     padding-left: 0; | ||||
|     margin-top: 2rem; | ||||
|     border-top: 1px solid #e0e0e0; | ||||
|     padding-top: 1.5rem; | ||||
|   } | ||||
|  | ||||
|   .partido-lista-container { | ||||
|     overflow-y: visible; | ||||
|     max-height: none; | ||||
|   } | ||||
|  | ||||
|   .congreso-footer { | ||||
|     flex-direction: column; /* Apila la leyenda y el timestamp verticalmente */ | ||||
|     align-items: flex-start; /* Alinea todo a la izquierda */ | ||||
|     gap: 0.5rem; /* Añade un pequeño espacio entre la leyenda y el timestamp */ | ||||
|     padding: 0.75rem 0rem; /* Ajusta el padding para móvil */ | ||||
|     align-items: center; | ||||
|   } | ||||
|  | ||||
|   .footer-legend { | ||||
|     gap: 0.75rem; /* Reduce el espacio entre los items de la leyenda */ | ||||
|   } | ||||
|  | ||||
|   .footer-legend-item{ | ||||
|     font-size: 1em; | ||||
|   } | ||||
|  | ||||
|   .footer-timestamp { | ||||
|     font-size: 0.75em; /* Reduce el tamaño de la fuente para que quepa mejor */ | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| /* --- Media Query para Escritorio (DESDE 769px en adelante) --- */ | ||||
| @media (min-width: 769px) { | ||||
|   .congreso-container { | ||||
|     flex-direction: row; | ||||
|     align-items: stretch; | ||||
|     height: 500px;  | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -245,4 +245,29 @@ export interface PanelElectoralDto { | ||||
|   mapaData: ResultadoMapaDto[]; | ||||
|   resultadosPanel: ResultadoTicker[]; // Reutilizamos el tipo que ya tienes | ||||
|   estadoRecuento: EstadoRecuentoTicker; // Reutilizamos el tipo que ya tienes | ||||
| } | ||||
|  | ||||
| // --- TIPOS PARA EL WIDGET DE TARJETAS --- | ||||
| export interface EstadoRecuentoDto { | ||||
|     participacionPorcentaje: number; | ||||
|     mesasTotalizadasPorcentaje: number; | ||||
|     cantidadVotantes: number; | ||||
| } | ||||
|  | ||||
| export interface ResultadoCandidato { | ||||
|     agrupacionId: string; | ||||
|     nombreCandidato: string | null; | ||||
|     nombreAgrupacion: string; | ||||
|     fotoUrl: string | null; | ||||
|     color: string | null; | ||||
|     porcentaje: number; | ||||
|     votos: number; | ||||
|     bancasObtenidas: number; | ||||
| } | ||||
|  | ||||
| export interface ResumenProvincia { | ||||
|     provinciaId: string; | ||||
|     provinciaNombre: string; | ||||
|     estadoRecuento: EstadoRecuentoDto | null; | ||||
|     resultados: ResultadoCandidato[]; | ||||
| } | ||||
| @@ -92,6 +92,36 @@ public class AdminController : ControllerBase | ||||
|     return Ok(); | ||||
|   } | ||||
|  | ||||
|   // --- ENDPOINTS PARA NACIONALES --- | ||||
|  | ||||
|   [HttpPut("agrupaciones/orden-diputados-nacionales")] | ||||
|   public async Task<IActionResult> UpdateDiputadosNacionalesOrden([FromBody] List<string> idsAgrupacionesOrdenadas) | ||||
|   { | ||||
|     await _dbContext.AgrupacionesPoliticas.ExecuteUpdateAsync(s => s.SetProperty(a => a.OrdenDiputadosNacionales, (int?)null)); | ||||
|  | ||||
|     for (int i = 0; i < idsAgrupacionesOrdenadas.Count; i++) | ||||
|     { | ||||
|       var agrupacion = await _dbContext.AgrupacionesPoliticas.FindAsync(idsAgrupacionesOrdenadas[i]); | ||||
|       if (agrupacion != null) agrupacion.OrdenDiputadosNacionales = i + 1; | ||||
|     } | ||||
|     await _dbContext.SaveChangesAsync(); | ||||
|     return Ok(); | ||||
|   } | ||||
|  | ||||
|   [HttpPut("agrupaciones/orden-senadores-nacionales")] | ||||
|   public async Task<IActionResult> UpdateSenadoresNacionalesOrden([FromBody] List<string> idsAgrupacionesOrdenadas) | ||||
|   { | ||||
|     await _dbContext.AgrupacionesPoliticas.ExecuteUpdateAsync(s => s.SetProperty(a => a.OrdenSenadoresNacionales, (int?)null)); | ||||
|  | ||||
|     for (int i = 0; i < idsAgrupacionesOrdenadas.Count; i++) | ||||
|     { | ||||
|       var agrupacion = await _dbContext.AgrupacionesPoliticas.FindAsync(idsAgrupacionesOrdenadas[i]); | ||||
|       if (agrupacion != null) agrupacion.OrdenSenadoresNacionales = i + 1; | ||||
|     } | ||||
|     await _dbContext.SaveChangesAsync(); | ||||
|     return Ok(); | ||||
|   } | ||||
|  | ||||
|   // LEER todas las configuraciones | ||||
|   [HttpGet("configuracion")] | ||||
|   public async Task<IActionResult> GetConfiguracion() | ||||
| @@ -125,14 +155,15 @@ public class AdminController : ControllerBase | ||||
|  | ||||
|   // LEER: Obtener todas las bancadas para una cámara, con su partido y ocupante actual | ||||
|   [HttpGet("bancadas/{camara}")] | ||||
|   public async Task<IActionResult> GetBancadas(TipoCamara camara) | ||||
|   public async Task<IActionResult> GetBancadas(TipoCamara camara, [FromQuery] int eleccionId) | ||||
|   { | ||||
|     // 3. La lógica interna se mantiene igual, ya que filtra por ambos parámetros. | ||||
|     var bancadas = await _dbContext.Bancadas | ||||
|         .AsNoTracking() | ||||
|         .Include(b => b.AgrupacionPolitica) | ||||
|         .Include(b => b.Ocupante) | ||||
|         .Where(b => b.Camara == camara) | ||||
|         .OrderBy(b => b.Id) // Ordenar por ID para consistencia | ||||
|         .Where(b => b.EleccionId == eleccionId && b.Camara == camara) | ||||
|         .OrderBy(b => b.NumeroBanca) | ||||
|         .ToListAsync(); | ||||
|  | ||||
|     return Ok(bancadas); | ||||
| @@ -181,10 +212,39 @@ public class AdminController : ControllerBase | ||||
|     return NoContent(); | ||||
|   } | ||||
|  | ||||
|   [HttpGet("logos")] | ||||
|   public async Task<IActionResult> GetLogos() | ||||
|   [HttpGet("catalogos/provincias")] | ||||
|   public async Task<IActionResult> GetProvinciasForAdmin() | ||||
|   { | ||||
|     return Ok(await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().ToListAsync()); | ||||
|     var provincias = await _dbContext.AmbitosGeograficos | ||||
|         .AsNoTracking() | ||||
|         .Where(a => a.NivelId == 10) // Nivel 10 = Provincia | ||||
|         .OrderBy(a => a.Nombre) | ||||
|         .Select(a => new { Id = a.Id.ToString(), Nombre = a.Nombre }) | ||||
|         .ToListAsync(); | ||||
|     return Ok(provincias); | ||||
|   } | ||||
|  | ||||
|   // --- ENDPOINTS MODIFICADOS --- | ||||
|  | ||||
|   [HttpGet("logos")] | ||||
|   public async Task<IActionResult> GetLogos([FromQuery] int eleccionId) | ||||
|   { | ||||
|     // Añadimos el filtro por EleccionId | ||||
|     return Ok(await _dbContext.LogosAgrupacionesCategorias | ||||
|         .AsNoTracking() | ||||
|         .Where(l => l.EleccionId == eleccionId) | ||||
|         .ToListAsync()); | ||||
|   } | ||||
|  | ||||
|   [HttpGet("candidatos")] | ||||
|   public async Task<IActionResult> GetCandidatos([FromQuery] int eleccionId) | ||||
|   { | ||||
|     // Añadimos el filtro por EleccionId | ||||
|     var candidatos = await _dbContext.CandidatosOverrides | ||||
|         .AsNoTracking() | ||||
|         .Where(c => c.EleccionId == eleccionId) | ||||
|         .ToListAsync(); | ||||
|     return Ok(candidatos); | ||||
|   } | ||||
|  | ||||
|   [HttpPut("logos")] | ||||
| @@ -239,18 +299,6 @@ public class AdminController : ControllerBase | ||||
|     return Ok(municipios); | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Obtiene todos los overrides de candidatos configurados. | ||||
|   /// </summary> | ||||
|   [HttpGet("candidatos")] | ||||
|   public async Task<IActionResult> GetCandidatos() | ||||
|   { | ||||
|     var candidatos = await _dbContext.CandidatosOverrides | ||||
|         .AsNoTracking() | ||||
|         .ToListAsync(); | ||||
|     return Ok(candidatos); | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Guarda (actualiza o crea) una lista de overrides de candidatos. | ||||
|   /// </summary> | ||||
| @@ -337,4 +385,40 @@ public class AdminController : ControllerBase | ||||
|     _logger.LogWarning("El nivel de logging ha sido cambiado a: {Level}", request.Level); | ||||
|     return Ok(new { message = $"Nivel de logging actualizado a '{request.Level}'." }); | ||||
|   } | ||||
|  | ||||
|   // LEER todas las bancas previas para una elección | ||||
|   [HttpGet("bancas-previas/{eleccionId}")] | ||||
|   public async Task<IActionResult> GetBancasPrevias(int eleccionId) | ||||
|   { | ||||
|     var bancas = await _dbContext.BancasPrevias | ||||
|         .AsNoTracking() | ||||
|         .Where(b => b.EleccionId == eleccionId) | ||||
|         .Include(b => b.AgrupacionPolitica) | ||||
|         .ToListAsync(); | ||||
|     return Ok(bancas); | ||||
|   } | ||||
|  | ||||
|   // GUARDAR (Upsert) una lista de bancas previas | ||||
|   [HttpPut("bancas-previas/{eleccionId}")] | ||||
|   public async Task<IActionResult> UpdateBancasPrevias(int eleccionId, [FromBody] List<BancaPrevia> bancas) | ||||
|   { | ||||
|     // Borramos los registros existentes para esta elección para simplificar la lógica | ||||
|     await _dbContext.BancasPrevias.Where(b => b.EleccionId == eleccionId).ExecuteDeleteAsync(); | ||||
|  | ||||
|     // Añadimos los nuevos registros que tienen al menos una banca | ||||
|     foreach (var banca in bancas.Where(b => b.Cantidad > 0)) | ||||
|     { | ||||
|       _dbContext.BancasPrevias.Add(new BancaPrevia | ||||
|       { | ||||
|         EleccionId = eleccionId, | ||||
|         Camara = banca.Camara, | ||||
|         AgrupacionPoliticaId = banca.AgrupacionPoliticaId, | ||||
|         Cantidad = banca.Cantidad | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     await _dbContext.SaveChangesAsync(); | ||||
|     _logger.LogInformation("Se actualizaron las bancas previas para la EleccionId: {EleccionId}", eleccionId); | ||||
|     return NoContent(); | ||||
|   } | ||||
| } | ||||
| @@ -1208,13 +1208,16 @@ public class ResultadosController : ControllerBase | ||||
|  | ||||
|     [HttpGet("mapa-resultados")] | ||||
|     public async Task<IActionResult> GetResultadosMapaPorMunicipio( | ||||
|     [FromRoute] int eleccionId, | ||||
|     [FromQuery] int categoriaId, | ||||
|     [FromQuery] string? distritoId = null) | ||||
| [FromRoute] int eleccionId, | ||||
| [FromQuery] int categoriaId, | ||||
| [FromQuery] string? distritoId = null) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(distritoId)) | ||||
|         { | ||||
|             // --- VISTA NACIONAL (Ya corregida y funcionando) --- | ||||
|             // --- VISTA NACIONAL (LÓGICA CORRECTA Y ROBUSTA) --- | ||||
|  | ||||
|             // PASO 1: Agrupar y sumar los votos por provincia y partido directamente en la BD. | ||||
|             // Esto crea una lista con los totales, que es mucho más pequeña que los datos crudos. | ||||
|             var votosAgregadosPorProvincia = await _dbContext.ResultadosVotos | ||||
|                 .AsNoTracking() | ||||
|                 .Where(r => r.EleccionId == eleccionId | ||||
| @@ -1224,23 +1227,26 @@ public class ResultadosController : ControllerBase | ||||
|                 .GroupBy(r => new { r.AmbitoGeografico.DistritoId, r.AgrupacionPoliticaId }) | ||||
|                 .Select(g => new | ||||
|                 { | ||||
|                     g.Key.DistritoId, | ||||
|                     g.Key.AgrupacionPoliticaId, | ||||
|                     DistritoId = g.Key.DistritoId!, // Sabemos que no es nulo por el .Where() | ||||
|                     AgrupacionPoliticaId = g.Key.AgrupacionPoliticaId, | ||||
|                     TotalVotos = g.Sum(r => r.CantidadVotos) | ||||
|                 }) | ||||
|                 .ToListAsync(); | ||||
|  | ||||
|             var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas.AsNoTracking().ToDictionaryAsync(a => a.Id); | ||||
|             var provinciasInfo = await _dbContext.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10).ToListAsync(); | ||||
|  | ||||
|             // PASO 2: Encontrar el ganador para cada provincia en la memoria de la aplicación. | ||||
|             // Esto es muy rápido porque se hace sobre la lista ya agregada. | ||||
|             var ganadoresPorProvincia = votosAgregadosPorProvincia | ||||
|                 .GroupBy(r => r.DistritoId) | ||||
|                 .Select(g => g.OrderByDescending(x => x.TotalVotos).First()) | ||||
|                 .ToList(); | ||||
|  | ||||
|             // PASO 3: Obtener los datos adicionales (nombres, colores) para construir la respuesta final. | ||||
|             var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas.AsNoTracking().ToDictionaryAsync(a => a.Id); | ||||
|             var provinciasInfo = await _dbContext.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10).ToListAsync(); | ||||
|  | ||||
|             var mapaDataNacional = ganadoresPorProvincia.Select(g => new ResultadoMapaDto | ||||
|             { | ||||
|                 AmbitoId = g.DistritoId!, | ||||
|                 AmbitoId = g.DistritoId, | ||||
|                 AmbitoNombre = provinciasInfo.FirstOrDefault(p => p.DistritoId == g.DistritoId)?.Nombre ?? "Desconocido", | ||||
|                 AgrupacionGanadoraId = g.AgrupacionPoliticaId, | ||||
|                 ColorGanador = agrupacionesInfo.GetValueOrDefault(g.AgrupacionPoliticaId)?.Color ?? "#808080" | ||||
| @@ -1250,16 +1256,13 @@ public class ResultadosController : ControllerBase | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // --- VISTA PROVINCIAL (AHORA CORREGIDA CON LA MISMA LÓGICA) --- | ||||
|  | ||||
|             // PASO 1: Agrupar por IDs y sumar votos en la base de datos. | ||||
|             // --- VISTA PROVINCIAL (SIN CAMBIOS, YA ERA EFICIENTE) --- | ||||
|             var votosAgregadosPorMunicipio = await _dbContext.ResultadosVotos | ||||
|                 .AsNoTracking() | ||||
|                 .Where(r => r.EleccionId == eleccionId | ||||
|                             && r.CategoriaId == categoriaId | ||||
|                             && r.AmbitoGeografico.DistritoId == distritoId | ||||
|                             && r.AmbitoGeografico.NivelId == 30) | ||||
|                 // Agrupamos por los IDs (int y string) | ||||
|                 .GroupBy(r => new { r.AmbitoGeograficoId, r.AgrupacionPoliticaId }) | ||||
|                 .Select(g => new | ||||
|                 { | ||||
| @@ -1269,13 +1272,11 @@ public class ResultadosController : ControllerBase | ||||
|                 }) | ||||
|                 .ToListAsync(); | ||||
|  | ||||
|             // PASO 2: Encontrar el ganador para cada municipio en memoria. | ||||
|             var ganadoresPorMunicipio = votosAgregadosPorMunicipio | ||||
|                 .GroupBy(r => r.AmbitoGeograficoId) | ||||
|                 .Select(g => g.OrderByDescending(x => x.TotalVotos).First()) | ||||
|                 .ToList(); | ||||
|  | ||||
|             // PASO 3: Hidratar con los nombres y colores (muy rápido). | ||||
|             var idsMunicipios = ganadoresPorMunicipio.Select(g => g.AmbitoGeograficoId).ToList(); | ||||
|             var idsAgrupaciones = ganadoresPorMunicipio.Select(g => g.AgrupacionPoliticaId).ToList(); | ||||
|  | ||||
| @@ -1285,7 +1286,6 @@ public class ResultadosController : ControllerBase | ||||
|             var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas.AsNoTracking() | ||||
|                 .Where(a => idsAgrupaciones.Contains(a.Id)).ToDictionaryAsync(a => a.Id); | ||||
|  | ||||
|             // Mapeo final a DTO. | ||||
|             var mapaDataProvincial = ganadoresPorMunicipio.Select(g => new ResultadoMapaDto | ||||
|             { | ||||
|                 AmbitoId = g.AmbitoGeograficoId.ToString(), | ||||
| @@ -1297,4 +1297,193 @@ public class ResultadosController : ControllerBase | ||||
|             return Ok(mapaDataProvincial); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpGet("composicion-nacional")] | ||||
|     public async Task<IActionResult> GetComposicionNacional([FromRoute] int eleccionId) | ||||
|     { | ||||
|         // 1. Obtener todas las configuraciones relevantes en una sola consulta. | ||||
|         var config = await _dbContext.Configuraciones.AsNoTracking().ToDictionaryAsync(c => c.Clave, c => c.Valor); | ||||
|  | ||||
|         // 2. Obtener todas las agrupaciones políticas en una sola consulta. | ||||
|         var todasAgrupaciones = await _dbContext.AgrupacionesPoliticas.AsNoTracking().ToDictionaryAsync(a => a.Id); | ||||
|  | ||||
|         // 3. Obtener las bancas PREVIAS (las que no están en juego). | ||||
|         var bancasPrevias = await _dbContext.BancasPrevias | ||||
|             .AsNoTracking() | ||||
|             .Where(b => b.EleccionId == eleccionId) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         // 4. Obtener las bancas EN JUEGO (proyectadas por provincia). | ||||
|         var proyecciones = await _dbContext.ProyeccionesBancas | ||||
|             .AsNoTracking() | ||||
|             .Where(p => p.EleccionId == eleccionId && p.AmbitoGeografico.NivelId == 10) // Nivel 10 = Ámbito Provincial | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         //Calculamos la fecha de la última proyección. | ||||
|         // Si no hay proyecciones aún, usamos la fecha y hora actual como un fallback seguro. | ||||
|         var ultimaActualizacion = proyecciones.Any() | ||||
|             ? proyecciones.Max(p => p.FechaTotalizacion) | ||||
|             : DateTime.UtcNow; | ||||
|  | ||||
|         // 5. Combinar los datos para obtener la composición final de cada partido. | ||||
|         var composicionFinal = todasAgrupaciones.Values.Select(agrupacion => new | ||||
|         { | ||||
|             Agrupacion = agrupacion, | ||||
|             DiputadosFijos = bancasPrevias.FirstOrDefault(b => b.AgrupacionPoliticaId == agrupacion.Id && b.Camara == Core.Enums.TipoCamara.Diputados)?.Cantidad ?? 0, | ||||
|             DiputadosGanados = proyecciones.Where(p => p.AgrupacionPoliticaId == agrupacion.Id && p.CategoriaId == 2).Sum(p => p.NroBancas), | ||||
|             SenadoresFijos = bancasPrevias.FirstOrDefault(b => b.AgrupacionPoliticaId == agrupacion.Id && b.Camara == Core.Enums.TipoCamara.Senadores)?.Cantidad ?? 0, | ||||
|             SenadoresGanados = proyecciones.Where(p => p.AgrupacionPoliticaId == agrupacion.Id && p.CategoriaId == 1).Sum(p => p.NroBancas) | ||||
|         }) | ||||
|         .Select(r => new | ||||
|         { | ||||
|             r.Agrupacion, | ||||
|             r.DiputadosFijos, | ||||
|             r.DiputadosGanados, | ||||
|             DiputadosTotales = r.DiputadosFijos + r.DiputadosGanados, | ||||
|             r.SenadoresFijos, | ||||
|             r.SenadoresGanados, | ||||
|             SenadoresTotales = r.SenadoresFijos + r.SenadoresGanados | ||||
|         }) | ||||
|         .ToList(); | ||||
|  | ||||
|         // 6. Determinar la información de la presidencia para cada cámara. | ||||
|         config.TryGetValue("PresidenciaDiputadosNacional", out var idPartidoPresDip); | ||||
|         var partidoPresidenteDiputados = !string.IsNullOrEmpty(idPartidoPresDip) ? todasAgrupaciones.GetValueOrDefault(idPartidoPresDip) : null; | ||||
|         config.TryGetValue("PresidenciaDiputadosNacional_TipoBanca", out var tipoBancaDip); | ||||
|  | ||||
|         config.TryGetValue("PresidenciaSenadoNacional", out var idPartidoPresSen); | ||||
|         var partidoPresidenteSenadores = !string.IsNullOrEmpty(idPartidoPresSen) ? todasAgrupaciones.GetValueOrDefault(idPartidoPresSen) : null; | ||||
|         config.TryGetValue("PresidenciaSenadoNacional_TipoBanca", out var tipoBancaSen); | ||||
|  | ||||
|  | ||||
|         // 7. Construir el objeto de respuesta final para D Diputados | ||||
|         var diputados = new | ||||
|         { | ||||
|             CamaraNombre = "Cámara de Diputados", | ||||
|             TotalBancas = 257, | ||||
|             BancasEnJuego = 127, | ||||
|             UltimaActualizacion = ultimaActualizacion, | ||||
|             Partidos = composicionFinal | ||||
|                 .Where(p => p.DiputadosTotales > 0) | ||||
|                 .OrderByDescending(p => p.DiputadosTotales) | ||||
|                 .Select(p => new | ||||
|                 { | ||||
|                     p.Agrupacion.Id, | ||||
|                     p.Agrupacion.Nombre, | ||||
|                     p.Agrupacion.NombreCorto, | ||||
|                     p.Agrupacion.Color, | ||||
|                     BancasFijos = p.DiputadosFijos, | ||||
|                     BancasGanadas = p.DiputadosGanados, | ||||
|                     BancasTotales = p.DiputadosTotales, | ||||
|                     p.Agrupacion.OrdenDiputadosNacionales, | ||||
|                     p.Agrupacion.OrdenSenadoresNacionales | ||||
|                 }).ToList(), | ||||
|             PresidenteBancada = partidoPresidenteDiputados != null | ||||
|                 ? new { Color = partidoPresidenteDiputados.Color, TipoBanca = tipoBancaDip ?? "ganada" } | ||||
|                 : null | ||||
|         }; | ||||
|  | ||||
|         // 8. Construir el objeto de respuesta final para Senadores | ||||
|         var senadores = new | ||||
|         { | ||||
|             CamaraNombre = "Senado de la Nación", | ||||
|             TotalBancas = 72, | ||||
|             BancasEnJuego = 24, | ||||
|             UltimaActualizacion = ultimaActualizacion, | ||||
|             Partidos = composicionFinal | ||||
|                 .Where(p => p.SenadoresTotales > 0) | ||||
|                 .OrderByDescending(p => p.SenadoresTotales) | ||||
|                 .Select(p => new | ||||
|                 { | ||||
|                     p.Agrupacion.Id, | ||||
|                     p.Agrupacion.Nombre, | ||||
|                     p.Agrupacion.NombreCorto, | ||||
|                     p.Agrupacion.Color, | ||||
|                     BancasFijos = p.SenadoresFijos, | ||||
|                     BancasGanadas = p.SenadoresGanados, | ||||
|                     BancasTotales = p.SenadoresTotales, | ||||
|                     p.Agrupacion.OrdenDiputadosNacionales, | ||||
|                     p.Agrupacion.OrdenSenadoresNacionales | ||||
|                 }).ToList(), | ||||
|             PresidenteBancada = partidoPresidenteSenadores != null | ||||
|                 ? new { Color = partidoPresidenteSenadores.Color, TipoBanca = tipoBancaSen ?? "ganada" } | ||||
|                 : null | ||||
|         }; | ||||
|  | ||||
|         return Ok(new { Diputados = diputados, Senadores = senadores }); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("resumen-por-provincia")] | ||||
|     public async Task<IActionResult> GetResumenPorProvincia([FromRoute] int eleccionId) | ||||
|     { | ||||
|         const int categoriaDiputadosNacionales = 2; | ||||
|  | ||||
|         var todasLasProyecciones = await _dbContext.ProyeccionesBancas.AsNoTracking() | ||||
|             .Where(p => p.EleccionId == eleccionId && p.CategoriaId == categoriaDiputadosNacionales) | ||||
|             .ToDictionaryAsync(p => p.AmbitoGeograficoId + "_" + p.AgrupacionPoliticaId); | ||||
|  | ||||
|         var todosLosOverrides = await _dbContext.CandidatosOverrides.AsNoTracking() | ||||
|             .Where(c => c.EleccionId == eleccionId && c.CategoriaId == categoriaDiputadosNacionales) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var todosLosLogos = await _dbContext.LogosAgrupacionesCategorias.AsNoTracking() | ||||
|             .Where(l => l.EleccionId == eleccionId && l.CategoriaId == categoriaDiputadosNacionales) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var datosBrutos = await _dbContext.AmbitosGeograficos.AsNoTracking() | ||||
|             .Where(a => a.NivelId == 10) | ||||
|             .Select(provincia => new | ||||
|             { | ||||
|                 ProvinciaAmbitoId = provincia.Id, | ||||
|                 ProvinciaDistritoId = provincia.DistritoId!, | ||||
|                 ProvinciaNombre = provincia.Nombre, | ||||
|                 EstadoRecuento = _dbContext.EstadosRecuentosGenerales | ||||
|                     .Where(e => e.EleccionId == eleccionId && e.CategoriaId == categoriaDiputadosNacionales && e.AmbitoGeograficoId == provincia.Id) | ||||
|                     .Select(e => new EstadoRecuentoDto { /* ... */ }) | ||||
|                     .FirstOrDefault(), | ||||
|                 ResultadosBrutos = _dbContext.ResultadosVotos | ||||
|                     .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaDiputadosNacionales && r.AmbitoGeografico.DistritoId == provincia.DistritoId) | ||||
|                     .GroupBy(r => r.AgrupacionPolitica) | ||||
|                     .Select(g => new { Agrupacion = g.Key, Votos = g.Sum(r => r.CantidadVotos) }) | ||||
|                     .OrderByDescending(x => x.Votos) | ||||
|                     .Take(2) | ||||
|                     .ToList() | ||||
|             }) | ||||
|             .OrderBy(p => p.ProvinciaNombre) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var resultadosFinales = datosBrutos.Select(provinciaData => | ||||
|         { | ||||
|             var totalVotosProvincia = (decimal)provinciaData.ResultadosBrutos.Sum(r => r.Votos); | ||||
|             return new ResumenProvinciaDto | ||||
|             { | ||||
|                 ProvinciaId = provinciaData.ProvinciaDistritoId, | ||||
|                 ProvinciaNombre = provinciaData.ProvinciaNombre, | ||||
|                 EstadoRecuento = provinciaData.EstadoRecuento, | ||||
|                 Resultados = provinciaData.ResultadosBrutos.Select(r => | ||||
|                 { | ||||
|                     var provinciaAmbitoId = provinciaData.ProvinciaAmbitoId; | ||||
|                     return new ResultadoCandidatoDto | ||||
|                     { | ||||
|                         AgrupacionId = r.Agrupacion.Id, | ||||
|                         NombreAgrupacion = r.Agrupacion.NombreCorto ?? r.Agrupacion.Nombre, | ||||
|                         Color = r.Agrupacion.Color, | ||||
|                         Votos = r.Votos, | ||||
|                         NombreCandidato = (todosLosOverrides.FirstOrDefault(c => c.AgrupacionPoliticaId == r.Agrupacion.Id && c.AmbitoGeograficoId == provinciaAmbitoId) | ||||
|                                           ?? todosLosOverrides.FirstOrDefault(c => c.AgrupacionPoliticaId == r.Agrupacion.Id && c.AmbitoGeograficoId == null)) | ||||
|                                           ?.NombreCandidato, | ||||
|                         FotoUrl = (todosLosLogos.FirstOrDefault(l => l.AgrupacionPoliticaId == r.Agrupacion.Id && l.AmbitoGeograficoId == provinciaAmbitoId) | ||||
|                                    ?? todosLosLogos.FirstOrDefault(l => l.AgrupacionPoliticaId == r.Agrupacion.Id && l.AmbitoGeograficoId == null)) | ||||
|                                    ?.LogoUrl, | ||||
|                         BancasObtenidas = todasLasProyecciones.ContainsKey(provinciaAmbitoId + "_" + r.Agrupacion.Id) | ||||
|                                         ? todasLasProyecciones[provinciaAmbitoId + "_" + r.Agrupacion.Id].NroBancas | ||||
|                                         : 0, | ||||
|                         Porcentaje = totalVotosProvincia > 0 ? (r.Votos / totalVotosProvincia) * 100 : 0 | ||||
|                     }; | ||||
|                 }).ToList() | ||||
|             }; | ||||
|         }).ToList(); | ||||
|  | ||||
|         return Ok(resultadosFinales); | ||||
|     } | ||||
| } | ||||
| @@ -10,6 +10,8 @@ using Microsoft.IdentityModel.Tokens; | ||||
| using Elecciones.Database.Entities; | ||||
| using System.Text.Json.Serialization; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
| using Elecciones.Core.Enums; | ||||
| using Microsoft.OpenApi.Models; | ||||
|  | ||||
| // Esta es la estructura estándar y recomendada. | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
| @@ -81,8 +83,40 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) | ||||
|     }); | ||||
|  | ||||
| builder.Services.AddEndpointsApiExplorer(); | ||||
| builder.Services.AddSwaggerGen(); | ||||
|  | ||||
| //builder.Services.AddSwaggerGen(); | ||||
|  | ||||
| builder.Services.AddSwaggerGen(options => | ||||
| { | ||||
|     // 1. Definir el esquema de seguridad que usaremos (Bearer Token) | ||||
|     options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme | ||||
|     { | ||||
|         Description = "Autorización JWT usando el esquema Bearer. Ingresa 'Bearer' [espacio] y luego tu token. Ejemplo: 'Bearer 12345abcdef'", | ||||
|         Name = "Authorization", // El nombre del header HTTP | ||||
|         In = ParameterLocation.Header, // Dónde se ubicará el token (en el header) | ||||
|         Type = SecuritySchemeType.ApiKey, // El tipo de esquema | ||||
|         Scheme = "Bearer" // El nombre del esquema | ||||
|     }); | ||||
|  | ||||
|     // 2. Aplicar este requisito de seguridad a todos los endpoints que lo necesiten | ||||
|     options.AddSecurityRequirement(new OpenApiSecurityRequirement() | ||||
|     { | ||||
|         { | ||||
|             new OpenApiSecurityScheme | ||||
|             { | ||||
|                 Reference = new OpenApiReference | ||||
|                 { | ||||
|                     Type = ReferenceType.SecurityScheme, | ||||
|                     Id = "Bearer" // Debe coincidir con el nombre que le dimos en AddSecurityDefinition | ||||
|                 }, | ||||
|                 Scheme = "oauth2", | ||||
|                 Name = "Bearer", | ||||
|                 In = ParameterLocation.Header, | ||||
|             }, | ||||
|             new List<string>() | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| builder.Services.Configure<ForwardedHeadersOptions>(options => | ||||
| { | ||||
| @@ -153,7 +187,23 @@ using (var scope = app.Services.CreateScope()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Seeder para las bancas vacías | ||||
| // --- SEEDER DE ELECCIONES (Añadir para asegurar que existan) --- | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var context = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|     if (!context.Elecciones.Any()) | ||||
|     { | ||||
|         context.Elecciones.AddRange( | ||||
|             new Eleccion { Id = 1, Nombre = "Elecciones Provinciales 2025", Nivel = "Provincial", DistritoId = "02", Fecha = new DateOnly(2025, 10, 26) }, | ||||
|             new Eleccion { Id = 2, Nombre = "Elecciones Nacionales 2025", Nivel = "Nacional", DistritoId = "00", Fecha = new DateOnly(2025, 10, 26) } | ||||
|         ); | ||||
|         context.SaveChanges(); | ||||
|         Console.WriteLine("--> Seeded Eleccion entities."); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| // --- SEEDER DE BANCAS (MODIFICADO Y COMPLETADO) --- | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var services = scope.ServiceProvider; | ||||
| @@ -161,27 +211,91 @@ using (var scope = app.Services.CreateScope()) | ||||
|     if (!context.Bancadas.Any()) | ||||
|     { | ||||
|         var bancas = new List<Bancada>(); | ||||
|         // 92 bancas de diputados | ||||
|         for (int i = 1; i <= 92; i++) // Bucle de 1 a 92 | ||||
|  | ||||
|         // --- BANCAS PROVINCIALES (EleccionId = 1) --- | ||||
|         // 92 bancas de diputados provinciales | ||||
|         for (int i = 1; i <= 92; i++) | ||||
|         { | ||||
|             bancas.Add(new Bancada | ||||
|             { | ||||
|                 EleccionId = 1, | ||||
|                 Camara = Elecciones.Core.Enums.TipoCamara.Diputados, | ||||
|                 NumeroBanca = i // Asignamos el número de banca | ||||
|                 NumeroBanca = i | ||||
|             }); | ||||
|         } | ||||
|         // 46 bancas de senadores | ||||
|         for (int i = 1; i <= 46; i++) // Bucle de 1 a 46 | ||||
|         // 46 bancas de senadores provinciales | ||||
|         for (int i = 1; i <= 46; i++) | ||||
|         { | ||||
|             bancas.Add(new Bancada | ||||
|             { | ||||
|                 EleccionId = 1, | ||||
|                 Camara = Elecciones.Core.Enums.TipoCamara.Senadores, | ||||
|                 NumeroBanca = i // Asignamos el número de banca | ||||
|                 NumeroBanca = i | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // --- BANCAS NACIONALES (EleccionId = 2) --- | ||||
|         // 257 bancas de diputados nacionales | ||||
|         for (int i = 1; i <= 257; i++) | ||||
|         { | ||||
|             bancas.Add(new Bancada | ||||
|             { | ||||
|                 EleccionId = 2, | ||||
|                 Camara = TipoCamara.Diputados, | ||||
|                 NumeroBanca = i | ||||
|             }); | ||||
|         } | ||||
|         // 72 bancas de senadores nacionales | ||||
|         for (int i = 1; i <= 72; i++) | ||||
|         { | ||||
|             bancas.Add(new Bancada | ||||
|             { | ||||
|                 EleccionId = 2, | ||||
|                 Camara = TipoCamara.Senadores, | ||||
|                 NumeroBanca = i | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         context.Bancadas.AddRange(bancas); | ||||
|         context.SaveChanges(); | ||||
|         Console.WriteLine("--> Seeded 138 bancas físicas."); | ||||
|         Console.WriteLine($"--> Seeded {bancas.Count} bancas físicas para ambas elecciones."); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // --- Seeder para Proyecciones de Bancas (Elección Nacional) --- | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var services = scope.ServiceProvider; | ||||
|     var context = services.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|     const int eleccionNacionalId = 2; | ||||
|     // Categoría 2: Diputados Nacionales, Categoría 1: Senadores Nacionales | ||||
|     if (!context.ProyeccionesBancas.Any(p => p.EleccionId == eleccionNacionalId)) | ||||
|     { | ||||
|         var partidos = await context.AgrupacionesPoliticas.Take(5).ToListAsync(); | ||||
|         var provincia = await context.AmbitosGeograficos.FirstOrDefaultAsync(a => a.NivelId == 10); // Asumimos un ámbito provincial genérico para la proyección total | ||||
|  | ||||
|         if (partidos.Count >= 5 && provincia != null) | ||||
|         { | ||||
|             var proyecciones = new List<ProyeccionBanca> | ||||
|             { | ||||
|                 // -- DIPUTADOS (Se renuevan 127) -- | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 2, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[0].Id, NroBancas = 50, FechaTotalizacion = DateTime.UtcNow }, | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 2, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[1].Id, NroBancas = 40, FechaTotalizacion = DateTime.UtcNow }, | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 2, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[2].Id, NroBancas = 20, FechaTotalizacion = DateTime.UtcNow }, | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 2, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[3].Id, NroBancas = 10, FechaTotalizacion = DateTime.UtcNow }, | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 2, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[4].Id, NroBancas = 7, FechaTotalizacion = DateTime.UtcNow }, | ||||
|  | ||||
|                 // -- SENADORES (Se renuevan 24) -- | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 1, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[0].Id, NroBancas = 10, FechaTotalizacion = DateTime.UtcNow }, | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 1, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[1].Id, NroBancas = 8, FechaTotalizacion = DateTime.UtcNow }, | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 1, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[2].Id, NroBancas = 4, FechaTotalizacion = DateTime.UtcNow }, | ||||
|                 new() { EleccionId = eleccionNacionalId, CategoriaId = 1, AmbitoGeograficoId = provincia.Id, AgrupacionPoliticaId = partidos[3].Id, NroBancas = 2, FechaTotalizacion = DateTime.UtcNow }, | ||||
|             }; | ||||
|             await context.ProyeccionesBancas.AddRangeAsync(proyecciones); | ||||
|             await context.SaveChangesAsync(); | ||||
|             Console.WriteLine("--> Seeded Proyecciones de Bancas para la Elección Nacional."); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -200,7 +314,10 @@ using (var scope = app.Services.CreateScope()) | ||||
|         { "Worker_Resultados_Activado", "false" }, | ||||
|         { "Worker_Bajas_Activado", "false" }, | ||||
|         { "Worker_Prioridad", "Resultados" }, | ||||
|         { "Logging_Level", "Information" } | ||||
|         { "Logging_Level", "Information" }, | ||||
|         { "PresidenciaDiputadosNacional", "" }, | ||||
|         { "PresidenciaDiputadosNacional_TipoBanca", "ganada" }, | ||||
|         { "PresidenciaSenadoNacional_TipoBanca", "ganada" } | ||||
|     }; | ||||
|  | ||||
|     foreach (var config in defaultConfiguraciones) | ||||
| @@ -230,7 +347,7 @@ using (var scope = app.Services.CreateScope()) | ||||
|  | ||||
|         var eleccionNacional = await context.Elecciones.FindAsync(eleccionNacionalId) ?? new Eleccion { Id = eleccionNacionalId, Nombre = "Elecciones Nacionales 2025", Nivel = "Nacional", DistritoId = "00", Fecha = new DateOnly(2025, 10, 26) }; | ||||
|         if (!context.Elecciones.Local.Any(e => e.Id == eleccionNacionalId)) context.Elecciones.Add(eleccionNacional); | ||||
|          | ||||
|  | ||||
|         var categoriaDiputadosNac = await context.CategoriasElectorales.FindAsync(2) ?? new CategoriaElectoral { Id = 2, Nombre = "DIPUTADOS NACIONALES", Orden = 3 }; | ||||
|         if (!context.CategoriasElectorales.Local.Any(c => c.Id == 2)) context.CategoriasElectorales.Add(categoriaDiputadosNac); | ||||
|         await context.SaveChangesAsync(); | ||||
| @@ -270,7 +387,8 @@ using (var scope = app.Services.CreateScope()) | ||||
|         await context.SaveChangesAsync(); | ||||
|  | ||||
|         var todosLosPartidos = await context.AgrupacionesPoliticas.Take(5).ToListAsync(); | ||||
|         if (!todosLosPartidos.Any()) { | ||||
|         if (!todosLosPartidos.Any()) | ||||
|         { | ||||
|             logger.LogWarning("--> No hay partidos, no se pueden generar votos."); | ||||
|             return; | ||||
|         } | ||||
| @@ -278,7 +396,7 @@ using (var scope = app.Services.CreateScope()) | ||||
|         var nuevosResultados = new List<ResultadoVoto>(); | ||||
|         var nuevosEstados = new List<EstadoRecuentoGeneral>(); | ||||
|         var rand = new Random(); | ||||
|          | ||||
|  | ||||
|         long totalVotosNacional = 0; | ||||
|         int totalMesasNacional = 0; | ||||
|         int totalMesasEscrutadasNacional = 0; | ||||
| @@ -287,9 +405,9 @@ using (var scope = app.Services.CreateScope()) | ||||
|         { | ||||
|             var municipiosDeProvincia = await context.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 30 && a.DistritoId == provincia.DistritoId).ToListAsync(); | ||||
|             if (!municipiosDeProvincia.Any()) continue; | ||||
|              | ||||
|  | ||||
|             long totalVotosProvincia = 0; | ||||
|              | ||||
|  | ||||
|             int partidoIndex = rand.Next(todosLosPartidos.Count); | ||||
|             foreach (var municipio in municipiosDeProvincia) | ||||
|             { | ||||
| @@ -299,7 +417,8 @@ using (var scope = app.Services.CreateScope()) | ||||
|                 totalVotosProvincia += votosGanador; | ||||
|  | ||||
|                 var otrosPartidos = todosLosPartidos.Where(p => p.Id != partidoGanador.Id).OrderBy(p => rand.Next()).Take(rand.Next(3, todosLosPartidos.Count)); | ||||
|                 foreach (var competidor in otrosPartidos) { | ||||
|                 foreach (var competidor in otrosPartidos) | ||||
|                 { | ||||
|                     var votosCompetidor = rand.Next(1000, 24000); | ||||
|                     nuevosResultados.Add(new ResultadoVoto { EleccionId = eleccionNacionalId, AmbitoGeograficoId = municipio.Id, CategoriaId = categoriaDiputadosNac.Id, AgrupacionPoliticaId = competidor.Id, CantidadVotos = votosCompetidor }); | ||||
|                     totalVotosProvincia += votosCompetidor; | ||||
| @@ -312,8 +431,11 @@ using (var scope = app.Services.CreateScope()) | ||||
|             var cantidadElectoresProvincia = mesasEsperadasProvincia * 350; | ||||
|             var participacionProvincia = (decimal)(rand.Next(65, 85) / 100.0); | ||||
|  | ||||
|             nuevosEstados.Add(new EstadoRecuentoGeneral { | ||||
|                 EleccionId = eleccionNacionalId, AmbitoGeograficoId = provincia.Id, CategoriaId = categoriaDiputadosNac.Id, | ||||
|             nuevosEstados.Add(new EstadoRecuentoGeneral | ||||
|             { | ||||
|                 EleccionId = eleccionNacionalId, | ||||
|                 AmbitoGeograficoId = provincia.Id, | ||||
|                 CategoriaId = categoriaDiputadosNac.Id, | ||||
|                 FechaTotalizacion = DateTime.UtcNow, | ||||
|                 MesasEsperadas = mesasEsperadasProvincia, | ||||
|                 MesasTotalizadas = mesasTotalizadasProvincia, | ||||
| @@ -322,7 +444,7 @@ using (var scope = app.Services.CreateScope()) | ||||
|                 CantidadVotantes = (int)(cantidadElectoresProvincia * participacionProvincia), | ||||
|                 ParticipacionPorcentaje = participacionProvincia * 100 | ||||
|             }); | ||||
|              | ||||
|  | ||||
|             totalVotosNacional += totalVotosProvincia; | ||||
|             totalMesasNacional += mesasEsperadasProvincia; | ||||
|             totalMesasEscrutadasNacional += mesasTotalizadasProvincia; | ||||
| @@ -330,14 +452,18 @@ using (var scope = app.Services.CreateScope()) | ||||
|  | ||||
|         // --- LÓGICA DE DATOS DE RECUENTO A NIVEL NACIONAL --- | ||||
|         var ambitoNacional = await context.AmbitosGeograficos.AsNoTracking().FirstOrDefaultAsync(a => a.NivelId == 0); | ||||
|         if (ambitoNacional == null) { | ||||
|         if (ambitoNacional == null) | ||||
|         { | ||||
|             ambitoNacional = new AmbitoGeografico { Nombre = "Nacional", NivelId = 0, DistritoId = "00" }; | ||||
|             context.AmbitosGeograficos.Add(ambitoNacional); | ||||
|             await context.SaveChangesAsync(); | ||||
|         } | ||||
|         var participacionNacional = (decimal)(rand.Next(70, 88) / 100.0); | ||||
|         nuevosEstados.Add(new EstadoRecuentoGeneral { | ||||
|             EleccionId = eleccionNacionalId, AmbitoGeograficoId = ambitoNacional.Id, CategoriaId = categoriaDiputadosNac.Id, | ||||
|         nuevosEstados.Add(new EstadoRecuentoGeneral | ||||
|         { | ||||
|             EleccionId = eleccionNacionalId, | ||||
|             AmbitoGeograficoId = ambitoNacional.Id, | ||||
|             CategoriaId = categoriaDiputadosNac.Id, | ||||
|             FechaTotalizacion = DateTime.UtcNow, | ||||
|             MesasEsperadas = totalMesasNacional, | ||||
|             MesasTotalizadas = totalMesasEscrutadasNacional, | ||||
| @@ -347,17 +473,56 @@ using (var scope = app.Services.CreateScope()) | ||||
|             ParticipacionPorcentaje = participacionNacional * 100 | ||||
|         }); | ||||
|  | ||||
|         if (nuevosResultados.Any()) { | ||||
|         if (nuevosResultados.Any()) | ||||
|         { | ||||
|             await context.ResultadosVotos.AddRangeAsync(nuevosResultados); | ||||
|             await context.EstadosRecuentosGenerales.AddRangeAsync(nuevosEstados); | ||||
|             await context.SaveChangesAsync(); | ||||
|             logger.LogInformation("--> Se generaron {Votos} registros de votos y {Estados} de estados de recuento.", nuevosResultados.Count, nuevosEstados.Count); | ||||
|         } else { | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             logger.LogWarning("--> No se generaron datos de simulación."); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // --- Seeder para Bancas Previas (Composición Nacional 2025) --- | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var services = scope.ServiceProvider; | ||||
|     var context = services.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|     const int eleccionNacionalId = 2; | ||||
|  | ||||
|     if (!context.BancasPrevias.Any(b => b.EleccionId == eleccionNacionalId)) | ||||
|     { | ||||
|         var partidos = await context.AgrupacionesPoliticas.Take(5).ToListAsync(); | ||||
|         if (partidos.Count >= 5) | ||||
|         { | ||||
|             var bancasPrevias = new List<BancaPrevia> | ||||
|             { | ||||
|                 // -- DIPUTADOS (Total: 257, se renuevan 127, quedan 130) -- | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = partidos[0].Id, Cantidad = 40 }, | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = partidos[1].Id, Cantidad = 35 }, | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = partidos[2].Id, Cantidad = 30 }, | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = partidos[3].Id, Cantidad = 15 }, | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Diputados, AgrupacionPoliticaId = partidos[4].Id, Cantidad = 10 }, | ||||
|  | ||||
|                 // -- SENADORES (Total: 72, se renuevan 24, quedan 48) -- | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = partidos[0].Id, Cantidad = 18 }, | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = partidos[1].Id, Cantidad = 15 }, | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = partidos[2].Id, Cantidad = 8 }, | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = partidos[3].Id, Cantidad = 4 }, | ||||
|                 new() { EleccionId = eleccionNacionalId, Camara = TipoCamara.Senadores, AgrupacionPoliticaId = partidos[4].Id, Cantidad = 3 }, | ||||
|             }; | ||||
|             await context.BancasPrevias.AddRangeAsync(bancasPrevias); | ||||
|             await context.SaveChangesAsync(); | ||||
|             Console.WriteLine("--> Seeded Bancas Previas para la Elección Nacional."); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Configurar el pipeline de peticiones HTTP. | ||||
| // Añadimos el logging de peticiones de Serilog aquí. | ||||
| app.UseSerilogRequestLogging(); | ||||
|   | ||||
| @@ -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+5a8bee52d57b0f215705f3a7efb654169f85a7ae")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+67634ae947197595f6f644f3a80a982dd3573dfb")] | ||||
| [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":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","ayv780bSyYJGn9z2hycOzUCHGRbnvrzG/wr0RB8XoSg=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","WbNXPR1x3J5zRGe6yPRR\u002BWmWo3I/jnjzOyd\u002BJP8MhMI="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","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=","0dvJZBTDvT8AWA99AJa8lh9rnQsEsujRTFe1QDxskcw=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","amEOUyqq4sgg/zUP6A7nQMqSHcl7G5zl2HvyHRlhDvU="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","ayv780bSyYJGn9z2hycOzUCHGRbnvrzG/wr0RB8XoSg=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","WbNXPR1x3J5zRGe6yPRR\u002BWmWo3I/jnjzOyd\u002BJP8MhMI="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","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=","0dvJZBTDvT8AWA99AJa8lh9rnQsEsujRTFe1QDxskcw=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","amEOUyqq4sgg/zUP6A7nQMqSHcl7G5zl2HvyHRlhDvU="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| @@ -0,0 +1,22 @@ | ||||
| // src/Elecciones.Core/DTOs/ApiResponses/ResumenProvinciaDto.cs | ||||
| namespace Elecciones.Core.DTOs.ApiResponses; | ||||
|  | ||||
| public class ResultadoCandidatoDto | ||||
| { | ||||
|      public string AgrupacionId { get; set; } = string.Empty; | ||||
|     public string? NombreCandidato { get; set; } | ||||
|     public string NombreAgrupacion { get; set; } = null!; | ||||
|     public string? FotoUrl { get; set; } | ||||
|     public string? Color { get; set; } | ||||
|     public decimal Porcentaje { get; set; } | ||||
|     public long Votos { get; set; } | ||||
|     public int BancasObtenidas { get; set; } | ||||
| } | ||||
|  | ||||
| public class ResumenProvinciaDto | ||||
| { | ||||
|     public string ProvinciaId { get; set; } = null!; // Corresponde al DistritoId | ||||
|     public string ProvinciaNombre { get; set; } = null!; | ||||
|     public EstadoRecuentoDto? EstadoRecuento { get; set; } | ||||
|     public List<ResultadoCandidatoDto> Resultados { get; set; } = new(); | ||||
| } | ||||
| @@ -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+5a8bee52d57b0f215705f3a7efb654169f85a7ae")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+67634ae947197595f6f644f3a80a982dd3573dfb")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -22,6 +22,7 @@ public class EleccionesDbContext(DbContextOptions<EleccionesDbContext> options) | ||||
|     public DbSet<LogoAgrupacionCategoria> LogosAgrupacionesCategorias { get; set; } | ||||
|     public DbSet<CandidatoOverride> CandidatosOverrides { get; set; } | ||||
|     public DbSet<Eleccion> Elecciones { get; set; } | ||||
|     public DbSet<BancaPrevia> BancasPrevias { get; set; } | ||||
|  | ||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||
|     { | ||||
| @@ -90,5 +91,10 @@ public class EleccionesDbContext(DbContextOptions<EleccionesDbContext> options) | ||||
|             entity.HasIndex(c => new { c.AgrupacionPoliticaId, c.CategoriaId, c.AmbitoGeograficoId }) | ||||
|                   .IsUnique(); | ||||
|         }); | ||||
|         modelBuilder.Entity<BancaPrevia>(entity => | ||||
|         { | ||||
|             entity.Property(e => e.AgrupacionPoliticaId) | ||||
|                   .UseCollation("Modern_Spanish_CI_AS"); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @@ -10,8 +10,14 @@ public class AgrupacionPolitica | ||||
|     public string IdTelegrama { get; set; } = null!; | ||||
|     [Required] | ||||
|     public string Nombre { get; set; } = null!; | ||||
|     public string? NombreCorto { get; set; } // Para leyendas y gráficos | ||||
|     public string? Color { get; set; }       // Código hexadecimal, ej: "#1f77b4" | ||||
|     public string? NombreCorto { get; set; } | ||||
|     public string? Color { get; set; } | ||||
|      | ||||
|     // --- Campos para Provinciales --- | ||||
|     public int? OrdenDiputados { get; set; } | ||||
|     public int? OrdenSenadores { get; set; } | ||||
|  | ||||
|     // --- Campos para Nacionales --- | ||||
|     public int? OrdenDiputadosNacionales { get; set; } | ||||
|     public int? OrdenSenadoresNacionales { get; set; } | ||||
| } | ||||
| @@ -0,0 +1,28 @@ | ||||
| // src/Elecciones.Database/Entities/BancaPrevia.cs | ||||
| using Elecciones.Core.Enums; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
|  | ||||
| namespace Elecciones.Database.Entities; | ||||
|  | ||||
| public class BancaPrevia | ||||
| { | ||||
|     [Key] | ||||
|     public int Id { get; set; } | ||||
|  | ||||
|     [Required] | ||||
|     public int EleccionId { get; set; } | ||||
|  | ||||
|     [Required] | ||||
|     public TipoCamara Camara { get; set; } | ||||
|  | ||||
|     [Required] | ||||
|     public string AgrupacionPoliticaId { get; set; } = null!; | ||||
|  | ||||
|     [ForeignKey("AgrupacionPoliticaId")] | ||||
|     public AgrupacionPolitica AgrupacionPolitica { get; set; } = null!; | ||||
|  | ||||
|     // Cantidad de bancas que el partido retiene (no están en juego) | ||||
|     [Required] | ||||
|     public int Cantidad { get; set; } | ||||
| } | ||||
| @@ -7,6 +7,7 @@ namespace Elecciones.Database.Entities; | ||||
| public class EstadoRecuentoGeneral | ||||
| { | ||||
|     public int AmbitoGeograficoId { get; set; } | ||||
|     public AmbitoGeografico AmbitoGeografico { get; set; } = null!; | ||||
|     public int CategoriaId { get; set; } | ||||
|     public DateTime FechaTotalizacion { get; set; } | ||||
|     public int MesasEsperadas { get; set; } | ||||
|   | ||||
							
								
								
									
										715
									
								
								Elecciones-Web/src/Elecciones.Database/Migrations/20250922213437_AddBancasPreviasTable.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,715 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using Elecciones.Database; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Metadata; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace Elecciones.Database.Migrations | ||||
| { | ||||
|     [DbContext(typeof(EleccionesDbContext))] | ||||
|     [Migration("20250922213437_AddBancasPreviasTable")] | ||||
|     partial class AddBancasPreviasTable | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .UseCollation("Modern_Spanish_CI_AS") | ||||
|                 .HasAnnotation("ProductVersion", "9.0.8") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 128); | ||||
|  | ||||
|             SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("Eleccion", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("DistritoId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<DateOnly>("Fecha") | ||||
|                         .HasColumnType("date"); | ||||
|  | ||||
|                     b.Property<string>("Nivel") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("Nombre") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("Elecciones"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.AdminUser", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("PasswordHash") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("PasswordSalt") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("Username") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(100) | ||||
|                         .HasColumnType("nvarchar(100)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("AdminUsers"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.AgrupacionPolitica", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<string>("Color") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("IdTelegrama") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("Nombre") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("NombreCorto") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenDiputados") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenSenadores") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("AgrupacionesPoliticas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.AmbitoGeografico", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("CircuitoId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("DistritoId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("EstablecimientoId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("MesaId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("MunicipioId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<int>("NivelId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("Nombre") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("SeccionId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("SeccionProvincialId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("AmbitosGeograficos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.BancaPrevia", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)") | ||||
|                         .UseCollation("Modern_Spanish_CI_AS"); | ||||
|  | ||||
|                     b.Property<int>("Camara") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("Cantidad") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.ToTable("BancasPrevias"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("Camara") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("NumeroBanca") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.ToTable("Bancadas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.CandidatoOverride", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int?>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("NombreCandidato") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(255) | ||||
|                         .HasColumnType("nvarchar(255)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AmbitoGeograficoId"); | ||||
|  | ||||
|                     b.HasIndex("CategoriaId"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId") | ||||
|                         .IsUnique() | ||||
|                         .HasFilter("[AmbitoGeograficoId] IS NOT NULL"); | ||||
|  | ||||
|                     b.ToTable("CandidatosOverrides"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.CategoriaElectoral", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("Nombre") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<int>("Orden") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("CategoriasElectorales"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Configuracion", b => | ||||
|                 { | ||||
|                     b.Property<string>("Clave") | ||||
|                         .HasMaxLength(100) | ||||
|                         .HasColumnType("nvarchar(100)"); | ||||
|  | ||||
|                     b.Property<string>("Valor") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(100) | ||||
|                         .HasColumnType("nvarchar(100)"); | ||||
|  | ||||
|                     b.HasKey("Clave"); | ||||
|  | ||||
|                     b.ToTable("Configuraciones"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b => | ||||
|                 { | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CantidadElectores") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CantidadVotantes") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaTotalizacion") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.Property<int>("MesasEsperadas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("MesasTotalizadas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<decimal>("MesasTotalizadasPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.Property<decimal>("ParticipacionPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.Property<long>("VotosEnBlanco") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<decimal>("VotosEnBlancoPorcentaje") | ||||
|                         .HasPrecision(18, 4) | ||||
|                         .HasColumnType("decimal(18,4)"); | ||||
|  | ||||
|                     b.Property<long>("VotosNulos") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<decimal>("VotosNulosPorcentaje") | ||||
|                         .HasPrecision(18, 4) | ||||
|                         .HasColumnType("decimal(18,4)"); | ||||
|  | ||||
|                     b.Property<long>("VotosRecurridos") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<decimal>("VotosRecurridosPorcentaje") | ||||
|                         .HasPrecision(18, 4) | ||||
|                         .HasColumnType("decimal(18,4)"); | ||||
|  | ||||
|                     b.HasKey("AmbitoGeograficoId", "CategoriaId"); | ||||
|  | ||||
|                     b.ToTable("EstadosRecuentos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b => | ||||
|                 { | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CantidadElectores") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CantidadVotantes") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaTotalizacion") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.Property<int>("MesasEsperadas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("MesasTotalizadas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<decimal>("MesasTotalizadasPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.Property<decimal>("ParticipacionPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.HasKey("AmbitoGeograficoId", "CategoriaId"); | ||||
|  | ||||
|                     b.HasIndex("CategoriaId"); | ||||
|  | ||||
|                     b.ToTable("EstadosRecuentosGenerales"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.LogoAgrupacionCategoria", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int?>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("LogoUrl") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId") | ||||
|                         .IsUnique() | ||||
|                         .HasFilter("[AmbitoGeograficoId] IS NOT NULL"); | ||||
|  | ||||
|                     b.ToTable("LogosAgrupacionesCategorias"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.OcupanteBanca", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<int>("BancadaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("FotoUrl") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("NombreOcupante") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("Periodo") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("BancadaId") | ||||
|                         .IsUnique(); | ||||
|  | ||||
|                     b.ToTable("OcupantesBancas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaTotalizacion") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.Property<int>("NroBancas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.HasIndex("AmbitoGeograficoId", "CategoriaId", "AgrupacionPoliticaId") | ||||
|                         .IsUnique(); | ||||
|  | ||||
|                     b.ToTable("ProyeccionesBancas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<long>("CantidadVotos") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<decimal>("PorcentajeVotos") | ||||
|                         .HasPrecision(18, 4) | ||||
|                         .HasColumnType("decimal(18,4)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.HasIndex("AmbitoGeograficoId", "CategoriaId", "AgrupacionPoliticaId") | ||||
|                         .IsUnique(); | ||||
|  | ||||
|                     b.ToTable("ResultadosVotos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<long>("Votos") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<decimal>("VotosPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.ToTable("ResumenesVotos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Telegrama", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("ContenidoBase64") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaEscaneo") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaTotalizacion") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("Telegramas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.BancaPrevia", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.CandidatoOverride", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AmbitoGeograficoId"); | ||||
|  | ||||
|                     b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("CategoriaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|  | ||||
|                     b.Navigation("AmbitoGeografico"); | ||||
|  | ||||
|                     b.Navigation("CategoriaElectoral"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AmbitoGeograficoId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AmbitoGeografico"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("CategoriaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("CategoriaElectoral"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.OcupanteBanca", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.Bancada", "Bancada") | ||||
|                         .WithOne("Ocupante") | ||||
|                         .HasForeignKey("Elecciones.Database.Entities.OcupanteBanca", "BancadaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("Bancada"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AmbitoGeograficoId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|  | ||||
|                     b.Navigation("AmbitoGeografico"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AmbitoGeograficoId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|  | ||||
|                     b.Navigation("AmbitoGeografico"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b => | ||||
|                 { | ||||
|                     b.Navigation("Ocupante"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace Elecciones.Database.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddBancasPreviasTable : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "BancasPrevias", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     Id = table.Column<int>(type: "int", nullable: false) | ||||
|                         .Annotation("SqlServer:Identity", "1, 1"), | ||||
|                     EleccionId = table.Column<int>(type: "int", nullable: false), | ||||
|                     Camara = table.Column<int>(type: "int", nullable: false), | ||||
|                     AgrupacionPoliticaId = table.Column<string>(type: "nvarchar(450)", nullable: false, collation: "Modern_Spanish_CI_AS"), | ||||
|                     Cantidad = table.Column<int>(type: "int", nullable: false) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("PK_BancasPrevias", x => x.Id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "FK_BancasPrevias_AgrupacionesPoliticas_AgrupacionPoliticaId", | ||||
|                         column: x => x.AgrupacionPoliticaId, | ||||
|                         principalTable: "AgrupacionesPoliticas", | ||||
|                         principalColumn: "Id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "IX_BancasPrevias_AgrupacionPoliticaId", | ||||
|                 table: "BancasPrevias", | ||||
|                 column: "AgrupacionPoliticaId"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "BancasPrevias"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,721 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using Elecciones.Database; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Metadata; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace Elecciones.Database.Migrations | ||||
| { | ||||
|     [DbContext(typeof(EleccionesDbContext))] | ||||
|     [Migration("20250924000007_AddOrdenNacionalToAgrupaciones")] | ||||
|     partial class AddOrdenNacionalToAgrupaciones | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .UseCollation("Modern_Spanish_CI_AS") | ||||
|                 .HasAnnotation("ProductVersion", "9.0.8") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 128); | ||||
|  | ||||
|             SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("Eleccion", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("DistritoId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<DateOnly>("Fecha") | ||||
|                         .HasColumnType("date"); | ||||
|  | ||||
|                     b.Property<string>("Nivel") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("Nombre") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("Elecciones"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.AdminUser", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("PasswordHash") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("PasswordSalt") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("Username") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(100) | ||||
|                         .HasColumnType("nvarchar(100)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("AdminUsers"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.AgrupacionPolitica", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<string>("Color") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("IdTelegrama") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("Nombre") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("NombreCorto") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenDiputados") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenDiputadosNacionales") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenSenadores") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenSenadoresNacionales") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("AgrupacionesPoliticas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.AmbitoGeografico", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("CircuitoId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("DistritoId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("EstablecimientoId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("MesaId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("MunicipioId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<int>("NivelId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("Nombre") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("SeccionId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("SeccionProvincialId") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("AmbitosGeograficos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.BancaPrevia", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)") | ||||
|                         .UseCollation("Modern_Spanish_CI_AS"); | ||||
|  | ||||
|                     b.Property<int>("Camara") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("Cantidad") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.ToTable("BancasPrevias"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("Camara") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("NumeroBanca") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.ToTable("Bancadas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.CandidatoOverride", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int?>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("NombreCandidato") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(255) | ||||
|                         .HasColumnType("nvarchar(255)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AmbitoGeograficoId"); | ||||
|  | ||||
|                     b.HasIndex("CategoriaId"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId") | ||||
|                         .IsUnique() | ||||
|                         .HasFilter("[AmbitoGeograficoId] IS NOT NULL"); | ||||
|  | ||||
|                     b.ToTable("CandidatosOverrides"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.CategoriaElectoral", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("Nombre") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<int>("Orden") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("CategoriasElectorales"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Configuracion", b => | ||||
|                 { | ||||
|                     b.Property<string>("Clave") | ||||
|                         .HasMaxLength(100) | ||||
|                         .HasColumnType("nvarchar(100)"); | ||||
|  | ||||
|                     b.Property<string>("Valor") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(100) | ||||
|                         .HasColumnType("nvarchar(100)"); | ||||
|  | ||||
|                     b.HasKey("Clave"); | ||||
|  | ||||
|                     b.ToTable("Configuraciones"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b => | ||||
|                 { | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CantidadElectores") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CantidadVotantes") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaTotalizacion") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.Property<int>("MesasEsperadas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("MesasTotalizadas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<decimal>("MesasTotalizadasPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.Property<decimal>("ParticipacionPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.Property<long>("VotosEnBlanco") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<decimal>("VotosEnBlancoPorcentaje") | ||||
|                         .HasPrecision(18, 4) | ||||
|                         .HasColumnType("decimal(18,4)"); | ||||
|  | ||||
|                     b.Property<long>("VotosNulos") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<decimal>("VotosNulosPorcentaje") | ||||
|                         .HasPrecision(18, 4) | ||||
|                         .HasColumnType("decimal(18,4)"); | ||||
|  | ||||
|                     b.Property<long>("VotosRecurridos") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<decimal>("VotosRecurridosPorcentaje") | ||||
|                         .HasPrecision(18, 4) | ||||
|                         .HasColumnType("decimal(18,4)"); | ||||
|  | ||||
|                     b.HasKey("AmbitoGeograficoId", "CategoriaId"); | ||||
|  | ||||
|                     b.ToTable("EstadosRecuentos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b => | ||||
|                 { | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CantidadElectores") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CantidadVotantes") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaTotalizacion") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.Property<int>("MesasEsperadas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("MesasTotalizadas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<decimal>("MesasTotalizadasPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.Property<decimal>("ParticipacionPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.HasKey("AmbitoGeograficoId", "CategoriaId"); | ||||
|  | ||||
|                     b.HasIndex("CategoriaId"); | ||||
|  | ||||
|                     b.ToTable("EstadosRecuentosGenerales"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.LogoAgrupacionCategoria", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int?>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("LogoUrl") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId") | ||||
|                         .IsUnique() | ||||
|                         .HasFilter("[AmbitoGeograficoId] IS NOT NULL"); | ||||
|  | ||||
|                     b.ToTable("LogosAgrupacionesCategorias"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.OcupanteBanca", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<int>("BancadaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("FotoUrl") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("NombreOcupante") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<string>("Periodo") | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("BancadaId") | ||||
|                         .IsUnique(); | ||||
|  | ||||
|                     b.ToTable("OcupantesBancas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaTotalizacion") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.Property<int>("NroBancas") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.HasIndex("AmbitoGeograficoId", "CategoriaId", "AgrupacionPoliticaId") | ||||
|                         .IsUnique(); | ||||
|  | ||||
|                     b.ToTable("ProyeccionesBancas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<long>("CantidadVotos") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<int>("CategoriaId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<decimal>("PorcentajeVotos") | ||||
|                         .HasPrecision(18, 4) | ||||
|                         .HasColumnType("decimal(18,4)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.HasIndex("AmbitoGeograficoId", "CategoriaId", "AgrupacionPoliticaId") | ||||
|                         .IsUnique(); | ||||
|  | ||||
|                     b.ToTable("ResultadosVotos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<long>("Votos") | ||||
|                         .HasColumnType("bigint"); | ||||
|  | ||||
|                     b.Property<decimal>("VotosPorcentaje") | ||||
|                         .HasPrecision(5, 2) | ||||
|                         .HasColumnType("decimal(5,2)"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.ToTable("ResumenesVotos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Telegrama", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasColumnType("nvarchar(450)"); | ||||
|  | ||||
|                     b.Property<int>("AmbitoGeograficoId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<string>("ContenidoBase64") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(max)"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaEscaneo") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.Property<DateTime>("FechaTotalizacion") | ||||
|                         .HasColumnType("datetime2"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("Telegramas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.BancaPrevia", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.CandidatoOverride", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AmbitoGeograficoId"); | ||||
|  | ||||
|                     b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("CategoriaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|  | ||||
|                     b.Navigation("AmbitoGeografico"); | ||||
|  | ||||
|                     b.Navigation("CategoriaElectoral"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AmbitoGeograficoId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AmbitoGeografico"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("CategoriaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("CategoriaElectoral"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.OcupanteBanca", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.Bancada", "Bancada") | ||||
|                         .WithOne("Ocupante") | ||||
|                         .HasForeignKey("Elecciones.Database.Entities.OcupanteBanca", "BancadaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("Bancada"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AmbitoGeograficoId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|  | ||||
|                     b.Navigation("AmbitoGeografico"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AmbitoGeograficoId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|  | ||||
|                     b.Navigation("AmbitoGeografico"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b => | ||||
|                 { | ||||
|                     b.Navigation("Ocupante"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,38 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace Elecciones.Database.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddOrdenNacionalToAgrupaciones : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<int>( | ||||
|                 name: "OrdenDiputadosNacionales", | ||||
|                 table: "AgrupacionesPoliticas", | ||||
|                 type: "int", | ||||
|                 nullable: true); | ||||
|  | ||||
|             migrationBuilder.AddColumn<int>( | ||||
|                 name: "OrdenSenadoresNacionales", | ||||
|                 table: "AgrupacionesPoliticas", | ||||
|                 type: "int", | ||||
|                 nullable: true); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "OrdenDiputadosNacionales", | ||||
|                 table: "AgrupacionesPoliticas"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "OrdenSenadoresNacionales", | ||||
|                 table: "AgrupacionesPoliticas"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -99,9 +99,15 @@ namespace Elecciones.Database.Migrations | ||||
|                     b.Property<int?>("OrdenDiputados") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenDiputadosNacionales") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenSenadores") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int?>("OrdenSenadoresNacionales") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.ToTable("AgrupacionesPoliticas"); | ||||
| @@ -148,6 +154,35 @@ namespace Elecciones.Database.Migrations | ||||
|                     b.ToTable("AmbitosGeograficos"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.BancaPrevia", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); | ||||
|  | ||||
|                     b.Property<string>("AgrupacionPoliticaId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("nvarchar(450)") | ||||
|                         .UseCollation("Modern_Spanish_CI_AS"); | ||||
|  | ||||
|                     b.Property<int>("Camara") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("Cantidad") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.Property<int>("EleccionId") | ||||
|                         .HasColumnType("int"); | ||||
|  | ||||
|                     b.HasKey("Id"); | ||||
|  | ||||
|                     b.HasIndex("AgrupacionPoliticaId"); | ||||
|  | ||||
|                     b.ToTable("BancasPrevias"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b => | ||||
|                 { | ||||
|                     b.Property<int>("Id") | ||||
| @@ -546,6 +581,17 @@ namespace Elecciones.Database.Migrations | ||||
|                     b.ToTable("Telegramas"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.BancaPrevia", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AgrupacionPoliticaId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired(); | ||||
|  | ||||
|                     b.Navigation("AgrupacionPolitica"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b => | ||||
|                 { | ||||
|                     b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica") | ||||
|   | ||||
| @@ -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+5a8bee52d57b0f215705f3a7efb654169f85a7ae")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+67634ae947197595f6f644f3a80a982dd3573dfb")] | ||||
| [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+5a8bee52d57b0f215705f3a7efb654169f85a7ae")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+67634ae947197595f6f644f3a80a982dd3573dfb")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||