bmc_hub/static/js/case-create.js

257 lines
28 KiB
JavaScript
Raw Normal View History

/* Progressive case creation. Existing searches and submit retain their API contracts. */
(() => {
'use strict';
const $ = id => document.getElementById(id);
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const state = {tags: [], hardware: [], tagCatalog: [], panels: {}, ready: false, restoring: false, key: null, pending: false, completed: false};
let saveTimer, duplicateTimer, duplicateToken = 0, workloadToken = 0, contactCasesToken = 0, tagSuggestionTimer, templates = [];
async function api(url, options) {
const response = await fetch(url, {credentials: 'include', ...options});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(typeof body.detail === 'string' ? body.detail : 'Kunne ikke hente oplysninger');
return body;
}
function panel(id, title, elements, open = false) {
const detail = document.createElement('details');
detail.className = 'cc-panel'; detail.id = `cc-${id}`; detail.open = open;
const icon = {relations:'bi-people',tags:'bi-tags',hardware:'bi-pc-display',pipeline:'bi-graph-up-arrow',order:'bi-bag',metadata:'bi-sliders'}[id];
detail.innerHTML = `<summary><span class="cc-panel-icon"><i class="bi ${icon}" aria-hidden="true"></i></span><span class="cc-panel-label">${esc(title)}<small class="cc-summary"></small></span></summary><div class="cc-panel-body"></div>`;
elements[0].before(detail);
elements.forEach(el => detail.lastElementChild.append(el));
state.panels[id] = detail;
detail.addEventListener('toggle', scheduleSave);
return detail;
}
function summaries() {
const values = {
relations: `${selectedCustomer?.name || 'Vælg firma'} · ${Object.keys(selectedContacts).length} kontakter`,
tags: state.tags.map(t => t.name).join(', ') || 'Ingen tags',
hardware: `${state.hardware.length} enheder valgt`,
pipeline: [$('pipeline_stage_id').selectedOptions[0]?.text, $('pipeline_amount').value ? `${$('pipeline_amount').value} kr.` : ''].filter(Boolean).join(' · '),
order: `${document.querySelectorAll('#orderLines .order-line').length} linjer`,
metadata: [$('status').selectedOptions[0]?.text, $('ansvarlig_bruger_id').selectedOptions[0]?.text, $('deadline').value.replace('T', ' ')].filter(Boolean).join(' · ')
};
Object.entries(values).forEach(([key, value]) => { if (state.panels[key]) state.panels[key].querySelector('.cc-summary').textContent = value; });
}
function fieldSnapshot() {
return Object.fromEntries(Array.from($('createForm').querySelectorAll('input[id],select[id],textarea[id]')).filter(el => !['customerSearch','contactSearch'].includes(el.id)).map(el => [el.id, el.type === 'checkbox' ? el.checked : el.value]));
}
function snapshot() {
return {version: 1, savedAt: Date.now(), fields: fieldSnapshot(), customer: selectedCustomer, contacts: selectedContacts, contactCompanies: selectedContactsCompanies,
tags: state.tags, hardware: state.hardware, orders: collectOrderItems(), prefill: telefoniPrefill,
customerSearch: $('customerSearch').value, contactSearch: $('contactSearch').value,
panels: Object.fromEntries(Object.entries(state.panels).map(([k, el]) => [k, el.open]))};
}
function save() {
if (!state.ready || state.pending || state.restoring || state.completed || !state.key) return;
try { localStorage.setItem(state.key, JSON.stringify(snapshot())); $('cc-save').textContent = 'Kladde gemt lokalt'; }
catch (error) { console.error('Kunne ikke gemme kladde', error); $('cc-save').textContent = 'Kladde kunne ikke gemmes i browseren'; }
}
function scheduleSave() {
summaries();
if (!state.ready || state.pending || state.restoring || state.completed) return;
$('cc-save').textContent = 'Gemmer kladde…'; clearTimeout(saveTimer); saveTimer = setTimeout(save, 500);
}
function renderTags() {
$('cc-tag-chips').innerHTML = state.tags.map(t => `<span class="badge me-1 mb-1" style="background-color:#64748b" data-tag-id="${Number(t.id)}">${/^bi-[a-z0-9-]+$/.test(t.icon || '') ? `<i class="bi ${esc(t.icon)}"></i> ` : ''}${esc(t.name)} <button type="button" class="btn-close btn-close-white" aria-label="Fjern ${esc(t.name)}"></button></span>`).join('');
state.tags.forEach(t => { const chip = $('cc-tag-chips').querySelector(`[data-tag-id="${Number(t.id)}"]`); if (/^#[0-9a-f]{6}$/i.test(t.color)) chip.style.backgroundColor = t.color; });
summaries();
renderTagSuggestions();
}
function renderTagSuggestions() {
const box = $('cc-tag-suggestions');
if (!box) return;
const haystack = `${$('titel')?.value || ''} ${$('beskrivelse')?.value || ''}`.toLocaleLowerCase('da-DK');
if (haystack.trim().length < 3 || !state.tagCatalog.length) { box.innerHTML = ''; return; }
const selected = new Set(state.tags.map(tag => Number(tag.id)));
const suggestions = state.tagCatalog.filter(tag => {
if (selected.has(Number(tag.id)) || !['brand', 'type'].includes(tag.type)) return false;
const words = Array.isArray(tag.catch_words) ? tag.catch_words : [];
return words.some(word => String(word).trim() && haystack.includes(String(word).trim().toLocaleLowerCase('da-DK')));
}).slice(0, 8);
box.innerHTML = suggestions.length ? `<div class="cc-tag-suggestions__label">Foreslået ud fra teksten</div><div class="d-flex flex-wrap gap-1">${suggestions.map(tag => `<button type="button" class="btn btn-sm btn-outline-primary" data-suggested-tag="${Number(tag.id)}" title="Tilføj ${esc(tag.name)}">${tag.icon ? `<i class="bi ${esc(tag.icon)}"></i> ` : ''}${esc(tag.name)} <i class="bi bi-plus-lg"></i></button>`).join('')}</div>` : '';
}
function scheduleTagSuggestions() {
clearTimeout(tagSuggestionTimer);
tagSuggestionTimer = setTimeout(renderTagSuggestions, 350);
}
async function addTag(tag) {
if (!state.tags.some(t => t.id === tag.id)) {
const groups = await api('/api/v1/tags/groups').catch(() => []);
if (groups.some(g => g.id === tag.tag_group_id && ['single','toggle'].includes(g.behavior))) state.tags = state.tags.filter(t => t.tag_group_id !== tag.tag_group_id);
state.tags.push(tag); renderTags(); scheduleSave();
}
}
function renderSelectedHardware() {
$('cc-hardware-chips').innerHTML = state.hardware.map(h => `<span class="badge bg-secondary me-1">${esc(h.name)} <button type="button" class="btn-close btn-close-white" data-remove-hardware="${Number(h.id)}" aria-label="Fjern hardware"></button></span>`).join('');
document.querySelectorAll('[data-hardware-choice]').forEach(el => { el.checked = state.hardware.some(h => h.id === Number(el.dataset.hardwareChoice)); });
summaries();
}
async function duplicates() {
const token = ++duplicateToken, customerId = selectedCustomer?.id, title = $('titel').value.trim();
$('cc-duplicates').innerHTML = '';
if (!customerId || title.length < 5) return;
try {
const rows = await api(`/api/v1/case-create/duplicates?customer_id=${customerId}&title=${encodeURIComponent(title)}`);
if (token !== duplicateToken) return;
$('cc-duplicates').innerHTML = rows.length ? `<div class="alert alert-warning"><strong>Mulige eksisterende sager</strong>${rows.map(r => `<div><a target="_blank" rel="noopener" href="/sag/${r.id}/v3">SAG-${r.id} · ${esc(r.titel)}</a><small> · ${esc(r.status)} · ${esc(r.ansvarlig_navn || 'Ingen ansvarlig')}</small></div>`).join('')}</div>` : '';
} catch { if (token === duplicateToken) $('cc-duplicates').textContent = 'Duplikatkontrol er midlertidigt utilgængelig. Du kan stadig oprette sagen.'; }
}
function scheduleDuplicates() { ++duplicateToken; clearTimeout(duplicateTimer); duplicateTimer = setTimeout(duplicates, 500); }
async function contactOpenCases() {
const box = $('cc-contact-cases');
if (!box) return;
const contacts = Object.values(selectedContacts);
const token = ++contactCasesToken;
if (!contacts.length) { box.hidden = true; box.innerHTML = ''; return; }
box.hidden = false;
box.innerHTML = '<span class="text-muted"><span class="spinner-border spinner-border-sm me-1"></span>Tjekker kontaktens åbne sager…</span>';
const query = contacts.map(contact => `contact_ids=${encodeURIComponent(contact.id)}`).join('&');
try {
const data = await api(`/api/v1/case-create/contacts-open-cases?${query}`);
if (token !== contactCasesToken) return;
const groups = contacts.map(contact => ({contact, data: data[contact.id] || {total:0, items:[]}}));
const total = groups.reduce((sum, group) => sum + Number(group.data.total || 0), 0);
const title = total ? `${total} åben${total === 1 ? '' : 'ne'} sag${total === 1 ? '' : 'er'} på valgt kontakt` : 'Ingen åbne sager på valgt kontakt';
box.innerHTML = `<div class="cc-contact-cases__header"><span class="cc-contact-cases__header-icon"><i class="bi ${total ? 'bi-clipboard2-check' : 'bi-check2-circle'}"></i></span><div><span class="cc-contact-cases__eyebrow">Dublettjek</span><span class="cc-contact-cases__title">${title}</span></div></div>${total ? `<div class="cc-contact-cases__body">${groups.map(({contact,data}) => data.total ? `<div class="cc-contact-cases__group"><span class="cc-contact-cases__group-name">${esc(contact.name)}</span>${data.items.map(item => `<div class="cc-contact-cases__item"><a href="/sag/${Number(item.id)}/v3" target="_blank" rel="noopener">SAG-${Number(item.id)} · ${esc(item.titel)}</a><small>${esc(item.status || '')} · ${esc(item.ansvarlig_navn || 'Ingen ansvarlig')}${item.deadline ? ` · ${esc(new Date(item.deadline).toLocaleDateString('da-DK'))}` : ''}</small></div>`).join('')}${data.total > data.items.length ? `<span class="cc-contact-cases__more">Viser ${data.items.length} af ${data.total} åbne sager</span>` : ''}</div>` : '').join('')}</div>` : '<div class="cc-contact-cases__empty">Du kan oprette sagen uden risiko for en kendt, åben sag på kontakten.</div>'}`;
} catch {
if (token === contactCasesToken) box.innerHTML = '<span class="text-muted">Kunne ikke tjekke kontaktens åbne sager. Du kan stadig oprette sagen.</span>';
}
}
async function workload() {
const token = ++workloadToken, id = $('ansvarlig_bruger_id').value;
$('cc-workload').textContent = id ? 'Henter åbne sager…' : 'Vælg en ansvarlig for at se åbne sager.';
if (!id) return;
try {
const data = await api(`/api/v1/case-create/workload?user_id=${id}`);
if (token !== workloadToken) return;
$('cc-workload').innerHTML = `<details><summary>${data.total} åbne sager · ${esc($('ansvarlig_bruger_id').selectedOptions[0].text)}</summary>${data.items.map(r => `<div class="cc-work-item"><a target="_blank" rel="noopener" href="/sag/${r.id}/v3">SAG-${r.id} · ${esc(r.titel)}</a><small>${esc(r.customer_name || '')} · ${esc(r.status)} <span class="${r.deadline && new Date(r.deadline) < new Date() ? 'text-danger' : ''}">${r.deadline ? esc(new Date(r.deadline).toLocaleString('da-DK')) : ''}</span></small></div>`).join('') || '<p class="text-success">Ingen åbne sager.</p>'}${data.total > 10 ? `<a href="/sag?ansvarlig_bruger_id=${id}" target="_blank" rel="noopener">Se alle ${data.total} sager</a>` : ''}</details>`;
} catch { if (token === workloadToken) $('cc-workload').textContent = 'Kunne ikke hente åbne sager.'; }
}
function typeChanged(type) {
document.querySelectorAll('[data-case-type]').forEach(b => { const active = b.dataset.caseType === type; b.classList.toggle('active', active); b.setAttribute('aria-pressed', String(active)); });
const key = {ticket:'hardware', pipeline:'pipeline', ordre:'order'}[type];
if (key && state.panels[key] && !state.restoring) state.panels[key].open = true;
if ($('cc-more')) $('cc-more').open = Boolean($('cc-more').querySelector(`[data-case-type="${CSS.escape(type)}"]`));
scheduleSave();
}
function buildTypes() {
const options = Array.from($('type').options), current = $('type').value;
const keys = [...new Set([current, 'ticket','pipeline','ordre','opgave', ...options.map(o => o.value)])].filter(k => options.some(o => o.value === k));
const hints = {ticket:'Support og hjælp',pipeline:'Muligheder og tilbud',ordre:'Indkøb og salg',opgave:'Noget der skal løses',projekt:'Større forløb',service:'Service og vedligehold',abonnement:'Løbende aftaler'};
const button = key => `<button type="button" class="btn btn-outline-primary" data-case-type="${esc(key)}" aria-pressed="false"><span class="cc-type-name">${esc(options.find(o => o.value === key).text)}</span><small class="cc-type-hint">${esc(hints[key] || 'Opret sag')}</small></button>`;
const messageButton = $('caseMessageTypeButton');
$('type').classList.add('d-none'); $('type').removeAttribute('required');
const div = document.createElement('div'); div.id = 'cc-types'; div.className = 'cc-types'; div.setAttribute('role', 'group'); div.setAttribute('aria-label', 'Sagstype');
div.innerHTML = keys.slice(0,4).map(button).join('') + (keys.length > 4 ? `<details id="cc-more"><summary>Flere</summary><div class="cc-types">${keys.slice(4).map(button).join('')}</div></details>` : '');
if (messageButton) { messageButton.classList.remove('cc-message-fallback'); messageButton.dataset.caseMessage = 'true'; div.insertBefore(messageButton, div.querySelector('#cc-more')); }
$('type').after(div);
div.addEventListener('click', e => { const message = e.target.closest('[data-case-message]'); if (message) { window.caseCreateUI?.openMessage(); return; } const b = e.target.closest('[data-case-type]'); if (b) { $('type').value = b.dataset.caseType; $('type').dispatchEvent(new Event('change', {bubbles:true})); } });
}
async function applyTemplate(id) {
const template = templates.find(t => t.id === Number(id)); if (!template) return;
const values = template.values, fields = {type: values.type, titel: values.titel, beskrivelse: values.beskrivelse, status: values.status, assigned_group_id: values.assigned_group_id ?? ''};
Object.entries(values.pipeline || {}).forEach(([k,v]) => { fields[`pipeline_${k === 'stage_id' ? 'stage_id' : k}`] = v ?? ''; });
if (Object.entries(fields).some(([k,v]) => $(k)?.value && String(v) !== $(k).value) && !confirm('Skabelonen ændrer udfyldte felter. Vil du bruge den?')) return;
Object.entries(fields).forEach(([k,v]) => { const el = $(k); if (el && (el.tagName !== 'SELECT' || Array.from(el.options).some(o => o.value === String(v)))) el.value = v; });
const tags = await api('/api/v1/tags?is_active=true');
for (const id of values.tag_ids || []) { const tag = tags.find(t => t.id === id); if (tag) await addTag(tag); }
updateCaseTypeSections(); $('charCount').textContent = `${$('beskrivelse').value.length} tegn`; scheduleSave(); scheduleDuplicates();
}
async function restore(draft) {
state.restoring = true;
try {
selectedCustomer = draft.customer; selectedContacts = draft.contacts || {}; selectedContactsCompanies = draft.contactCompanies || {};
telefoniPrefill = draft.prefill || telefoniPrefill;
Object.entries(draft.fields || {}).forEach(([id,value]) => { const el = $(id); if (el && !id.startsWith('cc-')) { if (el.type === 'checkbox') el.checked = value; else el.value = value; } });
state.tags = draft.tags || []; state.hardware = draft.hardware || [];
$('orderLines').innerHTML = '';
(draft.orders || []).forEach(item => { addOrderLine(item.type); const row = $('orderLines').lastElementChild; Object.entries({description:'description',quantity:'quantity',unit:'unit',unit_price:'unit-price',amount:'amount',currency:'currency',status:'status',external_ref:'reference',purchase_purpose:'purpose'}).forEach(([k,cls]) => { const el = row.querySelector(`.order-${cls}`); if (el) el.value = item[k] ?? ''; }); });
renderSelections(); await loadHardwareForContacts(); renderTags(); renderSelectedHardware(); renderOrderLinesEmptyState();
await loadCreateTopAlertsForCustomer(selectedCustomer?.id || null);
if (selectedCustomer) await loadSelectedCustomerContacts(selectedCustomer.id);
$('customerSearch').value = draft.customerSearch || ''; $('contactSearch').value = draft.contactSearch || '';
updateCaseTypeSections(); Object.entries(draft.panels || {}).forEach(([k,v]) => { if (state.panels[k]) state.panels[k].open = v; });
$('charCount').textContent = `${$('beskrivelse').value.length} tegn`;
} finally { state.restoring = false; }
state.pending = false; $('cc-draft').remove(); scheduleSave(); scheduleDuplicates(); workload();
}
function validate() {
document.querySelectorAll('.cc-error').forEach(el => el.remove());
document.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
const errors = [];
const add = (el, text) => { errors.push(text); el.classList.add('is-invalid'); el.insertAdjacentHTML('afterend', `<div class="text-danger small cc-error">${esc(text)}</div>`); const detail = el.closest('details'); if (detail) { detail.open = true; if (!detail.querySelector('summary .cc-error')) detail.querySelector('summary').insertAdjacentHTML('beforeend','<span class="text-danger small cc-error"> · Kontrollér felter</span>'); } };
if (!$('titel').value.trim()) add($('titel'), 'Titel er påkrævet');
if (!selectedCustomer && $('customerSearch').value.trim().length < 2) add($('customerSearch'), 'Vælg et firma');
$('createForm').querySelectorAll('input[type=number]').forEach(el => { if (!el.checkValidity()) add(el, 'Kontrollér tallets interval og format'); });
document.querySelectorAll('#orderLines .order-line').forEach(row => { for (const cls of ['description','amount']) { const el = row.querySelector(`.order-${cls}`); if (!el.value.trim()) add(el, cls === 'description' ? 'Ordrelinjen mangler beskrivelse' : 'Ordrelinjen mangler beløb'); } });
if (errors.length) { $('error').classList.remove('d-none'); $('error-text').textContent = [...new Set(errors)].join(' · '); document.querySelector('.is-invalid')?.focus(); return false; }
return true;
}
async function init() {
const form = $('createForm');
try {
const setting = await api('/api/v1/settings/case_statuses');
const statuses = JSON.parse(setting.value || '[]');
if (Array.isArray(statuses)) statuses.forEach(s => { const value = typeof s === 'string' ? s : s.value; if (value && !Array.from($('status').options).some(o => o.value === value)) $('status').add(new Option(typeof s === 'string' ? s : (s.label || value), value)); });
} catch { /* Keep the built-in statuses available. */ }
const headings = Array.from(form.querySelectorAll(':scope > h5'));
panel('relations', 'Kunde og kontakter', [headings[0], headings[0].nextElementSibling], true);
for (const [key,id,title] of [['hardware','hardwareSection','Hardware og AnyDesk'],['pipeline','pipelineSection','Pipeline'],['order','orderSection','Indkøb og salg']]) panel(key, title, [$(id)]);
const metadata = headings[1]; panel('metadata', 'Status, ansvar og deadline', [metadata,metadata.nextElementSibling,metadata.nextElementSibling.nextElementSibling]);
const tags = document.createElement('section'); tags.innerHTML = '<div id="cc-tag-chips"></div><button type="button" id="cc-tag-add" class="btn btn-sm btn-outline-primary">Tilføj tag</button><div id="cc-tag-suggestions" class="cc-tag-suggestions"></div>'; state.panels.hardware.before(tags); panel('tags', 'Tags', [tags]);
$('cc-tag-add').onclick = () => window.tagPicker.showSelection(addTag);
window.setTagPickerContext?.('case', null, addTag);
$('cc-tag-chips').onclick = e => { const chip = e.target.closest('[data-tag-id]'); if (chip && e.target.closest('button')) { state.tags = state.tags.filter(t => t.id !== Number(chip.dataset.tagId)); renderTags(); scheduleSave(); } };
$('cc-tag-suggestions').onclick = e => { const button = e.target.closest('[data-suggested-tag]'); const tag = state.tagCatalog.find(item => item.id === Number(button?.dataset.suggestedTag)); if (tag) addTag(tag).catch(error => showCreateError(error.message)); };
const chips = document.createElement('div'); chips.id = 'cc-hardware-chips'; $('hardwareList').before(chips);
chips.onclick = e => { const b = e.target.closest('[data-remove-hardware]'); if (b) { state.hardware = state.hardware.filter(h => h.id !== Number(b.dataset.removeHardware)); renderSelectedHardware(); scheduleSave(); } };
const originalRenderHardware = window.renderHardwareList;
window.renderHardwareList = items => { originalRenderHardware(items); Array.from($('hardwareList').children).forEach((row,i) => { if (!items[i]) return; const item = items[i]; const input = document.createElement('input'); input.type = 'checkbox'; input.className = 'form-check-input me-2'; input.dataset.hardwareChoice = item.id; input.setAttribute('aria-label', `Vælg ${item.model || item.brand || item.id}`); input.onchange = () => { state.hardware = state.hardware.filter(h => h.id !== item.id); if (input.checked) state.hardware.push({id:item.id,name:item.model || item.brand || `Hardware #${item.id}`}); renderSelectedHardware(); scheduleSave(); }; row.prepend(input); }); renderSelectedHardware(); };
window.quickCreateHardware = async () => {
const name = $('hardwareNameInput').value.trim(), customerId = selectedCustomer?.id || getSingleContactCompanyId();
if (!name || !customerId) { showCreateError('Udfyld hardwarenavn og vælg et firma'); return; }
const anydeskId = $('hardwareAnyDeskIdInput').value.trim();
const button = $('hardwareNameInput').closest('.card-body').querySelector('button'); button.disabled = true;
try { const item = await api('/api/v1/hardware/quick', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,customer_id:customerId,anydesk_id:anydeskId || null,anydesk_link:anydeskId ? `anydesk://${anydeskId}` : null})}); state.hardware.push({id:item.id,name}); $('hardwareNameInput').value = ''; $('hardwareAnyDeskIdInput').value = ''; renderSelectedHardware(); scheduleSave(); } catch(e) { showCreateError(e.message); } finally { button.disabled = false; }
};
const originalRender = window.renderSelections;
window.renderSelections = () => { originalRender(); if (state.ready) { scheduleSave(); scheduleDuplicates(); contactOpenCases(); } };
const contactCases = document.createElement('aside'); contactCases.id = 'cc-contact-cases'; contactCases.className = 'cc-contact-cases'; contactCases.hidden = true; $('selectedContacts').after(contactCases);
const workloadEl = document.createElement('div'); workloadEl.id = 'cc-workload'; workloadEl.className = 'cc-workload'; state.panels.metadata.after(workloadEl);
const duplicateEl = document.createElement('div'); duplicateEl.id = 'cc-duplicates'; $('titel').closest('.row').after(duplicateEl);
const toolbar = document.createElement('div'); toolbar.className = 'mb-3'; toolbar.innerHTML = '<label class="form-label" for="cc-template">Hurtigskabelon</label><select id="cc-template" class="form-select"><option value="">Start uden skabelon</option></select><small id="cc-template-error" class="text-muted"></small>'; form.prepend(toolbar);
try { templates = await api('/api/v1/case-create/templates'); $('cc-template').insertAdjacentHTML('beforeend',templates.map(t => `<option value="${t.id}">${esc(t.name)}</option>`).join('')); } catch { $('cc-template-error').textContent = 'Skabeloner er ikke tilgængelige.'; }
$('cc-template').onchange = e => applyTemplate(e.target.value).catch(err => showCreateError(err.message));
const actions = $('submitBtn').parentElement; actions.classList.add('cc-actions'); actions.insertAdjacentHTML('afterbegin','<small id="cc-save" role="status" class="me-auto align-self-center">Klargør kladde…</small>');
toolbar.classList.add('cc-template-toolbar');
buildTypes(); updateCaseTypeSections();
form.addEventListener('input', scheduleSave); form.addEventListener('change', scheduleSave); form.addEventListener('click', () => setTimeout(scheduleSave,0));
$('titel').addEventListener('input', () => { scheduleDuplicates(); scheduleTagSuggestions(); });
$('beskrivelse').addEventListener('input', scheduleTagSuggestions); $('ansvarlig_bruger_id').addEventListener('change', workload);
document.addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && !document.querySelector('.modal.show')) { e.preventDefault(); if (!$('submitBtn').disabled) form.requestSubmit(); } });
window.addEventListener('pagehide', save);
try {
const user = await api('/api/v1/auth/me');
if (!(user.user_id || user.id)) throw new Error('Mangler bruger');
const params = new URLSearchParams(location.search); params.sort();
state.key = `bmc:case-draft:v1:${user.user_id || user.id}:${params.toString() || 'new'}`;
const raw = localStorage.getItem(state.key); let draft;
try { draft = raw ? JSON.parse(raw) : null; } catch { localStorage.removeItem(state.key); }
if (draft && (draft.version !== 1 || !Number.isFinite(draft.savedAt) || !draft.fields || !Array.isArray(draft.orders) || Date.now()-draft.savedAt > 7*86400000)) { localStorage.removeItem(state.key); draft=null; }
if (draft) {
state.pending = true; const prompt = document.createElement('div'); prompt.id = 'cc-draft'; prompt.className = 'alert alert-info'; prompt.innerHTML = 'Der findes en lokal kladde. <button type="button" class="btn btn-sm btn-primary" id="cc-restore">Gendan kladde</button> <button type="button" class="btn btn-sm btn-outline-secondary" id="cc-discard">Kassér</button>'; form.prepend(prompt);
$('cc-restore').onclick = () => restore(draft).catch(e => showCreateError(e.message));
$('cc-discard').onclick = () => { localStorage.removeItem(state.key); state.pending=false; prompt.remove(); scheduleSave(); };
}
$('cc-save').textContent = draft ? 'Vælg om kladden skal gendannes' : 'Kladde gemmes i denne browser';
} catch { $('cc-save').textContent = 'Autosave er ikke tilgængelig'; }
state.tagCatalog = await api('/api/v1/tags?is_active=true').catch(() => []);
state.ready = true; summaries(); renderTagSuggestions(); workload(); scheduleDuplicates(); contactOpenCases(); await loadHardwareForContacts();
}
window.caseCreateUI = {init,typeChanged,contactsChanged:contactOpenCases,validate,openMessage:() => { const contacts=Object.values(selectedContacts); window.openInternalMessage({contact:contacts.length===1?contacts[0]:null}); },relations:() => ({tag_ids:state.tags.map(t=>t.id),hardware_ids:state.hardware.map(h=>h.id)}),created: result => {
state.completed = true; clearTimeout(saveTimer);
try { if (state.key) localStorage.removeItem(state.key); if (result.tag_actions?.length) sessionStorage.setItem(`bmc:case-tag-actions:${result.id}`, JSON.stringify(result.tag_actions)); } catch { /* Case is already saved. */ }
}};
})();