feat: Implementa Dashboard de estadísticas visuales del inventario
Se añade una nueva sección "Dashboard" para proporcionar una visión general y analítica del inventario de IT. Esta vista transforma los datos brutos en gráficos interactivos, facilitando la toma de decisiones y la comprensión del estado del parque informático. La implementación se realizó de forma iterativa, refinando tanto la obtención de datos como la presentación visual para una mejor experiencia de usuario. **Principales Cambios:** **1. Backend:** - Se crea un nuevo `DashboardController` con el endpoint `GET /api/dashboard/stats`. - Se implementan consultas SQL agregadas para obtener estadísticas por: - Sistema Operativo. - Cantidad de equipos por Sector (mostrando todos los sectores). - Modelos de CPU (mostrando todos los modelos). - Cantidad de Memoria RAM instalada (GB). **2. Frontend:** - Se integran las librerías `chart.js` y `react-chartjs-2` para la visualización de datos. - Se añade una nueva vista "Dashboard" accesible desde la barra de navegación principal. - Se crean componentes reutilizables para cada tipo de gráfico: `OsChart`, `RamChart`, `CpuChart` y `SectorChart`. - Se diseña un layout responsivo en formato de grilla 2x2 para una visualización equilibrada de los cuatro gráficos. **3. Mejoras de Experiencia de Usuario (UX):** - Se utilizan gráficos de barras horizontales (`indexAxis: 'y'`) para mejorar la legibilidad de etiquetas largas, como los modelos de CPU y nombres de sectores. - Se implementan contenedores con scroll vertical (`overflow-y: auto`) para los gráficos de CPU y Sectores. Esto permite mostrar la totalidad de los datos sin comprometer el diseño ni crear gráficos excesivamente grandes. - Se calcula una altura dinámica para los gráficos de barras para que se adapten a la cantidad de datos que contienen, mejorando la presentación.
This commit is contained in:
84
backend/Controllers/DashboardController.cs
Normal file
84
backend/Controllers/DashboardController.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Dapper;
|
||||
using Inventario.API.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Inventario.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DashboardController : ControllerBase
|
||||
{
|
||||
private readonly DapperContext _context;
|
||||
|
||||
public DashboardController(DapperContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public class StatItemDto
|
||||
{
|
||||
// Cambiamos el tipo de Label a string para que Dapper no tenga problemas
|
||||
// al leer el ram_installed (que es un int). Lo formatearemos en el frontend.
|
||||
public string Label { get; set; } = "";
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class DashboardStatsDto
|
||||
{
|
||||
public IEnumerable<StatItemDto> OsStats { get; set; } = new List<StatItemDto>();
|
||||
public IEnumerable<StatItemDto> SectorStats { get; set; } = new List<StatItemDto>();
|
||||
public IEnumerable<StatItemDto> CpuStats { get; set; } = new List<StatItemDto>();
|
||||
public IEnumerable<StatItemDto> RamStats { get; set; } = new List<StatItemDto>(); // <-- 1. Añadir propiedad para RAM
|
||||
}
|
||||
|
||||
[HttpGet("stats")]
|
||||
public async Task<IActionResult> GetDashboardStats()
|
||||
{
|
||||
var osQuery = @"
|
||||
SELECT Os AS Label, COUNT(Id) AS Count
|
||||
FROM dbo.equipos
|
||||
WHERE Os IS NOT NULL AND Os != ''
|
||||
GROUP BY Os
|
||||
ORDER BY Count DESC;";
|
||||
|
||||
var sectorQuery = @"
|
||||
SELECT s.Nombre AS Label, COUNT(e.Id) AS Count
|
||||
FROM dbo.equipos e
|
||||
JOIN dbo.sectores s ON e.sector_id = s.id
|
||||
GROUP BY s.Nombre
|
||||
ORDER BY Count DESC;";
|
||||
|
||||
var cpuQuery = @"
|
||||
SELECT Cpu AS Label, COUNT(Id) AS Count
|
||||
FROM dbo.equipos
|
||||
WHERE Cpu IS NOT NULL AND Cpu != ''
|
||||
GROUP BY Cpu
|
||||
ORDER BY Count DESC;";
|
||||
|
||||
var ramQuery = @"
|
||||
SELECT CAST(ram_installed AS VARCHAR) AS Label, COUNT(Id) AS Count
|
||||
FROM dbo.equipos
|
||||
WHERE ram_installed > 0
|
||||
GROUP BY ram_installed
|
||||
ORDER BY ram_installed ASC;";
|
||||
|
||||
using (var connection = _context.CreateConnection())
|
||||
{
|
||||
var osStats = await connection.QueryAsync<StatItemDto>(osQuery);
|
||||
var sectorStats = await connection.QueryAsync<StatItemDto>(sectorQuery);
|
||||
var cpuStats = await connection.QueryAsync<StatItemDto>(cpuQuery);
|
||||
var ramStats = await connection.QueryAsync<StatItemDto>(ramQuery);
|
||||
|
||||
var result = new DashboardStatsDto
|
||||
{
|
||||
OsStats = osStats,
|
||||
SectorStats = sectorStats,
|
||||
CpuStats = cpuStats,
|
||||
RamStats = ramStats
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Inventario.API")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+afd378712c9176f32a705fb3a10d2d56ed6c8ba2")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8162d59331f63963077dd822669378174380b386")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Inventario.API")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Inventario.API")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@@ -9,7 +9,9 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"chart.js": "^4.5.0",
|
||||
"react": "^19.1.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-tooltip": "^5.29.1"
|
||||
@@ -1034,6 +1036,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1987,6 +1995,18 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
|
||||
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
@@ -2985,6 +3005,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-chartjs-2": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
|
||||
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"chart.js": "^4.5.0",
|
||||
"react": "^19.1.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-tooltip": "^5.29.1"
|
||||
|
||||
@@ -2,10 +2,11 @@ import { useState } from 'react';
|
||||
import SimpleTable from "./components/SimpleTable";
|
||||
import GestionSectores from "./components/GestionSectores";
|
||||
import GestionComponentes from './components/GestionComponentes';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import Navbar from './components/Navbar';
|
||||
import './App.css';
|
||||
|
||||
export type View = 'equipos' | 'sectores' | 'admin';
|
||||
export type View = 'equipos' | 'sectores' | 'admin' | 'dashboard';
|
||||
|
||||
function App() {
|
||||
const [currentView, setCurrentView] = useState<View>('equipos');
|
||||
@@ -18,6 +19,7 @@ function App() {
|
||||
{currentView === 'equipos' && <SimpleTable />}
|
||||
{currentView === 'sectores' && <GestionSectores />}
|
||||
{currentView === 'admin' && <GestionComponentes />}
|
||||
{currentView === 'dashboard' && <Dashboard />}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
59
frontend/src/components/CpuChart.tsx
Normal file
59
frontend/src/components/CpuChart.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import type { StatItem } from '../types/interfaces';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||
|
||||
interface Props {
|
||||
data: StatItem[];
|
||||
}
|
||||
|
||||
const CpuChart: React.FC<Props> = ({ data }) => {
|
||||
// Altura dinámica: 40px de base + 20px por cada CPU
|
||||
const chartHeight = 40 + data.length * 20;
|
||||
|
||||
const options = {
|
||||
indexAxis: 'y' as const,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Equipos por CPU',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: { beginAtZero: true, ticks: { stepSize: 1 } },
|
||||
y: { ticks: { font: { size: 10 } } }
|
||||
}
|
||||
};
|
||||
|
||||
const chartData = {
|
||||
labels: data.map(item => item.label),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Nº de Equipos',
|
||||
data: data.map(item => item.count),
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.8)',
|
||||
borderColor: 'rgba(153, 102, 255, 1)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Envolvemos la barra en un div con altura calculada
|
||||
return (
|
||||
<div style={{ position: 'relative', height: `${chartHeight}px`, minHeight: '400px' }}>
|
||||
<Bar options={options} data={chartData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CpuChart;
|
||||
42
frontend/src/components/Dashboard.module.css
Normal file
42
frontend/src/components/Dashboard.module.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.dashboardHeader {
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dashboardHeader h2 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 300;
|
||||
color: #343a40;
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
/* Grilla simple de 2 columnas */
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chartContainer {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 450px; /* Altura mínima para todos los gráficos */
|
||||
}
|
||||
|
||||
/* Contenedor especial para los gráficos de barras horizontales con scroll */
|
||||
.scrollableChartContainer {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.statsGrid {
|
||||
grid-template-columns: 1fr; /* Una sola columna en pantallas pequeñas */
|
||||
}
|
||||
}
|
||||
71
frontend/src/components/Dashboard.tsx
Normal file
71
frontend/src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { dashboardService } from '../services/apiService';
|
||||
import type { DashboardStats } from '../types/interfaces';
|
||||
import OsChart from './OsChart';
|
||||
import SectorChart from './SectorChart';
|
||||
import CpuChart from './CpuChart';
|
||||
import RamChart from './RamChart'; // <-- 1. Importar el nuevo gráfico
|
||||
import styles from './Dashboard.module.css';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
dashboardService.getStats()
|
||||
.then(data => {
|
||||
setStats(data);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('No se pudieron cargar las estadísticas del dashboard.');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.dashboardHeader}>
|
||||
<h2>Cargando estadísticas...</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!stats) {
|
||||
return (
|
||||
<div className={styles.dashboardHeader}>
|
||||
<h2>No hay datos disponibles para mostrar.</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.dashboardHeader}>
|
||||
<h2>Dashboard de Inventario</h2>
|
||||
</div>
|
||||
<div className={styles.statsGrid}>
|
||||
{/* Fila 1, Columna 1 */}
|
||||
<div className={styles.chartContainer}>
|
||||
<OsChart data={stats.osStats} />
|
||||
</div>
|
||||
|
||||
{/* Fila 1, Columna 2 */}
|
||||
<div className={styles.chartContainer}>
|
||||
<RamChart data={stats.ramStats} />
|
||||
</div>
|
||||
|
||||
{/* Fila 2, Columna 1 */}
|
||||
<div className={`${styles.chartContainer} ${styles.scrollableChartContainer}`}>
|
||||
<CpuChart data={stats.cpuStats} />
|
||||
</div>
|
||||
|
||||
{/* Fila 2, Columna 2 */}
|
||||
<div className={`${styles.chartContainer} ${styles.scrollableChartContainer}`}>
|
||||
<SectorChart data={stats.sectorStats} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,7 +1,6 @@
|
||||
// frontend/src/components/Navbar.tsx
|
||||
import React from 'react';
|
||||
import type { View } from '../App'; // Importaremos el tipo desde App.tsx
|
||||
import '../App.css'; // Usaremos los estilos globales que acabamos de crear
|
||||
import type { View } from '../App';
|
||||
import '../App.css';
|
||||
|
||||
interface NavbarProps {
|
||||
currentView: View;
|
||||
@@ -33,6 +32,12 @@ const Navbar: React.FC<NavbarProps> = ({ currentView, setCurrentView }) => {
|
||||
>
|
||||
Administración
|
||||
</button>
|
||||
<button
|
||||
className={`nav-link ${currentView === 'dashboard' ? 'nav-link-active' : ''}`}
|
||||
onClick={() => setCurrentView('dashboard')}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
|
||||
49
frontend/src/components/OsChart.tsx
Normal file
49
frontend/src/components/OsChart.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend, Title } from 'chart.js';
|
||||
import { Doughnut } from 'react-chartjs-2';
|
||||
import type { StatItem } from '../types/interfaces';
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend, Title);
|
||||
|
||||
interface Props {
|
||||
data: StatItem[];
|
||||
}
|
||||
|
||||
const OsChart: React.FC<Props> = ({ data }) => {
|
||||
const chartData = {
|
||||
labels: data.map(item => item.label),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Nº de Equipos',
|
||||
data: data.map(item => item.count),
|
||||
backgroundColor: [
|
||||
'#4E79A7', '#F28E2B', '#E15759', '#76B7B2', '#59A14F',
|
||||
'#EDC948', '#B07AA1', '#FF9DA7', '#9C755F', '#BAB0AC'
|
||||
],
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right' as const, // <-- ¡CAMBIO CLAVE! Mueve la leyenda a la derecha
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Distribución por Sistema Operativo',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return <Doughnut data={chartData} options={options} />;
|
||||
};
|
||||
|
||||
export default OsChart;
|
||||
55
frontend/src/components/RamChart.tsx
Normal file
55
frontend/src/components/RamChart.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import type { StatItem } from '../types/interfaces';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||
|
||||
interface Props {
|
||||
data: StatItem[];
|
||||
}
|
||||
|
||||
const RamChart: React.FC<Props> = ({ data }) => {
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Distribución de Memoria RAM',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 2 // Escala de 2 en 2 para que sea más legible
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chartData = {
|
||||
// Formateamos la etiqueta para que diga "X GB"
|
||||
labels: data.map(item => `${item.label} GB`),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Nº de Equipos',
|
||||
data: data.map(item => item.count),
|
||||
backgroundColor: 'rgba(225, 87, 89, 0.8)', // Un color rojo/coral
|
||||
borderColor: 'rgba(225, 87, 89, 1)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return <Bar options={options} data={chartData} />;
|
||||
};
|
||||
|
||||
export default RamChart;
|
||||
58
frontend/src/components/SectorChart.tsx
Normal file
58
frontend/src/components/SectorChart.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import type { StatItem } from '../types/interfaces';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||
|
||||
interface Props {
|
||||
data: StatItem[];
|
||||
}
|
||||
|
||||
const SectorChart: React.FC<Props> = ({ data }) => {
|
||||
// Altura dinámica: 40px de base + 25px por cada sector
|
||||
const chartHeight = 40 + data.length * 25;
|
||||
|
||||
const options = {
|
||||
indexAxis: 'y' as const,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Equipos por Sector',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: { beginAtZero: true, ticks: { stepSize: 2 } }
|
||||
}
|
||||
};
|
||||
|
||||
const chartData = {
|
||||
labels: data.map(item => item.label),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Nº de Equipos',
|
||||
data: data.map(item => item.count),
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.8)',
|
||||
borderColor: 'rgba(76, 175, 80, 1)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Envolvemos la barra en un div con altura calculada
|
||||
return (
|
||||
<div style={{ position: 'relative', height: `${chartHeight}px`, minHeight: '400px' }}>
|
||||
<Bar options={options} data={chartData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectorChart;
|
||||
@@ -1,6 +1,6 @@
|
||||
// frontend/src/services/apiService.ts
|
||||
|
||||
import type { Equipo, Sector, HistorialEquipo, Usuario, MemoriaRam } from '../types/interfaces';
|
||||
import type { Equipo, Sector, HistorialEquipo, Usuario, MemoriaRam, DashboardStats } from '../types/interfaces';
|
||||
|
||||
const BASE_URL = '/api';
|
||||
|
||||
@@ -118,3 +118,7 @@ export const adminService = {
|
||||
|
||||
deleteTextComponent: (type: string, value: string) => request<void>(`${BASE_URL}/admin/componentes/${type}/${encodeURIComponent(value)}`, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
export const dashboardService = {
|
||||
getStats: () => request<DashboardStats>(`${BASE_URL}/dashboard/stats`),
|
||||
};
|
||||
@@ -81,3 +81,17 @@ export interface Equipo {
|
||||
memoriasRam: MemoriaRamEquipoDetalle[];
|
||||
historial: HistorialEquipo[];
|
||||
}
|
||||
|
||||
// --- Interfaces para el Dashboard ---
|
||||
|
||||
export interface StatItem {
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
osStats: StatItem[];
|
||||
sectorStats: StatItem[];
|
||||
cpuStats: StatItem[];
|
||||
ramStats: StatItem[];
|
||||
}
|
||||
Reference in New Issue
Block a user