<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><labelclass="d-block mt-2">Sagstitel til manuelle testdata<inputid="wf-test-case-title"class="form-control"placeholder="Fx TEST tid"></label><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 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;if(!entity&&$('entity-id').value)throw Error('Du har indtastet et ID. Vælg Sag eller Tidsregistrering under Eksisterende data, så testen bruger de faktiske oplysninger.');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,case_title:$('test-case-title').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):data.result.actions?.length?'Reglen matcher · '+data.result.actions.length+' handling(er) planlagt. Testen udfører dem ikke.':'Ingen handling matcher · '+(data.result.reason==='no_matching_rule'?'ingen matchende regel':'manuel vurdering'));$('test-explanation').append(summary);for(const trace of data.result.trace||[]){if(trace.source==='resource_coverage'){$('test-explanation').append(element('div','small',trace.matched?'Aftaler og klippekort: dækning matcher':'Aftaler og klippekort: ingen dækning matcher'));continue;}if(!Number.isInteger(trace.branch))continue;$('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.');});