bmc_hub/script_2.js
Christian bc504b9257 feat: Add subscription management functionality and AnyDesk API integration
- 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.
2026-03-30 07:50:15 +02:00

578 lines
25 KiB
JavaScript

function _escapeCommentHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function _removeQuotedMailLines(text) {
const source = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const lines = source.split('\n');
const kept = [];
const headerRe = /^(fra|from|sent|date|dato|to|til|emne|subject|cc):\s*/i;
const originalMessageRe = /^(original message|oprindelig besked|videresendt besked)/i;
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const trimmed = line.trim();
if (trimmed.startsWith('>')) break;
if (originalMessageRe.test(trimmed)) break;
if (/^[-_]{3,}$/.test(trimmed)) {
const lookahead = lines.slice(i + 1, i + 4);
if (lookahead.some((candidate) => headerRe.test(String(candidate || '').trim()))) {
break;
}
}
if (i > 0 && headerRe.test(trimmed) && String(lines[i - 1] || '').trim() === '') {
break;
}
kept.push(line);
}
while (kept.length > 0 && String(kept[kept.length - 1] || '').trim() === '') {
kept.pop();
}
return kept.join('\n').trim();
}
function _parseEmailComment(rawText) {
const normalized = String(rawText || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const emailIdMatch = normalized.match(/^Email-ID:\s*(\d+)\s*$/m);
const emailId = emailIdMatch ? Number(emailIdMatch[1]) : null;
const withoutMeta = normalized.replace(/^Email-ID:\s*\d+\s*\n?/m, '').trim();
return {
emailId,
visibleText: _removeQuotedMailLines(withoutMeta)
};
}
function _formatEmailHeaderTimestamp(value) {
if (!value) return '';
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return String(value);
return parsed.toLocaleString('da-DK');
}
function _buildEmailHeaderAndBody(visibleText) {
const text = String(visibleText || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
const lines = text.split('\n');
let idx = 0;
let typeLabel = 'Indgaaende email';
const firstLine = String(lines[0] || '').trim();
if (/^📧\s*Indgående email/i.test(firstLine)) {
typeLabel = 'Indgaaende email';
idx = 1;
} else if (/^📧\s*Udgående email/i.test(firstLine)) {
typeLabel = 'Udgaaende email';
idx = 1;
}
let fra = '';
let til = '';
let cc = '';
let emne = '';
let modtaget = '';
while (idx < lines.length) {
const line = String(lines[idx] || '').trim();
if (!line) {
idx += 1;
break;
}
if (/^Fra:\s*/i.test(line)) fra = line.replace(/^Fra:\s*/i, '').trim();
else if (/^Til:\s*/i.test(line)) til = line.replace(/^Til:\s*/i, '').trim();
else if (/^Cc:\s*/i.test(line)) cc = line.replace(/^Cc:\s*/i, '').trim();
else if (/^Emne:\s*/i.test(line)) emne = line.replace(/^Emne:\s*/i, '').trim();
else if (/^Modtaget:\s*/i.test(line)) modtaget = line.replace(/^Modtaget:\s*/i, '').trim();
else break;
idx += 1;
}
const bodyText = lines.slice(idx).join('\n').trim();
const summaryParts = [typeLabel];
if (fra) summaryParts.push(`Fra: ${fra}`);
if (til) summaryParts.push(`Til: ${til}`);
if (cc) summaryParts.push(`Cc: ${cc}`);
if (emne) summaryParts.push(`Emne: ${emne}`);
if (modtaget) summaryParts.push(`Modtaget: ${_formatEmailHeaderTimestamp(modtaget)}`);
return {
summary: summaryParts.join(' • '),
bodyText
};
}
function _extractEmailHeaderFields(visibleText) {
const text = String(visibleText || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
const lines = text.split('\n');
let idx = 0;
const firstLine = String(lines[0] || '').trim();
const isOutgoing = /^📧\s*Udgående email/i.test(firstLine);
if (/^📧\s*(Indgående|Udgående)\s+email/i.test(firstLine)) {
idx = 1;
}
let fra = '';
let til = '';
let emne = '';
let modtaget = '';
while (idx < lines.length) {
const line = String(lines[idx] || '').trim();
if (!line) break;
if (/^Fra:\s*/i.test(line)) fra = line.replace(/^Fra:\s*/i, '').trim();
else if (/^Til:\s*/i.test(line)) til = line.replace(/^Til:\s*/i, '').trim();
else if (/^Emne:\s*/i.test(line)) emne = line.replace(/^Emne:\s*/i, '').trim();
else if (/^Modtaget:\s*/i.test(line)) modtaget = line.replace(/^Modtaget:\s*/i, '').trim();
else break;
idx += 1;
}
return { fra, til, emne, modtaget, isOutgoing };
}
function _normalizeReplySubject(value) {
const subject = String(value || '').trim();
return subject.replace(/^(re|fw|fwd)\s*:\s*/ig, '').toLowerCase();
}
function _findBestLinkedEmailByHeader(header) {
const targetSubject = _normalizeReplySubject(header?.emne || '');
const targetFrom = String(header?.fra || '').trim().toLowerCase();
const targetTo = String(header?.til || '').trim().toLowerCase();
const candidates = (linkedEmailsCache || []).filter((email) => {
const emailSubject = _normalizeReplySubject(email?.subject || '');
if (targetSubject && emailSubject !== targetSubject) {
return false;
}
const sender = String(email?.sender_email || email?.sender_name || '').toLowerCase();
const recipient = String(email?.recipient_email || '').toLowerCase();
if (targetFrom && sender && sender.includes(targetFrom)) {
return true;
}
if (targetTo && recipient && recipient.includes(targetTo)) {
return true;
}
return !targetFrom && !targetTo;
});
if (!candidates.length) {
return null;
}
candidates.sort((a, b) => {
const aTs = a?.received_date ? new Date(a.received_date).getTime() : 0;
const bTs = b?.received_date ? new Date(b.received_date).getTime() : 0;
return bTs - aTs;
});
return Number(candidates[0]?.id) || null;
}
function _extractEmailAddress(value) {
const raw = String(value || '').trim();
const match = raw.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
return match ? match[0] : raw;
}
function _commentInitials(name) {
const clean = String(name || '').trim();
if (!clean) return 'EM';
const parts = clean.split(/\s+/).filter(Boolean);
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return `${parts[0][0] || ''}${parts[1][0] || ''}`.toUpperCase();
}
function _formatCommentTime(value) {
const parsed = new Date(value || Date.now());
if (Number.isNaN(parsed.getTime())) return '';
const pad = (n) => String(n).padStart(2, '0');
return `${pad(parsed.getDate())}/${pad(parsed.getMonth() + 1)}-${parsed.getFullYear()} ${pad(parsed.getHours())}:${pad(parsed.getMinutes())}`;
}
function _refreshCommentCountBadge() {
const container = document.getElementById('comments-container');
const badge = document.querySelector('#beskrivelse-comments-wrap .badge.bg-secondary');
if (!container || !badge) return;
badge.textContent = String(container.querySelectorAll('.comment-item').length);
}
function prependCommentToThread(comment) {
const container = document.getElementById('comments-container');
if (!container || !comment || !comment.indhold) return;
const emptyState = container.querySelector('p.text-center.text-muted.my-3');
if (emptyState) emptyState.remove();
const author = String(comment.forfatter || 'Email Bot');
const createdAtIso = String(comment.created_at || new Date().toISOString());
const createdAtMs = new Date(createdAtIso).getTime();
const createdAtUnix = Number.isFinite(createdAtMs) ? Math.floor(createdAtMs / 1000) : Math.floor(Date.now() / 1000);
const item = document.createElement('div');
item.className = 'comment-item comment-system';
item.dataset.createdAt = String(createdAtUnix);
const meta = document.createElement('div');
meta.className = 'comment-meta';
meta.innerHTML = `
<span class="comment-avatar">${_escapeCommentHtml(_commentInitials(author))}</span>
<b>${_escapeCommentHtml(author)}</b>
<span class="comment-time">${_escapeCommentHtml(_formatCommentTime(createdAtIso))}</span>
`;
const body = document.createElement('div');
body.className = 'comment-body';
body.setAttribute('data-comment-raw', String(comment.indhold));
body.textContent = String(comment.indhold);
item.appendChild(meta);
item.appendChild(body);
container.insertBefore(item, container.firstChild);
processCommentBodies();
sortCommentsNewestFirst();
_refreshCommentCountBadge();
}
let activeCommentQuickReply = null;
window.closeInlineCommentQuickReply = function() {
const host = document.getElementById('comment-quick-reply-host');
if (host) host.innerHTML = '';
activeCommentQuickReply = null;
}
window.sendInlineCommentQuickReply = async function() {
const host = document.getElementById('comment-quick-reply-host');
const textarea = document.getElementById('commentQuickReplyText');
const sendBtn = document.getElementById('commentQuickReplySendBtn');
const statusEl = document.getElementById('commentQuickReplyStatus');
if (!host || !textarea || !sendBtn || !statusEl || !activeCommentQuickReply) return;
const bodyText = String(textarea.value || '').trim();
if (!bodyText) {
statusEl.className = 'comment-quick-reply-status text-danger';
statusEl.textContent = 'Skriv et svar';
return;
}
const recipient = _extractEmailAddress(activeCommentQuickReply.recipient);
if (!recipient || recipient.indexOf('@') === -1) {
statusEl.className = 'comment-quick-reply-status text-danger';
statusEl.textContent = 'Ingen gyldig modtager fundet i kommentaren';
return;
}
sendBtn.disabled = true;
statusEl.className = 'comment-quick-reply-status';
statusEl.textContent = 'Sender...';
try {
await loadLinkedEmails();
let threadEmailId = Number(activeCommentQuickReply.emailId) || null;
if (!threadEmailId) {
threadEmailId = _findBestLinkedEmailByHeader(activeCommentQuickReply.header);
}
let threadKey = null;
if (threadEmailId) {
const linked = linkedEmailsCache.find((entry) => Number(entry.id) === Number(threadEmailId));
threadKey = linked?.thread_key || linked?.resolved_thread_key || null;
}
const response = await fetch(`/api/v1/sag/${caseIds}/emails/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: [recipient],
subject: activeCommentQuickReply.subject,
body_text: bodyText,
thread_email_id: threadEmailId,
thread_key: threadKey
})
});
if (!response.ok) {
let message = `HTTP ${response.status}`;
try {
const payload = await response.json();
message = payload?.detail || payload?.message || message;
} catch (_) {
}
throw new Error(message);
}
const result = await response.json();
if (result?.comment) {
prependCommentToThread(result.comment);
}
statusEl.className = 'comment-quick-reply-status text-success';
statusEl.textContent = 'Svar sendt';
textarea.value = '';
await loadLinkedEmails();
setTimeout(() => {
window.closeInlineCommentQuickReply();
}, 500);
} catch (error) {
statusEl.className = 'comment-quick-reply-status text-danger';
statusEl.textContent = error?.message || 'Kunne ikke sende svar';
} finally {
sendBtn.disabled = false;
}
}
function openInlineCommentQuickReply(rawText, emailId) {
const host = document.getElementById('comment-quick-reply-host');
if (!host) return;
const parsed = _parseEmailComment(rawText || '');
const header = _extractEmailHeaderFields(parsed.visibleText || '');
const fallbackRecipient = header.isOutgoing ? (header.til || header.fra) : (header.fra || header.til);
const subject = /^re:\s*/i.test(header.emne || '')
? (header.emne || `Sag #${caseIds}`)
: `Re: ${header.emne || `Sag #${caseIds}`}`;
activeCommentQuickReply = {
rawText,
header,
emailId: Number(emailId) || parsed.emailId || null,
recipient: fallbackRecipient,
subject
};
host.innerHTML = `
<div class="comment-quick-reply-box">
<div class="small text-muted mb-1">Quick svar til ${_escapeCommentHtml(String(fallbackRecipient || 'ukendt modtager'))}</div>
<textarea id="commentQuickReplyText" class="form-control" rows="2" placeholder="Skriv hurtigt svar..."></textarea>
<div class="comment-quick-reply-actions">
<div id="commentQuickReplyStatus" class="comment-quick-reply-status"></div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="closeInlineCommentQuickReply()">Annuller</button>
<button type="button" class="btn btn-sm btn-primary" id="commentQuickReplySendBtn" onclick="sendInlineCommentQuickReply()"><i class="bi bi-send me-1"></i>Send</button>
</div>
</div>
</div>
`;
const textarea = document.getElementById('commentQuickReplyText');
if (textarea) {
textarea.focus();
}
}
async function quickReplyToEmailFromCommentText(rawText) {
openCaseEmailTab();
const parsed = _parseEmailComment(rawText || '');
const header = _extractEmailHeaderFields(parsed.visibleText || '');
try {
await loadLinkedEmails();
const matchedEmailId = _findBestLinkedEmailByHeader(header);
if (matchedEmailId) {
await loadLinkedEmailDetail(matchedEmailId);
openReplyToLinkedEmail();
return;
}
} catch (error) {
console.error('Kunne ikke finde trådmail fra kommentar:', error);
}
const composeModalEl = document.getElementById('caseEmailComposeModal');
if (!composeModalEl) return;
const toInput = document.getElementById('caseEmailTo');
const subjectInput = document.getElementById('caseEmailSubject');
const bodyInput = document.getElementById('caseEmailBody');
const fallbackRecipient = (header.isOutgoing ? header.til : header.fra) || header.fra || header.til || '';
if (toInput && !toInput.value.trim() && fallbackRecipient) {
toInput.value = fallbackRecipient;
}
if (subjectInput && !subjectInput.value.trim()) {
subjectInput.value = escapeHtmlForInput(
/^re:\s*/i.test(header.emne || '')
? (header.emne || `Sag #${caseIds}`)
: `Re: ${header.emne || `Sag #${caseIds}`}`
);
}
if (bodyInput && !bodyInput.value.trim()) {
bodyInput.value = `\n\n---\nFra: ${header.fra || '-'}\nDato: ${header.modtaget || '-'}\nEmne: ${header.emne || '(Ingen emne)'}\n`;
}
bootstrap.Modal.getOrCreateInstance(composeModalEl).show();
}
async function openEmailFromComment(emailId) {
const parsedId = Number(emailId);
if (!Number.isFinite(parsedId)) return;
if (typeof openCaseEmailTab === 'function') {
openCaseEmailTab();
}
try {
if (typeof loadLinkedEmails === 'function') {
await loadLinkedEmails();
}
if (typeof loadLinkedEmailDetail === 'function') {
await loadLinkedEmailDetail(parsedId);
}
const emailTabPane = document.getElementById('emails');
if (emailTabPane) {
emailTabPane.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
} catch (error) {
console.error('Kunne ikke åbne email fra kommentar:', error);
}
}
function processCommentBodies() {
const commentItems = Array.from(document.querySelectorAll('#comments-container .comment-item'));
commentItems.forEach((item) => {
const body = item.querySelector('.comment-body');
if (!body) return;
const rawText = body.dataset.commentRaw || body.textContent || '';
if (!item.classList.contains('comment-system')) {
body.innerHTML = _escapeCommentHtml(String(rawText)).replace(/\n/g, '<br>');
return;
}
const hasEmailHeader = /(^|\n)\s*📧\s*(Indgående|Udgående)\s+email/i.test(String(rawText));
if (!hasEmailHeader) {
body.innerHTML = _escapeCommentHtml(String(rawText)).replace(/\n/g, '<br>');
return;
}
const parsed = _parseEmailComment(rawText);
const display = _buildEmailHeaderAndBody(parsed.visibleText || '');
const safeHeader = _escapeCommentHtml(display.summary || 'Indgaaende email');
const safeBody = _escapeCommentHtml(display.bodyText || '').replace(/\n/g, '<br>');
body.innerHTML = `
<div class="comment-email-header" title="${safeHeader}">${safeHeader}</div>
${display.bodyText ? `<div class="comment-email-text">${safeBody}</div>` : ''}
`;
const existingActions = item.querySelector('.comment-actions');
if (existingActions) {
existingActions.remove();
}
if (parsed.emailId) {
const actions = document.createElement('div');
actions.className = 'comment-actions';
actions.innerHTML = `
<button type="button" class="btn btn-link btn-sm" onclick="openEmailFromComment(${parsed.emailId})"><i class="bi bi-envelope-open me-1"></i>Aabn fuld mail</button>
<button type="button" class="btn btn-link btn-sm" onclick="quickReplyToEmailFromComment(${parsed.emailId})"><i class="bi bi-reply me-1"></i>Svar</button>
<button type="button" class="btn btn-link btn-sm js-quick-inline-reply"><i class="bi bi-lightning-charge me-1"></i>Quick svar</button>
`;
item.appendChild(actions);
const quickInlineBtn = actions.querySelector('.js-quick-inline-reply');
if (quickInlineBtn) {
quickInlineBtn.addEventListener('click', () => {
openInlineCommentQuickReply(rawText, parsed.emailId);
});
}
} else {
const actions = document.createElement('div');
actions.className = 'comment-actions';
actions.innerHTML = `
<button type="button" class="btn btn-link btn-sm" onclick="openCaseEmailTab()"><i class="bi bi-envelope me-1"></i>Aabn email-fane</button>
<button type="button" class="btn btn-link btn-sm js-reply-fallback"><i class="bi bi-reply me-1"></i>Svar</button>
<button type="button" class="btn btn-link btn-sm js-quick-reply-fallback"><i class="bi bi-lightning-charge me-1"></i>Quick svar</button>
`;
item.appendChild(actions);
const replyBtn = actions.querySelector('.js-reply-fallback');
if (replyBtn) {
replyBtn.addEventListener('click', () => {
quickReplyToEmailFromCommentText(rawText);
});
}
const quickReplyBtn = actions.querySelector('.js-quick-reply-fallback');
if (quickReplyBtn) {
quickReplyBtn.addEventListener('click', () => {
openInlineCommentQuickReply(rawText, null);
});
}
}
});
}
function sortCommentsNewestFirst() {
const container = document.getElementById('comments-container');
if (!container) return;
const items = Array.from(container.querySelectorAll('.comment-item'));
if (items.length < 2) return;
items
.sort((a, b) => Number(b.dataset.createdAt || 0) - Number(a.dataset.createdAt || 0))
.forEach((item) => container.appendChild(item));
}
async function submitComment(event) {
event.preventDefault();
const form = event.target;
const content = form.indhold.value;
const btn = form.querySelector('button');
const originalText = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Sender...';
btn.disabled = true;
try {
const response = await fetch('/api/v1/sag/{{ case.id }}/kommentarer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
indhold: content
})
});
if (response.ok) {
location.reload();
} else {
alert('Fejl ved oprettelse af kommentar');
btn.innerHTML = originalText;
btn.disabled = false;
}
} catch (error) {
console.error('Error:', error);
alert('Der skete en fejl. Prøv igen.');
btn.innerHTML = originalText;
btn.disabled = false;
}
}
// Keep newest comments visible at top
document.addEventListener('DOMContentLoaded', function() {
sortCommentsNewestFirst();
processCommentBodies();
const container = document.getElementById('comments-container');
if(container) {
container.scrollTop = 0;
}
});