tercer commit

This commit is contained in:
kvilches1410
2026-04-01 11:34:43 -03:00
parent 7410b22623
commit a2d49b441e
6 changed files with 400 additions and 26 deletions
+135 -12
View File
@@ -70,6 +70,7 @@ async function loadAndRender(forceRefresh = false) {
renderCurrentSection();
renderHistorySection();
renderQASection();
} catch(e) {
errorBanner.style.display = 'block';
@@ -197,19 +198,25 @@ function renderCurrentSection() {
const d = ad[p], total = d.in_progress + d.todo;
const c = chartsManager.personColor(p);
const tooltipIP = d.issues_ip.map(i => `${i.key} (${i.sp} SP)`).join('\n');
const tooltipTodo = d.issues_todo.map(i => `${i.key} (${i.sp} SP)`).join('\n');
const allKeys = [...d.issues_ip, ...d.issues_todo].map(i =>
`<a href="${getJiraUrl(i.key)}" target="_blank" class="jira-link-small" title="${i.summary}">${i.key}</a>`
).join(' ');
const popoverIP = d.issues_ip.map(i => `<div class="pop-item"><a href="${getJiraUrl(i.key)}" target="_blank">${i.key}</a>: ${i.summary.slice(0,30)}... <strong>(${i.sp} SP)</strong></div>`).join('');
const popoverTodo = d.issues_todo.map(i => `<div class="pop-item"><a href="${getJiraUrl(i.key)}" target="_blank">${i.key}</a>: ${i.summary.slice(0,30)}... <strong>(${i.sp} SP)</strong></div>`).join('');
tbody.innerHTML += `<tr>
<td>
<div class="name-cell"><div class="avatar" style="background:${c}">${initials(p)}</div>${p}</div>
<div class="ticket-list-inline">${allKeys}</div>
</td>
<td><span class="pill progress" title="${tooltipIP}">${d.in_progress} SP</span></td>
<td><span class="pill todo" title="${tooltipTodo}">${d.todo} SP</span></td>
<td>
<div class="pop-trigger">
<span class="pill progress">${d.in_progress} SP</span>
<div class="pop-content">${popoverIP || 'No hay tickets'}</div>
</div>
</td>
<td>
<div class="pop-trigger">
<span class="pill todo">${d.todo} SP</span>
<div class="pop-content">${popoverTodo || 'No hay tickets'}</div>
</div>
</td>
<td><strong>${total} SP</strong></td>
<td><div class="pbar-wrap">
<div class="pbar-ip" style="width:${pct(d.in_progress, total)}%"></div>
@@ -226,10 +233,13 @@ function renderHistorySection() {
// Consolidar todos los meses del rango actual
let periodData = {};
Object.values(data.monthlyDone).forEach(md => {
Object.entries(md).forEach(([p, sp]) => {
let periodTicketCount = {};
data.issuesList.filter(i => i.status === 'done').forEach(i => {
i.devs.forEach(p => {
if (state.selectedDev !== 'all' && p !== state.selectedDev) return;
periodData[p] = (periodData[p] || 0) + sp;
periodData[p] = (periodData[p] || 0) + i.sp;
periodTicketCount[p] = (periodTicketCount[p] || 0) + 1;
});
});
@@ -251,14 +261,16 @@ function renderHistorySection() {
const tbody = document.getElementById('tableHistory');
tbody.innerHTML = '';
if (sorted.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="no-data">No hay entregas en este período</td></tr>';
tbody.innerHTML = '<tr><td colspan="4" class="no-data">No hay entregas en este período</td></tr>';
} else {
sorted.forEach(([p, sp]) => {
const p_ = totalSP > 0 ? (sp / totalSP * 100).toFixed(1) : 0;
const c = chartsManager.personColor(p);
const tickets = periodTicketCount[p] || 0;
tbody.innerHTML += `<tr>
<td><div class="name-cell"><div class="avatar" style="background:${c}">${initials(p)}</div>${p}</div></td>
<td><span class="pill done">${sp} SP entregados</span></td>
<td><strong>${tickets} tickets</strong></td>
<td><div style="display:flex;align-items:center;gap:8px;">
<div style="background:#f0f2f5;border-radius:4px;height:8px;width:120px;overflow:hidden;">
<div style="background:#36b37e;height:100%;width:${p_}%"></div>
@@ -280,3 +292,114 @@ function renderHistorySection() {
backgroundColor: chartsManager.personColor(p), borderRadius: 3
})), { plugins:{ legend:{ position:'bottom'} }, scales:{ x:{stacked:true,grid:{display:false}}, y:{stacked:true} } });
}
// ── QA SECTION ─────────────────────────────────────────────────────────────
function renderQASection() {
const data = api.data;
if (!data || !data.qaData) return;
const qa = data.qaData;
// ── KPIs ──────────────────────────────────────────────────────────────────
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 ?? '—';
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`;
// ── GRÁFICO 1: Distribución de tiempos en QA (doughnut) ───────────────────
const buckets = { '<30min': 0, '30min–2hs': 0, '2–8hs': 0, '>8hs': 0 };
(qa.issues || []).forEach(i => {
const hs = i.timeQa;
if (hs < 0.5) buckets['<30min']++;
else if (hs < 2) buckets['30min–2hs']++;
else if (hs < 8) buckets['2–8hs']++;
else buckets['>8hs']++;
});
chartsManager.drawDoughnutChart(
'qaDist', 'chartQaDist',
Object.keys(buckets),
Object.values(buckets),
['#36b37e', '#00b8d9', '#ff8b00', '#ff5630']
);
// ── GRÁFICO 2: Devueltos vs Aprobados (doughnut) ──────────────────────────
const aprobados = qa.tickets - qa.ticketsReturned;
chartsManager.drawDoughnutChart(
'qaReturned', 'chartQaReturned',
['✅ Aprobados directo', '🔄 Devueltos (>1 vez en curso)'],
[aprobados, qa.ticketsReturned],
['#36b37e', '#ff5630']
);
// ── GRÁFICO 3: Tiempo promedio QA mes a mes (line) ────────────────────────
const qaMonths = Object.keys(qa.monthlyAvgQa || {}).sort();
chartsManager.drawLineChart(
'qaMonthly', 'chartQaMonthly',
qaMonths,
[{
label: 'Prom. tiempo en QA (hs)',
data: qaMonths.map(m => qa.monthlyAvgQa[m] || 0),
borderColor: '#6554c0',
backgroundColor: 'rgba(101,84,192,.12)',
fill: true,
pointBackgroundColor: '#6554c0',
borderWidth: 2,
}],
{ plugins:{ legend:{ display:false } }, scales:{ x:{grid:{display:false}}, y:{ beginAtZero:true } } }
);
// ── GRÁFICO 4: Tiempo en cola RTQA mes a mes (bar) ────────────────────────
const rtqaMonths = Object.keys(qa.monthlyAvgRtqa || {}).sort();
chartsManager.drawBarChart(
'rtqaMonthly', 'chartRtqaMonthly',
rtqaMonths,
[{
label: 'Prom. tiempo en cola RTQA (hs)',
data: rtqaMonths.map(m => qa.monthlyAvgRtqa[m] || 0),
backgroundColor: '#00b8d9',
borderRadius: 4,
}],
{ plugins:{ legend:{ display:false } }, scales:{ x:{grid:{display:false}}, y:{ beginAtZero:true } } }
);
// ── TABLA de tickets QA ───────────────────────────────────────────────────
const host = api.data?.config?.host || 'invera.atlassian.net';
const tbody = document.getElementById('tableQA');
tbody.innerHTML = '';
const sorted = [...(qa.issues || [])].sort((a, b) => b.timeQa - a.timeQa);
if (sorted.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="no-data">No hay tickets con datos de QA en este período</td></tr>';
return;
}
sorted.forEach(i => {
const isCritical = i.timeQa > 4;
const isReturned = i.timesReturned > 1;
const jiraUrl = `https://${host}/browse/${i.key}`;
const statusLabel = i.status === 'done'
? '<span class="pill done">✅ Done</span>'
: i.status === 'indeterminate'
? '<span class="pill progress">🔵 En curso</span>'
: '<span class="pill todo">⏳ Pendiente</span>';
const returnBadge = isReturned
? `<span class="pill returned">🔄 ${i.timesReturned}x</span>`
: `<span class="pill qa">1x</span>`;
tbody.innerHTML += `<tr class="${isCritical ? 'row-critical' : ''}">
<td><a href="${jiraUrl}" target="_blank" class="jira-link">${i.key}</a></td>
<td style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${i.summary}">${i.summary}</td>
<td><strong style="color:${isCritical ? '#ff5630' : '#172b4d'}">${i.timeQa}hs</strong>${isCritical ? ' ⚠️' : ''}</td>
<td>${i.timeRtqa > 0 ? i.timeRtqa + 'hs' : '—'}</td>
<td>${returnBadge}</td>
<td>${statusLabel}</td>
</tr>`;
});
}
+38 -1
View File
@@ -43,5 +43,42 @@ export const chartsManager = {
...options,
}
});
}
},
drawDoughnutChart(id, canvasId, labels, data, backgroundColors, options = {}) {
this.destroyChart(id);
const ctx = document.getElementById(canvasId);
if (!ctx) return;
this.instances[id] = new Chart(ctx, {
type: 'doughnut',
data: {
labels,
datasets: [{ data, backgroundColor: backgroundColors, borderWidth: 2, borderColor: '#fff' }],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'bottom', labels: { font: { size: 11 } } } },
...options,
}
});
},
drawLineChart(id, canvasId, labels, datasets, options = {}) {
this.destroyChart(id);
const ctx = document.getElementById(canvasId);
if (!ctx) return;
this.instances[id] = new Chart(ctx, {
type: 'line',
data: { labels, datasets },
options: {
responsive: true,
maintainAspectRatio: false,
tension: 0.4,
...options,
}
});
},
};