- 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.
918 lines
55 KiB
JavaScript
918 lines
55 KiB
JavaScript
|
||
(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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
// ── 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 & 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);
|
||
|
||
})();
|
||
|