Trabajo de ajuste en widgets y db para frontend
This commit is contained in:
		
							
								
								
									
										42
									
								
								Elecciones-Web/frontend-admin/src/App.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Elecciones-Web/frontend-admin/src/App.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| #root { | ||||
|   max-width: 1280px; | ||||
|   margin: 0 auto; | ||||
|   padding: 2rem; | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .logo { | ||||
|   height: 6em; | ||||
|   padding: 1.5em; | ||||
|   will-change: filter; | ||||
|   transition: filter 300ms; | ||||
| } | ||||
| .logo:hover { | ||||
|   filter: drop-shadow(0 0 2em #646cffaa); | ||||
| } | ||||
| .logo.react:hover { | ||||
|   filter: drop-shadow(0 0 2em #61dafbaa); | ||||
| } | ||||
|  | ||||
| @keyframes logo-spin { | ||||
|   from { | ||||
|     transform: rotate(0deg); | ||||
|   } | ||||
|   to { | ||||
|     transform: rotate(360deg); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @media (prefers-reduced-motion: no-preference) { | ||||
|   a:nth-of-type(2) .logo { | ||||
|     animation: logo-spin infinite 20s linear; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .card { | ||||
|   padding: 2em; | ||||
| } | ||||
|  | ||||
| .read-the-docs { | ||||
|   color: #888; | ||||
| } | ||||
							
								
								
									
										17
									
								
								Elecciones-Web/frontend-admin/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Elecciones-Web/frontend-admin/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // src/App.tsx | ||||
| import { useAuth } from './context/AuthContext'; | ||||
| import { LoginPage } from './components/LoginPage'; | ||||
| import { DashboardPage } from './components/DashboardPage'; | ||||
| import './App.css'; // Puede añadir estilos globales aquí | ||||
|  | ||||
| function App() { | ||||
|   const { isAuthenticated } = useAuth(); | ||||
|  | ||||
|   return ( | ||||
|     <div className="App"> | ||||
|       {isAuthenticated ? <DashboardPage /> : <LoginPage />} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default App; | ||||
| @@ -0,0 +1,77 @@ | ||||
| /* src/components/AgrupacionesManager.css */ | ||||
| .admin-module { | ||||
|     padding: 1rem; | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 8px; | ||||
|     margin-top: 2rem; | ||||
| } | ||||
|  | ||||
| table { | ||||
|     width: 100%; | ||||
|     border-collapse: collapse; | ||||
|     margin-top: 1rem; | ||||
| } | ||||
|  | ||||
| th, td { | ||||
|     border: 1px solid #ddd; | ||||
|     padding: 8px; | ||||
|     text-align: left; | ||||
| } | ||||
|  | ||||
| thead { | ||||
|     background-color: #f2f2f2; | ||||
| } | ||||
|  | ||||
| tr:nth-child(even) { | ||||
|     background-color: #f9f9f9; | ||||
| } | ||||
|  | ||||
| td input[type="text"] { | ||||
|     width: 100%; | ||||
|     padding: 4px; | ||||
|     box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| td button { | ||||
|     margin-right: 5px; | ||||
| } | ||||
|  | ||||
| .sortable-list-horizontal { | ||||
|   list-style: none; | ||||
|   padding: 8px; | ||||
|   margin: 0; | ||||
|   border: 1px dashed #ccc; | ||||
|   border-radius: 4px; | ||||
|   display: flex; /* <-- La clave para la alineación horizontal */ | ||||
|   flex-wrap: wrap; /* <-- La clave para que salte de línea */ | ||||
|   gap: 8px; /* Espacio entre elementos */ | ||||
|   min-height: 50px; /* Un poco de altura para que la zona de drop sea visible */ | ||||
| } | ||||
|  | ||||
| .sortable-item { | ||||
|   padding: 8px 12px; | ||||
|   border: 1px solid #ddd; | ||||
|   background-color: white; | ||||
|   border-radius: 4px; | ||||
|   cursor: grab; | ||||
|   /* Opcional: para que no se puedan seleccionar el texto mientras se arrastra */ | ||||
|   user-select: none;  | ||||
| } | ||||
|  | ||||
| .chamber-tabs { | ||||
|     display: flex; | ||||
|     margin-bottom: 1rem; | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 6px; | ||||
|     overflow: hidden; | ||||
| } | ||||
| .chamber-tabs button { | ||||
|     flex: 1; | ||||
|     padding: 0.75rem 0.5rem; | ||||
|     border: none; | ||||
|     background-color: #f8f9fa; | ||||
|     cursor: pointer; | ||||
|     transition: all 0.2s; | ||||
| } | ||||
| .chamber-tabs button:first-child { border-right: 1px solid #ccc; } | ||||
| .chamber-tabs button.active { background-color: #007bff; color: white; } | ||||
| @@ -0,0 +1,113 @@ | ||||
| // src/components/AgrupacionesManager.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { getAgrupaciones, updateAgrupacion } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica, UpdateAgrupacionData } from '../types'; | ||||
| import './AgrupacionesManager.css'; | ||||
|  | ||||
| export const AgrupacionesManager = () => { | ||||
|     const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); | ||||
|     const [loading, setLoading] = useState(true); | ||||
|     const [error, setError] = useState<string | null>(null); | ||||
|     const [editingId, setEditingId] = useState<string | null>(null); | ||||
|     const [formData, setFormData] = useState<UpdateAgrupacionData>({ | ||||
|         nombreCorto: '', | ||||
|         color: '#000000', | ||||
|         logoUrl: '', | ||||
|     }); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         fetchAgrupaciones(); | ||||
|     }, []); | ||||
|  | ||||
|     const fetchAgrupaciones = async () => { | ||||
|         try { | ||||
|             setLoading(true); | ||||
|             const data = await getAgrupaciones(); | ||||
|             setAgrupaciones(data); | ||||
|         } catch (err) { | ||||
|             setError('No se pudieron cargar las agrupaciones.'); | ||||
|         } finally { | ||||
|             setLoading(false); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const handleEdit = (agrupacion: AgrupacionPolitica) => { | ||||
|         setEditingId(agrupacion.id); | ||||
|         setFormData({ | ||||
|             nombreCorto: agrupacion.nombreCorto || '', | ||||
|             color: agrupacion.color || '#000000', | ||||
|             logoUrl: agrupacion.logoUrl || '', | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     const handleCancel = () => { | ||||
|         setEditingId(null); | ||||
|     }; | ||||
|  | ||||
|     const handleSave = async (id: string) => { | ||||
|         try { | ||||
|             await updateAgrupacion(id, formData); | ||||
|             setEditingId(null); | ||||
|             fetchAgrupaciones(); // Recargar datos para ver los cambios | ||||
|         } catch (err) { | ||||
|             alert('Error al guardar los cambios.'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|         setFormData({ ...formData, [e.target.name]: e.target.value }); | ||||
|     }; | ||||
|      | ||||
|     if (loading) return <p>Cargando agrupaciones...</p>; | ||||
|     if (error) return <p style={{ color: 'red' }}>{error}</p>; | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Gestión de Agrupaciones Políticas</h3> | ||||
|             <table> | ||||
|                 <thead> | ||||
|                     <tr> | ||||
|                         <th>Nombre</th> | ||||
|                         <th>Nombre Corto</th> | ||||
|                         <th>Color</th> | ||||
|                         <th>Logo URL</th> | ||||
|                         <th>Acciones</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|                 <tbody> | ||||
|                     {agrupaciones.map((agrupacion) => ( | ||||
|                         <tr key={agrupacion.id}> | ||||
|                             {editingId === agrupacion.id ? ( | ||||
|                                 <> | ||||
|                                     <td>{agrupacion.nombre}</td> | ||||
|                                     <td><input type="text" name="nombreCorto" value={formData.nombreCorto || ''} onChange={handleChange} /></td> | ||||
|                                     <td><input type="color" name="color" value={formData.color || '#000000'} onChange={handleChange} /></td> | ||||
|                                     <td><input type="text" name="logoUrl" value={formData.logoUrl || ''} onChange={handleChange} /></td> | ||||
|                                     <td> | ||||
|                                         <button onClick={() => handleSave(agrupacion.id)}>Guardar</button> | ||||
|                                         <button onClick={handleCancel}>Cancelar</button> | ||||
|                                     </td> | ||||
|                                 </> | ||||
|                             ) : ( | ||||
|                                 <> | ||||
|                                     <td>{agrupacion.nombre}</td> | ||||
|                                     <td>{agrupacion.nombreCorto}</td> | ||||
|                                     <td> | ||||
|                                         <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> | ||||
|                                             <div style={{ width: '20px', height: '20px', backgroundColor: agrupacion.color || 'transparent', border: '1px solid #ccc' }}></div> | ||||
|                                             {agrupacion.color} | ||||
|                                         </div> | ||||
|                                     </td> | ||||
|                                     <td>{agrupacion.logoUrl}</td> | ||||
|                                     <td> | ||||
|                                         <button onClick={() => handleEdit(agrupacion)}>Editar</button> | ||||
|                                     </td> | ||||
|                                 </> | ||||
|                             )} | ||||
|                         </tr> | ||||
|                     ))} | ||||
|                 </tbody> | ||||
|             </table> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										121
									
								
								Elecciones-Web/frontend-admin/src/components/BancasManager.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								Elecciones-Web/frontend-admin/src/components/BancasManager.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| // src/components/BancasManager.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'; // Asegúrate de que este CSS tenga los estilos de .chamber-tabs | ||||
|  | ||||
| const camaras = ['diputados', 'senadores'] as const; | ||||
|  | ||||
| export const BancasManager = () => { | ||||
|   const [activeTab, setActiveTab] = useState<'diputados' | 'senadores'>('diputados'); | ||||
|   const [modalVisible, setModalVisible] = useState(false); | ||||
|   const [bancadaSeleccionada, setBancadaSeleccionada] = useState<Bancada | null>(null); | ||||
|   const queryClient = useQueryClient(); | ||||
|  | ||||
|   // Obtenemos todas las agrupaciones para poblar el <select> | ||||
|   const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ | ||||
|     queryKey: ['agrupaciones'], | ||||
|     queryFn: getAgrupaciones | ||||
|   }); | ||||
|  | ||||
|   // Obtenemos las bancas para la cámara activa (diputados o senadores) | ||||
|   const { data: bancadas = [], isLoading, error } = useQuery<Bancada[]>({ | ||||
|     queryKey: ['bancadas', activeTab], | ||||
|     queryFn: () => getBancadas(activeTab), | ||||
|   }); | ||||
|  | ||||
|   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, | ||||
|       fotoUrl: nuevaAgrupacionId ? (bancadaActual.ocupante?.fotoUrl ?? null) : null, | ||||
|       periodo: nuevaAgrupacionId ? (bancadaActual.ocupante?.periodo ?? null) : null, | ||||
|     }; | ||||
|  | ||||
|     try { | ||||
|       await updateBancada(bancadaId, payload); | ||||
|       // Invalida la query para forzar una recarga de datos frescos desde el servidor | ||||
|       queryClient.invalidateQueries({ queryKey: ['bancadas', activeTab] }); | ||||
|     } catch (err) { | ||||
|       alert("Error al guardar el cambio."); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleOpenModal = (bancada: Bancada) => { | ||||
|     setBancadaSeleccionada(bancada); | ||||
|     setModalVisible(true); | ||||
|   }; | ||||
|  | ||||
|   if (error) return <p style={{ color: 'red' }}>Error al cargar las bancas.</p>; | ||||
|  | ||||
|   return ( | ||||
|     <div className="admin-module"> | ||||
|       <h2>Gestión de Ocupación de Bancas</h2> | ||||
|       <p>Asigne a cada banca un partido político y, opcionalmente, los datos de la persona que la ocupa.</p> | ||||
|        | ||||
|       <div className="chamber-tabs"> | ||||
|         {camaras.map(camara => ( | ||||
|           <button | ||||
|             key={camara} | ||||
|             className={activeTab === camara ? 'active' : ''} | ||||
|             onClick={() => setActiveTab(camara)} | ||||
|           > | ||||
|             {camara === 'diputados' ? 'Cámara de Diputados' : 'Cámara de Senadores'} | ||||
|           </button> | ||||
|         ))} | ||||
|       </div> | ||||
|  | ||||
|       {isLoading ? <p>Cargando bancas...</p> : ( | ||||
|         <table> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th>Banca #</th> | ||||
|               <th>Partido Asignado</th> | ||||
|               <th>Ocupante Actual</th> | ||||
|               <th>Acciones</th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody> | ||||
|             {bancadas.map((bancada, index) => ( | ||||
|               <tr key={bancada.id}> | ||||
|                 <td>{`${activeTab.charAt(0).toUpperCase()}-${index + 1}`}</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> | ||||
|       )} | ||||
|  | ||||
|       {modalVisible && bancadaSeleccionada && ( | ||||
|         <OcupantesModal | ||||
|           bancada={bancadaSeleccionada} | ||||
|           onClose={() => setModalVisible(false)} | ||||
|           activeTab={activeTab} | ||||
|         /> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,110 @@ | ||||
| // src/components/ConfiguracionGeneral.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { getAgrupaciones, getConfiguracion, updateConfiguracion } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica } from '../types'; | ||||
| import './AgrupacionesManager.css'; // Reutilizamos los estilos para mantener la consistencia | ||||
|  | ||||
| export const ConfiguracionGeneral = () => { | ||||
|     const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); | ||||
|     const [loading, setLoading] = useState(true); | ||||
|     const [error, setError] = useState<string | null>(null); | ||||
|     // Estado específico para la configuración de la presidencia del Senado | ||||
|     const [presidenciaSenadoId, setPresidenciaSenadoId] = useState<string>(''); | ||||
|     const [usarDatosOficiales, setUsarDatosOficiales] = useState(false); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const loadInitialData = async () => { | ||||
|             try { | ||||
|                 setLoading(true); | ||||
|                 setError(null); | ||||
|  | ||||
|                 // Hacemos ambas llamadas a la API en paralelo para más eficiencia | ||||
|                 const [agrupacionesData, configData] = await Promise.all([ | ||||
|                     getAgrupaciones(), | ||||
|                     getConfiguracion() | ||||
|                 ]); | ||||
|  | ||||
|                 setAgrupaciones(agrupacionesData); | ||||
|  | ||||
|                 // Asignamos el valor guardado, si existe | ||||
|                 if (configData && configData.PresidenciaSenadores) { | ||||
|                     setPresidenciaSenadoId(configData.PresidenciaSenadores); | ||||
|                 } | ||||
|                 setUsarDatosOficiales(configData.UsarDatosDeBancadasOficiales === 'true'); | ||||
|             } catch (err) { | ||||
|                 console.error("Error al cargar datos de configuración:", err); | ||||
|                 setError("No se pudieron cargar los datos necesarios para la configuración."); | ||||
|             } finally { | ||||
|                 setLoading(false); | ||||
|             } | ||||
|         }; | ||||
|         loadInitialData(); | ||||
|     }, []); | ||||
|  | ||||
|     const handleSave = async () => { | ||||
|         try { | ||||
|             await updateConfiguracion({ "PresidenciaSenadores": presidenciaSenadoId, "UsarDatosDeBancadasOficiales": usarDatosOficiales.toString() }); | ||||
|             alert('Configuración guardada con éxito.'); | ||||
|         } catch (err) { | ||||
|             console.error("Error al guardar la configuración:", err); | ||||
|             alert('Error al guardar la configuración.'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if (loading) return <div className="admin-module"><p>Cargando configuración...</p></div>; | ||||
|     if (error) return <div className="admin-module"><p style={{ color: 'red' }}>{error}</p></div>; | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Configuración General de Cámaras</h3> | ||||
|             <div className="form-group"> | ||||
|                 <label> | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         checked={usarDatosOficiales} | ||||
|                         onChange={e => setUsarDatosOficiales(e.target.checked)} | ||||
|                     /> | ||||
|                     Activar Modo "Resultados Oficiales" | ||||
|                 </label> | ||||
|                 <p style={{ fontSize: '0.8rem', color: '#666', margin: '0.5rem 0 0 0' }}> | ||||
|                     Si está activo, el widget del Congreso mostrará la composición gestionada manualmente en esta página. Si está inactivo, mostrará la proyección en tiempo real de las elecciones. | ||||
|                 </p> | ||||
|             </div> | ||||
|             <div style={{ marginTop: '1rem', paddingBottom: '1rem', borderBottom: '1px solid #eee' }}> | ||||
|                 <label | ||||
|                     htmlFor="presidencia-senado" | ||||
|                     style={{ display: 'block', fontWeight: 'bold', marginBottom: '0.5rem' }} | ||||
|                 > | ||||
|                     Presidencia Cámara de Senadores (Vicegobernador) | ||||
|                 </label> | ||||
|                 <select | ||||
|                     id="presidencia-senado" | ||||
|                     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> | ||||
|                 <p style={{ fontSize: '0.8rem', color: '#666', margin: '0.5rem 0 0 0' }}> | ||||
|                     Seleccione el partido político al que pertenece el Vicegobernador. El asiento presidencial del Senado se pintará con el color de este partido. | ||||
|                 </p> | ||||
|             </div> | ||||
|             <div style={{ marginTop: '1rem' }}> | ||||
|                 <p style={{ fontWeight: 'bold', margin: 0 }}> | ||||
|                     Presidencia Cámara de Diputados | ||||
|                 </p> | ||||
|                 <p style={{ fontSize: '0.8rem', color: '#666', margin: '0.5rem 0 0 0' }}> | ||||
|                     Esta banca se asigna y colorea automáticamente según la agrupación política con la mayoría de bancas totales en la cámara. | ||||
|                 </p> | ||||
|             </div> | ||||
|             <button onClick={handleSave} style={{ marginTop: '1.5rem' }}> | ||||
|                 Guardar Configuración | ||||
|             </button> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -0,0 +1,34 @@ | ||||
| // 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'; | ||||
|  | ||||
| export const DashboardPage = () => { | ||||
|     const { logout } = useAuth(); | ||||
|  | ||||
|     return ( | ||||
|         <div style={{ padding: '1rem 2rem' }}> | ||||
|             <header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '2px solid #eee', paddingBottom: '1rem' }}> | ||||
|                 <h1>Panel de Administración Electoral</h1> | ||||
|                 <button onClick={logout}>Cerrar Sesión</button> | ||||
|             </header> | ||||
|             <main style={{ marginTop: '2rem' }}> | ||||
|                 <ConfiguracionGeneral />                 | ||||
|                 <AgrupacionesManager /> | ||||
|                  | ||||
|                 <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> | ||||
|                 <BancasManager /> | ||||
|             </main> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										49
									
								
								Elecciones-Web/frontend-admin/src/components/LoginPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								Elecciones-Web/frontend-admin/src/components/LoginPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| // src/components/LoginPage.tsx | ||||
| import { useState } from 'react'; | ||||
| import { useAuth } from '../context/AuthContext'; | ||||
|  | ||||
| export const LoginPage = () => { | ||||
|   const [username, setUsername] = useState(''); | ||||
|   const [password, setPassword] = useState(''); | ||||
|   const [error, setError] = useState(''); | ||||
|   const { login } = useAuth(); | ||||
|  | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     setError(''); | ||||
|     const success = await login({ username, password }); | ||||
|     if (!success) { | ||||
|       setError('Usuario o contraseña incorrectos.'); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div style={{ /* Estilos simples para centrar */ }}> | ||||
|       <h2>Panel de Administración</h2> | ||||
|       <form onSubmit={handleSubmit}> | ||||
|         <div> | ||||
|           <label htmlFor="username">Usuario:</label> | ||||
|           <input | ||||
|             id="username" | ||||
|             type="text" | ||||
|             value={username} | ||||
|             onChange={(e) => setUsername(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|         </div> | ||||
|         <div> | ||||
|           <label htmlFor="password">Contraseña:</label> | ||||
|           <input | ||||
|             id="password" | ||||
|             type="password" | ||||
|             value={password} | ||||
|             onChange={(e) => setPassword(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|         </div> | ||||
|         {error && <p style={{ color: 'red' }}>{error}</p>} | ||||
|         <button type="submit">Ingresar</button> | ||||
|       </form> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,47 @@ | ||||
| .modal-overlay { | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     background-color: rgba(0, 0, 0, 0.6); | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     z-index: 1000; | ||||
| } | ||||
| .modal-content { | ||||
|     background: white; | ||||
|     padding: 2rem; | ||||
|     border-radius: 8px; | ||||
|     width: 90%; | ||||
|     max-width: 500px; | ||||
|     position: relative; | ||||
| } | ||||
| .modal-close { | ||||
|     position: absolute; | ||||
|     top: 10px; | ||||
|     right: 15px; | ||||
|     border: none; | ||||
|     background: none; | ||||
|     font-size: 1.5rem; | ||||
|     cursor: pointer; | ||||
| } | ||||
| .modal-content h4 { margin-top: 0; } | ||||
| .modal-content h5 { margin-top: 0; color: #666; } | ||||
| .form-group { | ||||
|     margin-bottom: 1rem; | ||||
| } | ||||
| .form-group label { | ||||
|     display: block; | ||||
|     margin-bottom: 0.5rem; | ||||
| } | ||||
| .form-group input { | ||||
|     width: 100%; | ||||
|     padding: 8px; | ||||
|     box-sizing: border-box; | ||||
| } | ||||
| .modal-actions { | ||||
|     text-align: right; | ||||
|     margin-top: 1.5rem; | ||||
| } | ||||
| @@ -0,0 +1,64 @@ | ||||
| // src/components/OcupantesModal.tsx | ||||
| import { useState } from 'react'; | ||||
| import { useQueryClient } from '@tanstack/react-query'; | ||||
| import { updateBancada, type UpdateBancadaData } from '../services/apiService'; | ||||
| import type { Bancada } from '../types'; | ||||
| import './OcupantesModal.css'; // Crearemos este archivo | ||||
|  | ||||
| interface Props { | ||||
|     bancada: Bancada; | ||||
|     onClose: () => void; | ||||
|     activeTab: 'diputados' | 'senadores'; | ||||
| } | ||||
|  | ||||
| export const OcupantesModal = ({ bancada, onClose, activeTab }: Props) => { | ||||
|     const queryClient = useQueryClient(); | ||||
|     const [nombre, setNombre] = useState(bancada.ocupante?.nombreOcupante || ''); | ||||
|     const [fotoUrl, setFotoUrl] = useState(bancada.ocupante?.fotoUrl || ''); | ||||
|     const [periodo, setPeriodo] = useState(bancada.ocupante?.periodo || ''); | ||||
|      | ||||
|     const handleSubmit = async (e: React.FormEvent) => { | ||||
|         e.preventDefault(); | ||||
|         const payload: UpdateBancadaData = { | ||||
|             agrupacionPoliticaId: bancada.agrupacionPoliticaId, | ||||
|             nombreOcupante: nombre || null, | ||||
|             fotoUrl: fotoUrl || null, | ||||
|             periodo: periodo || null, | ||||
|         }; | ||||
|         try { | ||||
|             await updateBancada(bancada.id, payload); | ||||
|             // Invalida la query para que la tabla principal se actualice | ||||
|             queryClient.invalidateQueries({ queryKey: ['bancadas', activeTab] }); | ||||
|             onClose(); | ||||
|         } catch (err) { | ||||
|             alert("Error al guardar el ocupante."); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div className="modal-overlay" onClick={onClose}> | ||||
|             <div className="modal-content" onClick={e => e.stopPropagation()}> | ||||
|                 <button className="modal-close" onClick={onClose}>×</button> | ||||
|                 <h4>Ocupante de la Banca #{bancada.id}</h4> | ||||
|                 <h5>{bancada.agrupacionPolitica?.nombre || 'Banca Vacante'}</h5> | ||||
|                 <form onSubmit={handleSubmit}> | ||||
|                     <div className="form-group"> | ||||
|                         <label htmlFor="nombre">Nombre Completo</label> | ||||
|                         <input id="nombre" type="text" value={nombre} onChange={e => setNombre(e.target.value)} /> | ||||
|                     </div> | ||||
|                     <div className="form-group"> | ||||
|                         <label htmlFor="fotoUrl">URL de la Foto</label> | ||||
|                         <input id="fotoUrl" type="text" value={fotoUrl} onChange={e => setFotoUrl(e.target.value)} /> | ||||
|                     </div> | ||||
|                     <div className="form-group"> | ||||
|                         <label htmlFor="periodo">Período (ej. 2023-2027)</label> | ||||
|                         <input id="periodo" type="text" value={periodo} onChange={e => setPeriodo(e.target.value)} /> | ||||
|                     </div> | ||||
|                     <div className="modal-actions"> | ||||
|                         <button type="submit">Guardar Cambios</button> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -0,0 +1,114 @@ | ||||
| // src/components/OrdenDiputadosManager.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { | ||||
|   DndContext, | ||||
|   closestCenter, | ||||
|   KeyboardSensor, | ||||
|   PointerSensor, | ||||
|   useSensor, | ||||
|   useSensors, | ||||
|   type DragEndEvent, | ||||
| } from '@dnd-kit/core'; | ||||
| import { | ||||
|   arrayMove, | ||||
|   SortableContext, | ||||
|   sortableKeyboardCoordinates, | ||||
|   horizontalListSortingStrategy, | ||||
| } from '@dnd-kit/sortable'; | ||||
|  | ||||
| import { getAgrupaciones } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica } from '../types'; | ||||
| import { SortableItem } from './SortableItem'; | ||||
| import './AgrupacionesManager.css'; // Reutilizamos los estilos | ||||
|  | ||||
| // Función para llamar al endpoint específico de diputados | ||||
| const updateOrdenDiputadosApi = async (ids: string[]) => { | ||||
|   const token = localStorage.getItem('admin-jwt-token'); | ||||
|   const response = await fetch('http://localhost:5217/api/admin/agrupaciones/orden-diputados', { | ||||
|       method: 'PUT', | ||||
|       headers: { | ||||
|           'Content-Type': 'application/json', | ||||
|           'Authorization': `Bearer ${token}` | ||||
|       }, | ||||
|       body: JSON.stringify(ids) | ||||
|   }); | ||||
|   if (!response.ok) { | ||||
|     throw new Error("Failed to save Diputados order"); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const OrdenDiputadosManager = () => { | ||||
|     const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); | ||||
|     const [loading, setLoading] = useState(true); | ||||
|     const sensors = useSensors( | ||||
|       useSensor(PointerSensor), | ||||
|       useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) | ||||
|     ); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const fetchAndSortAgrupaciones = async () => { | ||||
|             setLoading(true); | ||||
|             try { | ||||
|                 const data = await getAgrupaciones(); | ||||
|                 // Ordenar por el orden de Diputados. Los nulos van al final. | ||||
|                 data.sort((a, b) => (a.ordenDiputados || 999) - (b.ordenDiputados || 999)); | ||||
|                 setAgrupaciones(data); | ||||
|             } catch (error) { | ||||
|                 console.error("Failed to fetch agrupaciones for Diputados:", error); | ||||
|             } finally { | ||||
|                 setLoading(false); | ||||
|             } | ||||
|         }; | ||||
|         fetchAndSortAgrupaciones(); | ||||
|     }, []); | ||||
|  | ||||
|     const handleDragEnd = (event: DragEndEvent) => { | ||||
|       const { active, over } = event; | ||||
|       if (over && active.id !== over.id) { | ||||
|         setAgrupaciones((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 = agrupaciones.map(a => a.id); | ||||
|         try { | ||||
|             await updateOrdenDiputadosApi(idsOrdenados); | ||||
|             alert('Orden de Diputados guardado con éxito!'); | ||||
|         } catch (error) { | ||||
|             alert('Error al guardar el orden de Diputados.'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if (loading) return <p>Cargando orden de Diputados...</p>; | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Ordenar Agrupaciones (Diputados)</h3> | ||||
|             <p>Arrastre para reordenar.</p> | ||||
|             <p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p> | ||||
|             <DndContext | ||||
|               sensors={sensors} | ||||
|               collisionDetection={closestCenter} | ||||
|               onDragEnd={handleDragEnd} | ||||
|             > | ||||
|               <SortableContext | ||||
|                 items={agrupaciones.map(a => a.id)} | ||||
|                 strategy={horizontalListSortingStrategy} | ||||
|               > | ||||
|                 <ul className="sortable-list-horizontal"> | ||||
|                   {agrupaciones.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 Diputados</button> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -0,0 +1,114 @@ | ||||
| // src/components/OrdenSenadoresManager.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { | ||||
|   DndContext, | ||||
|   closestCenter, | ||||
|   KeyboardSensor, | ||||
|   PointerSensor, | ||||
|   useSensor, | ||||
|   useSensors, | ||||
|   type DragEndEvent, | ||||
| } from '@dnd-kit/core'; | ||||
| import { | ||||
|   arrayMove, | ||||
|   SortableContext, | ||||
|   sortableKeyboardCoordinates, | ||||
|   horizontalListSortingStrategy, | ||||
| } from '@dnd-kit/sortable'; | ||||
|  | ||||
| import { getAgrupaciones } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica } from '../types'; | ||||
| import { SortableItem } from './SortableItem'; | ||||
| import './AgrupacionesManager.css'; // Reutilizamos los estilos | ||||
|  | ||||
| // Función para llamar al endpoint específico de senadores | ||||
| const updateOrdenSenadoresApi = async (ids: string[]) => { | ||||
|   const token = localStorage.getItem('admin-jwt-token'); | ||||
|   const response = await fetch('http://localhost:5217/api/admin/agrupaciones/orden-senadores', { | ||||
|       method: 'PUT', | ||||
|       headers: { | ||||
|           'Content-Type': 'application/json', | ||||
|           'Authorization': `Bearer ${token}` | ||||
|       }, | ||||
|       body: JSON.stringify(ids) | ||||
|   }); | ||||
|   if (!response.ok) { | ||||
|     throw new Error("Failed to save Senadores order"); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const OrdenSenadoresManager = () => { | ||||
|     const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); | ||||
|     const [loading, setLoading] = useState(true); | ||||
|     const sensors = useSensors( | ||||
|       useSensor(PointerSensor), | ||||
|       useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) | ||||
|     ); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const fetchAndSortAgrupaciones = async () => { | ||||
|             setLoading(true); | ||||
|             try { | ||||
|                 const data = await getAgrupaciones(); | ||||
|                 // Ordenar por el orden de Senadores. Los nulos van al final. | ||||
|                 data.sort((a, b) => (a.ordenSenadores || 999) - (b.ordenSenadores || 999)); | ||||
|                 setAgrupaciones(data); | ||||
|             } catch (error) { | ||||
|                 console.error("Failed to fetch agrupaciones for Senadores:", error); | ||||
|             } finally { | ||||
|                 setLoading(false); | ||||
|             } | ||||
|         }; | ||||
|         fetchAndSortAgrupaciones(); | ||||
|     }, []); | ||||
|  | ||||
|     const handleDragEnd = (event: DragEndEvent) => { | ||||
|       const { active, over } = event; | ||||
|       if (over && active.id !== over.id) { | ||||
|         setAgrupaciones((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 = agrupaciones.map(a => a.id); | ||||
|         try { | ||||
|             await updateOrdenSenadoresApi(idsOrdenados); | ||||
|             alert('Orden de Senadores guardado con éxito!'); | ||||
|         } catch (error) { | ||||
|             alert('Error al guardar el orden de Senadores.'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if (loading) return <p>Cargando orden de Senadores...</p>; | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Ordenar Agrupaciones (Senado)</h3> | ||||
|             <p>Arrastre para reordenar.</p> | ||||
|             <p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p> | ||||
|             <DndContext | ||||
|               sensors={sensors} | ||||
|               collisionDetection={closestCenter} | ||||
|               onDragEnd={handleDragEnd} | ||||
|             > | ||||
|               <SortableContext | ||||
|                 items={agrupaciones.map(a => a.id)} | ||||
|                 strategy={horizontalListSortingStrategy} | ||||
|               > | ||||
|                 <ul className="sortable-list-horizontal"> | ||||
|                   {agrupaciones.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 Senado</button> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -0,0 +1,26 @@ | ||||
| // src/components/SortableItem.tsx | ||||
| import { useSortable } from '@dnd-kit/sortable'; | ||||
| import { CSS } from '@dnd-kit/utilities'; | ||||
|  | ||||
| export function SortableItem(props: { id: string, children: React.ReactNode }) { | ||||
|   const { | ||||
|     attributes, | ||||
|     listeners, | ||||
|     setNodeRef, | ||||
|     transform, | ||||
|     transition, | ||||
|   } = useSortable({ id: props.id }); | ||||
|    | ||||
|   // La única propiedad de estilo que necesitamos en línea es la que calcula dnd-kit | ||||
|   const style = { | ||||
|     transform: CSS.Transform.toString(transform), | ||||
|     transition, | ||||
|   }; | ||||
|    | ||||
|   // Añadimos la clase CSS que creamos | ||||
|   return ( | ||||
|     <li ref={setNodeRef} style={style} {...attributes} {...listeners} className="sortable-item"> | ||||
|       {props.children} | ||||
|     </li> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										71
									
								
								Elecciones-Web/frontend-admin/src/context/AuthContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								Elecciones-Web/frontend-admin/src/context/AuthContext.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| // src/context/AuthContext.tsx | ||||
| import { createContext, useState, useContext, type ReactNode, useEffect } from 'react'; | ||||
| import { loginUser } from '../services/apiService'; // Importaremos esta función | ||||
| import type { LoginCredentials } from '../services/apiService'; // y este tipo | ||||
| import { subscribeToLogout } from './authUtils'; | ||||
|  | ||||
| interface AuthContextType { | ||||
|   isAuthenticated: boolean; | ||||
|   token: string | null; | ||||
|   login: (credentials: LoginCredentials) => Promise<boolean>; | ||||
|   logout: () => void; | ||||
| } | ||||
|  | ||||
| const AuthContext = createContext<AuthContextType | undefined>(undefined); | ||||
|  | ||||
| export const AuthProvider = ({ children }: { children: ReactNode }) => { | ||||
|   const [token, setToken] = useState<string | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     // Al cargar la app, revisamos si ya hay un token guardado | ||||
|     const storedToken = localStorage.getItem('admin-jwt-token'); | ||||
|     if (storedToken) { | ||||
|       setToken(storedToken); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   const logout = () => { | ||||
|     localStorage.removeItem('admin-jwt-token'); | ||||
|     setToken(null); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     // Nos suscribimos al evento de logout global. | ||||
|     const handleLogout = () => logout(); | ||||
|     subscribeToLogout(handleLogout); | ||||
|   }, []); // Se ejecuta solo una vez | ||||
|  | ||||
|   const login = async (credentials: LoginCredentials): Promise<boolean> => { | ||||
|     try { | ||||
|       const receivedToken = await loginUser(credentials); | ||||
|       if (receivedToken) { | ||||
|         localStorage.setItem('admin-jwt-token', receivedToken); | ||||
|         setToken(receivedToken); | ||||
|         return true; | ||||
|       } | ||||
|       return false; | ||||
|     } catch (error) { | ||||
|       console.error("Login failed:", error); | ||||
|       // Asegurarse de que el usuario esté deslogueado si falla | ||||
|       logout(); | ||||
|       return false; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const value = { | ||||
|     isAuthenticated: !!token, | ||||
|     token, | ||||
|     login, | ||||
|     logout, | ||||
|   }; | ||||
|  | ||||
|   return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; | ||||
| }; | ||||
|  | ||||
| export const useAuth = () => { | ||||
|   const context = useContext(AuthContext); | ||||
|   if (context === undefined) { | ||||
|     throw new Error('useAuth must be used within an AuthProvider'); | ||||
|   } | ||||
|   return context; | ||||
| }; | ||||
							
								
								
									
										14
									
								
								Elecciones-Web/frontend-admin/src/context/authUtils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Elecciones-Web/frontend-admin/src/context/authUtils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| // src/context/authUtils.ts | ||||
|  | ||||
| // Creamos un "emisor de eventos" muy simple. | ||||
| const events = new EventTarget(); | ||||
|  | ||||
| // La API escuchará este evento personalizado. | ||||
| export function subscribeToLogout(callback: () => void) { | ||||
|   events.addEventListener('logout', callback); | ||||
| } | ||||
|  | ||||
| // El interceptor llamará a esta función para disparar el evento. | ||||
| export function triggerLogout() { | ||||
|   events.dispatchEvent(new Event('logout')); | ||||
| } | ||||
							
								
								
									
										0
									
								
								Elecciones-Web/frontend-admin/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								Elecciones-Web/frontend-admin/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										22
									
								
								Elecciones-Web/frontend-admin/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Elecciones-Web/frontend-admin/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| // src/main.tsx | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom/client'; | ||||
| import App from './App.tsx'; | ||||
| import './index.css'; | ||||
| import { AuthProvider } from './context/AuthContext.tsx'; | ||||
|  | ||||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||||
|  | ||||
| // 1. Crear una instancia del cliente de query | ||||
| const queryClient = new QueryClient(); | ||||
|  | ||||
| ReactDOM.createRoot(document.getElementById('root')!).render( | ||||
|   <React.StrictMode> | ||||
|     {/* 2. Envolver la aplicación con el proveedor */} | ||||
|     <QueryClientProvider client={queryClient}> | ||||
|       <AuthProvider> | ||||
|         <App /> | ||||
|       </AuthProvider> | ||||
|     </QueryClientProvider> | ||||
|   </React.StrictMode> | ||||
| ); | ||||
							
								
								
									
										97
									
								
								Elecciones-Web/frontend-admin/src/services/apiService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								Elecciones-Web/frontend-admin/src/services/apiService.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| // src/services/apiService.ts | ||||
| import axios from 'axios'; | ||||
| import { triggerLogout } from '../context/authUtils'; | ||||
| import type { AgrupacionPolitica, UpdateAgrupacionData, Bancada } from '../types'; | ||||
|  | ||||
| const AUTH_API_URL = 'http://localhost:5217/api/auth'; | ||||
| const ADMIN_API_URL = 'http://localhost:5217/api/admin'; | ||||
|  | ||||
| const adminApiClient = axios.create({ | ||||
|   baseURL: ADMIN_API_URL, | ||||
| }); | ||||
|  | ||||
| // --- INTERCEPTORES (una sola vez) --- | ||||
|  | ||||
| // Interceptor de Peticiones: Añade el token JWT a cada llamada | ||||
| adminApiClient.interceptors.request.use( | ||||
|   (config) => { | ||||
|     const token = localStorage.getItem('admin-jwt-token'); | ||||
|     if (token) { | ||||
|       config.headers['Authorization'] = `Bearer ${token}`; | ||||
|     } | ||||
|     return config; | ||||
|   }, | ||||
|   (error) => Promise.reject(error) | ||||
| ); | ||||
|  | ||||
| // Interceptor de Respuestas: Maneja la expiración del token (error 401) | ||||
| adminApiClient.interceptors.response.use( | ||||
|   (response) => response, | ||||
|   (error) => { | ||||
|     if (axios.isAxiosError(error) && error.response?.status === 401) { | ||||
|       console.log("Token expirado o inválido. Deslogueando..."); | ||||
|       triggerLogout(); | ||||
|     } | ||||
|     return Promise.reject(error); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // --- SERVICIOS DE API --- | ||||
|  | ||||
| // 1. Autenticación | ||||
| export interface LoginCredentials { username: string; password: string; } | ||||
|  | ||||
| export const loginUser = async (credentials: LoginCredentials): Promise<string | null> => { | ||||
|   try { | ||||
|     const response = await axios.post(`${AUTH_API_URL}/login`, credentials); | ||||
|     return response.data.token; | ||||
|   } catch (error) { | ||||
|     console.error("Error during login request:", error); | ||||
|     throw error; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // 2. Agrupaciones Políticas | ||||
| export const getAgrupaciones = async (): Promise<AgrupacionPolitica[]> => { | ||||
|   const response = await adminApiClient.get('/agrupaciones'); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| export const updateAgrupacion = async (id: string, data: UpdateAgrupacionData): Promise<void> => { | ||||
|   await adminApiClient.put(`/agrupaciones/${id}`, data); | ||||
| }; | ||||
|  | ||||
| // 3. Ordenamiento de Agrupaciones | ||||
| export const updateOrden = async (camara: 'diputados' | 'senadores', 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}`); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| export interface UpdateBancadaData { | ||||
|     agrupacionPoliticaId: string | null; | ||||
|     nombreOcupante: string | null; | ||||
|     fotoUrl: string | null; | ||||
|     periodo: string | null; | ||||
| } | ||||
|  | ||||
| export const updateBancada = async (bancadaId: number, data: UpdateBancadaData): Promise<void> => { | ||||
|     await adminApiClient.put(`/bancadas/${bancadaId}`, data); | ||||
| }; | ||||
|  | ||||
| // 5. Configuración General | ||||
| export type ConfiguracionResponse = Record<string, string>; | ||||
|  | ||||
| export const getConfiguracion = async (): Promise<ConfiguracionResponse> => { | ||||
|     const response = await adminApiClient.get('/configuracion'); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| export const updateConfiguracion = async (data: Record<string, string>): Promise<void> => { | ||||
|     await adminApiClient.put('/configuracion', data); | ||||
| }; | ||||
							
								
								
									
										42
									
								
								Elecciones-Web/frontend-admin/src/types/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Elecciones-Web/frontend-admin/src/types/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| // src/types/index.ts | ||||
|  | ||||
| export interface AgrupacionPolitica { | ||||
|   id: string; | ||||
|   idTelegrama: string; | ||||
|   nombre: string; | ||||
|   nombreCorto: string | null; | ||||
|   color: string | null; | ||||
|   logoUrl: string | null; | ||||
|   ordenDiputados: number | null; | ||||
|   ordenSenadores: number | null; | ||||
| } | ||||
|  | ||||
| export interface UpdateAgrupacionData { | ||||
|     nombreCorto: string | null; | ||||
|     color: string | null; | ||||
|     logoUrl: string | null; | ||||
| } | ||||
|  | ||||
| export const TipoCamara = { | ||||
|   Diputados: 0, | ||||
|   Senadores: 1, | ||||
| } as const; | ||||
|  | ||||
| export type TipoCamaraValue = typeof TipoCamara[keyof typeof TipoCamara]; | ||||
|  | ||||
| export interface OcupanteBanca { | ||||
|   id: number; | ||||
|   bancadaId: number; | ||||
|   nombreOcupante: string; | ||||
|   fotoUrl: string | null; | ||||
|   periodo: string | null; | ||||
| } | ||||
|  | ||||
| // Nueva interfaz para la Bancada | ||||
| export interface Bancada { | ||||
|   id: number; | ||||
|   camara: TipoCamaraValue; | ||||
|   agrupacionPoliticaId: string | null; | ||||
|   agrupacionPolitica: AgrupacionPolitica | null; | ||||
|   ocupante: OcupanteBanca | null; | ||||
| } | ||||
							
								
								
									
										1
									
								
								Elecciones-Web/frontend-admin/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Elecciones-Web/frontend-admin/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /// <reference types="vite/client" /> | ||||
		Reference in New Issue
	
	Block a user