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:
2026-01-19 03:26:16 +00:00
parent ed1658eb2b
commit 9936deaa90
25 changed files with 5501 additions and 282 deletions

View 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;