CR + Tooltips + Promedios

This commit is contained in:
kvilches1410
2026-04-04 00:49:55 -03:00
parent c8806a6701
commit 8dfa3bf137
3 changed files with 172 additions and 21 deletions
+54 -10
View File
@@ -59,9 +59,9 @@
<div class="main"> <div class="main">
<div class="section-block"> <div class="section-block">
<div class="kpi-row"> <div class="kpi-row">
<div class="kpi progress"><div class="kpi-value" id="kpi-ip">—</div><div class="kpi-label">SP En Progreso</div><div class="kpi-sub">Trabajo en curso</div></div> <div class="kpi progress" title="Suma de Story Points (SP) de todos los tickets actualmente en la columna En Progreso"><div class="kpi-value" id="kpi-ip">—</div><div class="kpi-label">SP En Progreso</div><div class="kpi-sub">Trabajo en curso</div></div>
<div class="kpi todo"> <div class="kpi-value" id="kpi-todo">—</div><div class="kpi-label">SP Por Hacer</div><div class="kpi-sub">Backlog del periodo</div></div> <div class="kpi todo" title="Suma de Story Points de tickets pendientes que aún no comenzaron"><div class="kpi-value" id="kpi-todo">—</div><div class="kpi-label">SP Por Hacer</div><div class="kpi-sub">Backlog del periodo</div></div>
<div class="kpi members"><div class="kpi-value" id="kpi-members">—</div><div class="kpi-label">Devs con Carga</div><div class="kpi-sub">Con tickets pendientes</div></div> <div class="kpi members" title="Cantidad de personas con carga de trabajo (tickets) asignados en este momento"><div class="kpi-value" id="kpi-members">—</div><div class="kpi-label">Devs con Carga</div><div class="kpi-sub">Con tickets pendientes</div></div>
</div> </div>
<div class="charts-grid"> <div class="charts-grid">
@@ -103,9 +103,14 @@
<div class="main"> <div class="main">
<div class="section-block"> <div class="section-block">
<div class="kpi-row-3"> <div class="kpi-row-3">
<div class="kpi done"> <div class="kpi-value" id="kpi-done-period">—</div><div class="kpi-label">SP Completados</div><div class="kpi-sub">en el período</div></div> <div class="kpi done" title="Suma total de Story Points entregados y completados en el período de tiempo seleccionado"><div class="kpi-value" id="kpi-done-period">—</div><div class="kpi-label">SP Completados</div><div class="kpi-sub">en el período</div></div>
<div class="kpi members"><div class="kpi-value" id="kpi-devs-period">—</div><div class="kpi-label">Desarrolladores</div><div class="kpi-sub">que completaron SP</div></div> <div class="kpi members" title="Cantidad de desarrolladores que han cerrado al menos 1 Story Point en este período"><div class="kpi-value" id="kpi-devs-period">—</div><div class="kpi-label">Desarrolladores</div><div class="kpi-sub">que completaron SP</div></div>
<div class="kpi progress"><div class="kpi-value" id="kpi-avg-period">—</div><div class="kpi-label">SP promedio / dev</div><div class="kpi-sub">en el período</div></div> <div class="kpi progress" title="Promedio simple: Total de SP completados dividido por la cantidad de desarrolladores"><div class="kpi-value" id="kpi-avg-period">—</div><div class="kpi-label">SP promedio / dev</div><div class="kpi-sub">en el período</div></div>
</div>
<div class="kpi-row-3" style="margin-top:-8px;">
<div class="kpi" style="border-color:#36b37e;" title="Lead Time: Tiempo total promedio que vive un ticket desde su creación hasta que es cerrado"><div class="kpi-value" id="kpi-lead-time">—</div><div class="kpi-label">Promedio Lead Time</div><div class="kpi-sub">Días (Creación a Fin)</div></div>
<div class="kpi" style="border-color:#00b8d9;" title="Cycle Time: Velocidad de desarrollo neto. Promedio en días desde que un developer mueve el ticket 'En Curso' hasta cerrarse"><div class="kpi-value" id="kpi-cycle-time">—</div><div class="kpi-label">Promedio Cycle Time</div><div class="kpi-sub">Días (En Curso a Fin)</div></div>
<div class="kpi warning" title="Variación entre el Story Point estimado al sprint vs el final real; un número en (+) indica Scope Creep (inflación de puntos)"><div class="kpi-value" id="kpi-sp-deviation">—</div><div class="kpi-label">Desviación SP</div><div class="kpi-sub" id="kpi-sp-deviation-sub">Original vs Final</div></div>
</div> </div>
<div class="charts-grid"> <div class="charts-grid">
@@ -133,6 +138,45 @@
<!-- ── SECCIÓN: Revisión de Código (Code Review) ── -->
<div class="section-header" style="border-color:#00b8d9;">
<div class="dot" style="background:#00b8d9;"></div>
<h2>Revisión de Código (Code Review)</h2>
<span style="font-size:12px;color:#5e6c84;margin-left:8px;">Métricas de revisión entre pares antes de QA</span>
</div>
<div class="main">
<div class="section-block">
<div class="kpi-row-3">
<div class="kpi" style="border-color:#00b8d9;" title="Cantidad de tickets que registraron actividad en la fase de Code Review">
<div class="kpi-value" id="kpi-cr-tickets">—</div>
<div class="kpi-label">Tickets en CR</div>
<div class="kpi-sub">Procesados en el período</div>
</div>
<div class="kpi" style="border-color:#00b8d9;" title="Cantidad de horas promedio que tarda en resolverse una revisión general de código (Code Review)">
<div class="kpi-value" id="kpi-cr-avg">—</div>
<div class="kpi-label">Tiempo Prom. CR</div>
<div class="kpi-sub">Horas promedio por ticket</div>
</div>
<div class="kpi warning" title="Tickets que se encuentran actualmente y desde hace más de 2 días atascados solos en la columna de revisión">
<div class="kpi-value" id="kpi-cr-stalled">—</div>
<div class="kpi-label">Tickets Atascados</div>
<div class="kpi-sub">Demorados actualmente en revisión</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-card full">
<h3>📈 Tiempo Promedio en Code Review — Mes a Mes</h3>
<div class="sub">Evolución de la velocidad de revisión de pares (horas)</div>
<div style="position:relative;height:240px"><canvas id="chartCrMonthly"></canvas></div>
</div>
</div>
</div>
</div>
<!-- ── SECCIÓN 3: Control de Calidad (QA) ── --> <!-- ── SECCIÓN 3: Control de Calidad (QA) ── -->
<div class="section-header qa"> <div class="section-header qa">
<div class="dot qa"></div> <div class="dot qa"></div>
@@ -145,22 +189,22 @@
<!-- KPIs --> <!-- KPIs -->
<div class="kpi-row"> <div class="kpi-row">
<div class="kpi qa"> <div class="kpi qa" title="Total de tickets que han pasado por QA">
<div class="kpi-value" id="kpi-qa-tickets">—</div> <div class="kpi-value" id="kpi-qa-tickets">—</div>
<div class="kpi-label">Tickets QA</div> <div class="kpi-label">Tickets QA</div>
<div class="kpi-sub">Procesados en el período</div> <div class="kpi-sub">Procesados en el período</div>
</div> </div>
<div class="kpi qa-time"> <div class="kpi qa-time" title="Horas dedicadas a revisar un ticket (Tiempo neto en la columna QA)">
<div class="kpi-value" id="kpi-qa-avg">—</div> <div class="kpi-value" id="kpi-qa-avg">—</div>
<div class="kpi-label">Tiempo Prom. QA</div> <div class="kpi-label">Tiempo Prom. QA</div>
<div class="kpi-sub">Horas promedio por ticket</div> <div class="kpi-sub">Horas promedio por ticket</div>
</div> </div>
<div class="kpi qa-queue"> <div class="kpi qa-queue" title="Tiempo pasivo promedio esperando ser tomado (Cola 'Ready to QA')">
<div class="kpi-value" id="kpi-qa-rtqa">—</div> <div class="kpi-value" id="kpi-qa-rtqa">—</div>
<div class="kpi-label">Tiempo Prom. en Cola</div> <div class="kpi-label">Tiempo Prom. en Cola</div>
<div class="kpi-sub">Horas esperando READY TO QA</div> <div class="kpi-sub">Horas esperando READY TO QA</div>
</div> </div>
<div class="kpi returned"> <div class="kpi returned" title="Tickets que rebotaron en QA y el desarrollador tuvo que retomar ('Reaperturas' o regresos al ciclo En Curso)">
<div class="kpi-value" id="kpi-qa-returned">—</div> <div class="kpi-value" id="kpi-qa-returned">—</div>
<div class="kpi-label">Tickets Devueltos</div> <div class="kpi-label">Tickets Devueltos</div>
<div class="kpi-sub" id="kpi-qa-returned-pct">Volvieron a En Curso</div> <div class="kpi-sub" id="kpi-qa-returned-pct">Volvieron a En Curso</div>
+42 -1
View File
@@ -70,6 +70,7 @@ async function loadAndRender(forceRefresh = false) {
renderCurrentSection(); renderCurrentSection();
renderHistorySection(); renderHistorySection();
renderCRSection();
renderQASection(); renderQASection();
} catch(e) { } 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 // Gráfico de Velocidad Mensual
const months = Object.keys(data.monthlyDone).sort(); const months = Object.keys(data.monthlyDone).sort();
const allDevsInHistory = [...new Set(months.flatMap(m => Object.keys(data.monthlyDone[m])))]; 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} } }); })), { 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 ───────────────────────────────────────────────────────────── // ── QA SECTION ─────────────────────────────────────────────────────────────
function renderQASection() { function renderQASection() {
const data = api.data; const data = api.data;
@@ -379,7 +419,8 @@ function renderQASection() {
return; return;
} }
sorted.forEach(i => { const top5 = sorted.slice(0, 5);
top5.forEach(i => {
const isCritical = i.timeQa > 4; const isCritical = i.timeQa > 4;
const isReturned = i.timesReturned > 1; const isReturned = i.timesReturned > 1;
const jiraUrl = `https://${host}/browse/${i.key}`; const jiraUrl = `https://${host}/browse/${i.key}`;
+76 -10
View File
@@ -24,6 +24,11 @@ const config = {
qaTotalTimeQa: 'customfield_10825', qaTotalTimeQa: 'customfield_10825',
qaLastDurQa: 'customfield_10826', qaLastDurQa: 'customfield_10826',
timesReturned: 'customfield_11217', 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', jql: process.env.JIRA_JQL || 'project = ${PROJECT} AND ${FIELD_SP_ESTIMATE} is not EMPTY ORDER BY updated DESC',
api: { api: {
@@ -99,7 +104,9 @@ function buildFieldsList() {
config.fields.spEstimate, config.fields.spFinal, config.fields.developer, config.fields.spEstimate, config.fields.spFinal, config.fields.developer,
config.fields.qaAssignee, config.fields.qaEntryRtqa, config.fields.qaTotalTimeRtqa, config.fields.qaAssignee, config.fields.qaEntryRtqa, config.fields.qaTotalTimeRtqa,
config.fields.qaLastDurRtqa, config.fields.qaEntryQa, config.fields.qaTotalTimeQa, 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(','); ].join(',');
} }
@@ -116,15 +123,17 @@ function processIssues(issues) {
const now = new Date(); const now = new Date();
const qaData = { const qaData = {
tickets: 0, tickets: 0, ticketsReturned: 0, totalTimeQa: 0, totalTimeRtqa: 0,
ticketsReturned: 0, monthlyTotalQa: {}, monthlyCountQa: {}, monthlyTotalRtqa: {}, monthlyCountRtqa: {}, issues: [],
totalTimeQa: 0, };
totalTimeRtqa: 0,
monthlyTotalQa: {}, const deliveryData = {
monthlyCountQa: {}, totalLeadTimeDays: 0, totalCycleTimeDays: 0, doneTicketsLead: 0, doneTicketsCycle: 0,
monthlyTotalRtqa: {}, doneSpOriginal: 0, doneSpFinal: 0
monthlyCountRtqa: {}, };
issues: [],
const crData = {
tickets: 0, totalTimeCr: 0, issues: [], monthlyTotalCr: {}, monthlyCountCr: {}, stalledIssues: []
}; };
for (const issue of issues) { for (const issue of issues) {
@@ -139,6 +148,10 @@ function processIssues(issues) {
devs = ['Sin asignar']; 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 sp = (f[config.fields.spFinal] != null ? f[config.fields.spFinal] : f[config.fields.spEstimate]) ?? 0;
const statusKey = f.status?.statusCategory?.key ?? 'new'; const statusKey = f.status?.statusCategory?.key ?? 'new';
const isDone = statusKey === 'done'; const isDone = statusKey === 'done';
@@ -167,6 +180,26 @@ function processIssues(issues) {
monthlyDone[month][dev] = (monthlyDone[month][dev] ?? 0) + sp; 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 }); issuesList.push({ id: issue.id, key: issue.key, summary: f.summary, status: statusKey, sp, devs, creMonth });
@@ -196,6 +229,25 @@ function processIssues(issues) {
qaData.monthlyCountRtqa[qaMonth] = (qaData.monthlyCountRtqa[qaMonth] || 0) + 1; 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) { if (isInProgress && f.updated) {
const days = (now - new Date(f.updated)) / (1000 * 60 * 60 * 24); const days = (now - new Date(f.updated)) / (1000 * 60 * 60 * 24);
if (days > 14) warnings.push({ type: 'stalled', key: issue.key, summary: f.summary, devs, sp, days: Math.round(days) }); if (days > 14) warnings.push({ type: 'stalled', key: issue.key, summary: f.summary, devs, sp, days: Math.round(days) });
@@ -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 { return {
lastUpdated: new Date().toISOString(), lastUpdated: new Date().toISOString(),
assigneeData, monthlyDone, issuesList, warnings, 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: {
...qaData, monthlyAvgQa, monthlyAvgRtqa, ...qaData, monthlyAvgQa, monthlyAvgRtqa,
avgTimeQa: qaData.tickets > 0 ? minsToHrs(qaData.totalTimeQa / qaData.tickets) : 0, avgTimeQa: qaData.tickets > 0 ? minsToHrs(qaData.totalTimeQa / qaData.tickets) : 0,