Feat Se añade Id de Agrupaciones en Componentes

This commit is contained in:
2025-10-20 11:03:19 -03:00
parent 069446b903
commit d6b4c3cc4d
12 changed files with 328 additions and 323 deletions

View File

@@ -123,7 +123,7 @@ export const AgrupacionesManager = () => {
<tbody> <tbody>
{agrupaciones.map(agrupacion => ( {agrupaciones.map(agrupacion => (
<tr key={agrupacion.id}> <tr key={agrupacion.id}>
<td>{agrupacion.nombre}</td> <td>({agrupacion.id}) {agrupacion.nombre}</td>
<td><input type="text" value={editedAgrupaciones[agrupacion.id]?.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)} onChange={(e) => handleInputChange(agrupacion.id, 'color', e.target.value)} /></td>
<td> <td>

View File

@@ -89,7 +89,7 @@ export const BancasNacionalesManager = () => {
onChange={(e) => handleAgrupacionChange(bancada.id, e.target.value || null)} onChange={(e) => handleAgrupacionChange(bancada.id, e.target.value || null)}
> >
<option value="">-- Vacante --</option> <option value="">-- Vacante --</option>
{agrupaciones.map(a => <option key={a.id} value={a.id}>{a.nombre}</option>)} {agrupaciones.map(a => <option key={a.id} value={a.id}>{`(${a.id}) ${a.nombre}`}</option>)}
</select> </select>
</td> </td>
<td>{bancada.ocupante?.nombreOcupante || 'Sin asignar'}</td> <td>{bancada.ocupante?.nombreOcupante || 'Sin asignar'}</td>

View File

@@ -95,7 +95,7 @@ export const BancasPreviasManager = () => {
<tbody> <tbody>
{agrupaciones.map(agrupacion => ( {agrupaciones.map(agrupacion => (
<tr key={agrupacion.id}> <tr key={agrupacion.id}>
<td>{agrupacion.nombre}</td> <td>({agrupacion.id}) {agrupacion.nombre}</td>
<td> <td>
<input <input
type="number" type="number"

View File

@@ -91,7 +91,7 @@ export const BancasProvincialesManager = () => {
onChange={(e) => handleAgrupacionChange(bancada.id, e.target.value || null)} onChange={(e) => handleAgrupacionChange(bancada.id, e.target.value || null)}
> >
<option value="">-- Vacante --</option> <option value="">-- Vacante --</option>
{agrupaciones.map(a => <option key={a.id} value={a.id}>{a.nombre}</option>)} {agrupaciones.map(a => <option key={a.id} value={a.id}>{`(${a.id}) ${a.nombre}`}</option>)}
</select> </select>
</td> </td>
<td>{bancada.ocupante?.nombreOcupante || 'Sin asignar'}</td> <td>{bancada.ocupante?.nombreOcupante || 'Sin asignar'}</td>

View File

@@ -6,7 +6,7 @@ import { getProvinciasForAdmin, getMunicipiosForAdmin, getAgrupaciones, getCandi
import type { MunicipioSimple, AgrupacionPolitica, CandidatoOverride, ProvinciaSimple } from '../types'; import type { MunicipioSimple, AgrupacionPolitica, CandidatoOverride, ProvinciaSimple } from '../types';
import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias'; import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias';
const ELECCION_OPTIONS = [ const ELECCION_OPTIONS = [
{ value: 2, label: 'Elecciones Nacionales' }, { value: 2, label: 'Elecciones Nacionales' },
{ value: 1, label: 'Elecciones Provinciales' } { value: 1, label: 'Elecciones Provinciales' }
]; ];
@@ -83,7 +83,14 @@ export const CandidatoOverridesManager = () => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', alignItems: 'flex-end' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', alignItems: 'flex-end' }}>
<Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => { setSelectedEleccion(opt!); setSelectedCategoria(null); }} /> <Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => { setSelectedEleccion(opt!); setSelectedCategoria(null); }} />
<Select options={categoriaOptions} value={selectedCategoria} onChange={setSelectedCategoria} placeholder="Seleccione Categoría..." isDisabled={!selectedEleccion} /> <Select options={categoriaOptions} value={selectedCategoria} onChange={setSelectedCategoria} placeholder="Seleccione Categoría..." isDisabled={!selectedEleccion} />
<Select options={agrupaciones.map(a => ({ value: a.id, label: a.nombre, ...a }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedAgrupacion} onChange={setSelectedAgrupacion} placeholder="Seleccione Agrupación..." /> <Select
options={agrupaciones.map(a => ({ value: a.id, label: a.nombre, ...a }))}
getOptionValue={opt => opt.id}
getOptionLabel={opt => `(${opt.id}) ${opt.nombre}`}
value={selectedAgrupacion}
onChange={setSelectedAgrupacion}
placeholder="Seleccione Agrupación..."
/>
<Select options={AMBITO_LEVEL_OPTIONS} value={selectedAmbitoLevel} onChange={(opt) => { setSelectedAmbitoLevel(opt!); setSelectedProvincia(null); setSelectedMunicipio(null); }} /> <Select options={AMBITO_LEVEL_OPTIONS} value={selectedAmbitoLevel} onChange={(opt) => { setSelectedAmbitoLevel(opt!); setSelectedProvincia(null); setSelectedMunicipio(null); }} />
{selectedAmbitoLevel.value === 'provincia' || selectedAmbitoLevel.value === 'municipio' ? ( {selectedAmbitoLevel.value === 'provincia' || selectedAmbitoLevel.value === 'municipio' ? (

View File

@@ -9,7 +9,7 @@ export const ConfiguracionNacional = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [presidenciaDiputadosId, setPresidenciaDiputadosId] = useState<string>(''); const [presidenciaDiputadosId, setPresidenciaDiputadosId] = useState<string>('');
const [presidenciaSenadoId, setPresidenciaSenadoId] = useState<string>(''); const [presidenciaSenadoId, setPresidenciaSenadoId] = useState<string>('');
const [modoOficialActivo, setModoOficialActivo] = useState(false); const [modoOficialActivo, setModoOficialActivo] = useState(false);
@@ -30,7 +30,7 @@ export const ConfiguracionNacional = () => {
setModoOficialActivo(configData.UsarDatosOficialesNacionales === 'true'); setModoOficialActivo(configData.UsarDatosOficialesNacionales === 'true');
setDiputadosTipoBanca(configData.PresidenciaDiputadosNacional_TipoBanca === 'previa' ? 'previa' : 'ganada'); setDiputadosTipoBanca(configData.PresidenciaDiputadosNacional_TipoBanca === 'previa' ? 'previa' : 'ganada');
setSenadoTipoBanca(configData.PresidenciaSenadoNacional_TipoBanca === 'previa' ? 'previa' : 'ganada'); setSenadoTipoBanca(configData.PresidenciaSenadoNacional_TipoBanca === 'previa' ? 'previa' : 'ganada');
} catch (err) { console.error("Error al cargar datos de configuración nacional:", err); } } catch (err) { console.error("Error al cargar datos de configuración nacional:", err); }
finally { setLoading(false); } finally { setLoading(false); }
}; };
loadInitialData(); loadInitialData();
@@ -56,16 +56,7 @@ export const ConfiguracionNacional = () => {
return ( return (
<div className="admin-module"> <div className="admin-module">
<h3>Configuración de Widgets Nacionales</h3> <h3>Configuración de Widgets Nacionales</h3>
{/*<div className="form-group">
<label>
<input type="checkbox" checked={modoOficialActivo} onChange={e => setModoOficialActivo(e.target.checked)} />
**Activar Modo "Resultados Oficiales" para Widgets Nacionales**
</label>
<p style={{ fontSize: '0.8rem', color: '#666' }}>
Si está activo, los widgets nacionales usarán la composición manual de bancas. Si no, usarán la proyección en tiempo real.
</p>
</div>*/}
<div style={{ display: 'flex', gap: '2rem', marginTop: '1rem' }}> <div style={{ display: 'flex', gap: '2rem', marginTop: '1rem' }}>
{/* Columna Diputados */} {/* Columna Diputados */}
<div style={{ flex: 1, borderRight: '1px solid #ccc', paddingRight: '1rem' }}> <div style={{ flex: 1, borderRight: '1px solid #ccc', paddingRight: '1rem' }}>
@@ -77,14 +68,14 @@ export const ConfiguracionNacional = () => {
</p> </p>
<select id="presidencia-diputados-nacional" value={presidenciaDiputadosId} onChange={e => setPresidenciaDiputadosId(e.target.value)} style={{ width: '100%', padding: '8px', marginBottom: '0.5rem' }}> <select id="presidencia-diputados-nacional" value={presidenciaDiputadosId} onChange={e => setPresidenciaDiputadosId(e.target.value)} style={{ width: '100%', padding: '8px', marginBottom: '0.5rem' }}>
<option value="">-- No Asignado --</option> <option value="">-- No Asignado --</option>
{agrupaciones.map(a => (<option key={a.id} value={a.id}>{a.nombre}</option>))} {agrupaciones.map(a => (<option key={a.id} value={a.id}>{`(${a.id}) ${a.nombre}`}</option>))}
</select> </select>
{presidenciaDiputadosId && ( {presidenciaDiputadosId && (
<div> <div>
<label><input type="radio" value="ganada" checked={diputadosTipoBanca === 'ganada'} onChange={() => setDiputadosTipoBanca('ganada')} /> Descontar de Banca Ganada</label> <label><input type="radio" value="ganada" checked={diputadosTipoBanca === 'ganada'} onChange={() => setDiputadosTipoBanca('ganada')} /> Descontar de Banca Ganada</label>
<label style={{marginLeft: '1rem'}}><input type="radio" value="previa" checked={diputadosTipoBanca === 'previa'} onChange={() => setDiputadosTipoBanca('previa')} /> Descontar de Banca Previa</label> <label style={{ marginLeft: '1rem' }}><input type="radio" value="previa" checked={diputadosTipoBanca === 'previa'} onChange={() => setDiputadosTipoBanca('previa')} /> Descontar de Banca Previa</label>
</div> </div>
)} )}
</div> </div>
{/* Columna Senadores */} {/* Columna Senadores */}
@@ -97,11 +88,11 @@ export const ConfiguracionNacional = () => {
</p> </p>
<select id="presidencia-senado-nacional" value={presidenciaSenadoId} onChange={e => setPresidenciaSenadoId(e.target.value)} style={{ width: '100%', padding: '8px' }}> <select id="presidencia-senado-nacional" value={presidenciaSenadoId} onChange={e => setPresidenciaSenadoId(e.target.value)} style={{ width: '100%', padding: '8px' }}>
<option value="">-- No Asignado --</option> <option value="">-- No Asignado --</option>
{agrupaciones.map(a => (<option key={a.id} value={a.id}>{a.nombre}</option>))} {agrupaciones.map(a => (<option key={a.id} value={a.id}>{`(${a.id}) ${a.nombre}`}</option>))}
</select> </select>
</div> </div>
</div> </div>
<button onClick={handleSave} style={{ marginTop: '1.5rem' }}> <button onClick={handleSave} style={{ marginTop: '1.5rem' }}>
Guardar Configuración Guardar Configuración
</button> </button>

View File

@@ -7,7 +7,7 @@ import type { MunicipioSimple, AgrupacionPolitica, LogoAgrupacionCategoria, Prov
import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias'; import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias';
const ELECCION_OPTIONS = [ const ELECCION_OPTIONS = [
{ value: 0, label: 'General (Toda la elección)' }, { value: 0, label: 'General (Todas las elecciones)' },
{ value: 2, label: 'Elecciones Nacionales' }, { value: 2, label: 'Elecciones Nacionales' },
{ value: 1, label: 'Elecciones Provinciales' } { value: 1, label: 'Elecciones Provinciales' }
]; ];
@@ -84,7 +84,14 @@ export const LogoOverridesManager = () => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', alignItems: 'flex-end' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', alignItems: 'flex-end' }}>
<Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => { setSelectedEleccion(opt!); setSelectedCategoria(null); }} /> <Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => { setSelectedEleccion(opt!); setSelectedCategoria(null); }} />
<Select options={categoriaOptions} value={selectedCategoria} onChange={setSelectedCategoria} placeholder="Seleccione Categoría..." isDisabled={!selectedEleccion} /> <Select options={categoriaOptions} value={selectedCategoria} onChange={setSelectedCategoria} placeholder="Seleccione Categoría..." isDisabled={!selectedEleccion} />
<Select options={agrupaciones.map(a => ({ value: a.id, label: a.nombre, ...a }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedAgrupacion} onChange={setSelectedAgrupacion} placeholder="Seleccione Agrupación..." /> <Select
options={agrupaciones.map(a => ({ value: a.id, label: a.nombre, ...a }))}
getOptionValue={opt => opt.id}
getOptionLabel={opt => `(${opt.id}) ${opt.nombre}`}
value={selectedAgrupacion}
onChange={setSelectedAgrupacion}
placeholder="Seleccione Agrupación..."
/>
<Select options={AMBITO_LEVEL_OPTIONS} value={selectedAmbitoLevel} onChange={(opt) => { setSelectedAmbitoLevel(opt!); setSelectedProvincia(null); setSelectedMunicipio(null); }} /> <Select options={AMBITO_LEVEL_OPTIONS} value={selectedAmbitoLevel} onChange={(opt) => { setSelectedAmbitoLevel(opt!); setSelectedProvincia(null); setSelectedMunicipio(null); }} />
{selectedAmbitoLevel.value === 'provincia' || selectedAmbitoLevel.value === 'municipio' ? ( {selectedAmbitoLevel.value === 'provincia' || selectedAmbitoLevel.value === 'municipio' ? (

View File

@@ -25,12 +25,12 @@ import './AgrupacionesManager.css'; // Reutilizamos los estilos
const updateOrdenDiputadosApi = async (ids: string[]) => { const updateOrdenDiputadosApi = async (ids: string[]) => {
const token = localStorage.getItem('admin-jwt-token'); const token = localStorage.getItem('admin-jwt-token');
const response = await fetch('http://localhost:5217/api/admin/agrupaciones/orden-diputados', { const response = await fetch('http://localhost:5217/api/admin/agrupaciones/orden-diputados', {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}, },
body: JSON.stringify(ids) body: JSON.stringify(ids)
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to save Diputados order"); throw new Error("Failed to save Diputados order");
@@ -38,77 +38,77 @@ const updateOrdenDiputadosApi = async (ids: string[]) => {
}; };
export const OrdenDiputadosManager = () => { export const OrdenDiputadosManager = () => {
const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
); );
useEffect(() => { useEffect(() => {
const fetchAndSortAgrupaciones = async () => { const fetchAndSortAgrupaciones = async () => {
setLoading(true); setLoading(true);
try { try {
const data = await getAgrupaciones(); const data = await getAgrupaciones();
// Ordenar por el orden de Diputados. Los nulos van al final. // Ordenar por el orden de Diputados. Los nulos van al final.
data.sort((a, b) => (a.ordenDiputados || 999) - (b.ordenDiputados || 999)); data.sort((a, b) => (a.ordenDiputados || 999) - (b.ordenDiputados || 999));
setAgrupaciones(data); setAgrupaciones(data);
} catch (error) { } catch (error) {
console.error("Failed to fetch agrupaciones for Diputados:", error); console.error("Failed to fetch agrupaciones for Diputados:", error);
} finally { } finally {
setLoading(false); setLoading(false);
}
};
fetchAndSortAgrupaciones();
}, []);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setAgrupaciones((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
} }
}; };
fetchAndSortAgrupaciones();
}, []);
const handleSaveOrder = async () => { const handleDragEnd = (event: DragEndEvent) => {
const idsOrdenados = agrupaciones.map(a => a.id); const { active, over } = event;
try { if (over && active.id !== over.id) {
await updateOrdenDiputadosApi(idsOrdenados); setAgrupaciones((items) => {
alert('Orden de Diputados guardado con éxito!'); const oldIndex = items.findIndex((item) => item.id === active.id);
} catch (error) { const newIndex = items.findIndex((item) => item.id === over.id);
alert('Error al guardar el orden de Diputados.'); return arrayMove(items, oldIndex, newIndex);
} });
}; }
};
if (loading) return <p>Cargando orden de Diputados...</p>; const handleSaveOrder = async () => {
const idsOrdenados = agrupaciones.map(a => a.id);
try {
await updateOrdenDiputadosApi(idsOrdenados);
alert('Orden de Diputados guardado con éxito!');
} catch (error) {
alert('Error al guardar el orden de Diputados.');
}
};
return ( if (loading) return <p>Cargando orden de Diputados...</p>;
<div className="admin-module">
<h3>Ordenar Agrupaciones (Diputados)</h3> return (
<p>Arrastre para reordenar.</p> <div className="admin-module">
<p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p> <h3>Ordenar Agrupaciones (Diputados)</h3>
<DndContext <p>Arrastre para reordenar.</p>
sensors={sensors} <p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p>
collisionDetection={closestCenter} <DndContext
onDragEnd={handleDragEnd} sensors={sensors}
> collisionDetection={closestCenter}
<SortableContext onDragEnd={handleDragEnd}
items={agrupaciones.map(a => a.id)} >
strategy={horizontalListSortingStrategy} <SortableContext
> items={agrupaciones.map(a => a.id)}
<ul className="sortable-list-horizontal"> strategy={horizontalListSortingStrategy}
{agrupaciones.map(agrupacion => ( >
<SortableItem key={agrupacion.id} id={agrupacion.id}> <ul className="sortable-list-horizontal">
{agrupacion.nombreCorto || agrupacion.nombre} {agrupaciones.map(agrupacion => (
</SortableItem> <SortableItem key={agrupacion.id} id={agrupacion.id}>
))} {`(${agrupacion.id}) ${agrupacion.nombreCorto || agrupacion.nombre}`}
</ul> </SortableItem>
</SortableContext> ))}
</DndContext> </ul>
<button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden Diputados</button> </SortableContext>
</div> </DndContext>
); <button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden Diputados</button>
</div>
);
}; };

View File

@@ -11,92 +11,92 @@ import './AgrupacionesManager.css';
const ELECCION_ID_NACIONAL = 2; const ELECCION_ID_NACIONAL = 2;
export const OrdenDiputadosNacionalesManager = () => { export const OrdenDiputadosNacionalesManager = () => {
// Estado para la lista que el usuario puede ordenar // Estado para la lista que el usuario puede ordenar
const [agrupacionesOrdenadas, setAgrupacionesOrdenadas] = useState<AgrupacionPolitica[]>([]); const [agrupacionesOrdenadas, setAgrupacionesOrdenadas] = useState<AgrupacionPolitica[]>([]);
// Query 1: Obtener TODAS las agrupaciones para tener sus datos completos (nombre, etc.)
const { data: todasAgrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
queryKey: ['agrupaciones'],
queryFn: getAgrupaciones,
});
// Query 2: Obtener los datos de composición para saber qué partidos tienen bancas
const { data: composicionData, isLoading: isLoadingComposicion } = useQuery({
queryKey: ['composicionNacional', ELECCION_ID_NACIONAL],
queryFn: () => getComposicionNacional(ELECCION_ID_NACIONAL),
});
// Este efecto se ejecuta cuando los datos de las queries estén disponibles // Query 1: Obtener TODAS las agrupaciones para tener sus datos completos (nombre, etc.)
useEffect(() => { const { data: todasAgrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
// No hacemos nada hasta que ambas queries hayan cargado sus datos queryKey: ['agrupaciones'],
if (!composicionData || !todasAgrupaciones || todasAgrupaciones.length === 0) { queryFn: getAgrupaciones,
return; });
}
// Creamos un Set con los IDs de los partidos que tienen al menos una banca de diputado // Query 2: Obtener los datos de composición para saber qué partidos tienen bancas
const partidosConBancasIds = new Set( const { data: composicionData, isLoading: isLoadingComposicion } = useQuery({
composicionData.diputados.partidos queryKey: ['composicionNacional', ELECCION_ID_NACIONAL],
.filter(p => p.bancasTotales > 0) queryFn: () => getComposicionNacional(ELECCION_ID_NACIONAL),
.map(p => p.id) });
);
// Filtramos la lista completa de agrupaciones, quedándonos solo con las relevantes // Este efecto se ejecuta cuando los datos de las queries estén disponibles
const agrupacionesFiltradas = todasAgrupaciones.filter(a => partidosConBancasIds.has(a.id)); useEffect(() => {
// No hacemos nada hasta que ambas queries hayan cargado sus datos
if (!composicionData || !todasAgrupaciones || todasAgrupaciones.length === 0) {
return;
}
// Ordenamos la lista filtrada según el orden guardado en la BD // Creamos un Set con los IDs de los partidos que tienen al menos una banca de diputado
agrupacionesFiltradas.sort((a, b) => (a.ordenDiputadosNacionales || 999) - (b.ordenDiputadosNacionales || 999)); const partidosConBancasIds = new Set(
composicionData.diputados.partidos
// Actualizamos el estado que se renderiza y que el usuario puede ordenar .filter(p => p.bancasTotales > 0)
setAgrupacionesOrdenadas(agrupacionesFiltradas); .map(p => p.id)
}, [todasAgrupaciones, composicionData]); // Dependencias: se re-ejecuta si los datos cambian
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
); );
const handleDragEnd = (event: DragEndEvent) => { // Filtramos la lista completa de agrupaciones, quedándonos solo con las relevantes
const { active, over } = event; const agrupacionesFiltradas = todasAgrupaciones.filter(a => partidosConBancasIds.has(a.id));
if (over && active.id !== over.id) {
setAgrupacionesOrdenadas((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
const handleSaveOrder = async () => { // Ordenamos la lista filtrada según el orden guardado en la BD
const idsOrdenados = agrupacionesOrdenadas.map(a => a.id); agrupacionesFiltradas.sort((a, b) => (a.ordenDiputadosNacionales || 999) - (b.ordenDiputadosNacionales || 999));
try {
await updateOrden('diputados-nacionales', idsOrdenados);
alert('Orden de Diputados Nacionales guardado con éxito!');
} catch (error) {
alert('Error al guardar el orden de Diputados Nacionales.');
}
};
const isLoading = isLoadingAgrupaciones || isLoadingComposicion; // Actualizamos el estado que se renderiza y que el usuario puede ordenar
if (isLoading) return <p>Cargando orden de Diputados Nacionales...</p>; setAgrupacionesOrdenadas(agrupacionesFiltradas);
return ( }, [todasAgrupaciones, composicionData]); // Dependencias: se re-ejecuta si los datos cambian
<div className="admin-module">
<h3>Ordenar Agrupaciones (Diputados Nacionales)</h3> const sensors = useSensors(
<p>Arrastre para reordenar. Solo se muestran los partidos con bancas.</p> useSensor(PointerSensor),
<p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p> useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> );
<SortableContext items={agrupacionesOrdenadas.map(a => a.id)} strategy={horizontalListSortingStrategy}>
<ul className="sortable-list-horizontal"> const handleDragEnd = (event: DragEndEvent) => {
{agrupacionesOrdenadas.map(agrupacion => ( const { active, over } = event;
<SortableItem key={agrupacion.id} id={agrupacion.id}> if (over && active.id !== over.id) {
{agrupacion.nombreCorto || agrupacion.nombre} setAgrupacionesOrdenadas((items) => {
</SortableItem> const oldIndex = items.findIndex((item) => item.id === active.id);
))} const newIndex = items.findIndex((item) => item.id === over.id);
</ul> return arrayMove(items, oldIndex, newIndex);
</SortableContext> });
</DndContext> }
<button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden</button> };
</div>
); const handleSaveOrder = async () => {
const idsOrdenados = agrupacionesOrdenadas.map(a => a.id);
try {
await updateOrden('diputados-nacionales', idsOrdenados);
alert('Orden de Diputados Nacionales guardado con éxito!');
} catch (error) {
alert('Error al guardar el orden de Diputados Nacionales.');
}
};
const isLoading = isLoadingAgrupaciones || isLoadingComposicion;
if (isLoading) return <p>Cargando orden de Diputados Nacionales...</p>;
return (
<div className="admin-module">
<h3>Ordenar Agrupaciones (Diputados Nacionales)</h3>
<p>Arrastre para reordenar. Solo se muestran los partidos con bancas.</p>
<p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={agrupacionesOrdenadas.map(a => a.id)} strategy={horizontalListSortingStrategy}>
<ul className="sortable-list-horizontal">
{agrupacionesOrdenadas.map(agrupacion => (
<SortableItem key={agrupacion.id} id={agrupacion.id}>
{`(${agrupacion.id}) ${agrupacion.nombreCorto || agrupacion.nombre}`}
</SortableItem>
))}
</ul>
</SortableContext>
</DndContext>
<button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden</button>
</div>
);
}; };

View File

@@ -25,12 +25,12 @@ import './AgrupacionesManager.css'; // Reutilizamos los estilos
const updateOrdenSenadoresApi = async (ids: string[]) => { const updateOrdenSenadoresApi = async (ids: string[]) => {
const token = localStorage.getItem('admin-jwt-token'); const token = localStorage.getItem('admin-jwt-token');
const response = await fetch('http://localhost:5217/api/admin/agrupaciones/orden-senadores', { const response = await fetch('http://localhost:5217/api/admin/agrupaciones/orden-senadores', {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}, },
body: JSON.stringify(ids) body: JSON.stringify(ids)
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to save Senadores order"); throw new Error("Failed to save Senadores order");
@@ -38,77 +38,77 @@ const updateOrdenSenadoresApi = async (ids: string[]) => {
}; };
export const OrdenSenadoresManager = () => { export const OrdenSenadoresManager = () => {
const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]); const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
); );
useEffect(() => { useEffect(() => {
const fetchAndSortAgrupaciones = async () => { const fetchAndSortAgrupaciones = async () => {
setLoading(true); setLoading(true);
try { try {
const data = await getAgrupaciones(); const data = await getAgrupaciones();
// Ordenar por el orden de Senadores. Los nulos van al final. // Ordenar por el orden de Senadores. Los nulos van al final.
data.sort((a, b) => (a.ordenSenadores || 999) - (b.ordenSenadores || 999)); data.sort((a, b) => (a.ordenSenadores || 999) - (b.ordenSenadores || 999));
setAgrupaciones(data); setAgrupaciones(data);
} catch (error) { } catch (error) {
console.error("Failed to fetch agrupaciones for Senadores:", error); console.error("Failed to fetch agrupaciones for Senadores:", error);
} finally { } finally {
setLoading(false); setLoading(false);
}
};
fetchAndSortAgrupaciones();
}, []);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setAgrupaciones((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
} }
}; };
fetchAndSortAgrupaciones();
}, []);
const handleSaveOrder = async () => { const handleDragEnd = (event: DragEndEvent) => {
const idsOrdenados = agrupaciones.map(a => a.id); const { active, over } = event;
try { if (over && active.id !== over.id) {
await updateOrdenSenadoresApi(idsOrdenados); setAgrupaciones((items) => {
alert('Orden de Senadores guardado con éxito!'); const oldIndex = items.findIndex((item) => item.id === active.id);
} catch (error) { const newIndex = items.findIndex((item) => item.id === over.id);
alert('Error al guardar el orden de Senadores.'); return arrayMove(items, oldIndex, newIndex);
} });
}; }
};
if (loading) return <p>Cargando orden de Senadores...</p>; const handleSaveOrder = async () => {
const idsOrdenados = agrupaciones.map(a => a.id);
try {
await updateOrdenSenadoresApi(idsOrdenados);
alert('Orden de Senadores guardado con éxito!');
} catch (error) {
alert('Error al guardar el orden de Senadores.');
}
};
return ( if (loading) return <p>Cargando orden de Senadores...</p>;
<div className="admin-module">
<h3>Ordenar Agrupaciones (Senado)</h3> return (
<p>Arrastre para reordenar.</p> <div className="admin-module">
<p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p> <h3>Ordenar Agrupaciones (Senado)</h3>
<DndContext <p>Arrastre para reordenar.</p>
sensors={sensors} <p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p>
collisionDetection={closestCenter} <DndContext
onDragEnd={handleDragEnd} sensors={sensors}
> collisionDetection={closestCenter}
<SortableContext onDragEnd={handleDragEnd}
items={agrupaciones.map(a => a.id)} >
strategy={horizontalListSortingStrategy} <SortableContext
> items={agrupaciones.map(a => a.id)}
<ul className="sortable-list-horizontal"> strategy={horizontalListSortingStrategy}
{agrupaciones.map(agrupacion => ( >
<SortableItem key={agrupacion.id} id={agrupacion.id}> <ul className="sortable-list-horizontal">
{agrupacion.nombreCorto || agrupacion.nombre} {agrupaciones.map(agrupacion => (
</SortableItem> <SortableItem key={agrupacion.id} id={agrupacion.id}>
))} {`(${agrupacion.id}) ${agrupacion.nombreCorto || agrupacion.nombre}`}
</ul> </SortableItem>
</SortableContext> ))}
</DndContext> </ul>
<button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden Senado</button> </SortableContext>
</div> </DndContext>
); <button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden Senado</button>
</div>
);
}; };

View File

@@ -11,84 +11,84 @@ import './AgrupacionesManager.css';
const ELECCION_ID_NACIONAL = 2; const ELECCION_ID_NACIONAL = 2;
export const OrdenSenadoresNacionalesManager = () => { export const OrdenSenadoresNacionalesManager = () => {
const [agrupacionesOrdenadas, setAgrupacionesOrdenadas] = useState<AgrupacionPolitica[]>([]); const [agrupacionesOrdenadas, setAgrupacionesOrdenadas] = useState<AgrupacionPolitica[]>([]);
const { data: todasAgrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
queryKey: ['agrupaciones'],
queryFn: getAgrupaciones,
});
const { data: composicionData, isLoading: isLoadingComposicion } = useQuery({
queryKey: ['composicionNacional', ELECCION_ID_NACIONAL],
queryFn: () => getComposicionNacional(ELECCION_ID_NACIONAL),
});
useEffect(() => { const { data: todasAgrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
if (!composicionData || !todasAgrupaciones || todasAgrupaciones.length === 0) { queryKey: ['agrupaciones'],
return; queryFn: getAgrupaciones,
} });
// Creamos un Set con los IDs de los partidos que tienen al menos una banca de senador const { data: composicionData, isLoading: isLoadingComposicion } = useQuery({
const partidosConBancasIds = new Set( queryKey: ['composicionNacional', ELECCION_ID_NACIONAL],
composicionData.senadores.partidos queryFn: () => getComposicionNacional(ELECCION_ID_NACIONAL),
.filter(p => p.bancasTotales > 0) });
.map(p => p.id)
);
const agrupacionesFiltradas = todasAgrupaciones.filter(a => partidosConBancasIds.has(a.id)); useEffect(() => {
if (!composicionData || !todasAgrupaciones || todasAgrupaciones.length === 0) {
return;
}
agrupacionesFiltradas.sort((a, b) => (a.ordenSenadoresNacionales || 999) - (b.ordenSenadoresNacionales || 999)); // Creamos un Set con los IDs de los partidos que tienen al menos una banca de senador
const partidosConBancasIds = new Set(
setAgrupacionesOrdenadas(agrupacionesFiltradas); composicionData.senadores.partidos
.filter(p => p.bancasTotales > 0)
}, [todasAgrupaciones, composicionData]); .map(p => p.id)
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
); );
const handleDragEnd = (event: DragEndEvent) => { const agrupacionesFiltradas = todasAgrupaciones.filter(a => partidosConBancasIds.has(a.id));
const { active, over } = event;
if (over && active.id !== over.id) {
setAgrupacionesOrdenadas((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
const handleSaveOrder = async () => { agrupacionesFiltradas.sort((a, b) => (a.ordenSenadoresNacionales || 999) - (b.ordenSenadoresNacionales || 999));
const idsOrdenados = agrupacionesOrdenadas.map(a => a.id);
try {
await updateOrden('senadores-nacionales', idsOrdenados);
alert('Orden de Senadores Nacionales guardado con éxito!');
} catch (error) {
alert('Error al guardar el orden de Senadores Nacionales.');
}
};
const isLoading = isLoadingAgrupaciones || isLoadingComposicion; setAgrupacionesOrdenadas(agrupacionesFiltradas);
if (isLoading) return <p>Cargando orden de Senadores Nacionales...</p>;
return ( }, [todasAgrupaciones, composicionData]);
<div className="admin-module">
<h3>Ordenar Agrupaciones (Senado de la Nación)</h3> const sensors = useSensors(
<p>Arrastre para reordenar. Solo se muestran los partidos con bancas.</p> useSensor(PointerSensor),
<p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p> useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> );
<SortableContext items={agrupacionesOrdenadas.map(a => a.id)} strategy={horizontalListSortingStrategy}>
<ul className="sortable-list-horizontal"> const handleDragEnd = (event: DragEndEvent) => {
{agrupacionesOrdenadas.map(agrupacion => ( const { active, over } = event;
<SortableItem key={agrupacion.id} id={agrupacion.id}> if (over && active.id !== over.id) {
{agrupacion.nombreCorto || agrupacion.nombre} setAgrupacionesOrdenadas((items) => {
</SortableItem> const oldIndex = items.findIndex((item) => item.id === active.id);
))} const newIndex = items.findIndex((item) => item.id === over.id);
</ul> return arrayMove(items, oldIndex, newIndex);
</SortableContext> });
</DndContext> }
<button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden</button> };
</div>
); const handleSaveOrder = async () => {
const idsOrdenados = agrupacionesOrdenadas.map(a => a.id);
try {
await updateOrden('senadores-nacionales', idsOrdenados);
alert('Orden de Senadores Nacionales guardado con éxito!');
} catch (error) {
alert('Error al guardar el orden de Senadores Nacionales.');
}
};
const isLoading = isLoadingAgrupaciones || isLoadingComposicion;
if (isLoading) return <p>Cargando orden de Senadores Nacionales...</p>;
return (
<div className="admin-module">
<h3>Ordenar Agrupaciones (Senado de la Nación)</h3>
<p>Arrastre para reordenar. Solo se muestran los partidos con bancas.</p>
<p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={agrupacionesOrdenadas.map(a => a.id)} strategy={horizontalListSortingStrategy}>
<ul className="sortable-list-horizontal">
{agrupacionesOrdenadas.map(agrupacion => (
<SortableItem key={agrupacion.id} id={agrupacion.id}>
{`(${agrupacion.id}) ${agrupacion.nombreCorto || agrupacion.nombre}`}
</SortableItem>
))}
</ul>
</SortableContext>
</DndContext>
<button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden</button>
</div>
);
}; };

View File

@@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2b7fb927e2f0d9ff06dffa820bc9809d6e138b01")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+069446b90326f5acee630bf3b88238e4e054764f")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]