bmc_hub/script_10.js

918 lines
55 KiB
JavaScript
Raw Normal View History

(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);
})();