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