- 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.
224 lines
9.1 KiB
Python
224 lines
9.1 KiB
Python
import re
|
|
|
|
with open("app/modules/sag/templates/detail.html", "r", encoding="utf-8") as f:
|
|
text = f.read()
|
|
|
|
old_css_pattern = r"\.time-v1-track \{.*?\n \}"
|
|
new_css = """
|
|
.time-v1-global-timeline {
|
|
position: relative;
|
|
padding-left: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.time-v1-global-timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0.75rem;
|
|
width: 2px;
|
|
background-color: var(--accent, #0f4c75);
|
|
opacity: 0.2;
|
|
}
|
|
|
|
.time-v1-date-node {
|
|
position: relative;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.time-v1-date-badge {
|
|
display: inline-block;
|
|
background-color: var(--accent, #0f4c75);
|
|
color: #fff;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 1rem;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1rem;
|
|
margin-left: -2.5rem;
|
|
position: relative;
|
|
z-index: 1;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.time-v1-item {
|
|
position: relative;
|
|
background: #fff;
|
|
border-radius: 0.5rem;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
border: 1px solid rgba(0,0,0,0.05);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.time-v1-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 1.5rem;
|
|
left: -2rem;
|
|
width: 1rem;
|
|
height: 2px;
|
|
background-color: var(--accent, #0f4c75);
|
|
opacity: 0.2;
|
|
}
|
|
|
|
.time-v1-item:hover {
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.time-v1-avatar {
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
border-radius: 50%;
|
|
background-color: color-mix(in srgb, var(--accent, #0f4c75) 10%, white);
|
|
color: var(--accent, #0f4c75);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 0.9rem;
|
|
flex-shrink: 0;
|
|
border: 2px solid white;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
"""
|
|
|
|
new_js = """function renderTimeV1Timeline(entries) {
|
|
const timeline = document.getElementById('timeV1Timeline');
|
|
if (!timeline) return;
|
|
|
|
if (!entries || entries.length === 0) {
|
|
timeline.innerHTML = '<div class="text-muted text-center p-4">Ingen tidsregistreringer endnu</div>';
|
|
return;
|
|
}
|
|
|
|
// Saml og sortér alle tidsregistreringer efter dato, nyeste først
|
|
const sortedEntries = [...entries].sort((a, b) => {
|
|
const dateA = new Date(a.worked_date || a.start_tid || 0);
|
|
const dateB = new Date(b.worked_date || b.start_tid || 0);
|
|
return dateB - dateA;
|
|
});
|
|
|
|
// Gruppér efter formatert dato
|
|
const groupedByDate = {};
|
|
sortedEntries.forEach((entry) => {
|
|
const rawDate = new Date(entry.worked_date || entry.start_tid || 0);
|
|
const dateKey = !isNaN(rawDate.getTime())
|
|
? rawDate.toLocaleDateString('da-DK', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })
|
|
: 'Ukendt dato';
|
|
|
|
if (!groupedByDate[dateKey]) groupedByDate[dateKey] = [];
|
|
groupedByDate[dateKey].push(entry);
|
|
});
|
|
|
|
// Byg HTML for den overordnede tidslinje
|
|
let html = '<div class="time-v1-global-timeline">';
|
|
|
|
Object.entries(groupedByDate).forEach(([dateLabel, dateEntries]) => {
|
|
// Konverter det første bogstav i dato-strengen til stort
|
|
const formattedDateLab = dateLabel.charAt(0).toUpperCase() + dateLabel.slice(1);
|
|
|
|
html += `
|
|
<div class="time-v1-date-node">
|
|
<div class="time-v1-date-badge">
|
|
<i class="bi bi-calendar3 me-1"></i>${formattedDateLab}
|
|
</div>
|
|
`;
|
|
|
|
dateEntries.forEach(entry => {
|
|
const desc = escapeHtml(entry.beskrivelse || 'Ingen beskrivelse');
|
|
const userName = escapeHtml(entry.bruger_navn || 'Ukendt');
|
|
|
|
// Lav initialer til Avatar
|
|
const initials = userName.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase() || '?';
|
|
|
|
// Formatér tid
|
|
let timeOutput = '0 t';
|
|
let isRunning = false;
|
|
let clockClass = "text-muted";
|
|
|
|
if (entry.kilde === 'live' && !entry.faktisk_tid_min && !entry.stop_tid) {
|
|
timeOutput = 'Kører...';
|
|
isRunning = true;
|
|
clockClass = "text-success fw-bold";
|
|
} else if (entry.is_running) {
|
|
timeOutput = 'Kører...';
|
|
isRunning = true;
|
|
clockClass = "text-success fw-bold";
|
|
} else if (entry.faktisk_tid_min !== null && entry.faktisk_tid_min !== undefined) {
|
|
const h = Math.floor(entry.faktisk_tid_min / 60);
|
|
const m = Math.floor(entry.faktisk_tid_min % 60);
|
|
timeOutput = `${h}t ${m}m`;
|
|
} else {
|
|
// Reservere for original_hours fallback
|
|
const origHours = parseFloat(entry.original_hours || 0);
|
|
const h = Math.floor(origHours);
|
|
const m = Math.round((origHours - h) * 60);
|
|
timeOutput = `${h}t ${m}m`;
|
|
}
|
|
|
|
// Tjek synlighed for kunden (intern markering)
|
|
const isInternal = entry.is_internal ? true : false;
|
|
const internalBadge = isInternal
|
|
? `<span class="badge bg-danger-subtle text-danger-emphasis border border-danger-subtle rounded-pill me-2" title="Skjult for kunde">
|
|
<i class="bi bi-eye-slash-fill me-1"></i>Intern
|
|
</span>`
|
|
: '';
|
|
|
|
html += `
|
|
<div class="time-v1-item d-flex gap-3 align-items-start">
|
|
<div class="time-v1-avatar" title="${userName}">
|
|
${initials}
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="fw-semibold text-dark">${userName}</div>
|
|
<div class="small text-muted mb-2">
|
|
<i class="bi bi-clock ${clockClass} me-1"></i>
|
|
<span class="${isRunning ? 'text-success fw-bold' : ''}">${timeOutput}</span>
|
|
${entry.entry_type ? ` · <span class="badge bg-light text-secondary border">${escapeHtml(entry.entry_type)}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
${internalBadge}
|
|
<button class="btn btn-sm btn-link text-muted p-0" onclick="deleteTimeV1Entry(${entry.id})" title="Slet">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="text-dark bg-light rounded p-2 small border" style="white-space: pre-wrap;">${desc}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `</div>`; // Luk time-v1-date-node
|
|
});
|
|
|
|
html += '</div>'; // Luk time-v1-global-timeline
|
|
timeline.innerHTML = html;
|
|
}"""
|
|
old_js_pattern = r'function renderTimeV1Timeline\(entries\).*?\n }'
|
|
|
|
orig_text_len = len(text)
|
|
|
|
import sys
|
|
if re.search(old_css_pattern, text, re.DOTALL):
|
|
text = re.sub(old_css_pattern, new_css.strip(), text, flags=re.DOTALL)
|
|
else:
|
|
print("Could NOT find old CSS!")
|
|
|
|
if re.search(old_js_pattern, text, re.DOTALL):
|
|
text = re.sub(old_js_pattern, new_js.strip(), text, flags=re.DOTALL)
|
|
else:
|
|
print("Could NOT find old JS!")
|
|
|
|
with open("app/modules/sag/templates/detail.html", "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
|
|
print(f"Replacement complete! Original length {orig_text_len}, new length {len(text)}")
|