bmc_hub/script_10.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

918 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
'use strict';
let _openPopover = null;
// ── helpers ───────────────────────────────────────────────────────
function closeAllPopovers() {
document.querySelectorAll('.rel-qa-menu').forEach(el => el.remove());
_openPopover = null;
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.rel-qa-menu') && !e.target.closest('.btn-rel-action')) closeAllPopovers();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeAllPopovers();
});
function popoverPos(btn) {
const r = btn.getBoundingClientRect();
return { top: r.bottom + window.scrollY + 4, left: r.left + window.scrollX };
}
function esc(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// ── load global entity tags into rel-tag-row divs (using global tag system) ──
async function loadAllRelationTags() {
const rows = Array.from(document.querySelectorAll('.rel-tag-row'));
if (!rows.length) return;
// Wait briefly for tag-picker.js to initialize
const renderFn = () => window.renderEntityTags;
await new Promise(res => { const t = setInterval(() => { if (renderFn()) { clearInterval(t); res(); } }, 50); setTimeout(() => { clearInterval(t); res(); }, 2000); });
await Promise.all(rows.map(async el => {
const caseId = parseInt(el.id.replace('rel-tags-', ''));
if (isNaN(caseId) || !window.renderEntityTags) return;
await window.renderEntityTags('case', caseId, el.id);
}));
}
// ── tag button → opens global tag picker ──────────────────────────
window.openRelTagPopover = function(caseId) {
if (!window.showTagPicker) return;
window.showTagPicker('case', caseId, () => {
if (window.renderEntityTags) window.renderEntityTags('case', caseId, 'rel-tags-' + caseId);
});
};
// ── quick action menu ─────────────────────────────────────────────
const QA_ITEMS = [
{ icon: 'bi-person-check', label: 'Tildel sag', action: 'assign' },
{ icon: 'bi-clock', label: 'Tidregistrering', action: 'time' },
{ icon: 'bi-chat-left-text', label: 'Kommentar', action: 'note' },
{ icon: 'bi-bell', label: 'Påmindelse', action: 'reminder' },
{ icon: 'bi-graph-up-arrow', label: 'Salgspipeline', action: 'pipeline' },
{ icon: 'bi-paperclip', label: 'Filer', action: 'files' },
{ icon: 'bi-cpu', label: 'Hardware', action: 'hardware' },
{ icon: 'bi-check2-square', label: 'Opgave', action: 'todo' },
{ icon: 'bi-lightbulb', label: 'Løsning', action: 'solution' },
{ icon: 'bi-bag', label: 'Varekøb & salg', action: 'sales' },
{ icon: 'bi-arrow-repeat', label: 'Abonnement', action: 'subscription' },
{ icon: 'bi-envelope', label: 'Send email', action: 'email' },
];
// cache pipeline presence per caseId so we only fetch once per page load
const _pipelineCache = {};
window.openRelQaMenu = async function(caseId, caseTitle, btn) {
closeAllPopovers();
btn.classList.add('active');
const pos = popoverPos(btn);
const menu = document.createElement('div');
menu.className = 'rel-qa-menu';
menu.style.cssText = `position:absolute;top:${pos.top}px;left:${Math.max(0, pos.left - 120)}px;`;
menu.innerHTML = `<div style="font-size:.72rem;font-weight:700;color:var(--accent);padding:4px 12px 4px;">SAG-${caseId}</div>`
+ `<div style="font-size:.72rem;color:var(--text-secondary,#aaa);padding:2px 12px 4px;"><span class="spinner-border spinner-border-sm" style="width:.6rem;height:.6rem;border-width:.1em;"></span></div>`;
document.body.appendChild(menu);
_openPopover = menu;
// Fetch case data to check pipeline presence (cached)
if (!(_pipelineCache[caseId] !== undefined)) {
try {
const r = await fetch(`/api/v1/sag/${caseId}`, { credentials: 'include' });
if (r.ok) {
const d = await r.json();
_pipelineCache[caseId] = !!(d.pipeline_stage_id || d.pipeline_amount || d.pipeline_description);
} else {
_pipelineCache[caseId] = false;
}
} catch { _pipelineCache[caseId] = false; }
}
const hasPipeline = _pipelineCache[caseId];
// Filter: hide pipeline item if case already has one; show "Se pipeline" link instead
const items = QA_ITEMS.filter(i => i.action !== 'pipeline' || !hasPipeline);
const extra = hasPipeline
? `<div class="qa-item" style="opacity:.55;font-style:italic;" onclick="window.open('/sag/${caseId}','_blank')"><i class="bi bi-graph-up-arrow"></i>Pipeline (se sagen)</div>`
: '';
if (!_openPopover || _openPopover !== menu) return; // closed before fetch returned
menu.innerHTML = `<div style="font-size:.72rem;font-weight:700;color:var(--accent);padding:4px 12px 4px;">SAG-${caseId}</div>`
+ items.map(item =>
`<div class="qa-item" onclick="relQaAction('${item.action}',${caseId},'${caseTitle.replace(/'/g,"\\'")}')"><i class="bi ${item.icon}"></i>${esc(item.label)}</div>`
).join('')
+ extra;
};
function getRelQaPrimaryButton() {
const sidePanel = document.getElementById('caseAddSidePanel');
if (sidePanel && sidePanel.classList.contains('open')) {
return sidePanel.querySelector('#relQaModalFooter .btn-primary');
}
return document.querySelector('#relQaModalEl .btn-primary');
}
function closeRelQaSurfaceAfterSave() {
const sidePanel = document.getElementById('caseAddSidePanel');
const panelOpen = !!(sidePanel && sidePanel.classList.contains('open'));
const relModalEl = document.getElementById('relQaModalEl');
const relModalInstance = relModalEl ? bootstrap.Modal.getInstance(relModalEl) : null;
if (relModalInstance) {
relModalInstance.hide();
}
// In sidepanel mode, refresh to reflect new persisted data across modules.
if (panelOpen) {
setTimeout(() => window.location.reload(), 120);
}
}
window.relQaAction = function(action, caseId, caseTitle) {
closeAllPopovers();
if (action === 'time') openRelTimeModal(caseId, caseTitle);
else if (action === 'email') openRelEmailModal(caseId, caseTitle);
else if (action === 'note') openRelNoteModal(caseId, caseTitle);
else if (action === 'reminder') openRelReminderModal(caseId, caseTitle);
else if (action === 'todo') openRelTodoModal(caseId, caseTitle);
else if (action === 'assign') openRelAssignModal(caseId, caseTitle);
else if (action === 'pipeline') openRelPipelineModal(caseId, caseTitle);
else if (action === 'files') openRelFilesModal(caseId, caseTitle);
else if (action === 'hardware') openRelHardwareModal(caseId, caseTitle);
else if (action === 'solution') openRelSolutionModal(caseId, caseTitle);
else if (action === 'sales') openRelSalesModal(caseId, caseTitle);
else if (action === 'subscription') openRelSubscriptionModal(caseId, caseTitle);
else window.open(`/sag/${caseId}`, '_blank');
};
// ── Quick Pipeline modal ──────────────────────────────────────────
window.openRelPipelineModal = function(caseId, caseTitle) {
_showRelModal(
`<i class="bi bi-graph-up-arrow me-2"></i>Salgspipeline`,
`<div class="mb-2"><label class="form-label small fw-semibold">SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="mb-2">
<label class="form-label small fw-semibold">Stage</label>
<select id="rqp_stage" class="form-select form-select-sm">
<option value="">-- Vælg stage --</option>
<option value="1">Ny</option>
<option value="2">Afklaring</option>
<option value="3">Tilbud</option>
<option value="4">Commit</option>
<option value="5">Vundet</option>
<option value="6">Tabt</option>
<option value="7">Opsalg</option>
<option value="8">Lead</option>
<option value="9">Kontakt</option>
<option value="10">Forhandling</option>
</select>
</div>
<div class="row g-2 mb-2">
<div class="col-7">
<label class="form-label small fw-semibold">Beløb (DKK)</label>
<input type="number" id="rqp_amount" class="form-control form-control-sm" min="0" step="0.01" placeholder="0">
</div>
<div class="col-5">
<label class="form-label small fw-semibold">Sandsynlighed %</label>
<input type="number" id="rqp_prob" class="form-control form-control-sm" min="0" max="100" placeholder="0">
</div>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Note</label>
<textarea id="rqp_desc" class="form-control form-control-sm" rows="2" placeholder="Pipeline-note…"></textarea>
</div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelPipeline(${caseId})"><i class="bi bi-check2 me-1"></i>Gem</button>`
);
};
window._submitRelPipeline = async function(caseId) {
const stage = document.getElementById('rqp_stage').value;
const amount = document.getElementById('rqp_amount').value;
const prob = document.getElementById('rqp_prob').value;
const desc = document.getElementById('rqp_desc').value;
const payload = {};
if (stage) payload.stage_id = parseInt(stage);
if (amount) payload.amount = parseFloat(amount);
if (prob) payload.probability = parseInt(prob);
if (desc) payload.description = desc;
if (!Object.keys(payload).length) { if (typeof showNotification === 'function') showNotification('Udfyld mindst ét felt', 'warning'); return; }
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) { saveBtn.disabled = true; }
try {
const r = await fetch(`/api/v1/sag/${caseId}/pipeline`, { method: 'PATCH', credentials: 'include', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Pipeline opdateret ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Fejl', 'error');
if (saveBtn) saveBtn.disabled = false;
}
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── Quick Files modal ─────────────────────────────────────────────
window.openRelFilesModal = function(caseId, caseTitle) {
_showRelModal(
`<i class="bi bi-paperclip me-2"></i>Upload fil`,
`<div class="mb-2"><label class="form-label small fw-semibold">SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="mb-2">
<label class="form-label small fw-semibold">Vælg fil</label>
<input type="file" id="rqf_file" class="form-control form-control-sm" multiple>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Beskrivelse (valgfri)</label>
<input type="text" id="rqf_desc" class="form-control form-control-sm" placeholder="Fil-note…">
</div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelFiles(${caseId})"><i class="bi bi-upload me-1"></i>Upload</button>`
);
};
window._submitRelFiles = async function(caseId) {
const fileInput = document.getElementById('rqf_file');
if (!fileInput.files.length) { if (typeof showNotification === 'function') showNotification('Vælg mindst én fil', 'warning'); return; }
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Uploader…'; }
let success = 0; let failed = 0;
for (const file of fileInput.files) {
try {
const fd = new FormData();
fd.append('file', file);
const desc = document.getElementById('rqf_desc').value;
if (desc) fd.append('description', desc);
const r = await fetch(`/api/v1/sag/${caseId}/files`, { method: 'POST', credentials: 'include', body: fd });
if (r.ok) success++; else failed++;
} catch { failed++; }
}
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') {
if (failed === 0) showNotification(`${success} fil(er) uploadet ✓`, 'success');
else showNotification(`${success} ok, ${failed} fejlede`, 'warning');
}
};
// ── Quick Hardware modal ──────────────────────────────────────────
window.openRelHardwareModal = async function(caseId, caseTitle) {
_showRelModal(
`<i class="bi bi-cpu me-2"></i>Hardware`,
`<div class="mb-2"><label class="form-label small fw-semibold">SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="mb-2">
<label class="form-label small fw-semibold">Søg hardware</label>
<input type="text" id="rqhw_search" class="form-control form-control-sm" placeholder="Serienummer, navn…" autocomplete="off">
<div id="rqhw_results" class="mt-1" style="max-height:180px;overflow-y:auto;border:1px solid var(--border,#dee2e6);border-radius:6px;display:none;"></div>
</div>
<div id="rqhw_selected" class="text-muted small"></div>
<div class="mb-2 mt-2">
<label class="form-label small fw-semibold">Note (valgfri)</label>
<input type="text" id="rqhw_note" class="form-control form-control-sm" placeholder="Note om hardware…">
</div>
<input type="hidden" id="rqhw_id" value="">`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelHardware(${caseId})"><i class="bi bi-check2 me-1"></i>Tilknyt</button>`
);
// Wire up search
const inp = document.getElementById('rqhw_search');
const res = document.getElementById('rqhw_results');
let _hwTimer;
inp.addEventListener('input', () => {
clearTimeout(_hwTimer);
_hwTimer = setTimeout(async () => {
const q = inp.value.trim();
if (q.length < 2) { res.style.display='none'; return; }
try {
const r = await fetch(`/api/v1/search/hardware?q=${encodeURIComponent(q)}`, { credentials: 'include' });
if (!r.ok) return;
const items = await r.json();
if (!items.length) { res.innerHTML = '<div class="p-2 text-muted small">Ingen resultater</div>'; res.style.display='block'; return; }
res.innerHTML = items.slice(0,10).map(h =>
`<div class="p-2 border-bottom hw-opt" style="cursor:pointer;font-size:.82rem;" data-id="${h.id}" data-label="${esc(h.name||h.serial_number||h.id)}">${esc(h.name||'')} <span class="text-muted">${esc(h.serial_number||'')}</span></div>`
).join('');
res.style.display = 'block';
res.querySelectorAll('.hw-opt').forEach(el => el.addEventListener('click', () => {
document.getElementById('rqhw_id').value = el.dataset.id;
document.getElementById('rqhw_selected').textContent = '✓ Valgt: ' + el.dataset.label;
inp.value = el.dataset.label;
res.style.display = 'none';
}));
} catch {}
}, 300);
});
};
window._submitRelHardware = async function(caseId) {
const hwId = document.getElementById('rqhw_id').value;
if (!hwId) { if (typeof showNotification === 'function') showNotification('Vælg hardware fra listen', 'warning'); return; }
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) saveBtn.disabled = true;
try {
const r = await fetch(`/api/v1/sag/${caseId}/hardware`, {
method: 'POST', credentials: 'include', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ hardware_id: parseInt(hwId), note: document.getElementById('rqhw_note').value })
});
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Hardware tilknyttet ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Fejl', 'error');
if (saveBtn) saveBtn.disabled = false;
}
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── Quick Løsning modal ───────────────────────────────────────────
window.openRelSolutionModal = function(caseId, caseTitle) {
const today = new Date().toISOString().split('T')[0];
_showRelModal(
`<i class="bi bi-lightbulb me-2"></i>Løsning`,
`<div class="mb-2"><label class="form-label small fw-semibold">SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="mb-2">
<label class="form-label small fw-semibold">Titel</label>
<input type="text" id="rqs_title" class="form-control form-control-sm" placeholder="Løsningstitel…">
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Type</label>
<select id="rqs_type" class="form-select form-select-sm">
<option value="standard">Standard</option>
<option value="workaround">Workaround</option>
<option value="permanent">Permanent</option>
<option value="external">Ekstern</option>
</select>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Resultat</label>
<select id="rqs_result" class="form-select form-select-sm">
<option value="resolved">Løst</option>
<option value="partial">Delvist løst</option>
<option value="unresolved">Uløst</option>
</select>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Beskrivelse</label>
<textarea id="rqs_desc" class="form-control form-control-sm" rows="3" placeholder="Beskriv løsningen…"></textarea>
</div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelSolution(${caseId})"><i class="bi bi-check2 me-1"></i>Gem</button>`
);
};
window._submitRelSolution = async function(caseId) {
const title = document.getElementById('rqs_title').value.trim();
if (!title) { if (typeof showNotification === 'function') showNotification('Angiv en titel', 'warning'); return; }
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) saveBtn.disabled = true;
try {
const r = await fetch(`/api/v1/sag/${caseId}/solution`, {
method: 'POST', credentials: 'include', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
sag_id: caseId,
title,
solution_type: document.getElementById('rqs_type').value,
result: document.getElementById('rqs_result').value,
description: document.getElementById('rqs_desc').value,
})
});
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Løsning gemt ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Fejl', 'error');
if (saveBtn) saveBtn.disabled = false;
}
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── Quick Varekøb & Salg modal ────────────────────────────────────
window.openRelSalesModal = function(caseId, caseTitle) {
const today = new Date().toISOString().split('T')[0];
_showRelModal(
`<i class="bi bi-bag me-2"></i>Varekøb &amp; salg`,
`<div class="mb-2"><label class="form-label small fw-semibold">SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="mb-2">
<label class="form-label small fw-semibold">Type</label>
<select id="rqsl_type" class="form-select form-select-sm">
<option value="sale">Salg</option>
<option value="purchase">Indkøb</option>
</select>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Beskrivelse</label>
<input type="text" id="rqsl_desc" class="form-control form-control-sm" placeholder="Varebeskrivelse…">
</div>
<div class="row g-2 mb-2">
<div class="col-4">
<label class="form-label small fw-semibold">Antal</label>
<input type="number" id="rqsl_qty" class="form-control form-control-sm" min="1" value="1" step="1">
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Stykpris</label>
<input type="number" id="rqsl_uprice" class="form-control form-control-sm" min="0" step="0.01" placeholder="0.00">
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Total (DKK)</label>
<input type="number" id="rqsl_total" class="form-control form-control-sm" min="0" step="0.01" placeholder="0.00">
</div>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Dato</label>
<input type="date" id="rqsl_date" class="form-control form-control-sm" value="${today}">
</div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelSales(${caseId})"><i class="bi bi-check2 me-1"></i>Gem</button>`
);
// Auto-calculate total when qty/uprice changes
setTimeout(() => {
const qtyEl = document.getElementById('rqsl_qty');
const uprEl = document.getElementById('rqsl_uprice');
const totEl = document.getElementById('rqsl_total');
function calcTotal() {
const q = parseFloat(qtyEl.value) || 0;
const u = parseFloat(uprEl.value) || 0;
if (q && u) totEl.value = (q * u).toFixed(2);
}
qtyEl.addEventListener('input', calcTotal);
uprEl.addEventListener('input', calcTotal);
}, 50);
};
window._submitRelSales = async function(caseId) {
const desc = document.getElementById('rqsl_desc').value.trim();
const total = parseFloat(document.getElementById('rqsl_total').value);
if (!desc) { if (typeof showNotification === 'function') showNotification('Angiv beskrivelse', 'warning'); return; }
if (!total) { if (typeof showNotification === 'function') showNotification('Angiv beløb', 'warning'); return; }
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) saveBtn.disabled = true;
try {
const r = await fetch(`/api/v1/sag/${caseId}/sale-items`, {
method: 'POST', credentials: 'include', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
type: document.getElementById('rqsl_type').value,
description: desc,
quantity: parseFloat(document.getElementById('rqsl_qty').value) || 1,
unit_price: parseFloat(document.getElementById('rqsl_uprice').value) || null,
amount: total,
line_date: document.getElementById('rqsl_date').value || null,
status: 'draft',
})
});
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Varelinje oprettet ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Fejl', 'error');
if (saveBtn) saveBtn.disabled = false;
}
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── Quick Abonnement modal ────────────────────────────────────────
window.openRelSubscriptionModal = function(caseId, caseTitle) {
const today = new Date().toISOString().split('T')[0];
_showRelModal(
`<i class="bi bi-arrow-repeat me-2"></i>Abonnement`,
`<div class="mb-2"><label class="form-label small fw-semibold">SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="row g-2 mb-2">
<div class="col-6">
<label class="form-label small fw-semibold">Faktureringsinterval</label>
<select id="rqsub_interval" class="form-select form-select-sm">
<option value="monthly">Månedlig</option>
<option value="quarterly">Kvartalsvis</option>
<option value="yearly">Årlig</option>
<option value="weekly">Ugentlig</option>
</select>
</div>
<div class="col-3">
<label class="form-label small fw-semibold">Fakturering dag</label>
<input type="number" id="rqsub_day" class="form-control form-control-sm" min="1" max="28" value="1">
</div>
<div class="col-3">
<label class="form-label small fw-semibold">Startdato</label>
<input type="date" id="rqsub_start" class="form-control form-control-sm" value="${today}">
</div>
</div>
<div class="border rounded p-2 mb-2">
<div class="small fw-semibold mb-1">Varelinje</div>
<div class="row g-1">
<div class="col-6"><input type="text" id="rqsub_li_desc" class="form-control form-control-sm" placeholder="Beskrivelse"></div>
<div class="col-3"><input type="number" id="rqsub_li_qty" class="form-control form-control-sm" placeholder="Antal" min="1" value="1"></div>
<div class="col-3"><input type="number" id="rqsub_li_price" class="form-control form-control-sm" placeholder="Pris" min="0" step="0.01"></div>
</div>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Note (valgfri)</label>
<input type="text" id="rqsub_notes" class="form-control form-control-sm" placeholder="Intern note…">
</div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelSubscription(${caseId})"><i class="bi bi-check2 me-1"></i>Opret</button>`
);
};
window._submitRelSubscription = async function(caseId) {
const interval = document.getElementById('rqsub_interval').value;
const day = parseInt(document.getElementById('rqsub_day').value);
const startDate = document.getElementById('rqsub_start').value;
const liDesc = document.getElementById('rqsub_li_desc').value.trim();
const liQty = parseFloat(document.getElementById('rqsub_li_qty').value) || 1;
const liPrice = parseFloat(document.getElementById('rqsub_li_price').value) || 0;
if (!startDate) { if (typeof showNotification === 'function') showNotification('Angiv startdato', 'warning'); return; }
if (!liDesc || !liPrice) { if (typeof showNotification === 'function') showNotification('Udfyld varelinje (beskrivelse + pris)', 'warning'); return; }
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) saveBtn.disabled = true;
try {
const r = await fetch('/api/v1/sag-subscriptions', {
method: 'POST', credentials: 'include', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
sag_id: caseId,
billing_interval: interval,
billing_day: day,
start_date: startDate,
notes: document.getElementById('rqsub_notes').value || null,
line_items: [{ description: liDesc, quantity: liQty, unit_price: liPrice }]
})
});
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Abonnement oprettet ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Fejl', 'error');
if (saveBtn) saveBtn.disabled = false;
}
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── Quick Time modal ──────────────────────────────────────────────
window.openRelTimeModal = function(caseId, caseTitle) {
const today = new Date().toISOString().split('T')[0];
_showRelModal(
`<i class="bi bi-clock me-2"></i>Tidregistrering`,
`<div class="mb-2"><label class="form-label small fw-semibold">Sag</label>
<input class="form-control form-control-sm" readonly value="SAG-${caseId} ${esc(caseTitle)}"></div>
<div class="row g-2 mb-2">
<div class="col-6"><label class="form-label small fw-semibold">Dato</label>
<input type="date" id="rqt_date" class="form-control form-control-sm" value="${today}"></div>
<div class="col-3"><label class="form-label small fw-semibold">Timer</label>
<input type="number" id="rqt_h" class="form-control form-control-sm" min="0" max="23" value="0"></div>
<div class="col-3"><label class="form-label small fw-semibold">Min</label>
<input type="number" id="rqt_m" class="form-control form-control-sm" min="0" max="59" step="15" value="30"></div>
</div>
<div class="mb-2"><label class="form-label small fw-semibold">Fakturering</label>
<select id="rqt_billing" class="form-select form-select-sm">
<option value="invoice">Fakturerbar</option>
<option value="internal">Intern</option>
<option value="prepaid">Forudbetalt</option>
</select></div>
<div class="mb-2"><label class="form-label small fw-semibold">Beskrivelse</label>
<textarea id="rqt_desc" class="form-control form-control-sm" rows="2"></textarea></div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelTime(${caseId})"><i class="bi bi-check2 me-1"></i>Gem</button>`
);
};
window._submitRelTime = async function(caseId) {
const h = parseInt(document.getElementById('rqt_h').value) || 0;
const m = parseInt(document.getElementById('rqt_m').value) || 0;
const totalHours = parseFloat((h + m / 60).toFixed(4));
if (totalHours <= 0) {
if (typeof showNotification === 'function') showNotification('Angiv tid (timer/minutter)', 'warning');
return;
}
const billing = document.getElementById('rqt_billing')?.value || 'invoice';
const payload = {
sag_id: caseId,
worked_date: document.getElementById('rqt_date').value,
original_hours: totalHours,
description: document.getElementById('rqt_desc').value,
billing_method: billing,
is_internal: billing === 'internal',
};
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; }
try {
const r = await fetch('/api/v1/timetracking/entries/internal', { method: 'POST', credentials: 'include', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Tid registreret ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Fejl ved registrering', 'error');
if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = '<i class="bi bi-check2 me-1"></i>Gem'; }
}
} catch { if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = 'Gem'; } }
};
// ── Quick Email modal ─────────────────────────────────────────────
window.openRelEmailModal = function(caseId, caseTitle) {
const defaultRecipient = typeof getDefaultCaseRecipient === 'function' ? getDefaultCaseRecipient() : '';
const defaultSubject = `Sag #${caseId}: `;
const attachmentOptions = Array.isArray(sagFilesCache) && sagFilesCache.length
? sagFilesCache
.map((file) => {
const fileId = Number(file.id);
const filename = esc(file.filename || `Fil ${fileId}`);
return `<option value="${fileId}">${filename}</option>`;
})
.join('')
: '<option disabled>Ingen sagsfiler</option>';
_showRelModal(
`<i class="bi bi-envelope me-2"></i>Email`,
`<div class="mb-2"><label class="form-label small fw-semibold">Sag: SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="row g-2 mb-2">
<div class="col-12"><label class="form-label small fw-semibold">Til</label>
<input type="text" id="rqe_to" class="form-control form-control-sm" placeholder="modtager@eksempel.dk" value="${esc(defaultRecipient)}"></div>
<div class="col-6"><label class="form-label small fw-semibold">Cc</label>
<input type="text" id="rqe_cc" class="form-control form-control-sm" placeholder="cc@eksempel.dk"></div>
<div class="col-6"><label class="form-label small fw-semibold">Bcc</label>
<input type="text" id="rqe_bcc" class="form-control form-control-sm" placeholder="bcc@eksempel.dk"></div>
</div>
<div class="mb-2"><label class="form-label small fw-semibold">Emne</label>
<input type="text" id="rqe_subject" class="form-control form-control-sm" value="${esc(defaultSubject)}"></div>
<div class="mb-2"><label class="form-label small fw-semibold">Vedhaeftninger</label>
<select id="rqe_attachment_ids" class="form-select form-select-sm" multiple>${attachmentOptions}</select>
</div>
<div class="mb-2"><label class="form-label small fw-semibold">Besked</label>
<textarea id="rqe_body" class="form-control form-control-sm" rows="6" placeholder="Skriv besked..."></textarea></div>
<div id="rqe_status" class="small text-muted"></div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelEmail(${caseId})"><i class="bi bi-send me-1"></i>Send email</button>`
);
};
window._submitRelEmail = async function(caseId) {
const toInput = document.getElementById('rqe_to');
const ccInput = document.getElementById('rqe_cc');
const bccInput = document.getElementById('rqe_bcc');
const subjectInput = document.getElementById('rqe_subject');
const bodyInput = document.getElementById('rqe_body');
const attachmentSelect = document.getElementById('rqe_attachment_ids');
const statusEl = document.getElementById('rqe_status');
const saveBtn = getRelQaPrimaryButton();
if (!toInput || !subjectInput || !bodyInput || !statusEl) return;
const to = parseEmailField(toInput.value);
const cc = parseEmailField(ccInput?.value || '');
const bcc = parseEmailField(bccInput?.value || '');
const subject = (subjectInput.value || '').trim();
const bodyText = (bodyInput.value || '').trim();
const attachmentFileIds = Array.from(attachmentSelect?.selectedOptions || [])
.map((opt) => Number(opt.value))
.filter((id) => Number.isInteger(id) && id > 0);
if (!to.length) {
if (typeof showNotification === 'function') showNotification('Udfyld mindst en modtager.', 'warning');
return;
}
if (!subject) {
if (typeof showNotification === 'function') showNotification('Udfyld emne.', 'warning');
return;
}
if (!bodyText) {
if (typeof showNotification === 'function') showNotification('Udfyld besked.', 'warning');
return;
}
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sender...';
}
statusEl.className = 'small text-muted';
statusEl.textContent = 'Sender e-mail...';
try {
const res = await fetch(`/api/v1/sag/${caseId}/emails/send`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
to,
cc,
bcc,
subject,
body_text: bodyText,
attachment_file_ids: attachmentFileIds,
thread_email_id: selectedLinkedEmailId || null,
thread_key: linkedEmailsCache.find((entry) => Number(entry.id) === Number(selectedLinkedEmailId))?.thread_key || null
})
});
if (!res.ok) {
let message = `HTTP ${res.status} ${res.statusText || 'Send failed'}`;
try {
const responseText = await res.text();
if (responseText) {
try {
const err = JSON.parse(responseText);
if (err?.detail) {
message = err.detail;
} else if (err?.message) {
message = err.message;
}
} catch (_) {
message = responseText.slice(0, 500);
}
}
} catch (_) {
}
throw new Error(message);
}
statusEl.className = 'small text-success';
statusEl.textContent = 'E-mail sendt.';
if (typeof loadLinkedEmails === 'function') {
loadLinkedEmails();
}
if (typeof showNotification === 'function') showNotification('E-mail sendt.', 'success');
const relModalEl = document.getElementById('relQaModalEl');
const relModal = relModalEl ? bootstrap.Modal.getInstance(relModalEl) : null;
if (relModal) relModal.hide();
} catch (error) {
statusEl.className = 'small text-danger';
statusEl.textContent = error?.message || 'Email send failed (ukendt fejl)';
if (typeof showNotification === 'function') showNotification(statusEl.textContent, 'error');
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="bi bi-send me-1"></i>Send email';
}
return;
}
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="bi bi-send me-1"></i>Send email';
}
};
// ── Quick Kommentar modal ─────────────────────────────────────────
window.openRelNoteModal = function(caseId, caseTitle) {
_showRelModal(
`<i class="bi bi-chat-left-text me-2"></i>Kommentar`,
`<div class="mb-2"><label class="form-label small fw-semibold">Sag: SAG-${caseId} ${esc(caseTitle)}</label></div>
<textarea id="rqn_text" class="form-control" rows="4" placeholder="Skriv kommentar..."></textarea>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelNote(${caseId})"><i class="bi bi-check2 me-1"></i>Gem</button>`
);
};
window._submitRelNote = async function(caseId) {
const text = document.getElementById('rqn_text').value.trim();
if (!text) return;
const saveBtn = document.querySelector('#relQaModalEl .btn-primary');
if (saveBtn) { saveBtn.disabled = true; }
try {
const r = await fetch(`/api/v1/sag/${caseId}/kommentarer`, {
method: 'POST', credentials: 'include',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ forfatter: 'Hurtig kommentar', indhold: text })
});
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Kommentar tilføjet ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Fejl ved gemning', 'error');
if (saveBtn) saveBtn.disabled = false;
}
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── Quick Opgave modal ────────────────────────────────────────────
window.openRelTodoModal = function(caseId, caseTitle) {
const today = new Date().toISOString().split('T')[0];
_showRelModal(
`<i class="bi bi-check2-square me-2"></i>Opgave`,
`<div class="mb-2"><label class="form-label small fw-semibold">Sag: SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="mb-2"><label class="form-label small fw-semibold">Opgavetitel</label>
<input type="text" id="rqtd_title" class="form-control form-control-sm" placeholder="Hvad skal gøres?"></div>
<div class="mb-2"><label class="form-label small fw-semibold">Frist (valgfri)</label>
<input type="date" id="rqtd_due" class="form-control form-control-sm" value="${today}"></div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelTodo(${caseId})"><i class="bi bi-check2 me-1"></i>Opret</button>`
);
};
window._submitRelTodo = async function(caseId) {
const title = document.getElementById('rqtd_title').value.trim();
if (!title) { if (typeof showNotification === 'function') showNotification('Angiv opgavetitel', 'warning'); return; }
const due = document.getElementById('rqtd_due').value || null;
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) { saveBtn.disabled = true; }
try {
const r = await fetch(`/api/v1/sag/${caseId}/todos`, {
method: 'POST', credentials: 'include',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ titel: title, frist: due, sag_id: caseId })
});
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Opgave oprettet ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Opgave-endpoint ikke tilgængeligt endnu', 'warning');
if (saveBtn) saveBtn.disabled = false;
}
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── Quick Tildel sag modal ────────────────────────────────────────
window.openRelAssignModal = async function(caseId, caseTitle) {
_showRelModal(
`<i class="bi bi-person-check me-2"></i>Tildel sag`,
`<div class="mb-2"><label class="form-label small fw-semibold">SAG-${caseId} ${esc(caseTitle)}</label></div>
<label class="form-label small fw-semibold">Ansvarlig bruger</label>
<select id="rqa_user" class="form-select form-select-sm"><option>Henter brugere…</option></select>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelAssign(${caseId})"><i class="bi bi-check2 me-1"></i>Tildel</button>`
);
try {
const r = await fetch('/api/v1/users', { credentials: 'include' });
if (r.ok) {
const users = await r.json();
const sel = document.getElementById('rqa_user');
if (sel) sel.innerHTML = '<option value="">Ingen (fjern tildeling)</option>'
+ users.map(u => `<option value="${u.user_id}">${esc(u.display_name || u.username || '')}</option>`).join('');
}
} catch {}
};
window._submitRelAssign = async function(caseId) {
const userId = document.getElementById('rqa_user')?.value;
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) { saveBtn.disabled = true; }
try {
const r = await fetch(`/api/v1/sag/${caseId}`, {
method: 'PATCH', credentials: 'include',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ ansvarlig_bruger_id: userId ? parseInt(userId) : null })
});
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Sag tildelt ✓', 'success');
} else {
const d = await r.json().catch(()=>({}));
if (typeof showNotification === 'function') showNotification(d.detail || 'Fejl ved tildeling', 'error');
if (saveBtn) saveBtn.disabled = false;
}
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── Quick Reminder modal ──────────────────────────────────────────
window.openRelReminderModal = function(caseId, caseTitle) {
const tmr = new Date(); tmr.setDate(tmr.getDate()+1);
const tmrStr = tmr.toISOString().slice(0,16);
_showRelModal(
`<i class="bi bi-bell me-2"></i>Påmindelse`,
`<div class="mb-2"><label class="form-label small fw-semibold">Sag: SAG-${caseId} ${esc(caseTitle)}</label></div>
<div class="mb-2"><label class="form-label small fw-semibold">Tidspunkt</label>
<input type="datetime-local" id="rqr_at" class="form-control form-control-sm" value="${tmrStr}"></div>
<div class="mb-2"><label class="form-label small fw-semibold">Besked</label>
<input type="text" id="rqr_msg" class="form-control form-control-sm" placeholder="Husk at…"></div>`,
`<button class="btn btn-sm btn-primary" onclick="_submitRelReminder(${caseId})"><i class="bi bi-check2 me-1"></i>Gem</button>`
);
};
window._submitRelReminder = async function(caseId) {
const payload = { sag_id: caseId, remind_at: document.getElementById('rqr_at').value, message: document.getElementById('rqr_msg').value };
const saveBtn = getRelQaPrimaryButton();
if (saveBtn) { saveBtn.disabled = true; }
try {
const r = await fetch('/api/v1/reminders', {
method: 'POST', credentials: 'include',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
if (r.ok) {
closeRelQaSurfaceAfterSave();
if (typeof showNotification === 'function') showNotification('Påmindelse oprettet', 'success');
} else { if (saveBtn) saveBtn.disabled = false; }
} catch { if (saveBtn) saveBtn.disabled = false; }
};
// ── shared modal helper ───────────────────────────────────────────
window._showRelModal = function(title, bodyHtml, footerBtns) {
let el = document.getElementById('relQaModalEl');
if (!el) {
el = document.createElement('div');
el.id = 'relQaModalEl';
el.className = 'modal fade';
el.tabIndex = -1;
el.innerHTML = `<div class="modal-dialog modal-dialog-centered"><div class="modal-content">
<div class="modal-header py-2 px-3">
<h6 class="modal-title mb-0" id="relQaModalTitle"></h6>
<button type="button" class="btn-close btn-sm" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="relQaModalBody"></div>
<div class="modal-footer py-2 px-3" id="relQaModalFooter">
<button class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Annuller</button>
</div>
</div></div>`;
document.body.appendChild(el);
}
document.getElementById('relQaModalTitle').innerHTML = title;
document.getElementById('relQaModalBody').innerHTML = bodyHtml;
const footer = document.getElementById('relQaModalFooter');
// Remove old action buttons (keep Annuller)
footer.querySelectorAll('.btn-primary').forEach(b => b.remove());
if (footerBtns) footer.insertAdjacentHTML('afterbegin', footerBtns);
new bootstrap.Modal(el).show();
};
// ── init on page load ─────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', loadAllRelationTags);
})();