tercer commit
This commit is contained in:
@@ -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
@@ -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,
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user