From 8dfa3bf1378f6e2a69efb45f973b616a89b3ff40 Mon Sep 17 00:00:00 2001 From: kvilches1410 Date: Sat, 4 Apr 2026 00:49:55 -0300 Subject: [PATCH] CR + Tooltips + Promedios --- index.html | 64 ++++++++++++++++++++++++----- js/app.js | 43 +++++++++++++++++++- netlify/functions/data.js | 86 ++++++++++++++++++++++++++++++++++----- 3 files changed, 172 insertions(+), 21 deletions(-) diff --git a/index.html b/index.html index 413ebc8..beef7d6 100644 --- a/index.html +++ b/index.html @@ -59,9 +59,9 @@
-
—
SP En Progreso
Trabajo en curso
-
—
SP Por Hacer
Backlog del periodo
-
—
Devs con Carga
Con tickets pendientes
+
—
SP En Progreso
Trabajo en curso
+
—
SP Por Hacer
Backlog del periodo
+
—
Devs con Carga
Con tickets pendientes
@@ -103,9 +103,14 @@
-
—
SP Completados
en el período
-
—
Desarrolladores
que completaron SP
-
—
SP promedio / dev
en el período
+
—
SP Completados
en el período
+
—
Desarrolladores
que completaron SP
+
—
SP promedio / dev
en el período
+
+
+
—
Promedio Lead Time
Días (Creación a Fin)
+
—
Promedio Cycle Time
Días (En Curso a Fin)
+
—
Desviación SP
Original vs Final
@@ -133,6 +138,45 @@ + +
+
+

Revisión de Código (Code Review)

+ Métricas de revisión entre pares antes de QA +
+ +
+
+
+
+
—
+
Tickets en CR
+
Procesados en el período
+
+
+
—
+
Tiempo Prom. CR
+
Horas promedio por ticket
+
+
+
—
+
Tickets Atascados
+
Demorados actualmente en revisión
+
+
+ +
+
+

📈 Tiempo Promedio en Code Review — Mes a Mes

