diff --git a/frontend/src/components/GestionComponentes.tsx b/frontend/src/components/GestionComponentes.tsx index 88868ed..0061295 100644 --- a/frontend/src/components/GestionComponentes.tsx +++ b/frontend/src/components/GestionComponentes.tsx @@ -1,6 +1,14 @@ // frontend/src/components/GestionComponentes.tsx -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo, useCallback } from 'react'; import toast from 'react-hot-toast'; +import { + useReactTable, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + flexRender, + type SortingState, +} from '@tanstack/react-table'; import styles from './SimpleTable.module.css'; import { adminService } from '../services/apiService'; @@ -17,14 +25,20 @@ interface RamValue { conteo: number; } +type ComponentValue = TextValue | RamValue; + const GestionComponentes = () => { const [componentType, setComponentType] = useState('os'); - const [valores, setValores] = useState<(TextValue | RamValue)[]>([]); + const [valores, setValores] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [valorAntiguo, setValorAntiguo] = useState(''); const [valorNuevo, setValorNuevo] = useState(''); + // Estados para la tabla (filtrado y ordenamiento) + const [globalFilter, setGlobalFilter] = useState(''); + const [sorting, setSorting] = useState([]); + useEffect(() => { setIsLoading(true); adminService.getComponentValues(componentType) @@ -37,11 +51,11 @@ const GestionComponentes = () => { .finally(() => setIsLoading(false)); }, [componentType]); - const handleOpenModal = (valor: string) => { + const handleOpenModal = useCallback((valor: string) => { setValorAntiguo(valor); setValorNuevo(valor); setIsModalOpen(true); - }; + }, []); const handleUnificar = async () => { const toastId = toast.loading('Unificando valores...'); @@ -56,36 +70,34 @@ const GestionComponentes = () => { } }; - // 2. FUNCIÓN DELETE ACTUALIZADA: Ahora maneja un grupo - const handleDeleteRam = async (ramGroup: RamValue) => { + const handleDeleteRam = useCallback(async (ramGroup: RamValue) => { if (!window.confirm("¿Estás seguro de eliminar todas las entradas maestras para este tipo de RAM? Esta acción es irreversible.")) { return; } const toastId = toast.loading('Eliminando grupo de módulos...'); try { - // El servicio ahora espera el objeto del grupo await adminService.deleteRamComponent({ - fabricante: ramGroup.fabricante, - tamano: ramGroup.tamano, - velocidad: ramGroup.velocidad + fabricante: ramGroup.fabricante, + tamano: ramGroup.tamano, + velocidad: ramGroup.velocidad }); setValores(prev => prev.filter(v => { - const currentRam = v as RamValue; - return !(currentRam.fabricante === ramGroup.fabricante && - currentRam.tamano === ramGroup.tamano && - currentRam.velocidad === ramGroup.velocidad); + const currentRam = v as RamValue; + return !(currentRam.fabricante === ramGroup.fabricante && + currentRam.tamano === ramGroup.tamano && + currentRam.velocidad === ramGroup.velocidad); })); toast.success("Grupo de módulos de RAM eliminado.", { id: toastId }); } catch (error) { if (error instanceof Error) toast.error(error.message, { id: toastId }); } - }; + }, []); - const handleDeleteTexto = async (valor: string) => { + const handleDeleteTexto = useCallback(async (valor: string) => { if (!window.confirm(`Este valor ya no está en uso. ¿Quieres eliminarlo de la base de datos maestra?`)) { - return; + return; } const toastId = toast.loading('Eliminando valor...'); try { @@ -93,103 +105,159 @@ const GestionComponentes = () => { setValores(prev => prev.filter(v => (v as TextValue).valor !== valor)); toast.success("Valor eliminado.", { id: toastId }); } catch (error) { - if (error instanceof Error) toast.error(error.message, { id: toastId }); + if (error instanceof Error) toast.error(error.message, { id: toastId }); } - }; + }, [componentType]); - const renderValor = (item: TextValue | RamValue) => { + const renderValor = useCallback((item: ComponentValue) => { if (componentType === 'ram') { const ram = item as RamValue; return `${ram.fabricante || 'Desconocido'} ${ram.tamano}GB ${ram.velocidad ? ram.velocidad + 'MHz' : ''}`; } return (item as TextValue).valor; - }; + }, [componentType]); + + const columns = useMemo(() => [ + { + header: 'Valor Registrado', + id: 'valor', + accessorFn: (row: ComponentValue) => renderValor(row), + }, + { + header: 'Nº de Equipos', + accessorKey: 'conteo', + }, + { + header: 'Acciones', + id: 'acciones', + cell: ({ row }: { row: { original: ComponentValue } }) => { + const item = row.original; + return ( +
+ {componentType === 'ram' ? ( + + ) : ( + <> + + + + )} +
+ ); + } + } + ], [componentType, renderValor, handleDeleteRam, handleDeleteTexto, handleOpenModal]); + + const table = useReactTable({ + data: valores, + columns, + state: { + sorting, + globalFilter, + }, + onSortingChange: setSorting, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); return ( -
-

Gestión de Componentes Maestros

-

Unifica valores inconsistentes y elimina registros no utilizados.

+
+

Gestión de Componentes Maestros ({table.getFilteredRowModel().rows.length})

+

Unifica valores inconsistentes y elimina registros no utilizados.

+

** La Tabla permite ordenar por multiple columnas manteniendo shift al hacer click en la cabecera. **

-
- - -
- - {isLoading ? ( -

Cargando...

- ) : ( - - - - - - - - - - {valores.map((item) => ( - - - - - - ))} - -
Valor RegistradoNº de EquiposAcciones
{renderValor(item)}{item.conteo} -
- {componentType === 'ram' ? ( - - ) : ( - <> - - - - )} -
-
- )} +
+ setGlobalFilter(e.target.value)} + className={styles.searchInput} + style={{ width: '300px' }} + /> + + +
- {isModalOpen && ( -
-
-

Unificar Valor

-

Se reemplazarán todas las instancias de:

- {valorAntiguo} - - setValorNuevo(e.target.value)} className={styles.modalInput} /> -
- - -
-
-
- )} + {isLoading ? ( +

Cargando...

+ ) : ( +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ))} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {header.column.getIsSorted() && ( + + {header.column.getIsSorted() === 'asc' ? '⇑' : '⇓'} + + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
- ); + )} + + {isModalOpen && ( +
+
+

Unificar Valor

+

Se reemplazarán todas las instancias de:

+ {valorAntiguo} + + setValorNuevo(e.target.value)} className={styles.modalInput} /> +
+ + +
+
+
+ )} +
+ ); }; export default GestionComponentes; \ No newline at end of file diff --git a/frontend/src/components/SimpleTable.tsx b/frontend/src/components/SimpleTable.tsx index 1586251..bc7b41b 100644 --- a/frontend/src/components/SimpleTable.tsx +++ b/frontend/src/components/SimpleTable.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'; import { useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel, - getPaginationRowModel, flexRender, type CellContext + getPaginationRowModel, flexRender, type CellContext, + type ColumnDef } from '@tanstack/react-table'; import { Tooltip } from 'react-tooltip'; import toast from 'react-hot-toast'; @@ -125,13 +126,18 @@ const SimpleTable = () => { }; const handleSave = async () => { - if (!modalData || !modalData.sector) return; + if (!modalData) return; const toastId = toast.loading('Guardando...'); try { - await equipoService.updateSector(modalData.id, modalData.sector.id); - const updatedData = data.map(e => e.id === modalData.id ? { ...e, sector: modalData.sector } : e); - setData(updatedData); - setFilteredData(updatedData); + const sectorId = modalData.sector?.id ?? 0; + await equipoService.updateSector(modalData.id, sectorId); + const equipoActualizado = { ...modalData, sector_id: modalData.sector?.id }; + const updateFunc = (prev: Equipo[]) => prev.map(e => e.id === modalData.id ? equipoActualizado : e); + setData(updateFunc); + setFilteredData(updateFunc); + if (selectedEquipo && selectedEquipo.id === modalData.id) { + setSelectedEquipo(equipoActualizado); + } toast.success('Sector actualizado.', { id: toastId }); setModalData(null); } catch (error) { @@ -143,28 +149,57 @@ const SimpleTable = () => { if (!modalPasswordData) return; const toastId = toast.loading('Actualizando...'); try { - const updatedUser = await usuarioService.updatePassword(modalPasswordData.id, password); - const updatedData = data.map(equipo => ({ - ...equipo, - usuarios: equipo.usuarios?.map(user => user.id === updatedUser.id ? { ...user, password } : user) - })); - setData(updatedData); - setFilteredData(updatedData); - toast.success(`Contraseña actualizada.`, { id: toastId }); + await usuarioService.updatePassword(modalPasswordData.id, password); + + const usernameToUpdate = modalPasswordData.username; + + const newData = data.map(equipo => { + if (!equipo.usuarios.some(u => u.username === usernameToUpdate)) { + return equipo; + } + const updatedUsers = equipo.usuarios.map(user => + user.username === usernameToUpdate ? { ...user, password: password } : user + ); + return { ...equipo, usuarios: updatedUsers }; + }); + setData(newData); + if (selectedSector === 'Todos') setFilteredData(newData); + else if (selectedSector === 'Asignar') setFilteredData(newData.filter(i => !i.sector)); + else setFilteredData(newData.filter(i => i.sector?.nombre === selectedSector)); + if (selectedEquipo) { + const updatedSelectedEquipo = newData.find(e => e.id === selectedEquipo.id); + if (updatedSelectedEquipo) { + setSelectedEquipo(updatedSelectedEquipo); + } + } + + toast.success(`Contraseña para '${usernameToUpdate}' actualizada en todos sus equipos.`, { id: toastId }); setModalPasswordData(null); + } catch (error) { if (error instanceof Error) toast.error(error.message, { id: toastId }); } }; + const handleRemoveUser = async (hostname: string, username: string) => { if (!window.confirm(`¿Quitar a ${username} de este equipo?`)) return; const toastId = toast.loading(`Quitando a ${username}...`); try { await usuarioService.removeUserFromEquipo(hostname, username); - const updateFunc = (prev: Equipo[]) => prev.map(e => e.hostname === hostname ? { ...e, usuarios: e.usuarios.filter(u => u.username !== username) } : e); + let equipoActualizado: Equipo | undefined; + const updateFunc = (prev: Equipo[]) => prev.map(e => { + if (e.hostname === hostname) { + equipoActualizado = { ...e, usuarios: e.usuarios.filter(u => u.username !== username) }; + return equipoActualizado; + } + return e; + }); setData(updateFunc); setFilteredData(updateFunc); + if (selectedEquipo && equipoActualizado && selectedEquipo.id === equipoActualizado.id) { + setSelectedEquipo(equipoActualizado); + } toast.success(`${username} quitado.`, { id: toastId }); } catch (error) { if (error instanceof Error) toast.error(error.message, { id: toastId }); @@ -188,7 +223,6 @@ const SimpleTable = () => { const handleRemoveAssociation = async (type: 'disco' | 'ram' | 'usuario', associationId: number | { equipoId: number, usuarioId: number }) => { if (!window.confirm('¿Estás seguro de que quieres eliminar esta asociación manual?')) return; - const toastId = toast.loading('Eliminando asociación...'); try { let successMessage = ''; @@ -204,7 +238,6 @@ const SimpleTable = () => { } else { throw new Error('Tipo de asociación no válido'); } - const updateState = (prevEquipos: Equipo[]) => prevEquipos.map(equipo => { if (equipo.id !== selectedEquipo?.id) return equipo; let updatedEquipo = { ...equipo }; @@ -217,11 +250,9 @@ const SimpleTable = () => { } return updatedEquipo; }); - setData(updateState); setFilteredData(updateState); setSelectedEquipo(prev => prev ? updateState([prev])[0] : null); - if (selectedEquipo) { await refreshHistory(selectedEquipo.hostname); } @@ -248,13 +279,10 @@ const SimpleTable = () => { const toastId = toast.loading('Guardando cambios...'); try { const equipoActualizadoDesdeBackend = await equipoService.updateManual(id, equipoEditado); - const updateState = (prev: Equipo[]) => - prev.map(e => e.id === id ? equipoActualizadoDesdeBackend : e); - + const updateState = (prev: Equipo[]) => prev.map(e => e.id === id ? equipoActualizadoDesdeBackend : e); setData(updateState); setFilteredData(updateState); setSelectedEquipo(equipoActualizadoDesdeBackend); - toast.success('Equipo actualizado.', { id: toastId }); return true; } catch (error) { @@ -275,30 +303,26 @@ const SimpleTable = () => { default: throw new Error('Tipo de componente no válido'); } await serviceCall; - - // Usar el servicio directamente para obtener el equipo actualizado const refreshedEquipo = await equipoService.getAll().then(equipos => equipos.find(e => e.id === selectedEquipo.id)); if (!refreshedEquipo) throw new Error("No se pudo recargar el equipo"); - const updateState = (prev: Equipo[]) => prev.map(e => e.id === selectedEquipo.id ? refreshedEquipo : e); setData(updateState); setFilteredData(updateState); setSelectedEquipo(refreshedEquipo); - await refreshHistory(selectedEquipo.hostname); - toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} añadido.`, { id: toastId }); setAddingComponent(null); } catch (error) { if (error instanceof Error) toast.error(error.message, { id: toastId }); } }; - - const columns = [ + + // --- DEFINICIÓN DE COLUMNAS CORREGIDA --- + const columns: ColumnDef[] = [ { header: "ID", accessorKey: "id", enableHiding: true }, { header: "Nombre", accessorKey: "hostname", - cell: ({ row }: CellContext) => () + cell: (info: CellContext) => () }, { header: "IP", accessorKey: "ip" }, { header: "MAC", accessorKey: "mac", enableHiding: true }, @@ -310,7 +334,8 @@ const SimpleTable = () => { { header: "Arquitectura", accessorKey: "architecture" }, { header: "Usuarios y Claves", - cell: ({ row }: CellContext) => { + cell: (info: CellContext) => { + const { row } = info; const usuarios = row.original.usuarios || []; return (
@@ -347,12 +372,13 @@ const SimpleTable = () => { }, { header: "Sector", id: 'sector', accessorFn: (row: Equipo) => row.sector?.nombre || 'Asignar', - cell: ({ row }: CellContext) => { + cell: (info: CellContext) => { + const { row } = info; const sector = row.original.sector; return (
{sector?.nombre || 'Asignar'} - +
); }