Files
Inventario-IT/frontend/src/components/GestionSectores.tsx
dmolinari 242c1345c0 feat: Implementación de gestión manual y panel de administración
Se introduce una refactorización masiva y se añaden nuevas funcionalidades críticas para la gestión del inventario, incluyendo un panel de administración para la limpieza de datos y un sistema completo para la gestión manual de equipos.

### Nuevas Funcionalidades

*   **Panel de Administración:** Se crea una nueva vista de "Administración" para la gestión de datos maestros. Permite unificar valores inconsistentes (ej: "W10" -> "Windows 10 Pro") y eliminar registros maestros no utilizados (ej: Módulos de RAM) para mantener la base de datos limpia.

*   **Gestión de Sectores (CRUD):** Se añade una vista dedicada para crear, editar y eliminar sectores de la organización.

*   **Diferenciación Manual vs. Automático:** Se introduce una columna `origen` en la base de datos para distinguir entre los datos recopilados automáticamente por el script y los introducidos manualmente por el usuario. La UI ahora refleja visualmente este origen.

*   **CRUD de Equipos Manuales:** Se implementa la capacidad de crear, editar y eliminar equipos de origen "manual" a través de la interfaz de usuario. Se protege la eliminación de equipos automáticos.

*   **Gestión de Componentes Manuales:** Se permite añadir y eliminar componentes (Discos, RAM, Usuarios) a los equipos de origen "manual".

### Mejoras de UI/UX

*   **Refactorización de Estilos:** Se migran todos los estilos en línea del componente `SimpleTable` a un archivo CSS Module (`SimpleTable.module.css`), mejorando la mantenibilidad y el rendimiento.

*   **Notificaciones de Usuario:** Se integra `react-hot-toast` para proporcionar feedback visual inmediato (carga, éxito, error) en todas las operaciones asíncronas, reemplazando los `alert`.

*   **Componentización:** Se extraen todos los modales (`ModalDetallesEquipo`, `ModalAnadirEquipo`, etc.) a sus propios componentes, limpiando y simplificando drásticamente el componente `SimpleTable`.

*   **Paginación en Tabla Principal:** Se implementa paginación completa en la tabla de equipos, con controles para navegar, ir a una página específica y cambiar el número de items por página. Se añade un indicador de carga inicial.

*   **Navegación Mejorada:** Se reemplaza la navegación por botones con un componente `Navbar` estilizado y dedicado, mejorando la estructura visual y de código.

*   **Autocompletado de Datos:** Se introduce un componente `AutocompleteInput` reutilizable para guiar al usuario a usar datos consistentes al rellenar campos como OS, CPU y Motherboard. Se implementa búsqueda dinámica para la asociación de usuarios.

*   **Validación de MAC Address:** Se añade validación de formato en tiempo real y auto-formateo para el campo de MAC Address, reduciendo errores humanos.

*   **Consistencia de Iconos:** Se unifica el icono de eliminación a (🗑️) en toda la aplicación para una experiencia de usuario más coherente.

### Mejoras en el Backend / API

*   **Seguridad de Credenciales:** Las credenciales SSH para la función Wake On Lan se mueven del código fuente a `appsettings.json`.

*   **Nuevo `AdminController`:** Se crea un controlador dedicado para las tareas administrativas, con endpoints para obtener valores únicos de componentes y para ejecutar la lógica de unificación y eliminación.

*   **Endpoints de Gestión Manual:** Se añaden rutas específicas (`/manual/...` y `/asociacion/...`) para la manipulación de datos de origen manual, separando la lógica de la gestión automática.

*   **Protección de Datos Automáticos:** Los endpoints `DELETE` y `PUT` ahora validan el campo `origen` para prevenir la modificación o eliminación no deseada de datos generados automáticamente.

*   **Correcciones y Refinamiento:** Se soluciona el mapeo incorrecto de fechas (`created_at`, `updated_at`), se corrigen errores de compilación y se refinan las consultas SQL para incluir los nuevos campos.
2025-10-07 14:44:16 -03:00

