(function () { let latestSections = {}; let latestContextActions = { global: [], context: [] }; let activeKey = 'timer'; let overviewFilter = null; let ws = null; let pollTimer = null; let wsReconnectTimer = null; let latestNotificationCount = 0; let latestNotifications = []; let switchCaseModalInstance = null; let quickNoteDraft = ''; let quickNoteHintState = { message: 'Tip: gemmer som kommentar på aktiv/åben sag.', level: 'muted' }; let switchCaseState = { activeTimer: null, decision: 'unchanged', timers: { active: [], paused: [] }, recentCases: [], unassignedCases: [] }; let noteEditorState = { editingId: 0, title: '', content: '' }; let noteTargetModalInstance = null; let noteTargetState = { target: 'case', noteId: 0 }; const LOCAL_NOTES_KEY = 'bmc_bottom_bar_notes_v1'; let notesApiUnavailable = false; function byId(id) { return document.getElementById(id); } function escapeHtml(value) { return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function loadLocalNotes() { try { const raw = window.localStorage.getItem(LOCAL_NOTES_KEY); const parsed = raw ? JSON.parse(raw) : []; return Array.isArray(parsed) ? parsed : []; } catch (e) { return []; } } function saveLocalNotes(notes) { try { window.localStorage.setItem(LOCAL_NOTES_KEY, JSON.stringify(Array.isArray(notes) ? notes : [])); } catch (e) { // Ignore storage failures (private mode/quota) } } function hydrateNotesFromLocalIfNeeded(sections) { const target = sections || {}; const notes = target.notes || { list: [], count: 0 }; const localNotes = loadLocalNotes(); const remoteList = Array.isArray(notes.list) ? notes.list : []; if (!notesApiUnavailable && remoteList.length > 0) { return target; } target.notes = { count: localNotes.length, list: localNotes .slice() .sort(function (a, b) { return Number(b.is_pinned || 0) - Number(a.is_pinned || 0) || String(b.updated_at || '').localeCompare(String(a.updated_at || '')); }) }; return target; } async function fetchBottomBarState() { const contextPath = encodeURIComponent(window.location.pathname || '/'); const response = await fetch('/api/v1/bottom-bar/state?context=' + contextPath, { credentials: 'same-origin', headers: { 'Accept': 'application/json' } }); if (!response.ok) { throw new Error('Could not load bottom bar state'); } return response.json(); } function applyState(data) { if (data && data.enabled) { latestSections = data.sections || {}; latestSections = hydrateNotesFromLocalIfNeeded(latestSections); latestContextActions = (latestSections.context_actions || { global: [], context: [] }); latestNotificationCount = Number((((data || {}).notifications || {}).count) || 0); latestNotifications = (((data || {}).notifications || {}).items || []); syncBossTabVisibility(); updateBar(latestSections); updateActivityZone(); const focusedId = document.activeElement && document.activeElement.id; const keepCurrentRender = activeKey === 'notes' && (focusedId === 'bbNoteTitleInput' || focusedId === 'bbNoteContentInput'); if (!keepCurrentRender) { renderTabPanel(); } setVisibility(true); return; } setVisibility(false); } function setVisibility(enabled) { const shell = byId('globalBottomBar'); if (!shell) { return; } if (enabled) { shell.hidden = false; window.requestAnimationFrame(function () { shell.classList.add('is-visible'); }); } else { shell.classList.remove('is-visible'); window.setTimeout(function () { if (!shell.classList.contains('is-visible')) { shell.hidden = true; } }, 320); } document.body.classList.toggle('bottom-bar-visible', !!enabled); if (!enabled) { document.body.classList.remove('bottom-bar-expanded'); } } function setExpanded(expanded) { const shell = byId('globalBottomBar'); const toggle = byId('bbSheetToggle'); const panel = byId('bbSheetPanel'); if (!shell || !toggle || !panel) { return; } shell.classList.toggle('is-expanded', !!expanded); document.body.classList.toggle('bottom-bar-expanded', !!expanded); toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false'); panel.setAttribute('aria-hidden', expanded ? 'false' : 'true'); } function syncBossTabVisibility() { const bossBtn = document.querySelector('.bb-tab-btn[data-bb-tab="boss"]'); if (!bossBtn) { return; } bossBtn.classList.remove('d-none'); } function getCounts(sections) { const mail = sections.mail || {}; const cases = sections.cases || {}; const urgent = sections.urgent || {}; const timer = sections.timer || {}; const kuma = sections.kuma || {}; const eset = sections.eset || {}; const unassigned = sections.unassigned || {}; return { mail: Number(mail.unread || 0), cases: Number(cases.open || 0), urgent: Number(urgent.count || 0), unassigned: Number(unassigned.count || 0), timer: Number(timer.active_count || 0), kuma: Number(kuma.down || 0), eset: Number(eset.incidents || 0) }; } function detailTextFor(key, sections) { const counts = getCounts(sections); const nameMap = { mail: 'Ubesvarede mails', cases: 'Åbne sager', urgent: 'Hastesager', unassigned: 'Sager uden ansvarlig', timer: 'Aktive timere', kuma: 'Kuma alerts', eset: 'ESET incidents' }; const val = counts[key] || 0; return nameMap[key] + ': ' + val; } function severityClassFor(key, value) { const val = Number(value || 0); if (key === 'urgent') { return val > 0 ? 'sev-critical' : 'sev-ok'; } if (key === 'unassigned') { if (val >= 3) return 'sev-critical'; if (val > 0) return 'sev-warn'; return 'sev-ok'; } if (key === 'mail') { if (val >= 10) return 'sev-critical'; if (val > 0) return 'sev-warn'; return 'sev-ok'; } return val > 0 ? 'sev-warn' : 'sev-ok'; } function listFor(key, sections) { const mail = sections.mail || {}; const cases = sections.cases || {}; const urgent = sections.urgent || {}; const unassigned = sections.unassigned || {}; const timer = sections.timer || {}; const kuma = sections.kuma || {}; const eset = sections.eset || {}; const messages = sections.messages || {}; const tasks = sections.tasks || {}; const notes = sections.notes || {}; const boss = sections.boss || {}; function esc(str) { return String(str).replace(/&/g, '&').replace(//g, '>'); } const quickNoteValue = esc(quickNoteDraft || ''); if (key === 'overview') { if (overviewFilter === 'urgent') return urgent.list ? urgent.list.map(u => '
Hastesag: ' + esc(u.title) + '
') : ['Ingen hastesager.']; if (overviewFilter === 'kuma') return kuma.list ? kuma.list.map(k => '
📉 ' + esc(k) + '
') : ['Alle systemer oppe.']; if (overviewFilter === 'eset') return eset.list ? eset.list.map(e => '
🔐 ' + esc(e) + '
') : ['Ingen ESET incidents.']; if (overviewFilter === 'cases') return cases.list ? cases.list.map(c => '
' + esc(c.title) + '
') : ['Ingen åbne sager.']; if (overviewFilter === 'mail') return ['
📧 ' + mail.unread + ' ulæste mails.
💬 ' + mail.customer_reply_needed + ' kræver kundesvar.
']; if (overviewFilter === 'unassigned') return unassigned.list ? unassigned.list.map(u => '
' + esc(u.title || ('Sag #' + (u.id || ''))) + '
') : ['Ingen åbne sager uden ansvarlig.']; let out = []; if (urgent.count > 0) out.push('
Hastesager: ' + urgent.count + ' aktive
'); if (mail.unread > 0) out.push('
Ubesvarede mails: ' + mail.unread + '
'); if (cases.open > 0) out.push('
Åbne sager i alt: ' + cases.open + '
'); if (kuma.down > 0) out.push('
Uptime Kuma nedetid: ' + kuma.down + ' enheder
'); if (eset.incidents > 0) out.push('
ESET incidents: ' + eset.incidents + '
'); if (out.length === 0) { out.push('
🎉 Alt ser grønt ud! Intet kritisk lige nu.
'); } // Add quick note button on overview out.push('
' + esc(quickNoteHintState.message || 'Tip: gemmer som kommentar på aktiv/åben sag.') + '
'); return out; } if (key === 'timer') { if (timer.active_count > 0) { return (timer.list || []).map(t => { const elapsedText = t.elapsed_hhmmss || (String(t.elapsed || 0) + 's'); return '
' + esc(t.desc) + ' (' + esc(elapsedText) + ')
'; }); } return ['Ingen aktive timere lige nu.']; } if (key === 'messages') { if (messages.count > 0) { return (messages.list || []).map(m => '
' + esc(m.from) + ': ' + esc(m.text) + '
'); } return ['Ingen nye beskeder.']; } if (key === 'tasks') { if (tasks.count > 0) { return (tasks.list || []).map(t => '
' + esc(t.title) + ' ' + esc(t.deadline) + '
'); } return ['Ingen aktuelle opgaver.']; } if (key === 'notes') { const noteItems = Array.isArray(notes.list) ? notes.list : []; const editorTitle = esc(noteEditorState.title || ''); const editorContent = esc(noteEditorState.content || ''); const editingId = Number(noteEditorState.editingId || 0); const out = [ '
' + '
Egne noter (vises i bundbar)
' + '' + '' + '
' + '' + '' + '
' + '
' ]; if (!noteItems.length) { out.push('
Ingen noter endnu.
'); return out; } noteItems.forEach(function (note) { const noteId = Number(note.id || 0); const noteTitle = esc((note.title || '').trim() || ('Note #' + noteId)); const content = String(note.content || ''); const preview = esc(content.length > 220 ? (content.slice(0, 220) + '...') : content); const pinned = !!note.is_pinned; out.push( '
' + '
' + '
' + noteTitle + '' + (pinned ? ' Pinned' : '') + '
' + '
' + '' + '' + '' + '
' + '
' + '
' + preview + '
' + '
' + '' + '' + '' + '
' + '
' ); }); return out; } if (key === 'boss') { const stats = boss.stats || {}; const workload = Array.isArray(boss.team_workload) ? boss.team_workload : []; const techniciansToday = Array.isArray(boss.technicians_today) ? boss.technicians_today : []; const escalations = Array.isArray(boss.escalations) ? boss.escalations : []; const unassigned = Array.isArray(boss.unassigned_cases) ? boss.unassigned_cases : []; const out = [ '
' + '
Åbne sager
' + Number(stats.open_cases || 0) + '
' + '
Hastesager
' + Number(stats.urgent_cases || 0) + '
' + '
Uden ansvarlig
' + Number(stats.unassigned || 0) + '
' + '
Stale >24t
' + Number(stats.stale_urgent_cases || 0) + '
' + '
', '
' + '' + '' + '' + '' + '
' ]; if (workload.length > 0) { out.push('
Team-belastning
'); workload.slice(0, 5).forEach(function (w) { out.push( '
' + '
' + esc(w.owner_name || 'Ukendt') + '
Åbne: ' + Number(w.open_cases || 0) + ' • Haste: ' + Number(w.urgent_cases || 0) + '
' + '' + '
' ); }); } if (techniciansToday.length > 0) { out.push('
Teknikernes opgaver i dag
'); techniciansToday.slice(0, 6).forEach(function (tech) { const todayTasks = Array.isArray(tech.today_tasks) ? tech.today_tasks : []; let tasksHtml = '
Ingen opgaver i dag.
'; if (todayTasks.length > 0) { tasksHtml = '
' + todayTasks.slice(0, 3).map(function (task) { return '
' + esc(task.title || ('Sag #' + task.id)) + '
'; }).join('') + '
'; } out.push( '
' + '
' + '
' + esc(tech.owner_name || 'Tekniker') + '
I dag: ' + Number(tech.due_today_cases || 0) + ' • Åbne: ' + Number(tech.open_cases || 0) + '
' + '
' + '' + '' + '
' + '
' + tasksHtml + '
' ); }); } if (escalations.length > 0) { out.push('
Eskaleringer
'); escalations.slice(0, 4).forEach(function (c) { const ageHours = Math.floor(Number(c.age_seconds || 0) / 3600); out.push( '
' + '
' + esc(c.title || 'Sag') + '
' + esc(c.owner_name || 'Ikke tildelt') + ' • ' + ageHours + 't siden opdatering
' + '' + '
' ); }); } if (unassigned.length > 0) { out.push('
Ufordelte sager
'); unassigned.slice(0, 4).forEach(function (c) { out.push( '
' + '
' + esc(c.title || 'Sag') + '
Prioritet: ' + esc(c.priority || 'normal') + '
' + '' + '
' ); }); } return out; } return ['Klik rundt i menuen for at se data.']; } function updateBar(sections) { const counts = getCounts(sections); const keys = Object.keys(counts); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const chipText = document.querySelector('.bb-chip[data-bb-key="' + key + '"] .bb-chip-text'); const chipLabel = document.querySelector('.bb-chip[data-bb-key="' + key + '"] .bb-chip-label'); const chipBubble = document.querySelector('.bb-chip[data-bb-key="' + key + '"] .bb-chip-bubble'); const chip = document.querySelector('.bb-chip[data-bb-key="' + key + '"]'); if (chipText && chip) { const val = counts[key]; const labels = { mail: 'Ulæste mails', cases: 'Sager', urgent: 'Hastesager', unassigned: 'Uden ansvarlig', timer: 'Timere', kuma: 'Kuma', eset: 'ESET' }; if (chipLabel) { chipLabel.textContent = labels[key]; } if (chipBubble) { chipBubble.textContent = String(val); } chipText.textContent = labels[key] + ': ' + val; chip.classList.toggle('has-items', val > 0); chip.classList.remove('sev-ok', 'sev-warn', 'sev-critical'); chip.classList.add(severityClassFor(key, val)); chip.setAttribute('title', detailTextFor(key, sections)); chip.setAttribute('aria-label', detailTextFor(key, sections)); } } } function bindChipHoverPreview() { const chips = document.querySelectorAll('.bb-chip'); const detail = byId('bbCountDetail'); if (!detail || !chips.length) { return; } chips.forEach(function (chip) { chip.addEventListener('mouseenter', function () { const key = chip.getAttribute('data-bb-key'); if (!key) return; detail.innerHTML = ' ' + detailTextFor(key, latestSections); }); chip.addEventListener('mouseleave', function () { detail.innerHTML = ' Klik på en kategori for at se detaljer'; }); }); } function updateActivityZone() { const timerChip = byId('bbActiveTimerChip'); const timerText = byId('bbActiveTimerText'); const notifCount = byId('bbNotificationsCount'); const timer = ((latestSections || {}).timer || {}).active || {}; const hasActiveTimer = !!timer.active; if (timerChip && timerText) { timerChip.classList.toggle('is-hidden', !hasActiveTimer); if (hasActiveTimer) { const elapsed = timer.elapsed_hhmmss || '00:00:00'; const name = timer.sag_navn || ('Sag #' + (timer.sag_id || '')); timerText.textContent = name + ' - ' + elapsed; } } if (notifCount) { const computed = Number(latestNotificationCount || ((latestSections.messages || {}).count || 0)); notifCount.textContent = String(computed); } } function renderTabPanel() { const titleContainer = byId('bbTabTitle'); const innerContent = byId('bbTabInnerContent'); if (!titleContainer || !innerContent) { return; } const titleText = titleContainer.querySelector('.bb-tab-title-text'); const titleByKey = { overview: 'Overblik', timer: 'Timere', messages: 'Beskeder', tasks: 'Opgaver', notes: 'Noter', boss: 'Chef Dashboard' }; const iconByKey = { overview: 'bi-bell', timer: 'bi-stopwatch', messages: 'bi-chat-dots', tasks: 'bi-calendar-check', notes: 'bi-journal-text', boss: 'bi-person-workspace' }; const activeTitle = titleByKey[activeKey] || 'Info'; if (titleText) { titleText.textContent = activeTitle; } else { titleContainer.textContent = activeTitle; } const iconSpan = titleContainer.querySelector('.bi'); if (iconSpan) { iconSpan.className = 'bi ' + (iconByKey[activeKey] || 'bi-info-circle') + ' me-2 text-accent'; } // Render rich UI lists const lines = listFor(activeKey, latestSections); const ul = document.createElement('ul'); ul.className = 'bb-tab-list'; lines.forEach(function (line) { const li = document.createElement('li'); // Allow rich HTML (buttons, inputs) - assuming listFor provides sanitized data wrapped in markup li.innerHTML = line; ul.appendChild(li); }); innerContent.innerHTML = ''; // Add specific headers/controls based on active tab if (activeKey === 'tasks') { const topBar = document.createElement('div'); topBar.className = 'bb-task-actions mb-3'; topBar.innerHTML = ''; innerContent.appendChild(topBar); } if (activeKey === 'messages') { const chatContainer = document.createElement('div'); chatContainer.className = 'd-flex flex-column h-100'; ul.classList.add('flex-grow-1', 'mb-3'); const replyBox = document.createElement('div'); replyBox.className = 'mt-2 border-top pt-2 border-primary-subtle'; replyBox.innerHTML = `
`; chatContainer.appendChild(ul); chatContainer.appendChild(replyBox); innerContent.appendChild(chatContainer); // Fetch users dynamically fetch('/api/v1/users?is_active=true', { credentials: 'include' }) .then(r => r.json()) .then(payload => { const users = Array.isArray(payload) ? payload : ((payload && payload.data && Array.isArray(payload.data)) ? payload.data : []); const sel = document.getElementById('chatRecipient'); if (sel) { sel.innerHTML = ''; users.forEach(u => { sel.innerHTML += ``; }); } }) .catch(e => console.error("Error fetching users for chat:", e)); } else { innerContent.appendChild(ul); } } function bindSideTabs() { const buttons = document.querySelectorAll('.bb-tab-btn'); for (let i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', function (e) { // Clear filter on direct human click of the button, unless we programmatically called click() if (e.isTrusted) overviewFilter = null; for (let j = 0; j < buttons.length; j++) { buttons[j].classList.remove('is-active'); buttons[j].setAttribute('aria-selected', 'false'); } this.classList.add('is-active'); this.setAttribute('aria-selected', 'true'); activeKey = this.getAttribute('data-bb-tab'); renderTabPanel(); const detail = byId('bbCountDetail'); if (detail) { detail.innerHTML = ' Viser: ' + (activeKey.charAt(0).toUpperCase() + activeKey.slice(1)); } }); } } function bindChipClicks() { const chips = document.querySelectorAll('.bb-chip'); for (let i = 0; i < chips.length; i++) { chips[i].addEventListener('click', function () { const key = this.getAttribute('data-bb-key'); if (!key) return; const detail = byId('bbCountDetail'); if (detail) { detail.innerHTML = ' ' + detailTextFor(key, latestSections); } const routes = { mail: '/emails', urgent: '/sag?priority=urgent', timer: '/timetracking', cases: '/sag' }; if (key === 'unassigned') { openUnassignedCasesPanel(); return; } const route = routes[key] || '/dashboard'; window.location.href = route; }); } } function bindSheetToggle() { const toggle = byId('bbSheetToggle'); if (!toggle) return; toggle.addEventListener('click', function () { const shell = byId('globalBottomBar'); if (!shell) return; const isExp = shell.classList.contains('is-expanded'); setExpanded(!isExp); }); } function stopPolling() { if (pollTimer) { window.clearTimeout(pollTimer); pollTimer = null; } } function pollOnce() { fetchBottomBarState().then(function (data) { applyState(data); }).catch(function (err) { console.warn('Bottom bar poll failed', err); }).finally(function () { pollTimer = window.setTimeout(pollOnce, 15000); }); } function startPollingFallback() { stopPolling(); pollOnce(); } function updateFromRealtimeEvent(payload) { if (!payload || !payload.event) { return; } if (payload.event === 'timer_tick') { const timer = payload.data || {}; latestSections.timer = latestSections.timer || {}; latestSections.timer.active = timer; latestSections.timer.active_count = timer.active ? 1 : 0; latestSections.timer.list = timer.active ? [{ id: timer.time_entry_id, sag_id: timer.sag_id, desc: timer.sag_navn || ('Sag #' + (timer.sag_id || '')), elapsed: timer.elapsed, elapsed_hhmmss: timer.elapsed_hhmmss }] : []; } if (payload.event === 'status_delta') { const status = payload.data || {}; latestSections.mail = latestSections.mail || {}; latestSections.cases = latestSections.cases || {}; latestSections.urgent = latestSections.urgent || {}; latestSections.unassigned = latestSections.unassigned || {}; latestSections.boss = latestSections.boss || { stats: {} }; latestSections.mail.unread = Number(status.mails_unread || 0); latestSections.mail.customer_reply_needed = Number(status.mails_unread || 0); latestSections.cases.open = Number(status.sager_open || 0); latestSections.urgent.count = Number(status.sager_urgent || 0); latestSections.unassigned.count = Number(status.sager_unassigned || 0); latestSections.boss.stats = latestSections.boss.stats || {}; latestSections.boss.stats.unassigned = Number(status.sager_unassigned || 0); } if (payload.event === 'notification_delta') { const notifications = payload.data || {}; const items = Array.isArray(notifications.items) ? notifications.items : []; latestSections.messages = latestSections.messages || {}; latestSections.tasks = latestSections.tasks || {}; latestSections.messages.count = items.length; latestSections.messages.list = items.slice(0, 5).map(function (item) { return { from: (item.type || 'System').toString(), text: (item.title || item.message || 'Notifikation').toString() }; }); latestSections.tasks.count = items.length; latestSections.tasks.list = items.slice(0, 5).map(function (item) { return { title: item.title || 'Notifikation', deadline: item.severity || 'info' }; }); latestNotificationCount = Number(notifications.count || items.length || 0); latestNotifications = items; } syncBossTabVisibility(); updateBar(latestSections); updateActivityZone(); const focusedId = document.activeElement && document.activeElement.id; const quickNoteFocused = activeKey === 'overview' && focusedId === 'bbQuickNoteInput'; const noteEditorFocused = activeKey === 'notes' && (focusedId === 'bbNoteTitleInput' || focusedId === 'bbNoteContentInput'); if (!quickNoteFocused && !noteEditorFocused) { renderTabPanel(); } } function scheduleWsReconnect() { if (wsReconnectTimer) { window.clearTimeout(wsReconnectTimer); } wsReconnectTimer = window.setTimeout(connectRealtime, 3000); } function connectRealtime() { if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = protocol + '//' + window.location.host + '/api/v1/bottom-bar/ws'; try { ws = new WebSocket(wsUrl); } catch (err) { console.warn('Bottom bar websocket init failed', err); startPollingFallback(); scheduleWsReconnect(); return; } ws.addEventListener('open', function () { stopPolling(); }); ws.addEventListener('message', function (event) { try { const payload = JSON.parse(event.data || '{}'); updateFromRealtimeEvent(payload); } catch (err) { console.warn('Bottom bar websocket parse error', err); } }); ws.addEventListener('close', function () { startPollingFallback(); scheduleWsReconnect(); }); ws.addEventListener('error', function (err) { console.warn('Bottom bar websocket error', err); startPollingFallback(); }); } function stopActiveTimer() { const active = ((latestSections || {}).timer || {}).active || {}; if (!active.time_entry_id) { return Promise.resolve(); } return fetch('/api/v1/timetracking/time/stop', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ time_id: active.time_entry_id }) }).catch(function (err) { console.warn('Failed stopping active timer', err); }); } function pauseActiveTimer() { return fetch('/api/v1/timetracking/time/pause', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: '{}' }).then(function (res) { if (!res.ok) { throw new Error('Kunne ikke pause timer'); } return res.json().catch(function () { return {}; }); }); } function resumeTimer(timeId) { const payload = {}; if (Number(timeId || 0) > 0) { payload.time_id = Number(timeId); } return fetch('/api/v1/timetracking/time/resume', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(function (res) { if (!res.ok) { throw new Error('Kunne ikke genoptage timer'); } return res.json().catch(function () { return {}; }); }); } function normalizeSwitchableTimerPayload(payload) { const out = { active: [], paused: [] }; if (!payload || typeof payload !== 'object') { return out; } if (Array.isArray(payload.active) || Array.isArray(payload.paused)) { out.active = Array.isArray(payload.active) ? payload.active : []; out.paused = Array.isArray(payload.paused) ? payload.paused : []; return out; } const active = payload.active || {}; const paused = Array.isArray(payload.paused) ? payload.paused : []; out.active = active && active.active ? [active] : []; out.paused = paused; return out; } async function fetchSwitchableTimers() { const primary = await fetch('/api/v1/timetracking/time/my-switchable', { credentials: 'include', headers: { 'Accept': 'application/json' } }); if (primary.ok) { const payload = await primary.json(); return normalizeSwitchableTimerPayload(payload); } const fallback = await fetch('/api/v1/bottom-bar/timers/own?paused_limit=10', { credentials: 'include', headers: { 'Accept': 'application/json' } }); if (!fallback.ok) { throw new Error('Kunne ikke hente timeroversigt'); } const payload = await fallback.json(); return normalizeSwitchableTimerPayload(payload); } async function fetchRecentCases() { const response = await fetch('/api/v1/sag/recent?limit=10', { credentials: 'include', headers: { 'Accept': 'application/json' } }); if (!response.ok) { throw new Error('Kunne ikke hente seneste sager'); } const payload = await response.json(); return Array.isArray(payload) ? payload : []; } function getSwitchCaseModal() { const modalEl = byId('bbSwitchCaseModal'); if (!modalEl || !window.bootstrap || !window.bootstrap.Modal) { return null; } if (!switchCaseModalInstance) { switchCaseModalInstance = window.bootstrap.Modal.getOrCreateInstance(modalEl); } return switchCaseModalInstance; } function switchCaseStatusMessage(html) { const statusEl = byId('bbSwitchCaseStatus'); if (statusEl) { statusEl.innerHTML = html; } } function timerDisplayName(timer) { const sagId = Number((timer && timer.sag_id) || 0); const title = (timer && (timer.sag_navn || timer.title || timer.beskrivelse || timer.desc)) || ''; if (title) { return escapeHtml(title); } return sagId > 0 ? ('Sag #' + sagId) : 'Ukendt sag'; } function renderSwitchCaseLists() { const timersEl = byId('bbSwitchTimersList'); const recentEl = byId('bbSwitchRecentCasesList'); const actionsEl = byId('bbSwitchTimerActions'); if (!timersEl || !recentEl) { return; } const active = Array.isArray(switchCaseState.timers.active) ? switchCaseState.timers.active : []; const paused = Array.isArray(switchCaseState.timers.paused) ? switchCaseState.timers.paused : []; const recentCases = Array.isArray(switchCaseState.recentCases) ? switchCaseState.recentCases : []; const unassignedCases = Array.isArray(switchCaseState.unassignedCases) ? switchCaseState.unassignedCases : []; const showUnassigned = unassignedCases.length > 0; if (actionsEl) { actionsEl.classList.toggle('d-none', !switchCaseState.activeTimer); } if (!active.length && !paused.length) { timersEl.innerHTML = '
Ingen aktive eller pausede timere.
'; } else { let timerItems = ''; active.forEach(function (t) { const timeId = Number((t && (t.id || t.time_entry_id)) || 0); timerItems += '
' + '
Aktiv' + timerDisplayName(t) + '
' + '' + '
'; if (timeId > 0) { timerItems += '
Timer ID: ' + timeId + '
'; } }); paused.forEach(function (t) { timerItems += '
' + '
Pauset' + timerDisplayName(t) + '
' + '' + '
'; }); timersEl.innerHTML = timerItems; } const sourceCases = showUnassigned ? unassignedCases : recentCases; const titleEl = byId('bbSwitchCaseModalLabel'); if (titleEl) { titleEl.innerHTML = showUnassigned ? 'Uden ansvarlig (åbne sager)' : 'Skift sag'; } if (!sourceCases.length) { recentEl.innerHTML = '
Ingen sager at vise.
'; return; } recentEl.innerHTML = sourceCases.map(function (row) { const caseId = Number((row && (row.sag_id || row.id)) || 0); const title = escapeHtml((row && (row.titel || row.title)) || (caseId > 0 ? ('Sag #' + caseId) : 'Ukendt sag')); const prefix = showUnassigned ? 'Uden ansvarlig' : 'Senest'; return ( '
' + '
' + prefix + title + '
' + '
' + '' + '' + '
' + '
' ); }).join(''); } async function loadSwitchCaseData(options) { const opts = options || {}; switchCaseState.decision = 'unchanged'; switchCaseState.activeTimer = (((latestSections || {}).timer || {}).active || {}).active ? ((latestSections || {}).timer || {}).active : null; switchCaseState.unassignedCases = []; switchCaseState.recentCases = []; switchCaseState.timers = { active: [], paused: [] }; if (opts.onlyUnassigned) { const unassigned = (((latestSections || {}).unassigned || {}).list || []); switchCaseState.unassignedCases = Array.isArray(unassigned) ? unassigned.slice(0, 25) : []; switchCaseStatusMessage('Viser kun åbne sager uden ansvarlig.'); renderSwitchCaseLists(); return; } switchCaseStatusMessage('Henter timer og seneste sager...'); const results = await Promise.allSettled([fetchSwitchableTimers(), fetchRecentCases()]); const timersResult = results[0]; const casesResult = results[1]; if (timersResult.status === 'fulfilled') { switchCaseState.timers = timersResult.value; } if (casesResult.status === 'fulfilled') { switchCaseState.recentCases = casesResult.value; } const failedParts = []; if (timersResult.status === 'rejected') failedParts.push('timere'); if (casesResult.status === 'rejected') failedParts.push('seneste sager'); if (failedParts.length) { switchCaseStatusMessage('Kunne ikke hente ' + escapeHtml(failedParts.join(' + ')) + '. Viser det vi har.'); } else if (switchCaseState.activeTimer) { const name = timerDisplayName(switchCaseState.activeTimer); switchCaseStatusMessage('Aktiv timer: ' + name + '. Vælg handling før du starter en ny timer.'); } else { switchCaseStatusMessage('Klar til skift af sag.'); } renderSwitchCaseLists(); } async function openSwitchCaseModal(options) { const modal = getSwitchCaseModal(); if (!modal) { window.location.href = '/sag'; return; } modal.show(); try { await loadSwitchCaseData(options); } catch (err) { console.warn('Switch case modal load failed', err); switchCaseStatusMessage('Kunne ikke hente data til skift af sag.'); renderSwitchCaseLists(); } } function openUnassignedCasesPanel() { window.location.href = '/sag?unassigned=1'; } async function startTimerForCase(caseId) { const validCaseId = Number(caseId || 0); if (validCaseId <= 0) { return; } const hasActiveTimer = !!(switchCaseState.activeTimer && switchCaseState.activeTimer.active); if (hasActiveTimer && switchCaseState.decision === 'unchanged') { switchCaseStatusMessage('Du har en aktiv timer. Vælg Pause nu, Stop nu eller Fortsæt uændret først.'); return; } try { const response = await fetch('/api/v1/timetracking/time/start', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sag_id: validCaseId }) }); if (!response.ok) { const payload = await response.json().catch(function () { return {}; }); const detail = (payload && payload.detail) ? payload.detail : ('HTTP ' + response.status); throw new Error(typeof detail === 'string' ? detail : 'Kunne ikke starte timer'); } const modal = getSwitchCaseModal(); if (modal) { modal.hide(); } window.location.href = '/sag/' + validCaseId; } catch (err) { switchCaseStatusMessage('' + escapeHtml(err.message || 'Kunne ikke starte timer for sag.')); } } function openCaseDetail(caseId) { const validCaseId = Number(caseId || 0); if (validCaseId <= 0) { return; } const modal = getSwitchCaseModal(); if (modal) { modal.hide(); } window.location.href = '/sag/' + validCaseId; } function resolveQuickNoteCaseId() { const match = (window.location.pathname || '').match(/^\/sag\/(\d+)$/); if (match && match[1]) { return Number(match[1]); } const active = (((latestSections || {}).timer || {}).active || {}); const activeSagId = Number(active.sag_id || 0); if (activeSagId > 0) { return activeSagId; } const recent = (((latestSections || {}).recent_cases || {}).items || []); const recentFirst = Number((((recent[0] || {}).id) || ((recent[0] || {}).sag_id) || 0)); return recentFirst > 0 ? recentFirst : 0; } function saveQuickNote(noteText, caseId) { return fetch('/api/v1/sag/' + Number(caseId) + '/kommentarer', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ indhold: noteText }) }).then(function (res) { if (!res.ok) { throw new Error('Kunne ikke gemme note'); } return res.json().catch(function () { return {}; }); }); } function noteById(noteId) { const notes = (((latestSections || {}).notes || {}).list || []); return notes.find(function (row) { return Number((row || {}).id || 0) === Number(noteId || 0); }) || null; } async function readApiError(response, fallbackMessage) { let payload = {}; try { payload = await response.json(); } catch (e) { payload = {}; } const detail = payload && payload.detail ? payload.detail : null; if (typeof detail === 'string' && detail.trim()) { return detail; } return fallbackMessage; } async function fetchWithNotesFallback(url, options) { const response = await fetch(url, options); if (response.status !== 404 || !/\/api\/v1\/bottom-bar\/notes(\/|$)/.test(url)) { return response; } const altUrl = url.endsWith('/') ? url.slice(0, -1) : (url + '/'); return fetch(altUrl, options); } async function createUserNote(title, content) { const endpoint = '/api/v1/bottom-bar/notes'; const res = await fetchWithNotesFallback(endpoint, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: title || '', content: content || '' }) }); if (res.status === 404) { notesApiUnavailable = true; const now = new Date().toISOString(); const local = loadLocalNotes(); const created = { id: Date.now(), title: String(title || '').trim(), content: String(content || '').trim(), is_pinned: false, is_archived: false, created_at: now, updated_at: now, _local_only: true }; local.unshift(created); saveLocalNotes(local); return created; } if (!res.ok) { throw new Error(await readApiError(res, 'Kunne ikke oprette note (' + endpoint + ')')); } notesApiUnavailable = false; return res.json().catch(function () { return {}; }); } async function updateUserNote(noteId, payload) { const endpoint = '/api/v1/bottom-bar/notes/' + Number(noteId || 0); const res = await fetchWithNotesFallback(endpoint, { method: 'PATCH', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload || {}) }); if (res.status === 404) { notesApiUnavailable = true; const local = loadLocalNotes(); const idNum = Number(noteId || 0); const updated = local.map(function (row) { if (Number((row || {}).id || 0) !== idNum) return row; return { ...row, ...payload, updated_at: new Date().toISOString(), _local_only: true }; }); saveLocalNotes(updated); return updated.find(function (row) { return Number((row || {}).id || 0) === idNum; }) || {}; } if (!res.ok) { throw new Error(await readApiError(res, 'Kunne ikke opdatere note (' + endpoint + ')')); } notesApiUnavailable = false; return res.json().catch(function () { return {}; }); } async function deleteUserNote(noteId) { const endpoint = '/api/v1/bottom-bar/notes/' + Number(noteId || 0); const res = await fetchWithNotesFallback(endpoint, { method: 'DELETE', credentials: 'include' }); if (res.status === 404) { notesApiUnavailable = true; const idNum = Number(noteId || 0); const local = loadLocalNotes().filter(function (row) { return Number((row || {}).id || 0) !== idNum; }); saveLocalNotes(local); return { status: 'deleted', note_id: idNum, _local_only: true }; } if (!res.ok) { throw new Error(await readApiError(res, 'Kunne ikke slette note (' + endpoint + ')')); } notesApiUnavailable = false; return res.json().catch(function () { return {}; }); } async function noteToCaseComment(noteId, caseId, excerpt) { const endpoint = '/api/v1/bottom-bar/notes/' + Number(noteId || 0) + '/actions/sag-comment'; const res = await fetchWithNotesFallback(endpoint, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sag_id: Number(caseId || 0), excerpt: excerpt || null }) }); if (!res.ok) { throw new Error(await readApiError(res, 'Kunne ikke indsætte i sag-kommentar (' + endpoint + ')')); } return res.json().catch(function () { return {}; }); } async function noteToContact(noteId, contactId, field, value, mode) { const endpoint = '/api/v1/bottom-bar/notes/' + Number(noteId || 0) + '/actions/contact-update'; const res = await fetchWithNotesFallback(endpoint, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contact_id: Number(contactId || 0), field: String(field || ''), value: value || '', mode: mode || 'append' }) }); if (!res.ok) { throw new Error(await readApiError(res, 'Kunne ikke opdatere kontakt fra note (' + endpoint + ')')); } return res.json().catch(function () { return {}; }); } async function noteToCustomer(noteId, customerId, field, value, mode) { const endpoint = '/api/v1/bottom-bar/notes/' + Number(noteId || 0) + '/actions/customer-update'; const res = await fetchWithNotesFallback(endpoint, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customer_id: Number(customerId || 0), field: String(field || ''), value: value || '', mode: mode || 'append' }) }); if (!res.ok) { throw new Error(await readApiError(res, 'Kunne ikke opdatere firma fra note (' + endpoint + ')')); } return res.json().catch(function () { return {}; }); } function getNoteTargetModal() { const modalEl = byId('bbNoteTargetModal'); if (!modalEl || !window.bootstrap || !window.bootstrap.Modal) { return null; } if (!noteTargetModalInstance) { noteTargetModalInstance = window.bootstrap.Modal.getOrCreateInstance(modalEl); } return noteTargetModalInstance; } function updateNoteTargetStatus(message, isError) { const statusEl = byId('bbNoteTargetStatus'); if (!statusEl) { return; } statusEl.textContent = String(message || ''); statusEl.classList.remove('text-muted', 'text-success', 'text-danger'); if (isError === true) { statusEl.classList.add('text-danger'); return; } if (isError === false) { statusEl.classList.add('text-success'); return; } statusEl.classList.add('text-muted'); } function renderNoteTargetFieldOptions(target) { const wrap = byId('bbNoteTargetFieldWrap'); const label = byId('bbNoteTargetFieldLabel'); const select = byId('bbNoteTargetFieldSelect'); if (!wrap || !label || !select) { return; } if (target === 'case') { wrap.classList.add('d-none'); select.innerHTML = ''; return; } const options = target === 'contact' ? [ { value: 'mobile', label: 'Mobile' }, { value: 'phone', label: 'Telefon' }, { value: 'email', label: 'Email' }, { value: 'title', label: 'Titel' }, { value: 'department', label: 'Afdeling' } ] : [ { value: 'note', label: 'Firma note' }, { value: 'mobile_phone', label: 'Mobil' }, { value: 'phone', label: 'Telefon' }, { value: 'email', label: 'Email' }, { value: 'address', label: 'Adresse' }, { value: 'invoice_email', label: 'Faktura-email' } ]; wrap.classList.remove('d-none'); label.textContent = target === 'contact' ? 'Kontaktfelt' : 'Firmafelt'; select.innerHTML = options.map(function (item) { return ''; }).join(''); } function openNoteTargetModal(target, noteId) { const modal = getNoteTargetModal(); if (!modal) { return; } const note = noteById(noteId); noteTargetState = { target: String(target || 'case'), noteId: Number(noteId || 0) }; const titleEl = byId('bbNoteTargetModalLabel'); const idLabel = byId('bbNoteTargetIdLabel'); const idInput = byId('bbNoteTargetIdInput'); const textInput = byId('bbNoteTargetTextInput'); const submitBtn = byId('bbNoteTargetSubmitBtn'); if (titleEl) { const targetTitle = noteTargetState.target === 'contact' ? 'kontakt' : (noteTargetState.target === 'customer' ? 'firma' : 'sag-kommentar'); titleEl.innerHTML = 'Indsæt note i ' + escapeHtml(targetTitle); } if (idLabel) { if (noteTargetState.target === 'contact') { idLabel.textContent = 'Kontakt ID'; } else if (noteTargetState.target === 'customer') { idLabel.textContent = 'Firma ID'; } else { idLabel.textContent = 'Sag ID'; } } if (idInput) { const defaultCaseId = resolveQuickNoteCaseId(); const defaultId = noteTargetState.target === 'case' && defaultCaseId > 0 ? defaultCaseId : 0; idInput.value = defaultId > 0 ? String(defaultId) : ''; } if (textInput) { textInput.value = String((note && note.content) || ''); } if (submitBtn) { submitBtn.disabled = false; submitBtn.dataset.target = noteTargetState.target; submitBtn.dataset.noteId = String(noteTargetState.noteId); } renderNoteTargetFieldOptions(noteTargetState.target); updateNoteTargetStatus('Vælg mål og indsæt tekst.', null); modal.show(); } function updateQuickNoteHint(message, isError) { const hint = byId('bbQuickNoteHint'); quickNoteHintState.message = message; quickNoteHintState.level = isError ? 'danger' : 'success'; if (!hint) { return; } hint.textContent = message; hint.classList.remove('text-muted'); hint.classList.toggle('text-danger', !!isError); hint.classList.toggle('text-success', !isError); } function bindHeaderActions() { const searchBtn = byId('bbSearchBtn'); const notificationsBtn = byId('bbNotificationsBtn'); const pauseBtn = byId('bbTimerPauseBtn'); const stopBtn = byId('bbTimerStopBtn'); const switchBtn = byId('bbTimerSwitchBtn'); const timerChip = byId('bbActiveTimerChip'); if (searchBtn) { searchBtn.addEventListener('click', function () { const trigger = byId('globalSearchBtn'); if (trigger) { trigger.click(); } }); } if (notificationsBtn) { notificationsBtn.addEventListener('click', function () { if (latestNotifications.length > 0) { const first = latestNotifications[0] || {}; if (first.action) { window.location.href = first.action; return; } } const trigger = byId('globalRemindersBtn'); if (trigger) { trigger.click(); } }); } if (pauseBtn) { pauseBtn.addEventListener('click', function () { const activeTimer = (((latestSections || {}).timer || {}).active || {}); const own = (((latestSections || {}).timer || {}).own || {}); const paused = Array.isArray(own.paused) ? own.paused : []; if (activeTimer.active) { pauseActiveTimer() .then(fetchBottomBarState) .then(applyState) .catch(function (err) { console.warn('Failed pausing timer', err); }); return; } const pausedTimeId = Number((((paused[0] || {}).time_entry_id) || ((paused[0] || {}).id) || 0)); resumeTimer(pausedTimeId || null) .then(fetchBottomBarState) .then(applyState) .catch(function (err) { console.warn('Failed resuming timer', err); }); }); } if (stopBtn) { stopBtn.addEventListener('click', function () { stopActiveTimer() .then(fetchBottomBarState) .then(applyState) .catch(function (err) { console.warn('Failed stopping timer', err); }); }); } if (switchBtn) { switchBtn.addEventListener('click', function () { openSwitchCaseModal(); }); } if (timerChip) { timerChip.addEventListener('click', function () { window.location.href = '/timetracking'; }); } document.addEventListener('click', function (e) { const actionBtn = e.target && e.target.closest('[data-bb-create]'); if (!actionBtn) return; const actionKey = actionBtn.getAttribute('data-bb-create'); const actions = (latestContextActions.context || []).concat(latestContextActions.global || []); const matched = actions.find(function (item) { return item.id === actionKey; }); if (actionKey === 'new_case') { const quickBtn = byId('quickCreateBtn'); if (quickBtn) { quickBtn.click(); return; } } if (matched && matched.action) { window.location.href = matched.action; } }); } function bindDynamicActions() { document.addEventListener('click', function (e) { const target = e.target; const btn = target && target.closest('button'); if (!btn) return; if (btn.id === 'bbNoteClearBtn') { noteEditorState = { editingId: 0, title: '', content: '' }; renderTabPanel(); return; } if (btn.id === 'bbNoteSaveBtn') { const titleInput = byId('bbNoteTitleInput'); const contentInput = byId('bbNoteContentInput'); const title = titleInput ? String(titleInput.value || '').trim() : ''; const content = contentInput ? String(contentInput.value || '').trim() : ''; if (!content) { const detail = byId('bbCountDetail'); if (detail) { detail.innerHTML = ' Noten er tom.'; } return; } const editId = Number(btn.getAttribute('data-note-edit-id') || 0); const action = editId > 0 ? updateUserNote(editId, { title: title, content: content }) : createUserNote(title, content); action .then(fetchBottomBarState) .then(function (data) { noteEditorState = { editingId: 0, title: '', content: '' }; applyState(data); const detail = byId('bbCountDetail'); if (detail) { detail.innerHTML = ' Note gemt.'; } }) .catch(function (err) { console.warn('Failed saving note', err); const detail = byId('bbCountDetail'); if (detail) { detail.innerHTML = ' ' + escapeHtml((err && err.message) ? err.message : 'Kunne ikke gemme note.'); } }); return; } const noteEditId = Number(btn.getAttribute('data-note-edit') || 0); if (noteEditId > 0) { const note = noteById(noteEditId); noteEditorState = { editingId: noteEditId, title: String((note && note.title) || ''), content: String((note && note.content) || '') }; renderTabPanel(); return; } const notePinId = Number(btn.getAttribute('data-note-pin') || 0); if (notePinId > 0) { const note = noteById(notePinId); const pinned = !!(note && note.is_pinned); updateUserNote(notePinId, { is_pinned: !pinned }) .then(fetchBottomBarState) .then(applyState) .catch(function (err) { console.warn('Failed pin toggle', err); }); return; } const noteDeleteId = Number(btn.getAttribute('data-note-delete') || 0); if (noteDeleteId > 0) { if (!window.confirm('Slet note permanent fra din liste?')) { return; } deleteUserNote(noteDeleteId) .then(fetchBottomBarState) .then(function (data) { if (Number(noteEditorState.editingId || 0) === noteDeleteId) { noteEditorState = { editingId: 0, title: '', content: '' }; } applyState(data); }) .catch(function (err) { console.warn('Failed deleting note', err); }); return; } const noteToCaseId = Number(btn.getAttribute('data-note-to-case') || 0); if (noteToCaseId > 0) { openNoteTargetModal('case', noteToCaseId); return; } const noteToContactId = Number(btn.getAttribute('data-note-to-contact') || 0); if (noteToContactId > 0) { openNoteTargetModal('contact', noteToContactId); return; } const noteToCustomerId = Number(btn.getAttribute('data-note-to-customer') || 0); if (noteToCustomerId > 0) { openNoteTargetModal('customer', noteToCustomerId); return; } if (btn.id === 'bbNoteTargetSubmitBtn') { const target = String(btn.dataset.target || 'case'); const noteId = Number(btn.dataset.noteId || 0); const targetId = Number((byId('bbNoteTargetIdInput') || {}).value || 0); const field = String(((byId('bbNoteTargetFieldSelect') || {}).value) || '').trim(); const text = String(((byId('bbNoteTargetTextInput') || {}).value) || '').trim(); if (!(noteId > 0)) { updateNoteTargetStatus('Mangler note-id.', true); return; } if (!(targetId > 0)) { updateNoteTargetStatus('Mål-ID skal være et tal større end 0.', true); return; } if (!text) { updateNoteTargetStatus('Tekstfeltet er tomt.', true); return; } btn.disabled = true; updateNoteTargetStatus('Gemmer...', null); let action; if (target === 'contact') { action = noteToContact(noteId, targetId, field || 'mobile', text, 'append'); } else if (target === 'customer') { action = noteToCustomer(noteId, targetId, field || 'note', text, 'append'); } else { action = noteToCaseComment(noteId, targetId, text); } action .then(function () { const detail = byId('bbCountDetail'); if (detail) { const targetLabel = target === 'contact' ? 'kontakt' : (target === 'customer' ? 'firma' : 'sag'); detail.innerHTML = ' Note-data gemt på ' + targetLabel + ' #' + targetId; } updateNoteTargetStatus('Gemt.', false); const modal = getNoteTargetModal(); if (modal) { modal.hide(); } }) .catch(function (err) { console.warn('Failed note target insert', err); updateNoteTargetStatus((err && err.message) ? err.message : 'Kunne ikke gemme note-data.', true); }) .finally(function () { btn.disabled = false; }); return; } const switchAction = btn.getAttribute('data-bb-switch-action'); if (switchAction) { if (switchAction === 'continue-unchanged') { switchCaseState.decision = 'unchanged'; switchCaseStatusMessage('Timer fortsætter uændret. Du kan åbne en sag uden at starte ny timer.'); return; } if (switchAction === 'pause-now') { pauseActiveTimer().then(function () { switchCaseState.decision = 'pause'; switchCaseState.activeTimer = null; switchCaseStatusMessage('Timer sat på pause. Du kan nu starte ny timer.'); return loadSwitchCaseData(); }).catch(function (err) { switchCaseStatusMessage('' + escapeHtml(err.message || 'Kunne ikke pause timer.')); }); return; } if (switchAction === 'stop-now') { stopActiveTimer().then(function () { switchCaseState.decision = 'stop'; switchCaseState.activeTimer = null; switchCaseStatusMessage('Aktiv timer stoppet. Du kan nu starte ny timer.'); return loadSwitchCaseData(); }).catch(function () { switchCaseStatusMessage('Kunne ikke stoppe timer.'); }); return; } } const openCaseId = Number(btn.getAttribute('data-bb-open-case') || 0); if (openCaseId > 0) { openCaseDetail(openCaseId); return; } const startCaseId = Number(btn.getAttribute('data-bb-start-case') || 0); if (startCaseId > 0) { startTimerForCase(startCaseId); return; } const stopTimeId = Number(btn.getAttribute('data-bb-stop-time') || 0); if (stopTimeId > 0) { fetch('/api/v1/timetracking/time/stop', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ time_id: stopTimeId }) }) .then(fetchBottomBarState) .then(applyState) .catch(function (err) { console.warn('Failed stopping timer from list', err); }); return; } if (btn.id === 'bbQuickNoteSaveBtn') { const input = byId('bbQuickNoteInput'); const value = input ? String(input.value || '').trim() : ''; quickNoteDraft = value; if (!value) { updateQuickNoteHint('Skriv en note først.', true); return; } const caseId = resolveQuickNoteCaseId(); if (!(caseId > 0)) { updateQuickNoteHint('Åbn en sag (eller start timer på en sag) før du gemmer quick note.', true); return; } const originalHtml = btn.innerHTML; btn.disabled = true; btn.innerHTML = 'Gemmer...'; saveQuickNote(value, caseId) .then(function () { if (input) { input.value = ''; } quickNoteDraft = ''; updateQuickNoteHint('Gemte note på sag #' + caseId + '.', false); const detail = byId('bbCountDetail'); if (detail) { detail.innerHTML = ' Quick note gemt på sag #' + caseId; } }) .catch(function (err) { updateQuickNoteHint((err && err.message) ? err.message : 'Kunne ikke gemme note.', true); }) .finally(function () { btn.disabled = false; btn.innerHTML = originalHtml; }); return; } const bossAction = btn.getAttribute('data-boss-action'); if (bossAction) { if (bossAction === 'assign_next_to_owner') { const ownerId = Number(btn.getAttribute('data-owner-id') || 0); if (ownerId <= 0) { return; } const originalHtml = btn.innerHTML; btn.disabled = true; btn.innerHTML = 'Tildeler...'; fetch('/api/v1/bottom-bar/boss/assign-next-to-user', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assignee_user_id: ownerId }) }) .then(async r => { const body = await r.json().catch(() => ({})); if (!r.ok) { const detail = body && body.detail ? body.detail : 'Kunne ikke tildele næste sag'; throw new Error(typeof detail === 'string' ? detail : 'Kunne ikke tildele næste sag'); } return body; }) .then(data => { const detail = byId('bbCountDetail'); if (detail) { if (data.status === 'assigned' && data.case) { detail.innerHTML = ' Tildelt: ' + escapeHtml(data.case.title || 'Sag') + ' til tekniker.'; } else { detail.innerHTML = ' ' + escapeHtml(data.message || 'Ingen sager at tildele'); } } return fetchBottomBarState(); }) .then(applyState) .catch(err => { console.error('Assign next to owner failed', err); const detail = byId('bbCountDetail'); if (detail) { detail.innerHTML = ' ' + escapeHtml(err.message || 'Fejl ved tildeling'); } }) .finally(() => { btn.disabled = false; btn.innerHTML = originalHtml; }); return; } if (bossAction === 'auto_assign_next') { const originalHtml = btn.innerHTML; btn.disabled = true; btn.innerHTML = 'Fordeler...'; fetch('/api/v1/bottom-bar/boss/auto-assign-next', { method: 'POST', credentials: 'include' }) .then(async r => { const body = await r.json().catch(() => ({})); if (!r.ok) { const detail = body && body.detail ? body.detail : 'Kunne ikke auto-fordele sag'; throw new Error(typeof detail === 'string' ? detail : 'Kunne ikke auto-fordele sag'); } return body; }) .then(data => { const detail = byId('bbCountDetail'); if (detail) { if (data.status === 'assigned' && data.case && data.assignee) { detail.innerHTML = ' Auto-fordelt: ' + escapeHtml(data.case.title || 'Sag') + ' → ' + escapeHtml(data.assignee.name || 'medarbejder'); } else { detail.innerHTML = ' ' + escapeHtml(data.message || 'Ingen sager at fordele'); } } return fetchBottomBarState(); }) .then(applyState) .catch(err => { console.error('Boss auto-assign failed', err); const detail = byId('bbCountDetail'); if (detail) { detail.innerHTML = ' ' + escapeHtml(err.message || 'Fejl ved auto-fordeling'); } }) .finally(() => { btn.disabled = false; btn.innerHTML = originalHtml; }); return; } if (bossAction === 'open_unassigned') { openUnassignedCasesPanel(); return; } if (bossAction === 'open_escalations') { window.location.href = '/sag?priority=urgent'; return; } if (bossAction === 'open_team') { window.location.href = '/timetracking'; return; } if (bossAction === 'open_owner') { const ownerId = Number(btn.getAttribute('data-owner-id') || 0); window.location.href = ownerId > 0 ? ('/sag?ansvarlig=' + ownerId) : '/sag'; return; } if (bossAction === 'open_case') { const caseId = Number(btn.getAttribute('data-case-id') || 0); window.location.href = caseId > 0 ? ('/sag/' + caseId) : '/sag'; return; } } if (btn.id === 'btnNextTask') { console.log("-> Beder backend om næste opgave..."); btn.innerHTML = ' Omsætter kalender og SLA...'; btn.disabled = true; fetch('/api/v1/bottom-bar/next_task', { method: 'POST', credentials: 'include' }) .then(r => { if (!r.ok) { throw new Error('Kunne ikke hente næste opgave'); } return r.json(); }) .then(data => { const task = data && data.task ? data.task : {}; const taskTitle = task.title || 'Ingen opgave fundet'; const caseId = task.case_id || '-'; const freeMins = data && data.free_time_calculated ? data.free_time_calculated : 0; btn.innerHTML = 'Du fik tildelt: ' + taskTitle + ' (Sag #' + caseId + ') ' + freeMins + 'm fri'; btn.classList.add('btn-success'); btn.classList.remove('btn-primary'); }) .catch(err => { console.error("Fejl:", err); btn.innerHTML = "Fejl - prøv igen"; btn.disabled = false; }); } if (btn.id === 'btnSendMsg') { const input = document.getElementById('chatInputQuick'); const recipientObj = document.getElementById('chatRecipient'); if (input && input.value.trim() !== '') { const recipient = recipientObj ? recipientObj.options[recipientObj.selectedIndex].text : 'Alle'; console.log("-> Sender besked til", recipient, ":", input.value); const msgVal = input.value; input.value = ''; const msgContainer = document.createElement('div'); msgContainer.className = 'mb-2 text-end'; msgContainer.innerHTML = '
Til: ' + escapeHtml(recipient) + '
Mig: ' + escapeHtml(msgVal) + '
'; const chatContainer = document.querySelector('#bbTabInnerContent .d-flex.flex-column.h-100'); if (!chatContainer) { return; } const listUl = chatContainer.querySelector('ul.bb-tab-list'); if (!listUl) { return; } listUl.appendChild(msgContainer); // Simple hacky scroll down const tabInner = document.getElementById('bbTabInnerContent'); if(tabInner) { tabInner.scrollTop = tabInner.scrollHeight + 500; } } } }); document.addEventListener('keydown', function (e) { if (e.key !== 'Enter') { return; } const input = byId('bbQuickNoteInput'); if (!input || document.activeElement !== input) { return; } e.preventDefault(); const saveBtn = byId('bbQuickNoteSaveBtn'); if (saveBtn) { saveBtn.click(); } }); document.addEventListener('input', function (e) { const target = e.target; if (!target || target.id !== 'bbQuickNoteInput') { if (target && target.id === 'bbNoteTitleInput') { noteEditorState.title = String(target.value || ''); } if (target && target.id === 'bbNoteContentInput') { noteEditorState.content = String(target.value || ''); } return; } quickNoteDraft = String(target.value || ''); if (quickNoteHintState.level !== 'muted') { quickNoteHintState = { message: 'Tip: gemmer som kommentar på aktiv/åben sag.', level: 'muted' }; const hint = byId('bbQuickNoteHint'); if (hint) { hint.textContent = quickNoteHintState.message; hint.classList.remove('text-danger', 'text-success'); hint.classList.add('text-muted'); } } }); } document.addEventListener('DOMContentLoaded', function () { activeKey = 'overview'; // Default overview state bindChipClicks(); bindChipHoverPreview(); bindSheetToggle(); bindHeaderActions(); bindDynamicActions(); bindSideTabs(); startPollingFallback(); connectRealtime(); }); })();