Add charts for invoice status and bar data; update dashboard layout and styles
This commit is contained in:
parent
eae9f65a87
commit
a78ed7268c
|
|
@ -7,6 +7,9 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "doli-front",
|
"name": "doli-front",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"chart.js": "^4.5.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^7.2.4"
|
"vite": "^7.2.4"
|
||||||
}
|
}
|
||||||
|
|
@ -453,6 +456,12 @@
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.55.1",
|
"version": "4.55.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
|
||||||
|
|
@ -810,6 +819,18 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^7.2.4"
|
"vite": "^7.2.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chart.js": "^4.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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');
|
const container = document.createElement('div');
|
||||||
container.className = 'dashboard-container';
|
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*/`
|
container.innerHTML = /*html*/`
|
||||||
<h1>Welcome to the Dashboard</h1>
|
<style>
|
||||||
<p>This is the main dashboard page.</p>
|
.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;
|
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);
|
||||||
Loading…
Reference in New Issue