feat: Partido Politico Manual

This commit is contained in:
2025-10-20 14:38:10 -03:00
parent 99d56033b1
commit a78fcf66c0
12 changed files with 280 additions and 46 deletions

View File

@@ -0,0 +1,79 @@
// src/components/AddAgrupacionForm.tsx
import { useState } from 'react';
import { createAgrupacion } from '../services/apiService';
import type { CreateAgrupacionData } from '../services/apiService';
// Importa el nuevo archivo CSS si lo creaste, o el existente
import './FormStyles.css';
interface Props {
onSuccess: () => void;
}
export const AddAgrupacionForm = ({ onSuccess }: Props) => {
const [nombre, setNombre] = useState('');
const [nombreCorto, setNombreCorto] = useState('');
const [color, setColor] = useState('#000000');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!nombre.trim()) {
setError('El nombre es obligatorio.');
return;
}
setIsLoading(true);
setError('');
const payload: CreateAgrupacionData = {
nombre: nombre.trim(),
nombreCorto: nombreCorto.trim() || null,
color: color,
};
try {
await createAgrupacion(payload);
alert(`Partido '${payload.nombre}' creado con éxito.`);
// Limpiar formulario
setNombre('');
setNombreCorto('');
setColor('#000000');
// Notificar al componente padre para que refresque los datos
onSuccess();
} catch (err: any) {
const errorMessage = err.response?.data?.message || 'Ocurrió un error inesperado.';
setError(errorMessage);
console.error(err);
} finally {
setIsLoading(false);
}
};
return (
<div className="add-entity-form-container">
<h4>Añadir Partido Manualmente</h4>
<form onSubmit={handleSubmit} className="add-entity-form">
<div className="form-field">
<label>Nombre Completo</label>
<input type="text" value={nombre} onChange={e => setNombre(e.target.value)} required />
</div>
<div className="form-field">
<label>Nombre Corto</label>
<input type="text" value={nombreCorto} onChange={e => setNombreCorto(e.target.value)} />
</div>
<div className="form-field">
<label>Color</label>
<input type="color" value={color} onChange={e => setColor(e.target.value)} />
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Guardando...' : 'Guardar Partido'}
</button>
</form>
{error && <p style={{ color: 'red', marginTop: '0.5rem', textAlign: 'left' }}>{error}</p>}
</div>
);
};

View File

