- Implemented subscription creation, updating, and rendering in script_9.js. - Added functions for handling subscription line items, product selection, and total calculations. - Integrated AnyDesk API for session management in test_anydesk.py. - Created REST client test requests for API endpoints in api.http. - Developed a script to check ESET machine status and save details in tmp_check_eset_machine.py.
196 lines
10 KiB
Python
196 lines
10 KiB
Python
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)} • ${hStr}${desc ? ' · <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")
|