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; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user