diff --git a/index.html b/index.html index 2b95407..aafc409 100644 --- a/index.html +++ b/index.html @@ -231,27 +231,64 @@

Métricas del período seleccionado

-
+
—
Tickets QA
Procesados en el período
-
-
—
-
Tiempo Prom. QA
-
Horas promedio por ticket
+
+
—
+
+
Tiempo P50 QA +
+
Percentil 50 (Mediana)
El 50% de las pruebas se completan en esta cantidad de horas netas o menos.
+
+
+
+
Horas en prueba
-
-
—
-
Tiempo Prom. en Cola
-
Horas esperando READY TO QA
+
+
—
+
+
Tiempos P90 y P99 QA +
+
Cuellos de Botella QA
P90: El 90% logran probarse en este tiempo.
P99: El 99% logra terminar en este tiempo.
+
+
+
+
P99: — hs
+
+ +
—
Tickets Devueltos
Volvieron a En Curso
+
+
—
+
+
Tiempo P50 RTQA +
+
Percentil 50 (Mediana)
El 50% de los tickets espera esta cantidad de horas (o menos) en la cola antes de ser tomado por QA.
+
+
+
+
Horas en cola
+
+
+
—
+
+
Tiempos P90 y P99 Cola +
+
Cuellos de Botella RTQA
Casos atascados sin tester asignado.

P90: El 90% de los tickets sale de la cola en este tiempo.
P99: Bloqueos máximos en la cola.
+
+
+
+
P99: — hs
+
diff --git a/js/app.js b/js/app.js index 09c9104..b840064 100644 --- a/js/app.js +++ b/js/app.js @@ -357,10 +357,16 @@ function renderQASection() { // ── KPIs DEL PERÍODO ────────────────────────────────────────────────────── document.getElementById('kpi-qa-tickets').textContent = qa.tickets ?? '—'; - document.getElementById('kpi-qa-avg').textContent = qa.avgTimeQa != null ? qa.avgTimeQa + 'hs' : '—'; - document.getElementById('kpi-qa-rtqa').textContent = qa.avgTimeRtqa != null ? qa.avgTimeRtqa + 'hs' : '—'; document.getElementById('kpi-qa-returned').textContent = qa.ticketsReturned ?? '—'; + document.getElementById('kpi-qa-p50').textContent = qa.p50Qa != null ? qa.p50Qa + 'hs' : '—'; + document.getElementById('kpi-qa-p90').textContent = qa.p90Qa != null ? qa.p90Qa + 'hs' : '—'; + document.getElementById('kpi-qa-p99-sub').textContent = qa.p99Qa != null ? 'P99: ' + qa.p99Qa + ' hs' : 'P99: — hs'; + + document.getElementById('kpi-qa-rtqa-p50').textContent = qa.p50Rtqa != null ? qa.p50Rtqa + 'hs' : '—'; + document.getElementById('kpi-qa-rtqa-p90').textContent = qa.p90Rtqa != null ? qa.p90Rtqa + 'hs' : '—'; + document.getElementById('kpi-qa-rtqa-p99-sub').textContent = qa.p99Rtqa != null ? 'P99: ' + qa.p99Rtqa + ' hs' : 'P99: — hs'; + const pctRet = qa.tickets > 0 ? ((qa.ticketsReturned / qa.tickets) * 100).toFixed(1) : 0; document.getElementById('kpi-qa-returned-pct').textContent = `${pctRet}% del total pasó por QA más de una vez`; diff --git a/netlify/functions/data.js b/netlify/functions/data.js index 6a08d61..58be3b5 100644 --- a/netlify/functions/data.js +++ b/netlify/functions/data.js @@ -138,6 +138,7 @@ function processIssues(issues, period = '90d') { const qaData = { tickets: 0, ticketsReturned: 0, totalTimeQa: 0, totalTimeRtqa: 0, monthlyTotalQa: {}, monthlyCountQa: {}, monthlyTotalRtqa: {}, monthlyCountRtqa: {}, issues: [], + timesQa: [], timesRtqa: [] }; const deliveryData = { @@ -253,6 +254,7 @@ function processIssues(issues, period = '90d') { if (timeQaMin > 0) { qaData.tickets++; qaData.totalTimeQa += timeQaMin; + qaData.timesQa.push(timeQaMin); if (timesRet > 1) qaData.ticketsReturned++; if (qaMonth) { qaData.monthlyTotalQa[qaMonth] = (qaData.monthlyTotalQa[qaMonth] || 0) + timeQaMin; @@ -264,10 +266,13 @@ function processIssues(issues, period = '90d') { }); } - if (timeRtqaMin > 0 && qaMonth) { - qaData.totalTimeRtqa += timeRtqaMin; - qaData.monthlyTotalRtqa[qaMonth] = (qaData.monthlyTotalRtqa[qaMonth] || 0) + timeRtqaMin; - qaData.monthlyCountRtqa[qaMonth] = (qaData.monthlyCountRtqa[qaMonth] || 0) + 1; + if (timeRtqaMin > 0) { + qaData.timesRtqa.push(timeRtqaMin); + if (qaMonth) { + qaData.totalTimeRtqa += timeRtqaMin; + qaData.monthlyTotalRtqa[qaMonth] = (qaData.monthlyTotalRtqa[qaMonth] || 0) + timeRtqaMin; + qaData.monthlyCountRtqa[qaMonth] = (qaData.monthlyCountRtqa[qaMonth] || 0) + 1; + } } const timeCrMin = f[config.fields.crTotalTime] ?? 0; @@ -341,6 +346,16 @@ function processIssues(issues, period = '90d') { const p90Cr = minsToHrs(getPercentile(crTimesSorted, 0.9)); const p99Cr = minsToHrs(getPercentile(crTimesSorted, 0.99)); + const qaTimesSorted = qaData.timesQa.sort((a, b) => a - b); + const p50Qa = minsToHrs(getPercentile(qaTimesSorted, 0.5)); + const p90Qa = minsToHrs(getPercentile(qaTimesSorted, 0.9)); + const p99Qa = minsToHrs(getPercentile(qaTimesSorted, 0.99)); + + const rtqaTimesSorted = qaData.timesRtqa.sort((a, b) => a - b); + const p50Rtqa = minsToHrs(getPercentile(rtqaTimesSorted, 0.5)); + const p90Rtqa = minsToHrs(getPercentile(rtqaTimesSorted, 0.9)); + const p99Rtqa = minsToHrs(getPercentile(rtqaTimesSorted, 0.99)); + return { lastUpdated: new Date().toISOString(), assigneeData, monthlyDone, issuesList, warnings, currentStates, @@ -357,6 +372,7 @@ function processIssues(issues, period = '90d') { }, qaData: { ...qaData, monthlyAvgQa, monthlyAvgRtqa, + p50Qa, p90Qa, p99Qa, p50Rtqa, p90Rtqa, p99Rtqa, avgTimeQa: qaData.tickets > 0 ? minsToHrs(qaData.totalTimeQa / qaData.tickets) : 0, avgTimeRtqa: qaData.tickets > 0 ? minsToHrs(qaData.totalTimeRtqa / qaData.tickets) : 0 }