142 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import toast from 'react-hot-toast';
import type { Sector } from '../types/interfaces';
import styles from './SimpleTable.module.css';
import ModalSector from './ModalSector';
const BASE_URL = 'http://localhost:5198/api';
const GestionSectores = () => {
const [sectores, setSectores] = useState<Sector[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingSector, setEditingSector] = useState<Sector | null>(null);
useEffect(() => {
fetch(`${BASE_URL}/sectores`)
.then(res => res.json())
.then((data: Sector[]) => {
setSectores(data);
setIsLoading(false);
})
.catch(err => {
toast.error("No se pudieron cargar los sectores.");
console.error(err);
setIsLoading(false);
});
}, []);
const handleOpenCreateModal = () => {
setEditingSector(null); // Poner en modo 'crear'
setIsModalOpen(true);
};
const handleOpenEditModal = (sector: Sector) => {
setEditingSector(sector); // Poner en modo 'editar' con los datos del sector
setIsModalOpen(true);
};
const handleSave = async (id: number | null, nombre: string) => {
const isEditing = id !== null;
const url = isEditing ? `${BASE_URL}/sectores/${id}` : `${BASE_URL}/sectores`;
const method = isEditing ? 'PUT' : 'POST';
const toastId = toast.loading(isEditing ? 'Actualizando...' : 'Creando...');
try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nombre }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'La operación falló.');
}
if (isEditing) {
// Actualizar el sector en la lista local
setSectores(prev => prev.map(s => s.id === id ? { ...s, nombre } : s));
toast.success('Sector actualizado.', { id: toastId });
} else {
// Añadir el nuevo sector a la lista local
const nuevoSector = await response.json();
setSectores(prev => [...prev, nuevoSector]);
toast.success('Sector creado.', { id: toastId });
}
setIsModalOpen(false); // Cerrar el modal
} catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId });
}
};
const handleDelete = async (id: number) => {
if (!window.confirm("¿Estás seguro de eliminar este sector? Los equipos asociados quedarán sin sector.")) {
return;
}
const toastId = toast.loading('Eliminando...');
try {
const response = await fetch(`${BASE_URL}/sectores/${id}`, { method: 'DELETE' });
if (response.status === 409) {
throw new Error("No se puede eliminar. Hay equipos asignados a este sector.");
}
if (!response.ok) {
throw new Error("El sector no se pudo eliminar.");
}
setSectores(prev => prev.filter(s => s.id !== id));
toast.success("Sector eliminado.", { id: toastId });
} catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId });
}
};
if (isLoading) {
return <div>Cargando sectores...</div>;
}
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
<h2>Gestión de Sectores</h2>
<button onClick={handleOpenCreateModal} className={`${styles.btn} ${styles.btnPrimary}`}>
+ Añadir Sector
</button>
</div>
<table className={styles.table}>
<thead>
<tr>
<th className={styles.th}>Nombre del Sector</th>
<th className={styles.th} style={{ width: '200px' }}>Acciones</th>
</tr>
</thead>
<tbody>
{sectores.map(sector => (
<tr key={sector.id} className={styles.tr}>
<td className={styles.td}>{sector.nombre}</td>
<td className={styles.td}>
<div style={{ display: 'flex', gap: '10px' }}>
<button onClick={() => handleOpenEditModal(sector)} className={styles.tableButton}> Editar</button>
<button onClick={() => handleDelete(sector.id)} className={styles.deleteUserButton} style={{fontSize: '1em', padding: '0.375rem 0.75rem', border: '1px solid #dc3545', borderRadius: '4px'}}>
🗑 Eliminar
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{isModalOpen && (
<ModalSector
sector={editingSector}
onClose={() => setIsModalOpen(false)}
onSave={handleSave}
/>
)}
</div>
);
};
export default GestionSectores;