Feat Widgets 1541

This commit is contained in:
2025-09-02 15:39:17 -03:00
parent da581d9714
commit f961f9d8e7
16 changed files with 1306 additions and 81 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,4 +45,7 @@ export interface LogoAgrupacionCategoria {
agrupacionPoliticaId: string;
categoriaId: number;
logoUrl: string | null;
}
ambitoGeograficoId: number | null;
}
export interface MunicipioSimple { id: string; nombre: string; }