CR + Tooltips + Promedios
This commit is contained in:
+54
-10
@@ -59,9 +59,9 @@
|
||||
<div class="main">
|
||||
<div class="section-block">
|
||||
<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 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 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 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" 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" 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 class="charts-grid">
|
||||
@@ -103,9 +103,14 @@
|
||||
<div class="main">
|
||||
<div class="section-block">
|
||||
<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 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 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 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" 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" 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 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) ── -->
|
||||
<div class="section-header qa">
|
||||
<div class="dot qa"></div>
|
||||
@@ -145,22 +189,22 @@
|
||||
|
||||
<!-- KPIs -->
|
||||
<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-label">Tickets QA</div>
|
||||
<div class="kpi-sub">Procesados en el período</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-label">Tiempo Prom. QA</div>
|
||||
<div class="kpi-sub">Horas promedio por ticket</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-label">Tiempo Prom. en Cola</div>
|
||||
<div class="kpi-sub">Horas esperando READY TO QA</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-label">Tickets Devueltos</div>
|
||||
<div class="kpi-sub" id="kpi-qa-returned-pct">Volvieron a En Curso</div>
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
+76
-10
@@ -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) {
|
||||
@@ -139,6 +148,10 @@ function processIssues(issues) {
|
||||
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';
|
||||
const isDone = statusKey === 'done';
|
||||
@@ -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 });
|
||||
@@ -196,6 +229,25 @@ function processIssues(issues) {
|
||||
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);
|
||||
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 {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user