2026-05-01 20:58:13 +02:00
( function ( ) {
'use strict' ;
const DEFAULT _ACTION _CONFIG = {
default _category : null ,
allow _company _templates : true ,
allow _global _templates : true
} ;
let modalElement = null ;
let modalInstance = null ;
let currentCaseId = null ;
let currentAction = null ;
let availableTemplates = [ ] ;
let currentPreview = null ;
function notify ( message , level ) {
if ( typeof window . showNotification === 'function' ) {
window . showNotification ( message , level || 'info' ) ;
return ;
}
if ( level === 'error' ) {
alert ( message ) ;
}
}
function ensureModal ( ) {
if ( modalElement ) return ;
const html = `
< div class = "modal fade" id = "taskTemplateSelectorModal" tabindex = "-1" aria - hidden = "true" >
< div class = "modal-dialog modal-xl modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" >
2026-08-17 19:27:43 +02:00
< h5 class = "modal-title" > < i class = "bi bi-file-earmark-check me-2" > < / i > V æ l g o p g a v e - t e m p l a t e < / h 5 >
2026-05-01 20:58:13 +02:00
< button type = "button" class = "btn-close" data - bs - dismiss = "modal" aria - label = "Close" > < / b u t t o n >
< / d i v >
< div class = "modal-body" >
< div class = "row g-3" >
< div class = "col-lg-4" >
< label class = "form-label" > Template - kilde < / l a b e l >
< select id = "ttSource" class = "form-select" >
2026-08-17 19:27:43 +02:00
< option value = "all" selected > Firma + fælles < / o p t i o n >
2026-05-01 20:58:13 +02:00
< option value = "company" > Kun firma < / o p t i o n >
2026-08-17 19:27:43 +02:00
< option value = "global" > Kun fælles < / o p t i o n >
2026-05-01 20:58:13 +02:00
< option value = "internal" > Kun intern < / o p t i o n >
< / s e l e c t >
< / d i v >
< div class = "col-lg-4" >
< label class = "form-label" > Kategori < / l a b e l >
< select id = "ttCategory" class = "form-select" >
< option value = "" > Alle < / o p t i o n >
< option value = "onboarding" > Onboarding < / o p t i o n >
< option value = "offboarding" > Offboarding < / o p t i o n >
< option value = "simkort" > Mobil / SIM - kort < / o p t i o n >
< option value = "hardwarebestilling" > Hardwarebestilling < / o p t i o n >
2026-08-17 19:27:43 +02:00
< option value = "brugerandring" > Brugerændring < / o p t i o n >
2026-05-01 20:58:13 +02:00
< option value = "andet" > Andet < / o p t i o n >
< / s e l e c t >
< / d i v >
< div class = "col-lg-4" >
< label class = "form-label" > Startdato < / l a b e l >
< input id = "ttStartDate" type = "date" class = "form-control" / >
< / d i v >
< div class = "col-lg-8" >
< label class = "form-label" > Template < / l a b e l >
2026-08-17 19:27:43 +02:00
< input id = "ttTemplateSearch" class = "form-control mb-2" placeholder = "Søg template..." / >
2026-05-01 20:58:13 +02:00
< select id = "ttTemplate" class = "form-select" size = "8" > < / s e l e c t >
< div id = "ttTemplateEmpty" class = "small text-muted mt-2 d-none" > Ingen templates matchede dit filter . < / d i v >
< / d i v >
< div class = "col-lg-4" >
< label class = "form-label" > Oprettelsestype < / l a b e l >
< select id = "ttMode" class = "form-select mb-3" >
< option value = "subcases" > Opret som undersager < / o p t i o n >
2026-08-17 19:27:43 +02:00
< option value = "tasks" > Opret som opgaver på nuværende sag < / o p t i o n >
2026-05-01 20:58:13 +02:00
< option value = "combined" selected > Kombineret < / o p t i o n >
< / s e l e c t >
< label class = "form-label" > Ansvarlig < / l a b e l >
< select id = "ttAssigneeMode" class = "form-select mb-2" >
< option value = "template_default" selected > Brug standard fra template < / o p t i o n >
2026-08-17 19:27:43 +02:00
< option value = "specific_user" > Vælg specifik medarbejder < / o p t i o n >
< option value = "specific_role" > Vælg team / rolle < / o p t i o n >
2026-05-01 20:58:13 +02:00
< / s e l e c t >
< input id = "ttAssigneeUserId" type = "number" class = "form-control mb-2 d-none" placeholder = "Medarbejder ID" min = "1" step = "1" / >
< input id = "ttAssigneeRoleId" type = "number" class = "form-control d-none" placeholder = "Rolle ID" min = "1" step = "1" / >
< / d i v >
< / d i v >
< div class = "border-top mt-4 pt-3" >
< div class = "d-flex justify-content-between align-items-center mb-2" >
< h6 class = "mb-0" > Preview < / h 6 >
< button type = "button" class = "btn btn-sm btn-outline-primary" id = "ttPreviewBtn" > Opdater preview < / b u t t o n >
< / d i v >
< div id = "ttPreviewSummary" class = "small text-muted mb-2" > Ingen preview endnu . < / d i v >
< div id = "ttPreviewList" class = "list-group" style = "max-height: 280px; overflow:auto;" > < / d i v >
< / d i v >
< div class = "border-top mt-4 pt-3" >
< div class = "d-flex justify-content-between align-items-center mb-2" >
2026-08-17 19:27:43 +02:00
< h6 class = "mb-0" > Seneste template - kørsler på sagen < / h 6 >
2026-05-01 20:58:13 +02:00
< button type = "button" class = "btn btn-sm btn-outline-secondary" id = "ttRefreshRunsBtn" > Opdater < / b u t t o n >
< / d i v >
2026-08-17 19:27:43 +02:00
< div id = "ttRunHistory" class = "small text-muted" > Ingen template - kørsler endnu . < / d i v >
2026-05-01 20:58:13 +02:00
< / d i v >
< / d i v >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-outline-secondary" data - bs - dismiss = "modal" > Annuller < / b u t t o n >
< button type = "button" class = "btn btn-primary" id = "ttRunBtn" disabled > Opret < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
< / d i v > ` ;
document . body . insertAdjacentHTML ( 'beforeend' , html ) ;
modalElement = document . getElementById ( 'taskTemplateSelectorModal' ) ;
modalInstance = new bootstrap . Modal ( modalElement ) ;
document . getElementById ( 'ttSource' ) . addEventListener ( 'change' , refreshTemplates ) ;
document . getElementById ( 'ttCategory' ) . addEventListener ( 'change' , refreshTemplates ) ;
document . getElementById ( 'ttTemplateSearch' ) . addEventListener ( 'input' , renderTemplateList ) ;
document . getElementById ( 'ttTemplate' ) . addEventListener ( 'change' , onTemplateSelected ) ;
document . getElementById ( 'ttAssigneeMode' ) . addEventListener ( 'change' , onAssigneeModeChanged ) ;
document . getElementById ( 'ttPreviewBtn' ) . addEventListener ( 'click' , runPreview ) ;
document . getElementById ( 'ttRunBtn' ) . addEventListener ( 'click' , runTemplate ) ;
document . getElementById ( 'ttRefreshRunsBtn' ) . addEventListener ( 'click' , loadRunHistory ) ;
modalElement . addEventListener ( 'shown.bs.modal' , ( ) => {
document . getElementById ( 'ttTemplateSearch' ) . focus ( ) ;
} ) ;
}
function todayIso ( ) {
const now = new Date ( ) ;
const month = String ( now . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
const day = String ( now . getDate ( ) ) . padStart ( 2 , '0' ) ;
return ` ${ now . getFullYear ( ) } - ${ month } - ${ day } ` ;
}
async function fetchCase ( caseId ) {
const response = await fetch ( ` /api/v1/sag/ ${ caseId } ` , { credentials : 'include' } ) ;
if ( ! response . ok ) {
throw new Error ( 'Kunne ikke hente sag' ) ;
}
return response . json ( ) ;
}
function getActionConfig ( ) {
const cfg = ( currentAction && currentAction . config ) || { } ;
return { ... DEFAULT _ACTION _CONFIG , ... cfg } ;
}
async function refreshTemplates ( ) {
const source = document . getElementById ( 'ttSource' ) . value ;
const category = document . getElementById ( 'ttCategory' ) . value ;
const params = new URLSearchParams ( ) ;
if ( source ) params . set ( 'source' , source ) ;
if ( category ) params . set ( 'category' , category ) ;
try {
const caseRow = await fetchCase ( currentCaseId ) ;
if ( caseRow && caseRow . customer _id ) {
params . set ( 'company_id' , String ( caseRow . customer _id ) ) ;
}
const response = await fetch ( ` /api/v1/task-templates? ${ params . toString ( ) } ` , { credentials : 'include' } ) ;
if ( ! response . ok ) {
const payload = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
throw new Error ( payload . detail || 'Kunne ikke hente templates' ) ;
}
availableTemplates = await response . json ( ) ;
renderTemplateList ( ) ;
} catch ( error ) {
console . error ( 'Failed to load templates:' , error ) ;
availableTemplates = [ ] ;
renderTemplateList ( ) ;
notify ( error . message || 'Kunne ikke hente templates' , 'error' ) ;
}
}
function renderTemplateList ( ) {
const search = String ( document . getElementById ( 'ttTemplateSearch' ) . value || '' ) . toLowerCase ( ) . trim ( ) ;
const select = document . getElementById ( 'ttTemplate' ) ;
const emptyState = document . getElementById ( 'ttTemplateEmpty' ) ;
const filtered = availableTemplates . filter ( ( template ) => {
if ( ! search ) return true ;
const haystack = ` ${ template . name || '' } ${ template . description || '' } ${ template . category || '' } ` . toLowerCase ( ) ;
return haystack . includes ( search ) ;
} ) ;
select . innerHTML = filtered . map ( ( template ) => {
2026-08-17 19:27:43 +02:00
const scopeLabel = template . template _type === 'company' ? 'Firma' : template . template _type === 'global' ? 'Fælles' : 'Intern' ;
2026-05-01 20:58:13 +02:00
const cat = template . category || 'andet' ;
return ` <option value=" ${ template . id } "> ${ template . name } [ ${ scopeLabel } ] ( ${ cat } )</option> ` ;
} ) . join ( '' ) ;
emptyState . classList . toggle ( 'd-none' , filtered . length > 0 ) ;
document . getElementById ( 'ttRunBtn' ) . disabled = true ;
currentPreview = null ;
document . getElementById ( 'ttPreviewSummary' ) . textContent = 'Ingen preview endnu.' ;
document . getElementById ( 'ttPreviewList' ) . innerHTML = '' ;
}
function onTemplateSelected ( ) {
currentPreview = null ;
document . getElementById ( 'ttRunBtn' ) . disabled = true ;
document . getElementById ( 'ttPreviewSummary' ) . textContent = 'Template valgt. Klik "Opdater preview".' ;
document . getElementById ( 'ttPreviewList' ) . innerHTML = '' ;
}
function onAssigneeModeChanged ( ) {
const mode = document . getElementById ( 'ttAssigneeMode' ) . value ;
const userInput = document . getElementById ( 'ttAssigneeUserId' ) ;
const roleInput = document . getElementById ( 'ttAssigneeRoleId' ) ;
userInput . classList . toggle ( 'd-none' , mode !== 'specific_user' ) ;
roleInput . classList . toggle ( 'd-none' , mode !== 'specific_role' ) ;
}
function buildPayload ( ) {
const templateId = Number ( document . getElementById ( 'ttTemplate' ) . value ) ;
if ( ! templateId ) {
2026-08-17 19:27:43 +02:00
throw new Error ( 'Vælg en template' ) ;
2026-05-01 20:58:13 +02:00
}
const mode = document . getElementById ( 'ttMode' ) . value ;
const assigneeMode = document . getElementById ( 'ttAssigneeMode' ) . value ;
const startDate = document . getElementById ( 'ttStartDate' ) . value || todayIso ( ) ;
const assigneeUserIdRaw = document . getElementById ( 'ttAssigneeUserId' ) . value ;
const assigneeRoleIdRaw = document . getElementById ( 'ttAssigneeRoleId' ) . value ;
const payload = {
template _id : templateId ,
start _date : startDate ,
mode ,
assignee _mode : assigneeMode ,
} ;
if ( assigneeMode === 'specific_user' ) {
const parsed = Number ( assigneeUserIdRaw ) ;
if ( ! parsed ) throw new Error ( 'Udfyld medarbejder ID' ) ;
payload . assignee _user _id = parsed ;
}
if ( assigneeMode === 'specific_role' ) {
const parsed = Number ( assigneeRoleIdRaw ) ;
if ( ! parsed ) throw new Error ( 'Udfyld rolle ID' ) ;
payload . assignee _role _id = parsed ;
}
return payload ;
}
function renderPreview ( preview ) {
const summary = preview . summary || { } ;
const list = Array . isArray ( preview . items ) ? preview . items : [ ] ;
document . getElementById ( 'ttPreviewSummary' ) . textContent =
` Du er ved at oprette: ${ summary . subcases || 0 } undersager, ${ summary . tasks || 0 } opgaver, ${ summary . assignments || 0 } tildelinger, ${ summary . deadlines || 0 } deadlines. ` ;
document . getElementById ( 'ttPreviewList' ) . innerHTML = list . map ( ( item ) => {
const typeLabel = item . item _type === 'subcase' ? 'Undersag' : 'Task' ;
return `
< div class = "list-group-item" >
< div class = "d-flex justify-content-between align-items-start gap-3" >
< div >
< div class = "fw-semibold" > $ { escapeHtml ( item . title || '' ) } < / d i v >
< div class = "small text-muted" > $ { escapeHtml ( item . description || '' ) } < / d i v >
< / d i v >
< div class = "text-end small text-muted" >
< div > $ { typeLabel } < / d i v >
< div > $ { escapeHtml ( item . planned _due _date || '-' ) } < / d i v >
< / d i v >
< / d i v >
< / d i v >
` ;
} ) . join ( '' ) ;
}
async function runPreview ( ) {
if ( ! currentCaseId ) return ;
try {
const payload = buildPayload ( ) ;
const response = await fetch ( ` /api/v1/cases/ ${ currentCaseId } /template-preview ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
credentials : 'include' ,
body : JSON . stringify ( payload )
} ) ;
if ( ! response . ok ) {
const body = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
throw new Error ( body . detail || 'Preview fejlede' ) ;
}
const preview = await response . json ( ) ;
currentPreview = preview ;
renderPreview ( preview ) ;
document . getElementById ( 'ttRunBtn' ) . disabled = false ;
} catch ( error ) {
console . error ( 'Preview failed:' , error ) ;
notify ( error . message || 'Preview fejlede' , 'error' ) ;
}
}
async function runTemplate ( ) {
if ( ! currentCaseId ) return ;
if ( ! currentPreview ) {
2026-08-17 19:27:43 +02:00
notify ( 'Kør forhåndsvisning først' , 'error' ) ;
2026-05-01 20:58:13 +02:00
return ;
}
const runButton = document . getElementById ( 'ttRunBtn' ) ;
runButton . disabled = true ;
const originalText = runButton . textContent ;
runButton . textContent = 'Opretter...' ;
try {
const payload = buildPayload ( ) ;
const response = await fetch ( ` /api/v1/cases/ ${ currentCaseId } /run-template ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
credentials : 'include' ,
body : JSON . stringify ( payload )
} ) ;
if ( ! response . ok ) {
const body = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
2026-08-17 19:27:43 +02:00
throw new Error ( body . detail || 'Kørsel fejlede' ) ;
2026-05-01 20:58:13 +02:00
}
const result = await response . json ( ) ;
2026-08-17 19:27:43 +02:00
notify ( ` Template kørt. Oprettet ${ result . summary ? . tasks || 0 } opgaver og ${ result . summary ? . subcases || 0 } undersager. ` , 'success' ) ;
2026-05-01 20:58:13 +02:00
if ( typeof window . syncCaseTagsUi === 'function' ) {
window . syncCaseTagsUi ( ) ;
}
if ( typeof window . loadTodoSteps === 'function' ) {
window . loadTodoSteps ( ) ;
}
await loadRunHistory ( ) ;
document . getElementById ( 'ttRunBtn' ) . disabled = true ;
2026-08-17 19:27:43 +02:00
document . getElementById ( 'ttPreviewSummary' ) . textContent = 'Template kørt. Vælg en ny template eller opdater forhåndsvisningen igen.' ;
2026-05-01 20:58:13 +02:00
} catch ( error ) {
console . error ( 'Run failed:' , error ) ;
2026-08-17 19:27:43 +02:00
notify ( error . message || 'Kørsel fejlede' , 'error' ) ;
2026-05-01 20:58:13 +02:00
} finally {
runButton . disabled = false ;
runButton . textContent = originalText ;
}
}
function escapeHtml ( value ) {
return String ( value || '' )
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' ) ;
}
function formatDateTime ( value ) {
if ( ! value ) return '-' ;
const parsed = new Date ( value ) ;
if ( Number . isNaN ( parsed . getTime ( ) ) ) return String ( value ) ;
return parsed . toLocaleString ( 'da-DK' , {
year : 'numeric' ,
month : '2-digit' ,
day : '2-digit' ,
hour : '2-digit' ,
minute : '2-digit'
} ) ;
}
async function loadRunHistory ( ) {
const container = document . getElementById ( 'ttRunHistory' ) ;
if ( ! container || ! currentCaseId ) return ;
2026-08-17 19:27:43 +02:00
container . innerHTML = '<div class="text-muted">Indlæser historik...</div>' ;
2026-05-01 20:58:13 +02:00
try {
const response = await fetch ( ` /api/v1/cases/ ${ currentCaseId } /template-runs?limit=10 ` , {
credentials : 'include'
} ) ;
if ( ! response . ok ) {
const body = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
throw new Error ( body . detail || 'Kunne ikke hente historik' ) ;
}
const rows = await response . json ( ) ;
if ( ! Array . isArray ( rows ) || rows . length === 0 ) {
2026-08-17 19:27:43 +02:00
container . innerHTML = '<div class="text-muted">Ingen template-kørsler endnu.</div>' ;
2026-05-01 20:58:13 +02:00
return ;
}
container . innerHTML = rows . map ( ( run ) => {
const status = escapeHtml ( run . status || 'ukendt' ) ;
const templateName = escapeHtml ( run . template _name || ` Template # ${ run . template _id || '-' } ` ) ;
const startedAt = formatDateTime ( run . started _at ) ;
const taskCount = Number ( run . created _tasks || 0 ) ;
const subcaseCount = Number ( run . created _subcases || 0 ) ;
const statusClass = status === 'completed' ? 'text-success' : ( status === 'failed' ? 'text-danger' : 'text-muted' ) ;
return `
< div class = "border rounded p-2 mb-2" >
< div class = "d-flex justify-content-between align-items-start gap-2" >
< div >
< div class = "fw-semibold" > $ { templateName } < / d i v >
< div class = "small text-muted" > $ { escapeHtml ( startedAt ) } < / d i v >
< / d i v >
< span class = "small fw-semibold ${statusClass}" > $ { status } < / s p a n >
< / d i v >
< div class = "small mt-1" > Oprettet : $ { taskCount } opgaver , $ { subcaseCount } undersager < / d i v >
$ { run . template _id ? `
< div class = "mt-2" >
2026-08-17 19:27:43 +02:00
< button type = "button" class = "btn btn-sm btn-outline-primary" onclick = "window.reuseCaseTemplateFromHistory(${Number(run.template_id)})" > Vælg igen < / b u t t o n >
2026-05-01 20:58:13 +02:00
< / d i v >
` : ''}
$ { run . error _message ? ` <div class="small text-danger mt-1"> ${ escapeHtml ( run . error _message ) } </div> ` : '' }
< / d i v >
` ;
} ) . join ( '' ) ;
} catch ( error ) {
container . innerHTML = ` <div class="text-danger"> ${ escapeHtml ( error . message || 'Kunne ikke hente historik' ) } </div> ` ;
}
}
async function reuseTemplateFromHistory ( templateId ) {
const wantedId = Number ( templateId || 0 ) ;
if ( ! wantedId ) {
notify ( 'Ugyldigt template-id' , 'error' ) ;
return ;
}
const sourceSelect = document . getElementById ( 'ttSource' ) ;
const categorySelect = document . getElementById ( 'ttCategory' ) ;
const searchInput = document . getElementById ( 'ttTemplateSearch' ) ;
const templateSelect = document . getElementById ( 'ttTemplate' ) ;
// Widen filters to make sure template is available in selector.
sourceSelect . value = 'all' ;
categorySelect . value = '' ;
searchInput . value = '' ;
await refreshTemplates ( ) ;
const hasOption = Array . from ( templateSelect . options ) . some ( ( opt ) => Number ( opt . value ) === wantedId ) ;
if ( ! hasOption ) {
notify ( 'Template findes ikke i nuvaerende scope eller er deaktiveret' , 'error' ) ;
return ;
}
templateSelect . value = String ( wantedId ) ;
onTemplateSelected ( ) ;
await runPreview ( ) ;
}
async function openTaskTemplateSelectorModal ( detailOrCaseId ) {
ensureModal ( ) ;
if ( typeof detailOrCaseId === 'object' && detailOrCaseId !== null ) {
currentCaseId = Number ( detailOrCaseId . entity _id || detailOrCaseId . case _id || 0 ) ;
currentAction = detailOrCaseId . action || null ;
} else {
currentCaseId = Number ( detailOrCaseId || 0 ) ;
currentAction = null ;
}
if ( ! currentCaseId ) {
notify ( 'Kunne ikke finde sag-id til template modal' , 'error' ) ;
return ;
}
document . getElementById ( 'ttStartDate' ) . value = todayIso ( ) ;
document . getElementById ( 'ttTemplateSearch' ) . value = '' ;
document . getElementById ( 'ttAssigneeMode' ) . value = 'template_default' ;
document . getElementById ( 'ttAssigneeUserId' ) . value = '' ;
document . getElementById ( 'ttAssigneeRoleId' ) . value = '' ;
onAssigneeModeChanged ( ) ;
const actionConfig = getActionConfig ( ) ;
const sourceSelect = document . getElementById ( 'ttSource' ) ;
sourceSelect . value = 'all' ;
if ( ! actionConfig . allow _company _templates && actionConfig . allow _global _templates ) {
sourceSelect . value = 'global' ;
}
if ( actionConfig . allow _company _templates && ! actionConfig . allow _global _templates ) {
sourceSelect . value = 'company' ;
}
const categorySelect = document . getElementById ( 'ttCategory' ) ;
categorySelect . value = actionConfig . default _category || '' ;
await refreshTemplates ( ) ;
await loadRunHistory ( ) ;
modalInstance . show ( ) ;
}
window . reuseCaseTemplateFromHistory = reuseTemplateFromHistory ;
window . openTaskTemplateSelectorModal = openTaskTemplateSelectorModal ;
window . addEventListener ( 'hub:tag-action' , function ( event ) {
const detail = event && event . detail ? event . detail : null ;
if ( ! detail || ! detail . action || detail . action . type !== 'open_task_template_modal' ) {
return ;
}
// Auto-open is disabled. Users open the selector by clicking the template tag.
} ) ;
} ) ( ) ;