Trabajo de ajuste en widgets y db para frontend
This commit is contained in:
		| @@ -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> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user