Add charts for invoice status and bar data; update dashboard layout and styles

This commit is contained in:
Olivia Mestre Llobell 2026-02-03 20:46:02 +01:00
parent eae9f65a87
commit a78ed7268c
5 changed files with 451 additions and 4 deletions

21
package-lock.json generated
View File

@ -7,6 +7,9 @@
"": {
"name": "doli-front",
"version": "0.0.0",
"dependencies": {
"chart.js": "^4.5.1"
},
"devDependencies": {
"vite": "^7.2.4"
}
@ -453,6 +456,12 @@
"node": ">=18"
}
},
"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/@rollup/rollup-android-arm-eabi": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
@ -810,6 +819,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",

View File

@ -10,5 +10,8 @@
},
"devDependencies": {
"vite": "^7.2.4"
},
"dependencies": {
"chart.js": "^4.5.1"
}
}

View File

@ -0,0 +1,79 @@
import { Chart, BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';
Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend);
const DEFAULT_DATA = {
labels: ['T1', 'T2', 'T3', 'T4'],
values: [12500.00, 18300.50, 9800.75, 15400.00]
};
export function renderBarChart(data = DEFAULT_DATA) {
const wrapper = document.createElement('div');
wrapper.className = 'chart-wrapper';
wrapper.innerHTML = `
<h3 class="chart-title">Ingresos por trimestre</h3>
<canvas id="bar-chart"></canvas>
`;
const canvas = wrapper.querySelector('canvas');
const chart = new Chart(canvas, {
type: 'bar',
data: {
labels: data.labels,
datasets: [{
label: 'Ganado (€)',
data: data.values,
backgroundColor: '#3b82f6',
borderRadius: 6,
borderSkipped: false
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (ctx) => {
const value = ctx.parsed.y.toLocaleString('es-ES', { minimumFractionDigits: 2 });
return ` ${value} €`;
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (val) => `${val.toLocaleString('es-ES')} €`
},
grid: {
color: '#f3f4f6'
}
},
x: {
grid: {
display: false
}
}
}
}
});
wrapper._chart = chart;
return wrapper;
}
export function updateBarChart(wrapper, newData) {
const chart = wrapper._chart;
if (!chart) return;
chart.data.labels = newData.labels;
chart.data.datasets[0].data = newData.values;
chart.update();
}

View File

@ -0,0 +1,84 @@
import { Chart, DoughnutController, ArcElement, Tooltip, Legend } from 'chart.js';
Chart.register(DoughnutController, ArcElement, Tooltip, Legend);
const STATUS_LABELS = {
draft: 'Borrador',
unpaid: 'Pendiente',
paid: 'Pagada'
};
const STATUS_COLORS = {
draft: '#6b7280',
unpaid: '#f59e0b',
paid: '#22c55e'
};
const DEFAULT_DATA = {
draft: 1200.50,
unpaid: 3400.00,
paid: 8500.75
};
export function renderInvoiceStatusChart(data = DEFAULT_DATA) {
const wrapper = document.createElement('div');
wrapper.className = 'chart-wrapper';
wrapper.innerHTML = `
<h3 class="chart-title">Facturas por estado</h3>
<canvas id="invoice-status-chart"></canvas>
`;
const canvas = wrapper.querySelector('canvas');
const statuses = Object.keys(data);
const chart = new Chart(canvas, {
type: 'doughnut',
data: {
labels: statuses.map(s => STATUS_LABELS[s] || s),
datasets: [{
data: statuses.map(s => data[s]),
backgroundColor: statuses.map(s => STATUS_COLORS[s] || '#9ca3af'),
borderWidth: 2,
borderColor: '#ffffff'
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 16,
usePointStyle: true,
pointStyleWidth: 10,
font: { size: 13 }
}
},
tooltip: {
callbacks: {
label: (ctx) => {
const value = ctx.parsed.toLocaleString('es-ES', { minimumFractionDigits: 2 });
return ` ${ctx.label}: ${value} €`;
}
}
}
}
}
});
wrapper._chart = chart;
return wrapper;
}
export function updateInvoiceStatusChart(wrapper, newData) {
const chart = wrapper._chart;
if (!chart) return;
const statuses = Object.keys(newData);
chart.data.labels = statuses.map(s => STATUS_LABELS[s] || s);
chart.data.datasets[0].data = statuses.map(s => newData[s]);
chart.data.datasets[0].backgroundColor = statuses.map(s => STATUS_COLORS[s] || '#9ca3af');
chart.update();
}

View File

