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 = '
Ingen tidsregistreringer endnu
'; 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 += `
${dateLab}
`; for (let i = 0; i <= TOTAL_HOURS; i++) { const h = START_HOUR + i; html += `
${String(h).padStart(2,'0')}:00
`; } html += `
`; 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 += `
${escapeHtml(tech)} ${totS}
`; 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 += `
${e.timeStr}
${e.desc}
`; }); html += `
`; }); html += `
`; if (unplaced.length > 0) { html += `
Uden tidsrum:`; 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 += `
${escapeHtml(tech)} • ${hStr}${desc ? ' · '+desc+'' : ''}
`; }); html += `
`; } html += `
`; }); 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")