<headerclass="workflow-heading"><div><divclass="workflow-eyebrow">Administration</div><h1>Workflows <spanid="wf-runtime-pill"class="badge text-bg-secondary fs-6">Henter status…</span></h1><p>Byg regler, test resultatet og aktivér en gemt version. Tidligere versioner kan aktiveres igen.</p></div></header>
<mainclass="col-lg-9"><divclass="card p-3"><h5class="mb-3">Regler og afregning</h5>{% if not wp.get("manage") %}<pclass="workflow-readonly"><iclass="bi bi-eye me-1"></i>Du kan se definitioner og historik. Ændringer kræver administrationsadgang.</p>{% endif %}<fieldset{%ifnotwp.get("manage")%}disabled{%endif%}>
<divclass="row g-2 mt-2"><labelclass="col-md-4">Testkunde<selectid="wf-test-customer"class="form-select"><optionvalue="">Ingen kunde</option></select></label><labelclass="col-md-2">Timer<inputid="wf-test-hours"type="number"min="0"step="0.25"value="1"class="form-control"></label><labelclass="col-md-6">Beskrivelse<inputid="wf-test-description"class="form-control"placeholder="Fx hjælp med Excel"></label></div><pclass="small text-muted mt-2">Vælg en eksisterende sag eller tid ovenfor for at teste med de faktiske tags, aftaler og klippekort.</p>
const $=id=>document.getElementById('wf-'+id); let tagRows=[],customerRows=[],actionOptions={users:[],groups:[],workflows:[],agreements:[],cards:[]};let catalog={}, current=null, version=null, receipt=null, dirty=false;
let definition={trigger:'time.created',stop_after_match:false,branches:[{condition:{all:[{field:'description',operator:'contains',value:''}]},actions:[{type:'set_billing',method:'invoice'}]}]};
function message(text,error=false){$('message').textContent=text;$('message').className='alert '+(error?'alert-danger':'alert-info');}
async function api(path='',body){const response=await fetch('/api/v1/workflows'+path,{method:body?'POST':'GET',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});const data=await response.json();if(!response.ok)throw Error(typeof data.detail==='string'?data.detail:JSON.stringify(data.detail));return data;}
function changed(){dirty=true;receipt=null;$('activate').disabled=true;}
function element(tag,className,text){const el=document.createElement(tag);if(className)el.className=className;if(text!==undefined)el.textContent=text;return el;}
function select(values,value,onchange){const el=element('select','form-select form-select-sm');values.forEach(v=>{const opt=element('option','',labels[v]||v);opt.value=v;el.append(opt);});el.value=value;el.onchange=()=>{onchange(el.value);changed();};return el;}
function input(value,onchange){const el=element('input','form-control form-control-sm');el.value=typeof value==='string'?value:JSON.stringify(value??'');el.onchange=()=>{let v=el.value;try{v=JSON.parse(v);}catch{}onchange(v);changed();};return el;}
function button(text,fn){const el=element('button','btn btn-sm btn-outline-secondary',text);el.type='button';el.onclick=()=>{fn();changed();render();};return el;}
function namedSelect(rows,value,onchange,multiple=false){const el=element('select','form-select form-select-sm');el.multiple=multiple;if(!multiple){const placeholder=element('option','','Vælg…');placeholder.value='';el.append(placeholder);}for(const row of rows){const opt=element('option','',row.label||row.name);opt.value=String(row.id);opt.selected=(Array.isArray(value)?value.map(String):[String(value)]).includes(String(row.id));el.append(opt);}el.onchange=()=>{onchange(multiple?[...el.selectedOptions].map(o=>Number(o.value)):(el.value?Number(el.value):null));changed();};return el;}function conditionValue(node){if(['tags','tag_ids'].includes(node.field)){const box=element('div','');box.append(namedSelect(tagRows,node.value,v=>node.value=v,['in','not_in'].includes(node.operator)));const label=element('label','small',' Medtag undertags');const check=element('input');check.type='checkbox';check.checked=!!node.include_descendants;check.onchange=()=>{node.include_descendants=check.checked;changed();};label.prepend(check);box.append(label);return box;}if(node.field==='customer_id')return namedSelect(customerRows,node.value,v=>node.value=v===null?null:Array.isArray(v)?v.map(String):String(v),['in','not_in'].includes(node.operator));return input(node.value,v=>node.value=v);}function readableCondition(node,host,depth=0){if(!node)return;const names={customer_id:'Kunde',description:'Beskrivelse',hours:'Timer',tags:'Tags',tag_ids:'Tags',worked_date:'Arbejdsdato',status:'Status'},operators={eq:'er',ne:'er ikke',contains:'indeholder',not_contains:'indeholder ikke',in:'er en af',not_in:'er ingen af',gt:'større end',gte:'mindst',lt:'mindre end',lte:'højst',exists:'findes'};let text;if(node.children)text=node.kind==='all'?'Alle betingelser':'Mindst én betingelse';else if(node.kind==='else')text='Ellers';else{const values=Array.isArray(node.expected)?node.expected:[node.expected];const expected=values.map(v=>['tags','tag_ids'].includes(node.field)?tagRows.find(t=>Number(t.id)===Number(v))?.label||v:node.field==='customer_id'?customerRows.find(c=>String(c.id)===String(v))?.name||v:v).join(', ');text=(names[node.field]||node.field)+' '+(operators[node.operator]||node.operator)+' '+expected;}const line=element('div','small',`${node.matched?'✓':'×'} ${text}`);line.style.paddingLeft=(depth*16)+'px';host.append(line);for(const child of node.children||[])readableCondition(child,host,depth+1);}function leaf(){return {field:'description',operator:'contains',value:''};}
function loadVersion(v){version=v;definition=structuredClone(v.definition);const meta=definition.metadata||{};$('name').value=meta.name||current.name;$('customer').value=meta.customer_id??current.customer_id??'';$('priority').value=meta.priority??current.priority;$('trigger').value=definition.trigger;$('stop').checked=!!definition.stop_after_match;dirty=false;receipt=null;$('activate').disabled=true;render();}
async function open(id){current=await api('/'+id);$('versions').replaceChildren();current.versions.forEach(v=>{const opt=element('option','',`Version ${v.version}${current.active_version_id===v.id?' (aktiv)':''}`);opt.value=v.id;$('versions').append(opt);});loadVersion(current.versions[0]);$('history').replaceChildren();current.activation_history.forEach(h=>$('history').append(element('div','small',`${h.created_at} · version-ID ${h.version_id??'ingen'} · bruger ${h.created_by}`)));}
async function list(){const rows=await api();$('list').replaceChildren();if(!rows.length)$('list').append(element('div','workflow-empty','Ingen workflows endnu. Opret et workflow for at komme i gang.'));rows.forEach(row=>{const btn=element('button','list-group-item list-group-item-action',row.name+(row.enabled?' · aktiv':' · inaktiv'));btn.onclick=()=>guard(async()=>{await open(row.id);for(const item of $('list').children)item.classList.remove('active');btn.classList.add('active');});$('list').append(btn);});}
let requestBusy=false;async function guard(fn){if(requestBusy)return;requestBusy=true;document.querySelector('.workflow-page').setAttribute('aria-busy','true');try{await fn();}catch(error){message(error.message,true);}finally{requestBusy=false;document.querySelector('.workflow-page').setAttribute('aria-busy','false');}}
$('save').onclick=()=>guard(async()=>{definition.trigger=$('trigger').value;definition.stop_after_match=$('stop').checked;const draft={name:$('name').value,customer_id:$('customer').value||null,priority:Number($('priority').value),definition};const saved=await api(current?'/'+current.id+'/versions':'',draft);await list();await open(current?current.id:saved.id);message('Ny version gemt. Test den før aktivering.');});
$('test').onclick=()=>guard(async()=>{if(!current||dirty)throw Error('Gem ændringerne som en ny version før test');const entity=$('entity').value;const payload={version_id:version.id};if(entity){payload.entity_type=entity;payload.entity_id=Number($('entity-id').value);}else payload.context={customer_id:$('test-customer').value||null,description:$('test-description').value,hours:Number($('test-hours').value),tag_ids:[]};const data=await api('/'+current.id+'/simulate',payload);receipt=data.simulation_receipt;$('test-explanation').replaceChildren();const summary=element('p','',data.result.status==='decided'?'Afregning: '+(labels[data.result.method]||data.result.method):'Afventer manuel vurdering');$('test-explanation').append(summary);for(const trace of data.result.trace||[]){$('test-explanation').append(element('div','small fw-semibold',`Workflow #${trace.workflow_id} · gren ${trace.branch+1}: ${trace.matched?'opfyldt':'ikke opfyldt'}`));readableCondition(trace.condition,$('test-explanation'));}$('result').textContent=JSON.stringify(data.result,null,2);$('activate').disabled=false;});
$('activate').onclick=()=>guard(async()=>{if(!receipt||dirty)throw Error('Test versionen først');await api('/'+current.id+'/activate',{version_id:version.id,simulation_receipt:receipt});await list();await open(current.id);message('Den valgte version er aktiveret. Motorens featureflag styrer behandling.');});
$('disable').onclick=()=>guard(async()=>{if(!current)throw Error('Vælg et workflow');await api('/'+current.id+'/disable',{});await list();await open(current.id);message('Workflow deaktiveret.');});