refactor: implement dashboard UI enhancements, add health alert tooltips, and introduce modular JS application structure with Netlify data functions.

This commit is contained in:
kvilches1410
2026-04-07 17:32:33 -03:00
parent 72050df789
commit 76edc176f9
3 changed files with 91 additions and 36 deletions
+53 -23
View File
@@ -238,6 +238,12 @@ function renderHistorySection() {
let periodData = {};
let periodTicketCount = {};
Object.keys(data.assigneeData || {}).forEach(p => {
if (state.selectedDev !== 'all' && p !== state.selectedDev) return;
periodData[p] = 0;
periodTicketCount[p] = 0;
});
data.issuesList.filter(i => i.status === 'done').forEach(i => {
i.devs.forEach(p => {
if (state.selectedDev !== 'all' && p !== state.selectedDev) return;
@@ -247,37 +253,60 @@ function renderHistorySection() {
});
const totalSP = Object.values(periodData).reduce((a,b) => a+b, 0);
const devCount = Object.keys(periodData).filter(p => periodData[p] > 0).length;
const devsWithActiveTickets = Object.keys(periodData).filter(p => periodData[p] > 0).length;
// Promedio global del equipo usando total de devs (incluyendo los de 0) para medir impacto real
const allDevsCount = Object.keys(periodData).length;
const teamAvgSP = allDevsCount > 0 ? (totalSP / allDevsCount) : 0;
document.getElementById('kpi-done-period').textContent = totalSP;
document.getElementById('kpi-devs-period').textContent = devCount;
document.getElementById('kpi-avg-period').textContent = devCount > 0 ? Math.round(totalSP / devCount) : 0;
const labelEl = document.getElementById('teamAvgLabel');
if (labelEl && teamAvgSP > 0 && state.selectedDev === 'all') {
labelEl.textContent = `🎯 Media Equipo: ${teamAvgSP.toFixed(1)} SP por Dev`;
labelEl.style.display = 'inline-block';
} else if (labelEl) {
labelEl.style.display = 'none';
}
// Gráfico de impacto personal (Terminados)
const sorted = Object.entries(periodData).filter(([,sp]) => sp > 0).sort((a,b) => b[1]-a[1]);
chartsManager.drawBarChart('doneDev', 'chartDoneDev', sorted.map(([p]) => shortName(p)), [{
label: 'SP entregados', data: sorted.map(([,sp]) => sp),
backgroundColor: sorted.map(([p]) => chartsManager.personColor(p)), borderRadius: 5
document.getElementById('kpi-done-period').textContent = totalSP;
document.getElementById('kpi-devs-period').textContent = devsWithActiveTickets;
document.getElementById('kpi-avg-period').textContent = devsWithActiveTickets > 0 ? Math.round(totalSP / devsWithActiveTickets) : 0;
// Gráfico de impacto personal (Terminados) (solo mayores a 0)
const sortedGraph = Object.entries(periodData).filter(([,sp]) => sp > 0).sort((a,b) => b[1]-a[1]);
chartsManager.drawBarChart('doneDev', 'chartDoneDev', sortedGraph.map(([p]) => shortName(p)), [{
label: 'SP entregados', data: sortedGraph.map(([,sp]) => sp),
backgroundColor: sortedGraph.map(([p]) => chartsManager.personColor(p)), borderRadius: 5
}], { plugins:{ legend:{ display:false } }, scales:{ x:{grid:{display:false}} } });
// Tabla de Resultados
const sortedAll = Object.entries(periodData).sort((a,b) => b[1]-a[1]);
const tbody = document.getElementById('tableHistory');
tbody.innerHTML = '';
if (sorted.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="no-data">No hay entregas en este período</td></tr>';
if (sortedAll.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" 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;
sortedAll.forEach(([p, sp]) => {
const p_vsTeam = teamAvgSP > 0 ? (sp / teamAvgSP * 100).toFixed(0) : 0;
const c = chartsManager.personColor(p);
const tickets = periodTicketCount[p] || 0;
const avgPerTicket = tickets > 0 ? (sp / tickets).toFixed(1) : 0;
let badgeHtml = '';
if (sp === 0) {
badgeHtml = `<span class="badge" style="background:#ff5630;color:white;font-size:10px;">0 SP (SIN ENTREGAS)</span>`;
} else if (p_vsTeam >= 150) {
badgeHtml = `<span style="font-size:12px;color:#36b37e;font-weight:bold;">🔥 +${p_vsTeam - 100}% sup. prom.</span>`;
} else if (p_vsTeam <= 50) {
badgeHtml = `<span style="font-size:12px;color:#ff8b00;font-weight:bold;">📉 -${100 - p_vsTeam}% del prom.</span>`;
} else {
badgeHtml = `<span style="font-size:12px;color:#5e6c84;">~ ${p_vsTeam}% del prom.</span>`;
}
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>
</div><span style="font-size:12px;color:#5e6c84;">${p_}%</span></div></td>
<td><span class="pill done">${sp} SP</span></td>
<td><strong>${tickets} tkts</strong></td>
<td><span style="font-size:13px;color:#172b4d;">${avgPerTicket} SP/tkt</span></td>
<td>${badgeHtml}</td>
</tr>`;
});
}
@@ -369,7 +398,7 @@ function renderQASection() {
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`;
`${pctRet}% del total sufrió al menos 1 rechazo`;
// ── GRÁFICO 1: Distribución de tiempos en QA (doughnut) ───────────────────
const buckets = { '<30min': 0, '30min–2hs': 0, '2–8hs': 0, '>8hs': 0 };
@@ -391,7 +420,7 @@ function renderQASection() {
const aprobados = qa.tickets - qa.ticketsReturned;
chartsManager.drawDoughnutChart(
'qaReturned', 'chartQaReturned',
['✅ Aprobados directo', '🔄 Devueltos (>1 vez en curso)'],
['✅ Flujo directo (0 rechazos)', '🔄 Retrabajo (Rechazo en CR/QA)'],
[aprobados, qa.ticketsReturned],
['#36b37e', '#ff5630']
);
@@ -458,8 +487,9 @@ function renderQASection() {
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 style="color:#0065ff;">${i.timeDev > 0 ? i.timeDev + 'hs' : '—'}</td>
<td style="color:#00b8d9;">${i.timeCr > 0 ? i.timeCr + 'hs' : '—'}</td>
<td><strong style="color:${isCritical ? '#ff5630' : '#36b37e'}">${i.timeQa}hs</strong>${isCritical ? ' ⚠️' : ''}</td>
<td>${returnBadge}</td>
<td>${statusLabel}</td>
</tr>`;