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;
}