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",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -10,5 +10,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"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');
|
||||
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);
|
||||
Loading…
Reference in New Issue