first commit
This commit is contained in:
+456
@@ -0,0 +1,456 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard LABS — Story Points</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f0f2f5; color: #172b4d; }
|
||||
header { background: #0052cc; color: white; padding: 20px 32px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
|
||||
header h1 { font-size: 22px; font-weight: 700; }
|
||||
.header-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.header-meta span { font-size: 12px; opacity: 0.75; }
|
||||
.refresh-btn { background: rgba(255,255,255,.15); border: 1px solid rgba(255,255,255,.3); color: white; padding: 6px 14px; border-radius: 5px; cursor: pointer; font-size: 13px; transition: background .15s; }
|
||||
.refresh-btn:hover { background: rgba(255,255,255,.25); }
|
||||
.refresh-btn:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.cache-badge { font-size: 11px; opacity: .7; }
|
||||
/* Loading */
|
||||
.loading-overlay { position: fixed; inset: 0; background: rgba(240,242,245,.9); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; gap: 16px; }
|
||||
.spinner { width: 40px; height: 40px; border: 3px solid #dfe1e6; border-top-color: #0052cc; border-radius: 50%; animation: spin .7s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.loading-overlay p { color: #5e6c84; font-size: 14px; }
|
||||
/* Error */
|
||||
.error-banner { background: #ffebe6; border: 1px solid #ff5630; color: #bf2600; padding: 14px 32px; font-size: 14px; display: none; }
|
||||
.section-header { padding: 14px 32px; display: flex; align-items: center; gap: 12px; border-bottom: 2px solid #dfe1e6; background: white; flex-wrap: wrap; }
|
||||
.section-header.current { border-color: #0065ff; }
|
||||
.section-header.history { border-color: #36b37e; }
|
||||
.section-header h2 { font-size: 14px; font-weight: 700; }
|
||||
.section-header .dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.section-header.current .dot { background: #0065ff; }
|
||||
.section-header.history .dot { background: #36b37e; }
|
||||
.section-header label { font-size: 13px; font-weight: 600; color: #5e6c84; margin-left: auto; }
|
||||
.section-header select { padding: 6px 12px; border: 1px solid #dfe1e6; border-radius: 4px; font-size: 14px; color: #172b4d; background: #fafbfc; cursor: pointer; outline: none; }
|
||||
.section-header select:focus { border-color: #0052cc; }
|
||||
.badge { background: #e3f2fd; color: #0052cc; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.main { padding: 24px 32px; max-width: 1400px; margin: 0 auto; }
|
||||
.section-block { margin-bottom: 36px; }
|
||||
.kpi-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
|
||||
.kpi-row-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
|
||||
.kpi { background: white; border-radius: 8px; padding: 20px 24px; border-left: 4px solid #dfe1e6; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
||||
.kpi.done { border-color: #36b37e; }
|
||||
.kpi.progress{ border-color: #0065ff; }
|
||||
.kpi.todo { border-color: #ff8b00; }
|
||||
.kpi.members { border-color: #6554c0; }
|
||||
.kpi-value { font-size: 36px; font-weight: 700; line-height: 1; }
|
||||
.kpi.done .kpi-value { color: #36b37e; }
|
||||
.kpi.progress .kpi-value { color: #0065ff; }
|
||||
.kpi.todo .kpi-value { color: #ff8b00; }
|
||||
.kpi.members .kpi-value { color: #6554c0; }
|
||||
.kpi-label { font-size: 12px; color: #5e6c84; margin-top: 6px; font-weight: 500; text-transform: uppercase; letter-spacing: .5px; }
|
||||
.kpi-sub { font-size: 11px; color: #97a0af; margin-top: 4px; }
|
||||
.charts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px; }
|
||||
.chart-card { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
||||
.chart-card.full { grid-column: 1 / -1; }
|
||||
.chart-card h3 { font-size: 14px; font-weight: 700; color: #172b4d; margin-bottom: 4px; }
|
||||
.chart-card .sub { font-size: 12px; color: #5e6c84; margin-bottom: 20px; }
|
||||
.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 { 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; }
|
||||
td { padding: 12px 14px; border-bottom: 1px solid #f4f5f7; vertical-align: middle; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: #f8f9fa; }
|
||||
.pill { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
|
||||
.pill.done { background: #e3fcef; color: #006644; }
|
||||
.pill.progress { background: #deebff; color: #0052cc; }
|
||||
.pill.todo { background: #fffae6; color: #7a4b00; }
|
||||
.pbar-wrap { background: #f4f5f7; border-radius: 4px; height: 8px; width: 140px; overflow: hidden; display: flex; }
|
||||
.pbar-done { background: #36b37e; height: 100%; }
|
||||
.pbar-ip { background: #0065ff; height: 100%; }
|
||||
.pbar-todo { background: #ff8b00; height: 100%; }
|
||||
.avatar { width: 28px; height: 28px; border-radius: 50%; color: white; font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; margin-right: 8px; flex-shrink: 0; }
|
||||
.name-cell { display: flex; align-items: center; }
|
||||
.no-data { text-align: center; color: #97a0af; padding: 40px; font-size: 14px; }
|
||||
footer { text-align: center; padding: 24px; font-size: 11px; color: #97a0af; }
|
||||
@media (max-width: 768px) {
|
||||
.kpi-row, .kpi-row-3 { grid-template-columns: 1fr 1fr; }
|
||||
.charts-grid { grid-template-columns: 1fr; }
|
||||
.chart-card.full { grid-column: 1; }
|
||||
.main { padding: 16px; }
|
||||
.section-header { gap: 8px; }
|
||||
.section-header label { margin-left: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading overlay -->
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="spinner"></div>
|
||||
<p>Consultando Jira en tiempo real…</p>
|
||||
</div>
|
||||
<!-- Error banner -->
|
||||
<div class="error-banner" id="errorBanner"></div>
|
||||
<header>
|
||||
<div>
|
||||
<h1>📊 Dashboard LABS — Story Points</h1>
|
||||
<div style="font-size:13px;opacity:.7;margin-top:4px;">
|
||||
Proyecto: <strong>LABS</strong> · invera.atlassian.net · Campo: Desarrollador
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-meta">
|
||||
<span id="lastUpdatedLabel">—</span>
|
||||
<span class="cache-badge" id="cacheBadge"></span>
|
||||
<button class="refresh-btn" id="refreshBtn" onclick="loadData(true)">↻ Actualizar</button>
|
||||
</div>
|
||||
</header>
|
||||
<!-- ── SECCIÓN 1: Estado Actual ── -->
|
||||
<div class="section-header current">
|
||||
<div class="dot"></div>
|
||||
<h2>Estado Actual del Equipo</h2>
|
||||
<span style="font-size:12px;color:#5e6c84;margin-left:8px;">Tickets activos en el tablero ahora mismo</span>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="section-block">
|
||||
<div class="kpi-row">
|
||||
<div class="kpi progress"><div class="kpi-value" id="kpi-ip">—</div><div class="kpi-label">SP En Progreso</div><div class="kpi-sub">En curso · Code Review · QA</div></div>
|
||||
<div class="kpi todo"> <div class="kpi-value" id="kpi-todo">—</div><div class="kpi-label">SP Por Hacer</div><div class="kpi-sub">Backlog asignado</div></div>
|
||||
<div class="kpi done"> <div class="kpi-value" id="kpi-done-total">—</div><div class="kpi-label">SP Completados (total)</div><div class="kpi-sub">Histórico acumulado</div></div>
|
||||
<div class="kpi members"><div class="kpi-value" id="kpi-members">—</div><div class="kpi-label">Devs activos</div><div class="kpi-sub">Con SP asignados</div></div>
|
||||
</div>
|
||||
<div class="charts-grid">
|
||||
<div class="chart-card">
|
||||
<h3>🔵 SP en Progreso — por Desarrollador</h3>
|
||||
<div class="sub">Estado actual · En curso / Code Review / QA</div>
|
||||
<div style="position:relative;height:260px"><canvas id="chartInProgress"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>📦 Estado actual por Desarrollador</h3>
|
||||
<div class="sub">Completados · En Progreso · Por Hacer (acumulado)</div>
|
||||
<div style="position:relative;height:260px"><canvas id="chartCurrentState"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card">
|
||||
<h3>Detalle por Desarrollador — Estado Actual</h3>
|
||||
<table>
|
||||
<thead><tr><th>Desarrollador</th><th>✅ Completados</th><th>🔵 En Progreso</th><th>⏳ Por Hacer</th><th>Total SP</th><th>Progreso</th></tr></thead>
|
||||
<tbody id="tableCurrentState"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ── SECCIÓN 2: Completados por período ── -->
|
||||
<div class="section-header history">
|
||||
<div class="dot"></div>
|
||||
<h2>Completados por Período</h2>
|
||||
<span style="font-size:12px;color:#5e6c84;margin-left:8px;">Filtrar por mes de resolución</span>
|
||||
<label>Ver mes:</label>
|
||||
<select id="monthFilter" onchange="updateHistory()"></select>
|
||||
<span class="badge" id="filterBadge">Todos los meses</span>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="section-block">
|
||||
<div class="kpi-row-3">
|
||||
<div class="kpi done"> <div class="kpi-value" id="kpi-done-period">—</div><div class="kpi-label">SP Completados</div><div class="kpi-sub" id="kpi-done-period-sub">en el período</div></div>
|
||||
<div class="kpi members"><div class="kpi-value" id="kpi-devs-period">—</div><div class="kpi-label">Desarrolladores</div><div class="kpi-sub">que completaron SP</div></div>
|
||||
<div class="kpi progress"><div class="kpi-value" id="kpi-avg-period">—</div><div class="kpi-label">SP promedio / dev</div><div class="kpi-sub">en el período</div></div>
|
||||
</div>
|
||||
<div class="charts-grid">
|
||||
<div class="chart-card">
|
||||
<h3>✅ SP Completados — por Desarrollador</h3>
|
||||
<div class="sub" id="subtitle-history-dev">Período seleccionado</div>
|
||||
<div style="position:relative;height:260px"><canvas id="chartDoneDev"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>📈 Velocidad del Equipo</h3>
|
||||
<div class="sub">SP completados por mes · cada color = un dev</div>
|
||||
<div style="position:relative;height:260px"><canvas id="chartVelocity"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="charts-grid">
|
||||
<div class="chart-card full">
|
||||
<h3>🗂 SP por Épica — Estado Actual</h3>
|
||||
<div class="sub">Completados · En Progreso · Por Hacer en cada épica</div>
|
||||
<div style="position:relative;height:220px"><canvas id="chartEpic"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card">
|
||||
<h3 id="table-history-title">SP Completados — Todos los meses</h3>
|
||||
<table>
|
||||
<thead><tr><th>Desarrollador</th><th>✅ SP Completados</th><th>% del total del período</th></tr></thead>
|
||||
<tbody id="tableHistory"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
Dashboard en tiempo real · Proyecto LABS · invera.atlassian.net ·
|
||||
Campo: <strong>Desarrollador</strong> (customfield_10622) + SP Finales (customfield_11019) ·
|
||||
Cache: 5 min · <span id="totalIssuesLabel"></span>
|
||||
</footer>
|
||||
<script>
|
||||
// ── Colores ────────────────────────────────────────────────────────────────
|
||||
const COLORS = [
|
||||
'#0052cc','#36b37e','#ff5630','#6554c0','#ff8b00',
|
||||
'#00b8d9','#57d9a3','#f99cdb','#8777d9','#344563',
|
||||
'#403294','#00875a','#de350b','#505f79','#42526e',
|
||||
];
|
||||
const MONTH_LABELS_MAP = {
|
||||
'2025-01':'Ene 2025','2025-02':'Feb 2025','2025-03':'Mar 2025',
|
||||
'2025-04':'Abr 2025','2025-05':'May 2025','2025-06':'Jun 2025',
|
||||
'2025-07':'Jul 2025','2025-08':'Ago 2025','2025-09':'Sep 2025',
|
||||
'2025-10':'Oct 2025','2025-11':'Nov 2025','2025-12':'Dic 2025',
|
||||
'2026-01':'Ene 2026','2026-02':'Feb 2026','2026-03':'Mar 2026',
|
||||
'2026-04':'Abr 2026','2026-05':'May 2026','2026-06':'Jun 2026',
|
||||
};
|
||||
function monthLabel(m) { return MONTH_LABELS_MAP[m] || m; }
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
let _peopleOrder = [];
|
||||
function personColor(name) {
|
||||
let idx = _peopleOrder.indexOf(name);
|
||||
if (idx < 0) { _peopleOrder.push(name); idx = _peopleOrder.length - 1; }
|
||||
return COLORS[idx % COLORS.length];
|
||||
}
|
||||
function initials(name) { return name.split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase(); }
|
||||
function shortName(name) { const p = name.split(' '); return p[0] + (p[1] ? ' ' + p[1] : ''); }
|
||||
// ── Chart instances ────────────────────────────────────────────────────────
|
||||
let charts = {};
|
||||
function destroyChart(id) { if (charts[id]) { charts[id].destroy(); delete charts[id]; } }
|
||||
// ── Global data ────────────────────────────────────────────────────────────
|
||||
let DATA = null;
|
||||
// ── Load data from API ─────────────────────────────────────────────────────
|
||||
async function loadData(forceRefresh = false) {
|
||||
document.getElementById('loadingOverlay').style.display = 'flex';
|
||||
document.getElementById('errorBanner').style.display = 'none';
|
||||
document.getElementById('refreshBtn').disabled = true;
|
||||
try {
|
||||
const url = '/api/data' + (forceRefresh ? '?refresh=1' : '');
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || `HTTP ${res.status}`);
|
||||
}
|
||||
DATA = await res.json();
|
||||
_peopleOrder = Object.keys(DATA.assigneeData);
|
||||
buildMonthSelector();
|
||||
renderCurrentSection();
|
||||
renderHistorySection();
|
||||
// Last updated label
|
||||
const d = new Date(DATA.lastUpdated);
|
||||
document.getElementById('lastUpdatedLabel').textContent =
|
||||
'Actualizado: ' + d.toLocaleString('es-AR', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' });
|
||||
document.getElementById('cacheBadge').textContent = DATA.fromCache ? '(desde caché)' : '(CACHE)';
|
||||
if (DATA.jiraTotal > DATA.totalIssues) {
|
||||
document.getElementById('totalIssuesLabel').textContent =
|
||||
`Mostrando ${DATA.totalIssues} de ${DATA.jiraTotal} issues totales con SP`;
|
||||
}
|
||||
} catch(e) {
|
||||
const banner = document.getElementById('errorBanner');
|
||||
banner.style.display = 'block';
|
||||
banner.textContent = '⚠️ Error al cargar datos: ' + e.message;
|
||||
console.error(e);
|
||||
} finally {
|
||||
document.getElementById('loadingOverlay').style.display = 'none';
|
||||
document.getElementById('refreshBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
// ── Month selector ─────────────────────────────────────────────────────────
|
||||
function buildMonthSelector() {
|
||||
const sel = document.getElementById('monthFilter');
|
||||
const months = Object.keys(DATA.monthlyDone).sort();
|
||||
sel.innerHTML = '<option value="all">Todos los meses</option>'
|
||||
+ months.map(m => `<option value="${m}">${monthLabel(m)}</option>`).join('');
|
||||
// Default: mostrar el mes más reciente
|
||||
if (months.length > 0) sel.value = months[months.length - 1];
|
||||
}
|
||||
// ── Sección 1: Estado Actual ───────────────────────────────────────────────
|
||||
function renderCurrentSection() {
|
||||
const ad = DATA.assigneeData;
|
||||
const people = Object.keys(ad).filter(p => ad[p].done + ad[p].in_progress + ad[p].todo > 0);
|
||||
// KPIs
|
||||
let ip = 0, todo = 0, done = 0, devs = 0;
|
||||
people.forEach(p => {
|
||||
ip += ad[p].in_progress;
|
||||
todo += ad[p].todo;
|
||||
done += ad[p].done;
|
||||
devs++;
|
||||
});
|
||||
document.getElementById('kpi-ip').textContent = ip;
|
||||
document.getElementById('kpi-todo').textContent = todo;
|
||||
document.getElementById('kpi-done-total').textContent = done;
|
||||
document.getElementById('kpi-members').textContent = devs;
|
||||
// Chart: solo en progreso
|
||||
{
|
||||
destroyChart('inProgress');
|
||||
const rows = people.filter(p => ad[p].in_progress > 0).sort((a,b) => ad[b].in_progress - ad[a].in_progress);
|
||||
charts['inProgress'] = new Chart(document.getElementById('chartInProgress'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: rows.map(shortName),
|
||||
datasets: [{ label: 'SP en progreso', data: rows.map(p => ad[p].in_progress),
|
||||
backgroundColor: rows.map(personColor), borderRadius: 5 }]
|
||||
},
|
||||
options: { responsive:true, maintainAspectRatio:false,
|
||||
plugins:{ legend:{ display:false } },
|
||||
scales:{ x:{ grid:{display:false}, ticks:{font:{size:11}} },
|
||||
y:{ beginAtZero:true, ticks:{font:{size:11}}, grid:{color:'#f0f2f5'} } } }
|
||||
});
|
||||
}
|
||||
// Chart: estado full
|
||||
{
|
||||
destroyChart('currentState');
|
||||
const rows = [...people].sort((a,b) => (ad[b].done+ad[b].in_progress)-(ad[a].done+ad[a].in_progress));
|
||||
charts['currentState'] = new Chart(document.getElementById('chartCurrentState'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: rows.map(shortName),
|
||||
datasets: [
|
||||
{ label:'Completados', data:rows.map(p=>ad[p].done), backgroundColor:'#36b37e', borderRadius:3 },
|
||||
{ label:'En Progreso', data:rows.map(p=>ad[p].in_progress), backgroundColor:'#0065ff', borderRadius:3 },
|
||||
{ label:'Por Hacer', data:rows.map(p=>ad[p].todo), backgroundColor:'#ff8b00', borderRadius:3 },
|
||||
]
|
||||
},
|
||||
options:{ responsive:true, maintainAspectRatio:false,
|
||||
plugins:{ legend:{ position:'bottom', labels:{ font:{size:11} } } },
|
||||
scales:{ x:{stacked:true,grid:{display:false},ticks:{font:{size:11}}},
|
||||
y:{stacked:true,beginAtZero:true,ticks:{font:{size:11}},grid:{color:'#f0f2f5'}} } }
|
||||
});
|
||||
}
|
||||
// Tabla estado actual
|
||||
const tbody = document.getElementById('tableCurrentState');
|
||||
tbody.innerHTML = '';
|
||||
const sorted = [...people].sort((a,b) => (ad[b].done+ad[b].in_progress)-(ad[a].done+ad[a].in_progress));
|
||||
sorted.forEach(p => {
|
||||
const d = ad[p], total = d.done + d.in_progress + d.todo;
|
||||
const dp = pct(d.done, total), ipp = pct(d.in_progress, total), tp = pct(d.todo, total);
|
||||
const c = personColor(p);
|
||||
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">${d.done} SP</span></td>
|
||||
<td><span class="pill progress">${d.in_progress} SP</span></td>
|
||||
<td><span class="pill todo">${d.todo} SP</span></td>
|
||||
<td><strong>${total}</strong></td>
|
||||
<td><div class="pbar-wrap"><div class="pbar-done" style="width:${dp}%"></div><div class="pbar-ip" style="width:${ipp}%"></div><div class="pbar-todo" style="width:${tp}%"></div></div></td>
|
||||
</tr>`;
|
||||
});
|
||||
}
|
||||
// ── Sección 2: Historial ───────────────────────────────────────────────────
|
||||
function renderHistorySection() {
|
||||
buildVelocityChart();
|
||||
buildEpicChart();
|
||||
updateHistory();
|
||||
}
|
||||
function buildVelocityChart() {
|
||||
destroyChart('velocity');
|
||||
const months = Object.keys(DATA.monthlyDone).sort();
|
||||
const allDevs = [...new Set(months.flatMap(m => Object.keys(DATA.monthlyDone[m])))];
|
||||
charts['velocity'] = new Chart(document.getElementById('chartVelocity'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: months.map(monthLabel),
|
||||
datasets: allDevs.map((p, i) => ({
|
||||
label: shortName(p),
|
||||
data: months.map(m => DATA.monthlyDone[m][p] || 0),
|
||||
backgroundColor: COLORS[i % COLORS.length],
|
||||
borderRadius: 3,
|
||||
}))
|
||||
},
|
||||
options:{ responsive:true, maintainAspectRatio:false,
|
||||
plugins:{ legend:{ position:'bottom', labels:{ font:{size:10}, boxWidth:12 } } },
|
||||
scales:{ x:{stacked:true,grid:{display:false},ticks:{font:{size:11}}},
|
||||
y:{stacked:true,beginAtZero:true,ticks:{font:{size:11}},grid:{color:'#f0f2f5'}} } }
|
||||
});
|
||||
}
|
||||
function buildEpicChart() {
|
||||
destroyChart('epic');
|
||||
const top = [...DATA.epicData]
|
||||
.sort((a,b) => (b.done+b.in_progress+b.todo)-(a.done+a.in_progress+a.todo))
|
||||
.slice(0, 15);
|
||||
charts['epic'] = new Chart(document.getElementById('chartEpic'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: top.map(e => e.name.length > 42 ? e.name.slice(0,40)+'…' : e.name),
|
||||
datasets: [
|
||||
{ label:'Completados', data:top.map(e=>e.done), backgroundColor:'#36b37e', borderRadius:3 },
|
||||
{ label:'En Progreso', data:top.map(e=>e.in_progress), backgroundColor:'#0065ff', borderRadius:3 },
|
||||
{ label:'Por Hacer', data:top.map(e=>e.todo), backgroundColor:'#ff8b00', borderRadius:3 },
|
||||
]
|
||||
},
|
||||
options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false,
|
||||
plugins:{ legend:{ position:'bottom', labels:{ font:{size:11} } } },
|
||||
scales:{ x:{stacked:true,beginAtZero:true,ticks:{font:{size:11}},grid:{color:'#f0f2f5'}},
|
||||
y:{stacked:true,grid:{display:false},ticks:{font:{size:11}}} } }
|
||||
});
|
||||
}
|
||||
function updateHistory() {
|
||||
if (!DATA) return;
|
||||
const month = document.getElementById('monthFilter').value;
|
||||
document.getElementById('filterBadge').textContent =
|
||||
month === 'all' ? 'Todos los meses' : monthLabel(month);
|
||||
// Aggregate
|
||||
let periodData = {};
|
||||
if (month === 'all') {
|
||||
Object.values(DATA.monthlyDone).forEach(md => {
|
||||
Object.entries(md).forEach(([p, sp]) => { periodData[p] = (periodData[p] || 0) + sp; });
|
||||
});
|
||||
} else {
|
||||
periodData = { ...(DATA.monthlyDone[month] || {}) };
|
||||
}
|
||||
const totalSP = Object.values(periodData).reduce((a,b) => a+b, 0);
|
||||
const devCount = Object.keys(periodData).filter(p => periodData[p] > 0).length;
|
||||
const avgSP = devCount > 0 ? Math.round(totalSP / devCount) : 0;
|
||||
document.getElementById('kpi-done-period').textContent = totalSP;
|
||||
document.getElementById('kpi-devs-period').textContent = devCount;
|
||||
document.getElementById('kpi-avg-period').textContent = avgSP;
|
||||
const lbl = month === 'all' ? 'todos los meses' : monthLabel(month);
|
||||
document.getElementById('kpi-done-period-sub').textContent = `SP completados · ${lbl}`;
|
||||
document.getElementById('subtitle-history-dev').textContent = `SP completados en ${lbl}`;
|
||||
document.getElementById('table-history-title').textContent =
|
||||
`SP Completados — ${month === 'all' ? 'Todos los meses' : monthLabel(month)}`;
|
||||
// Chart done by dev
|
||||
destroyChart('doneDev');
|
||||
const sorted = Object.entries(periodData).filter(([,sp]) => sp > 0).sort((a,b) => b[1]-a[1]);
|
||||
charts['doneDev'] = new Chart(document.getElementById('chartDoneDev'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: sorted.map(([p]) => shortName(p)),
|
||||
datasets: [{ label:'SP completados', data:sorted.map(([,sp]) => sp),
|
||||
backgroundColor: sorted.map(([p]) => personColor(p)), borderRadius:5 }]
|
||||
},
|
||||
options:{ responsive:true, maintainAspectRatio:false,
|
||||
plugins:{ legend:{ display:false } },
|
||||
scales:{ x:{grid:{display:false},ticks:{font:{size:11}}},
|
||||
y:{beginAtZero:true,ticks:{font:{size:11}},grid:{color:'#f0f2f5'}} } }
|
||||
});
|
||||
// Tabla historial
|
||||
const tbody = document.getElementById('tableHistory');
|
||||
tbody.innerHTML = '';
|
||||
if (sorted.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="no-data">No hay datos para el período seleccionado</td></tr>';
|
||||
return;
|
||||
}
|
||||
sorted.forEach(([p, sp]) => {
|
||||
const p_ = totalSP > 0 ? (sp / totalSP * 100).toFixed(1) : 0;
|
||||
const c = personColor(p);
|
||||
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</span></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>
|
||||
</tr>`;
|
||||
});
|
||||
}
|
||||
// ── Utils ──────────────────────────────────────────────────────────────────
|
||||
function pct(val, total) { return total > 0 ? (val / total * 100).toFixed(1) : 0; }
|
||||
// ── Auto-refresh cada 24 horas ────────────────────────────────────────────
|
||||
setInterval(() => loadData(false), 24 * 60 * 60 * 1000);
|
||||
// ── Init ───────────────────────────────────────────────────────────────────
|
||||
loadData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user