Muchos cambios
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
.vercel
|
.vercel
|
||||||
.env
|
.env
|
||||||
ex.env
|
ex.env
|
||||||
|
|
||||||
|
# Local Netlify folder
|
||||||
|
.netlify
|
||||||
|
|||||||
+2
-1
@@ -86,7 +86,7 @@ select:focus { border-color: #0052cc; }
|
|||||||
.chart-card .sub { font-size: 12px; color: #5e6c84; margin-bottom: 20px; }
|
.chart-card .sub { font-size: 12px; color: #5e6c84; margin-bottom: 20px; }
|
||||||
|
|
||||||
/* Tables */
|
/* Tables */
|
||||||
.table-card { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,.06); margin-bottom: 24px; overflow-x: auto; }
|
.table-card { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,.06); margin-bottom: 24px; }
|
||||||
.table-card h3 { font-size: 14px; font-weight: 700; margin-bottom: 16px; }
|
.table-card h3 { font-size: 14px; font-weight: 700; margin-bottom: 16px; }
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
th { text-align: left; padding: 10px 14px; background: #f4f5f7; color: #5e6c84; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid #dfe1e6; }
|
th { text-align: left; padding: 10px 14px; background: #f4f5f7; color: #5e6c84; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid #dfe1e6; }
|
||||||
@@ -158,4 +158,5 @@ footer { text-align: center; padding: 24px; font-size: 11px; color: #97a0af; }
|
|||||||
.main { padding: 16px; }
|
.main { padding: 16px; }
|
||||||
.section-header { gap: 8px; flex-direction: column; align-items: flex-start;}
|
.section-header { gap: 8px; flex-direction: column; align-items: flex-start;}
|
||||||
.filter-group { margin-left: 0; }
|
.filter-group { margin-left: 0; }
|
||||||
|
.table-card { overflow-x: auto; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"remote": {
|
||||||
|
"https://edge.netlify.com/": "fd941d61d88673d5f28aab283fb86fcc50f08a3bc80ee5470498fcfa88c65cfb",
|
||||||
|
"https://edge.netlify.com/bootstrap/config.ts": "6a2ce0e544e15e8f8883a5c18da5948e37fd0f2619f68cb31f3af53c51817025",
|
||||||
|
"https://edge.netlify.com/bootstrap/context.ts": "e97240232121e2f369f6546ce961490f34d961ea1ea54be3ff09633e3f08373f",
|
||||||
|
"https://edge.netlify.com/bootstrap/cookie.ts": "8b0baae708989ca183c6f3b4ab3d029e6abcbc2e43f93edeb0ff447b3bbc3a05",
|
||||||
|
"https://edge.netlify.com/bootstrap/edge_function.ts": "b8253e86aa83c67341f5cfedeba5049d77fbf84dcab7eceff7566b7728ae9b39",
|
||||||
|
"https://edge.netlify.com/bootstrap/globals/types.ts": "eaa6148ded3121d8dee62dd91c86e7fe76601df0f3ca8d7962243a30f4c8935f"
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-10
@@ -147,21 +147,49 @@
|
|||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<div class="kpi-row-3">
|
<!-- Colas Actuales -->
|
||||||
|
<div class="kpi-row" style="margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #dfe1e6;">
|
||||||
|
<div class="kpi warning" style="border-color:#ff8b00;" title="Tickets que se encuentran en este preciso instante en la columna Code Review esperando o siendo revisados">
|
||||||
|
<div class="kpi-value" id="kpi-cr-queue-now">—</div>
|
||||||
|
<div class="kpi-label">En Cola Actual CR</div>
|
||||||
|
<div class="kpi-sub" id="kpi-cr-queue-sp">— SP en espera</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 CR</div>
|
||||||
|
<div class="kpi-sub">Demorados actualmente (más de 2 días)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Historial del período -->
|
||||||
|
<h3 style="font-size:14px; margin-bottom:12px; color:#172b4d;">Métricas del período seleccionado</h3>
|
||||||
|
<div class="kpi-row">
|
||||||
<div class="kpi" style="border-color:#00b8d9;" title="Cantidad de tickets que registraron actividad en la fase de Code Review">
|
<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-value" id="kpi-cr-tickets">—</div>
|
||||||
<div class="kpi-label">Tickets en CR</div>
|
<div class="kpi-label">Tickets en CR</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" 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" style="border-color:#00b8d9;">
|
||||||
<div class="kpi-value" id="kpi-cr-avg">—</div>
|
<div class="kpi-value" id="kpi-cr-p50">—</div>
|
||||||
<div class="kpi-label">Tiempo Prom. CR</div>
|
<div class="kpi-label">
|
||||||
<div class="kpi-sub">Horas promedio por ticket</div>
|
<div class="pop-trigger" style="border-bottom: 1px dotted #5e6c84;">Tiempo P50 CR
|
||||||
|
<div class="pop-content" style="text-transform:none; font-weight:normal; letter-spacing:normal; text-align:left;">
|
||||||
|
<div class="pop-item" style="font-size:12px;"><strong>Percentil 50 (Mediana)</strong><br>El 50% de las revisiones de código se completan en esta cantidad de horas o menos.<br><br>A diferencia del promedio tradicional, el P50 muestra un valor "habitual" sin ser distorsionado por tickets que queden atascados durante meses.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-sub">Horas (Mediana)</div>
|
||||||
</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" style="border-color:#ff8b00;">
|
||||||
<div class="kpi-value" id="kpi-cr-stalled">—</div>
|
<div class="kpi-value" id="kpi-cr-p90">—</div>
|
||||||
<div class="kpi-label">Tickets Atascados</div>
|
<div class="kpi-label">
|
||||||
<div class="kpi-sub">Demorados actualmente en revisión</div>
|
<div class="pop-trigger" style="border-bottom: 1px dotted #5e6c84;">Tiempos P90 y P99
|
||||||
|
<div class="pop-content" style="text-transform:none; font-weight:normal; letter-spacing:normal; text-align:left;">
|
||||||
|
<div class="pop-item" style="font-size:12px;"><strong>Métricas de Cuello de Botella</strong><br>Muestran los "peores escenarios" posibles.<br><br><strong>P90:</strong> El 90% de los tickets logra pasar en este tiempo. El 10% restante demoró aún más.<br><strong>P99:</strong> El 99% pasa en este tiempo. Ideal para medir los bloqueos máximos.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-sub" id="kpi-cr-p99-sub">P99: — hs</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -187,7 +215,22 @@
|
|||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
|
|
||||||
<!-- KPIs -->
|
<!-- Colas Actuales -->
|
||||||
|
<div class="kpi-row" style="margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #dfe1e6;">
|
||||||
|
<div class="kpi qa-queue" style="border-color:#ff8b00;" title="Tickets que se encuentran en este preciso instante en la columna Ready to QA">
|
||||||
|
<div class="kpi-value" id="kpi-qa-queue-now">—</div>
|
||||||
|
<div class="kpi-label">En Cola Actual RTQA</div>
|
||||||
|
<div class="kpi-sub" id="kpi-qa-queue-now-sp">— SP esperando</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi qa" style="border-color:#0065ff;" title="Tickets que se encuentran en este preciso instante siendo probados en la columna QA">
|
||||||
|
<div class="kpi-value" id="kpi-qa-active-now">—</div>
|
||||||
|
<div class="kpi-label">En Cola Actual QA</div>
|
||||||
|
<div class="kpi-sub" id="kpi-qa-active-now-sp">— SP en prueba</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPIs del período -->
|
||||||
|
<h3 style="font-size:14px; margin-bottom:12px; color:#172b4d;">Métricas del período seleccionado</h3>
|
||||||
<div class="kpi-row">
|
<div class="kpi-row">
|
||||||
<div class="kpi qa" title="Total de tickets que han pasado por 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>
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ function renderHealthWarnings(warnings = []) {
|
|||||||
const link = w.key ? `<a href="${getJiraUrl(w.key)}" target="_blank" class="jira-link">${w.key}</a>` : '';
|
const link = w.key ? `<a href="${getJiraUrl(w.key)}" target="_blank" class="jira-link">${w.key}</a>` : '';
|
||||||
|
|
||||||
if(w.type === 'stalled') { badge = 'ESTANCADO'; text = `🎟️ [${link}] ${w.summary.slice(0,50)}... (${w.devs.join(', ')})`; }
|
if(w.type === 'stalled') { badge = 'ESTANCADO'; text = `🎟️ [${link}] ${w.summary.slice(0,50)}... (${w.devs.join(', ')})`; }
|
||||||
|
if(w.type === 'expired') { badge = 'VENCIDO'; text = `🗓️ [${link}] ${w.summary.slice(0,50)}... (Vencido hace ${w.days} días)`; }
|
||||||
if(w.type === 'done_no_sp') { badge = 'SIN SP'; text = `🎟️ [${link}] ${w.summary.slice(0,50)}... (Completado sin estimación)`; }
|
if(w.type === 'done_no_sp') { badge = 'SIN SP'; text = `🎟️ [${link}] ${w.summary.slice(0,50)}... (Completado sin estimación)`; }
|
||||||
if(w.type === 'unassigned_sp') { badge = 'HUÉRFANO'; text = `🎟️ [${link}] ${w.summary.slice(0,50)}... (${w.sp} SP sin dueño)`; }
|
if(w.type === 'unassigned_sp') { badge = 'HUÉRFANO'; text = `🎟️ [${link}] ${w.summary.slice(0,50)}... (${w.sp} SP sin dueño)`; }
|
||||||
if(w.type === 'free_dev') { badge = 'DEV LIBRE'; text = `👤 ${w.summary}`; }
|
if(w.type === 'free_dev') { badge = 'DEV LIBRE'; text = `👤 ${w.summary}`; }
|
||||||
@@ -312,9 +313,15 @@ function renderCRSection() {
|
|||||||
if (!data || !data.crData) return;
|
if (!data || !data.crData) return;
|
||||||
|
|
||||||
const cr = data.crData;
|
const cr = data.crData;
|
||||||
|
const cur = data.currentStates || { codeReview: {count:0, sp:0} };
|
||||||
|
|
||||||
|
document.getElementById('kpi-cr-queue-now').textContent = cur.codeReview.count;
|
||||||
|
document.getElementById('kpi-cr-queue-sp').textContent = cur.codeReview.sp + ' SP en espera';
|
||||||
|
|
||||||
document.getElementById('kpi-cr-tickets').textContent = cr.tickets ?? '—';
|
document.getElementById('kpi-cr-tickets').textContent = cr.tickets ?? '—';
|
||||||
document.getElementById('kpi-cr-avg').textContent = cr.avgTimeCr != null ? cr.avgTimeCr + 'hs' : '—';
|
document.getElementById('kpi-cr-p50').textContent = cr.p50Cr != null ? cr.p50Cr + 'hs' : '—';
|
||||||
|
document.getElementById('kpi-cr-p90').textContent = cr.p90Cr != null ? cr.p90Cr + 'hs' : '—';
|
||||||
|
document.getElementById('kpi-cr-p99-sub').textContent = cr.p99Cr != null ? 'P99: ' + cr.p99Cr + ' hs' : 'P99: — hs';
|
||||||
document.getElementById('kpi-cr-stalled').textContent = (cr.stalledIssues?.length) || '0';
|
document.getElementById('kpi-cr-stalled').textContent = (cr.stalledIssues?.length) || '0';
|
||||||
|
|
||||||
const crMonths = Object.keys(cr.monthlyAvgCr || {}).sort();
|
const crMonths = Object.keys(cr.monthlyAvgCr || {}).sort();
|
||||||
@@ -340,8 +347,15 @@ function renderQASection() {
|
|||||||
if (!data || !data.qaData) return;
|
if (!data || !data.qaData) return;
|
||||||
|
|
||||||
const qa = data.qaData;
|
const qa = data.qaData;
|
||||||
|
const cur = data.currentStates || { readyToQa: {count:0, sp:0}, inQa: {count:0, sp:0} };
|
||||||
|
|
||||||
// ── KPIs ──────────────────────────────────────────────────────────────────
|
// ── COLAS ACTUALES ─────────────────────────────────────────────────────────
|
||||||
|
document.getElementById('kpi-qa-queue-now').textContent = cur.readyToQa.count;
|
||||||
|
document.getElementById('kpi-qa-queue-now-sp').textContent = cur.readyToQa.sp + ' SP esperando';
|
||||||
|
document.getElementById('kpi-qa-active-now').textContent = cur.inQa.count;
|
||||||
|
document.getElementById('kpi-qa-active-now-sp').textContent = cur.inQa.sp + ' SP en prueba';
|
||||||
|
|
||||||
|
// ── KPIs DEL PERÍODO ──────────────────────────────────────────────────────
|
||||||
document.getElementById('kpi-qa-tickets').textContent = qa.tickets ?? '—';
|
document.getElementById('kpi-qa-tickets').textContent = qa.tickets ?? '—';
|
||||||
document.getElementById('kpi-qa-avg').textContent = qa.avgTimeQa != null ? qa.avgTimeQa + 'hs' : '—';
|
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-rtqa').textContent = qa.avgTimeRtqa != null ? qa.avgTimeRtqa + 'hs' : '—';
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ async function fetchJiraIssues(email, token, period) {
|
|||||||
|
|
||||||
function buildFieldsList() {
|
function buildFieldsList() {
|
||||||
return [
|
return [
|
||||||
'summary', 'status', 'assignee', 'resolutiondate', 'created', 'updated',
|
'summary', 'status', 'assignee', 'resolutiondate', 'created', 'updated', 'duedate',
|
||||||
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,
|
||||||
@@ -115,13 +115,26 @@ function minsToHrs(mins) {
|
|||||||
return Math.round((mins / 60) * 10) / 10;
|
return Math.round((mins / 60) * 10) / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
function processIssues(issues) {
|
function processIssues(issues, period = '90d') {
|
||||||
const assigneeData = {};
|
const assigneeData = {};
|
||||||
const monthlyDone = {};
|
const monthlyDone = {};
|
||||||
const issuesList = [];
|
const issuesList = [];
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
let cutoffDate = null;
|
||||||
|
if (period !== 'all') {
|
||||||
|
const periodDays = period === '1d' ? 1 : parseInt(period.replace('d', ''));
|
||||||
|
if (!isNaN(periodDays)) {
|
||||||
|
if (period === '1d') {
|
||||||
|
cutoffDate = new Date();
|
||||||
|
cutoffDate.setHours(0,0,0,0);
|
||||||
|
} else {
|
||||||
|
cutoffDate = new Date(Date.now() - (periodDays * 24 * 60 * 60 * 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const qaData = {
|
const qaData = {
|
||||||
tickets: 0, ticketsReturned: 0, totalTimeQa: 0, totalTimeRtqa: 0,
|
tickets: 0, ticketsReturned: 0, totalTimeQa: 0, totalTimeRtqa: 0,
|
||||||
monthlyTotalQa: {}, monthlyCountQa: {}, monthlyTotalRtqa: {}, monthlyCountRtqa: {}, issues: [],
|
monthlyTotalQa: {}, monthlyCountQa: {}, monthlyTotalRtqa: {}, monthlyCountRtqa: {}, issues: [],
|
||||||
@@ -133,7 +146,13 @@ function processIssues(issues) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const crData = {
|
const crData = {
|
||||||
tickets: 0, totalTimeCr: 0, issues: [], monthlyTotalCr: {}, monthlyCountCr: {}, stalledIssues: []
|
tickets: 0, totalTimeCr: 0, issues: [], monthlyTotalCr: {}, monthlyCountCr: {}, stalledIssues: [], times: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentStates = {
|
||||||
|
codeReview: { count: 0, sp: 0 },
|
||||||
|
readyToQa: { count: 0, sp: 0 },
|
||||||
|
inQa: { count: 0, sp: 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const issue of issues) {
|
for (const issue of issues) {
|
||||||
@@ -154,8 +173,30 @@ function processIssues(issues) {
|
|||||||
|
|
||||||
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 statusName = f.status?.name?.toLowerCase() ?? '';
|
||||||
const isDone = statusKey === 'done';
|
const isDone = statusKey === 'done';
|
||||||
|
|
||||||
|
if (isDone && f.resolutiondate && cutoffDate) {
|
||||||
|
const resDate = new Date(f.resolutiondate);
|
||||||
|
if (resDate < cutoffDate) {
|
||||||
|
continue; // Ticket resuelto antes del período (actualizado recientemente por otro motivo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isInProgress = statusKey === 'indeterminate';
|
const isInProgress = statusKey === 'indeterminate';
|
||||||
|
|
||||||
|
if (!isDone) {
|
||||||
|
if (statusName === 'code review' || statusName.includes('code review') || statusName === 'cr') {
|
||||||
|
currentStates.codeReview.count++;
|
||||||
|
currentStates.codeReview.sp += sp;
|
||||||
|
} else if (statusName === 'ready to qa' || statusName.includes('ready to qa') || statusName === 'listo para qa') {
|
||||||
|
currentStates.readyToQa.count++;
|
||||||
|
currentStates.readyToQa.sp += sp;
|
||||||
|
} else if (statusName === 'qa' || statusName.includes('in qa') || statusName === 'en qa') {
|
||||||
|
currentStates.inQa.count++;
|
||||||
|
currentStates.inQa.sp += sp;
|
||||||
|
}
|
||||||
|
}
|
||||||
const resMonth = f.resolutiondate ? f.resolutiondate.slice(0, 7) : null;
|
const resMonth = f.resolutiondate ? f.resolutiondate.slice(0, 7) : null;
|
||||||
const creMonth = f.created ? f.created.slice(0, 7) : null;
|
const creMonth = f.created ? f.created.slice(0, 7) : null;
|
||||||
|
|
||||||
@@ -233,6 +274,7 @@ function processIssues(issues) {
|
|||||||
if (timeCrMin > 0) {
|
if (timeCrMin > 0) {
|
||||||
crData.tickets++;
|
crData.tickets++;
|
||||||
crData.totalTimeCr += timeCrMin;
|
crData.totalTimeCr += timeCrMin;
|
||||||
|
crData.times.push(timeCrMin);
|
||||||
if (qaMonth) {
|
if (qaMonth) {
|
||||||
crData.monthlyTotalCr[qaMonth] = (crData.monthlyTotalCr[qaMonth] || 0) + timeCrMin;
|
crData.monthlyTotalCr[qaMonth] = (crData.monthlyTotalCr[qaMonth] || 0) + timeCrMin;
|
||||||
crData.monthlyCountCr[qaMonth] = (crData.monthlyCountCr[qaMonth] || 0) + 1;
|
crData.monthlyCountCr[qaMonth] = (crData.monthlyCountCr[qaMonth] || 0) + 1;
|
||||||
@@ -252,6 +294,13 @@ function processIssues(issues) {
|
|||||||
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) });
|
||||||
}
|
}
|
||||||
|
if (!isDone && f.duedate) {
|
||||||
|
const dueDate = new Date(f.duedate);
|
||||||
|
if (dueDate < now) {
|
||||||
|
const daysOverdue = Math.floor((now - dueDate) / (1000 * 60 * 60 * 24));
|
||||||
|
warnings.push({ type: 'expired', key: issue.key, summary: f.summary, devs, sp, days: daysOverdue });
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isDone && sp === 0) warnings.push({ type: 'done_no_sp', key: issue.key, summary: f.summary, devs, sp: 0 });
|
if (isDone && sp === 0) warnings.push({ type: 'done_no_sp', key: issue.key, summary: f.summary, devs, sp: 0 });
|
||||||
if (!isDone && devs.includes('Sin asignar') && sp > 0) warnings.push({ type: 'unassigned_sp', key: issue.key, summary: f.summary, devs, sp });
|
if (!isDone && devs.includes('Sin asignar') && sp > 0) warnings.push({ type: 'unassigned_sp', key: issue.key, summary: f.summary, devs, sp });
|
||||||
|
|
||||||
@@ -276,9 +325,25 @@ function processIssues(issues) {
|
|||||||
const monthlyAvgCr = {};
|
const monthlyAvgCr = {};
|
||||||
for (const m of Object.keys(crData.monthlyTotalCr)) monthlyAvgCr[m] = minsToHrs(crData.monthlyTotalCr[m] / crData.monthlyCountCr[m]);
|
for (const m of Object.keys(crData.monthlyTotalCr)) monthlyAvgCr[m] = minsToHrs(crData.monthlyTotalCr[m] / crData.monthlyCountCr[m]);
|
||||||
|
|
||||||
|
const crTimesSorted = crData.times.sort((a, b) => a - b);
|
||||||
|
function getPercentile(sortedArr, p) {
|
||||||
|
if (sortedArr.length === 0) return 0;
|
||||||
|
if (p <= 0) return sortedArr[0];
|
||||||
|
if (p >= 1) return sortedArr[sortedArr.length - 1];
|
||||||
|
const index = (sortedArr.length - 1) * p;
|
||||||
|
const lower = Math.floor(index);
|
||||||
|
const upper = lower + 1;
|
||||||
|
const weight = index % 1;
|
||||||
|
if (upper >= sortedArr.length) return sortedArr[lower];
|
||||||
|
return sortedArr[lower] * (1 - weight) + sortedArr[upper] * weight;
|
||||||
|
}
|
||||||
|
const p50Cr = minsToHrs(getPercentile(crTimesSorted, 0.5));
|
||||||
|
const p90Cr = minsToHrs(getPercentile(crTimesSorted, 0.9));
|
||||||
|
const p99Cr = minsToHrs(getPercentile(crTimesSorted, 0.99));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
assigneeData, monthlyDone, issuesList, warnings,
|
assigneeData, monthlyDone, issuesList, warnings, currentStates,
|
||||||
deliveryData: {
|
deliveryData: {
|
||||||
avgLeadTime: deliveryData.doneTicketsLead > 0 ? Math.round(deliveryData.totalLeadTimeDays / deliveryData.doneTicketsLead) : 0,
|
avgLeadTime: deliveryData.doneTicketsLead > 0 ? Math.round(deliveryData.totalLeadTimeDays / deliveryData.doneTicketsLead) : 0,
|
||||||
avgCycleTime: deliveryData.doneTicketsCycle > 0 ? Math.round(deliveryData.totalCycleTimeDays / deliveryData.doneTicketsCycle) : 0,
|
avgCycleTime: deliveryData.doneTicketsCycle > 0 ? Math.round(deliveryData.totalCycleTimeDays / deliveryData.doneTicketsCycle) : 0,
|
||||||
@@ -287,7 +352,7 @@ function processIssues(issues) {
|
|||||||
spDeviationPct: deliveryData.doneSpOriginal > 0 ? ((deliveryData.doneSpFinal - deliveryData.doneSpOriginal) / deliveryData.doneSpOriginal * 100).toFixed(1) : 0
|
spDeviationPct: deliveryData.doneSpOriginal > 0 ? ((deliveryData.doneSpFinal - deliveryData.doneSpOriginal) / deliveryData.doneSpOriginal * 100).toFixed(1) : 0
|
||||||
},
|
},
|
||||||
crData: {
|
crData: {
|
||||||
...crData, monthlyAvgCr,
|
...crData, monthlyAvgCr, p50Cr, p90Cr, p99Cr,
|
||||||
avgTimeCr: crData.tickets > 0 ? minsToHrs(crData.totalTimeCr / crData.tickets) : 0
|
avgTimeCr: crData.tickets > 0 ? minsToHrs(crData.totalTimeCr / crData.tickets) : 0
|
||||||
},
|
},
|
||||||
qaData: {
|
qaData: {
|
||||||
@@ -316,7 +381,7 @@ exports.handler = async (event) => {
|
|||||||
if (!config.jira.email || !config.jira.token) throw new Error('Faltan credenciales JIRA en variables de entorno');
|
if (!config.jira.email || !config.jira.token) throw new Error('Faltan credenciales JIRA en variables de entorno');
|
||||||
|
|
||||||
const jiraData = await fetchJiraIssues(config.jira.email, config.jira.token, period);
|
const jiraData = await fetchJiraIssues(config.jira.email, config.jira.token, period);
|
||||||
const data = processIssues(jiraData.issues);
|
const data = processIssues(jiraData.issues, period);
|
||||||
|
|
||||||
data.isTruncated = jiraData.isTruncated;
|
data.isTruncated = jiraData.isTruncated;
|
||||||
data.jiraTotal = jiraData.isTruncated ? "1500+" : jiraData.issues.length;
|
data.jiraTotal = jiraData.isTruncated ? "1500+" : jiraData.issues.length;
|
||||||
|
|||||||
Reference in New Issue
Block a user