2026-10-08 08:58:02 +02:00
window . TimeBillingReview = ( ( ) => {
const labels = { draft : 'Kladde' , operations : 'Drift' , development : 'Udvikling' } ;
const needsReview = entry => Object . hasOwn ( labels , entry . billing _method || entry . _billing _method || '' ) ;
const badge = entry => needsReview ( entry ) ? ` <span class="badge text-bg-warning"> ${ labels [ entry . billing _method || entry . _billing _method ] } · Chefkontrol</span> ` : '' ;
2026-10-08 21:36:42 +02:00
const methods = { invoice : 'Faktura' , prepaid : 'Klippekort' , subscription : 'Fastpris' , fixed _price : 'Fastpris' , internal : 'Intern tid' , non _billable : 'Ikke fakturerbar' , draft : 'Kladde' , operations : 'Drift' , development : 'Udvikling' } ;
const settled = entry => entry . status === 'billed' || ( entry . workflow _active === true && entry . status === 'approved' ) || ! ! entry . economy _settled _at || ! ! entry . economy _order _draft _id || ! ! entry . billed _via _thehub _id ;
const canChoose = entry => ! settled ( entry ) && ( entry . workflow _active === true ? entry . workflow _can _override === true : needsReview ( entry ) ) ;
const displayBadge = entry => {
const method = entry . selected _billing _method || entry . workflow _decision ? . method || entry . billing _method || 'draft' ;
const text = method === 'manual_review' ? 'Manuel vurdering' : methods [ method ] || 'Afregning afventer' ;
return ` <span class="badge text-bg-secondary"> ${ text } ${ entry . workflow _stale ? ' · skal genberegnes' : '' } </span> ` ;
} ;
async function choose ( entry ) {
if ( ! canChoose ( entry ) ) throw new Error ( 'Afregningen kan ikke ændres for denne tidsregistrering.' ) ;
if ( entry . workflow _active !== true ) return approve ( entry ) ;
const optionsResponse = await fetch ( ` /api/v1/timetracking/time/ ${ Number ( entry . id ) } /settlement-options ` , { credentials : 'include' } ) ;
if ( ! optionsResponse . ok ) throw new Error ( 'Afregningsmuligheder kunne ikke hentes' ) ;
const data = await optionsResponse . json ( ) ;
const decisionResponse = await fetch ( ` /api/v1/timetracking/time/ ${ Number ( entry . id ) } /workflow-decision ` , { credentials : 'include' } ) ;
if ( ! decisionResponse . ok ) throw new Error ( 'Afregningsbeslutningen kunne ikke hentes' ) ;
const history = await decisionResponse . json ( ) , current = history . current ;
if ( current ? . status === 'settled' ) throw new Error ( 'Afregnet tid kræver en særskilt korrektion.' ) ;
const host = document . createElement ( 'div' ) ; host . className = 'modal fade' ; host . tabIndex = - 1 ;
host . innerHTML = '<div class="modal-dialog"><form class="modal-content"><div class="modal-header"><h5 class="modal-title">Vælg afregning</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button></div><div class="modal-body"><p class="small text-muted">Valget gemmes med begrundelse. Bogholderiet godkender og afregner tiden.</p><label class="form-label">Afregning</label><select class="form-select" required></select><label class="form-label mt-3">Begrundelse</label><textarea class="form-control" minlength="3" maxlength="2000" required></textarea><div class="small mt-2 text-danger" role="status"></div></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button><button type="submit" class="btn btn-primary">Gem afregningsvalg</button></div></form></div>' ;
const select = host . querySelector ( 'select' ) ; select . add ( new Option ( 'Faktura' , 'invoice' ) ) ; select . add ( new Option ( 'Intern tid' , 'internal' ) ) ; select . add ( new Option ( 'Ikke fakturerbar' , 'non_billable' ) ) ;
( 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 ) ) ) ;
const details = current ? . details || { } , billing = details . billing || details . decision || { } ;
select . value = current ? . method === 'prepaid' ? 'card_' + billing . card _id : current ? . method === 'subscription' ? 'fpa_' + billing . agreement _id : current ? . method || entry . selected _billing _method || 'invoice' ;
if ( ! select . value ) select . value = 'invoice' ;
document . body . append ( host ) ; const modal = new bootstrap . Modal ( host ) ;
return new Promise ( resolve => {
let changed = false ;
host . addEventListener ( 'hidden.bs.modal' , ( ) => { modal . dispose ( ) ; host . remove ( ) ; resolve ( changed ) ; } , { once : true } ) ;
host . querySelector ( 'form' ) . onsubmit = async event => {
event . preventDefault ( ) ; const reason = host . querySelector ( 'textarea' ) . value . trim ( ) ;
if ( reason . length < 3 ) { host . querySelector ( '[role=status]' ) . textContent = 'Angiv en kort begrundelse.' ; return ; }
const value = select . value , payload = { method : value , reason } , button = host . querySelector ( '[type=submit]' ) ; button . disabled = true ;
if ( value . startsWith ( 'card_' ) ) { payload . method = 'prepaid' ; payload . card _id = Number ( value . slice ( 5 ) ) ; }
if ( value . startsWith ( 'fpa_' ) ) { payload . method = 'subscription' ; payload . agreement _id = Number ( value . slice ( 4 ) ) ; }
if ( billing . product _number ) payload . product _number = billing . product _number ;
try { const response = await fetch ( ` /api/v1/timetracking/time/ ${ Number ( entry . id ) } /workflow-override ` , { method : 'POST' , credentials : 'include' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( payload ) } ) ; const result = await response . json ( ) . catch ( ( ) => ( { } ) ) ; if ( ! response . ok ) throw new Error ( typeof result . detail === 'string' ? result . detail : 'Afregningsvalget kunne ikke gemmes' ) ; changed = true ; modal . hide ( ) ; } catch ( error ) { host . querySelector ( '[role=status]' ) . textContent = error . message ; button . disabled = false ; }
} ; modal . show ( ) ;
} ) ;
}
2026-10-08 08:58:02 +02:00
async function approve ( entry , hours ) {
const response = await fetch ( ` /api/v1/timetracking/time/ ${ Number ( entry . id ) } /settlement-options ` , { credentials : 'include' } ) ;
if ( ! response . ok ) throw new Error ( 'Afregningsmuligheder kunne ikke hentes' ) ;
const data = await response . json ( ) ;
const host = document . createElement ( 'div' ) ; host . className = 'modal fade' ; host . tabIndex = - 1 ;
host . innerHTML = '<div class="modal-dialog"><form class="modal-content"><div class="modal-header"><h5 class="modal-title">Chefkontrol – vælg afregning</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button></div><div class="modal-body"><p data-label></p><label class="form-label">Endelig afregning</label><select class="form-select" required><option value="">Vælg afregning…</option></select><div class="small mt-3" role="status"></div></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button><button type="submit" class="btn btn-success">Godkend</button></div></form></div>' ;
host . querySelector ( '[data-label]' ) . textContent = labels [ entry . billing _method || entry . _billing _method ] + ' · ' + ( entry . description || entry . beskrivelse || 'Tidsregistrering' ) ;
const select = host . querySelector ( 'select' ) ; 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 } ` ) ) ) ;
document . body . append ( host ) ; const modal = new bootstrap . Modal ( host ) ;
return new Promise ( resolve => {
let approved = false ;
host . addEventListener ( 'hidden.bs.modal' , ( ) => { modal . dispose ( ) ; host . remove ( ) ; resolve ( approved ) ; } , { once : true } ) ;
host . querySelector ( 'form' ) . onsubmit = async event => {
event . preventDefault ( ) ; const button = host . querySelector ( '[type=submit]' ) ; button . disabled = true ;
const value = select . value , payload = { billing _method : value } ;
if ( value . startsWith ( 'card_' ) ) { payload . billing _method = 'prepaid' ; payload . prepaid _card _id = Number ( value . slice ( 5 ) ) ; }
if ( value . startsWith ( 'fpa_' ) ) { payload . billing _method = 'subscription' ; payload . fixed _price _agreement _id = Number ( value . slice ( 4 ) ) ; }
if ( Number . isFinite ( Number ( hours ) ) && Number ( hours ) > 0 ) payload . fakturerbar _tid _min = Math . round ( Number ( hours ) * 60 ) ;
try {
const result = await fetch ( ` /api/v1/timetracking/time/ ${ Number ( entry . id ) } /approve ` , { method : 'POST' , credentials : 'include' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( payload ) } ) ;
const body = await result . json ( ) . catch ( ( ) => ( { } ) ) ; if ( ! result . ok ) throw new Error ( body . detail || 'Kunne ikke godkende' ) ;
approved = true ; modal . hide ( ) ;
} catch ( error ) { host . querySelector ( '[role=status]' ) . textContent = error . message ; button . disabled = false ; }
} ; modal . show ( ) ;
} ) ;
}
2026-10-08 21:36:42 +02:00
return { needsReview , badge , approve , choose , canChoose , displayBadge } ;
2026-10-08 08:58:02 +02:00
} ) ( ) ;