Feat Modo Oscuro y Otras Estéticas
This commit is contained in:
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -2746,6 +2747,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.545.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz",
|
||||||
|
"integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ main {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos para la nueva Barra de Navegación */
|
|
||||||
.navbar {
|
.navbar {
|
||||||
background-color: #343a40; /* Un color oscuro para el fondo */
|
background-color: var(--color-navbar-bg);
|
||||||
padding: 0 2rem;
|
padding: 0 2rem;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--color-border); /* Borde sutil */
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links {
|
.nav-links {
|
||||||
@@ -21,27 +21,27 @@ main {
|
|||||||
.nav-link {
|
.nav-link {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #adb5bd; /* Color de texto gris claro */
|
color: var(--color-navbar-text);
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
|
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
||||||
border-bottom: 3px solid transparent; /* Borde inferior para el indicador activo */
|
border-bottom: 3px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover {
|
.nav-link:hover {
|
||||||
color: #ffffff; /* Texto blanco al pasar el ratón */
|
color: var(--color-navbar-text-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link-active {
|
.nav-link-active {
|
||||||
color: #ffffff;
|
color: var(--color-navbar-text-hover);
|
||||||
border-bottom: 3px solid #007bff; /* Indicador azul para la vista activa */
|
border-bottom: 3px solid var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-title {
|
.app-title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: #ffffff;
|
color: var(--color-navbar-text-hover);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
.dashboardHeader {
|
.dashboardHeader {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
border-bottom: 1px solid #dee2e6;
|
border-bottom: 1px solid var(--color-border);
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #343a40;
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.statsGrid {
|
.statsGrid {
|
||||||
@@ -19,14 +19,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chartContainer {
|
.chartContainer {
|
||||||
background-color: #ffffff;
|
background-color: var(--color-surface);
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid var(--color-border-subtle);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 450px; /* Altura mínima para todos los gráficos */
|
min-height: 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Contenedor especial para los gráficos de barras horizontales con scroll */
|
/* Contenedor especial para los gráficos de barras horizontales con scroll */
|
||||||
@@ -37,6 +37,6 @@
|
|||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.statsGrid {
|
.statsGrid {
|
||||||
grid-template-columns: 1fr; /* Una sola columna en pantallas pequeñas */
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// frontend/src/components/Dashboard.tsx
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { dashboardService } from '../services/apiService';
|
import { dashboardService } from '../services/apiService';
|
||||||
@@ -5,8 +6,9 @@ import type { DashboardStats } from '../types/interfaces';
|
|||||||
import OsChart from './OsChart';
|
import OsChart from './OsChart';
|
||||||
import SectorChart from './SectorChart';
|
import SectorChart from './SectorChart';
|
||||||
import CpuChart from './CpuChart';
|
import CpuChart from './CpuChart';
|
||||||
import RamChart from './RamChart'; // <-- 1. Importar el nuevo gráfico
|
import RamChart from './RamChart';
|
||||||
import styles from './Dashboard.module.css';
|
import styles from './Dashboard.module.css';
|
||||||
|
import skeletonStyles from './Skeleton.module.css';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||||
@@ -24,13 +26,24 @@ const Dashboard = () => {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
<div className={styles.dashboardHeader}>
|
<div className={styles.dashboardHeader}>
|
||||||
<h2>Cargando estadísticas...</h2>
|
<h2>Dashboard de Inventario</h2>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
{/* Replicamos la estructura de la grilla con esqueletos */}
|
||||||
|
<div className={`${styles.chartContainer} ${skeletonStyles.skeleton}`}></div>
|
||||||
|
<div className={`${styles.chartContainer} ${skeletonStyles.skeleton}`}></div>
|
||||||
|
<div className={`${styles.chartContainer} ${skeletonStyles.skeleton}`}></div>
|
||||||
|
<div className={`${styles.chartContainer} ${skeletonStyles.skeleton}`}></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.dashboardHeader}>
|
<div className={styles.dashboardHeader}>
|
||||||
|
|||||||
@@ -9,22 +9,22 @@ import {
|
|||||||
flexRender,
|
flexRender,
|
||||||
type SortingState,
|
type SortingState,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
import styles from './SimpleTable.module.css';
|
import styles from './SimpleTable.module.css';
|
||||||
import { adminService } from '../services/apiService';
|
import { adminService } from '../services/apiService';
|
||||||
|
import TableSkeleton from './TableSkeleton';
|
||||||
|
|
||||||
// Interfaces para los diferentes tipos de datos
|
// Interfaces
|
||||||
interface TextValue {
|
interface TextValue {
|
||||||
valor: string;
|
valor: string;
|
||||||
conteo: number;
|
conteo: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RamValue {
|
interface RamValue {
|
||||||
fabricante?: string;
|
fabricante?: string;
|
||||||
tamano: number;
|
tamano: number;
|
||||||
velocidad?: number;
|
velocidad?: number;
|
||||||
conteo: number;
|
conteo: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComponentValue = TextValue | RamValue;
|
type ComponentValue = TextValue | RamValue;
|
||||||
|
|
||||||
const GestionComponentes = () => {
|
const GestionComponentes = () => {
|
||||||
@@ -34,8 +34,6 @@ const GestionComponentes = () => {
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [valorAntiguo, setValorAntiguo] = useState('');
|
const [valorAntiguo, setValorAntiguo] = useState('');
|
||||||
const [valorNuevo, setValorNuevo] = useState('');
|
const [valorNuevo, setValorNuevo] = useState('');
|
||||||
|
|
||||||
// Estados para la tabla (filtrado y ordenamiento)
|
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
@@ -71,23 +69,13 @@ const GestionComponentes = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRam = useCallback(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.")) {
|
if (!window.confirm("¿Estás seguro de eliminar todas las entradas maestras para este tipo de RAM? Esta acción es irreversible.")) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastId = toast.loading('Eliminando grupo de módulos...');
|
const toastId = toast.loading('Eliminando grupo de módulos...');
|
||||||
try {
|
try {
|
||||||
await adminService.deleteRamComponent({
|
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 => {
|
setValores(prev => prev.filter(v => {
|
||||||
const currentRam = v as RamValue;
|
const currentRam = v as RamValue;
|
||||||
return !(currentRam.fabricante === ramGroup.fabricante &&
|
return !(currentRam.fabricante === ramGroup.fabricante && currentRam.tamano === ramGroup.tamano && currentRam.velocidad === ramGroup.velocidad);
|
||||||
currentRam.tamano === ramGroup.tamano &&
|
|
||||||
currentRam.velocidad === ramGroup.velocidad);
|
|
||||||
}));
|
}));
|
||||||
toast.success("Grupo de módulos de RAM eliminado.", { id: toastId });
|
toast.success("Grupo de módulos de RAM eliminado.", { id: toastId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -96,9 +84,7 @@ const GestionComponentes = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDeleteTexto = useCallback(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?`)) {
|
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...');
|
const toastId = toast.loading('Eliminando valor...');
|
||||||
try {
|
try {
|
||||||
await adminService.deleteTextComponent(componentType, valor);
|
await adminService.deleteTextComponent(componentType, valor);
|
||||||
@@ -130,49 +116,43 @@ const GestionComponentes = () => {
|
|||||||
{
|
{
|
||||||
header: 'Acciones',
|
header: 'Acciones',
|
||||||
id: 'acciones',
|
id: 'acciones',
|
||||||
cell: ({ row }: { row: { original: ComponentValue } }) => {
|
cell: ({ row }: { row: { original: ComponentValue } }) => (
|
||||||
const item = row.original;
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', gap: '5px' }}>
|
<div style={{ display: 'flex', gap: '5px' }}>
|
||||||
{componentType === 'ram' ? (
|
{componentType === 'ram' ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteRam(item as RamValue)}
|
onClick={() => handleDeleteRam(row.original as RamValue)}
|
||||||
className={styles.deleteUserButton}
|
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 }}
|
style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.9em', border: '1px solid', borderRadius: '4px', padding: '4px 8px' }}
|
||||||
disabled={item.conteo > 0}
|
disabled={row.original.conteo > 0}
|
||||||
title={item.conteo > 0 ? `En uso por ${item.conteo} equipo(s)` : 'Eliminar este grupo de módulos maestros'}
|
title={row.original.conteo > 0 ? `En uso por ${row.original.conteo} equipo(s)` : 'Eliminar este grupo de módulos maestros'}
|
||||||
>
|
>
|
||||||
🗑️ Eliminar
|
<Trash2 size={14} /> Eliminar
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button onClick={() => handleOpenModal((item as TextValue).valor)} className={styles.tableButton}>
|
<button onClick={() => handleOpenModal((row.original as TextValue).valor)} className={styles.tableButton} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
✏️ Unificar
|
<Pencil size={14} /> Unificar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteTexto((item as TextValue).valor)}
|
onClick={() => handleDeleteTexto((row.original as TextValue).valor)}
|
||||||
className={styles.deleteUserButton}
|
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 }}
|
style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.9em', border: '1px solid', borderRadius: '4px', padding: '4px 8px' }}
|
||||||
disabled={item.conteo > 0}
|
disabled={row.original.conteo > 0}
|
||||||
title={item.conteo > 0 ? `En uso por ${item.conteo} equipo(s). Unifique primero.` : 'Eliminar este valor'}
|
title={row.original.conteo > 0 ? `En uso por ${row.original.conteo} equipo(s). Unifique primero.` : 'Eliminar este valor'}
|
||||||
>
|
>
|
||||||
🗑️ Eliminar
|
<Trash2 size={14} /> Eliminar
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
], [componentType, renderValor, handleDeleteRam, handleDeleteTexto, handleOpenModal]);
|
], [componentType, renderValor, handleDeleteRam, handleDeleteTexto, handleOpenModal]);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: valores,
|
data: valores,
|
||||||
columns,
|
columns,
|
||||||
state: {
|
state: { sorting, globalFilter },
|
||||||
sorting,
|
|
||||||
globalFilter,
|
|
||||||
},
|
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
@@ -180,6 +160,19 @@ const GestionComponentes = () => {
|
|||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// CORRECCIÓN: Añadimos el tipo React.CSSProperties
|
||||||
|
const tableContainerStyle: React.CSSProperties = {
|
||||||
|
overflowX: 'auto',
|
||||||
|
border: '1px solid #dee2e6',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginTop: '1rem'
|
||||||
|
};
|
||||||
|
|
||||||
|
// CORRECCIÓN: Añadimos el tipo React.CSSProperties
|
||||||
|
const tableStyle: React.CSSProperties = {
|
||||||
|
minWidth: 'auto'
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Gestión de Componentes Maestros ({table.getFilteredRowModel().rows.length})</h2>
|
<h2>Gestión de Componentes Maestros ({table.getFilteredRowModel().rows.length})</h2>
|
||||||
@@ -206,15 +199,25 @@ const GestionComponentes = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<p>Cargando...</p>
|
<div style={tableContainerStyle}>
|
||||||
|
<TableSkeleton rows={6} columns={3} />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ overflowX: 'auto', border: '1px solid #dee2e6', borderRadius: '8px', marginTop: '1rem' }}>
|
<div style={tableContainerStyle}>
|
||||||
<table className={styles.table}>
|
<table className={styles.table} style={tableStyle}>
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map(header => (
|
{headerGroup.headers.map(header => {
|
||||||
<th key={header.id} className={styles.th} onClick={header.column.getToggleSortingHandler()}>
|
const classNames = [styles.th];
|
||||||
|
if (header.id === 'conteo') classNames.push(styles.thNumeric);
|
||||||
|
if (header.id === 'acciones') classNames.push(styles.thActions);
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
key={header.id}
|
||||||
|
className={classNames.join(' ')}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
>
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
{header.column.getIsSorted() && (
|
{header.column.getIsSorted() && (
|
||||||
<span className={styles.sortIndicator}>
|
<span className={styles.sortIndicator}>
|
||||||
@@ -222,18 +225,27 @@ const GestionComponentes = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map(row => (
|
{table.getRowModel().rows.map(row => (
|
||||||
<tr key={row.id} className={styles.tr}>
|
<tr key={row.id} className={styles.tr}>
|
||||||
{row.getVisibleCells().map(cell => (
|
{row.getVisibleCells().map(cell => {
|
||||||
<td key={cell.id} className={styles.td}>
|
const classNames = [styles.td];
|
||||||
|
if (cell.column.id === 'conteo') classNames.push(styles.tdNumeric);
|
||||||
|
if (cell.column.id === 'acciones') classNames.push(styles.tdActions);
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={classNames.join(' ')}
|
||||||
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { Sector } from '../types/interfaces';
|
|||||||
import styles from './SimpleTable.module.css';
|
import styles from './SimpleTable.module.css';
|
||||||
import ModalSector from './ModalSector';
|
import ModalSector from './ModalSector';
|
||||||
import { sectorService } from '../services/apiService';
|
import { sectorService } from '../services/apiService';
|
||||||
|
import { PlusCircle, Pencil, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
const GestionSectores = () => {
|
const GestionSectores = () => {
|
||||||
const [sectores, setSectores] = useState<Sector[]>([]);
|
const [sectores, setSectores] = useState<Sector[]>([]);
|
||||||
@@ -80,8 +81,8 @@ const GestionSectores = () => {
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
|
||||||
<h2>Gestión de Sectores</h2>
|
<h2>Gestión de Sectores</h2>
|
||||||
<button onClick={handleOpenCreateModal} className={`${styles.btn} ${styles.btnPrimary}`}>
|
<button onClick={handleOpenCreateModal} className={`${styles.btn} ${styles.btnPrimary}`} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
+ Añadir Sector
|
<PlusCircle size={18} /> Añadir Sector
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
@@ -97,9 +98,9 @@ const GestionSectores = () => {
|
|||||||
<td className={styles.td}>{sector.nombre}</td>
|
<td className={styles.td}>{sector.nombre}</td>
|
||||||
<td className={styles.td}>
|
<td className={styles.td}>
|
||||||
<div style={{ display: 'flex', gap: '10px' }}>
|
<div style={{ display: 'flex', gap: '10px' }}>
|
||||||
<button onClick={() => handleOpenEditModal(sector)} className={styles.tableButton}>✏️ Editar</button>
|
<button onClick={() => handleOpenEditModal(sector)} className={styles.tableButton} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}><Pencil size={16} /> Editar</button>
|
||||||
<button onClick={() => handleDelete(sector.id)} className={styles.deleteUserButton} style={{ fontSize: '1em', padding: '0.375rem 0.75rem', border: '1px solid #dc3545', borderRadius: '4px' }}>
|
<button onClick={() => handleDelete(sector.id)} className={styles.deleteUserButton} style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '1em', padding: '0.375rem 0.75rem', border: '1px solid #dc3545', borderRadius: '4px' }}>
|
||||||
🗑️ Eliminar
|
<Trash2 size={16} /> Eliminar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import styles from './SimpleTable.module.css';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import AutocompleteInput from './AutocompleteInput';
|
import AutocompleteInput from './AutocompleteInput';
|
||||||
import { equipoService } from '../services/apiService';
|
import { equipoService } from '../services/apiService';
|
||||||
|
import { X, Pencil, HardDrive, MemoryStick, UserPlus, Trash2, Power, Info, Component, Keyboard, Cog, Zap, History } from 'lucide-react';
|
||||||
|
|
||||||
interface ModalDetallesEquipoProps {
|
interface ModalDetallesEquipoProps {
|
||||||
equipo: Equipo;
|
equipo: Equipo;
|
||||||
@@ -81,7 +82,6 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleWolClick = async () => {
|
const handleWolClick = async () => {
|
||||||
// La validación ahora es redundante por el 'disabled', pero la dejamos como buena práctica
|
|
||||||
if (!equipo.mac || !equipo.ip) {
|
if (!equipo.mac || !equipo.ip) {
|
||||||
toast.error("Este equipo no tiene MAC o IP para encenderlo.");
|
toast.error("Este equipo no tiene MAC o IP para encenderlo.");
|
||||||
return;
|
return;
|
||||||
@@ -118,7 +118,9 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.modalLarge}>
|
<div className={styles.modalLarge}>
|
||||||
<button onClick={onClose} className={styles.closeButton} disabled={isChildModalOpen}>×</button>
|
<button onClick={onClose} className={styles.closeButton} disabled={isChildModalOpen}>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className={styles.modalLargeContent}>
|
<div className={styles.modalLargeContent}>
|
||||||
<div className={styles.modalLargeHeader}>
|
<div className={styles.modalLargeHeader}>
|
||||||
@@ -131,7 +133,7 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
<button onClick={handleCancel} className={`${styles.btn} ${styles.btnSecondary}`}>Cancelar</button>
|
<button onClick={handleCancel} className={`${styles.btn} ${styles.btnSecondary}`}>Cancelar</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={handleEditClick} className={`${styles.btn} ${styles.btnPrimary}`}>✏️ Editar</button>
|
<button onClick={handleEditClick} className={`${styles.btn} ${styles.btnPrimary}`} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}><Pencil size={16} /> Editar</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -141,12 +143,12 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
<div className={styles.mainColumn}>
|
<div className={styles.mainColumn}>
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<h3 className={styles.sectionTitle} style={{ border: 'none', padding: 0 }}>🔗 Datos Principales</h3>
|
<h3 className={styles.sectionTitle} style={{ border: 'none', padding: 0 }}><Info size={20} /> Datos Principales</h3>
|
||||||
{equipo.origen === 'manual' && (
|
{equipo.origen === 'manual' && (
|
||||||
<div style={{ display: 'flex', gap: '5px' }}>
|
<div style={{ display: 'flex', gap: '5px' }}>
|
||||||
<button onClick={() => onAddComponent('disco')} className={styles.tableButtonMas}>Agregar Disco</button>
|
<button onClick={() => onAddComponent('disco')} className={styles.tableButtonMas} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}><HardDrive size={16} /> Disco</button>
|
||||||
<button onClick={() => onAddComponent('ram')} className={styles.tableButtonMas}>Agregar RAM</button>
|
<button onClick={() => onAddComponent('ram')} className={styles.tableButtonMas} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}><MemoryStick size={16} /> RAM</button>
|
||||||
<button onClick={() => onAddComponent('usuario')} className={styles.tableButtonMas}>Agregar Usuario</button>
|
<button onClick={() => onAddComponent('usuario')} className={styles.tableButtonMas} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}><UserPlus size={16} /> Usuario</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -158,12 +160,12 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
<div className={styles.detailItem}><strong className={styles.detailLabel}>Sector:</strong>{isEditing ? <select name="sector_id" value={editableEquipo.sector_id || ''} onChange={handleChange} className={styles.modalInput}><option value="">- Sin Asignar -</option>{sectores.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}</select> : <span className={styles.detailValue}>{equipo.sector?.nombre || 'No asignado'}</span>}</div>
|
<div className={styles.detailItem}><strong className={styles.detailLabel}>Sector:</strong>{isEditing ? <select name="sector_id" value={editableEquipo.sector_id || ''} onChange={handleChange} className={styles.modalInput}><option value="">- Sin Asignar -</option>{sectores.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}</select> : <span className={styles.detailValue}>{equipo.sector?.nombre || 'No asignado'}</span>}</div>
|
||||||
<div className={styles.detailItem}><strong className={styles.detailLabel}>Creación:</strong><span className={styles.detailValue}>{formatDate(equipo.created_at)}</span></div>
|
<div className={styles.detailItem}><strong className={styles.detailLabel}>Creación:</strong><span className={styles.detailValue}>{formatDate(equipo.created_at)}</span></div>
|
||||||
<div className={styles.detailItem}><strong className={styles.detailLabel}>Última Actualización:</strong><span className={styles.detailValue}>{formatDate(equipo.updated_at)}</span></div>
|
<div className={styles.detailItem}><strong className={styles.detailLabel}>Última Actualización:</strong><span className={styles.detailValue}>{formatDate(equipo.updated_at)}</span></div>
|
||||||
<div className={styles.detailItemFull}><strong className={styles.detailLabel}>Usuarios:</strong><span className={styles.detailValue}>{equipo.usuarios?.length > 0 ? equipo.usuarios.map(u => (<div key={u.id} className={styles.componentItem}><div><span title={`Origen: ${u.origen}`}>{u.origen === 'manual' ? '⌨️' : '⚙️'}</span>{` ${u.username}`}</div>{u.origen === 'manual' && (<button onClick={() => onRemoveAssociation('usuario', { equipoId: equipo.id, usuarioId: u.id })} className={styles.deleteUserButton} title="Quitar este usuario">🗑️</button>)}</div>)) : 'N/A'}</span></div>
|
<div className={styles.detailItemFull}><strong className={styles.detailLabel}>Usuarios:</strong><span className={styles.detailValue}>{equipo.usuarios?.length > 0 ? equipo.usuarios.map(u => (<div key={u.id} className={styles.componentItem}><div><span title={`Origen: ${u.origen}`}>{u.origen === 'manual' ? <Keyboard size={16} /> : <Cog size={16} />}</span>{` ${u.username}`}</div>{u.origen === 'manual' && (<button onClick={() => onRemoveAssociation('usuario', { equipoId: equipo.id, usuarioId: u.id })} className={styles.deleteUserButton} title="Quitar este usuario"><Trash2 size={16} /></button>)}</div>)) : 'N/A'}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h3 className={styles.sectionTitle}>💻 Componentes</h3>
|
<h3 className={styles.sectionTitle}><Component size={20} /> Componentes</h3>
|
||||||
<div className={styles.detailsGrid}>
|
<div className={styles.detailsGrid}>
|
||||||
<div className={styles.detailItem}><strong className={styles.detailLabel}>Motherboard:</strong>{isEditing ? <AutocompleteInput mode="static" name="motherboard" value={editableEquipo.motherboard} onChange={handleChange} className={styles.modalInput} fetchSuggestions={fetchMotherboardSuggestions} /> : <span className={styles.detailValue}>{equipo.motherboard || 'N/A'}</span>}</div>
|
<div className={styles.detailItem}><strong className={styles.detailLabel}>Motherboard:</strong>{isEditing ? <AutocompleteInput mode="static" name="motherboard" value={editableEquipo.motherboard} onChange={handleChange} className={styles.modalInput} fetchSuggestions={fetchMotherboardSuggestions} /> : <span className={styles.detailValue}>{equipo.motherboard || 'N/A'}</span>}</div>
|
||||||
<div className={styles.detailItem}><strong className={styles.detailLabel}>CPU:</strong>{isEditing ? <AutocompleteInput mode="static" name="cpu" value={editableEquipo.cpu} onChange={handleChange} className={styles.modalInput} fetchSuggestions={fetchCpuSuggestions} /> : <span className={styles.detailValue}>{equipo.cpu || 'N/A'}</span>}</div>
|
<div className={styles.detailItem}><strong className={styles.detailLabel}>CPU:</strong>{isEditing ? <AutocompleteInput mode="static" name="cpu" value={editableEquipo.cpu} onChange={handleChange} className={styles.modalInput} fetchSuggestions={fetchCpuSuggestions} /> : <span className={styles.detailValue}>{equipo.cpu || 'N/A'}</span>}</div>
|
||||||
@@ -185,7 +187,7 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
<span className={styles.detailValue}>{equipo.architecture || 'N/A'}</span>
|
<span className={styles.detailValue}>{equipo.architecture || 'N/A'}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.detailItemFull}><strong className={styles.detailLabel}>Discos:</strong><span className={styles.detailValue}>{equipo.discos?.length > 0 ? equipo.discos.map(d => (<div key={d.equipoDiscoId} className={styles.componentItem}><div><span title={`Origen: ${d.origen}`}>{d.origen === 'manual' ? '⌨️' : '⚙️'}</span>{` ${d.mediatype} ${d.size}GB`}</div>{d.origen === 'manual' && (<button onClick={() => onRemoveAssociation('disco', d.equipoDiscoId)} className={styles.deleteUserButton} title="Eliminar este disco">🗑️</button>)}</div>)) : 'N/A'}</span></div>
|
<div className={styles.detailItemFull}><strong className={styles.detailLabel}>Discos:</strong><span className={styles.detailValue}>{equipo.discos?.length > 0 ? equipo.discos.map(d => (<div key={d.equipoDiscoId} className={styles.componentItem}><div><span title={`Origen: ${d.origen}`}>{d.origen === 'manual' ? <Keyboard size={16} /> : <Cog size={16} />}</span>{` ${d.mediatype} ${d.size}GB`}</div>{d.origen === 'manual' && (<button onClick={() => onRemoveAssociation('disco', d.equipoDiscoId)} className={styles.deleteUserButton} title="Eliminar este disco"><Trash2 size={16} /></button>)}</div>)) : 'N/A'}</span></div>
|
||||||
<div className={styles.detailItem}>
|
<div className={styles.detailItem}>
|
||||||
<strong className={styles.detailLabel}>Total Slots RAM:</strong>
|
<strong className={styles.detailLabel}>Total Slots RAM:</strong>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
@@ -201,14 +203,14 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
<span className={styles.detailValue}>{equipo.ram_slots || 'N/A'}</span>
|
<span className={styles.detailValue}>{equipo.ram_slots || 'N/A'}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.detailItemFull}><strong className={styles.detailLabel}>Módulos RAM:</strong><span className={styles.detailValue}>{equipo.memoriasRam?.length > 0 ? equipo.memoriasRam.map(m => (<div key={m.equipoMemoriaRamId} className={styles.componentItem}><div><span title={`Origen: ${m.origen}`}>{m.origen === 'manual' ? '⌨️' : '⚙️'}</span>{` Slot ${m.slot}: ${m.partNumber || 'N/P'} ${m.tamano}GB ${m.velocidad || ''}MHz`}</div>{m.origen === 'manual' && (<button onClick={() => onRemoveAssociation('ram', m.equipoMemoriaRamId)} className={styles.deleteUserButton} title="Eliminar este módulo">🗑️</button>)}</div>)) : 'N/A'}</span></div>
|
<div className={styles.detailItemFull}><strong className={styles.detailLabel}>Módulos RAM:</strong><span className={styles.detailValue}>{equipo.memoriasRam?.length > 0 ? equipo.memoriasRam.map(m => (<div key={m.equipoMemoriaRamId} className={styles.componentItem}><div><span title={`Origen: ${m.origen}`}>{m.origen === 'manual' ? <Keyboard size={16} /> : <Cog size={16} />}</span>{` Slot ${m.slot}: ${m.partNumber || 'N/P'} ${m.tamano}GB ${m.velocidad || ''}MHz`}</div>{m.origen === 'manual' && (<button onClick={() => onRemoveAssociation('ram', m.equipoMemoriaRamId)} className={styles.deleteUserButton} title="Eliminar este módulo"><Trash2 size={16} /></button>)}</div>)) : 'N/A'}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.sidebarColumn}>
|
<div className={styles.sidebarColumn}>
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h3 className={styles.sectionTitle}>⚡ Acciones y Estado</h3>
|
<h3 className={styles.sectionTitle}><Zap size={20} /> Acciones y Estado</h3>
|
||||||
<div className={styles.actionsGrid}>
|
<div className={styles.actionsGrid}>
|
||||||
<div className={styles.detailItem}><strong className={styles.detailLabel}>Estado:</strong><div className={styles.statusIndicator}><div className={`${styles.statusDot} ${isOnline ? styles.statusOnline : styles.statusOffline}`} /><span>{isOnline ? 'En línea' : 'Sin conexión'}</span></div></div>
|
<div className={styles.detailItem}><strong className={styles.detailLabel}>Estado:</strong><div className={styles.statusIndicator}><div className={`${styles.statusDot} ${isOnline ? styles.statusOnline : styles.statusOffline}`} /><span>{isOnline ? 'En línea' : 'Sin conexión'}</span></div></div>
|
||||||
|
|
||||||
@@ -220,7 +222,7 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
data-tooltip-id="modal-power-tooltip"
|
data-tooltip-id="modal-power-tooltip"
|
||||||
disabled={!equipo.mac}
|
disabled={!equipo.mac}
|
||||||
>
|
>
|
||||||
<img src="./power.png" alt="Encender equipo" className={styles.powerIcon} />
|
<Power size={18} />
|
||||||
Encender (WOL)
|
Encender (WOL)
|
||||||
</button>
|
</button>
|
||||||
<Tooltip id="modal-power-tooltip" place="top">
|
<Tooltip id="modal-power-tooltip" place="top">
|
||||||
@@ -235,7 +237,7 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
className={styles.deleteButton}
|
className={styles.deleteButton}
|
||||||
data-tooltip-id="modal-delete-tooltip"
|
data-tooltip-id="modal-delete-tooltip"
|
||||||
>
|
>
|
||||||
🗑️ Eliminar
|
<Trash2 size={16} /> Eliminar
|
||||||
</button>
|
</button>
|
||||||
<Tooltip id="modal-delete-tooltip" place="top">
|
<Tooltip id="modal-delete-tooltip" place="top">
|
||||||
Eliminar este equipo permanentemente del inventario
|
Eliminar este equipo permanentemente del inventario
|
||||||
@@ -247,7 +249,7 @@ const ModalDetallesEquipo: React.FC<ModalDetallesEquipoProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${styles.section} ${styles.historySectionFullWidth}`}>
|
<div className={`${styles.section} ${styles.historySectionFullWidth}`}>
|
||||||
<h3 className={styles.sectionTitle}>📜 Historial de cambios</h3>
|
<h3 className={styles.sectionTitle}><History size={20} /> Historial de cambios</h3>
|
||||||
<div className={styles.historyContainer}>
|
<div className={styles.historyContainer}>
|
||||||
<table className={styles.historyTable}>
|
<table className={styles.historyTable}>
|
||||||
<thead><tr><th className={styles.historyTh}>Fecha</th><th className={styles.historyTh}>Campo</th><th className={styles.historyTh}>Valor anterior</th><th className={styles.historyTh}>Valor nuevo</th></tr></thead>
|
<thead><tr><th className={styles.historyTh}>Fecha</th><th className={styles.historyTh}>Campo</th><th className={styles.historyTh}>Valor anterior</th><th className={styles.historyTh}>Valor nuevo</th></tr></thead>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
// frontend/src/components/Navbar.tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { View } from '../App';
|
import type { View } from '../App';
|
||||||
|
import ThemeToggle from './ThemeToggle';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
@@ -38,7 +40,11 @@ const Navbar: React.FC<NavbarProps> = ({ currentView, setCurrentView }) => {
|
|||||||
>
|
>
|
||||||
Dashboard
|
Dashboard
|
||||||
</button>
|
</button>
|
||||||
|
<div style={{ padding: '0.25rem' }}>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,33 +9,37 @@
|
|||||||
.searchInput, .sectorSelect {
|
.searchInput, .sectorSelect {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #ced4da;
|
border: 1px solid var(--color-border);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos de la tabla */
|
|
||||||
.table {
|
.table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1200px; /* Ancho mínimo para forzar el scroll horizontal si es necesario */
|
min-width: 1200px;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.th {
|
.th {
|
||||||
color: #212529;
|
color: var(--color-text-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 2px solid #dee2e6;
|
border-bottom: 2px solid var(--color-border);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0; /* Mantiene la posición sticky en la parte superior del viewport */
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background-color: #f8f9fa; /* Es crucial tener un fondo sólido */
|
background-color: var(--color-background);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortIndicator {
|
.sortIndicator {
|
||||||
@@ -43,7 +47,7 @@
|
|||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
color: #007bff;
|
color: var(--color-primary);
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,70 +60,105 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tr:hover {
|
.tr:hover {
|
||||||
background-color: #f1f3f5;
|
background-color: var(--color-surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.td {
|
.td {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
color: #495057;
|
color: var(--color-text-secondary);
|
||||||
background-color: white;
|
background-color: var(--color-surface);
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* NUEVA SECCIÓN PARA ANCHOS DE COLUMNA */
|
||||||
|
|
||||||
|
/* Columna numérica (Nº de Equipos) */
|
||||||
|
.thNumeric,
|
||||||
|
.tdNumeric {
|
||||||
|
width: 150px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thActions,
|
||||||
|
.tdActions {
|
||||||
|
width: 220px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NUEVO: Columnas de Tabla Principal (SimpleTable) */
|
||||||
|
.thIp, .tdIp { width: 90px; }
|
||||||
|
.thRam, .tdRam { width: 40px; text-align: center; }
|
||||||
|
.thArch, .tdArch { width: 80px; }
|
||||||
|
.thUsers, .tdUsers { width: 230px; }
|
||||||
|
.thSector, .tdSector { width: 170px; }
|
||||||
|
|
||||||
/* Estilos de botones dentro de la tabla */
|
/* Estilos de botones dentro de la tabla */
|
||||||
.hostnameButton {
|
.hostnameButton {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #007bff;
|
color: var(--color-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hostnameButton:hover {
|
||||||
|
color: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.tableButton {
|
.tableButton {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid var(--color-border);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #212529;
|
color: var(--color-text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableButton:hover {
|
.tableButton:hover {
|
||||||
background-color: #e9ecef;
|
background-color: var(--color-surface-hover);
|
||||||
border-color: #adb5bd;
|
border-color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableButtonMas {
|
.tableButtonMas {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #007bff;
|
border: 1px solid var(--color-primary);
|
||||||
background-color: #007bff;
|
background-color: var(--color-primary);
|
||||||
color: #ffffff;
|
color: var(--color-navbar-text-hover);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableButtonMas:hover {
|
.tableButtonMas:hover {
|
||||||
background-color: #0056b3;
|
background-color: var(--color-primary-hover);
|
||||||
border-color: #0056b3;
|
border-color: var(--color-primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteUserButton {
|
.deleteUserButton {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #dc3545;
|
color: var(--color-danger);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
opacity: 0.7;
|
transition: opacity 0.3s ease, color 0.3s ease, background-color 0.3s ease;
|
||||||
transition: opacity 0.3s ease, color 0.3s ease;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.deleteUserButton:hover {
|
|
||||||
opacity: 1;
|
.deleteUserButton:hover:not(:disabled) {
|
||||||
color: #a4202e;
|
color: var(--color-danger-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteUserButton:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilo para el botón de scroll-to-top */
|
/* Estilo para el botón de scroll-to-top */
|
||||||
@@ -130,7 +169,7 @@
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #007bff;
|
background-color: var(--color-primary);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -139,12 +178,37 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: opacity 0.3s, transform 0.3s;
|
transition: opacity 0.3s, transform 0.3s, background-color 0.3s;
|
||||||
z-index: 1002;
|
z-index: 1002;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollToTop:hover {
|
.scrollToTop:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
background-color: #0056b3;
|
background-color: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== INICIO DE CAMBIOS PARA MODALES Y ANIMACIONES ===== */
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos genéricos para modales */
|
/* Estilos genéricos para modales */
|
||||||
@@ -159,23 +223,27 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
/* Aplicamos animación */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
background-color: #ffffff;
|
background-color: var(--color-surface);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
box-shadow: 0px 8px 30px rgba(0, 0, 0, 0.12);
|
box-shadow: 0px 8px 30px rgba(0, 0, 0, 0.12);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--color-border);
|
||||||
font-family: 'Segoe UI', sans-serif;
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
animation: scaleIn 0.2s ease-out;
|
||||||
|
/* Aplicamos animación */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal h3 {
|
.modal h3 {
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.5rem;
|
||||||
color: #2d3436;
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal label {
|
.modal label {
|
||||||
@@ -187,18 +255,32 @@
|
|||||||
.modalInput {
|
.modalInput {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #ced4da;
|
border: 1px solid var(--color-border);
|
||||||
|
background-color: var(--color-background); /* Ligeramente diferente para contraste */
|
||||||
|
color: var(--color-text-primary);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-top: 4px; /* Separado del label */
|
margin-top: 4px;
|
||||||
margin-bottom: 4px; /* Espacio antes del siguiente elemento */
|
/* Separado del label */
|
||||||
|
margin-bottom: 4px;
|
||||||
|
/* Espacio antes del siguiente elemento */
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
/* Transición para el foco */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modalInput:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.modalActions {
|
.modalActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
justify-content: flex-end; /* Alinea los botones a la derecha por defecto */
|
justify-content: flex-end;
|
||||||
|
/* Alinea los botones a la derecha por defecto */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos de botones para modales */
|
/* Estilos de botones para modales */
|
||||||
@@ -213,24 +295,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btnPrimary {
|
.btnPrimary {
|
||||||
background-color: #007bff;
|
background-color: var(--color-primary);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.btnPrimary:hover {
|
.btnPrimary:hover {
|
||||||
background-color: #0056b3;
|
background-color: var(--color-primary-hover);
|
||||||
}
|
}
|
||||||
.btnPrimary:disabled {
|
.btnPrimary:disabled {
|
||||||
background-color: #e9ecef;
|
background-color: var(--color-surface-hover);
|
||||||
color: #6c757d;
|
color: var(--color-text-muted);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btnSecondary {
|
.btnSecondary {
|
||||||
background-color: #6c757d;
|
background-color: var(--color-text-muted);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.btnSecondary:hover {
|
.btnSecondary:hover {
|
||||||
background-color: #5a6268;
|
background-color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== NUEVOS ESTILOS PARA EL MODAL DE DETALLES ===== */
|
/* ===== NUEVOS ESTILOS PARA EL MODAL DE DETALLES ===== */
|
||||||
@@ -241,19 +323,24 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #f8f9fa; /* Un fondo ligeramente gris para el modal */
|
background-color: var(--color-background);
|
||||||
|
/* Un fondo ligeramente gris para el modal */
|
||||||
z-index: 1003;
|
z-index: 1003;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
/* Animación ligeramente más lenta para pantalla completa */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalLargeContent {
|
.modalLargeContent {
|
||||||
max-width: 1400px; /* Ancho máximo del contenido */
|
max-width: 1400px;
|
||||||
|
/* Ancho máximo del contenido */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto; /* Centrar el contenido */
|
margin: 0 auto;
|
||||||
|
/* Centrar el contenido */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalLargeHeader {
|
.modalLargeHeader {
|
||||||
@@ -267,7 +354,7 @@
|
|||||||
.modalLargeHeader h2 {
|
.modalLargeHeader h2 {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: #343a40;
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeButton {
|
.closeButton {
|
||||||
@@ -289,6 +376,7 @@
|
|||||||
right: 30px;
|
right: 30px;
|
||||||
top: 30px;
|
top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeButton:hover {
|
.closeButton:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
@@ -314,8 +402,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
background-color: #ffffff;
|
background-color: var(--color-surface);
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
@@ -325,8 +413,8 @@
|
|||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 1rem 0;
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
color: #2d3436;
|
color: var(--color-text-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -339,7 +427,6 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CAMBIO: Se aplica el mismo estilo de grid a componentsGrid para que se vea igual que detailsGrid */
|
|
||||||
.componentsGrid {
|
.componentsGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
@@ -352,24 +439,25 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailItem, .detailItemFull {
|
.detailItem,
|
||||||
|
.detailItemFull {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e9ecef;
|
background-color: var(--color-background);
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailLabel {
|
.detailLabel {
|
||||||
color: #6c757d;
|
color: var(--color-text-muted);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailValue {
|
.detailValue {
|
||||||
color: #495057;
|
color: var(--color-text-secondary);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -383,9 +471,10 @@
|
|||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.powerButton, .deleteButton {
|
.powerButton,
|
||||||
|
.deleteButton {
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -398,9 +487,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.powerButton:hover {
|
.powerButton:hover {
|
||||||
border-color: #007bff;
|
border-color: var(--color-primary);
|
||||||
background-color: #e7f1ff;
|
background-color: color-mix(in srgb, var(--color-primary) 15%, transparent);
|
||||||
color: #0056b3;
|
color: var(--color-primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.powerIcon {
|
.powerIcon {
|
||||||
@@ -409,13 +498,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.deleteButton {
|
.deleteButton {
|
||||||
color: #dc3545;
|
color: var(--color-danger);
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteButton:hover {
|
.deleteButton:hover {
|
||||||
border-color: #dc3545;
|
border-color: var(--color-danger);
|
||||||
background-color: #fbebee;
|
background-color: var(--color-danger-background);
|
||||||
color: #a4202e;
|
color: var(--color-danger-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteButton:disabled {
|
.deleteButton:disabled {
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
@@ -425,7 +517,7 @@
|
|||||||
.historyContainer {
|
.historyContainer {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +527,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.historyTh {
|
.historyTh {
|
||||||
background-color: #f8f9fa;
|
background-color: var(--color-background);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@@ -445,16 +537,15 @@
|
|||||||
|
|
||||||
.historyTd {
|
.historyTd {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
color: #495057;
|
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
border-bottom: 1px solid #dee2e6;
|
color: var(--color-text-secondary);
|
||||||
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.historyTr:last-child .historyTd {
|
.historyTr:last-child .historyTd {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CAMBIO: Nueva clase para dar espacio a la sección de historial */
|
|
||||||
.historySectionFullWidth {
|
.historySectionFullWidth {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
@@ -493,30 +584,66 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clases para la sección de usuarios y claves - No se usan en el nuevo modal pero se mantienen por si acaso */
|
.userList {
|
||||||
.userList { min-width: 240px; }
|
min-width: 240px;
|
||||||
.userItem { display: flex; align-items: center; justify-content: space-between; margin: 4px 0; padding: 6px; background-color: #f8f9fa; border-radius: 4px; position: relative; }
|
}
|
||||||
.userInfo { color: #495057; }
|
|
||||||
.userActions { display: flex; gap: 4px; align-items: center; }
|
.userItem {
|
||||||
.sectorContainer { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 0.5rem; }
|
display: flex;
|
||||||
.sectorName { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
align-items: center;
|
||||||
.sectorNameAssigned { color: #212529; font-style: normal; }
|
justify-content: space-between;
|
||||||
.sectorNameUnassigned { color: #6c757d; font-style: italic; }
|
margin: 4px 0;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfo {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectorContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectorName {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectorNameAssigned {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectorNameUnassigned {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* Estilo para el overlay de un modal anidado */
|
|
||||||
.modalOverlay--nested {
|
.modalOverlay--nested {
|
||||||
/* z-index superior al del botón de cierre del modal principal (1004) */
|
|
||||||
z-index: 1005;
|
z-index: 1005;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* También nos aseguramos de que el contenido del modal anidado tenga un z-index superior */
|
|
||||||
.modalOverlay--nested .modal {
|
.modalOverlay--nested .modal {
|
||||||
z-index: 1006;
|
z-index: 1006;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilo para deshabilitar el botón de cierre del modal principal */
|
|
||||||
.closeButton:disabled {
|
.closeButton:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
background-color: #6c757d; /* Gris para indicar inactividad */
|
background-color: #6c757d;
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,10 @@ import { Tooltip } from 'react-tooltip';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import type { Equipo, Sector, Usuario, UsuarioEquipoDetalle } from '../types/interfaces';
|
import type { Equipo, Sector, Usuario, UsuarioEquipoDetalle } from '../types/interfaces';
|
||||||
import styles from './SimpleTable.module.css';
|
import styles from './SimpleTable.module.css';
|
||||||
|
import skeletonStyles from './Skeleton.module.css'; // Importamos el estilo del esqueleto
|
||||||
|
|
||||||
import { equipoService, sectorService, usuarioService } from '../services/apiService';
|
import { equipoService, sectorService, usuarioService } from '../services/apiService';
|
||||||
|
import { PlusCircle, KeyRound, UserX, Pencil, ArrowUp, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||||
|
|
||||||
import ModalAnadirEquipo from './ModalAnadirEquipo';
|
import ModalAnadirEquipo from './ModalAnadirEquipo';
|
||||||
import ModalEditarSector from './ModalEditarSector';
|
import ModalEditarSector from './ModalEditarSector';
|
||||||
@@ -19,6 +21,7 @@ import ModalDetallesEquipo from './ModalDetallesEquipo';
|
|||||||
import ModalAnadirDisco from './ModalAnadirDisco';
|
import ModalAnadirDisco from './ModalAnadirDisco';
|
||||||
import ModalAnadirRam from './ModalAnadirRam';
|
import ModalAnadirRam from './ModalAnadirRam';
|
||||||
import ModalAnadirUsuario from './ModalAnadirUsuario';
|
import ModalAnadirUsuario from './ModalAnadirUsuario';
|
||||||
|
import TableSkeleton from './TableSkeleton'; // Importamos el componente de esqueleto
|
||||||
|
|
||||||
const SimpleTable = () => {
|
const SimpleTable = () => {
|
||||||
const [data, setData] = useState<Equipo[]>([]);
|
const [data, setData] = useState<Equipo[]>([]);
|
||||||
@@ -317,23 +320,23 @@ const SimpleTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- DEFINICIÓN DE COLUMNAS CORREGIDA ---
|
|
||||||
const columns: ColumnDef<Equipo>[] = [
|
const columns: ColumnDef<Equipo>[] = [
|
||||||
{ header: "ID", accessorKey: "id", enableHiding: true },
|
{ header: "ID", accessorKey: "id", enableHiding: true },
|
||||||
{
|
{
|
||||||
header: "Nombre", accessorKey: "hostname",
|
header: "Nombre", accessorKey: "hostname",
|
||||||
cell: (info: CellContext<Equipo, any>) => (<button onClick={() => setSelectedEquipo(info.row.original)} className={styles.hostnameButton}>{info.row.original.hostname}</button>)
|
cell: (info: CellContext<Equipo, any>) => (<button onClick={() => setSelectedEquipo(info.row.original)} className={styles.hostnameButton}>{info.row.original.hostname}</button>)
|
||||||
},
|
},
|
||||||
{ header: "IP", accessorKey: "ip" },
|
{ header: "IP", accessorKey: "ip", id: 'ip' },
|
||||||
{ header: "MAC", accessorKey: "mac", enableHiding: true },
|
{ header: "MAC", accessorKey: "mac", enableHiding: true },
|
||||||
{ header: "Motherboard", accessorKey: "motherboard" },
|
{ header: "Motherboard", accessorKey: "motherboard" },
|
||||||
{ header: "CPU", accessorKey: "cpu" },
|
{ header: "CPU", accessorKey: "cpu" },
|
||||||
{ header: "RAM", accessorKey: "ram_installed" },
|
{ header: "RAM", accessorKey: "ram_installed", id: 'ram' },
|
||||||
{ header: "Discos", accessorFn: (row: Equipo) => row.discos?.map(d => `${d.mediatype} ${d.size}GB`).join(" | ") || "Sin discos" },
|
{ header: "Discos", accessorFn: (row: Equipo) => row.discos?.map(d => `${d.mediatype} ${d.size}GB`).join(" | ") || "Sin discos" },
|
||||||
{ header: "OS", accessorKey: "os" },
|
{ header: "OS", accessorKey: "os" },
|
||||||
{ header: "Arquitectura", accessorKey: "architecture" },
|
{ header: "Arquitectura", accessorKey: "architecture", id: 'arch' },
|
||||||
{
|
{
|
||||||
header: "Usuarios y Claves",
|
header: "Usuarios y Claves",
|
||||||
|
id: 'usuarios',
|
||||||
cell: (info: CellContext<Equipo, any>) => {
|
cell: (info: CellContext<Equipo, any>) => {
|
||||||
const { row } = info;
|
const { row } = info;
|
||||||
const usuarios = row.original.usuarios || [];
|
const usuarios = row.original.usuarios || [];
|
||||||
@@ -351,7 +354,7 @@ const SimpleTable = () => {
|
|||||||
className={styles.tableButton}
|
className={styles.tableButton}
|
||||||
data-tooltip-id={`edit-${u.id}`}
|
data-tooltip-id={`edit-${u.id}`}
|
||||||
>
|
>
|
||||||
✏️
|
<KeyRound size={16} />
|
||||||
<Tooltip id={`edit-${u.id}`} className={styles.tooltip} place="top">Cambiar Clave</Tooltip>
|
<Tooltip id={`edit-${u.id}`} className={styles.tooltip} place="top">Cambiar Clave</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -360,7 +363,7 @@ const SimpleTable = () => {
|
|||||||
className={styles.deleteUserButton}
|
className={styles.deleteUserButton}
|
||||||
data-tooltip-id={`remove-${u.id}`}
|
data-tooltip-id={`remove-${u.id}`}
|
||||||
>
|
>
|
||||||
🗑️
|
<UserX size={16} />
|
||||||
<Tooltip id={`remove-${u.id}`} className={styles.tooltip} place="top">Eliminar Usuario</Tooltip>
|
<Tooltip id={`remove-${u.id}`} className={styles.tooltip} place="top">Eliminar Usuario</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,7 +381,7 @@ const SimpleTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.sectorContainer}>
|
<div className={styles.sectorContainer}>
|
||||||
<span className={`${styles.sectorName} ${sector ? styles.sectorNameAssigned : styles.sectorNameUnassigned}`}>{sector?.nombre || 'Asignar'}</span>
|
<span className={`${styles.sectorName} ${sector ? styles.sectorNameAssigned : styles.sectorNameUnassigned}`}>{sector?.nombre || 'Asignar'}</span>
|
||||||
<button onClick={() => setModalData(row.original)} className={styles.tableButton} data-tooltip-id={`editSector-${row.original.id}`}>✏️<Tooltip id={`editSector-${row.original.id}`} className={styles.tooltip} place="top">Cambiar Sector</Tooltip></button>
|
<button onClick={() => setModalData(row.original)} className={styles.tableButton} data-tooltip-id={`editSector-${row.original.id}`}><Pencil size={16} /><Tooltip id={`editSector-${row.original.id}`} className={styles.tooltip} place="top">Cambiar Sector</Tooltip></button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -410,8 +413,18 @@ const SimpleTable = () => {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
<div>
|
||||||
<h2>Cargando Equipos...</h2>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<h2>Equipos (...)</h2>
|
||||||
|
<div className={`${skeletonStyles.skeleton}`} style={{ height: '40px', width: '160px' }}></div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controlsContainer}>
|
||||||
|
<div className={`${skeletonStyles.skeleton}`} style={{ height: '38px', width: '300px' }}></div>
|
||||||
|
<div className={`${skeletonStyles.skeleton}`} style={{ height: '38px', width: '200px' }}></div>
|
||||||
|
</div>
|
||||||
|
<div><p style={{ fontSize: '12px', fontWeight: 'bold' }}>** La tabla permite ordenar por múltiples columnas manteniendo shift al hacer click en la cabecera. **</p></div>
|
||||||
|
<div className={`${skeletonStyles.skeleton}`} style={{ height: '54px', marginBottom: '1rem' }}></div>
|
||||||
|
<TableSkeleton rows={15} columns={11} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -420,16 +433,16 @@ const SimpleTable = () => {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1rem 0' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1rem 0' }}>
|
||||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||||
<button onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()} className={styles.tableButton}>
|
<button onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()} className={styles.tableButton}>
|
||||||
{'<<'}
|
<ChevronsLeft size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} className={styles.tableButton}>
|
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} className={styles.tableButton}>
|
||||||
{'<'}
|
<ChevronLeft size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} className={styles.tableButton}>
|
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} className={styles.tableButton}>
|
||||||
{'>'}
|
<ChevronRight size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()} className={styles.tableButton}>
|
<button onClick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()} className={styles.tableButton}>
|
||||||
{'>>'}
|
<ChevronsRight size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
@@ -474,8 +487,9 @@ const SimpleTable = () => {
|
|||||||
<button
|
<button
|
||||||
className={`${styles.btn} ${styles.btnPrimary}`}
|
className={`${styles.btn} ${styles.btnPrimary}`}
|
||||||
onClick={() => setIsAddModalOpen(true)}
|
onClick={() => setIsAddModalOpen(true)}
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
||||||
>
|
>
|
||||||
+ Añadir Equipo
|
<PlusCircle size={18} /> Añadir Equipo
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.controlsContainer}>
|
<div className={styles.controlsContainer}>
|
||||||
@@ -495,23 +509,41 @@ const SimpleTable = () => {
|
|||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map(hg => (
|
{table.getHeaderGroups().map(hg => (
|
||||||
<tr key={hg.id}>
|
<tr key={hg.id}>
|
||||||
{hg.headers.map(h => (
|
{hg.headers.map(h => {
|
||||||
<th key={h.id} className={styles.th} onClick={h.column.getToggleSortingHandler()}>
|
const classNames = [styles.th];
|
||||||
|
if (h.id === 'ip') classNames.push(styles.thIp);
|
||||||
|
if (h.id === 'ram') classNames.push(styles.thRam);
|
||||||
|
if (h.id === 'arch') classNames.push(styles.thArch);
|
||||||
|
if (h.id === 'usuarios') classNames.push(styles.thUsers);
|
||||||
|
if (h.id === 'sector') classNames.push(styles.thSector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th key={h.id} className={classNames.join(' ')} onClick={h.column.getToggleSortingHandler()}>
|
||||||
{flexRender(h.column.columnDef.header, h.getContext())}
|
{flexRender(h.column.columnDef.header, h.getContext())}
|
||||||
{h.column.getIsSorted() && (<span className={styles.sortIndicator}>{h.column.getIsSorted() === 'asc' ? '⇑' : '⇓'}</span>)}
|
{h.column.getIsSorted() && (<span className={styles.sortIndicator}>{h.column.getIsSorted() === 'asc' ? '⇑' : '⇓'}</span>)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map(row => (
|
{table.getRowModel().rows.map(row => (
|
||||||
<tr key={row.id} className={styles.tr}>
|
<tr key={row.id} className={styles.tr}>
|
||||||
{row.getVisibleCells().map(cell => (
|
{row.getVisibleCells().map(cell => {
|
||||||
<td key={cell.id} className={styles.td}>
|
const classNames = [styles.td];
|
||||||
|
if (cell.column.id === 'ip') classNames.push(styles.tdIp);
|
||||||
|
if (cell.column.id === 'ram') classNames.push(styles.tdRam);
|
||||||
|
if (cell.column.id === 'arch') classNames.push(styles.tdArch);
|
||||||
|
if (cell.column.id === 'usuarios') classNames.push(styles.tdUsers);
|
||||||
|
if (cell.column.id === 'sector') classNames.push(styles.tdSector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td key={cell.id} className={classNames.join(' ')}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -520,7 +552,7 @@ const SimpleTable = () => {
|
|||||||
|
|
||||||
{PaginacionControles}
|
{PaginacionControles}
|
||||||
|
|
||||||
{showScrollButton && (<button onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} className={styles.scrollToTop} title="Volver arriba">↑</button>)}
|
{showScrollButton && (<button onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} className={styles.scrollToTop} title="Volver arriba"><ArrowUp size={24} /></button>)}
|
||||||
|
|
||||||
{modalData && <ModalEditarSector modalData={modalData} setModalData={setModalData} sectores={sectores} onClose={() => setModalData(null)} onSave={handleSave} />}
|
{modalData && <ModalEditarSector modalData={modalData} setModalData={setModalData} sectores={sectores} onClose={() => setModalData(null)} onSave={handleSave} />}
|
||||||
|
|
||||||
|
|||||||
33
frontend/src/components/Skeleton.module.css
Normal file
33
frontend/src/components/Skeleton.module.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* frontend/src/components/Skeleton.module.css */
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.4),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
frontend/src/components/TableSkeleton.tsx
Normal file
49
frontend/src/components/TableSkeleton.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// frontend/src/components/TableSkeleton.tsx
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './SimpleTable.module.css';
|
||||||
|
import skeletonStyles from './Skeleton.module.css';
|
||||||
|
|
||||||
|
interface SkeletonProps {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Skeleton: React.FC<SkeletonProps> = ({ style }) => {
|
||||||
|
return <div className={skeletonStyles.skeleton} style={style}></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TableSkeletonProps {
|
||||||
|
rows?: number;
|
||||||
|
columns?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableSkeleton: React.FC<TableSkeletonProps> = ({ rows = 10, columns = 8 }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ overflowX: 'auto', border: '1px solid #dee2e6', borderRadius: '8px' }}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{Array.from({ length: columns }).map((_, i) => (
|
||||||
|
<th key={i} className={styles.th}>
|
||||||
|
<Skeleton style={{ height: '20px', width: '80%' }} />
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: rows }).map((_, i) => (
|
||||||
|
<tr key={i} className={styles.tr}>
|
||||||
|
{Array.from({ length: columns }).map((_, j) => (
|
||||||
|
<td key={j} className={styles.td}>
|
||||||
|
<Skeleton style={{ height: '20px' }} />
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableSkeleton;
|
||||||
20
frontend/src/components/ThemeToggle.css
Normal file
20
frontend/src/components/ThemeToggle.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/* frontend/src/components/ThemeToggle.css */
|
||||||
|
.theme-toggle-button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--color-text-muted);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-button:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
transform: rotate(15deg);
|
||||||
|
}
|
||||||
20
frontend/src/components/ThemeToggle.tsx
Normal file
20
frontend/src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// frontend/src/components/ThemeToggle.tsx
|
||||||
|
import { Sun, Moon } from 'lucide-react';
|
||||||
|
import { useTheme } from '../context/ThemeContext';
|
||||||
|
import './ThemeToggle.css';
|
||||||
|
|
||||||
|
const ThemeToggle = () => {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="theme-toggle-button"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
title={theme === 'light' ? 'Activar modo oscuro' : 'Activar modo claro'}
|
||||||
|
>
|
||||||
|
{theme === 'light' ? <Moon size={20} /> : <Sun size={20} />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeToggle;
|
||||||
58
frontend/src/context/ThemeContext.tsx
Normal file
58
frontend/src/context/ThemeContext.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// frontend/src/context/ThemeContext.tsx
|
||||||
|
|
||||||
|
import React, { createContext, useState, useEffect, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creamos el contexto con un valor por defecto.
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
// Creamos el proveedor del contexto.
|
||||||
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [theme, setTheme] = useState<Theme>(() => {
|
||||||
|
// 1. Intentamos leer el tema desde localStorage.
|
||||||
|
const savedTheme = localStorage.getItem('theme') as Theme | null;
|
||||||
|
if (savedTheme) return savedTheme;
|
||||||
|
|
||||||
|
// 2. Si no hay nada, respetamos la preferencia del sistema operativo.
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Como última opción, usamos el tema claro por defecto.
|
||||||
|
return 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Aplicamos el tema al body y lo guardamos en localStorage cada vez que cambia.
|
||||||
|
document.body.dataset.theme = theme;
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usamos useMemo para evitar que el valor del contexto se recalcule en cada render.
|
||||||
|
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook personalizado para usar el contexto de forma sencilla.
|
||||||
|
export const useTheme = (): ThemeContextType => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme debe ser utilizado dentro de un ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,23 +1,77 @@
|
|||||||
/* Limpieza básica y configuración de fuente */
|
/* frontend/src/index.css */
|
||||||
|
|
||||||
|
/* 1. Definición de variables de color para el tema claro (por defecto) */
|
||||||
|
:root {
|
||||||
|
--color-background: #f8f9fa;
|
||||||
|
--color-text-primary: #212529;
|
||||||
|
--color-text-secondary: #495057;
|
||||||
|
--color-text-muted: #6c757d;
|
||||||
|
|
||||||
|
--color-surface: #ffffff; /* Para tarjetas, modales, etc. */
|
||||||
|
--color-surface-hover: #f1f3f5;
|
||||||
|
--color-border: #dee2e6;
|
||||||
|
--color-border-subtle: #e9ecef;
|
||||||
|
|
||||||
|
--color-primary: #007bff;
|
||||||
|
--color-primary-hover: #0056b3;
|
||||||
|
--color-danger: #dc3545;
|
||||||
|
--color-danger-hover: #a4202e;
|
||||||
|
--color-danger-background: #fbebee;
|
||||||
|
|
||||||
|
--color-navbar-bg: #343a40;
|
||||||
|
--color-navbar-text: #adb5bd;
|
||||||
|
--color-navbar-text-hover: #ffffff;
|
||||||
|
|
||||||
|
--scrollbar-bg: #f1f1f1;
|
||||||
|
--scrollbar-thumb: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Sobrescribir variables para el tema oscuro */
|
||||||
|
[data-theme='dark'] {
|
||||||
|
--color-background: #121212;
|
||||||
|
--color-text-primary: #e0e0e0;
|
||||||
|
--color-text-secondary: #b0b0b0;
|
||||||
|
--color-text-muted: #888;
|
||||||
|
|
||||||
|
--color-surface: #1e1e1e;
|
||||||
|
--color-surface-hover: #2a2a2a;
|
||||||
|
--color-border: #333;
|
||||||
|
--color-border-subtle: #2c2c2c;
|
||||||
|
|
||||||
|
--color-primary: #3a97ff;
|
||||||
|
--color-primary-hover: #63b0ff;
|
||||||
|
--color-danger: #ff5252;
|
||||||
|
--color-danger-hover: #ff8a80;
|
||||||
|
--color-danger-background: #4d2323;
|
||||||
|
|
||||||
|
--color-navbar-bg: #1e1e1e;
|
||||||
|
--color-navbar-text: #888;
|
||||||
|
--color-navbar-text-hover: #ffffff;
|
||||||
|
|
||||||
|
--scrollbar-bg: #2c2c2c;
|
||||||
|
--scrollbar-thumb: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 3. Aplicar las variables a los estilos base */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
background-color: #f8f9fa;
|
background-color: var(--color-background);
|
||||||
color: #212529;
|
color: var(--color-text-primary);
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease; /* Transición suave al cambiar de tema */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos de la scrollbar que estaban en index.html */
|
|
||||||
body::-webkit-scrollbar {
|
body::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
background-color: #f1f1f1;
|
background-color: var(--scrollbar-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
body::-webkit-scrollbar-thumb {
|
body::-webkit-scrollbar-thumb {
|
||||||
background-color: #888;
|
background-color: var(--scrollbar-thumb);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clase para bloquear el scroll cuando un modal está abierto */
|
|
||||||
body.scroll-lock {
|
body.scroll-lock {
|
||||||
padding-right: 8px !important;
|
padding-right: 8px !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import { Toaster } from 'react-hot-toast' // Importamos el Toaster
|
import { Toaster } from 'react-hot-toast'
|
||||||
|
import { ThemeProvider } from './context/ThemeContext';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<ThemeProvider>
|
||||||
<App />
|
<App />
|
||||||
<Toaster
|
<Toaster
|
||||||
position="bottom-right" // Posición de las notificaciones
|
position="bottom-right"
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
// Estilos por defecto para las notificaciones
|
|
||||||
success: {
|
success: {
|
||||||
style: {
|
style: {
|
||||||
background: '#28a745',
|
background: '#28a745',
|
||||||
@@ -26,5 +27,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
@@ -84,6 +84,11 @@ export interface Equipo {
|
|||||||
|
|
||||||
// --- Interfaces para el Dashboard ---
|
// --- Interfaces para el Dashboard ---
|
||||||
|
|
||||||
|
export interface EquipoFilter {
|
||||||
|
field: string; // 'os', 'cpu', 'sector', etc.
|
||||||
|
value: string; // El valor específico del filtro
|
||||||
|
}
|
||||||
|
|
||||||
export interface StatItem {
|
export interface StatItem {
|
||||||
label: string;
|
label: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user