2026-10-03 14:06:51 +02:00
/* 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' ) ;
2026-10-08 08:58:02 +02:00
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' ;
2026-10-08 21:36:42 +02:00
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 ( ) ;
2026-10-08 23:06:55 +02:00
const previewFields = [
{ host : compactBillingHost , minutes : 'caseQuickTimeMinutes' , description : 'caseQuickTimeDescription' , type : 'caseQuickTimeType' } ,
{ host : billingHost , minutes : 'commentQuickTimeMinutes' , description : 'commentText' , type : 'commentQuickTimeType' }
] ;
let previewTimer ;
async function refreshBillingPreview ( ) {
const names = { subscription : 'Fastpris' , prepaid : 'Klippekort' , invoice : 'Faktura' , internal : 'Intern tid' , non _billable : 'Ikke fakturerbar' , draft : 'Kladde' } ;
await Promise . all ( previewFields . map ( async fields => {
const sequence = ( fields . sequence || 0 ) + 1 ; fields . sequence = sequence ;
const hint = fields . host . querySelector ( '.form-text' ) ;
const params = new URLSearchParams ( { minutes : Math . max ( 1 , Number ( byId ( fields . minutes ) ? . value ) || 30 ) , description : ( byId ( fields . description ) || document . querySelector ( 'textarea[name=indhold]' ) ) ? . value || '' , time _type : byId ( fields . type ) ? . value || 'support' } ) ;
try {
const response = await fetch ( ` /api/v1/timetracking/cases/ ${ config . caseId } /billing-preview? ${ params } ` , { credentials : 'include' } ) ;
const data = await response . json ( ) ; if ( ! response . ok ) throw new Error ( 'Preview failed' ) ;
if ( sequence !== fields . sequence ) return ;
hint . textContent = 'Forslag: ' + ( data . status === 'manual_review' ? 'Manuel vurdering' : names [ data . method ] || 'Manuel vurdering' ) ;
} catch ( error ) { if ( sequence === fields . sequence ) hint . textContent = 'Afregningsforslag kunne ikke beregnes.' ; }
} ) ) ;
}
function scheduleBillingPreview ( ) { clearTimeout ( previewTimer ) ; previewTimer = setTimeout ( refreshBillingPreview , 250 ) ; }
for ( const fields of previewFields ) for ( const id of [ fields . minutes , fields . description , fields . type ] ) {
const input = byId ( id ) || ( id === 'commentText' ? document . querySelector ( 'textarea[name=indhold]' ) : null ) ; if ( input ) { input . addEventListener ( 'input' , scheduleBillingPreview ) ; input . addEventListener ( 'change' , scheduleBillingPreview ) ; input . addEventListener ( 'focus' , scheduleBillingPreview ) ; }
}
const tagList = byId ( 'case-tags-module' ) ;
if ( tagList ) new MutationObserver ( scheduleBillingPreview ) . observe ( tagList , { childList : true , subtree : true } ) ;
2026-10-08 08:58:02 +02:00
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' ) ;
2026-10-08 21:36:42 +02:00
setAutomaticRegistrationControls ( data . workflow _active === true ) ;
2026-10-08 23:06:55 +02:00
await refreshBillingPreview ( ) ;
2026-10-08 21:36:42 +02:00
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 ) ;
2026-10-08 08:58:02 +02:00
}
2026-10-08 21:36:42 +02:00
} catch ( error ) { byId ( 'v5BillingHint' ) . textContent = 'Tid kan stadig registreres som kladde. Afregningsmuligheder kunne ikke hentes.' ; }
2026-10-08 08:58:02 +02:00
}
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 ( ) ;
2026-10-03 14:06:51 +02:00
const board = document . createElement ( 'div' ) ;
board . className = 'v5-board' ;
const center = document . createElement ( 'main' ) ;
const modules = document . createElement ( 'aside' ) ;
modules . className = 'v5-case-modules' ;
2026-10-08 08:58:02 +02:00
modules . setAttribute ( 'aria-label' , 'Todo, filer og moduler' ) ;
2026-10-03 14:06:51 +02:00
const tabs = byId ( 'caseTabs' ) ;
tabs . before ( board ) ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
board . append ( center , modules ) ;
2026-10-03 14:06:51 +02:00
const widgets = [ ... shell . querySelectorAll ( '.case-v4-widget-dashboard .row > div > .card' ) ] ;
2026-11-03 01:16:12 +01:00
// Show current facts directly; reuse the established controls when editing.
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
const factsMenu = document . createElement ( 'nav' ) ;
factsMenu . className = 'v5-hero-facts-menu' ;
factsMenu . setAttribute ( 'aria-label' , 'Kontakt, tildeling og klassifikation' ) ;
2026-11-03 01:16:12 +01:00
function heroFactValue ( index ) {
const selected = id => byId ( id ) ? . selectedOptions ? . [ 0 ] ? . textContent . trim ( ) || '' ;
if ( index === 3 ) {
const card = widgets [ 3 ] ;
return [ card ? . querySelector ( '.contact-widget-company .contact-widget-value' ) ? . textContent . trim ( ) ,
card ? . querySelector ( '.contact-widget-primary .contact-widget-value' ) ? . textContent . trim ( ) ] . filter ( Boolean ) . join ( ' · ' ) || 'Vælg kontakt' ;
}
if ( index === 1 ) return [ selected ( 'tabsAssignmentUserSelect' ) , selected ( 'tabsAssignmentGroupSelect' ) ] . filter ( Boolean ) . join ( ' · ' ) || 'Ingen tildeling' ;
return selected ( 'topbarTypeSelect' ) || 'Vælg type' ;
}
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
[ [ 3 , 'Kontakt' , 'person-vcard' ] , [ 1 , 'Tildeling' , 'people' ] , [ 0 , 'Klassifikation' , 'tags' ] ] . forEach ( ( [ index , label , icon ] ) => {
const card = widgets [ index ] ;
if ( ! card ) return ;
const button = document . createElement ( 'button' ) ;
button . type = 'button' ;
button . id = ` v5HeroFacts ${ index } ` ;
2026-11-03 01:16:12 +01:00
button . className = 'v5-hero-facts-button' ;
button . setAttribute ( 'aria-haspopup' , 'dialog' ) ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
button . setAttribute ( 'aria-controls' , ` v5HeroFactsPanel ${ index } ` ) ;
2026-11-03 01:16:12 +01:00
button . innerHTML = ` <i class="bi bi- ${ icon } " aria-hidden="true"></i><span class="v5-fact-copy"><span class="v5-fact-label"> ${ label } </span><span class="v5-fact-value"></span></span><i class="bi bi-pencil v5-fact-edit" aria-hidden="true"></i> ` ;
const value = button . querySelector ( '.v5-fact-value' ) ;
const sync = ( ) => { value . textContent = heroFactValue ( index ) ; button . setAttribute ( 'aria-label' , ` ${ label } : ${ value . textContent } . Klik for at ændre ` ) ; } ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
const panel = document . createElement ( 'div' ) ;
panel . id = ` v5HeroFactsPanel ${ index } ` ;
2026-11-03 01:16:12 +01:00
panel . className = 'modal v5-facts-editor case-v5' ;
panel . tabIndex = - 1 ;
panel . setAttribute ( 'aria-hidden' , 'true' ) ;
panel . setAttribute ( 'aria-labelledby' , ` v5FactsTitle ${ index } ` ) ;
panel . innerHTML = ` <div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="v5FactsTitle ${ index } ">Redigér ${ label . toLocaleLowerCase ( 'da-DK' ) } </h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button></div><div class="modal-body v5-case-facts"></div></div></div> ` ;
panel . querySelector ( '.modal-body' ) . append ( card ) ;
document . body . append ( panel ) ;
button . addEventListener ( 'click' , ( ) => bootstrap . Modal . getOrCreateInstance ( panel ) . show ( ) ) ;
panel . addEventListener ( 'hidden.bs.modal' , sync ) ;
panel . addEventListener ( 'change' , sync ) ;
// Close synchronously before an existing control opens its own editor.
card . addEventListener ( 'click' , event => {
if ( event . target . closest ( 'button, a' ) ) bootstrap . Modal . getOrCreateInstance ( panel ) . hide ( ) ;
} , true ) ;
new MutationObserver ( sync ) . observe ( card , { subtree : true , childList : true , characterData : true , attributes : true , attributeFilter : [ 'selected' , 'data-current-type' ] } ) ;
factsMenu . append ( button ) ;
sync ( ) ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
} ) ;
shell . querySelector ( '.case-header-copy' ) . append ( factsMenu ) ;
2026-10-08 08:58:02 +02:00
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 ) ;
2026-10-03 14:06:51 +02:00
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' ) ) ;
2026-10-08 08:58:02 +02:00
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 ) ;
}
2026-10-03 14:06:51 +02:00
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 ) ;
} ) ;
2026-10-20 07:18:57 +02:00
// Keep the case plan primary; existing subcases remain available on demand.
const subcaseForm = byId ( 'casePlannerSubcaseForm' ) ;
const subcaseCard = subcaseForm ? . closest ( '.case-planner-card' ) ;
const plannerBody = byId ( 'casePlannerAllocationForm' ) ? . closest ( '.case-planner-card-body' ) ;
if ( subcaseCard && plannerBody ) {
const subcases = document . createElement ( 'details' ) ;
subcases . className = 'v5-planner-subcases mt-3' ;
const summary = document . createElement ( 'summary' ) ;
summary . textContent = 'Vis undersagers planlægning' ;
subcases . append ( summary ) ;
const calendar = byId ( 'casePlannerSubcaseWeek' ) ? . closest ( '.case-planner-calendar' ) ;
if ( calendar ) subcases . append ( calendar ) ;
const list = byId ( 'casePlannerSubcaseList' ) ;
if ( list ) subcases . append ( list ) ;
plannerBody . append ( subcases ) ;
subcaseCard . remove ( ) ;
}
if ( plannerBody ) {
// The existing form still supplies defaults to the shared booking dialog.
byId ( 'casePlannerAllocationForm' ) . hidden = true ;
const header = plannerBody . previousElementSibling ;
header . querySelector ( 'h5' ) . textContent = 'Hvornår skal sagen løses?' ;
header . querySelector ( '.small' ) . textContent = 'Klik på en dag i kalenderen for at planlægge arbejdet.' ;
const refresh = header . querySelector ( 'button' ) ;
refresh . title = 'Opdatér planen' ;
refresh . setAttribute ( 'aria-label' , 'Opdatér planen' ) ;
const add = document . createElement ( 'button' ) ;
add . type = 'button' ;
add . className = 'btn btn-primary v5-plan-add' ;
add . textContent = '+ Planlæg tid' ;
add . addEventListener ( 'click' , ( ) => {
const today = new Date ( ) ;
const localDate = new Date ( today . getTime ( ) - today . getTimezoneOffset ( ) * 60000 ) . toISOString ( ) . slice ( 0 , 10 ) ;
window . openEmbeddedPlannerSlot ( localDate ) ;
} ) ;
header . insertBefore ( add , refresh ) ;
const metrics = byId ( 'casePlannerEstimated' ) . closest ( '.row' ) ;
const overview = document . createElement ( 'details' ) ;
overview . className = 'v5-planner-overview mt-3' ;
const overviewTitle = document . createElement ( 'summary' ) ;
overviewTitle . textContent = 'Vis forventet og planlagt tid' ;
overview . append ( overviewTitle , metrics ) ;
plannerBody . append ( overview ) ;
const calendar = byId ( 'casePlannerWeek' ) . closest ( '.case-planner-calendar' ) ;
calendar . querySelector ( '.case-planner-calendar-toolbar > span' ) . remove ( ) ;
calendar . querySelector ( 'button:nth-child(2)' ) . textContent = 'Denne uge' ;
const listTitle = document . createElement ( 'h6' ) ;
listTitle . className = 'mt-4 mb-0' ;
listTitle . textContent = 'Aftaler på sagen' ;
byId ( 'casePlannerAllocationList' ) . before ( listTitle ) ;
byId ( 'casePlannerSlotModalTitle' ) . textContent = 'Planlæg tid på sagen' ;
byId ( 'casePlannerSlotSaveBtn' ) . textContent = 'Gem aftale' ;
const duration = byId ( 'casePlannerSlotMinutes' ) ;
duration . closest ( '.col-12' ) . querySelector ( 'label' ) . textContent = 'Hvor lang tid?' ;
const presets = document . createElement ( 'div' ) ;
presets . className = 'd-flex gap-2 mt-2' ;
for ( const [ minutes , label ] of [ [ 30 , '30 min' ] , [ 60 , '1 time' ] , [ 120 , '2 timer' ] ] ) {
const button = document . createElement ( 'button' ) ;
button . type = 'button' ;
button . className = 'btn btn-sm btn-outline-secondary' ;
button . textContent = label ;
button . addEventListener ( 'click' , ( ) => { duration . value = String ( minutes ) ; } ) ;
presets . append ( button ) ;
}
duration . closest ( '.col-12' ) . append ( presets ) ;
const note = byId ( 'casePlannerSlotNote' ) . closest ( '.col-12' ) ;
const noteDetails = document . createElement ( 'details' ) ;
noteDetails . className = 'col-12' ;
const noteTitle = document . createElement ( 'summary' ) ;
noteTitle . textContent = 'Tilføj en note (valgfrit)' ;
note . before ( noteDetails ) ;
noteDetails . append ( noteTitle , note ) ;
const modal = byId ( 'casePlannerSlotModal' ) ;
modal . querySelector ( '.alert' ) . textContent = 'Aftalen reserverer tid i kalenderen. Udført arbejde registreres separat.' ;
modal . querySelector ( '.modal-body > .small' ) . textContent = 'Du får besked, hvis tiden overlapper en anden aftale.' ;
2026-10-03 14:06:51 +02:00
}
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 ) ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
// Keep the existing call action and data; give the narrow sidebar a list layout.
byId ( 'caseCallHistoryRows' ) ? . querySelectorAll ( 'tr' ) . forEach ( row => {
const cells = row . querySelectorAll ( 'td' ) ;
const callButton = cells [ 2 ] ? . querySelector ( 'button' ) ;
if ( callButton ) {
callButton . title = ` Ring til ${ cells [ 2 ] . querySelector ( 'span' ) ? . textContent . trim ( ) || 'nummeret' } ` ;
callButton . setAttribute ( 'aria-label' , callButton . title ) ;
callButton . innerHTML = '<i class="bi bi-telephone-outbound" aria-hidden="true"></i>' ;
}
if ( cells [ 1 ] ) {
const outgoing = cells [ 1 ] . textContent . trim ( ) === 'Udgående' ;
const icon = document . createElement ( 'i' ) ;
icon . className = ` bi bi-arrow- ${ outgoing ? 'up-right' : 'down-left' } ` ;
icon . setAttribute ( 'aria-hidden' , 'true' ) ;
cells [ 1 ] . prepend ( icon ) ;
}
} ) ;
2026-10-03 14:06:51 +02:00
const rightGrid = right . querySelector ( '.right-modules-grid' ) ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
const tagsCard = rightGrid ? . querySelector ( '[data-module="tags"]' ) ;
if ( tagsCard ) {
rightGrid . prepend ( tagsCard ) ;
tagsCard . classList . remove ( 'd-none' ) ;
tagsCard . style . setProperty ( 'display' , 'flex' , 'important' ) ;
}
2026-10-08 08:58:02 +02:00
[ 'todo-steps' , 'files' , 'hardware' , 'time' ] . reverse ( ) . forEach ( key => {
2026-10-03 14:06:51 +02:00
const card = shell . querySelector ( ` .right-module-card[data-module=" ${ key } "] ` ) ;
if ( card ) rightGrid . prepend ( card ) ;
} ) ;
2026-10-08 08:58:02 +02:00
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 ) ;
}
2026-10-03 14:06:51 +02:00
// 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"]' ) ) ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
// Keep action selection and its fields together above the comment editor.
const composer = form . querySelector ( '.comment-composer' ) ;
const actionToolbar = byId ( 'v5ActionToggle' ) . parentElement ;
const submitButton = form . querySelector ( 'button[type="submit"]' ) ;
const submitRow = document . createElement ( 'div' ) ;
submitRow . className = 'v5-submit-row' ;
submitRow . append ( submitButton ) ;
composer . before ( actionToolbar , byId ( 'v5ActionMenu' ) , byId ( 'v5AssignmentFields' ) , byId ( 'commentQuickTimeRow' ) ) ;
composer . after ( submitRow ) ;
byId ( 'v5ActionToggle' ) . innerHTML = '<i class="bi bi-plus-lg" aria-hidden="true"></i><span>Handling</span>' ;
const commandFilterLabel = document . createElement ( 'label' ) ;
commandFilterLabel . className = 'comment-category-check' ;
commandFilterLabel . title = 'Vis eller skjul /-kommandoer' ;
commandFilterLabel . innerHTML = '<input type="checkbox" id="commentShowCommands" checked aria-label="Vis /-kommandoer"><i class="bi bi-slash-lg" aria-hidden="true"></i><span id="commentSourceCountCommands" class="count">0</span>' ;
shell . querySelector ( '.comment-toolbar' ) . append ( commandFilterLabel ) ;
const originalCommentFilters = window . applyCommentFilters ;
window . applyCommentFilters = function ( ... args ) {
originalCommentFilters ( ... args ) ;
const items = [ ... byId ( 'comments-container' ) . querySelectorAll ( '.comment-item' ) ] ;
const commandItems = items . filter ( item => {
const raw = item . querySelector ( '.comment-body' ) ? . dataset . commentRaw || '' ;
return /^\s*\/\/?(?:tid|tildel|forventet|undersag|mig|todo|afventer|løsning|deadline|udsat|fil|hjælp|timer|relation|prioritet|gruppe|mail|note)(?:\s|$)/im . test ( raw )
|| /^(?:deadline|defer|priority):/i . test ( raw . trim ( ) ) ;
} ) ;
byId ( 'commentSourceCountCommands' ) . textContent = String ( commandItems . length ) ;
if ( ! byId ( 'commentShowCommands' ) . checked ) commandItems . forEach ( item => { item . hidden = true ; } ) ;
const count = items . filter ( item => ! item . hidden ) . length ;
byId ( 'commentsVisibleCountBadge' ) . textContent = ` ${ count } / ${ items . length } vises ` ;
byId ( 'commentFilterEmptyState' ) ? . classList . toggle ( 'd-none' , count > 0 || items . length === 0 ) ;
} ;
byId ( 'commentShowCommands' ) . addEventListener ( 'change' , ( ) => window . applyCommentFilters ( ) ) ;
window . applyCommentFilters ( ) ;
2026-10-03 14:06:51 +02:00
let assignSelected = false ;
let groupId = config . groupId ;
let userId = config . userId ;
let members = [ ] ;
let memberRequest = 0 ;
let membersLoading = false ;
2026-10-08 08:58:02 +02:00
const memberCache = new Map ( ) ;
const disabledBeforeLock = new Map ( ) ;
2026-10-03 14:06:51 +02:00
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' ;
2026-10-08 08:58:02 +02:00
window . CaseCommandComposer ? . updateSubmitLabel ( ) ;
2026-10-03 14:06:51 +02:00
}
function toggleAction ( action ) {
if ( busy || pending ) return ;
if ( action [ 0 ] === 'assign' ) {
assignSelected = ! assignSelected ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
if ( assignSelected ) loadMembers ( ) ;
2026-10-03 14:06:51 +02:00
} else {
const checkbox = byId ( action [ 3 ] ) ;
checkbox . checked = ! checkbox . checked ;
checkbox . dispatchEvent ( new Event ( 'change' , { bubbles : true } ) ) ;
}
renderActions ( ) ;
2026-10-08 08:58:02 +02:00
closeMenu ( ) ;
2026-10-03 14:06:51 +02:00
}
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 ) ;
}
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
function employeeInitials ( name ) {
const words = String ( name || '' ) . trim ( ) . split ( /\s+/ ) . filter ( Boolean ) ;
return ( words . length > 1 ? words [ 0 ] [ 0 ] + words [ words . length - 1 ] [ 0 ] : ( words [ 0 ] || '?' ) . slice ( 0 , 2 ) ) . toLocaleUpperCase ( 'da' ) ;
}
2026-10-03 14:06:51 +02:00
function renderUsers ( ) {
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
const host = byId ( 'v5UserOptions' ) ;
host . replaceChildren ( ) ;
const query = byId ( 'v5UserSearch' ) . value . trim ( ) . toLocaleLowerCase ( 'da' ) ;
members . filter ( member => ` ${ member . display _name } ${ employeeInitials ( member . display _name ) } ` . toLocaleLowerCase ( 'da' ) . includes ( query ) ) . forEach ( member => {
const button = document . createElement ( 'button' ) ;
button . type = 'button' ;
button . textContent = employeeInitials ( member . display _name ) ;
button . title = member . display _name ;
button . setAttribute ( 'aria-label' , member . display _name ) ;
button . setAttribute ( 'aria-pressed' , String ( member . user _id === userId ) ) ;
button . disabled = busy || Boolean ( pending ) ;
button . addEventListener ( 'click' , ( ) => {
userId = member . user _id ;
// Keep the current group when possible; otherwise infer it from
// the employee's membership without changing the API contract.
groupId = member . group _ids . includes ( groupId ) ? groupId : member . group _ids [ 0 ] ;
renderGroups ( ) ; renderUsers ( ) ;
} ) ;
host . append ( button ) ;
} ) ;
if ( ! host . children . length ) host . textContent = 'Ingen medarbejdere fundet' ;
2026-10-03 14:06:51 +02:00
renderActions ( ) ;
}
async function loadMembers ( ) {
const sequence = ++ memberRequest ;
membersLoading = true ;
byId ( 'v5UserSearch' ) . disabled = true ;
byId ( 'v5UserOptions' ) . textContent = 'Henter medarbejdere…' ;
try {
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
const results = await Promise . all ( config . groups . map ( async group => {
let data = memberCache . get ( group . id ) ;
if ( ! data ) {
const response = await fetch ( ` /api/v1/sag/ ${ config . caseId } /assignments?group_id= ${ group . id } ` , { credentials : 'include' } ) ;
if ( ! response . ok ) throw new Error ( 'Kunne ikke hente medarbejdere. Åbn Tildel igen for at prøve igen.' ) ;
data = await response . json ( ) ;
memberCache . set ( group . id , data ) ;
}
return { groupId : group . id , members : data . members || [ ] } ;
} ) ) ;
2026-10-03 14:06:51 +02:00
if ( sequence !== memberRequest ) return ;
Add comprehensive test coverage for case creation, assignment, and remote recovery
- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads.
- Added tests for v5 assignment functionality, validating employee identification and group assignment.
- Implemented tests for remote recovery scenarios, including handling network failures and response validation.
- Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation.
- Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions.
- Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
2026-10-15 12:17:09 +02:00
const employees = new Map ( ) ;
results . forEach ( result => result . members . forEach ( member => {
if ( ! employees . has ( member . user _id ) ) employees . set ( member . user _id , { ... member , group _ids : [ ] } ) ;
employees . get ( member . user _id ) . group _ids . push ( result . groupId ) ;
} ) ) ;
members = [ ... employees . values ( ) ] . sort ( ( a , b ) => a . display _name . localeCompare ( b . display _name , 'da' ) ) ;
2026-10-03 14:06:51 +02:00
if ( ! members . some ( member => member . user _id === userId ) ) userId = null ;
byId ( 'v5UserSearch' ) . disabled = false ;
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 ) ;
2026-10-08 08:58:02 +02:00
form . querySelector ( 'textarea[name="indhold"]' ) . addEventListener ( 'input' , renderActions ) ;
2026-10-03 14:06:51 +02:00
function lock ( locked ) {
2026-10-08 08:58:02 +02:00
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 ) ;
}
} ) ;
2026-10-03 14:06:51 +02:00
// 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 ( ) ;
}
}
}
2026-10-08 08:58:02 +02:00
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' ;
2026-10-03 14:06:51 +02:00
form . removeAttribute ( 'onsubmit' ) ;
form . addEventListener ( 'submit' , async event => {
event . preventDefault ( ) ;
if ( busy ) return ;
2026-10-08 08:58:02 +02:00
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 ;
}
2026-10-03 14:06:51 +02:00
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 ;
}
2026-10-08 08:58:02 +02:00
if ( chosen . includes ( 'log_time' ) && byId ( 'v5QuickBilling' ) . dataset . billingReady !== 'true' ) { feedback ( 'Afregningsmuligheder er ikke indlæst. Genindlæs siden.' , true ) ; return ; }
2026-10-03 14:06:51 +02:00
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.' ) ;
2026-10-08 08:58:02 +02:00
window . CaseCommandComposer ? . reset ( ) ;
2026-10-03 14:06:51 +02:00
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 ( ) ;
2026-10-08 08:58:02 +02:00
applyCommentFilters ( ) ;
2026-10-03 14:06:51 +02:00
} ) ( ) ;