Feat Widgets 1541
This commit is contained in:
@@ -11,7 +11,7 @@ const CONCEJALES_ID = 7;
|
||||
|
||||
export const AgrupacionesManager = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
const [editedAgrupaciones, setEditedAgrupaciones] = useState<Record<string, Partial<AgrupacionPolitica>>>({});
|
||||
const [editedLogos, setEditedLogos] = useState<LogoAgrupacionCategoria[]>([]);
|
||||
|
||||
@@ -52,11 +52,22 @@ export const AgrupacionesManager = () => {
|
||||
const handleLogoChange = (agrupacionId: string, categoriaId: number, value: string) => {
|
||||
setEditedLogos(prev => {
|
||||
const newLogos = [...prev];
|
||||
const existing = newLogos.find(l => l.agrupacionPoliticaId === agrupacionId && l.categoriaId === categoriaId);
|
||||
const existing = newLogos.find(l =>
|
||||
l.agrupacionPoliticaId === agrupacionId &&
|
||||
l.categoriaId === categoriaId &&
|
||||
l.ambitoGeograficoId == null
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
existing.logoUrl = value;
|
||||
} else {
|
||||
newLogos.push({ id: 0, agrupacionPoliticaId: agrupacionId, categoriaId, logoUrl: value });
|
||||
newLogos.push({
|
||||
id: 0,
|
||||
agrupacionPoliticaId: agrupacionId,
|
||||
categoriaId,
|
||||
logoUrl: value,
|
||||
ambitoGeograficoId: null
|
||||
});
|
||||
}
|
||||
return newLogos;
|
||||
});
|
||||
@@ -75,9 +86,9 @@ export const AgrupacionesManager = () => {
|
||||
});
|
||||
|
||||
const logoPromise = updateLogos(editedLogos);
|
||||
|
||||
|
||||
await Promise.all([...agrupacionPromises, logoPromise]);
|
||||
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['agrupaciones'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['logos'] });
|
||||
|
||||
@@ -91,7 +102,11 @@ export const AgrupacionesManager = () => {
|
||||
const isLoading = isLoadingAgrupaciones || isLoadingLogos;
|
||||
|
||||
const getLogoUrl = (agrupacionId: string, categoriaId: number) => {
|
||||
return editedLogos.find(l => l.agrupacionPoliticaId === agrupacionId && l.categoriaId === categoriaId)?.logoUrl || '';
|
||||
return editedLogos.find(l =>
|
||||
l.agrupacionPoliticaId === agrupacionId &&
|
||||
l.categoriaId === categoriaId &&
|
||||
l.ambitoGeograficoId == null
|
||||
)?.logoUrl || '';
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { OrdenDiputadosManager } from './OrdenDiputadosManager';
|
||||
import { OrdenSenadoresManager } from './OrdenSenadoresManager';
|
||||
import { ConfiguracionGeneral } from './ConfiguracionGeneral';
|
||||
import { BancasManager } from './BancasManager';
|
||||
import { LogoOverridesManager } from './LogoOverridesManager';
|
||||
|
||||
export const DashboardPage = () => {
|
||||
const { logout } = useAuth();
|
||||
@@ -17,6 +18,7 @@ export const DashboardPage = () => {
|
||||
</header>
|
||||
<main style={{ marginTop: '2rem' }}>
|
||||
<AgrupacionesManager />
|
||||
<LogoOverridesManager />
|
||||
<div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', marginTop: '2rem' }}>
|
||||
<div style={{ flex: '1 1 400px' }}>
|
||||
<OrdenDiputadosManager />
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// src/components/LogoOverridesManager.tsx
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import Select from 'react-select';
|
||||
import { getMunicipiosForAdmin, getAgrupaciones, getLogos, updateLogos } from '../services/apiService';
|
||||
import type { MunicipioSimple, AgrupacionPolitica, LogoAgrupacionCategoria } from '../types';
|
||||
|
||||
// --- AÑADIMOS LAS CATEGORÍAS PARA EL SELECTOR ---
|
||||
const CATEGORIAS_OPTIONS = [
|
||||
{ value: 5, label: 'Senadores' },
|
||||
{ value: 6, label: 'Diputados' },
|
||||
{ value: 7, label: 'Concejales' }
|
||||
];
|
||||
|
||||
export const LogoOverridesManager = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: municipios = [] } = useQuery<MunicipioSimple[]>({ queryKey: ['municipiosForAdmin'], queryFn: getMunicipiosForAdmin });
|
||||
const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ queryKey: ['agrupaciones'], queryFn: getAgrupaciones });
|
||||
const { data: logos = [] } = useQuery<LogoAgrupacionCategoria[]>({ queryKey: ['logos'], queryFn: getLogos });
|
||||
|
||||
// --- NUEVO ESTADO PARA LA CATEGORÍA ---
|
||||
const [selectedCategoria, setSelectedCategoria] = useState<{ value: number; label: string } | null>(null);
|
||||
const [selectedMunicipio, setSelectedMunicipio] = useState<{ value: string; label: string } | null>(null);
|
||||
const [selectedAgrupacion, setSelectedAgrupacion] = useState<{ value: string; label: string } | null>(null);
|
||||
const [logoUrl, setLogoUrl] = useState('');
|
||||
|
||||
const municipioOptions = useMemo(() => municipios.map(m => ({ value: m.id, label: m.nombre })), [municipios]);
|
||||
const agrupacionOptions = useMemo(() => agrupaciones.map(a => ({ value: a.id, label: a.nombre })), [agrupaciones]);
|
||||
|
||||
const currentLogo = useMemo(() => {
|
||||
// La búsqueda ahora depende de los 3 selectores
|
||||
if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return '';
|
||||
return logos.find(l =>
|
||||
l.ambitoGeograficoId === parseInt(selectedMunicipio.value) &&
|
||||
l.agrupacionPoliticaId === selectedAgrupacion.value &&
|
||||
l.categoriaId === selectedCategoria.value
|
||||
)?.logoUrl || '';
|
||||
}, [logos, selectedMunicipio, selectedAgrupacion, selectedCategoria]);
|
||||
|
||||
useEffect(() => { setLogoUrl(currentLogo) }, [currentLogo]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return;
|
||||
const newLogoEntry: LogoAgrupacionCategoria = {
|
||||
id: 0,
|
||||
agrupacionPoliticaId: selectedAgrupacion.value,
|
||||
categoriaId: selectedCategoria.value,
|
||||
ambitoGeograficoId: parseInt(selectedMunicipio.value),
|
||||
logoUrl: logoUrl || null
|
||||
};
|
||||
try {
|
||||
await updateLogos([newLogoEntry]);
|
||||
queryClient.invalidateQueries({ queryKey: ['logos'] });
|
||||
alert('Override de logo guardado.');
|
||||
} catch { alert('Error al guardar.'); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="admin-module">
|
||||
<h3>Overrides de Logos por Municipio y Categoría</h3>
|
||||
<p>Configure una imagen específica para un partido en un municipio y categoría determinados.</p>
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label>Categoría</label>
|
||||
<Select options={CATEGORIAS_OPTIONS} value={selectedCategoria} onChange={setSelectedCategoria} isClearable placeholder="Seleccione..."/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label>Municipio</label>
|
||||
<Select options={municipioOptions} value={selectedMunicipio} onChange={setSelectedMunicipio} isClearable placeholder="Seleccione..."/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label>Agrupación</label>
|
||||
<Select options={agrupacionOptions} value={selectedAgrupacion} onChange={setSelectedAgrupacion} isClearable placeholder="Seleccione..."/>
|
||||
</div>
|
||||
<div style={{ flex: 2 }}>
|
||||
<label>URL del Logo Específico</label>
|
||||
<input type="text" value={logoUrl} onChange={e => setLogoUrl(e.target.value)} style={{ width: '100%' }} disabled={!selectedMunicipio || !selectedAgrupacion || !selectedCategoria} />
|
||||
</div>
|
||||
<button onClick={handleSave} disabled={!selectedMunicipio || !selectedAgrupacion || !selectedCategoria}>Guardar</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/services/apiService.ts
|
||||
import axios from 'axios';
|
||||
import { triggerLogout } from '../context/authUtils';
|
||||
import type { AgrupacionPolitica, UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria } from '../types';
|
||||
import type { AgrupacionPolitica, UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria, MunicipioSimple } from '../types';
|
||||
|
||||
const AUTH_API_URL = 'http://localhost:5217/api/auth';
|
||||
const ADMIN_API_URL = 'http://localhost:5217/api/admin';
|
||||
@@ -10,7 +10,7 @@ const adminApiClient = axios.create({
|
||||
baseURL: ADMIN_API_URL,
|
||||
});
|
||||
|
||||
// --- INTERCEPTORES (una sola vez) ---
|
||||
// --- INTERCEPTORES ---
|
||||
|
||||
// Interceptor de Peticiones: Añade el token JWT a cada llamada
|
||||
adminApiClient.interceptors.request.use(
|
||||
@@ -63,44 +63,51 @@ export const updateAgrupacion = async (id: string, data: UpdateAgrupacionData):
|
||||
|
||||
// 3. Ordenamiento de Agrupaciones
|
||||
export const updateOrden = async (camara: 'diputados' | 'senadores', ids: string[]): Promise<void> => {
|
||||
await adminApiClient.put(`/agrupaciones/orden-${camara}`, ids);
|
||||
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;
|
||||
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;
|
||||
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);
|
||||
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;
|
||||
const response = await adminApiClient.get('/configuracion');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateConfiguracion = async (data: Record<string, string>): Promise<void> => {
|
||||
await adminApiClient.put('/configuracion', data);
|
||||
await adminApiClient.put('/configuracion', data);
|
||||
};
|
||||
|
||||
export const getLogos = async (): Promise<LogoAgrupacionCategoria[]> => {
|
||||
const response = await adminApiClient.get('/logos');
|
||||
return response.data;
|
||||
const response = await adminApiClient.get('/logos');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateLogos = async (data: LogoAgrupacionCategoria[]): Promise<void> => {
|
||||
await adminApiClient.put('/logos', data);
|
||||
await adminApiClient.put('/logos', data);
|
||||
};
|
||||
|
||||
export const getMunicipiosForAdmin = async (): Promise<MunicipioSimple[]> => {
|
||||
// Ahora usa adminApiClient, que apunta a /api/admin/
|
||||
// La URL final será /api/admin/catalogos/municipios
|
||||
const response = await adminApiClient.get('/catalogos/municipios');
|
||||
return response.data;
|
||||
};
|
||||
@@ -45,4 +45,7 @@ export interface LogoAgrupacionCategoria {
|
||||
agrupacionPoliticaId: string;
|
||||
categoriaId: number;
|
||||
logoUrl: string | null;
|
||||
}
|
||||
ambitoGeograficoId: number | null;
|
||||
}
|
||||
|
||||
export interface MunicipioSimple { id: string; nombre: string; }
|
||||
Reference in New Issue
Block a user