+
Evolución de la velocidad de revisión de pares (horas)
+
+
+
+
+
+ + +
@@ -145,22 +189,22 @@
-
+
—
Tickets QA
Procesados en el período
-
+
—
Tiempo Prom. QA
Horas promedio por ticket
-
+
—
Tiempo Prom. en Cola
Horas esperando READY TO QA
-
+
—
Tickets Devueltos
Volvieron a En Curso
diff --git a/js/app.js b/js/app.js index 1ce4068..79b6e0f 100644 --- a/js/app.js +++ b/js/app.js @@ -70,6 +70,7 @@ async function loadAndRender(forceRefresh = false) { renderCurrentSection(); renderHistorySection(); + renderCRSection(); renderQASection(); } catch(e) { @@ -280,6 +281,17 @@ function renderHistorySection() { }); } + // Delivery KPIs + if (data.deliveryData) { + const { avgLeadTime, avgCycleTime, spOriginal, spFinal, spDeviationPct } = data.deliveryData; + document.getElementById('kpi-lead-time').textContent = avgLeadTime; + document.getElementById('kpi-cycle-time').textContent = avgCycleTime; + + const deviationPrefix = spDeviationPct > 0 ? '+' : ''; + document.getElementById('kpi-sp-deviation').textContent = `${deviationPrefix}${spDeviationPct}%`; + document.getElementById('kpi-sp-deviation-sub').textContent = `${spOriginal} orig. → ${spFinal} fin.`; + } + // Gráfico de Velocidad Mensual const months = Object.keys(data.monthlyDone).sort(); const allDevsInHistory = [...new Set(months.flatMap(m => Object.keys(data.monthlyDone[m])))]; @@ -294,6 +306,34 @@ function renderHistorySection() { })), { plugins:{ legend:{ position:'bottom'} }, scales:{ x:{stacked:true,grid:{display:false}}, y:{stacked:true} } }); } +// ── CODE REVIEW SECTION ──────────────────────────────────────────────────── +function renderCRSection() { + const data = api.data; + if (!data || !data.crData) return; + + const cr = data.crData; + + document.getElementById('kpi-cr-tickets').textContent = cr.tickets ?? '—'; + document.getElementById('kpi-cr-avg').textContent = cr.avgTimeCr != null ? cr.avgTimeCr + 'hs' : '—'; + document.getElementById('kpi-cr-stalled').textContent = (cr.stalledIssues?.length) || '0'; + + const crMonths = Object.keys(cr.monthlyAvgCr || {}).sort(); + chartsManager.drawLineChart( + 'crMonthly', 'chartCrMonthly', + crMonths, + [{ + label: 'Prom. tiempo en CR (hs)', + data: crMonths.map(m => cr.monthlyAvgCr[m] || 0), + borderColor: '#00b8d9', + backgroundColor: 'rgba(0,184,217,.12)', + fill: true, + pointBackgroundColor: '#00b8d9', + borderWidth: 2, + }], + { plugins:{ legend:{ display:false } }, scales:{ x:{grid:{display:false}}, y:{ beginAtZero:true } } } + ); +} + // ── QA SECTION ───────────────────────────────────────────────────────────── function renderQASection() { const data = api.data; @@ -379,7 +419,8 @@ function renderQASection() { return; } - sorted.forEach(i => { + const top5 = sorted.slice(0, 5); + top5.forEach(i => { const isCritical = i.timeQa > 4; const isReturned = i.timesReturned > 1; const jiraUrl = `https://${host}/browse/${i.key}`; diff --git a/netlify/functions/data.js b/netlify/functions/data.js index 0c01c0d..4da5fe4 100644 --- a/netlify/functions/data.js +++ b/netlify/functions/data.js @@ -24,6 +24,11 @@ const config = { qaTotalTimeQa: 'customfield_10825', qaLastDurQa: 'customfield_10826', timesReturned: 'customfield_11217', + spFinalReal: 'customfield_11216', + crEntry: 'customfield_10818', + crTotalTime: 'customfield_10819', + enCursoEntry: 'customfield_10815', + enCursoTotalTime: 'customfield_10816', }, jql: process.env.JIRA_JQL || 'project = ${PROJECT} AND ${FIELD_SP_ESTIMATE} is not EMPTY ORDER BY updated DESC', api: { @@ -99,7 +104,9 @@ function buildFieldsList() { config.fields.spEstimate, config.fields.spFinal, config.fields.developer, config.fields.qaAssignee, config.fields.qaEntryRtqa, config.fields.qaTotalTimeRtqa, config.fields.qaLastDurRtqa, config.fields.qaEntryQa, config.fields.qaTotalTimeQa, - config.fields.qaLastDurQa, config.fields.timesReturned, + config.fields.qaLastDurQa, config.fields.timesReturned, config.fields.spFinalReal, + config.fields.crEntry, config.fields.crTotalTime, config.fields.enCursoEntry, + config.fields.enCursoTotalTime, ].join(','); } @@ -116,15 +123,17 @@ function processIssues(issues) { const now = new Date(); const qaData = { - tickets: 0, - ticketsReturned: 0, - totalTimeQa: 0, - totalTimeRtqa: 0, - monthlyTotalQa: {}, - monthlyCountQa: {}, - monthlyTotalRtqa: {}, - monthlyCountRtqa: {}, - issues: [], + tickets: 0, ticketsReturned: 0, totalTimeQa: 0, totalTimeRtqa: 0, + monthlyTotalQa: {}, monthlyCountQa: {}, monthlyTotalRtqa: {}, monthlyCountRtqa: {}, issues: [], + }; + + const deliveryData = { + totalLeadTimeDays: 0, totalCycleTimeDays: 0, doneTicketsLead: 0, doneTicketsCycle: 0, + doneSpOriginal: 0, doneSpFinal: 0 + }; + + const crData = { + tickets: 0, totalTimeCr: 0, issues: [], monthlyTotalCr: {}, monthlyCountCr: {}, stalledIssues: [] }; for (const issue of issues) { @@ -138,6 +147,10 @@ function processIssues(issues) { } else { devs = ['Sin asignar']; } + + const INACTIVE_DEVS = ['Felipe Esposito', 'heiner', 'Ciro Bruno']; + devs = devs.filter(d => !INACTIVE_DEVS.includes(d)); + if (devs.length === 0) continue; const sp = (f[config.fields.spFinal] != null ? f[config.fields.spFinal] : f[config.fields.spEstimate]) ?? 0; const statusKey = f.status?.statusCategory?.key ?? 'new'; @@ -167,6 +180,26 @@ function processIssues(issues) { monthlyDone[month][dev] = (monthlyDone[month][dev] ?? 0) + sp; } } + + // Delivery Metrics + const createdDate = f.created ? new Date(f.created) : null; + const resolvedDate = f.resolutiondate ? new Date(f.resolutiondate) : null; + const enCursoDate = f[config.fields.enCursoEntry] ? new Date(f[config.fields.enCursoEntry]) : null; + const spOriginal = f[config.fields.spEstimate] || 0; + const spReal = f[config.fields.spFinalReal] != null ? f[config.fields.spFinalReal] : spOriginal; + + if (resolvedDate) { + if (createdDate) { + deliveryData.totalLeadTimeDays += (resolvedDate - createdDate) / (1000 * 60 * 60 * 24); + deliveryData.doneTicketsLead++; + } + if (enCursoDate) { + deliveryData.totalCycleTimeDays += (resolvedDate - enCursoDate) / (1000 * 60 * 60 * 24); + deliveryData.doneTicketsCycle++; + } + deliveryData.doneSpOriginal += spOriginal; + deliveryData.doneSpFinal += spReal; + } } issuesList.push({ id: issue.id, key: issue.key, summary: f.summary, status: statusKey, sp, devs, creMonth }); @@ -195,6 +228,25 @@ function processIssues(issues) { qaData.monthlyTotalRtqa[qaMonth] = (qaData.monthlyTotalRtqa[qaMonth] || 0) + timeRtqaMin; qaData.monthlyCountRtqa[qaMonth] = (qaData.monthlyCountRtqa[qaMonth] || 0) + 1; } + + const timeCrMin = f[config.fields.crTotalTime] ?? 0; + if (timeCrMin > 0) { + crData.tickets++; + crData.totalTimeCr += timeCrMin; + if (qaMonth) { + crData.monthlyTotalCr[qaMonth] = (crData.monthlyTotalCr[qaMonth] || 0) + timeCrMin; + crData.monthlyCountCr[qaMonth] = (crData.monthlyCountCr[qaMonth] || 0) + 1; + } + crData.issues.push({ key: issue.key, summary: f.summary, timeCr: minsToHrs(timeCrMin), status: statusKey }); + } + + const crEntryDate = f[config.fields.crEntry]; + if (crEntryDate && !isDone && !f[config.fields.qaEntryRtqa]) { + const daysInCr = (now - new Date(crEntryDate)) / (1000 * 60 * 60 * 24); + if (daysInCr > 2) { + crData.stalledIssues.push({ key: issue.key, summary: f.summary, days: Math.round(daysInCr * 10)/10 }); + } + } if (isInProgress && f.updated) { const days = (now - new Date(f.updated)) / (1000 * 60 * 60 * 24); @@ -221,9 +273,23 @@ function processIssues(issues) { } } + const monthlyAvgCr = {}; + for (const m of Object.keys(crData.monthlyTotalCr)) monthlyAvgCr[m] = minsToHrs(crData.monthlyTotalCr[m] / crData.monthlyCountCr[m]); + return { lastUpdated: new Date().toISOString(), assigneeData, monthlyDone, issuesList, warnings, + deliveryData: { + avgLeadTime: deliveryData.doneTicketsLead > 0 ? Math.round(deliveryData.totalLeadTimeDays / deliveryData.doneTicketsLead) : 0, + avgCycleTime: deliveryData.doneTicketsCycle > 0 ? Math.round(deliveryData.totalCycleTimeDays / deliveryData.doneTicketsCycle) : 0, + spOriginal: deliveryData.doneSpOriginal, + spFinal: deliveryData.doneSpFinal, + spDeviationPct: deliveryData.doneSpOriginal > 0 ? ((deliveryData.doneSpFinal - deliveryData.doneSpOriginal) / deliveryData.doneSpOriginal * 100).toFixed(1) : 0 + }, + crData: { + ...crData, monthlyAvgCr, + avgTimeCr: crData.tickets > 0 ? minsToHrs(crData.totalTimeCr / crData.tickets) : 0 + }, qaData: { ...qaData, monthlyAvgQa, monthlyAvgRtqa, avgTimeQa: qaData.tickets > 0 ? minsToHrs(qaData.totalTimeQa / qaData.tickets) : 0,