@@ -4,6 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import Select from 'react-select';
import { getAgrupaciones, updateAgrupacion, getLogos, updateLogos } from '../services/apiService';
import type { AgrupacionPolitica, LogoAgrupacionCategoria, UpdateAgrupacionData } from '../types';
import { AddAgrupacionForm } from './AddAgrupacionForm';
import './AgrupacionesManager.css';
const GLOBAL_ELECTION_ID = 0;
@@ -28,12 +29,17 @@ export const AgrupacionesManager = () => {
const { data: agrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
queryKey: ['agrupaciones'], queryFn: getAgrupaciones,
});
const { data: logos = [], isLoading: isLoadingLogos } = useQuery<LogoAgrupacionCategoria[]>({
queryKey: ['allLogos'],
queryFn: () => Promise.all([getLogos(0), getLogos(1), getLogos(2)]).then(res => res.flat()),
});
const handleCreationSuccess = () => {
// Invalida la query de agrupaciones para forzar una actualización
queryClient.invalidateQueries({ queryKey: ['agrupaciones'] });
};
useEffect(() => {
if (agrupaciones.length > 0) {
const initialEdits = Object.fromEntries(
@@ -63,7 +69,7 @@ export const AgrupacionesManager = () => {
const key = `${agrupacionId}-${selectedEleccion.value}`;
setEditedLogos(prev => ({ ...prev, [key]: value }));
};
const handleSaveAll = async () => {
try {
const agrupacionPromises = agrupaciones.map(agrupacion => {
@@ -74,7 +80,7 @@ export const AgrupacionesManager = () => {
};
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]) => {
@@ -85,13 +91,13 @@ export const AgrupacionesManager = () => {
const logoPromise = updateLogos(logosPayload);
await Promise.all([...agrupacionPromises, logoPromise]);
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."); }
};
const getLogoValue = (agrupacionId: string): string => {
const key = `${agrupacionId}-${selectedEleccion.value}`;
return editedLogos[key] ?? '';
@@ -101,9 +107,9 @@ export const AgrupacionesManager = () => {
return (
<div className="admin-module">
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3>Gestión de Agrupaciones y Logos</h3>
<div style={{width: '350px', zIndex: 100 }}>
<div style={{ width: '350px', zIndex: 100 }}>
<Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => setSelectedEleccion(opt!)} />
</div>
</div>
@@ -127,11 +133,11 @@ export const AgrupacionesManager = () => {
<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="text"
placeholder="URL..."
value={getLogoValue(agrupacion.id)}
onChange={(e) => handleLogoInputChange(agrupacion.id, e.target.value)}
<input
type="text"
placeholder="URL..."
value={getLogoValue(agrupacion.id)}
onChange={(e) => handleLogoInputChange(agrupacion.id, e.target.value)}
/>
</td>
</tr>
@@ -142,6 +148,7 @@ export const AgrupacionesManager = () => {
<button onClick={handleSaveAll} style={{ marginTop: '1rem' }}>
Guardar Todos los Cambios
</button>
<AddAgrupacionForm onSuccess={handleCreationSuccess} />
</>
)}
</div>

View File

@@ -0,0 +1,72 @@
/* src/components/FormStyles.css */
.add-entity-form-container {
border-top: 2px solid #007bff;
padding-top: 1.5rem;
margin-top: 2rem;
}
.add-entity-form-container h4 {
margin-top: 0;
margin-bottom: 1rem;
color: #333;
}
.add-entity-form {
display: grid;
/* Usamos grid para un control preciso de las columnas */
grid-template-columns: 3fr 2fr 0.5fr auto;
gap: 1rem;
align-items: flex-end; /* Alinea los elementos en la parte inferior de la celda */
}
.form-field {
display: flex;
flex-direction: column;
margin-right: 15px;
}
.form-field label {
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 0.25rem;
color: #555;
text-align: left;
}
.form-field input[type="text"] {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
width: 100%;
}
.form-field input[type="color"] {
height: 38px; /* Misma altura que los inputs de texto */
width: 100%;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px; /* Padding interno para el color */
}
.add-entity-form button {
padding: 8px 16px;
height: 38px; /* Misma altura que los inputs */
border: none;
background-color: #28a745; /* Un color verde para la acción de "crear" */
color: white;
font-weight: bold;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.add-entity-form button:hover:not(:disabled) {
background-color: #218838;
}
.add-entity-form button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}

View File

@@ -1,9 +1,11 @@
// src/services/apiService.ts
import axios from 'axios';
import { triggerLogout } from '../context/authUtils';
import type { CandidatoOverride, AgrupacionPolitica,
import type {
CandidatoOverride, AgrupacionPolitica,
UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria,
MunicipioSimple, BancaPrevia, ProvinciaSimple } from '../types';
MunicipioSimple, BancaPrevia, ProvinciaSimple
} from '../types';
/**
* URL base para las llamadas a la API.
@@ -29,8 +31,8 @@ const adminApiClient = axios.create({
// Cliente de API para endpoints públicos (no envía token)
const apiClient = axios.create({
baseURL: API_URL_BASE,
headers: { 'Content-Type': 'application/json' },
baseURL: API_URL_BASE,
headers: { 'Content-Type': 'application/json' },
});
@@ -60,26 +62,26 @@ adminApiClient.interceptors.response.use(
// --- INTERFACES PARA COMPOSICIÓN NACIONAL (NECESARIAS PARA EL NUEVO MÉTODO) ---
export interface PartidoComposicionNacional {
id: string;
nombre: string;
nombreCorto: string | null;
color: string | null;
bancasFijos: number;
bancasGanadas: number;
bancasTotales: number;
ordenDiputadosNacionales: number | null;
ordenSenadoresNacionales: number | null;
id: string;
nombre: string;
nombreCorto: string | null;
color: string | null;
bancasFijos: number;
bancasGanadas: number;
bancasTotales: number;
ordenDiputadosNacionales: number | null;
ordenSenadoresNacionales: number | null;
}
export interface CamaraComposicionNacional {
camaraNombre: string;
totalBancas: number;
bancasEnJuego: number;
partidos: PartidoComposicionNacional[];
presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null;
camaraNombre: string;
totalBancas: number;
bancasEnJuego: number;
partidos: PartidoComposicionNacional[];
presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null;
}
export interface ComposicionNacionalData {
diputados: CamaraComposicionNacional;
senadores: CamaraComposicionNacional;
diputados: CamaraComposicionNacional;
senadores: CamaraComposicionNacional;
}
@@ -173,22 +175,34 @@ export const updateLoggingLevel = async (data: UpdateLoggingLevelData): Promise<
// 9. Bancas Previas
export const getBancasPrevias = async (eleccionId: number): Promise<BancaPrevia[]> => {
const response = await adminApiClient.get(`/bancas-previas/${eleccionId}`);
return response.data;
const response = await adminApiClient.get(`/bancas-previas/${eleccionId}`);
return response.data;
};
export const updateBancasPrevias = async (eleccionId: number, data: BancaPrevia[]): Promise<void> => {
await adminApiClient.put(`/bancas-previas/${eleccionId}`, data);
await adminApiClient.put(`/bancas-previas/${eleccionId}`, data);
};
// 10. Obtener Composición Nacional (Endpoint Público)
export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => {
// Este es un endpoint público, por lo que usamos el cliente sin token de admin.
const response = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`);
return response.data;
// Este es un endpoint público, por lo que usamos el cliente sin token de admin.
const response = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`);
return response.data;
};
// Obtenemos las provincias para el selector de ámbito
export const getProvinciasForAdmin = async (): Promise<ProvinciaSimple[]> => {
const response = await adminApiClient.get('/catalogos/provincias');
return response.data;
const response = await adminApiClient.get('/catalogos/provincias');
return response.data;
};
export interface CreateAgrupacionData {
nombre: string;
nombreCorto: string | null;
color: string | null;
}
// Servicio para crear una nueva agrupación
export const createAgrupacion = async (data: CreateAgrupacionData): Promise<AgrupacionPolitica> => {
const response = await adminApiClient.post('/agrupaciones', data);
return response.data;
};