2026-10-08 08:58:02 +02:00
/* V5 command preview. No command writes happen before the existing Save action. */
( ( ) => {
'use strict' ;
async function init ( ) {
const form = document . getElementById ( 'comment-form' ) ;
const input = form ? . querySelector ( 'textarea[name="indhold"]' ) ;
if ( ! input || ! window . CaseCommands ) return ;
const config = JSON . parse ( document . getElementById ( 'caseV5Data' ) . textContent ) ;
const saveButton = form . querySelector ( 'button[type="submit"]' ) ;
const saveDisabled = saveButton ? . disabled ;
if ( saveButton ) saveButton . disabled = true ;
let key = null ;
try {
const response = await fetch ( '/api/v1/auth/me' , { credentials : 'same-origin' } ) ;
const actor = response . ok ? await response . json ( ) : null ;
if ( actor ? . id ) key = ` case-command-draft: ${ actor . id } : ${ config . caseId } ` ;
} catch ( _ ) { /* Without verified actor identity keep the draft only in memory. */ }
const panel = document . createElement ( 'section' ) ;
panel . className = 'case-command-panel' ;
panel . setAttribute ( 'aria-label' , 'Kommandoer og forhåndsvisning' ) ;
const hint = document . createElement ( 'div' ) ;
hint . className = 'case-command-hint' ;
hint . textContent = 'Skriv / på en ny linje for handlinger. Gem udfører kommandoerne. Vælg filer uploader særskilt med det samme.' ;
const suggestions = document . createElement ( 'div' ) ;
suggestions . className = 'case-command-suggestions' ;
const help = document . createElement ( 'details' ) ;
const helpTitle = document . createElement ( 'summary' ) ; helpTitle . textContent = 'Kommandohjælp' ;
const helpList = document . createElement ( 'div' ) ; helpList . className = 'case-command-help' ;
help . append ( helpTitle , helpList ) ;
const chips = document . createElement ( 'div' ) ;
chips . className = 'case-command-chips' ;
const status = document . createElement ( 'div' ) ;
status . className = 'case-command-status' ;
status . setAttribute ( 'role' , 'status' ) ;
status . setAttribute ( 'aria-live' , 'polite' ) ;
const drafts = document . createElement ( 'details' ) ;
const draftTitle = document . createElement ( 'summary' ) ; draftTitle . textContent = 'Mine gemte mailkladder' ;
const draftList = document . createElement ( 'div' ) ; draftList . className = 'case-command-drafts' ;
drafts . append ( draftTitle , draftList ) ;
const receipt = document . createElement ( 'div' ) ; receipt . className = 'case-command-receipt' ; receipt . setAttribute ( 'role' , 'status' ) ; receipt . hidden = true ;
panel . append ( receipt , hint , suggestions , chips , status , help , drafts ) ;
form . querySelector ( '.comment-composer' ) . after ( panel ) ;
const examples = {
tid : '30m remote "Arbejde udført"' , tildel : 'navn "Begrundelse"' , forventet : '1h' ,
undersag : '"Titel"' , mig : '' , todo : '"Opgave"' , afventer : 'kunde "Begrundelse"' ,
løsning : '"Løsning"' , deadline : 'i morgen' , udsat : 'i morgen' , fil : '' , hjælp : '' ,
timer : 'start "Arbejdsbeskrivelse"' , relation : '#1248' , prioritet : 'høj "Begrundelse"' ,
gruppe : 'Support "Begrundelse"' , mail : '"Mailkladde"' , note : '"Intern note"'
} ;
help . className = 'case-command-help-disclosure' ;
helpTitle . textContent = 'Kommandoer · hjælp og eksempler' ;
const intro = document . createElement ( 'p' ) ; intro . className = 'case-command-help-intro' ;
intro . textContent = 'Klik på en kommando for at indsætte et eksempel. Ret teksten og kontrollér handlingerne før gemning.' ;
helpList . append ( intro ) ;
const groups = [
[ 'Tid og planlægning' , 'clock-history' , [
2026-10-08 21:36:42 +02:00
[ 'tid' , 'Registrer tid som kladde eller med automatisk afregning' ] , [ 'timer' , 'Start timer · brug /timer pause for at pause' ] ,
2026-10-08 08:58:02 +02:00
[ 'forventet' , 'Sæt forventet tidsforbrug' ] , [ 'deadline' , 'Angiv seneste dato' ] , [ 'udsat' , 'Udsæt sagen inden deadline' ] ] ] ,
[ 'Ansvar og opgaver' , 'person-check' , [
[ 'tildel' , 'Vælg medarbejder med begrundelse' ] , [ 'mig' , 'Tildel sagen til dig selv' ] ,
[ 'gruppe' , 'Overdrag til en gruppe' ] , [ 'todo' , 'Opret en opgave' ] , [ 'afventer' , 'Afvent kunden og opret en todo' ] ] ] ,
[ 'Sager og relationer' , 'diagram-3' , [
[ 'undersag' , 'Opret en undersag' ] , [ 'relation' , 'Kobl en eksisterende sag' ] , [ 'prioritet' , 'Sæt prioritet med begrundelse' ] ] ] ,
[ 'Noter og kommunikation' , 'chat-left-text' , [
[ 'note' , 'Gem en intern note' ] , [ 'løsning' , 'Gem en ny løsning' ] , [ 'mail' , 'Opret mailkladde · sender ikke' ] ,
[ 'fil' , 'Åbn filvalg · særskilt upload' ] , [ 'hjælp' , 'Vis denne hjælp' ] ] ]
] ;
groups . forEach ( ( [ title , icon , commands ] ) => {
const section = document . createElement ( 'section' ) ; section . className = 'case-command-help-group' ;
const heading = document . createElement ( 'h6' ) ;
const symbol = document . createElement ( 'i' ) ; symbol . className = ` bi bi- ${ icon } ` ; symbol . setAttribute ( 'aria-hidden' , 'true' ) ;
heading . append ( symbol , document . createTextNode ( title ) ) ; section . append ( heading ) ;
commands . forEach ( ( [ name , description ] ) => {
const button = document . createElement ( 'button' ) ; button . type = 'button' ; button . className = 'case-command-help-item' ;
const command = document . createElement ( 'code' ) ; command . textContent = ` / ${ name } ` ;
const explain = document . createElement ( 'span' ) ; explain . textContent = description ;
const example = document . createElement ( 'small' ) ; example . textContent = ` / ${ name } ${ examples [ name ] ? ' ' + examples [ name ] : '' } ` ;
button . append ( command , explain , example ) ;
button . addEventListener ( 'click' , ( ) => {
if ( frozenText !== null ) { message ( 'Afslut den igangværende gemning før du ændrer kladden.' , true ) ; return ; }
const prefix = input . value && ! input . value . endsWith ( '\n' ) ? '\n' : '' ;
input . value += ` ${ prefix } / ${ name } ${ examples [ name ] ? ' ' + examples [ name ] : '' } ` ;
input . focus ( ) ; input . setSelectionRange ( input . value . length , input . value . length ) ;
update ( ) ;
} ) ; section . append ( button ) ;
} ) ; helpList . append ( section ) ;
} ) ;
const scopeHelp = document . createElement ( 'div' ) ; scopeHelp . className = 'case-command-help-scope' ;
const scopeTitle = document . createElement ( 'strong' ) ; scopeTitle . textContent = 'Handlinger på en undersag' ;
const scopeText = document . createElement ( 'p' ) ; scopeText . textContent = '/ gælder hovedsagen. // gælder den seneste undersag ovenover.' ;
const scopeExample = document . createElement ( 'pre' ) ; scopeExample . textContent = '/undersag "Router opsætning"\n//tildel mkk "Opsæt router"\n//forventet 30m' ;
scopeHelp . append ( scopeTitle , scopeText , scopeExample ) ; helpList . append ( scopeHelp ) ;
let parsed , requestId = null , frozenText = null , frozenBody = null , timer , generation = 0 , serverValid = false ;
let suggestionIndex = - 1 ;
const settlements = new Map ( ) ;
2026-10-08 18:00:47 +02:00
let workflowActive = false ;
2026-10-08 08:58:02 +02:00
const billingChoices = [ [ 'draft' , 'Kladde · chefkontrol' ] , [ 'operations' , 'Drift · chef vælger afregning' ] , [ 'development' , 'Udvikling · chef vælger afregning' ] ] ;
async function loadBillingChoices ( ) {
try {
const response = await fetch ( ` /api/v1/timetracking/cases/ ${ config . caseId } /billing-options ` , { credentials : 'same-origin' } ) ;
if ( ! response . ok ) return ;
const data = await response . json ( ) ;
2026-10-08 18:00:47 +02:00
workflowActive = data . workflow _active === true ;
if ( workflowActive ) { if ( frozenText === null ) update ( ) ; return ; }
2026-10-08 08:58:02 +02:00
if ( data . has _customer ) billingChoices . push ( [ 'invoice' , ` Faktura · ${ Number ( data . hourly _rate ) . toLocaleString ( 'da-DK' ) } kr./time ` ] ) ;
( data . prepaid _cards || [ ] ) . forEach ( card => billingChoices . push ( [ ` card_ ${ card . id } ` , ` Klippekort # ${ card . card _number || card . id } · ${ Number ( card . remaining _hours ) . toLocaleString ( 'da-DK' ) } timer tilbage ` ] ) ) ;
( data . agreements || [ ] ) . forEach ( agreement => billingChoices . push ( [ ` fpa_ ${ agreement . id } ` , ` Fastpris # ${ agreement . agreement _number || agreement . id } ` ] ) ) ;
if ( frozenText === null ) update ( ) ;
} catch ( _ ) { /* The three safe chef-review choices remain available. */ }
}
let storageAvailable = Boolean ( key ) ;
function store ( ) {
if ( ! storageAvailable ) return ;
try { sessionStorage . setItem ( key , JSON . stringify ( { text : input . value , requestId , frozenText , frozenBody , settlements : [ ... settlements ] } ) ) ; }
catch ( _ ) { storageAvailable = false ; }
}
try {
const saved = key ? JSON . parse ( sessionStorage . getItem ( key ) || 'null' ) : null ;
if ( saved && ! input . value && typeof saved . text === 'string' ) {
input . value = saved . text ; requestId = saved . requestId || null ; frozenText = saved . frozenText || null ; frozenBody = saved . frozenBody || null ;
2026-10-08 21:36:42 +02:00
if ( frozenBody && frozenBody . billing _mode !== 'automatic' ) message ( 'En tidligere gemning med afregningsvalg afventer svar. Prøv den samme gemning igen; nye registreringer gemmes derefter uden afregningsvalg.' ) ;
2026-10-08 08:58:02 +02:00
if ( Array . isArray ( saved . settlements ) ) saved . settlements . forEach ( ( [ id , value ] ) => { if ( typeof id === 'string' && typeof value === 'string' ) settlements . set ( id , value ) ; } ) ;
}
} catch ( _ ) { storageAvailable = false ; }
function currentLine ( ) {
const start = input . value . lastIndexOf ( '\n' , Math . max ( 0 , input . selectionStart - 1 ) ) + 1 ;
const end = input . value . indexOf ( '\n' , input . selectionStart ) ;
return { start , end : end < 0 ? input . value . length : end , text : input . value . slice ( start , end < 0 ? undefined : end ) } ;
}
function editLine ( line ) {
const rows = input . value . split ( '\n' ) ;
const start = rows . slice ( 0 , line - 1 ) . reduce ( ( sum , row ) => sum + row . length + 1 , 0 ) ;
input . focus ( ) ; input . setSelectionRange ( start , start + ( rows [ line - 1 ] || '' ) . length ) ;
}
function message ( text , error = false ) {
status . textContent = text ; status . classList . toggle ( 'is-error' , error ) ;
}
2026-10-08 21:36:42 +02:00
function body ( ) { return frozenBody || { comment : parsed . comment , commands : parsed . commands , internal : true , billing _mode : 'automatic' } ; }
2026-10-08 08:58:02 +02:00
function label ( command ) {
const names = { time : 'Tid' , assign : 'Tildel' , estimate : 'Estimat' , subcase : 'Opret undersag' , self : 'Tildel mig' , todo : 'Todo' , waiting : 'Afventer kunde' , solution : 'Løsning' , deadline : 'Deadline' , defer : 'Udsæt' , file : 'Vedhæft filer' , help : 'Hjælp' , timer : 'Timer' , relation : 'Relation' , priority : 'Prioritet' , group : 'Gruppe' , mail : 'Mailkladde' , note : 'Intern note' } ;
const target = command . scope === 'main' ? 'Hovedsag' : ` Undersag ${ Number ( command . scope ) + 1 } ` ;
const details = [ command . minutes ? ` ${ command . minutes } min ` : '' , command . category , command . user _name || command . assignee _name || command . assignee , command . group _name || command . group , command . title , command . date , command . priority , command . caseId ? ` # ${ command . caseId } ` : '' , command . action ] . filter ( Boolean ) ;
return ` ${ names [ command . type ] || command . type } ${ details . length ? ' · ' + details . join ( ' · ' ) : '' } — ${ target } ` ;
}
async function api ( path , payload ) {
const response = await fetch ( ` /api/v1/sag/ ${ config . caseId } / ${ path } ` , {
method : 'POST' , credentials : 'same-origin' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( payload )
} ) ;
const result = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! response . ok ) {
const details = result . detail ;
const error = new Error ( typeof details === 'string' ? details : details ? . errors ? . map ( e => ` Linje ${ e . line } : ${ e . message } ` ) . join ( '\n' ) || ` Handlingen fejlede (HTTP ${ response . status } ). ` ) ;
error . status = response . status ; throw error ;
}
return result ;
}
async function preview ( version ) {
if ( ! parsed . commands . length || parsed . errors . length ) return ;
try {
const result = await api ( 'command-preview' , body ( ) ) ;
if ( version !== generation ) return ;
if ( result . errors ? . length ) { message ( result . errors . map ( e => ` Linje ${ e . line } : ${ e . message } ` ) . join ( '\n' ) , true ) ; return ; }
if ( result . commands ) result . commands . forEach ( command => {
const chip = chips . querySelector ( ` [data-line=" ${ Number ( command . line ) } "] ` ) ;
if ( chip ) chip . textContent = label ( command ) ;
} ) ;
serverValid = true ; message ( 'Forhåndsvisning klar. Gem udfører handlingerne; mail åbnes kun som kladde.' ) ;
} catch ( error ) { if ( version === generation ) message ( error . message , true ) ; }
}
let completionValues = { users : [ ] , groups : config . groups || [ ] , cases : [ ] } ;
suggestions . setAttribute ( 'aria-label' , 'Forslag til næste værdi' ) ;
async function loadCompletionValues ( ) {
try {
const response = await fetch ( ` /api/v1/sag/ ${ config . caseId } /command-options ` , { credentials : 'same-origin' } ) ;
if ( response . ok ) completionValues = await response . json ( ) ;
renderSuggestions ( ) ;
} catch ( _ ) { /* Fixed values remain available when the catalog cannot load. */ }
}
function renderSuggestions ( ) {
suggestions . replaceChildren ( ) ; suggestionIndex = - 1 ;
if ( frozenText !== null || ! window . CaseCommandSuggestions ) return ;
const line = currentLine ( ) ;
const options = window . CaseCommandSuggestions . suggest ( line . text , input . selectionStart - line . start , completionValues ) ;
options . forEach ( option => {
const button = document . createElement ( 'button' ) ; button . type = 'button' ;
const title = document . createElement ( 'strong' ) ; title . textContent = option . label ;
const detail = document . createElement ( 'small' ) ; detail . textContent = option . hint ;
button . append ( title , detail ) ;
button . addEventListener ( 'click' , ( ) => {
if ( frozenText !== null ) return ;
input . setRangeText ( option . value , line . start + option . from , line . start + option . to , 'end' ) ;
input . focus ( ) ; update ( ) ;
} ) ; suggestions . append ( button ) ;
} ) ;
}
function update ( ) {
generation ++ ; clearTimeout ( timer ) ; serverValid = false ;
parsed = window . CaseCommands . parse ( input . value , { now : new Date ( ) } ) ;
chips . replaceChildren ( ) ;
parsed . commands . forEach ( command => {
const chip = document . createElement ( 'button' ) ; chip . type = 'button' ;
chip . dataset . line = String ( command . line ) ;
chip . textContent = label ( command ) ;
chip . title = 'Klik for at rette kommandolinjen' ;
chip . addEventListener ( 'click' , ( ) => editLine ( command . line ) ) ; chips . append ( chip ) ;
if ( command . type === 'time' ) {
2026-10-08 21:36:42 +02:00
delete command . settlement _choice ;
const note = document . createElement ( 'span' ) ; note . className = 'small text-muted' ; note . textContent = workflowActive ? 'Afregning vælges automatisk' : 'Tid gemmes som kladde · afregning vælges i Tidsregistrering' ; chips . append ( note ) ;
2026-10-08 08:58:02 +02:00
}
if ( command . type === 'file' ) {
const file = document . createElement ( 'button' ) ; file . type = 'button' ; file . textContent = 'Vælg filer…' ;
file . addEventListener ( 'click' , ( ) => document . getElementById ( 'topbarAttachmentInput' ) ? . click ( ) ) ;
chips . append ( file ) ;
}
} ) ;
renderSuggestions ( ) ; store ( ) ; updateSubmitLabel ( ) ;
if ( parsed . commands . some ( command => command . type === 'help' ) ) help . open = true ;
2026-10-08 21:36:42 +02:00
if ( frozenText !== null ) { input . readOnly = true ; message ( frozenBody && frozenBody . billing _mode !== 'automatic' ? 'En tidligere gemning med afregningsvalg afventer svar. Tryk Gem igen for at fortsætte den samme gemning sikkert.' : 'En gemning afventer svar. Tryk Gem igen for at fortsætte sikkert.' ) ; return ; }
2026-10-08 08:58:02 +02:00
if ( parsed . errors . length ) message ( parsed . errors . map ( e => ` Linje ${ e . line } : ${ e . message } ` ) . join ( '\n' ) , true ) ;
else if ( parsed . commands . length ) { message ( 'Kontrollerer handlinger…' ) ; const version = generation ; timer = setTimeout ( ( ) => preview ( version ) , 350 ) ; }
else message ( '' ) ;
}
function reset ( ) {
requestId = null ; frozenText = null ; frozenBody = null ; input . readOnly = false ; input . value = '' ;
settlements . clear ( ) ;
try { if ( key ) sessionStorage . removeItem ( key ) ; } catch ( _ ) { }
update ( ) ;
}
function openMailDraft ( text ) {
window . openCaseEmailTab ? . ( ) ;
const bodyInput = document . getElementById ( 'caseEmailBody' ) ;
if ( bodyInput ) bodyInput . value = text ;
const modal = document . getElementById ( 'caseEmailComposeModal' ) ;
if ( modal && window . bootstrap ? . Modal ) window . bootstrap . Modal . getOrCreateInstance ( modal ) . show ( ) ;
}
function receiptSummary ( result ) {
const counts = { } ; ( result . results || [ ] ) . forEach ( item => { if ( ! [ 'help' , 'file' ] . includes ( item . type ) ) counts [ item . type ] = ( counts [ item . type ] || 0 ) + 1 ; } ) ;
const names = { comment : 'kommentar' , subcase : 'undersag' , time : 'tidsregistrering' , todo : 'todo' , assign : 'tildeling' , self : 'tildeling' , group : 'gruppeskift' , mail : 'mailkladde' , note : 'note' , solution : 'løsning' , relation : 'relation' , timer : 'timerhandling' , estimate : 'estimat' , deadline : 'deadline' , defer : 'udsættelse' , priority : 'prioritet' , waiting : 'afventer' } ;
const parts = Object . entries ( counts ) . map ( ( [ type , count ] ) => ` ${ count } ${ names [ type ] || type } ` ) ;
return parts . length ? ` Gemt: ${ parts . join ( ', ' ) } . ` : 'Handlingerne er behandlet.' ;
}
function showReceipt ( result ) {
receipt . replaceChildren ( ) ; receipt . hidden = false ;
const summary = document . createElement ( 'div' ) ; summary . textContent = receiptSummary ( result ) ; receipt . append ( summary ) ;
( result . results || [ ] ) . filter ( item => [ 'subcase' , 'time' , 'mail' ] . includes ( item . type ) ) . forEach ( item => {
const caseId = Number ( item . type === 'subcase' ? item . id : item . case _id ) ;
if ( ! Number . isSafeInteger ( caseId ) || caseId < 1 ) return ;
const link = document . createElement ( 'a' ) ; link . href = ` /sag/ ${ caseId } /v5 ${ item . type === 'time' ? '#timetracking' : '' } ` ;
link . textContent = item . type === 'subcase' ? ` Åbn undersag # ${ caseId } ` : item . type === 'time' ? ` Tid # ${ item . id } på sag # ${ caseId } ` : ` Mailkladde # ${ item . id } på sag # ${ caseId } ` ;
receipt . append ( link ) ;
} ) ;
}
async function loadDrafts ( ) {
try {
const response = await fetch ( ` /api/v1/sag/ ${ config . caseId } /command-mail-drafts ` , { credentials : 'same-origin' } ) ;
if ( ! response . ok ) throw new Error ( 'Mailkladder kunne ikke hentes.' ) ;
const rows = await response . json ( ) ; draftList . replaceChildren ( ) ;
draftTitle . textContent = ` Mine gemte mailkladder ( ${ rows . length } ) ` ;
if ( ! rows . length ) draftList . textContent = 'Ingen gemte mailkladder.' ;
rows . forEach ( draft => {
const button = document . createElement ( 'button' ) ; button . type = 'button' ;
button . textContent = ` Åbn kladde # ${ draft . id } · ${ draft . subject || 'Uden emne' } ` ;
button . addEventListener ( 'click' , ( ) => {
openMailDraft ( draft . body || '' ) ;
const subject = document . getElementById ( 'caseEmailSubject' ) ; if ( subject ) subject . value = draft . subject || '' ;
} ) ; draftList . append ( button ) ;
} ) ;
} catch ( error ) { draftList . textContent = error . message ; }
}
drafts . addEventListener ( 'toggle' , ( ) => { if ( drafts . open ) loadDrafts ( ) ; } ) ;
function updateSubmitLabel ( ) {
if ( /^\s*\//m . test ( input . value ) ) {
const button = form . querySelector ( 'button[type="submit"]' ) ;
if ( button ) button . textContent = frozenText !== null ? 'Prøv gemning igen' : 'Gem kommentar og handlinger' ;
}
}
window . CaseCommandComposer = {
hasCommands : ( ) => / ^ \ s * \ //m.test(input.value), getParsed:()=>parsed, reset, updateSubmitLabel,
async submit ( ) {
clearTimeout ( timer ) ;
if ( parsed . errors . length ) throw new Error ( parsed . errors . map ( e => ` Linje ${ e . line } : ${ e . message } ` ) . join ( '\n' ) ) ;
if ( ! parsed . commands . length ) throw new Error ( 'Ingen gyldige kommandoer.' ) ;
const wasFrozen = frozenText !== null ;
frozenBody = body ( ) ; frozenText = input . value ; input . readOnly = true ;
chips . querySelectorAll ( 'select,button' ) . forEach ( control => { control . disabled = true ; } ) ;
try {
if ( ! serverValid && ! wasFrozen ) {
const result = await api ( 'command-preview' , body ( ) ) ;
if ( result . errors ? . length ) throw new Error ( result . errors . map ( e => e . message ) . join ( '\n' ) ) ;
}
if ( ! requestId ) requestId = crypto . randomUUID ( ) ;
store ( ) ;
const result = await api ( 'command-execute' , { ... body ( ) , request _id : requestId } ) ;
const mail = parsed . commands . find ( c => c . type === 'mail' ) ;
const childMails = ( result . results || [ ] ) . filter ( item => item . type === 'mail' && Number ( item . case _id ) !== Number ( config . caseId ) ) ;
reset ( ) ;
showReceipt ( result ) ;
try { if ( key ) sessionStorage . setItem ( ` ${ key } :receipt ` , JSON . stringify ( result ) ) ; } catch ( _ ) { }
await loadDrafts ( ) ;
if ( mail ) drafts . open = true ;
if ( mail && mail . scope === 'main' ) openMailDraft ( mail . text ) ;
childMails . forEach ( item => {
const link = document . createElement ( 'a' ) ; link . href = ` /sag/ ${ Number ( item . case _id ) } /v5 ` ;
link . textContent = ` Åbn mailkladde på undersag # ${ item . case _id } ` ; panel . append ( link ) ;
} ) ;
return { ok : true , message : receiptSummary ( result ) , reload : ! mail , result } ;
} catch ( error ) {
if ( ! requestId || ( error . status && error . status < 500 ) ) {
requestId = null ; frozenText = null ; frozenBody = null ; input . readOnly = false ; store ( ) ;
chips . querySelectorAll ( 'select,button' ) . forEach ( control => { control . disabled = false ; } ) ;
}
message ( error . message , true ) ; throw error ;
}
}
} ;
input . addEventListener ( 'input' , update ) ;
input . addEventListener ( 'click' , renderSuggestions ) ;
input . addEventListener ( 'keyup' , event => { if ( ! [ 'ArrowDown' , 'ArrowUp' , 'Enter' , 'Escape' ] . includes ( event . key ) ) renderSuggestions ( ) ; } ) ;
input . addEventListener ( 'keydown' , event => {
const options = [ ... suggestions . querySelectorAll ( 'button' ) ] ;
if ( ! options . length ) return ;
if ( event . key === 'Escape' ) { suggestions . replaceChildren ( ) ; suggestionIndex = - 1 ; return ; }
if ( [ 'ArrowDown' , 'ArrowUp' ] . includes ( event . key ) ) {
event . preventDefault ( ) ; suggestionIndex = ( suggestionIndex + ( event . key === 'ArrowDown' ? 1 : - 1 ) + options . length ) % options . length ;
options . forEach ( ( option , index ) => { option . classList . toggle ( 'is-active' , index === suggestionIndex ) ; option . setAttribute ( 'aria-pressed' , String ( index === suggestionIndex ) ) ; } ) ;
options [ suggestionIndex ] . scrollIntoView ( { block : 'nearest' } ) ;
} else if ( event . key === 'Enter' && suggestionIndex >= 0 ) { event . preventDefault ( ) ; options [ suggestionIndex ] . click ( ) ; }
} ) ;
update ( ) ;
try {
const previous = key ? JSON . parse ( sessionStorage . getItem ( ` ${ key } :receipt ` ) || 'null' ) : null ;
if ( previous ? . results ) { showReceipt ( previous ) ; sessionStorage . removeItem ( ` ${ key } :receipt ` ) ; }
} catch ( _ ) { }
if ( saveButton ) saveButton . disabled = Boolean ( saveDisabled ) ;
loadDrafts ( ) ;
loadBillingChoices ( ) ;
loadCompletionValues ( ) ;
}
if ( document . readyState === 'loading' ) document . addEventListener ( 'DOMContentLoaded' , init ) ; else init ( ) ;
} ) ( ) ;