Actualizar server.js
This commit is contained in:
138
server.js
138
server.js
@@ -248,6 +248,20 @@ async function autoUpdateDB() {
|
||||
await client.query(`
|
||||
DO $$ BEGIN
|
||||
|
||||
-- AÑADIDO: Token mágico para el Portal del Cliente
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='clients' AND column_name='portal_token') THEN
|
||||
ALTER TABLE clients ADD COLUMN portal_token TEXT UNIQUE;
|
||||
UPDATE clients SET portal_token = substr(md5(random()::text || id::text), 1, 12) WHERE portal_token IS NULL;
|
||||
ALTER TABLE clients ALTER COLUMN portal_token SET DEFAULT substr(md5(random()::text || clock_timestamp()::text), 1, 12);
|
||||
END IF;
|
||||
|
||||
-- AÑADIDO: Motor de Ranking
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='ranking_score') THEN
|
||||
ALTER TABLE users ADD COLUMN ranking_score NUMERIC DEFAULT 50.0;
|
||||
ALTER TABLE users ADD COLUMN ranking_data JSONB DEFAULT '{}'::jsonb;
|
||||
END IF;
|
||||
|
||||
|
||||
-- NUEVO: Columna para colores personalizados de la App
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='app_settings') THEN
|
||||
ALTER TABLE users ADD COLUMN app_settings JSONB DEFAULT '{"primary": "#1e3a8a", "secondary": "#2563eb", "bg": "#f8fafc"}';
|
||||
@@ -1645,6 +1659,130 @@ app.put("/guilds/:id/ia-rules", authMiddleware, async (req, res) => {
|
||||
} catch (e) { res.status(500).json({ ok: false, error: e.message }); }
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 🏆 MOTOR DE RANKING Y ESTADÍSTICAS
|
||||
// ==========================================
|
||||
|
||||
// Función interna que calcula el algoritmo exacto sobre 100
|
||||
function calculateScore(services) {
|
||||
let score = 0;
|
||||
const now = new Date();
|
||||
const thirtyDaysAgo = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||
|
||||
// Separar servicios
|
||||
const openServices = services.filter(s => !s.is_final);
|
||||
const closedLast30Days = services.filter(s => s.is_final && new Date(s.updated_at) >= thirtyDaysAgo);
|
||||
|
||||
// --- 1. VELOCIDAD DE CIERRE (Max 30 Puntos) ---
|
||||
let scoreCierre = 0;
|
||||
if (closedLast30Days.length > 0) {
|
||||
let totalDaysToClose = 0;
|
||||
closedLast30Days.forEach(s => {
|
||||
const created = new Date(s.created_at);
|
||||
const closed = new Date(s.updated_at);
|
||||
totalDaysToClose += (closed - created) / (1000 * 60 * 60 * 24);
|
||||
});
|
||||
const avgCloseDays = totalDaysToClose / closedLast30Days.length;
|
||||
|
||||
// Algoritmo: <= 2 días = 30 pts. >= 14 días = 0 pts.
|
||||
if (avgCloseDays <= 2) scoreCierre = 30;
|
||||
else if (avgCloseDays >= 14) scoreCierre = 0;
|
||||
else scoreCierre = 30 - ((avgCloseDays - 2) * (30 / 12));
|
||||
}
|
||||
|
||||
// --- 2. CITA RÁPIDA < 24h (Max 30 Puntos) ---
|
||||
let scoreCita = 0;
|
||||
let validSchedules = 0;
|
||||
let fastSchedules = 0;
|
||||
|
||||
const recentServices = services.filter(s => new Date(s.created_at) >= thirtyDaysAgo);
|
||||
recentServices.forEach(s => {
|
||||
const raw = s.raw_data || {};
|
||||
if (raw.scheduled_date) {
|
||||
validSchedules++;
|
||||
const created = new Date(s.created_at);
|
||||
const [y, m, d] = raw.scheduled_date.split('-');
|
||||
const schedDate = new Date(y, m - 1, d);
|
||||
|
||||
const diffDays = (schedDate - created) / (1000 * 60 * 60 * 24);
|
||||
if (diffDays <= 1.5) fastSchedules++;
|
||||
}
|
||||
});
|
||||
|
||||
if (validSchedules > 0) {
|
||||
const fastRatio = fastSchedules / validSchedules;
|
||||
scoreCita = fastRatio * 30;
|
||||
}
|
||||
|
||||
// --- 3. VOLUMEN DE TRABAJO AL DÍA (Max 20 Puntos) ---
|
||||
let scoreVolumen = 0;
|
||||
const closedPerDay = closedLast30Days.length / 22;
|
||||
|
||||
if (closedPerDay >= 3) scoreVolumen = 20;
|
||||
else scoreVolumen = (closedPerDay / 3) * 20;
|
||||
|
||||
// --- 4. PENALIZACIÓN POR RE-CITAS (Max 20 Puntos) ---
|
||||
let scoreRecitas = 20;
|
||||
let totalCalled = 0;
|
||||
recentServices.forEach(s => {
|
||||
const raw = s.raw_data || {};
|
||||
const calls = parseInt(raw.called_times || 0);
|
||||
if (calls > 1) totalCalled += (calls - 1);
|
||||
});
|
||||
|
||||
scoreRecitas = Math.max(0, 20 - (totalCalled * 2));
|
||||
|
||||
// --- SUMA BASE ---
|
||||
let totalScore = scoreCierre + scoreCita + scoreVolumen + scoreRecitas;
|
||||
|
||||
// --- 5. PENALIZACIÓN POR ACUMULACIÓN DE ABIERTOS ---
|
||||
let penalizacionAbiertos = 0;
|
||||
if (openServices.length > 15) {
|
||||
penalizacionAbiertos = (openServices.length - 15) * 1.5;
|
||||
}
|
||||
|
||||
totalScore -= penalizacionAbiertos;
|
||||
totalScore = Math.min(100, Math.max(0, totalScore));
|
||||
|
||||
return {
|
||||
score: Math.round(totalScore),
|
||||
details: {
|
||||
cierre: Math.round(scoreCierre),
|
||||
cita: Math.round(scoreCita),
|
||||
volumen: Math.round(scoreVolumen),
|
||||
recitas: Math.round(scoreRecitas),
|
||||
penalizacion: Math.round(penalizacionAbiertos),
|
||||
abiertos: openServices.length,
|
||||
cerrados_mes: closedLast30Days.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// RUTA GET PARA EL RANKING
|
||||
app.get("/ranking", authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const q = await pool.query(`
|
||||
SELECT id, created_at, updated_at, raw_data,
|
||||
(SELECT is_final FROM service_statuses WHERE id::text = raw_data->>'status_operativo') as is_final
|
||||
FROM scraped_services
|
||||
WHERE assigned_to = $1
|
||||
`, [req.user.sub]);
|
||||
|
||||
const rankingData = calculateScore(q.rows);
|
||||
|
||||
await pool.query(
|
||||
"UPDATE users SET ranking_score = $1, ranking_data = $2 WHERE id = $3",
|
||||
[rankingData.score, rankingData.details, req.user.sub]
|
||||
);
|
||||
|
||||
res.json({ ok: true, ranking: rankingData });
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error en ranking:", error);
|
||||
res.status(500).json({ ok: false });
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 🕒 EL RELOJ DEL SISTEMA (Ejecutar cada minuto)
|
||||
// ==========================================
|
||||
|
||||
Reference in New Issue
Block a user