bmc_hub/fix_timeline_colors.py

196 lines
10 KiB
Python
Raw Normal View History

with open('app/modules/sag/templates/detail.html', 'r', encoding='utf-8') as f:
text = f.read()
start_marker = " function renderTimeV1Timeline(entries) {"
end_marker = " async function loadTimeTrackingTab() {"
start_idx = text.index(start_marker)
end_idx = text.index(end_marker)
print(f"Replacing lines {text[:start_idx].count(chr(10))+1} to {text[:end_idx].count(chr(10))+1}")
new_func = r""" function renderTimeV1Timeline(entries) {
const timeline = document.getElementById('timeTimelineColumns');
if (!timeline) return;
if (!entries || entries.length === 0) {
timeline.innerHTML = '<div class="text-muted text-center p-4">Ingen tidsregistreringer endnu</div>';
return;
}
const START_HOUR = 7;
const TOTAL_HOURS = 10;
const HOUR_HEIGHT = 60;
const PALETTE = [
{ border: '#0f4c75', bg: 'rgba(15,76,117,0.09)', header: 'rgba(15,76,117,0.08)' },
{ border: '#ef4444', bg: 'rgba(239,68,68,0.09)', header: 'rgba(239,68,68,0.08)' },
{ border: '#10b981', bg: 'rgba(16,185,129,0.09)', header: 'rgba(16,185,129,0.08)' },
{ border: '#f59e0b', bg: 'rgba(245,158,11,0.09)', header: 'rgba(245,158,11,0.08)' },
{ border: '#8b5cf6', bg: 'rgba(139,92,246,0.09)', header: 'rgba(139,92,246,0.08)' },
{ border: '#ec4899', bg: 'rgba(236,72,153,0.09)', header: 'rgba(236,72,153,0.08)' },
{ border: '#06b6d4', bg: 'rgba(6,182,212,0.09)', header: 'rgba(6,182,212,0.08)' },
{ border: '#f97316', bg: 'rgba(249,115,22,0.09)', header: 'rgba(249,115,22,0.08)' },
];
const allUsers = [...new Set(entries.map(e => e.bruger_navn || e.user_name || 'Ukendt'))].sort();
const userColor = {};
allUsers.forEach((u, i) => { userColor[u] = PALETTE[i % PALETTE.length]; });
const groupedByDate = {};
entries.forEach(entry => {
let dateKey;
if (entry.start_tid) dateKey = entry.start_tid.substring(0, 10);
else if (entry.worked_date) dateKey = entry.worked_date.substring(0, 10);
else if (entry.created_at) dateKey = entry.created_at.substring(0, 10);
else dateKey = 'Ukendt dato';
if (!groupedByDate[dateKey]) groupedByDate[dateKey] = [];
groupedByDate[dateKey].push(entry);
});
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
let html = '';
sortedDates.forEach(dateStr => {
const dayEntries = groupedByDate[dateStr];
let dateLab = dateStr;
try {
const d = new Date(dateStr);
if (!isNaN(d.getTime())) {
dateLab = d.toLocaleDateString('da-DK', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
dateLab = dateLab.charAt(0).toUpperCase() + dateLab.slice(1);
}
} catch(e) {}
const techPlaced = {};
const unplaced = [];
const userTotals = {};
dayEntries.forEach(entry => {
const tech = entry.bruger_navn || entry.user_name || 'Ukendt';
const mins = entry.faktisk_tid_min
? parseInt(entry.faktisk_tid_min)
: Math.round(parseFloat(entry.original_hours || entry.timer || 0) * 60);
userTotals[tech] = (userTotals[tech] || 0) + mins;
if (entry.start_tid) {
if (!techPlaced[tech]) techPlaced[tech] = [];
techPlaced[tech].push(entry);
} else {
unplaced.push(entry);
}
});
const techNames = Object.keys(techPlaced).sort();
html += `<div class="time-v1-calendar-container">
<div class="time-v1-calendar-header">
<i class="bi bi-calendar3 text-primary"></i> ${dateLab}
</div>
<div class="time-v1-calendar-grid">
<div class="time-v1-time-axis">`;
for (let i = 0; i <= TOTAL_HOURS; i++) {
const h = START_HOUR + i;
html += `<div class="time-v1-hour-marker" style="top:${i * HOUR_HEIGHT}px">${String(h).padStart(2,'0')}:00</div>`;
}
html += `</div>`;
techNames.forEach(tech => {
const c = userColor[tech] || PALETTE[0];
const tot = userTotals[tech] || 0;
const totS = tot >= 60
? `${Math.floor(tot/60)}t${tot%60 ? ' '+tot%60+'m' : ''}`
: `${tot}m`;
html += `<div class="time-v1-tech-col" style="border-top:3px solid ${c.border};">
<div class="time-v1-tech-header" style="background:${c.header};">
<i class="bi bi-person-fill" style="color:${c.border};"></i>
<span style="color:${c.border};font-weight:600;">${escapeHtml(tech)}</span>
<span class="ms-auto badge" style="background:${c.border};color:#fff;font-size:0.7rem;">${totS}</span>
</div>
<div class="time-v1-tech-body">`;
const posEntries = [];
techPlaced[tech].forEach(entry => {
const desc = escapeHtml(entry.beskrivelse || entry.description || 'Ingen beskrivelse');
const startObj = new Date(entry.start_tid);
let durMin = 30;
if (entry.faktisk_tid_min) durMin = parseInt(entry.faktisk_tid_min);
else if (entry.original_hours || entry.timer) durMin = Math.round(parseFloat(entry.original_hours || entry.timer) * 60);
let sH = startObj.getHours(), sM = startObj.getMinutes();
if (sH < START_HOUR) { durMin -= (START_HOUR * 60 - sH * 60 - sM); sH = START_HOUR; sM = 0; }
let topPx = ((sH - START_HOUR) + sM / 60) * HOUR_HEIGHT;
let heightPx = (durMin / 60) * HOUR_HEIGHT;
if (topPx < 0) topPx = 0;
if (topPx + heightPx > TOTAL_HOURS * HOUR_HEIGHT) heightPx = TOTAL_HOURS * HOUR_HEIGHT - topPx;
if (heightPx > 5 && topPx < TOTAL_HOURS * HOUR_HEIGHT) {
const endObj = new Date(startObj.getTime() + durMin * 60000);
const timeStr = `${String(startObj.getHours()).padStart(2,'0')}:${String(startObj.getMinutes()).padStart(2,'0')} \u2013 ${String(endObj.getHours()).padStart(2,'0')}:${String(endObj.getMinutes()).padStart(2,'0')}`;
posEntries.push({ topPx, heightPx, desc, timeStr, startMin: topPx, endMin: topPx + heightPx });
}
});
posEntries.sort((a, b) => a.startMin - b.startMin);
const lanes = [];
posEntries.forEach(e => {
let placed = false;
for (let li = 0; li < lanes.length; li++) {
if (lanes[li] <= e.startMin) { e.lane = li; lanes[li] = e.endMin; placed = true; break; }
}
if (!placed) { e.lane = lanes.length; lanes.push(e.endMin); }
});
const numLanes = lanes.length || 1;
posEntries.forEach(e => {
e.laneSpan = 1;
for (let li = e.lane + 1; li < numLanes; li++) {
if (!posEntries.some(o => o !== e && o.lane === li && o.startMin < e.endMin && o.endMin > e.startMin)) e.laneSpan++;
else break;
}
const lW = 100 / numLanes;
html += `<div class="time-v1-entry-block"
style="top:${e.topPx}px;height:${e.heightPx}px;left:${(e.lane*lW).toFixed(1)}%;width:calc(${(e.laneSpan*lW).toFixed(1)}% - 3px);border-left-color:${c.border};background:${c.bg};"
title="${e.desc}">
<div class="time-v1-entry-time">${e.timeStr}</div>
<div class="time-v1-entry-desc">${e.desc}</div>
</div>`;
});
html += `</div></div>`;
});
html += `</div>`;
if (unplaced.length > 0) {
html += `<div class="time-v1-unplaced-container">
<span class="text-muted small fw-semibold me-2"><i class="bi bi-clock-history"></i> Uden tidsrum:</span>`;
unplaced.forEach(u => {
const tech = u.bruger_navn || u.user_name || 'Ukendt';
const c = userColor[tech] || PALETTE[0];
const mins = u.faktisk_tid_min ? parseInt(u.faktisk_tid_min) : Math.round(parseFloat(u.original_hours || u.timer || 0) * 60);
const hStr = mins >= 60 ? `${Math.floor(mins/60)}t${mins%60?' '+mins%60+'m':''}` : `${mins}m`;
const desc = escapeHtml(u.beskrivelse || u.description || '');
html += `<div class="time-v1-unplaced-item" style="border-color:${c.border};color:${c.border};">
<i class="bi bi-person-fill"></i> ${escapeHtml(tech)} &bull; ${hStr}${desc ? ' &middot; <em style="opacity:.7;font-size:.72rem">'+desc+'</em>' : ''}
</div>`;
});
html += `</div>`;
}
html += `</div>`;
});
timeline.innerHTML = html;
}
"""
text = text[:start_idx] + new_func + text[end_idx:]
with open('app/modules/sag/templates/detail.html', 'w', encoding='utf-8') as f:
f.write(text)
print("Done - renderTimeV1Timeline replaced with user-color version")