425 lines
24 KiB
JavaScript
425 lines
24 KiB
JavaScript
/* V5 composes the established V4 controls without copying their implementations. */
|
||
(() => {
|
||
'use strict';
|
||
const config = JSON.parse(document.getElementById('caseV5Data').textContent);
|
||
const byId = id => document.getElementById(id);
|
||
const shell = document.querySelector('.case-detail-page-shell');
|
||
shell.classList.add('case-v5');
|
||
const billingHost = document.createElement('div');
|
||
billingHost.className='col-12';
|
||
billingHost.innerHTML='<label for="v5QuickBilling" class="form-label small">Afregning</label><select id="v5QuickBilling" class="form-select form-select-sm" disabled><option>Indlæser afregning…</option></select><div id="v5BillingHint" class="form-text">Kladder kontrolleres af en chef før afregning.</div>';
|
||
byId('commentQuickTimeRow').querySelector('.row').append(billingHost);
|
||
const compactBillingHost=billingHost.cloneNode(true);
|
||
compactBillingHost.querySelector('label').htmlFor='v5CompactBilling';
|
||
compactBillingHost.querySelector('select').id='v5CompactBilling';
|
||
compactBillingHost.querySelector('.form-text').removeAttribute('id');
|
||
byId('caseQuickTimeDraftForm').querySelector('.row').insertBefore(compactBillingHost,byId('caseQuickTimeSaveButton').parentElement);
|
||
byId('caseQuickTimeSaveButton').innerHTML='<i class="bi bi-plus-lg me-1"></i>Gem tid';
|
||
function setAutomaticRegistrationControls(workflowActive = false) {
|
||
const text=workflowActive?'Afregning vælges automatisk':'Tid gemmes som kladde. Afregning vælges i Tidsregistrering.';
|
||
for(const select of [byId('v5QuickBilling'),byId('v5CompactBilling'),byId('time_billing_method'),byId('quickTimeBillingMethod')].filter(Boolean)) {
|
||
select.replaceChildren(new Option('Automatisk / kladde','draft'));
|
||
select.value='draft';select.disabled=true;select.hidden=true;select.dataset.billingReady='true';
|
||
const label=select.parentElement.querySelector(`label[for="${select.id}"]`);if(label)label.hidden=true;
|
||
}
|
||
for(const host of [billingHost,compactBillingHost])host.querySelector('.form-text').textContent=text;
|
||
for(const id of ['time_internal','sol_time_internal']){const checkbox=byId(id);if(checkbox){checkbox.checked=false;checkbox.disabled=true;const wrapper=checkbox.closest('.form-check');if(wrapper)wrapper.hidden=true;}}
|
||
}
|
||
setAutomaticRegistrationControls();
|
||
async function loadBillingOptions() {
|
||
try {
|
||
const response=await fetch(`/api/v1/timetracking/cases/${config.caseId}/billing-options`,{credentials:'include'});
|
||
const data=await response.json();
|
||
if(!response.ok)throw new Error(data.detail || 'Afregningsmuligheder kunne ikke hentes');
|
||
setAutomaticRegistrationControls(data.workflow_active===true);
|
||
const anchor=byId('timeV1Date');
|
||
if(anchor&&!byId('v5TabBilling')) {
|
||
const host=document.createElement('div');host.className='col-12';
|
||
const label=document.createElement('label');label.htmlFor='v5TabBilling';label.className='form-label small';label.textContent='Afregning (valgfrit)';
|
||
const select=document.createElement('select');select.id='v5TabBilling';select.className='form-select form-select-sm';
|
||
select.add(new Option('Kladde / automatisk','draft'));
|
||
if(data.workflow_active!==true){if(data.has_customer)select.add(new Option('Faktura','invoice'));(data.prepaid_cards||[]).forEach(c=>select.add(new Option('Klippekort '+(c.card_number||c.id),'card_'+c.id)));(data.agreements||[]).forEach(a=>select.add(new Option('Fastpris '+(a.agreement_number||a.id),'fpa_'+a.id)));select.add(new Option('Intern tid','internal'));select.add(new Option('Ikke fakturerbar','non_billable'));}
|
||
const hint=document.createElement('div');hint.className='form-text';hint.textContent=data.workflow_active?'Afregningen vælges af workflows. Brug afregningsbeslutningen nedenfor for en begrundet ændring.':'Manuel afregning kan kun vælges her i Tidsregistrering.';
|
||
host.append(label,select,hint);anchor.closest('.row')?.append(host);
|
||
}
|
||
} catch(error){byId('v5BillingHint').textContent='Tid kan stadig registreres som kladde. Afregningsmuligheder kunne ikke hentes.';}
|
||
}
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',loadBillingOptions,{once:true});else loadBillingOptions();
|
||
const deferredInput = byId('topbarDeferredInput');
|
||
const deadlineInput = byId('topbarDeadlineInput');
|
||
function syncDateLimits() {
|
||
if (deadlineInput.value) deferredInput.max = deadlineInput.value;
|
||
else deferredInput.removeAttribute('max');
|
||
if (deferredInput.value) deadlineInput.min = deferredInput.value;
|
||
else deadlineInput.removeAttribute('min');
|
||
}
|
||
[deferredInput, deadlineInput].forEach(input => input.addEventListener('change', event => {
|
||
if (deferredInput.value && deadlineInput.value && deferredInput.value > deadlineInput.value) {
|
||
event.stopImmediatePropagation();
|
||
input.value = input.defaultValue;
|
||
showCaseFeedback('Udsat må ikke ligge efter deadline', 'danger');
|
||
}
|
||
syncDateLimits();
|
||
}, true));
|
||
syncDateLimits();
|
||
const board = document.createElement('div');
|
||
board.className = 'v5-board';
|
||
const facts = document.createElement('aside');
|
||
facts.className = 'v5-case-facts';
|
||
facts.setAttribute('aria-label', 'Kunde og sagsoplysninger');
|
||
const center = document.createElement('main');
|
||
const modules = document.createElement('aside');
|
||
modules.className = 'v5-case-modules';
|
||
modules.setAttribute('aria-label', 'Todo, filer og moduler');
|
||
const tabs = byId('caseTabs');
|
||
tabs.before(board);
|
||
board.append(facts, center, modules);
|
||
const widgets = [...shell.querySelectorAll('.case-v4-widget-dashboard .row > div > .card')];
|
||
[3, 1, 0].forEach(index => { if (widgets[index]) facts.append(widgets[index]); });
|
||
const factsDisclosure = document.createElement('details');
|
||
factsDisclosure.className = 'v5-facts-disclosure';
|
||
const factsSummary = document.createElement('summary');
|
||
factsSummary.textContent = 'Kunde, kontakt og sagsoplysninger';
|
||
const factsContent = document.createElement('div');
|
||
factsContent.className = 'v5-facts-content';
|
||
factsContent.append(...facts.children);
|
||
factsDisclosure.append(factsSummary, factsContent);
|
||
facts.append(factsDisclosure);
|
||
const compactScreen = window.matchMedia('(max-width: 1400px)');
|
||
function adaptFacts(event) { factsDisclosure.open = !event.matches; }
|
||
adaptFacts(compactScreen);
|
||
compactScreen.addEventListener('change', adaptFacts);
|
||
const anyDeskButton = document.createElement('button');
|
||
anyDeskButton.id = 'v5AnyDeskOpenButton';
|
||
anyDeskButton.type = 'button';
|
||
anyDeskButton.className = 'v5-anydesk-button';
|
||
anyDeskButton.title = 'Forbind med AnyDesk';
|
||
anyDeskButton.innerHTML = '<i class="bi bi-display" aria-hidden="true"></i><span>AnyDesk</span>';
|
||
anyDeskButton.addEventListener('click', () => window.openCaseAnyDeskModal());
|
||
shell.querySelector('.case-header-identity')?.append(anyDeskButton);
|
||
const heroStatus = document.createElement('div');
|
||
heroStatus.className = 'v5-hero-status';
|
||
['topbarStatusSelect', 'topbarPrioritySelect'].forEach(id => {
|
||
const row = byId(id)?.parentElement;
|
||
if (row) heroStatus.append(row);
|
||
});
|
||
shell.querySelector('.case-header-main').insertBefore(heroStatus, shell.querySelector('.case-header-todo'));
|
||
const heroTodo = shell.querySelector('.case-header-todo');
|
||
if (heroTodo) {
|
||
const addTodo = document.createElement('button');
|
||
addTodo.type = 'button';
|
||
addTodo.className = 'v5-hero-add-todo';
|
||
addTodo.title = 'Opret todo på sagen';
|
||
addTodo.setAttribute('aria-label', 'Opret todo på sagen');
|
||
addTodo.innerHTML = '<i class="bi bi-plus-lg" aria-hidden="true"></i>';
|
||
addTodo.addEventListener('click', () => window.openRelTodoModal(config.caseId, byId('sag-titel-text')?.textContent.trim() || ''));
|
||
heroTodo.append(addTodo);
|
||
}
|
||
const timeline = byId('topbarStartDateInput')?.closest('.card');
|
||
if (timeline) {
|
||
timeline.classList.add('v5-hero-timeline');
|
||
shell.querySelector('.case-header-main').append(timeline);
|
||
}
|
||
center.append(tabs, byId('timeActiveBanner'), byId('caseTabsContent'));
|
||
// Normalize legacy closing tags: browsers can parse later panes outside
|
||
// their tab-content. Every V5 pane belongs to the same center column.
|
||
tabs.querySelectorAll('[data-bs-target]').forEach(button => {
|
||
const pane = document.querySelector(button.getAttribute('data-bs-target'));
|
||
if (pane && pane.parentElement !== byId('caseTabsContent')) byId('caseTabsContent').append(pane);
|
||
});
|
||
const planner = byId('planner');
|
||
if (planner) { planner.hidden = true; planner.style.setProperty('display', 'none', 'important'); }
|
||
byId('planner-tab')?.closest('.nav-item')?.remove();
|
||
// Keep planner markup for existing dialogs, but never expose its pane in V5.
|
||
if (new URLSearchParams(location.search).get('tab') === 'planner') {
|
||
const url = new URL(location.href);
|
||
url.searchParams.set('tab', 'details');
|
||
history.replaceState(null, '', url);
|
||
}
|
||
const details = byId('details');
|
||
const left = byId('case-left-column');
|
||
const right = byId('case-right-column');
|
||
const detailsRow = document.createElement('div');
|
||
detailsRow.className = 'case-v4-details-columns';
|
||
details.prepend(detailsRow);
|
||
detailsRow.append(left);
|
||
modules.append(right);
|
||
const rightGrid = right.querySelector('.right-modules-grid');
|
||
['todo-steps', 'files', 'hardware', 'time'].reverse().forEach(key => {
|
||
const card = shell.querySelector(`.right-module-card[data-module="${key}"]`);
|
||
if (card) rightGrid.prepend(card);
|
||
});
|
||
const relations = shell.querySelector('.right-module-card[data-module="relations"]');
|
||
if (relations) {
|
||
relations.classList.add('v5-center-relations');
|
||
const commentsCard = byId('comment-form').closest('.left-module-card');
|
||
commentsCard.after(relations);
|
||
}
|
||
// V4 refreshes this hook after async module loads. Keep its columns in V5.
|
||
window.ensureCaseDetailsColumns = () => detailsRow;
|
||
window.updateRightColumnVisibility = () => right.classList.remove('d-none');
|
||
const originalApplyLayout = window.applyViewLayout;
|
||
window.applyViewLayout = function (...args) {
|
||
originalApplyLayout(...args);
|
||
right.classList.remove('d-none');
|
||
};
|
||
|
||
const actions = [
|
||
['assign', 'Tildel sag', 'person-check', null],
|
||
['reply_customer', 'Svar kunden', 'envelope', 'commentActionReplyCustomer'],
|
||
['log_time', 'Registrer tid', 'clock-history', 'commentActionLogTime'],
|
||
['solution', 'Gem som løsning', 'lightbulb', 'commentActionSolution'],
|
||
['close_case', 'Luk sag', 'check-circle', 'commentActionCloseCase'],
|
||
['await_customer', 'Afventer kunde', 'hourglass-split', 'commentActionAwaitCustomer'],
|
||
['append_case', 'Tilføj til sag', 'file-earmark-plus', 'commentActionAppendCase']
|
||
];
|
||
const form = byId('comment-form');
|
||
const legacyChips = form.querySelector('.comment-action-chips');
|
||
legacyChips.before(byId('caseV5ComposerTools').content.cloneNode(true));
|
||
byId('v5SelectedActions').parentElement.append(form.querySelector('button[type="submit"]'));
|
||
let assignSelected = false;
|
||
let groupId = config.groupId;
|
||
let userId = config.userId;
|
||
let members = [];
|
||
let memberRequest = 0;
|
||
let membersLoading = false;
|
||
const memberCache = new Map();
|
||
const disabledBeforeLock = new Map();
|
||
let busy = false;
|
||
let pending = null;
|
||
const feedback = (message, error = false) => {
|
||
const el = byId('v5SubmitFeedback');
|
||
el.textContent = message;
|
||
el.classList.toggle('text-danger', error);
|
||
};
|
||
const selected = action => action[0] === 'assign' ? assignSelected : Boolean(byId(action[3])?.checked);
|
||
function renderActions() {
|
||
const host = byId('v5SelectedActions');
|
||
host.replaceChildren();
|
||
actions.forEach(action => {
|
||
const active = selected(action);
|
||
byId(`v5Option-${action[0]}`).setAttribute('aria-pressed', String(active));
|
||
if (!active) return;
|
||
const chip = document.createElement('button');
|
||
chip.type = 'button';
|
||
let label = action[1];
|
||
if (action[0] === 'assign') {
|
||
const target = members.find(member => member.user_id === userId)?.display_name
|
||
|| config.groups.find(group => group.id === groupId)?.name || 'Vælg modtager';
|
||
label = `Tildel: ${target}`;
|
||
}
|
||
if (action[0] === 'log_time') label = `Tid: ${byId('commentQuickTimeMinutes').value || '0'} min`;
|
||
chip.textContent = `${label} ×`;
|
||
chip.setAttribute('aria-label', `Fjern ${action[1]}`);
|
||
chip.addEventListener('click', () => toggleAction(action));
|
||
chip.disabled = busy || Boolean(pending);
|
||
host.append(chip);
|
||
});
|
||
byId('v5AssignmentFields').hidden = !assignSelected;
|
||
form.querySelector('button[type="submit"]').textContent = pending ? 'Fortsæt resterende handlinger' : assignSelected ? 'Gem og tildel' : actions.some(selected) ? 'Gem med handlinger' : 'Send';
|
||
window.CaseCommandComposer?.updateSubmitLabel();
|
||
}
|
||
function toggleAction(action) {
|
||
if (busy || pending) return;
|
||
if (action[0] === 'assign') {
|
||
assignSelected = !assignSelected;
|
||
if (assignSelected && groupId) loadMembers();
|
||
} else {
|
||
const checkbox = byId(action[3]);
|
||
checkbox.checked = !checkbox.checked;
|
||
checkbox.dispatchEvent(new Event('change', {bubbles: true}));
|
||
}
|
||
renderActions();
|
||
closeMenu();
|
||
}
|
||
actions.forEach(action => {
|
||
const button = document.createElement('button');
|
||
button.id = `v5Option-${action[0]}`;
|
||
button.type = 'button';
|
||
button.innerHTML = `<i class="bi bi-${action[2]} me-1"></i>`;
|
||
button.append(document.createTextNode(action[1]));
|
||
button.addEventListener('click', () => toggleAction(action));
|
||
byId('v5ActionOptions').append(button);
|
||
byId(action[3])?.addEventListener('change', renderActions);
|
||
});
|
||
const toggle = byId('v5ActionToggle');
|
||
function closeMenu() { byId('v5ActionMenu').hidden = true; toggle.setAttribute('aria-expanded', 'false'); }
|
||
toggle.addEventListener('click', () => {
|
||
const menu = byId('v5ActionMenu');
|
||
menu.hidden = !menu.hidden;
|
||
toggle.setAttribute('aria-expanded', String(!menu.hidden));
|
||
});
|
||
document.addEventListener('click', event => {
|
||
if (!byId('v5ActionMenu').contains(event.target) && !toggle.contains(event.target)) closeMenu();
|
||
});
|
||
form.addEventListener('keydown', event => { if (event.key === 'Escape') { closeMenu(); toggle.focus(); } });
|
||
function renderPicker(host, items, current, choose, search) {
|
||
host.replaceChildren();
|
||
const filtered = items.filter(item => item.name.toLocaleLowerCase('da').includes(search.toLocaleLowerCase('da')));
|
||
filtered.forEach(item => {
|
||
const button = document.createElement('button');
|
||
button.type = 'button'; button.textContent = item.name;
|
||
button.setAttribute('aria-pressed', String(item.id === current));
|
||
button.disabled = busy || Boolean(pending);
|
||
button.addEventListener('click', () => choose(item.id));
|
||
host.append(button);
|
||
});
|
||
if (!filtered.length) host.textContent = 'Ingen match';
|
||
}
|
||
function renderGroups() {
|
||
renderPicker(byId('v5GroupOptions'), config.groups, groupId, id => {
|
||
groupId = id; userId = null; members = []; renderGroups(); loadMembers();
|
||
}, byId('v5GroupSearch').value);
|
||
}
|
||
function renderUsers() {
|
||
const items = [{id: null, name: 'Kun gruppen'}, ...members.map(member => ({id: member.user_id, name: member.display_name}))];
|
||
renderPicker(byId('v5UserOptions'), items, userId, id => { userId = id; renderUsers(); }, byId('v5UserSearch').value);
|
||
renderActions();
|
||
}
|
||
async function loadMembers() {
|
||
const requestedGroupId = groupId;
|
||
const sequence = ++memberRequest;
|
||
membersLoading = true;
|
||
byId('v5UserSearch').disabled = true;
|
||
byId('v5UserOptions').textContent = 'Henter medarbejdere…';
|
||
try {
|
||
let data = memberCache.get(requestedGroupId);
|
||
if (!data) {
|
||
const response = await fetch(`/api/v1/sag/${config.caseId}/assignments?group_id=${requestedGroupId}`, {credentials: 'include'});
|
||
if (!response.ok) throw new Error('Kunne ikke hente gruppens medarbejdere. Vælg gruppen igen for at prøve igen.');
|
||
data = await response.json();
|
||
memberCache.set(requestedGroupId, data);
|
||
}
|
||
if (sequence !== memberRequest) return;
|
||
members = data.members || [];
|
||
if (!members.some(member => member.user_id === userId)) userId = null;
|
||
byId('v5UserSearch').disabled = false;
|
||
byId('v5UserSearch').placeholder = 'Find medarbejder…';
|
||
renderUsers(); feedback('');
|
||
} catch (error) {
|
||
if (sequence === memberRequest) { byId('v5UserOptions').textContent = ''; feedback(error.message, true); }
|
||
} finally { if (sequence === memberRequest) membersLoading = false; }
|
||
}
|
||
byId('v5GroupSearch').addEventListener('input', renderGroups);
|
||
byId('v5UserSearch').addEventListener('input', renderUsers);
|
||
byId('commentQuickTimeMinutes').addEventListener('input', renderActions);
|
||
form.querySelector('textarea[name="indhold"]').addEventListener('input', renderActions);
|
||
function lock(locked) {
|
||
form.querySelectorAll('input, textarea, select, button').forEach(el => {
|
||
if (locked) {
|
||
if (!disabledBeforeLock.has(el)) disabledBeforeLock.set(el, el.disabled);
|
||
el.disabled = true;
|
||
} else if (disabledBeforeLock.has(el)) {
|
||
el.disabled = disabledBeforeLock.get(el);
|
||
disabledBeforeLock.delete(el);
|
||
}
|
||
});
|
||
// Failed submissions remain locked except for retry, avoiding an altered
|
||
// draft being attached to an already persisted comment.
|
||
if (pending && !busy) {
|
||
form.querySelector('button[type="submit"]').disabled = false;
|
||
if (pending.remaining.includes('log_time')) {
|
||
byId('commentQuickTimeRow').querySelectorAll('input, select').forEach(el => { el.disabled = false; });
|
||
toggleCommentQuickTimeStartField();
|
||
}
|
||
}
|
||
}
|
||
form.querySelector('textarea[name="indhold"]').addEventListener('keydown', event => {
|
||
if (event.key !== 'Enter' || (!event.metaKey && !event.ctrlKey) || event.isComposing) return;
|
||
event.preventDefault();
|
||
event.stopImmediatePropagation();
|
||
const submit = form.querySelector('button[type="submit"]');
|
||
if (!busy && submit && !submit.disabled) form.requestSubmit(submit);
|
||
});
|
||
form.querySelector('button[type="submit"]').title = 'Gem kommentar · Cmd+Enter / Ctrl+Enter';
|
||
form.removeAttribute('onsubmit');
|
||
form.addEventListener('submit', async event => {
|
||
event.preventDefault();
|
||
if (busy) return;
|
||
const containsCommand = /^\s*\//m.test(form.querySelector('textarea[name="indhold"]').value);
|
||
if (containsCommand && !window.CaseCommandComposer) {
|
||
feedback('Kommandofeltet indlæses. Vent et øjeblik og prøv igen.', true);
|
||
return;
|
||
}
|
||
if (window.CaseCommandComposer?.hasCommands()) {
|
||
if (pending || assignSelected || getSelectedCommentActions().length) {
|
||
feedback('Fjern handlingerne fra plusmenuen, eller skriv dem som kommandoer, før du gemmer.', true);
|
||
return;
|
||
}
|
||
busy = true; lock(true); feedback('Gemmer kommentar og kommandoer…');
|
||
try {
|
||
const result = await window.CaseCommandComposer.submit();
|
||
feedback(result.message);
|
||
window.dispatchEvent(new CustomEvent('bmc:timer-state-changed'));
|
||
if (result.reload) reloadCasePreservingContext();
|
||
else {
|
||
if (typeof loadTimeTrackingTab === 'function') loadTimeTrackingTab();
|
||
if (typeof loadTodoSteps === 'function') loadTodoSteps();
|
||
}
|
||
} catch (error) { feedback(error.message, true); }
|
||
finally {
|
||
busy = false; lock(false); renderActions();
|
||
window.CaseCommandComposer.updateSubmitLabel();
|
||
}
|
||
return;
|
||
}
|
||
const textarea = form.querySelector('textarea[name="indhold"]');
|
||
const content = textarea.value.trim();
|
||
const reason = byId('v5AssignmentReason').value.trim();
|
||
const chosen = getSelectedCommentActions();
|
||
if (!pending) {
|
||
if (!content) { feedback('Skriv en kommentar først.', true); return; }
|
||
if (assignSelected && (!groupId || !reason || membersLoading || byId('v5UserSearch').disabled)) {
|
||
feedback('Vælg gruppe, vent på medarbejderlisten og skriv en begrundelse.', true); return;
|
||
}
|
||
if (chosen.includes('log_time') && byId('v5QuickBilling').dataset.billingReady !== 'true') { feedback('Afregningsmuligheder er ikke indlæst. Genindlæs siden.', true); return; }
|
||
if (chosen.includes('log_time') && (Number(byId('commentQuickTimeMinutes').value) <= 0 || !byId('commentQuickTimeDate').value)) {
|
||
feedback('Vælg dato og et positivt minuttal før tidsregistrering.', true); return;
|
||
}
|
||
if (chosen.includes('close_case')) {
|
||
busy = true; lock(true);
|
||
let confirmed = false;
|
||
try { confirmed = await confirmCaseClosureIfNeeded({timeWillBeRegistered: chosen.includes('log_time')}); }
|
||
finally { busy = false; lock(false); }
|
||
if (!confirmed) return;
|
||
}
|
||
}
|
||
busy = true; lock(true); feedback('Gemmer…');
|
||
try {
|
||
if (!pending) {
|
||
const url = `/api/v1/sag/${config.caseId}/${assignSelected ? 'assign-with-reason' : 'kommentarer'}`;
|
||
const internal = assignSelected || chosen.includes('solution') || chosen.includes('log_time');
|
||
const body = assignSelected ? {group_id: groupId, primary_user_id: userId, reason, content} : {indhold: content, er_intern: internal};
|
||
const response = await fetch(url, {method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)});
|
||
const result = await response.json();
|
||
if (!response.ok) throw new Error(typeof result.detail === 'string' ? result.detail : 'Kunne ikke gemme kommentaren.');
|
||
const comment = assignSelected ? result.comment : result;
|
||
pending = {content, remaining: chosen.slice(), assigned: assignSelected};
|
||
let visibleContent = comment.indhold || content;
|
||
if (assignSelected) {
|
||
const decoder = document.createElement('textarea');
|
||
decoder.innerHTML = visibleContent;
|
||
visibleContent = decoder.value;
|
||
}
|
||
injectCreatedComment(comment, visibleContent, internal);
|
||
}
|
||
// Finish one action at a time. Retry never creates another comment
|
||
// or repeats actions that already returned successfully.
|
||
while (pending.remaining.length) {
|
||
await applyCommentActionPrefill(pending.remaining[0], pending.content);
|
||
pending.remaining.shift();
|
||
}
|
||
pending = null;
|
||
textarea.value = ''; byId('v5AssignmentReason').value = '';
|
||
assignSelected = false; clearSelectedCommentActions();
|
||
feedback('Kommentar og handlinger gemt.');
|
||
window.CaseCommandComposer?.reset();
|
||
reloadCasePreservingContext();
|
||
} catch (error) {
|
||
feedback(pending ? `Kommentaren er gemt. En handling mangler: ${error.message}. Brug Fortsæt for at prøve den igen.` : error.message, true);
|
||
} finally { busy = false; lock(Boolean(pending)); renderActions(); }
|
||
});
|
||
renderGroups(); renderActions();
|
||
applyCommentFilters();
|
||
})();
|