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:
@@ -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' ? '✅' : f.status === 'warning' ? '⚠' : '❌';
|
||||
return ic + ' ' + f.name + ': ' + f.score + '/100';
|
||||
}).join(' ') + '</div>';
|
||||
}
|
||||
|
||||
// === Data Loading ===
|
||||
function loadData() {
|
||||
if (!selectedClientId) return;
|
||||
|
||||
Reference in New Issue
Block a user