Feat Widgets
This commit is contained in:
		| @@ -1,113 +1,133 @@ | ||||
| // src/components/AgrupacionesManager.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { getAgrupaciones, updateAgrupacion } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica, UpdateAgrupacionData } from '../types'; | ||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { getAgrupaciones, updateAgrupacion, getLogos, updateLogos } from '../services/apiService'; | ||||
| import type { AgrupacionPolitica, LogoAgrupacionCategoria } from '../types'; | ||||
| import './AgrupacionesManager.css'; | ||||
|  | ||||
| const SENADORES_ID = 5; | ||||
| const DIPUTADOS_ID = 6; | ||||
| const CONCEJALES_ID = 7; | ||||
|  | ||||
| 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: '', | ||||
|     const queryClient = useQueryClient(); | ||||
|      | ||||
|     const [editedAgrupaciones, setEditedAgrupaciones] = useState<Record<string, Partial<AgrupacionPolitica>>>({}); | ||||
|     const [editedLogos, setEditedLogos] = useState<LogoAgrupacionCategoria[]>([]); | ||||
|  | ||||
|     // Query 1: Obtener agrupaciones | ||||
|     const { data: agrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({ | ||||
|         queryKey: ['agrupaciones'], | ||||
|         queryFn: getAgrupaciones, | ||||
|     }); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         fetchAgrupaciones(); | ||||
|     }, []); | ||||
|     // Query 2: Obtener logos | ||||
|     const { data: logos = [], isLoading: isLoadingLogos } = useQuery<LogoAgrupacionCategoria[]>({ | ||||
|         queryKey: ['logos'], | ||||
|         queryFn: getLogos, | ||||
|     }); | ||||
|  | ||||
|     const fetchAgrupaciones = async () => { | ||||
|         try { | ||||
|             setLoading(true); | ||||
|             const data = await getAgrupaciones(); | ||||
|             setAgrupaciones(data); | ||||
|         } catch (err) { | ||||
|             setError('No se pudieron cargar las agrupaciones.'); | ||||
|         } finally { | ||||
|             setLoading(false); | ||||
|     // Usamos useEffect para reaccionar cuando los datos de 'logos' se cargan o cambian. | ||||
|     useEffect(() => { | ||||
|         if (logos) { | ||||
|             setEditedLogos(logos); | ||||
|         } | ||||
|     }, [logos]); | ||||
|  | ||||
|     // Usamos otro useEffect para reaccionar a los datos de 'agrupaciones'. | ||||
|     useEffect(() => { | ||||
|         if (agrupaciones) { | ||||
|             const initialEdits = Object.fromEntries(agrupaciones.map(a => [a.id, {}])); | ||||
|             setEditedAgrupaciones(initialEdits); | ||||
|         } | ||||
|     }, [agrupaciones]); | ||||
|  | ||||
|     const handleInputChange = (id: string, field: 'nombreCorto' | 'color', value: string) => { | ||||
|         setEditedAgrupaciones(prev => ({ | ||||
|             ...prev, | ||||
|             [id]: { ...prev[id], [field]: value } | ||||
|         })); | ||||
|     }; | ||||
|  | ||||
|     const handleEdit = (agrupacion: AgrupacionPolitica) => { | ||||
|         setEditingId(agrupacion.id); | ||||
|         setFormData({ | ||||
|             nombreCorto: agrupacion.nombreCorto || '', | ||||
|             color: agrupacion.color || '#000000', | ||||
|             logoUrl: agrupacion.logoUrl || '', | ||||
|     const handleLogoChange = (agrupacionId: string, categoriaId: number, value: string) => { | ||||
|         setEditedLogos(prev => { | ||||
|             const newLogos = [...prev]; | ||||
|             const existing = newLogos.find(l => l.agrupacionPoliticaId === agrupacionId && l.categoriaId === categoriaId); | ||||
|             if (existing) { | ||||
|                 existing.logoUrl = value; | ||||
|             } else { | ||||
|                 newLogos.push({ id: 0, agrupacionPoliticaId: agrupacionId, categoriaId, logoUrl: value }); | ||||
|             } | ||||
|             return newLogos; | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     const handleCancel = () => { | ||||
|         setEditingId(null); | ||||
|     }; | ||||
|  | ||||
|     const handleSave = async (id: string) => { | ||||
|     const handleSaveAll = async () => { | ||||
|         try { | ||||
|             await updateAgrupacion(id, formData); | ||||
|             setEditingId(null); | ||||
|             fetchAgrupaciones(); // Recargar datos para ver los cambios | ||||
|             const agrupacionPromises = Object.entries(editedAgrupaciones).map(([id, changes]) => { | ||||
|                 if (Object.keys(changes).length > 0) { | ||||
|                     const original = agrupaciones.find(a => a.id === id); | ||||
|                     if (original) { // Chequeo de seguridad | ||||
|                         return updateAgrupacion(id, { ...original, ...changes }); | ||||
|                     } | ||||
|                 } | ||||
|                 return Promise.resolve(); | ||||
|             }); | ||||
|  | ||||
|             const logoPromise = updateLogos(editedLogos); | ||||
|              | ||||
|             await Promise.all([...agrupacionPromises, logoPromise]); | ||||
|              | ||||
|             queryClient.invalidateQueries({ queryKey: ['agrupaciones'] }); | ||||
|             queryClient.invalidateQueries({ queryKey: ['logos'] }); | ||||
|  | ||||
|             alert('¡Todos los cambios han sido guardados!'); | ||||
|         } catch (err) { | ||||
|             alert('Error al guardar los cambios.'); | ||||
|             console.error("Error al guardar todo:", err); | ||||
|             alert("Ocurrió un error al guardar los cambios."); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|         setFormData({ ...formData, [e.target.name]: e.target.value }); | ||||
|     const isLoading = isLoadingAgrupaciones || isLoadingLogos; | ||||
|  | ||||
|     const getLogoUrl = (agrupacionId: string, categoriaId: number) => { | ||||
|         return editedLogos.find(l => l.agrupacionPoliticaId === agrupacionId && l.categoriaId === categoriaId)?.logoUrl || ''; | ||||
|     }; | ||||
|      | ||||
|     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 ? ( | ||||
|                                 <> | ||||
|             <h3>Gestión de Agrupaciones y Logos</h3> | ||||
|             {isLoading ? <p>Cargando...</p> : ( | ||||
|                 <> | ||||
|                     <table> | ||||
|                         <thead> | ||||
|                             <tr> | ||||
|                                 <th>Nombre</th> | ||||
|                                 <th>Nombre Corto</th> | ||||
|                                 <th>Color</th> | ||||
|                                 <th>Logo Senadores</th> | ||||
|                                 <th>Logo Diputados</th> | ||||
|                                 <th>Logo Concejales</th> | ||||
|                             </tr> | ||||
|                         </thead> | ||||
|                         <tbody> | ||||
|                             {agrupaciones.map(agrupacion => ( | ||||
|                                 <tr key={agrupacion.id}> | ||||
|                                     <td>{agrupacion.nombre}</td> | ||||
|                                     <td><input type="text" 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> | ||||
|                                     <td><input type="text" value={editedAgrupaciones[agrupacion.id]?.nombreCorto ?? agrupacion.nombreCorto ?? ''} onChange={(e) => handleInputChange(agrupacion.id, 'nombreCorto', e.target.value)} /></td> | ||||
|                                     <td><input type="color" value={editedAgrupaciones[agrupacion.id]?.color ?? agrupacion.color ?? '#000000'} onChange={(e) => handleInputChange(agrupacion.id, 'color', e.target.value)} /></td> | ||||
|                                     <td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, SENADORES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, SENADORES_ID, e.target.value)} /></td> | ||||
|                                     <td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, DIPUTADOS_ID)} onChange={(e) => handleLogoChange(agrupacion.id, DIPUTADOS_ID, e.target.value)} /></td> | ||||
|                                     <td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, CONCEJALES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, CONCEJALES_ID, e.target.value)} /></td> | ||||
|                                 </tr> | ||||
|                             ))} | ||||
|                         </tbody> | ||||
|                     </table> | ||||
|                     <button onClick={handleSaveAll} style={{ marginTop: '1rem' }}> | ||||
|                         Guardar Todos los Cambios | ||||
|                     </button> | ||||
|                 </> | ||||
|             )} | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -10,6 +10,8 @@ export const ConfiguracionGeneral = () => { | ||||
|     const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); | ||||
|     const [loading, setLoading] = useState(true); | ||||
|     const [error, setError] = useState<string | null>(null); | ||||
|     const [tickerCantidad, setTickerCantidad] = useState('3'); | ||||
|     const [concejalesCantidad, setConcejalesCantidad] = useState('5'); | ||||
|  | ||||
|     const [presidenciaSenadoId, setPresidenciaSenadoId] = useState<string>(''); | ||||
|     // Renombramos el estado para mayor claridad | ||||
| @@ -24,6 +26,8 @@ export const ConfiguracionGeneral = () => { | ||||
|                 setAgrupaciones(agrupacionesData); | ||||
|                 setPresidenciaSenadoId(configData.PresidenciaSenadores || ''); | ||||
|                 setModoOficialActivo(configData.UsarDatosDeBancadasOficiales === 'true'); | ||||
|                 setTickerCantidad(configData.TickerResultadosCantidad || '3'); | ||||
|                 setConcejalesCantidad(configData.ConcejalesResultadosCantidad || '5'); | ||||
|             } catch (err) { | ||||
|                 console.error("Error al cargar datos de configuración:", err); | ||||
|                 setError("No se pudieron cargar los datos necesarios para la configuración."); | ||||
| @@ -36,7 +40,9 @@ export const ConfiguracionGeneral = () => { | ||||
|         try { | ||||
|             await updateConfiguracion({ | ||||
|                 "PresidenciaSenadores": presidenciaSenadoId, | ||||
|                 "UsarDatosDeBancadasOficiales": modoOficialActivo.toString() | ||||
|                 "UsarDatosDeBancadasOficiales": modoOficialActivo.toString(), | ||||
|                 "TickerResultadosCantidad": tickerCantidad, | ||||
|                 "ConcejalesResultadosCantidad": concejalesCantidad | ||||
|             }); | ||||
|             await queryClient.invalidateQueries({ queryKey: ['composicionCongreso'] }); | ||||
|             await queryClient.invalidateQueries({ queryKey: ['bancadasDetalle'] }); | ||||
| @@ -89,7 +95,7 @@ export const ConfiguracionGeneral = () => { | ||||
|                     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' }}> | ||||
|             <div style={{ marginTop: '1rem', borderBottom: '2px solid #eee' }}> | ||||
|                 <p style={{ fontWeight: 'bold', margin: 0 }}> | ||||
|                     Presidencia Cámara de Diputados | ||||
|                 </p> | ||||
| @@ -97,6 +103,19 @@ export const ConfiguracionGeneral = () => { | ||||
|                     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> | ||||
|             <div className="form-group" style={{ marginTop: '2rem' }}> | ||||
|                 <label htmlFor="ticker-cantidad">Cantidad en Ticker (Dip/Sen)</label> | ||||
|                 <input id="ticker-cantidad" type="number" value={tickerCantidad} onChange={e => setTickerCantidad(e.target.value)} /> | ||||
|             </div> | ||||
|             <div className="form-group" style={{ marginTop: '2rem' }}> | ||||
|                 <label htmlFor="concejales-cantidad">Cantidad en Widget Concejales</label> | ||||
|                 <input  | ||||
|                     id="concejales-cantidad" | ||||
|                     type="number"  | ||||
|                     value={concejalesCantidad} | ||||
|                     onChange={e => setConcejalesCantidad(e.target.value)} | ||||
|                 /> | ||||
|             </div> | ||||
|             <button onClick={handleSave} style={{ marginTop: '1.5rem' }}> | ||||
|                 Guardar Configuración | ||||
|             </button> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| // src/services/apiService.ts | ||||
| import axios from 'axios'; | ||||
| import { triggerLogout } from '../context/authUtils'; | ||||
| import type { AgrupacionPolitica, UpdateAgrupacionData, Bancada } from '../types'; | ||||
| import type { AgrupacionPolitica, UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria } from '../types'; | ||||
|  | ||||
| const AUTH_API_URL = 'http://localhost:5217/api/auth'; | ||||
| const ADMIN_API_URL = 'http://localhost:5217/api/admin'; | ||||
| @@ -94,4 +94,13 @@ export const getConfiguracion = async (): Promise<ConfiguracionResponse> => { | ||||
|  | ||||
| export const updateConfiguracion = async (data: Record<string, string>): Promise<void> => { | ||||
|     await adminApiClient.put('/configuracion', data); | ||||
| }; | ||||
|  | ||||
| export const getLogos = async (): Promise<LogoAgrupacionCategoria[]> => { | ||||
|     const response = await adminApiClient.get('/logos'); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| export const updateLogos = async (data: LogoAgrupacionCategoria[]): Promise<void> => { | ||||
|     await adminApiClient.put('/logos', data); | ||||
| }; | ||||
| @@ -6,7 +6,6 @@ export interface AgrupacionPolitica { | ||||
|   nombre: string; | ||||
|   nombreCorto: string | null; | ||||
|   color: string | null; | ||||
|   logoUrl: string | null; | ||||
|   ordenDiputados: number | null; | ||||
|   ordenSenadores: number | null; | ||||
| } | ||||
| @@ -14,7 +13,6 @@ export interface AgrupacionPolitica { | ||||
| export interface UpdateAgrupacionData { | ||||
|     nombreCorto: string | null; | ||||
|     color: string | null; | ||||
|     logoUrl: string | null; | ||||
| } | ||||
|  | ||||
| export const TipoCamara = { | ||||
| @@ -40,4 +38,11 @@ export interface Bancada { | ||||
|   agrupacionPoliticaId: string | null; | ||||
|   agrupacionPolitica: AgrupacionPolitica | null; | ||||
|   ocupante: OcupanteBanca | null; | ||||
| } | ||||
|  | ||||
| export interface LogoAgrupacionCategoria { | ||||
|     id: number; | ||||
|     agrupacionPoliticaId: string; | ||||
|     categoriaId: number; | ||||
|     logoUrl: string | null; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user