Add monthly overview and difference to target (e.g. 36,5h)

This commit is contained in:
Oracle 2026-06-23 11:50:11 +02:00
parent f2198994d5
commit 882ea491e7
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8

View file

@ -13,7 +13,7 @@ const REPO_FILTER = import.meta.env.PUBLIC_REPO_FILTER || '';
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<title>Time Tracking Dashboard</title> <title>Time Tracking Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/apexcharts@3.49.2/dist/apexcharts.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/apexcharts@3.49.2/dist/apexcharts.css" />
<style> <style is:global>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
html, body { html, body {
margin: 0; width: 100%; height: 100%; margin: 0; width: 100%; height: 100%;
@ -27,12 +27,24 @@ const REPO_FILTER = import.meta.env.PUBLIC_REPO_FILTER || '';
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
.stat-card { background: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.2); border-radius: 12px; padding: 20px; text-align: center; backdrop-filter: blur(10px); } .stat-card { background: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.2); border-radius: 12px; padding: 20px; text-align: center; backdrop-filter: blur(10px); }
.stat-value { font-size: 32px; font-weight: 700; color: #c4a7ff; margin-bottom: 4px; } .stat-value { font-size: 32px; font-weight: 700; color: #c4a7ff; margin-bottom: 4px; }
.stat-value.positive { color: #22c55e; }
.stat-value.negative { color: #ff6b6b; }
.stat-label { font-size: 13px; color: #a0a0b8; text-transform: uppercase; letter-spacing: 0.5px; } .stat-label { font-size: 13px; color: #a0a0b8; text-transform: uppercase; letter-spacing: 0.5px; }
.charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 16px; margin-bottom: 16px; } .charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 16px; margin-bottom: 16px; }
.chart-card { background: rgba(139, 92, 246, 0.08); border: 1px solid rgba(139, 92, 246, 0.15); border-radius: 12px; padding: 20px; backdrop-filter: blur(10px); } .chart-card { background: rgba(139, 92, 246, 0.08); border: 1px solid rgba(139, 92, 246, 0.15); border-radius: 12px; padding: 20px; backdrop-filter: blur(10px); }
.chart-card.full-width { grid-column: 1 / -1; } .chart-card.full-width { grid-column: 1 / -1; }
.chart-title { font-size: 16px; font-weight: 500; color: #c4a7ff; margin-bottom: 16px; } .chart-title { font-size: 16px; font-weight: 500; color: #c4a7ff; margin-bottom: 16px; }
.chart { width: 100%; min-height: 350px; } .chart { width: 100%; min-height: 350px; }
.table-card { background: rgba(139, 92, 246, 0.08); border: 1px solid rgba(139, 92, 246, 0.15); border-radius: 12px; padding: 20px; backdrop-filter: blur(10px); margin-top: 32px; margin-bottom: 16px; }
.monthly-table { width: 100%; border-collapse: collapse; }
.monthly-table th { text-align: left; padding: 10px 12px; border-bottom: 1px solid rgba(139, 92, 246, 0.2); color: #c4a7ff; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }
.monthly-table th:nth-child(2), .monthly-table th:nth-child(3) { text-align: right; }
.monthly-table td { padding: 10px 12px; border-bottom: 1px solid rgba(139, 92, 246, 0.08); font-size: 14px; }
.monthly-table td:nth-child(1) { font-weight: 700; }
.monthly-table td:nth-child(2), .monthly-table td:nth-child(3) { text-align: right; font-weight: 700; font-size: 16px; }
.monthly-table td.diff-positive { color: #22c55e; }
.monthly-table td.diff-negative { color: #ff6b6b; }
.monthly-table tr:last-child td { border-bottom: none; }
.loading { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; } .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; }
.spinner { width: 48px; height: 48px; border: 4px solid rgba(139, 92, 246, 0.2); border-top-color: #c4a7ff; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 16px; } .spinner { width: 48px; height: 48px; border: 4px solid rgba(139, 92, 246, 0.2); border-top-color: #c4a7ff; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 16px; }
@keyframes spin { to { transform: rotate(360deg); } } @keyframes spin { to { transform: rotate(360deg); } }
@ -67,6 +79,10 @@ const REPO_FILTER = import.meta.env.PUBLIC_REPO_FILTER || '';
<div class="stat-value" id="tracked-items">-</div> <div class="stat-value" id="tracked-items">-</div>
<div class="stat-label">Tracked Items</div> <div class="stat-label">Tracked Items</div>
</div> </div>
<div class="stat-card">
<div class="stat-value" id="time-12month-balance">-</div>
<div class="stat-label">12-Month Balance (target 36.5h/mo)</div>
</div>
</div> </div>
<div class="charts-grid"> <div class="charts-grid">
@ -95,6 +111,20 @@ const REPO_FILTER = import.meta.env.PUBLIC_REPO_FILTER || '';
<h2 class="chart-title">Time Distribution - Issues vs Pull Requests</h2> <h2 class="chart-title">Time Distribution - Issues vs Pull Requests</h2>
<div id="chart-distribution" class="chart"></div> <div id="chart-distribution" class="chart"></div>
</div> </div>
<div class="table-card">
<h2 class="chart-title">Monthly Summary - Last 12 Months (target 36.5h)</h2>
<table class="monthly-table">
<thead>
<tr>
<th>Month</th>
<th>Hours Worked</th>
<th>Difference</th>
</tr>
</thead>
<tbody id="monthly-table-body"></tbody>
</table>
</div>
</div> </div>
<div id="error" class="error" style="display: none;"> <div id="error" class="error" style="display: none;">
<h2>Error Loading Data</h2> <h2>Error Loading Data</h2>
@ -152,7 +182,9 @@ const REPO_FILTER = import.meta.env.PUBLIC_REPO_FILTER || '';
function getSinceDate() { function getSinceDate() {
var now = new Date(); var now = new Date();
return new Date(Date.UTC(now.getFullYear(), now.getMonth(), 1)).toISOString(); var d = new Date(Date.UTC(now.getFullYear(), now.getMonth(), 1));
d.setUTCFullYear(d.getUTCFullYear() - 1);
return d.toISOString();
} }
function utcDayKey(d) { function utcDayKey(d) {
@ -195,6 +227,7 @@ const REPO_FILTER = import.meta.env.PUBLIC_REPO_FILTER || '';
var monthStart = getMonthStart(); var monthStart = getMonthStart();
var dayMap = {}; var dayMap = {};
var monthMap = {};
var issuesData = {}; var issuesData = {};
var prsData = {}; var prsData = {};
var issuesTotal = 0; var issuesTotal = 0;
@ -216,6 +249,11 @@ const REPO_FILTER = import.meta.env.PUBLIC_REPO_FILTER || '';
if (!dayMap[dayKey]) dayMap[dayKey] = 0; if (!dayMap[dayKey]) dayMap[dayKey] = 0;
dayMap[dayKey] += timeSec; dayMap[dayKey] += timeSec;
var createdDate = new Date(entry.created);
var monthKey = createdDate.getUTCFullYear() + '-' + String(createdDate.getUTCMonth() + 1).padStart(2, '0');
if (!monthMap[monthKey]) monthMap[monthKey] = 0;
monthMap[monthKey] += timeSec;
if (entry.issue) { if (entry.issue) {
var itemId = entry.issue.pull_request ? 'pr_' + entry.issue.id : 'issue_' + entry.issue.id; var itemId = entry.issue.pull_request ? 'pr_' + entry.issue.id : 'issue_' + entry.issue.id;
trackedItems.add(itemId); trackedItems.add(itemId);
@ -329,6 +367,32 @@ const REPO_FILTER = import.meta.env.PUBLIC_REPO_FILTER || '';
legend: { show: false } legend: { show: false }
}).render(); }).render();
// 12-month balance
var targetHours = 36.5;
var totalBalance = 0;
var monthRows = '';
var now_ = new Date();
var monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
for (var m = 11; m >= 0; m--) {
var y = now_.getUTCFullYear();
var mo = now_.getUTCMonth();
var targetMonth = mo - m;
while (targetMonth < 0) { targetMonth += 12; y -= 1; }
var mk = y + '-' + String(targetMonth + 1).padStart(2, '0');
var workedHours = parseFloat((monthMap[mk] || 0) / 3600).toFixed(1);
if (parseFloat(workedHours) === 0) continue;
var diff = parseFloat(workedHours) - targetHours;
totalBalance += diff;
var diffClass = diff >= 0 ? 'diff-positive' : 'diff-negative';
var diffStr = (diff >= 0 ? '+' : '') + diff.toFixed(1) + 'h';
monthRows += '<tr><td>' + monthNames[targetMonth] + ' ' + y + '</td><td>' + workedHours + 'h</td><td class="' + diffClass + '">' + diffStr + '</td></tr>';
}
document.getElementById('monthly-table-body').innerHTML = monthRows;
var balanceEl = document.getElementById('time-12month-balance');
var balanceStr = (totalBalance >= 0 ? '+' : '') + totalBalance.toFixed(1) + 'h';
balanceEl.textContent = balanceStr;
balanceEl.className = 'stat-value ' + (totalBalance >= 0 ? 'positive' : 'negative');
// Distribution chart - donut // Distribution chart - donut
new ApexCharts(document.querySelector("#chart-distribution"), { new ApexCharts(document.querySelector("#chart-distribution"), {
chart: { type: 'donut', height: 400, background: 'transparent' }, chart: { type: 'donut', height: 400, background: 'transparent' },