diff --git a/package-lock.json b/package-lock.json index 3dc0ea2..3dbed70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4ff63ae..3f7e936 100644 --- a/package.json +++ b/package.json @@ -10,5 +10,8 @@ }, "devDependencies": { "vite": "^7.2.4" + }, + "dependencies": { + "chart.js": "^4.5.1" } } diff --git a/src/components/charts/BarChart.js b/src/components/charts/BarChart.js new file mode 100644 index 0000000..ce4d9c2 --- /dev/null +++ b/src/components/charts/BarChart.js @@ -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 = ` +

Ingresos por trimestre

+ + `; + + 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(); +} diff --git a/src/components/charts/InvoiceStatusChart.js b/src/components/charts/InvoiceStatusChart.js new file mode 100644 index 0000000..db545a7 --- /dev/null +++ b/src/components/charts/InvoiceStatusChart.js @@ -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 = ` +

Facturas por estado

+ + `; + + 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(); +} diff --git a/src/components/dashboard.js b/src/components/dashboard.js index 3dfdb73..8c42603 100644 --- a/src/components/dashboard.js +++ b/src/components/dashboard.js @@ -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: ` + + + + `, + iconBg: '#374151', + valueColor: '#3b82f6', + subtitle: 'Este mes', + formatValue: (val) => `${val.toLocaleString('es-ES', { minimumFractionDigits: 2 })} €` + }, + { + id: 'facturasEmitidas', + title: 'Facturas Emitidas', + icon: ` + + + + + `, + iconBg: '#22c55e', + valueColor: '#1f2937', + subtitle: 'Este mes', + formatValue: (val) => val.toString() + }, + { + id: 'clientesActivos', + title: 'Clientes Activos', + icon: ` + + + + + `, + iconBg: '#f59e0b', + valueColor: '#1f2937', + subtitle: 'Con factura este mes', + formatValue: (val) => val.toString() + }, + { + id: 'pendienteCobro', + title: 'Pendiente de Cobro', + icon: ` + + + `, + iconBg: '#8b5cf6', + valueColor: '#1f2937', + subtitle: 'Facturas vencidas', + formatValue: (val) => val.toString() + } +]; + +// Función para crear una tarjeta individual +function createStatCard(config, value) { + return ` +
+
+ ${config.icon} +
+
+ ${config.title} + ${config.formatValue(value)} + ${config.subtitle} +
+
+ `; +} + +// 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*/` -

Welcome to the Dashboard

-

This is the main dashboard page.

+ + +
+

Dashboard

+

Resumen de facturación y estado de tus facturas

+
+ +
+ ${renderStatsCards(data)} +
+ +
+
`; + const chartsRow = container.querySelector('#charts-row'); + chartsRow.appendChild(renderInvoiceStatusChart()); + chartsRow.appendChild(renderBarChart()); + return container; -} \ No newline at end of file +} + +// 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); \ No newline at end of file