feat: Partido Politico Manual
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
72
Elecciones-Web/frontend-admin/src/components/FormStyles.css
Normal file
72
Elecciones-Web/frontend-admin/src/components/FormStyles.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user