diff --git a/migrations/1016_contact_merge_history.sql b/migrations/1016_contact_merge_history.sql
new file mode 100644
index 0000000..9e42d31
--- /dev/null
+++ b/migrations/1016_contact_merge_history.sql
@@ -0,0 +1,14 @@
+CREATE TABLE IF NOT EXISTS contact_merge_history (
+ id BIGSERIAL PRIMARY KEY,
+ target_contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE RESTRICT,
+ source_contact_id INTEGER NOT NULL,
+ source_snapshot JSONB NOT NULL,
+ moved_relations JSONB NOT NULL DEFAULT '{}'::jsonb,
+ merged_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_contact_merge_history_target
+ ON contact_merge_history(target_contact_id, merged_at DESC);
+
+COMMENT ON TABLE contact_merge_history IS
+ 'Audit trail for destructive contact merges. Source contact snapshots and moved relation counts are retained.';
diff --git a/static/js/bottom-bar.js b/static/js/bottom-bar.js
index f02721c..0e416ac 100644
--- a/static/js/bottom-bar.js
+++ b/static/js/bottom-bar.js
@@ -2,6 +2,7 @@
let latestSections = {};
let latestContextActions = { global: [], context: [] };
let activeKey = 'timer';
+ let timerPanelState = { scope: 'mine', loading: false, loaded: false, rows: [], error: '' };
let overviewFilter = null;
let ws = null;
let pollTimer = null;
@@ -17,7 +18,7 @@
let switchCaseState = {
activeTimer: null,
decision: 'unchanged',
- timers: { active: [], paused: [] },
+ timers: { active: [], paused: [], stopped: [] },
recentCases: [],
unassignedCases: []
};
@@ -855,10 +856,10 @@
if (key === 'overview') {
if (overviewFilter === 'urgent') return urgent.list ? urgent.list.map(u => '
Hastesag: ' + esc(u.title) + '
') : ['Ingen hastesager.'];
- if (overviewFilter === 'drift') return drift.list ? drift.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 === 'drift') return drift.list ? drift.list.map(k => '
📉 ' + esc(k) + ' Håndter i Drift ') : ['Alle systemer oppe.'];
+ if (overviewFilter === 'eset') return eset.list ? eset.list.map(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 === 'mail') return ['
📧
' + mail.unread + ' ulæste mails.
💬
' + mail.customer_reply_needed + ' kræver kundesvar.
Åbn indbakke'];
if (overviewFilter === 'unassigned') return unassigned.list ? unassigned.list.map(u => '
' + esc(u.title || ('Sag #' + (u.id || ''))) + '
') : ['Ingen åbne sager uden ansvarlig.'];
let out = [];
@@ -871,6 +872,15 @@
if (out.length === 0) {
out.push('
🎉 Alt ser grønt ud! Intet kritisk lige nu.
');
}
+
+ const contextActions = (latestContextActions.context || []);
+ const globalActions = (latestContextActions.global || []);
+ const smartActions = contextActions.length ? contextActions : globalActions;
+ if (smartActions.length) {
+ out.push('
Smarte handlinger
' + smartActions.map(function (action) {
+ return '';
+ }).join('') + '
');
+ }
// Add quick note button on overview
out.push('
');
@@ -882,10 +892,10 @@
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 '
' + esc(t.desc) + '
Aktiv tid · ' + esc(elapsedText) + '
';
});
}
- return ['Ingen aktive timere lige nu.'];
+ return ['
Ingen aktiv timerBrug “Skift sag” i bundlinjen for at starte arbejdet.
'];
}
if (key === 'messages') {
@@ -929,9 +939,9 @@
if (key === 'tasks') {
if (tasks.count > 0) {
- return (tasks.list || []).map(t => '
' + esc(t.title) + ' ' + esc(t.deadline) + '
');
+ return (tasks.list || []).map(t => '
' + esc(t.title) + 'Prioritet: ' + esc(t.deadline) + '
Aktuel');
}
- return ['Ingen aktuelle opgaver.'];
+ return ['
Du er ajourDer er ingen aktuelle opgaver eller påmindelser.
'];
}
if (key === 'notes') {
@@ -1153,8 +1163,12 @@
const timerChip = byId('bbActiveTimerChip');
const timerText = byId('bbActiveTimerText');
const notifCount = byId('bbNotificationsCount');
+ const pauseBtn = byId('bbTimerPauseBtn');
+ const stopBtn = byId('bbTimerStopBtn');
const timer = ((latestSections || {}).timer || {}).active || {};
+ const ownTimers = ((latestSections || {}).timer || {}).own || {};
+ const hasPausedTimer = Array.isArray(ownTimers.paused) && ownTimers.paused.length > 0;
const hasActiveTimer = !!timer.active;
if (timerChip && timerText) {
timerChip.classList.toggle('is-hidden', !hasActiveTimer);
@@ -1170,6 +1184,15 @@
const computed = Number(latestNotificationCount || 0) + unreadMessages;
notifCount.textContent = String(computed);
}
+ if (pauseBtn) {
+ pauseBtn.disabled = !hasActiveTimer && !hasPausedTimer;
+ pauseBtn.title = hasActiveTimer ? 'Pause timer' : (hasPausedTimer ? 'Genoptag senest pausede timer' : 'Ingen timer at pause');
+ pauseBtn.innerHTML = hasActiveTimer ? '
' : '
';
+ }
+ if (stopBtn) {
+ stopBtn.disabled = !hasActiveTimer;
+ stopBtn.title = hasActiveTimer ? 'Stop timer' : 'Ingen aktiv timer';
+ }
}
function renderTabPanel() {
@@ -1193,6 +1216,7 @@
}
const titleText = titleContainer.querySelector('.bb-tab-title-text');
+ const descriptionEl = byId('bbTabDescription');
const titleByKey = {
overview: 'Overblik',
@@ -1212,12 +1236,24 @@
boss: 'bi-person-workspace'
};
+ const descriptionByKey = {
+ overview: 'Det vigtigste samlet ét sted.',
+ timer: 'Se aktiv tid, stop registreringen eller skift direkte til en anden sag.',
+ messages: 'Interne samtaler samlet efter modtager med tydelig læst-status.',
+ tasks: 'Prioritér næste handling ud fra deadlines og aktuelle påmindelser.',
+ notes: 'Skriv hurtigt til venstre og genbrug dine noter fra arkivet til højre.',
+ boss: 'Fordel supportkøen ud fra kapacitet, hast og ventetid.'
+ };
+
const activeTitle = titleByKey[activeKey] || 'Info';
if (titleText) {
titleText.textContent = activeTitle;
} else {
titleContainer.textContent = activeTitle;
}
+ if (descriptionEl) {
+ descriptionEl.textContent = descriptionByKey[activeKey] || '';
+ }
const iconSpan = titleContainer.querySelector('.bi');
if (iconSpan) {
@@ -1228,6 +1264,7 @@
const lines = listFor(activeKey, latestSections);
const ul = document.createElement('ul');
ul.className = 'bb-tab-list';
+ ul.classList.add('bb-panel-' + activeKey);
lines.forEach(function (line) {
const li = document.createElement('li');
@@ -1237,12 +1274,17 @@
});
innerContent.innerHTML = '';
+
+ if (activeKey === 'timer') {
+ renderTimerWorkQueue(innerContent);
+ return;
+ }
// 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 = '
';
+ topBar.innerHTML = '
';
innerContent.appendChild(topBar);
}
if (activeKey === 'messages') {
@@ -1393,6 +1435,105 @@
}
+ function timerStateLabel(state) {
+ if (state === 'active') return ['Aktiv', 'success'];
+ if (state === 'paused') return ['Pauset', 'warning'];
+ return ['Klar til registrering', 'info'];
+ }
+
+ function formatTimerSeconds(value) {
+ const seconds = Math.max(0, Number(value || 0));
+ return [Math.floor(seconds / 3600), Math.floor((seconds % 3600) / 60), Math.floor(seconds % 60)]
+ .map(function (part) { return String(part).padStart(2, '0'); }).join(':');
+ }
+
+ function renderTimerWorkQueue(container) {
+ const scope = timerPanelState.scope;
+ const toolbar = document.createElement('div');
+ toolbar.className = 'd-flex justify-content-between align-items-center gap-2 mb-3';
+ toolbar.innerHTML = '
' +
+ '' +
+ '
' +
+ '
' + timerPanelState.rows.length + ' vist';
+ container.appendChild(toolbar);
+
+ if (timerPanelState.loading || !timerPanelState.loaded) {
+ const loading = document.createElement('div');
+ loading.className = 'bb-panel-empty';
+ loading.innerHTML = '
Henter timere...
';
+ container.appendChild(loading);
+ if (!timerPanelState.loading) loadTimerWorkQueue(scope);
+ return;
+ }
+
+ if (timerPanelState.error) {
+ const errorBox = document.createElement('div');
+ errorBox.className = 'alert alert-warning d-flex justify-content-between align-items-center gap-3';
+ errorBox.innerHTML = '
' + escapeHtml(timerPanelState.error) + '' +
+ '
';
+ container.appendChild(errorBox);
+ }
+
+ const list = document.createElement('div');
+ list.className = 'bb-timer-work-grid';
+ if (!timerPanelState.rows.length) {
+ list.innerHTML = '
';
+ } else {
+ list.innerHTML = timerPanelState.rows.map(function (row) {
+ const state = String(row.timer_state || 'pending_conversion');
+ const label = timerStateLabel(state);
+ const timeId = Number(row.id || row.time_entry_id || 0);
+ const sagId = Number(row.sag_id || 0);
+ const isOwn = row.is_own_timer === true;
+ let action = '';
+ if (isOwn && state === 'paused') action = '
';
+ if (isOwn && state === 'pending_conversion') action = '
';
+ if (isOwn && state === 'active') action = '
';
+ const elapsedValue = row.elapsed_hhmmss || formatTimerSeconds(row.live_elapsed_seconds || row.elapsed_seconds || row.elapsed);
+ const elapsed = state === 'active' ? '
' + escapeHtml(elapsedValue) + ' · ' : '';
+ const employeeName = row.employee_display_name || row.medarbejder_navn || row.user_name || (isOwn ? 'Min timer' : 'Ukendt medarbejder');
+ return '
' + label[0] + '' + escapeHtml(row.sag_navn || ('Sag #' + sagId)) + 'Sag #' + sagId + '' + elapsed + escapeHtml(employeeName) + '
' + action + '
';
+ }).join('');
+ }
+ container.appendChild(list);
+ }
+
+ function ownTimerRows(payload) {
+ const groups = normalizeSwitchableTimerPayload(payload);
+ return groups.active.map(function (row) {
+ return Object.assign({}, row, { timer_state: 'active', is_own_timer: true });
+ }).concat(groups.paused.map(function (row) {
+ return Object.assign({}, row, { timer_state: 'paused', is_own_timer: true });
+ }), groups.stopped.map(function (row) {
+ return Object.assign({}, row, { timer_state: 'pending_conversion', is_own_timer: true });
+ }));
+ }
+
+ async function loadTimerWorkQueue(scope) {
+ timerPanelState.scope = scope || 'mine';
+ timerPanelState.loading = true;
+ timerPanelState.error = '';
+ try {
+ if (timerPanelState.scope === 'mine') {
+ const ownTimers = await fetchSwitchableTimers();
+ timerPanelState.rows = ownTimerRows(ownTimers);
+ switchCaseState.timers.stopped = timerPanelState.rows.filter(function (row) { return row.timer_state === 'pending_conversion'; });
+ } else {
+ const response = await fetch('/api/v1/timetracking/time/team-status?scope=all', { credentials: 'include' });
+ if (!response.ok) throw new Error('Kunne ikke hente alle medarbejderes timere');
+ const payload = await response.json();
+ timerPanelState.rows = Array.isArray(payload.rows) ? payload.rows : [];
+ }
+ timerPanelState.loaded = true;
+ } catch (err) {
+ timerPanelState.error = err && err.message ? err.message : 'Kunne ikke hente timeroversigten';
+ timerPanelState.loaded = true;
+ } finally {
+ timerPanelState.loading = false;
+ if (activeKey === 'timer') renderTabPanel();
+ }
+ }
+
function bindSideTabs() {
const buttons = document.querySelectorAll('.bb-tab-btn');
for (let i = 0; i < buttons.length; i++) {
@@ -1680,14 +1821,15 @@
}
function normalizeSwitchableTimerPayload(payload) {
- const out = { active: [], paused: [] };
+ const out = { active: [], paused: [], stopped: [] };
if (!payload || typeof payload !== 'object') {
return out;
}
- if (Array.isArray(payload.active) || Array.isArray(payload.paused)) {
+ if (Array.isArray(payload.active) || Array.isArray(payload.paused) || Array.isArray(payload.stopped)) {
out.active = Array.isArray(payload.active) ? payload.active : [];
out.paused = Array.isArray(payload.paused) ? payload.paused : [];
+ out.stopped = Array.isArray(payload.stopped) ? payload.stopped : [];
return out;
}
@@ -1762,6 +1904,32 @@
return sagId > 0 ? ('Sag #' + sagId) : 'Ukendt sag';
}
+ function timerDurationLabel(timer) {
+ let seconds = Number((timer && (timer.live_elapsed_seconds || timer.elapsed_seconds || timer.elapsed)) || 0);
+ if (!seconds && timer && timer.start_tid && timer.slut_tid) {
+ const start = new Date(timer.start_tid).getTime();
+ const end = new Date(timer.slut_tid).getTime();
+ if (Number.isFinite(start) && Number.isFinite(end)) {
+ seconds = Math.max(0, Math.floor((end - start) / 1000) - Number(timer.pause_total_seconds || 0));
+ }
+ }
+ if (!seconds && timer && timer.faktisk_tid_min) seconds = Number(timer.faktisk_tid_min) * 60;
+ return formatTimerSeconds(seconds);
+ }
+
+ function timerCaseInfo(timer, includeDuration) {
+ const sagId = Number((timer && timer.sag_id) || 0);
+ const customer = String((timer && timer.customer_name) || 'Ingen kunde');
+ const contact = String((timer && timer.contact_name) || 'Ingen kontakt');
+ const status = String((timer && timer.case_status) || 'Ukendt status');
+ return '
' + timerDisplayName(timer) + '' +
+ '
#' + sagId + '' +
+ '
' + escapeHtml(customer) + '' +
+ '
' + escapeHtml(contact) + '' +
+ '
' + escapeHtml(status) + '' +
+ (includeDuration ? '
' + timerDurationLabel(timer) + '' : '');
+ }
+
function renderSwitchCaseLists() {
const timersEl = byId('bbSwitchTimersList');
const recentEl = byId('bbSwitchRecentCasesList');
@@ -1772,43 +1940,54 @@
const active = Array.isArray(switchCaseState.timers.active) ? switchCaseState.timers.active : [];
const paused = Array.isArray(switchCaseState.timers.paused) ? switchCaseState.timers.paused : [];
+ const stopped = Array.isArray(switchCaseState.timers.stopped) ? switchCaseState.timers.stopped : [];
const recentCases = Array.isArray(switchCaseState.recentCases) ? switchCaseState.recentCases : [];
const unassignedCases = Array.isArray(switchCaseState.unassignedCases) ? switchCaseState.unassignedCases : [];
const showUnassigned = unassignedCases.length > 0;
+ const searchEl = byId('bbSwitchCaseSearch');
+ const query = String((searchEl && searchEl.value) || '').trim().toLocaleLowerCase('da-DK');
if (actionsEl) {
actionsEl.classList.toggle('d-none', !switchCaseState.activeTimer);
}
- if (!active.length && !paused.length) {
- timersEl.innerHTML = '
Ingen aktive eller pausede timere.
';
+ if (!active.length && !paused.length && !stopped.length) {
+ timersEl.innerHTML = '
Ingen tidligere timere at fortsætte.
';
} 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 + '
';
- }
+ '
Aktiv' + timerCaseInfo(t, true) + '
' +
+ '
';
});
paused.forEach(function (t) {
+ const timeId = Number((t && (t.time_entry_id || t.id)) || 0);
timerItems +=
- '
' +
- '
Pauset' + timerDisplayName(t) + '
' +
- '
' +
- '
';
+ '
Pauset' + timerCaseInfo(t, true) + '
' +
+ '
' +
+ '
';
+ });
+
+ stopped.forEach(function (t) {
+ const sagId = Number((t && t.sag_id) || 0);
+ const timeId = Number((t && (t.time_entry_id || t.id)) || 0);
+ timerItems +=
+ '
' +
+ '
' + timerCaseInfo(t, true) + '
' +
+ '
' +
+ '
';
});
timersEl.innerHTML = timerItems;
}
- const sourceCases = showUnassigned ? unassignedCases : recentCases;
+ const sourceCases = (showUnassigned ? unassignedCases : recentCases).filter(function (row) {
+ if (!query) return true;
+ const searchable = String((row && (row.sag_id || row.id)) || '') + ' ' + String((row && (row.titel || row.title)) || '');
+ return searchable.toLocaleLowerCase('da-DK').includes(query);
+ });
const titleEl = byId('bbSwitchCaseModalLabel');
if (titleEl) {
titleEl.innerHTML = showUnassigned
@@ -1817,26 +1996,94 @@
}
if (!sourceCases.length) {
- recentEl.innerHTML = '
Ingen sager at vise.
';
+ recentEl.innerHTML = '
' + (query ? 'Ingen sager matcher din søgning.' : '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';
+ const meta = showUnassigned ? '
Uden ansvarlig' : 'Senest anvendt';
return (
- '
' +
- '
' + prefix + title + '
' +
- '
' +
- '
' +
- '
' +
+ '
' +
+ '
' + title + '
Sag #' + caseId + ' · ' + meta + '
' +
+ '
' +
+ '' +
+ '' +
'
' +
'
'
);
}).join('');
}
+ async function openTimeConversion(timeId) {
+ const timer = (switchCaseState.timers.stopped || []).concat(timerPanelState.rows || []).find(function (row) {
+ return Number((row && (row.time_entry_id || row.id)) || 0) === Number(timeId);
+ });
+ if (!timer) return;
+
+ const panel = byId('bbConvertTimePanel');
+ const select = byId('bbConvertBillingMethod');
+ if (!panel || !select) return;
+ panel.dataset.timeId = String(timeId);
+ byId('bbConvertTimeName').textContent = String((timer.sag_navn || timer.title) || ('Sag #' + (timer.sag_id || '')));
+ byId('bbConvertWorkType').value = timer.work_type || 'support';
+ byId('bbConvertMinutes').value = Number(timer.fakturerbar_tid_min != null ? timer.fakturerbar_tid_min : (timer.faktisk_tid_min || 0));
+ select.innerHTML = '
';
+ panel.classList.remove('d-none');
+
+ try {
+ const response = await fetch('/api/v1/timetracking/time/' + Number(timeId) + '/settlement-options', { credentials: 'include' });
+ if (!response.ok) throw new Error('Kunne ikke hente afregningsmuligheder');
+ const options = await response.json();
+ const recommended = options.recommended || { method: 'invoice' };
+ let html = '
';
+ (options.prepaid_cards || []).forEach(function (card) {
+ html += '
';
+ });
+ (options.agreements || []).forEach(function (agreement) {
+ html += '
';
+ });
+ html += '
';
+ select.innerHTML = html;
+ const recommendedId = recommended.prepaid_card_id || recommended.fixed_price_agreement_id;
+ select.value = recommended.method + (recommendedId ? ':' + recommendedId : '');
+ byId('bbConvertRecommendation').textContent = 'Foreslået: ' + (recommended.reason || 'Faktura');
+ } catch (err) {
+ select.innerHTML = '
';
+ byId('bbConvertRecommendation').textContent = err.message || 'Kunne ikke hente forslag.';
+ }
+ }
+
+ async function submitTimeConversion() {
+ const panel = byId('bbConvertTimePanel');
+ const button = byId('bbConvertSubmit');
+ const timeId = Number((panel && panel.dataset.timeId) || 0);
+ if (!timeId || !button) return;
+ const selected = String(byId('bbConvertBillingMethod').value || 'invoice').split(':');
+ const payload = {
+ billing_method: selected[0],
+ work_type: byId('bbConvertWorkType').value,
+ fakturerbar_tid_min: Math.max(0, Number(byId('bbConvertMinutes').value || 0)),
+ entry_type: 'manuel'
+ };
+ if (selected[0] === 'prepaid') payload.prepaid_card_id = Number(selected[1]);
+ if (selected[0] === 'subscription') payload.fixed_price_agreement_id = Number(selected[1]);
+ button.disabled = true;
+ try {
+ const response = await fetch('/api/v1/timetracking/time/' + timeId + '/approve', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
+ const result = await response.json().catch(function () { return {}; });
+ if (!response.ok) throw new Error(result.detail || 'Kunne ikke konvertere tiden');
+ panel.classList.add('d-none');
+ await loadSwitchCaseData();
+ if (activeKey === 'timer') await loadTimerWorkQueue(timerPanelState.scope);
+ } catch (err) {
+ switchCaseStatusMessage('
' + escapeHtml(err.message || 'Kunne ikke konvertere tiden.'));
+ } finally {
+ button.disabled = false;
+ }
+ }
+
async function loadSwitchCaseData(options) {
const opts = options || {};
switchCaseState.decision = 'unchanged';
@@ -1845,7 +2092,7 @@
: null;
switchCaseState.unassignedCases = [];
switchCaseState.recentCases = [];
- switchCaseState.timers = { active: [], paused: [] };
+ switchCaseState.timers = { active: [], paused: [], stopped: [] };
if (opts.onlyUnassigned) {
const unassigned = (((latestSections || {}).unassigned || {}).list || []);
@@ -1937,7 +2184,7 @@
if (modal) {
modal.hide();
}
- window.location.href = '/sag/' + validCaseId;
+ window.location.href = '/sag/' + validCaseId + '/v3';
} catch (err) {
switchCaseStatusMessage('
' + escapeHtml(err.message || 'Kunne ikke starte timer for sag.'));
}
@@ -1952,11 +2199,41 @@
if (modal) {
modal.hide();
}
- window.location.href = '/sag/' + validCaseId;
+ window.location.href = '/sag/' + validCaseId + '/v3';
+ }
+
+ function executeBottomBarAction(action) {
+ if (!action) return false;
+ const command = String(action.command || '');
+ if (command.indexOf('case_add:') === 0) {
+ const caseAction = command.slice('case_add:'.length);
+ if (typeof window.openCaseModuleAddPanel === 'function' && typeof window.openCaseAddAction === 'function') {
+ Promise.resolve(window.openCaseModuleAddPanel()).then(function () {
+ return window.openCaseAddAction(caseAction);
+ });
+ setExpanded(false);
+ return true;
+ }
+ }
+ if (command === 'switch_timer') {
+ openSwitchCaseModal();
+ return true;
+ }
+ if (command === 'open_notes') {
+ activeKey = 'notes';
+ setExpanded(true);
+ renderTabPanel();
+ return true;
+ }
+ if (action.action) {
+ window.location.href = action.action;
+ return true;
+ }
+ return false;
}
function resolveQuickNoteCaseId() {
- const match = (window.location.pathname || '').match(/^\/sag\/(\d+)$/);
+ const match = (window.location.pathname || '').match(/^\/sag\/(\d+)(?:\/v3)?\/?$/);
if (match && match[1]) {
return Number(match[1]);
}
@@ -2319,13 +2596,6 @@
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();
@@ -2378,7 +2648,9 @@
if (timerChip) {
timerChip.addEventListener('click', function () {
- window.location.href = '/timetracking';
+ const timer = (((latestSections || {}).timer || {}).active || {});
+ const sagId = Number(timer.sag_id || 0);
+ window.location.href = sagId > 0 ? ('/sag/' + sagId) : '/timetracking';
});
}
@@ -2397,15 +2669,26 @@
return;
}
}
- if (matched && matched.action) {
- window.location.href = matched.action;
- }
+ executeBottomBarAction(matched);
});
}
function bindDynamicActions() {
document.addEventListener('click', function (e) {
const target = e.target;
+ const timerScopeButton = target && target.closest('[data-bb-timer-scope]');
+ if (timerScopeButton) {
+ timerPanelState.loaded = false;
+ loadTimerWorkQueue(timerScopeButton.getAttribute('data-bb-timer-scope') || 'mine');
+ renderTabPanel();
+ return;
+ }
+ const caseLink = target && target.closest('[data-bb-case-link]');
+ if (caseLink && !target.closest('button, a, input, select, textarea')) {
+ const sagId = Number(caseLink.getAttribute('data-bb-case-link') || 0);
+ if (sagId > 0) openCaseDetail(sagId);
+ return;
+ }
const btn = target && target.closest('button');
if (!btn) return;
@@ -2661,12 +2944,47 @@
}
}
+ if (btn.hasAttribute('data-bb-cancel-convert')) {
+ const panel = byId('bbConvertTimePanel');
+ if (panel) panel.classList.add('d-none');
+ return;
+ }
+
+ if (btn.id === 'bbConvertSubmit') {
+ submitTimeConversion();
+ return;
+ }
+
+ const convertTimeId = Number(btn.getAttribute('data-bb-convert-time') || 0);
+ if (convertTimeId > 0) {
+ openTimeConversion(convertTimeId);
+ return;
+ }
+
const openCaseId = Number(btn.getAttribute('data-bb-open-case') || 0);
if (openCaseId > 0) {
openCaseDetail(openCaseId);
return;
}
+ const resumeTimeId = Number(btn.getAttribute('data-bb-resume-time') || 0);
+ if (resumeTimeId > 0) {
+ btn.disabled = true;
+ switchCaseStatusMessage('
Genoptager timer...');
+ resumeTimer(resumeTimeId)
+ .then(fetchBottomBarState)
+ .then(function (state) {
+ applyState(state);
+ switchCaseStatusMessage('
Timeren er genoptaget.');
+ return loadSwitchCaseData();
+ })
+ .catch(function (err) {
+ btn.disabled = false;
+ switchCaseStatusMessage('
' + escapeHtml(err.message || 'Kunne ikke genoptage timer.'));
+ });
+ return;
+ }
+
const startCaseId = Number(btn.getAttribute('data-bb-start-case') || 0);
if (startCaseId > 0) {
startTimerForCase(startCaseId);
@@ -2878,7 +3196,7 @@
}
if (bossAction === 'open_case') {
const caseId = Number(btn.getAttribute('data-case-id') || 0);
- window.location.href = caseId > 0 ? ('/sag/' + caseId) : '/sag';
+ window.location.href = caseId > 0 ? ('/sag/' + caseId + '/v3') : '/sag';
return;
}
}
@@ -2998,6 +3316,10 @@
document.addEventListener('input', function (e) {
const target = e.target;
+ if (target && target.id === 'bbSwitchCaseSearch') {
+ renderSwitchCaseLists();
+ return;
+ }
if (!target || target.id !== 'bbQuickNoteInput') {
if (target && target.id === 'bbNoteTitleInput') {
noteEditorState.title = String(target.value || '');
diff --git a/static/js/task-template-selector.js b/static/js/task-template-selector.js
index 2e87bb4..2374193 100644
--- a/static/js/task-template-selector.js
+++ b/static/js/task-template-selector.js
@@ -32,7 +32,7 @@
@@ -40,9 +40,9 @@
@@ -54,7 +54,7 @@
-
+
@@ -65,7 +65,7 @@
@@ -74,15 +74,15 @@
@@ -100,10 +100,10 @@
-
Seneste template-koersler paa sagen
+ Seneste template-kørsler på sagen
-
Ingen template-koersler endnu.
+
Ingen template-kørsler endnu.