263 lines
9.8 KiB
TypeScript
263 lines
9.8 KiB
TypeScript
// 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; |