Files
Inventario-IT/frontend/src/components/GestionComponentes.tsx
2025-10-09 11:32:56 -03:00

263 lines
9.8 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.

// frontend/src/components/GestionComponentes.tsx
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';
// Interfaces para los diferentes tipos de datos
interface TextValue {
valor: string;
conteo: number;
}
interface RamValue {
fabricante?: string;
tamano: number;
velocidad?: number;
conteo: number;
}
type ComponentValue = TextValue | RamValue;
const GestionComponentes = () => {
const [componentType, setComponentType] = useState('os');
const [valores, setValores] = useState<ComponentValue[]>([]);
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<SortingState>([]);
useEffect(() => {
setIsLoading(true);
adminService.getComponentValues(componentType)
.then(data => {
setValores(data);
})
.catch(_err => {
toast.error(`No se pudieron cargar los datos de ${componentType}.`);
})
.finally(() => setIsLoading(false));
}, [componentType]);
const handleOpenModal = useCallback((valor: string) => {
setValorAntiguo(valor);
setValorNuevo(valor);
setIsModalOpen(true);
}, []);
const handleUnificar = async () => {
const toastId = toast.loading('Unificando valores...');
try {
await adminService.unifyComponentValues(componentType, valorAntiguo, valorNuevo);
const refreshedData = await adminService.getComponentValues(componentType);
setValores(refreshedData);
toast.success('Valores unificados correctamente.', { id: toastId });
setIsModalOpen(false);
} catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId });
}
};
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 {
await adminService.deleteRamComponent({
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);
}));
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 = useCallback(async (valor: string) => {
if (!window.confirm(`Este valor ya no está en uso. ¿Quieres eliminarlo de la base de datos maestra?`)) {
return;
}
const toastId = toast.loading('Eliminando valor...');
try {
await adminService.deleteTextComponent(componentType, valor);
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 });
}
}, [componentType]);
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 (
<div style={{ display: 'flex', gap: '5px' }}>
{componentType === 'ram' ? (
<button
onClick={() => handleDeleteRam(item as RamValue)}
className={styles.deleteUserButton}
style={{ fontSize: '0.9em', border: '1px solid', borderRadius: '4px', padding: '4px 8px', cursor: item.conteo > 0 ? 'not-allowed' : 'pointer', opacity: item.conteo > 0 ? 0.5 : 1 }}
disabled={item.conteo > 0}
title={item.conteo > 0 ? `En uso por ${item.conteo} equipo(s)` : 'Eliminar este grupo de módulos maestros'}
>
🗑 Eliminar
</button>
) : (
<>
<button onClick={() => handleOpenModal((item as TextValue).valor)} className={styles.tableButton}>
Unificar
</button>
<button
onClick={() => handleDeleteTexto((item as TextValue).valor)}
className={styles.deleteUserButton}
style={{ fontSize: '0.9em', border: '1px solid', borderRadius: '4px', padding: '4px 8px', cursor: item.conteo > 0 ? 'not-allowed' : 'pointer', opacity: item.conteo > 0 ? 0.5 : 1 }}
disabled={item.conteo > 0}
title={item.conteo > 0 ? `En uso por ${item.conteo} equipo(s). Unifique primero.` : 'Eliminar este valor'}
>
🗑 Eliminar
</button>
</>
)}
</div>
);
}
}
], [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 (
<div>
<h2>Gestión de Componentes Maestros ({table.getFilteredRowModel().rows.length})</h2>
<p>Unifica valores inconsistentes y elimina registros no utilizados.</p>
<p style={{ fontSize: '12px', fontWeight: 'bold' }}>** La Tabla permite ordenar por multiple columnas manteniendo shift al hacer click en la cabecera. **</p>
<div className={styles.controlsContainer}>
<input
type="text"
placeholder="Filtrar registros..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className={styles.searchInput}
style={{ width: '300px' }}
/>
<label><strong>Tipo de componente:</strong></label>
<select value={componentType} onChange={e => setComponentType(e.target.value)} className={styles.sectorSelect}>
<option value="os">Sistema Operativo</option>
<option value="cpu">CPU</option>
<option value="motherboard">Motherboard</option>
<option value="architecture">Arquitectura</option>
<option value="ram">Memorias RAM</option>
</select>
</div>
{isLoading ? (
<p>Cargando...</p>
) : (
<div style={{ overflowX: 'auto', border: '1px solid #dee2e6', borderRadius: '8px', marginTop: '1rem' }}>
<table className={styles.table}>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} className={styles.th} onClick={header.column.getToggleSortingHandler()}>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() && (
<span className={styles.sortIndicator}>
{header.column.getIsSorted() === 'asc' ? '⇑' : '⇓'}
</span>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id} className={styles.tr}>
{row.getVisibleCells().map(cell => (
<td key={cell.id} className={styles.td}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
{isModalOpen && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<h3>Unificar Valor</h3>
<p>Se reemplazarán todas las instancias de:</p>
<strong style={{ display: 'block', marginBottom: '1rem', background: '#e9ecef', padding: '8px', borderRadius: '4px' }}>{valorAntiguo}</strong>
<label>Por el nuevo valor:</label>
<input type="text" value={valorNuevo} onChange={e => setValorNuevo(e.target.value)} className={styles.modalInput} />
<div className={styles.modalActions}>
<button onClick={handleUnificar} className={`${styles.btn} ${styles.btnPrimary}`} disabled={!valorNuevo.trim() || valorNuevo === valorAntiguo}>Unificar</button>
<button onClick={() => setIsModalOpen(false)} className={`${styles.btn} ${styles.btnSecondary}`}>Cancelar</button>
</div>
</div>
</div>
)}
</div>
);
};
export default GestionComponentes;