feat: BI-CCC evolution — 6-phase platform upgrade (45→85 maturity)

Phase 1: Refactor queries.js (1787 lines) into domain modules with facade pattern
  - src/queries/{helpers,payin,payout,corporate,bi,client,provider,compliance}.queries.js
  - New provider performance + compliance data layer queries
  - Health check endpoint (GET /health)

Phase 2: Provider Performance Dashboard (src/admin-providers.js)
  - Hero cards, sortable tables, Chart.js charts, date range filter
  - API routes: /admin/api/providers, /admin/api/providers/failed, /admin/api/providers/trend

Phase 3: Excel Export (exceljs)
  - CambioReal-branded exports for BI, clients, providers, transactions
  - Export buttons added to BI and Client 360 dashboards

Phase 4: Alert System (node-cron + nodemailer)
  - 5 alert rules: volume spike, spread anomaly, large tx, failed tx spike, provider inactivity
  - SQLite alerts table, bell icon UI with acknowledge workflow
  - Email notifications via SMTP

Phase 5: Enhanced Analytics
  - Churn prediction: weighted RFM model (src/services/churn-predictor.js)
  - Volume forecasting: exponential smoothing with confidence bands (src/services/forecast.js)
  - Forecast chart in BI dashboard, churn risk in Client 360

Phase 6: SQLite Analytics Store (ETL)
  - src/db-analytics.js: daily_metrics, client_health_daily, monthly_revenue tables
  - src/etl/daily-sync.js: MySQL RDS → SQLite daily sync at 1 AM + 90-day backfill
  - src/etl/data-quality.js: post-sync validation (row counts, reconciliation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-16 20:22:23 -05:00
parent a76ab30730
commit 844f931076
25 changed files with 5686 additions and 1786 deletions

View File

@@ -205,6 +205,9 @@ function buildAdminClienteHTML(user) {
.date-inputs label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.date-inputs input[type="date"] { padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: inherit; background: var(--bg); color: var(--text); }
.period-info { margin-left: auto; font-size: 12px; color: var(--text-muted); font-weight: 500; background: var(--bg); padding: 6px 12px; border-radius: 6px; }
.export-btn { background: var(--green); color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: all 0.15s; }
.export-btn:hover { opacity: 0.85; transform: translateY(-1px); }
[data-theme="dark"] .export-btn { background: rgba(0,255,136,0.15); color: #00FF88; border: 1px solid rgba(0,255,136,0.3); }
/* === Hero KPIs === */
.hero-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; margin-bottom: 28px; }
@@ -413,6 +416,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
<div class="health-score-number" id="healthScoreNum">--</div>
<div class="health-score-label" id="healthScoreLabel">Health</div>
</div>
<div id="churnRisk" style="padding:0 16px 16px;"></div>
</div>
<!-- Date Filter -->
@@ -431,6 +435,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
<label>Ate:</label><input type="date" id="dateEnd" value="${today}">
</div>
<span class="period-info" id="periodInfo">--</span>
<button class="export-btn" onclick="window.location.href='/admin/api/export/clients-excel'" title="Export Top Clients to Excel">Export Excel</button>
</div>
<!-- Hero KPIs (6) -->
@@ -904,6 +909,10 @@ function clearClient() {
function loadProfile() {
fetch('/admin/api/cliente/' + selectedClientId + '/profile').then(function(r){return r.json();}).then(function(data) {
profileData = data; renderProfile(data);
// Load churn risk
fetch('/admin/api/cliente/' + selectedClientId + '/churn').then(function(r){return r.json();}).then(function(churn) {
renderChurnRisk(churn);
}).catch(function(){});
});
}
function renderProfile(p) {
@@ -932,6 +941,23 @@ function renderProfile(p) {
});
}
function renderChurnRisk(churn) {
var el = document.getElementById('churnRisk');
if (!el) return;
var colors = { low: 'var(--green)', medium: 'var(--orange)', high: 'var(--red)', critical: 'var(--red)' };
var labels = { low: 'Low Risk', medium: 'Medium Risk', high: 'High Risk', critical: 'Critical' };
el.innerHTML = '<div style="display:flex;align-items:center;gap:8px;margin-top:8px;">' +
'<div style="width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;' +
'background:' + colors[churn.risk] + '20;color:' + colors[churn.risk] + ';font-weight:700;font-size:14px;">' + churn.score + '</div>' +
'<div><div style="font-size:12px;font-weight:600;color:' + colors[churn.risk] + '">' + labels[churn.risk] + '</div>' +
'<div style="font-size:10px;color:var(--text-muted)">Health: ' + churn.health_score + '/100</div></div></div>' +
'<div style="margin-top:6px;font-size:10px;color:var(--text-muted)">' +
(churn.factors || []).slice(0,3).map(function(f){
var ic = f.status === 'good' ? '&#x2705;' : f.status === 'warning' ? '&#x26A0;' : '&#x274C;';
return ic + ' ' + f.name + ': ' + f.score + '/100';
}).join(' &nbsp; ') + '</div>';
}
// === Data Loading ===
function loadData() {
if (!selectedClientId) return;