const caseId = {{ case.id }}; const wikiCustomerId = {{ customer.id if customer else 'null' }}; const wikiDefaultTag = "guide"; let contactSearchTimeout; let customerSearchTimeout; let relationSearchTimeout; let wikiSearchTimeout; let selectedRelationCaseId = null; const caseTypeKey = "{{ (case.template_key or case.type or 'ticket')|lower }}"; function forceCaseTabActivation(tabId) { if (!tabId) return; const tabContent = document.getElementById('caseTabsContent'); const targetPane = document.getElementById(tabId); if (!tabContent || !targetPane) return; tabContent.querySelectorAll(':scope > .tab-pane').forEach((pane) => { pane.classList.remove('show', 'active'); pane.style.display = 'none'; }); targetPane.classList.add('show', 'active'); targetPane.style.display = 'block'; const tabButtons = document.querySelectorAll('#caseTabs [data-bs-target]'); tabButtons.forEach((btn) => { btn.classList.toggle('active', btn.getAttribute('data-bs-target') === `#${tabId}`); }); } window.moduleDisplayNames = { 'relations': 'Relationer', 'call-history': 'Opkaldshistorik', 'files': 'Filer', 'emails': 'E-mails', 'pipeline': 'Salgspipeline', 'hardware': 'Hardware', 'locations': 'Lokationer', 'contacts': 'Kontakter', 'customers': 'Kunder', 'tags': 'Tags', 'wiki': 'Wiki', 'todo-steps': 'Todo-opgaver', 'time': 'Tid', 'timetracking': 'Tidsforbrug', 'solution': 'Løsning', 'sales': 'Varekøb & salg', 'subscription': 'Abonnement', 'reminders': 'Påmindelser', 'calendar': 'Kalender' }; let caseTypeModuleDefaults = {}; // Modal instances let contactSearchModal, customerSearchModal, relationModal, contactInfoModal, createRelatedCaseModalInstance; let currentContactInfo = null; // Initialize everything when DOM is ready document.addEventListener('DOMContentLoaded', () => { hydrateTopbarStatusOptions(); // Initialize modals contactSearchModal = new bootstrap.Modal(document.getElementById('contactSearchModal')); customerSearchModal = new bootstrap.Modal(document.getElementById('customerSearchModal')); relationModal = new bootstrap.Modal(document.getElementById('relationModal')); contactInfoModal = new bootstrap.Modal(document.getElementById('contactInfoModal')); createRelatedCaseModalInstance = new bootstrap.Modal(document.getElementById('createRelatedCaseModal')); // Setup search handlers setupContactSearch(); setupCustomerSearch(); setupRelationSearch(); updateRelationTypeHint(); updateNewCaseRelationTypeHint(); // Initialize all tooltips on the page document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { bootstrap.Tooltip.getOrCreateInstance(el, { html: true, container: 'body' }); }); Promise.all([loadModulePrefs(), loadCaseTypeModuleDefaultsSetting()]).then(() => applyViewFromTags()); // Set default context for keyboard shortcuts (Option+Shift+T) if (window.setTagPickerContext) { window.setTagPickerContext('case', {{ case.id }}, () => syncCaseTagsUi()); } // Load Hardware & Locations loadCaseHardware(); loadCaseLocations(); loadCaseWiki(); loadTodoSteps(); loadCaseTagsModule(); loadCaseTagSuggestions(); // Keep suggestions fresh while user works on the case. setInterval(loadCaseTagSuggestions, 30000); const wikiSearchInput = document.getElementById('wikiSearchInput'); if (wikiSearchInput) { wikiSearchInput.addEventListener('input', () => { clearTimeout(wikiSearchTimeout); wikiSearchTimeout = setTimeout(() => { loadCaseWiki(wikiSearchInput.value || ''); }, 300); }); } const todoForm = document.getElementById('todoStepForm'); if (todoForm) { todoForm.addEventListener('submit', createTodoStep); } const caseTabs = document.getElementById('caseTabs'); if (caseTabs) { caseTabs.addEventListener('shown.bs.tab', async (event) => { const targetSelector = event?.target?.getAttribute('data-bs-target') || ''; const tabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : targetSelector; forceCaseTabActivation(tabId); try { if (tabId === 'sales' && typeof loadVarekobSalg === 'function') { await loadVarekobSalg(); } else if (tabId === 'timetracking' && typeof loadTimeTrackingTab === 'function') { await loadTimeTrackingTab(); } else if (tabId === 'subscription' && typeof loadSubscriptionForCase === 'function') { await loadSubscriptionForCase(); } else if (tabId === 'reminders') { if (typeof loadReminders === 'function') await loadReminders(); if (typeof loadCaseCalendar === 'function') await loadCaseCalendar(); } } catch (tabLoadError) { console.error('Tab data reload failed:', tabLoadError); } }); caseTabs.addEventListener('click', (event) => { const btn = event.target.closest('[data-bs-target]'); if (!btn) return; const targetSelector = btn.getAttribute('data-bs-target') || ''; const tabId = targetSelector.startsWith('#') ? targetSelector.slice(1) : targetSelector; if (tabId) { setTimeout(() => forceCaseTabActivation(tabId), 0); } }); } forceCaseTabActivation('details'); // Focus on title when create modal opens const createModalEl = document.getElementById('createRelatedCaseModal'); if (createModalEl) { createModalEl.addEventListener('shown.bs.modal', function () { document.getElementById('newCaseTitle').focus(); }); } }); // Show modal functions function showContactSearch() { contactSearchModal.show(); setTimeout(() => document.getElementById('contactSearch').focus(), 300); } function showCustomerSearch() { customerSearchModal.show(); setTimeout(() => document.getElementById('customerSearch').focus(), 300); } function showRelationModal() { relationModal.show(); updateRelationTypeHint(); setTimeout(() => document.getElementById('relationCaseSearch').focus(), 300); } function showContactInfoModal(el) { currentContactInfo = { id: el.dataset.contactId, name: el.dataset.name || '-', title: el.dataset.title || '-', company: el.dataset.company || '-', email: el.dataset.email || '-', phone: el.dataset.phone || '-', mobile: el.dataset.mobile || '-', role: el.dataset.role || '-', isPrimary: el.dataset.isPrimary === 'true' }; document.getElementById('contactInfoName').textContent = currentContactInfo.name; document.getElementById('contactInfoTitle').textContent = currentContactInfo.title || '-'; document.getElementById('contactInfoCompany').textContent = currentContactInfo.company || '-'; document.getElementById('contactInfoEmail').textContent = currentContactInfo.email || '-'; document.getElementById('contactInfoPhone').innerHTML = renderCasePhone(currentContactInfo.phone); document.getElementById('contactInfoMobile').innerHTML = renderCaseMobile(currentContactInfo.mobile, currentContactInfo.name); document.getElementById('contactInfoRole').textContent = currentContactInfo.role || '-'; const primaryBadge = document.getElementById('contactInfoPrimary'); if (currentContactInfo.isPrimary) { primaryBadge.classList.remove('d-none'); } else { primaryBadge.classList.add('d-none'); } contactInfoModal.show(); } function renderCasePhone(number) { const clean = String(number || '').trim(); if (!clean || clean === '-') return '-'; return `${escapeHtml(clean)}`; } function renderCaseMobile(number, name) { const clean = String(number || '').trim(); if (!clean || clean === '-') return '-'; return `
${escapeHtml(clean)}
`; } function openContactRoleFromInfo() { if (!currentContactInfo) return; contactInfoModal.hide(); openContactRoleModal( currentContactInfo.id, currentContactInfo.name, currentContactInfo.role || 'Kontakt', currentContactInfo.isPrimary ); } function showCreateRelatedModal() { createRelatedCaseModalInstance.show(); updateNewCaseRelationTypeHint(); } function relationTypeMeaning(type) { const map = { 'Relateret til': { icon: '🔗', text: 'Sagerne hænger fagligt sammen, men ingen af dem er direkte afhængig af den anden.' }, 'Afledt af': { icon: '↪', text: 'Denne sag er opstået på baggrund af den anden sag (den anden er ophav/forløber).' }, 'Årsag til': { icon: '➡', text: 'Denne sag er årsag til den anden sag (du peger frem mod en konsekvens/opfølgning).' }, 'Blokkerer': { icon: '⛔', text: 'Arbejdet i denne sag stopper fremdrift i den anden sag, indtil blokeringen er løst.' } }; return map[type] || null; } function updateRelationTypeHint() { const select = document.getElementById('relationTypeSelect'); const hint = document.getElementById('relationTypeHint'); if (!select || !hint) return; const meaning = relationTypeMeaning(select.value); if (!meaning) { hint.style.display = 'none'; hint.innerHTML = ''; return; } hint.style.display = 'block'; hint.innerHTML = `${meaning.icon} Betydning: ${meaning.text}`; } function updateNewCaseRelationTypeHint() { const select = document.getElementById('newCaseRelationType'); const hint = document.getElementById('newCaseRelationTypeHint'); if (!select || !hint) return; const selected = select.value; if (selected === 'Afledt af') { hint.innerHTML = '↪ Effekt: Nuværende sag markeres som afledt af den nye sag.'; return; } if (selected === 'Årsag til') { hint.innerHTML = '➡ Effekt: Nuværende sag markeres som årsag til den nye sag.'; return; } if (selected === 'Blokkerer') { hint.innerHTML = '⛔ Effekt: Nuværende sag markeres som blokering for den nye sag.'; return; } hint.innerHTML = '🔗 Effekt: Sagerne kobles fagligt uden direkte afhængighed.'; } async function createRelatedCase() { const title = document.getElementById('newCaseTitle').value; const relationType = document.getElementById('newCaseRelationType').value; const description = document.getElementById('newCaseDescription').value; if (!title) { alert('Titel er påkrævet'); return; } // 1. Create the new case try { const caseResponse = await fetch('/api/v1/sag', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ titel: title, beskrivelse: description, customer_id: {{ case.customer_id }}, status: 'åben' }) }); if (!caseResponse.ok) throw new Error('Kunne ikke oprette sag'); const newCase = await caseResponse.json(); // 2. Create the relation const relationResponse = await fetch(`/api/v1/sag/${caseId}/relationer`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ målsag_id: newCase.id, relationstype: relationType }) }); if (!relationResponse.ok) throw new Error('Kunne ikke oprette relation'); // 3. Reload to show new relation window.location.reload(); } catch (err) { console.error('Error creating related case:', err); alert('Der opstod en fejl: ' + err.message); } } function confirmDeleteCase() { if(confirm('Slet denne sag?')) { fetch('/api/v1/sag/{{ case.id }}', {method: 'DELETE'}) .then(() => window.location='/sag'); } } // Contact Search function setupContactSearch() { const contactSearchInput = document.getElementById('contactSearch'); contactSearchInput.addEventListener('input', function(e) { clearTimeout(contactSearchTimeout); const query = e.target.value.trim(); if (query.length < 2) { document.getElementById('contactSearchResults').innerHTML = ''; return; } contactSearchTimeout = setTimeout(async () => { try { const response = await fetch(`/api/v1/search/contacts?q=${encodeURIComponent(query)}`); const contacts = await response.json(); const resultsDiv = document.getElementById('contactSearchResults'); if (contacts.length === 0) { resultsDiv.innerHTML = '
Ingen kontakter fundet
'; } else { resultsDiv.innerHTML = contacts.map(c => `
${c.first_name} ${c.last_name}
${c.email || ''} ${c.user_company ? '(' + c.user_company + ')' : ''}
`).join(''); } } catch (err) { console.error('Error searching contacts:', err); } }, 300); }); } async function addContact(caseId, contactId, contactName) { try { const response = await fetch(`/api/v1/sag/${caseId}/contacts`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({contact_id: contactId, role: 'Kontakt'}) }); if (response.ok) { contactSearchModal.hide(); window.location.reload(); } else { const error = await response.json(); alert(`Fejl: ${error.detail}`); } } catch (err) { alert('Fejl ved tilføjelse af kontakt: ' + err.message); } } async function removeContact(caseId, contactId) { if (confirm('Fjern denne kontakt fra sagen?')) { const response = await fetch(`/api/v1/sag/${caseId}/contacts/${contactId}`, {method: 'DELETE'}); if (response.ok) { window.location.reload(); } else { alert('Fejl ved fjernelse af kontakt'); } } } // Customer Search function setupCustomerSearch() { const customerSearchInput = document.getElementById('customerSearch'); customerSearchInput.addEventListener('input', function(e) { clearTimeout(customerSearchTimeout); const query = e.target.value.trim(); if (query.length < 2) { document.getElementById('customerSearchResults').innerHTML = ''; return; } customerSearchTimeout = setTimeout(async () => { try { const response = await fetch(`/api/v1/search/customers?q=${encodeURIComponent(query)}`); const customers = await response.json(); const resultsDiv = document.getElementById('customerSearchResults'); if (customers.length === 0) { resultsDiv.innerHTML = '
Ingen kunder fundet
'; } else { resultsDiv.innerHTML = customers.map(c => `
${c.name}
${c.email || ''} ${c.cvr_number ? '(CVR: ' + c.cvr_number + ')' : ''}
`).join(''); } } catch (err) { console.error('Error searching customers:', err); } }, 300); }); } async function addCustomer(caseId, customerId, customerName) { try { const response = await fetch(`/api/v1/sag/${caseId}/customers`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({customer_id: customerId, role: 'Kunde'}) }); if (response.ok) { customerSearchModal.hide(); window.location.reload(); } else { const error = await response.json(); alert(`Fejl: ${error.detail}`); } } catch (err) { alert('Fejl ved tilføjelse af kunde: ' + err.message); } } async function removeCustomer(caseId, customerId) { if (confirm('Fjern denne kunde fra sagen?')) { const response = await fetch(`/api/v1/sag/${caseId}/customers/${customerId}`, {method: 'DELETE'}); if (response.ok) { window.location.reload(); } else { alert('Fejl ved fjernelse af kunde'); } } } // Relation Search - Enhanced version let currentFocusIndex = -1; let searchResults = []; function setupRelationSearch() { const relationSearchInput = document.getElementById('relationCaseSearch'); // Input handler relationSearchInput.addEventListener('input', function(e) { clearTimeout(relationSearchTimeout); const query = e.target.value.trim(); currentFocusIndex = -1; if (query.length < 2) { document.getElementById('relationSearchResults').innerHTML = ''; document.getElementById('relationSearchResults').style.display = 'none'; return; } relationSearchTimeout = setTimeout(async () => { try { const response = await fetch(`/api/v1/search/sag?q=${encodeURIComponent(query)}`); const cases = await response.json(); searchResults = cases.filter(c => c.id !== caseId); renderRelationSearchResults(searchResults); } catch (err) { console.error('Error searching cases:', err); } }, 200); }); // Keyboard navigation relationSearchInput.addEventListener('keydown', function(e) { const resultsDiv = document.getElementById('relationSearchResults'); const items = resultsDiv.querySelectorAll('.relation-search-item'); if (e.key === 'ArrowDown') { e.preventDefault(); currentFocusIndex = (currentFocusIndex + 1) % items.length; updateFocusedItem(items); } else if (e.key === 'ArrowUp') { e.preventDefault(); currentFocusIndex = currentFocusIndex <= 0 ? items.length - 1 : currentFocusIndex - 1; updateFocusedItem(items); } else if (e.key === 'Enter') { e.preventDefault(); if (currentFocusIndex >= 0 && currentFocusIndex < items.length) { items[currentFocusIndex].click(); } } }); } function updateFocusedItem(items) { items.forEach((item, index) => { if (index === currentFocusIndex) { item.classList.add('active'); item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } else { item.classList.remove('active'); } }); } function renderRelationSearchResults(cases) { const resultsDiv = document.getElementById('relationSearchResults'); if (cases.length === 0) { resultsDiv.innerHTML = '
Ingen sager fundet
'; resultsDiv.style.display = 'block'; return; } // Group by status const grouped = {}; cases.forEach(c => { const status = c.status || 'ukendt'; if (!grouped[status]) grouped[status] = []; grouped[status].push(c); }); let html = '
'; // Sort status groups: åben first, then others const statusOrder = ['åben', 'under behandling', 'afventer', 'løst', 'lukket']; const sortedStatuses = Object.keys(grouped).sort((a, b) => { const aIndex = statusOrder.indexOf(a); const bIndex = statusOrder.indexOf(b); if (aIndex === -1 && bIndex === -1) return a.localeCompare(b); if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; }); sortedStatuses.forEach(status => { const statusCases = grouped[status]; // Status group header html += `
${status} ${statusCases.length}
`; statusCases.forEach(c => { const createdDate = c.created_at ? new Date(c.created_at).toLocaleDateString('da-DK') : 'N/A'; const beskrivelse = c.beskrivelse ? c.beskrivelse.substring(0, 80) + '...' : ''; const customerName = c.customer_name || ''; const safeTitle = (c.titel || '').replace(/"/g, '"').replace(/'/g, '''); const safeCustomer = customerName.replace(/"/g, '"').replace(/'/g, '''); html += `
#${c.id} ${escapeHtml(c.titel)}
${c.customer_name ? `
${escapeHtml(c.customer_name)}
` : ''} ${beskrivelse ? `
${escapeHtml(beskrivelse)}
` : ''}
${createdDate}
`; }); }); html += '
'; resultsDiv.innerHTML = html; resultsDiv.style.display = 'block'; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function selectRelationCase(caseIdValue, caseTitel, customerName, status) { selectedRelationCaseId = caseIdValue; // Update preview const previewDiv = document.getElementById('selectedCasePreview'); const titleDiv = document.getElementById('selectedCaseTitle'); titleDiv.innerHTML = `
#${caseIdValue} ${escapeHtml(caseTitel)} ${status}
${customerName ? `
${escapeHtml(customerName)}
` : ''} `; previewDiv.style.display = 'block'; document.getElementById('relationSearchResults').innerHTML = ''; document.getElementById('relationSearchResults').style.display = 'none'; document.getElementById('relationCaseSearch').value = ''; // Enable add button updateAddRelationButton(); } function clearSelectedRelationCase() { selectedRelationCaseId = null; document.getElementById('selectedCasePreview').style.display = 'none'; document.getElementById('relationCaseSearch').value = ''; document.getElementById('relationCaseSearch').focus(); updateAddRelationButton(); } function updateAddRelationButton() { const btn = document.getElementById('addRelationBtn'); const relationType = document.getElementById('relationTypeSelect').value; btn.disabled = !selectedRelationCaseId || !relationType; } async function addRelation() { const relationType = document.getElementById('relationTypeSelect').value; const btn = document.getElementById('addRelationBtn'); if (!selectedRelationCaseId) { alert('Vælg en sag først'); return; } if (!relationType) { alert('Vælg en relationstype'); return; } // Disable button during request btn.disabled = true; btn.innerHTML = 'Tilføjer...'; try { const response = await fetch(`/api/v1/sag/${caseId}/relationer`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ målsag_id: selectedRelationCaseId, relationstype: relationType }) }); if (response.ok) { selectedRelationCaseId = null; relationModal.hide(); window.location.reload(); } else { const error = await response.json(); alert(`Fejl: ${error.detail}`); btn.disabled = false; btn.innerHTML = ' Tilføj relation'; } } catch (err) { alert('Fejl ved tilføjelse af relation: ' + err.message); btn.disabled = false; btn.innerHTML = ' Tilføj relation'; } } async function deleteRelation(relationId) { if (confirm('Fjern denne relation?')) { const response = await fetch(`/api/v1/sag/${caseId}/relationer/${relationId}`, {method: 'DELETE'}); if (response.ok) { window.location.reload(); } else { alert('Fejl ved fjernelse af relation'); } } } // ============ Hardware Handling ============ async function loadCaseHardware() { try { const res = await fetch(`/api/v1/sag/{{ case.id }}/hardware`); if (!res.ok) { let message = 'Kunne ikke hente hardware.'; try { const err = await res.json(); if (err?.detail) { message = err.detail; } } catch (_) { } throw new Error(message); } const hardware = await res.json(); if (!Array.isArray(hardware)) { throw new Error('Uventet svar fra serveren ved hardware-hentning.'); } const container = document.getElementById('hardware-list'); if (hardware.length === 0) { container.innerHTML = '
Ingen hardware tilknyttet
'; setModuleContentState('hardware', false); return; } container.innerHTML = `
Enhed SN Slet
${hardware.map(h => `
${h.brand} ${h.model}
${h.serial_number || '-'}
`).join('')} `; setModuleContentState('hardware', true); } catch (e) { console.error("Error loading hardware:", e); const message = (e?.message || '').trim() || 'Fejl ved hentning'; document.getElementById('hardware-list').innerHTML = `
${escapeHtml(message)}
`; setModuleContentState('hardware', true); } } async function promptLinkHardware() { const id = prompt("Indtast Hardware ID:"); if (!id) return; try { const res = await fetch(`/api/v1/sag/{{ case.id }}/hardware`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ hardware_id: parseInt(id) }) }); if (!res.ok) throw await res.json(); loadCaseHardware(); } catch (e) { alert("Fejl: " + (e.detail || e.message)); } } async function unlinkHardware(hwId) { if(!confirm("Fjern link til dette hardware?")) return; try { await fetch(`/api/v1/sag/{{ case.id }}/hardware/${hwId}`, { method: 'DELETE' }); loadCaseHardware(); } catch (e) { alert("Fejl ved sletning"); } } // ============ Location Handling ============ async function loadCaseLocations() { try { const res = await fetch(`/api/v1/sag/{{ case.id }}/locations`); if (!res.ok) { let message = 'Kunne ikke hente lokationer.'; try { const err = await res.json(); if (err?.detail) { message = err.detail; } } catch (_) { } throw new Error(message); } const locations = await res.json(); if (!Array.isArray(locations)) { throw new Error('Uventet svar fra serveren ved lokations-hentning.'); } const container = document.getElementById('locations-list'); if (locations.length === 0) { container.innerHTML = '
Ingen lokationer tilknyttet
'; setModuleContentState('locations', false); return; } container.innerHTML = `
Navn Type Slet
${locations.map(l => `
${l.name}
${l.location_type || '-'}
`).join('')} `; setModuleContentState('locations', true); } catch (e) { console.error("Error loading locations:", e); const message = (e?.message || '').trim() || 'Fejl ved hentning'; document.getElementById('locations-list').innerHTML = `
${escapeHtml(message)}
`; setModuleContentState('locations', true); } } // ============ Wiki Handling ============ async function loadCaseWiki(searchValue = '') { const container = document.getElementById('wiki-list'); if (!container) return; if (!wikiCustomerId) { container.innerHTML = '
Ingen kunde tilknyttet
'; setModuleContentState('wiki', false); return; } container.innerHTML = '
Henter wiki...
'; const params = new URLSearchParams(); const trimmed = (searchValue || '').trim(); if (trimmed) { params.set('query', trimmed); } else { params.set('tag', wikiDefaultTag); } try { const res = await fetch(`/api/v1/wiki/customers/${wikiCustomerId}/pages?${params.toString()}`); if (!res.ok) { throw new Error('Kunne ikke hente Wiki'); } const payload = await res.json(); if (payload.errors && payload.errors.length) { container.innerHTML = '
Wiki API fejlede
'; setModuleContentState('wiki', true); return; } const pages = Array.isArray(payload.pages) ? payload.pages : []; if (!pages.length) { container.innerHTML = '
Ingen sider fundet
'; setModuleContentState('wiki', false); return; } container.innerHTML = pages.map(page => { const title = page.title || page.path || 'Wiki side'; const url = page.url || page.path || '#'; const safeUrl = url ? encodeURI(url) : '#'; return `
${escapeHtml(title)}
${escapeHtml(page.path || '')}
`; }).join(''); setModuleContentState('wiki', true); } catch (e) { console.error('Error loading Wiki:', e); container.innerHTML = '
Fejl ved hentning
'; setModuleContentState('wiki', true); } } async function loadCaseTagsModule() { const moduleContainer = document.getElementById('case-tags-module'); if (!moduleContainer) return; try { const response = await fetch(`/api/v1/tags/entity/case/${caseId}`); if (!response.ok) throw new Error('Kunne ikke hente tags'); const tags = await response.json(); if (!Array.isArray(tags) || tags.length === 0) { moduleContainer.innerHTML = '
Ingen tags paaa sagen endnu
'; setModuleContentState('tags', false); return; } moduleContainer.innerHTML = tags.map((tag) => ` ${tag.icon ? ` ` : ''}${escapeHtml(tag.name)} `).join(''); setModuleContentState('tags', true); } catch (error) { console.error('Error loading case tags module:', error); moduleContainer.innerHTML = '
Fejl ved hentning af tags
'; setModuleContentState('tags', true); } } async function loadCaseTagSuggestions() { const suggestionsContainer = document.getElementById('case-tag-suggestions'); if (!suggestionsContainer) return; try { const response = await fetch(`/api/v1/tags/entity/case/${caseId}/suggestions`); if (!response.ok) throw new Error('Kunne ikke hente forslag'); const suggestions = await response.json(); if (!Array.isArray(suggestions) || suggestions.length === 0) { suggestionsContainer.innerHTML = '
Ingen nye forslag lige nu
'; return; } suggestionsContainer.innerHTML = suggestions.slice(0, 8).map((item) => { const tag = item.tag || {}; const matched = Array.isArray(item.matched_words) ? item.matched_words.join(', ') : ''; return `
${tag.icon ? ` ` : ''}${escapeHtml(tag.name || 'Tag')} ${matched ? `
Match: ${escapeHtml(matched)}
` : ''}
`; }).join(''); } catch (error) { console.error('Error loading tag suggestions:', error); suggestionsContainer.innerHTML = '
Fejl ved forslag
'; } } async function applySuggestedCaseTag(tagId) { if (!tagId) return; try { const response = await fetch('/api/v1/tags/entity', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity_type: 'case', entity_id: caseId, tag_id: tagId }) }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || 'Kunne ikke tilfoeje tag'); } await syncCaseTagsUi(); if (typeof showNotification === 'function') { showNotification('Tag tilfoejet', 'success'); } } catch (error) { alert('Fejl: ' + error.message); } } async function removeCaseTagAndSync(tagId) { await window.removeEntityTag('case', caseId, tagId, 'case-tags-module'); await syncCaseTagsUi(); } async function syncCaseTagsUi() { if (window.renderEntityTags) { await window.renderEntityTags('case', caseId, 'case-tags'); } await loadCaseTagsModule(); await loadCaseTagSuggestions(); } let todoUserId = null; function getTodoUserId() { if (todoUserId) return todoUserId; const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token'); if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); todoUserId = payload.sub || payload.user_id; return todoUserId; } catch (e) { console.warn('Could not decode token for todo user_id'); } } const metaTag = document.querySelector('meta[name="user-id"]'); if (metaTag) { todoUserId = metaTag.getAttribute('content'); } return todoUserId; } function formatTodoDate(value) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '-'; return date.toLocaleDateString('da-DK'); } function formatTodoDateTime(value) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '-'; return date.toLocaleString('da-DK', { hour: '2-digit', minute: '2-digit', hour12: false }); } function getNextTodoOverrideStorageKey() { return `case:${caseId}:nextTodoStepId`; } function getNextTodoOverrideId() { const raw = localStorage.getItem(getNextTodoOverrideStorageKey()); const parsed = Number(raw); return Number.isInteger(parsed) && parsed > 0 ? parsed : null; } function setNextTodoOverrideId(stepIdOrNull) { const key = getNextTodoOverrideStorageKey(); if (stepIdOrNull === null || stepIdOrNull === undefined) { localStorage.removeItem(key); return; } localStorage.setItem(key, String(stepIdOrNull)); } function renderTodoSteps(steps) { const list = document.getElementById('todo-steps-list'); if (!list) return; updateTopbarNextTodo(steps || []); const escapeAttr = (value) => String(value ?? '') .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); if (!steps || steps.length === 0) { list.innerHTML = '
Ingen opgaver endnu
'; setModuleContentState('todo-steps', false); return; } const openSteps = steps.filter(step => !step.is_done); const doneSteps = steps.filter(step => step.is_done); const nextOverrideId = getNextTodoOverrideId(); const renderStep = (step) => { const createdBy = step.created_by_name || 'Ukendt'; const completedBy = step.completed_by_name || 'Ukendt'; const dueLabel = step.due_date ? formatTodoDate(step.due_date) : '-'; const createdLabel = formatTodoDateTime(step.created_at); const completedLabel = step.completed_at ? formatTodoDateTime(step.completed_at) : null; const isNextEffective = !step.is_done && (!!step.is_next || (nextOverrideId !== null && step.id === nextOverrideId)); const statusBadge = step.is_done ? 'Færdig' : `${isNextEffective ? 'Næste' : 'Åben'}`; const toggleLabel = step.is_done ? 'Genåbn' : 'Færdig'; const toggleClass = step.is_done ? 'btn-outline-secondary' : 'btn-outline-success'; const nextLabel = isNextEffective ? 'Fjern som næste' : 'Sæt som næste'; const nextClass = isNextEffective ? 'btn-primary' : 'btn-outline-primary'; const tooltipText = [ `Oprettet af: ${createdBy}`, `Oprettet: ${createdLabel}`, `Forfald: ${dueLabel}`, isNextEffective ? 'Markeret som næste opgave' : null, step.is_done && completedLabel ? `Færdiggjort af: ${completedBy}` : null, step.is_done && completedLabel ? `Færdiggjort: ${completedLabel}` : null ].filter(Boolean).join('
'); return `
${step.title}
${statusBadge}
${!step.is_done ? ` ` : ''}
${step.description ? `
${step.description}
` : ''}
Forfald: ${dueLabel}
`; }; const sections = []; if (openSteps.length) { sections.push(`
Åbne (${openSteps.length})
${openSteps.map(renderStep).join('')} `); } if (doneSteps.length) { sections.push(`
Færdige (${doneSteps.length})
${doneSteps.map(renderStep).join('')} `); } list.innerHTML = sections.join(''); if (window.bootstrap) { list.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((el) => { bootstrap.Tooltip.getOrCreateInstance(el, { trigger: 'hover focus', placement: 'left', container: 'body', html: true }); }); } setModuleContentState('todo-steps', true); } function updateTopbarNextTodo(steps) { const valueEl = document.getElementById('topbarNextTodoValue'); const metaEl = document.getElementById('topbarNextTodoMeta'); if (!valueEl || !metaEl) return; const openSteps = Array.isArray(steps) ? steps.filter((step) => !step.is_done) : []; if (!openSteps.length) { valueEl.textContent = 'Ingen åbne todo-opgaver'; metaEl.textContent = 'Alt er færdigt'; setNextTodoOverrideId(null); return; } const nextOverrideId = getNextTodoOverrideId(); const overrideStep = nextOverrideId ? openSteps.find((step) => step.id === nextOverrideId) : null; const nextStep = overrideStep || openSteps.find((step) => !!step.is_next) || openSteps[0]; if (!overrideStep && nextOverrideId) { setNextTodoOverrideId(null); } valueEl.textContent = nextStep.title || 'Untitled todo'; metaEl.textContent = nextStep.due_date ? `Forfald: ${formatTodoDate(nextStep.due_date)}` : 'Ingen forfaldsdato'; } async function setNextTodoStep(stepId, isNext) { try { const res = await fetch(`/api/v1/sag/todo-steps/${stepId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_next: isNext, is_done: false }) }); if (!res.ok) { const error = await res.json().catch(() => ({})); throw new Error(error.detail || 'Kunne ikke opdatere næste-opgave'); } setNextTodoOverrideId(isNext ? stepId : null); await loadTodoSteps(); } catch (e) { alert('Fejl: ' + e.message); } } async function loadTodoSteps() { const list = document.getElementById('todo-steps-list'); if (!list) return; list.innerHTML = '
Henter opgaver...
'; try { const res = await fetch(`/api/v1/sag/${caseId}/todo-steps`); if (!res.ok) throw new Error('Kunne ikke hente steps'); const steps = await res.json(); renderTodoSteps(steps || []); } catch (e) { console.error('Error loading todo steps:', e); list.innerHTML = '
Fejl ved hentning
'; setModuleContentState('todo-steps', true); } } function toggleTodoStepForm(forceOpen = null) { const form = document.getElementById('todoStepForm'); const moduleCard = document.querySelector('[data-module="todo-steps"]'); if (!form) return; const shouldOpen = forceOpen === null ? form.classList.contains('d-none') : Boolean(forceOpen); if (shouldOpen) { form.classList.remove('d-none'); if (moduleCard) { moduleCard.classList.remove('module-empty-compact'); } const titleInput = document.getElementById('todoStepTitle'); if (titleInput) { titleInput.focus(); } } else { form.classList.add('d-none'); applyViewLayout(currentCaseView); } } async function createTodoStep(event) { event.preventDefault(); const titleInput = document.getElementById('todoStepTitle'); const descInput = document.getElementById('todoStepDescription'); const dueInput = document.getElementById('todoStepDueDate'); if (!titleInput) return; const title = titleInput.value.trim(); if (!title) { alert('Titel er paakraevet'); return; } const userId = getTodoUserId(); if (!userId) { alert('Mangler bruger-id. Log ind igen.'); return; } try { const res = await fetch(`/api/v1/sag/${caseId}/todo-steps?user_id=${userId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, description: descInput.value.trim() || null, due_date: dueInput.value || null }) } ); if (!res.ok) { const err = await res.json(); throw new Error(err.detail || 'Kunne ikke oprette step'); } titleInput.value = ''; descInput.value = ''; dueInput.value = ''; await loadTodoSteps(); toggleTodoStepForm(false); } catch (e) { alert('Fejl: ' + e.message); } } async function toggleTodoStep(stepId, isDone) { const userId = getTodoUserId(); if (!userId) { alert('Mangler bruger-id. Log ind igen.'); return; } try { const res = await fetch(`/api/v1/sag/todo-steps/${stepId}?user_id=${userId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_done: isDone }) } ); if (!res.ok) throw new Error('Kunne ikke opdatere step'); await loadTodoSteps(); } catch (e) { alert('Fejl: ' + e.message); } } async function deleteTodoStep(stepId) { if (!confirm('Slet dette step?')) return; try { const res = await fetch(`/api/v1/sag/todo-steps/${stepId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Kunne ikke slette step'); await loadTodoSteps(); } catch (e) { alert('Fejl: ' + e.message); } } async function promptLinkLocation() { const id = prompt("Indtast Lokations ID:"); if (!id) return; try { const res = await fetch(`/api/v1/sag/{{ case.id }}/locations`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ location_id: parseInt(id) }) }); if (!res.ok) throw await res.json(); loadCaseLocations(); } catch (e) { alert("Fejl: " + (e.detail || e.message)); } } async function unlinkLocation(locId) { if(!confirm("Fjern link til denne lokation?")) return; try { const res = await fetch(`/api/v1/sag/{{ case.id }}/locations/${locId}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || 'Kunne ikke fjerne lokation'); } loadCaseLocations(); } catch (e) { alert("Fejl ved sletning: " + (e.message || 'Ukendt fejl')); } } // Initialize relation search when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupRelationSearch); } else { setupRelationSearch(); } // Kontakt Modal functions function showKontaktModal() { const modal = new bootstrap.Modal(document.getElementById('kontaktModal')); modal.show(); } // Afdeling Modal functions function showAfdelingModal() { const modal = new bootstrap.Modal(document.getElementById('afdelingModal')); modal.show(); } async function updateAfdeling() { const newAfdeling = document.getElementById('afdelingInput').value.trim(); try { const response = await fetch('/api/v1/customers/{{ customer.id if customer else 0 }}', { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ department: newAfdeling }) }); if (!response.ok) throw await response.json(); // Reload page to show updated data window.location.reload(); } catch (e) { alert("Fejl ved opdatering: " + (e.detail || e.message)); } }