@ -1,11 +1,271 @@
export function renderDashboard() {
import { renderInvoiceStatusChart } from './charts/InvoiceStatusChart.js';
import { renderBarChart } from './charts/BarChart.js';
// Configuración de las tarjetas de estadísticas
const statsConfig = [
{
id: 'totalFacturado',
title: 'Total Facturado',
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>`,
iconBg: '#374151',
valueColor: '#3b82f6',
subtitle: 'Este mes',
formatValue: (val) => `${val.toLocaleString('es-ES', { minimumFractionDigits: 2 })} €`
},
{
id: 'facturasEmitidas',
title: 'Facturas Emitidas',
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="12" y1="18" x2="12" y2="12"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>`,
iconBg: '#22c55e',
valueColor: '#1f2937',
subtitle: 'Este mes',
formatValue: (val) => val.toString()
},
{
id: 'clientesActivos',
title: 'Clientes Activos',
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>`,
iconBg: '#f59e0b',
valueColor: '#1f2937',
subtitle: 'Con factura este mes',
formatValue: (val) => val.toString()
},
{
id: 'pendienteCobro',
title: 'Pendiente de Cobro',
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>`,
iconBg: '#8b5cf6',
valueColor: '#1f2937',
subtitle: 'Facturas vencidas',
formatValue: (val) => val.toString()
}
];
// Función para crear una tarjeta individual
function createStatCard(config, value) {
return `
<div class="stat-card" data-stat-id="${config.id}">
<div class="stat-icon" style="background-color: ${config.iconBg}">
${config.icon}
</div>
<div class="stat-content">
<span class="stat-title">${config.title}</span>
<span class="stat-value" style="color: ${config.valueColor}">${config.formatValue(value)}</span>
<span class="stat-subtitle">${config.subtitle}</span>
</div>
</div>
`;
}
// Función para renderizar todas las tarjetas
export function renderStatsCards(data = {}) {
const defaultData = {
totalFacturado: 0,
facturasEmitidas: 0,
clientesActivos: 0,
pendienteCobro: 0
};
const statsData = { ...defaultData, ...data };
return statsConfig
.map(config => createStatCard(config, statsData[config.id]))
.join('');
}
// Función para actualizar un valor específico
export function updateStatValue(containerId, statId, newValue) {
const container = document.querySelector(`#${containerId} [data-stat-id="${statId}"] .stat-value`);
if (container) {
const config = statsConfig.find(c => c.id === statId);
if (config) {
container.textContent = config.formatValue(newValue);
}
}
}
// Dashboard completo con estilos incluidos
export function renderDashboard(initialData = {}) {
const container = document.createElement('div');
container.className = 'dashboard-container';
// Datos por defecto (puedes pasarlos como parámetro)
const data = {
totalFacturado: 50.00,
facturasEmitidas: 0,
clientesActivos: 0,
pendienteCobro: 0,
...initialData
};
container.innerHTML = /*html*/`
<h1>Welcome to the Dashboard</h1>
<p>This is the main dashboard page.</p>
<style>
.dashboard-container {
padding: 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.dashboard-text h2 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: #1f2937;
}
.dashboard-text p {
margin: 0 0 24px 0;
color: #6b7280;
font-size: 14px;
}
.data-container {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.stat-card {
display: flex;
align-items: center;
gap: 16px;
background: #ffffff;
border-radius: 16px;
padding: 20px 24px;
flex: 1;
min-width: 200px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
border: 1px solid #f3f4f6;
transition: box-shadow 0.2s ease;
}
.stat-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
.stat-icon svg {
width: 24px;
height: 24px;
}
.stat-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-title {
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
.stat-value {
font-size: 24px;
font-weight: 700;
line-height: 1.2;
}
.stat-subtitle {
font-size: 12px;
color: #9ca3af;
}
.charts-row {
display: flex;
gap: 16px;
}
.chart-wrapper {
flex: 1;
background: #ffffff;
border-radius: 16px;
padding: 24px;
border: 1px solid #f3f4f6;
min-width: 0;
}
.chart-title {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
@media (max-width: 768px) {
.data-container {
flex-direction: column;
}
.stat-card {
min-width: 100%;
}
.charts-row {
flex-direction: column;
}
}
</style>
<div class="dashboard-text">
<h2>Dashboard</h2>
<p>Resumen de facturación y estado de tus facturas</p>
</div>
<div class="data-container" id="stats-container">
${renderStatsCards(data)}
</div>
<div class="charts-row" id="charts-row">
</div>
`;
const chartsRow = container.querySelector('#charts-row');
chartsRow.appendChild(renderInvoiceStatusChart());
chartsRow.appendChild(renderBarChart());
return container;
}
}
// Ejemplo de uso:
// import { renderDashboard, updateStatValue } from './dashboard-stats.js';
//
// // Renderizar con datos iniciales
// const dashboard = renderDashboard({
// totalFacturado: 50.00,
// facturasEmitidas: 0,
// clientesActivos: 0,
// pendienteCobro: 0
// });
// document.body.appendChild(dashboard);
//
// // Actualizar un valor dinámicamente
// updateStatValue('stats-container', 'totalFacturado', 1500.50);