Feat Widgets
- Widget de Home - Widget Cards por Provincias - Widget Mapa por Categorias
This commit is contained in:
@@ -1,148 +1,110 @@
|
||||
// src/components/AgrupacionesManager.tsx
|
||||
// EN: src/components/AgrupacionesManager.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import Select from 'react-select'; // Importamos Select
|
||||
import Select from 'react-select';
|
||||
import { getAgrupaciones, updateAgrupacion, getLogos, updateLogos } from '../services/apiService';
|
||||
import type { AgrupacionPolitica, LogoAgrupacionCategoria } from '../types';
|
||||
import type { AgrupacionPolitica, LogoAgrupacionCategoria, UpdateAgrupacionData } from '../types';
|
||||
import './AgrupacionesManager.css';
|
||||
|
||||
// Constantes para los IDs de categoría
|
||||
const SENADORES_ID = 5;
|
||||
const DIPUTADOS_ID = 6;
|
||||
const CONCEJALES_ID = 7;
|
||||
const SENADORES_NAC_ID = 1;
|
||||
const DIPUTADOS_NAC_ID = 2;
|
||||
const GLOBAL_ELECTION_ID = 0;
|
||||
|
||||
// Opciones para el nuevo selector de Elección
|
||||
const ELECCION_OPTIONS = [
|
||||
{ value: 2, label: 'Elecciones Nacionales' },
|
||||
{ value: 1, label: 'Elecciones Provinciales' }
|
||||
{ value: GLOBAL_ELECTION_ID, label: 'Global (Logo por Defecto)' },
|
||||
{ value: 2, label: 'Elecciones Nacionales (Override General)' },
|
||||
{ value: 1, label: 'Elecciones Provinciales (Override General)' }
|
||||
];
|
||||
|
||||
const sanitizeColor = (color: string | null | undefined): string => {
|
||||
if (!color) return '#000000';
|
||||
const sanitized = color.replace(/[^#0-9a-fA-F]/g, '');
|
||||
return sanitized.startsWith('#') ? sanitized : `#${sanitized}`;
|
||||
return color.startsWith('#') ? color : `#${color}`;
|
||||
};
|
||||
|
||||
export const AgrupacionesManager = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// --- NUEVO ESTADO PARA LA ELECCIÓN SELECCIONADA ---
|
||||
const [selectedEleccion, setSelectedEleccion] = useState(ELECCION_OPTIONS[0]);
|
||||
|
||||
const [editedAgrupaciones, setEditedAgrupaciones] = useState<Record<string, Partial<AgrupacionPolitica>>>({});
|
||||
const [editedLogos, setEditedLogos] = useState<LogoAgrupacionCategoria[]>([]);
|
||||
const [editedAgrupaciones, setEditedAgrupaciones] = useState<Record<string, { nombreCorto: string | null; color: string | null; }>>({});
|
||||
const [editedLogos, setEditedLogos] = useState<Record<string, string | null>>({});
|
||||
|
||||
const { data: agrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
|
||||
queryKey: ['agrupaciones'],
|
||||
queryFn: getAgrupaciones,
|
||||
queryKey: ['agrupaciones'], queryFn: getAgrupaciones,
|
||||
});
|
||||
|
||||
// --- CORRECCIÓN: La query de logos ahora depende del ID de la elección ---
|
||||
|
||||
const { data: logos = [], isLoading: isLoadingLogos } = useQuery<LogoAgrupacionCategoria[]>({
|
||||
queryKey: ['logos', selectedEleccion.value],
|
||||
queryFn: () => getLogos(selectedEleccion.value), // Pasamos el valor numérico
|
||||
queryKey: ['allLogos'],
|
||||
queryFn: () => Promise.all([getLogos(0), getLogos(1), getLogos(2)]).then(res => res.flat()),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (agrupaciones && agrupaciones.length > 0) {
|
||||
setEditedAgrupaciones(prev => {
|
||||
if (Object.keys(prev).length === 0) {
|
||||
return Object.fromEntries(agrupaciones.map(a => [a.id, {}]));
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
if (agrupaciones.length > 0) {
|
||||
const initialEdits = Object.fromEntries(
|
||||
agrupaciones.map(a => [a.id, { nombreCorto: a.nombreCorto, color: a.color }])
|
||||
);
|
||||
setEditedAgrupaciones(initialEdits);
|
||||
}
|
||||
}, [agrupaciones]);
|
||||
|
||||
// Este useEffect ahora también depende de 'logos' para reinicializarse
|
||||
|
||||
useEffect(() => {
|
||||
if (logos) {
|
||||
setEditedLogos(JSON.parse(JSON.stringify(logos)));
|
||||
const logoMap = Object.fromEntries(
|
||||
logos
|
||||
// --- CORRECCIÓN CLAVE 1: Comprobar contra `null` en lugar de `0` ---
|
||||
.filter(l => l.categoriaId === 0 && l.ambitoGeograficoId === null)
|
||||
.map(l => [`${l.agrupacionPoliticaId}-${l.eleccionId}`, l.logoUrl])
|
||||
);
|
||||
setEditedLogos(logoMap);
|
||||
}
|
||||
}, [logos]);
|
||||
|
||||
const handleInputChange = (id: string, field: 'nombreCorto' | 'color', value: string) => {
|
||||
setEditedAgrupaciones(prev => ({
|
||||
...prev,
|
||||
[id]: { ...prev[id], [field]: value }
|
||||
}));
|
||||
const handleInputChange = (id: string, field: 'nombreCorto' | 'color', value: string | null) => {
|
||||
setEditedAgrupaciones(prev => ({ ...prev, [id]: { ...prev[id], [field]: value } }));
|
||||
};
|
||||
|
||||
const handleLogoChange = (agrupacionId: string, categoriaId: number, value: string) => {
|
||||
setEditedLogos(prev => {
|
||||
const newLogos = [...prev];
|
||||
const existing = newLogos.find(l =>
|
||||
l.eleccionId === selectedEleccion.value &&
|
||||
l.agrupacionPoliticaId === agrupacionId &&
|
||||
l.categoriaId === categoriaId &&
|
||||
l.ambitoGeograficoId == null
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
existing.logoUrl = value;
|
||||
} else {
|
||||
newLogos.push({
|
||||
id: 0,
|
||||
eleccionId: selectedEleccion.value, // Añadimos el ID de la elección
|
||||
agrupacionPoliticaId: agrupacionId,
|
||||
categoriaId,
|
||||
logoUrl: value,
|
||||
ambitoGeograficoId: null
|
||||
});
|
||||
}
|
||||
return newLogos;
|
||||
});
|
||||
const handleLogoInputChange = (agrupacionId: string, value: string | null) => {
|
||||
const key = `${agrupacionId}-${selectedEleccion.value}`;
|
||||
setEditedLogos(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
|
||||
const handleSaveAll = async () => {
|
||||
try {
|
||||
const agrupacionPromises = Object.entries(editedAgrupaciones).map(([id, changes]) => {
|
||||
if (Object.keys(changes).length > 0) {
|
||||
const original = agrupaciones.find(a => a.id === id);
|
||||
if (original) {
|
||||
return updateAgrupacion(id, { ...original, ...changes });
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
const agrupacionPromises = agrupaciones.map(agrupacion => {
|
||||
const changes = editedAgrupaciones[agrupacion.id] || {};
|
||||
const payload: UpdateAgrupacionData = {
|
||||
nombreCorto: changes.nombreCorto ?? agrupacion.nombreCorto,
|
||||
color: changes.color ?? agrupacion.color,
|
||||
};
|
||||
return updateAgrupacion(agrupacion.id, payload);
|
||||
});
|
||||
|
||||
// --- CORRECCIÓN CLAVE 2: Enviar `null` a la API en lugar de `0` ---
|
||||
const logosPayload = Object.entries(editedLogos)
|
||||
.map(([key, logoUrl]) => {
|
||||
const [agrupacionPoliticaId, eleccionIdStr] = key.split('-');
|
||||
return { id: 0, eleccionId: parseInt(eleccionIdStr), agrupacionPoliticaId, categoriaId: 0, logoUrl: logoUrl || null, ambitoGeograficoId: null };
|
||||
});
|
||||
|
||||
const logoPromise = updateLogos(editedLogos);
|
||||
const logoPromise = updateLogos(logosPayload);
|
||||
|
||||
await Promise.all([...agrupacionPromises, logoPromise]);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['agrupaciones'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['logos', selectedEleccion.value] }); // Invalidamos la query correcta
|
||||
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ['agrupaciones'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['allLogos'] });
|
||||
alert('¡Todos los cambios han sido guardados!');
|
||||
} catch (err) {
|
||||
console.error("Error al guardar todo:", err);
|
||||
alert("Ocurrió un error al guardar los cambios.");
|
||||
}
|
||||
} catch (err) { console.error("Error al guardar todo:", err); alert("Ocurrió un error."); }
|
||||
};
|
||||
|
||||
const getLogoValue = (agrupacionId: string): string => {
|
||||
const key = `${agrupacionId}-${selectedEleccion.value}`;
|
||||
return editedLogos[key] ?? '';
|
||||
};
|
||||
|
||||
const isLoading = isLoadingAgrupaciones || isLoadingLogos;
|
||||
|
||||
const getLogoUrl = (agrupacionId: string, categoriaId: number) => {
|
||||
return editedLogos.find(l =>
|
||||
l.eleccionId === selectedEleccion.value &&
|
||||
l.agrupacionPoliticaId === agrupacionId &&
|
||||
l.categoriaId === categoriaId &&
|
||||
l.ambitoGeograficoId == null
|
||||
)?.logoUrl || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="admin-module">
|
||||
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||||
<h3>Gestión de Agrupaciones y Logos Generales</h3>
|
||||
<div style={{width: '250px', zIndex: 100 }}>
|
||||
<Select
|
||||
options={ELECCION_OPTIONS}
|
||||
value={selectedEleccion}
|
||||
onChange={(opt) => setSelectedEleccion(opt!)}
|
||||
/>
|
||||
<h3>Gestión de Agrupaciones y Logos</h3>
|
||||
<div style={{width: '350px', zIndex: 100 }}>
|
||||
<Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => setSelectedEleccion(opt!)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -155,42 +117,23 @@ export const AgrupacionesManager = () => {
|
||||
<th>Nombre</th>
|
||||
<th>Nombre Corto</th>
|
||||
<th>Color</th>
|
||||
{/* --- CABECERAS CONDICIONALES --- */}
|
||||
{selectedEleccion.value === 2 ? (
|
||||
<>
|
||||
<th>Logo Senadores Nac.</th>
|
||||
<th>Logo Diputados Nac.</th>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<th>Logo Senadores Prov.</th>
|
||||
<th>Logo Diputados Prov.</th>
|
||||
<th>Logo Concejales</th>
|
||||
</>
|
||||
)}
|
||||
<th>Logo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{agrupaciones.map(agrupacion => (
|
||||
<tr key={agrupacion.id}>
|
||||
<td>{agrupacion.nombre}</td>
|
||||
<td><input type="text" value={editedAgrupaciones[agrupacion.id]?.nombreCorto ?? agrupacion.nombreCorto ?? ''} onChange={(e) => handleInputChange(agrupacion.id, 'nombreCorto', e.target.value)} /></td>
|
||||
<td><input type="text" value={editedAgrupaciones[agrupacion.id]?.nombreCorto ?? ''} onChange={(e) => handleInputChange(agrupacion.id, 'nombreCorto', e.target.value)} /></td>
|
||||
<td><input type="color" value={sanitizeColor(editedAgrupaciones[agrupacion.id]?.color)} onChange={(e) => handleInputChange(agrupacion.id, 'color', e.target.value)} /></td>
|
||||
<td>
|
||||
<input type="color" value={sanitizeColor(editedAgrupaciones[agrupacion.id]?.color ?? agrupacion.color)} onChange={(e) => handleInputChange(agrupacion.id, 'color', e.target.value)} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="URL..."
|
||||
value={getLogoValue(agrupacion.id)}
|
||||
onChange={(e) => handleLogoInputChange(agrupacion.id, e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
{/* --- CELDAS CONDICIONALES --- */}
|
||||
{selectedEleccion.value === 2 ? (
|
||||
<>
|
||||
<td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, SENADORES_NAC_ID)} onChange={(e) => handleLogoChange(agrupacion.id, SENADORES_NAC_ID, e.target.value)} /></td>
|
||||
<td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, DIPUTADOS_NAC_ID)} onChange={(e) => handleLogoChange(agrupacion.id, DIPUTADOS_NAC_ID, e.target.value)} /></td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, SENADORES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, SENADORES_ID, e.target.value)} /></td>
|
||||
<td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, DIPUTADOS_ID)} onChange={(e) => handleLogoChange(agrupacion.id, DIPUTADOS_ID, e.target.value)} /></td>
|
||||
<td><input type="text" placeholder="URL..." value={getLogoUrl(agrupacion.id, CONCEJALES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, CONCEJALES_ID, e.target.value)} /></td>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { MunicipioSimple, AgrupacionPolitica, LogoAgrupacionCategoria, Prov
|
||||
import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias';
|
||||
|
||||
const ELECCION_OPTIONS = [
|
||||
{ value: 0, label: 'General (Toda la elección)' },
|
||||
{ value: 2, label: 'Elecciones Nacionales' },
|
||||
{ value: 1, label: 'Elecciones Provinciales' }
|
||||
];
|
||||
@@ -44,7 +45,7 @@ export const LogoOverridesManager = () => {
|
||||
const getAmbitoId = () => {
|
||||
if (selectedAmbitoLevel.value === 'municipio' && selectedMunicipio) return parseInt(selectedMunicipio.id);
|
||||
if (selectedAmbitoLevel.value === 'provincia' && selectedProvincia) return parseInt(selectedProvincia.id);
|
||||
return null;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const currentLogo = useMemo(() => {
|
||||
|
||||
@@ -8,7 +8,6 @@ export interface AgrupacionPolitica {
|
||||
color: string | null;
|
||||
ordenDiputados: number | null;
|
||||
ordenSenadores: number | null;
|
||||
// Añadimos los nuevos campos para el ordenamiento nacional
|
||||
ordenDiputadosNacionales: number | null;
|
||||
ordenSenadoresNacionales: number | null;
|
||||
}
|
||||
@@ -58,7 +57,7 @@ export interface LogoAgrupacionCategoria {
|
||||
id: number;
|
||||
eleccionId: number; // Clave para diferenciar
|
||||
agrupacionPoliticaId: string;
|
||||
categoriaId: number;
|
||||
categoriaId: number | null;
|
||||
logoUrl: string | null;
|
||||
ambitoGeograficoId: number | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user