Feat Widgets

This commit is contained in:
2025-09-01 14:04:40 -03:00
parent 608ae655be
commit 12860f2406
30 changed files with 1904 additions and 247 deletions

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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);
};

View File

@@ -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;
}