(function (root) { 'use strict'; const labels = {onboarding: 'Onboarding', offboarding: 'Offboarding'}; const statuses = {draft: 'Dato gemt · automatik slået fra', scheduled: 'Planlagt', needs_parent: 'Vælg hovedsag', created: 'Sag oprettet', manual_review: 'Kræver manuel behandling', cancelled: 'Annulleret', error: 'Oprettelse fejlede'}; const escape = value => String(value ?? '').replace(/[&<>"']/g, char => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char])); const date = value => value ? new Intl.DateTimeFormat('da-DK', {timeZone:'Europe/Copenhagen', dateStyle:'medium'}).format(new Date(value.length === 10 ? value + 'T12:00:00Z' : value)) : 'Ikke planlagt'; const schedule = value => value ? new Intl.DateTimeFormat('da-DK', {timeZone:'Europe/Copenhagen', dateStyle:'medium', timeStyle:'short'}).format(new Date(value)) : ''; function templateOptions(templates, customerId, type) { return templates.filter(item => (['global','internal'].includes(item.template_type) || !item.customer_id || Number(item.customer_id) === Number(customerId)) && (!item.category || item.category === type)); } function editPayload(payload, created) { if (!created) return payload; return {event_date:payload.event_date,lead_days:payload.lead_days,auto_create:payload.auto_create}; } root.ContactLifecycle = {escape, templateOptions, editPayload}; if (typeof document === 'undefined') return; document.addEventListener('DOMContentLoaded', () => { const cards = document.getElementById('contactLifecycleCards'); if (!cards) return; const contactId = Number(location.pathname.split('/').filter(Boolean).pop()); const base = `/api/v1/contacts/${contactId}/lifecycle`; let data = {items:[],companies:[],users:[],templates:[]}; let current = null; let previewVersion = 0; const feedback = document.getElementById('contactLifecycleFeedback'); async function request(url, method = 'GET', body) { const headers = {'Content-Type':'application/json'}; const token = localStorage.getItem('access_token'); if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(url, {method, headers, credentials:'same-origin', ...(body === undefined ? {} : {body:JSON.stringify(body)})}); const result = await response.json(); if (!response.ok) throw new Error(typeof result.detail === 'string' ? result.detail : 'Kontrollér oplysningerne, og prøv igen.'); return result; } function notify(message, danger = false) { feedback.className = `alert ${danger ? 'alert-danger' : 'alert-success'} py-2`; feedback.textContent = message; } async function load() { try { data = await request(base); cards.innerHTML = Object.entries(labels).map(([type,label]) => { const items = data.items.filter(item => item.event_type === type); const live = items.filter(item => item.status !== 'cancelled'); return `
${label}
${live.length ? live.map(item => `
${escape(date(item.event_date))}
${escape((data.companies.find(company => Number(company.id) === Number(item.customer_id)) || {}).name || '')}
${escape(statuses[item.status] || item.status)}
${item.auto_create && item.scheduled_at && !item.case_id ? `
Oprettelse: ${escape(schedule(item.scheduled_at))}
` : ''}${item.last_error ? `
${escape(item.last_error)}
` : ''}
${item.case_id ? `Åbn sag #${Number(item.case_id)}` : ''}
`).join('') : '
Ingen dato planlagt
'}${items.some(item => item.status === 'cancelled') ? `
${items.filter(item => item.status === 'cancelled').length} annulleret · eksisterende sager er bevaret
` : ''}
`; }).join(''); if (data.can_edit === false) cards.querySelectorAll('[data-lifecycle-new],[data-lifecycle-edit],[data-lifecycle-cancel]').forEach(button => button.remove()); } catch (error) { cards.innerHTML = '
'; notify(`Forløbene kunne ikke hentes: ${error.message}`, true); } } cards.addEventListener('click', async event => { const button = event.target.closest('button'); if (!button) return; if (button.hasAttribute('data-lifecycle-reload')) return load(); if (button.dataset.lifecycleNew) return open(button.dataset.lifecycleNew); if (button.dataset.lifecycleEdit) { const item = data.items.find(item => Number(item.id) === Number(button.dataset.lifecycleEdit)); if (item) open(item.event_type, item); } if (button.dataset.lifecycleCancel) { const item = data.items.find(item => Number(item.id) === Number(button.dataset.lifecycleCancel)); if (!item || !confirm(`Annuller ${labels[item.event_type].toLowerCase()} den ${date(item.event_date)}? Eksisterende sager bevares.`)) return; button.disabled = true; try { await request(`${base}/${item.id}/cancel`, 'POST'); await load(); notify('Forløbet er annulleret. Eksisterende sager er bevaret.'); } catch (error) { notify(error.message, true); button.disabled = false; } } }); function open(type, item = null) { if (data.can_edit === false) return; current = item; previewVersion++; document.getElementById('contactLifecycleModal')?.remove(); const element = document.createElement('div'); element.id = 'contactLifecycleModal'; element.className = 'modal fade'; element.tabIndex = -1; element.setAttribute('aria-labelledby','contactLifecycleModalTitle'); element.innerHTML = ``; document.body.append(element); const form = element.querySelector('form'); const field = name => form.elements.namedItem(name); field('event_date').value = item?.event_date || ''; field('customer_id').value = item?.customer_id || (data.companies.length === 1 ? data.companies[0].id : ''); field('lead_days').value = item?.lead_days ?? 14; field('auto_create').checked = Boolean(item?.auto_create); field('responsible_user_id').value = item?.responsible_user_id || ''; if (item?.case_id) ['customer_id','template_id','parent_case_id','responsible_user_id'].forEach(name => { field(name).disabled = true; }); let templateId = item?.template_id || ''; function templates() { const available = templateOptions(data.templates, field('customer_id').value, type); field('template_id').innerHTML = '' + available.map(template => ``).join(''); field('template_id').value = available.some(template => Number(template.id) === Number(templateId)) ? templateId : ''; } async function preview() { const version = ++previewVersion; const container = element.querySelector('#lifecyclePreview'); element.querySelector('#lifecycleAutoFields').classList.toggle('d-none', !field('auto_create').checked); const customer = field('customer_id').value; const eventDate = field('event_date').value; const days = Number(field('lead_days').value); if (!customer || !eventDate || !Number.isInteger(days) || days < 0) { container.textContent = 'Vælg dato og kunde.'; return; } if (current?.case_id) { field('parent_case_id').innerHTML = ``; element.querySelector('#lifecycleParentHelp').textContent = 'Sagens tilknytninger bevares.'; container.textContent = `Sag #${current.case_id} bevares. Deadline ændres til ${date(eventDate)}. Afsluttede sager kræver manuel behandling.`; return; } const selectedParent = field('parent_case_id').value || current?.parent_case_id; container.textContent = 'Henter forslag…'; try { const query = new URLSearchParams({customer_id:customer,event_type:type,event_date:eventDate,lead_days:String(days)}); const result = await request(`${base}/preview?${query}`); if (version !== previewVersion || !element.isConnected) return; const candidates = result.candidates || []; field('parent_case_id').innerHTML = `` + candidates.map(candidate => ``).join(''); field('parent_case_id').value = candidates.some(candidate => Number(candidate.id) === Number(selectedParent)) ? selectedParent : (result.parent_case_id || ''); element.querySelector('#lifecycleParentHelp').textContent = candidates.length > 1 ? 'Flere relevante sager: vælg hovedsag. Uden valg afventer automatikken.' : candidates.length === 1 ? 'Der oprettes en undersag til denne åbne sag.' : 'Ingen relevant åben sag. Der oprettes en selvstændig sag.'; container.textContent = current?.case_id ? `Sag #${current.case_id} bevares. Deadline ændres til ${date(eventDate)}. Afsluttede sager kræver manuel behandling.` : field('auto_create').checked ? `Sagen oprettes ${schedule(result.scheduled_at)} med deadline ${date(eventDate)}.${candidates.length > 1 && !field('parent_case_id').value ? ' Afventer valg af hovedsag.' : ''}` : `Datoen ${date(eventDate)} gemmes. Ingen automatisk sagsoprettelse.`; } catch (error) { if (version === previewVersion) container.textContent = `Forslag kunne ikke hentes: ${error.message}`; } } templates(); form.addEventListener('change', event => { if (event.target.name === 'customer_id') templates(); if (event.target.name === 'template_id') templateId = event.target.value; if (!['template_id','responsible_user_id'].includes(event.target.name)) preview(); }); form.addEventListener('click', event => { const button = event.target.closest('[data-lead]'); if (button) { field('lead_days').value = button.dataset.lead; preview(); } }); form.addEventListener('submit', async event => { event.preventDefault(); if (!form.reportValidity()) return; const button = element.querySelector('#lifecycleSave'); const errorBox = element.querySelector('#lifecycleError'); button.disabled = true; errorBox.classList.add('d-none'); const payload = {customer_id:Number(field('customer_id').value),event_date:field('event_date').value,lead_days:Number(field('lead_days').value),auto_create:field('auto_create').checked,template_id:Number(field('template_id').value)||null,responsible_user_id:Number(field('responsible_user_id').value)||null,parent_case_id:Number(field('parent_case_id').value)||null}; if (!current) payload.event_type = type; try { const saved = await request(current ? `${base}/${current.id}` : base, current ? 'PATCH' : 'POST', editPayload(payload, Boolean(current?.case_id))); modal.hide(); await load(); notify(saved.status === 'needs_parent' ? 'Forløbet er gemt. Vælg en hovedsag for at aktivere sagsoprettelsen.' : 'Forløbet er gemt.'); } catch (error) { errorBox.textContent = error.message; errorBox.classList.remove('d-none'); button.disabled = false; } }); const modal = new bootstrap.Modal(element); element.addEventListener('shown.bs.modal', () => field('event_date').focus(), {once:true}); element.addEventListener('hidden.bs.modal', () => { previewVersion++; modal.dispose(); element.remove(); }, {once:true}); modal.show(); preview(); } load(); }); })(typeof window === 'undefined' ? globalThis : window);