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 `
`;
}
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)}
` : ''}
`;
});
});
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 = `
${hardware.map(h => `
${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 = `
${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)}
` : ''}
Tilfoej
`;
}).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.description ? `
${step.description}
` : ''}
Forfald: ${dueLabel}
`;
};
const sections = [];
if (openSteps.length) {
sections.push(`
${openSteps.map(renderStep).join('')}
`);
}
if (doneSteps.length) {
sections.push(`
${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));
}
}