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