(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('');
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();
});
})();