feat: Implementar PWA, Analytics, Reportes PDF y mejoras OCR
FASE 1 - PWA y Frontend: - Crear templates/base.html, dashboard.html, analytics.html, executive.html - Crear static/css/main.css con diseño responsivo - Agregar static/js/app.js, pwa.js, camera.js, charts.js - Implementar manifest.json y service-worker.js para PWA - Soporte para captura de tickets desde cámara móvil FASE 2 - Analytics: - Crear módulo analytics/ con predictions.py, trends.py, comparisons.py - Implementar predicción básica con promedio móvil + tendencia lineal - Agregar endpoints /api/analytics/trends, predictions, comparisons - Integrar Chart.js para gráficas interactivas FASE 3 - Reportes PDF: - Crear módulo reports/ con pdf_generator.py - Implementar SalesReportPDF con generar_reporte_diario y ejecutivo - Agregar comando /reporte [diario|semanal|ejecutivo] - Agregar endpoints /api/reports/generate y /api/reports/download FASE 4 - Mejoras OCR: - Crear módulo ocr/ con processor.py, preprocessor.py, patterns.py - Implementar AmountDetector con patrones múltiples de montos - Agregar preprocesador adaptativo con pipelines para diferentes condiciones - Soporte para corrección de rotación (deskew) y threshold Otsu Dependencias agregadas: - reportlab, matplotlib (PDF) - scipy, pandas (analytics) - imutils, deskew, cachetools (OCR) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
266
sales-bot/static/js/charts.js
Normal file
266
sales-bot/static/js/charts.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Sales Bot - Chart.js Integration and Chart Utilities
|
||||
*/
|
||||
|
||||
// Chart default configuration
|
||||
Chart.defaults.color = '#888';
|
||||
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
|
||||
Chart.defaults.font.family = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
||||
|
||||
// Color palette
|
||||
const ChartColors = {
|
||||
primary: '#00d4ff',
|
||||
secondary: '#00ff88',
|
||||
warning: '#ffaa00',
|
||||
danger: '#ff4444',
|
||||
purple: '#aa00ff',
|
||||
gradient: (ctx, color1, color2) => {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 300);
|
||||
gradient.addColorStop(0, color1);
|
||||
gradient.addColorStop(1, color2);
|
||||
return gradient;
|
||||
}
|
||||
};
|
||||
|
||||
// Chart factory
|
||||
const ChartFactory = {
|
||||
// Line chart for trends
|
||||
createTrendChart(canvasId, data, options = {}) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return null;
|
||||
|
||||
return new Chart(ctx.getContext('2d'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: [{
|
||||
label: data.label || 'Datos',
|
||||
data: data.values || [],
|
||||
borderColor: options.color || ChartColors.primary,
|
||||
backgroundColor: options.fill ?
|
||||
ChartColors.gradient(ctx.getContext('2d'), 'rgba(0, 212, 255, 0.3)', 'rgba(0, 212, 255, 0)') :
|
||||
'transparent',
|
||||
fill: options.fill !== false,
|
||||
tension: 0.4,
|
||||
pointRadius: options.points ? 4 : 0,
|
||||
pointHoverRadius: 6
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: options.legend !== false,
|
||||
labels: { color: '#888' }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(26, 26, 46, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#888',
|
||||
borderColor: ChartColors.primary,
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
label: (ctx) => options.formatValue ?
|
||||
options.formatValue(ctx.parsed.y) :
|
||||
ctx.parsed.y
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||||
ticks: { color: '#888', maxRotation: 45 }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||||
ticks: {
|
||||
color: '#888',
|
||||
callback: options.formatYAxis || ((value) => value)
|
||||
},
|
||||
beginAtZero: options.beginAtZero !== false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Bar chart for comparisons
|
||||
createBarChart(canvasId, data, options = {}) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return null;
|
||||
|
||||
return new Chart(ctx.getContext('2d'), {
|
||||
type: options.horizontal ? 'bar' : 'bar',
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: [{
|
||||
label: data.label || 'Datos',
|
||||
data: data.values || [],
|
||||
backgroundColor: data.colors || [
|
||||
ChartColors.primary,
|
||||
ChartColors.secondary,
|
||||
ChartColors.warning,
|
||||
ChartColors.purple
|
||||
],
|
||||
borderRadius: 8,
|
||||
barThickness: options.barThickness || 'flex'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: options.horizontal ? 'y' : 'x',
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(26, 26, 46, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#888',
|
||||
borderColor: ChartColors.primary,
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
callbacks: {
|
||||
label: (ctx) => options.formatValue ?
|
||||
options.formatValue(ctx.parsed[options.horizontal ? 'x' : 'y']) :
|
||||
ctx.parsed[options.horizontal ? 'x' : 'y']
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: !options.horizontal },
|
||||
ticks: { color: '#888' }
|
||||
},
|
||||
y: {
|
||||
grid: { color: options.horizontal ? 'transparent' : 'rgba(255,255,255,0.05)' },
|
||||
ticks: { color: '#888' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Doughnut chart for distribution
|
||||
createDoughnutChart(canvasId, data, options = {}) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return null;
|
||||
|
||||
return new Chart(ctx.getContext('2d'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: [{
|
||||
data: data.values || [],
|
||||
backgroundColor: data.colors || [
|
||||
ChartColors.primary,
|
||||
ChartColors.secondary,
|
||||
ChartColors.warning,
|
||||
ChartColors.purple,
|
||||
ChartColors.danger
|
||||
],
|
||||
borderWidth: 0,
|
||||
cutout: options.cutout || '70%'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: options.legend !== false,
|
||||
position: options.legendPosition || 'bottom',
|
||||
labels: { color: '#888', padding: 15 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(26, 26, 46, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#888',
|
||||
borderColor: ChartColors.primary,
|
||||
borderWidth: 1,
|
||||
padding: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Multi-line chart for comparisons
|
||||
createMultiLineChart(canvasId, datasets, labels, options = {}) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return null;
|
||||
|
||||
const colors = [ChartColors.primary, ChartColors.secondary, ChartColors.warning, ChartColors.purple];
|
||||
|
||||
return new Chart(ctx.getContext('2d'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets.map((ds, i) => ({
|
||||
label: ds.label,
|
||||
data: ds.values,
|
||||
borderColor: ds.color || colors[i % colors.length],
|
||||
backgroundColor: 'transparent',
|
||||
borderDash: ds.dashed ? [5, 5] : [],
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 6
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: { color: '#888' }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(26, 26, 46, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#888',
|
||||
borderColor: ChartColors.primary,
|
||||
borderWidth: 1,
|
||||
padding: 12
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||||
ticks: { color: '#888' }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||||
ticks: { color: '#888' },
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to format currency in charts
|
||||
function formatCurrency(value) {
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: 'MXN',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
// Export
|
||||
window.ChartFactory = ChartFactory;
|
||||
window.ChartColors = ChartColors;
|
||||
window.formatCurrency = formatCurrency;
|
||||
Reference in New Issue
Block a user