125 lines
46 KiB
HTML
125 lines
46 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
||
{% block title %}Tidskø til afregning{% endblock %}
|
||
{% block content %}
|
||
<style>
|
||
.time-queue{max-width:none;color:var(--text-primary)}
|
||
.time-queue .text-primary,.time-queue .btn-link{color:var(--accent)!important}
|
||
.time-queue .btn-outline-primary{--bs-btn-color:var(--accent);--bs-btn-border-color:var(--frame-border-strong);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--accent);--bs-btn-hover-border-color:var(--accent);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--accent);--bs-btn-active-border-color:var(--accent)}
|
||
.time-queue .card,.queue-summary,.time-group{background:var(--bg-card);border:1px solid var(--bs-border-color);border-radius:.85rem;box-shadow:0 2px 8px rgba(0,0,0,.035)}
|
||
.queue-stat{min-width:110px}.queue-stat strong{font-size:1.35rem;color:var(--text-primary)}
|
||
.queue-summary .queue-stat+.queue-stat{border-left:1px solid var(--bs-border-color);padding-left:1.25rem}
|
||
.time-group{overflow:hidden}.time-group header{padding:.65rem 1rem!important;background:var(--accent-light);border-bottom:1px solid var(--bs-border-color)}
|
||
.time-row{padding:.8rem 1rem!important}.time-row+.time-row{border-top:1px solid var(--bs-border-color)}.time-row:hover{background:var(--accent-light)}
|
||
.time-row .row{display:grid;grid-template-columns:24px minmax(220px,2fr) minmax(140px,1fr) minmax(100px,.65fr) minmax(160px,1fr) minmax(150px,.8fr);gap:.75rem;align-items:center;margin:0}
|
||
.time-row .row>[class*="col"]{width:auto;max-width:none;flex:none;padding:0;min-width:0}
|
||
.time-row .small{font-size:.82rem;line-height:1.4}.time-row .fw-semibold,.time-row strong{font-size:.95rem;line-height:1.35}
|
||
.time-row .col-6 .small.text-muted,.time-row .col-8 .small.text-muted{display:none}
|
||
.time-row > .mt-2{margin-top:.35rem!important}.time-row .btn{font-size:.82rem}.time-row .case-details{text-align:left;white-space:normal}
|
||
.time-row .badge{font-weight:500}.queue-tags .badge{margin-bottom:.2rem}
|
||
.queue-actions{position:sticky;bottom:1rem;z-index:20;box-shadow:0 .3rem 1rem rgba(0,0,0,.1)!important}
|
||
.preview-group{border:1px solid var(--bs-border-color);background:var(--accent-light)!important}.empty-state{border:1px dashed var(--bs-border-color);border-radius:.85rem}
|
||
.queue-details summary{cursor:pointer;font-weight:500}.queue-details[open] summary{margin-bottom:1rem}
|
||
.queue-method-badge{background:var(--accent-light);color:var(--text-primary);border:1px solid var(--bs-border-color)}
|
||
.case-detail-meta{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.case-detail-meta div{padding:.65rem .75rem;background:var(--accent-light);border-radius:.55rem}.case-detail-meta small{display:block;color:var(--text-secondary);margin-bottom:.12rem}
|
||
.case-closed-badge{background:var(--accent-light);color:var(--text-primary);border:1px solid var(--bs-border-color)}.case-open-badge{background:var(--bg-card);color:var(--text-secondary);border:1px solid var(--bs-border-color)}
|
||
.preview-items{max-height:320px;overflow:auto}.preview-item{border-bottom:1px solid var(--bs-border-color);padding:.65rem 0}.preview-item:last-child{border-bottom:0}
|
||
@media(max-width:1250px){.time-row .row{grid-template-columns:24px minmax(210px,2fr) minmax(135px,1fr) minmax(100px,.8fr)}.time-row .col-8{grid-column:2/4}.time-row .col-4{grid-column:4;text-align:right}}
|
||
@media(max-width:720px){.time-row .row{display:flex;flex-wrap:wrap;gap:.5rem}.time-row .col-12.col-lg-4{flex:1 1 calc(100% - 32px)}.time-row .col-6,.time-row .col-8{flex:1 1 auto}.time-row .col-4{margin-left:auto}.case-detail-meta{grid-template-columns:1fr}.triage-tabs{display:flex;flex-wrap:wrap;gap:.35rem}.triage-tabs>.btn{border-radius:.4rem!important}}
|
||
|
||
/* Compact work list: one selection bar, short rows and progressive details. */
|
||
.time-queue{padding-top:1rem!important;padding-bottom:5rem!important}
|
||
.time-queue h1{font-size:1.5rem}.time-queue main>p{margin:0}
|
||
.queue-summary{box-shadow:none}.queue-stat{min-width:0;display:flex;align-items:baseline;gap:.5rem}.queue-stat strong{font-size:1rem}
|
||
.queue-summary .queue-stat+.queue-stat{padding-left:1rem}.queue-summary .gap-4{gap:1rem!important}
|
||
.queue-filters .form-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}
|
||
.queue-filters .form-control,.queue-filters .form-select,.queue-filters .btn{font-size:.875rem;min-height:34px;padding-top:.3rem;padding-bottom:.3rem}
|
||
.queue-filters .queue-details{margin-top:.4rem!important}.queue-details summary{font-size:.82rem;color:var(--text-secondary)}
|
||
.queue-obsolete{display:none!important}
|
||
.time-group{margin-bottom:.6rem!important;box-shadow:none}.time-group header{padding:.4rem .75rem!important}
|
||
.time-group header strong{font-size:.85rem}.time-group header .btn{padding:.15rem .45rem;font-size:.75rem}
|
||
.time-row{position:relative;padding:.5rem .75rem!important}
|
||
.time-row .row{grid-template-columns:22px minmax(220px,2fr) minmax(170px,1fr) 65px minmax(170px,1fr) 80px;gap:.5rem}
|
||
.time-row .col-12.col-lg-4>.small{display:none}
|
||
.time-row .badge{font-size:.72rem}.time-row .btn{font-size:.8rem}
|
||
.queue-description{margin:.2rem 0 0 30px;max-width:75%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.queue-tags{display:inline-block;margin:.2rem 0 0 30px!important;padding:0!important}
|
||
.queue-tags .badge{padding:.2rem .35rem;background:transparent!important;border:0!important;color:var(--text-secondary);font-size:.72rem}
|
||
.queue-issue{margin:.2rem 0 0 30px}
|
||
.queue-row-details{display:inline-block;margin-left:.6rem;font-size:.75rem;color:var(--text-secondary)}
|
||
.queue-row-details summary{cursor:pointer}.queue-row-details[open]{display:block;margin:.4rem 0 0 30px}
|
||
.queue-actions{bottom:4.5rem;border-color:var(--accent);background:var(--bg-card)}
|
||
@media(max-width:1100px){.time-row .row{grid-template-columns:22px minmax(160px,2fr) minmax(150px,1fr) 60px}.time-row .col-8{grid-column:2/4}.time-row .col-4{grid-column:4}.queue-description{max-width:90%}}
|
||
@media(max-width:720px){.time-row .row{display:flex}.queue-actions{bottom:4rem}.queue-stat{font-size:.8rem}.queue-tags{display:block}.queue-description{white-space:normal}}
|
||
</style>
|
||
<main class="container-fluid py-4 time-queue">
|
||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><div class="text-primary text-uppercase fw-semibold small">Økonomi</div><h1 class="h2 mb-1">Tidskø til afregning</h1><p class="text-muted mb-0">Kontrollér afregningen, godkend tiden og send den videre.</p></div><button class="btn btn-outline-secondary" id="reloadBtn"><i class="bi bi-arrow-clockwise me-1"></i>Opdatér</button></div>
|
||
<div id="messageArea" aria-live="polite"></div>
|
||
<div class="btn-group mb-3" aria-label="Tidskø"><button class="btn btn-primary scope-tab" data-scope="active">Aktiv kø</button><button class="btn btn-outline-primary scope-tab" data-scope="history">Historik</button></div>
|
||
<section class="queue-summary py-2 px-3 mb-2"><div class="d-flex flex-wrap gap-4 align-items-center"><div class="queue-stat"><div class="small text-muted">Registreringer</div><strong id="statEntries">–</strong></div><div class="queue-stat"><div class="small text-muted">Timer</div><strong id="statHours">–</strong></div><div class="queue-stat"><div class="small text-muted">Valgt</div><strong id="statSelected">0</strong></div><div class="d-none"><span id="statInvoice"></span><span id="statClosedCases"></span></div></div></section>
|
||
<section class="card mb-2 queue-filters"><div class="card-body py-2"><div class="row g-2 align-items-end"><div class="col-12 col-md-6"><label class="form-label" for="filterQuery">Søg</label><input id="filterQuery" class="form-control" placeholder="Kunde, sag eller beskrivelse"></div><div class="col-12 col-md-4"><label class="form-label" for="filterCustomer">Kunde</label><select id="filterCustomer" class="form-select"><option value="">Alle firmaer</option></select></div><div class="col-12 col-md-2"><button class="btn btn-outline-secondary w-100" id="clearFiltersBtn">Ryd filtre</button></div></div><details class="queue-details mt-3"><summary>Flere filtre</summary><div class="row g-3"><div class="col-6 col-lg-3"><label class="form-label" for="filterStatus">Tidsstatus</label><select id="filterStatus" class="form-select"><option value="">Alle statusser</option><option value="pending">Afventer</option><option value="approved">Godkendt</option><option value="rejected">Afvist</option></select></div><div class="col-6 col-lg-3"><label class="form-label" for="filterCaseState">Sagens status</label><select id="filterCaseState" class="form-select"><option value="">Alle sager (inkl. løst/lukket)</option><option value="closed">Løst eller lukket</option><option value="open">Åbne sager</option><option value="no_case">Uden sag</option></select></div><div class="col-6 col-lg-3"><label class="form-label" for="filterBillable">Fakturerbar</label><select id="filterBillable" class="form-select"><option value="">Alle</option><option value="true">Ja</option><option value="false">Nej</option></select></div><div class="col-6 col-lg-3"><label class="form-label" for="filterMethod">Afregningsmetode</label><select id="filterMethod" class="form-select"><option value="">Alle metoder</option><option value="invoice">Faktura</option><option value="subscription">Fastpris</option><option value="prepaid">Klippekort</option><option value="internal">Intern tid</option><option value="non_billable">Ikke fakturerbar</option><option value="draft">Kladde</option><option value="operations">Drift</option><option value="development">Udvikling</option><option value="manual_review">Manuel vurdering</option></select></div></div></details></div></section>
|
||
<section class="d-none queue-obsolete" id="billingSelectionPanel"><div class="card-body"><div class="d-flex justify-content-between flex-wrap gap-2 mb-3"><div><h2 class="h5 mb-0">Valgte registreringer</h2><div class="small text-muted">Behold forslagene, eller justér afregningen.</div></div><span class="badge text-bg-primary align-self-center" id="selectedBadge">0 valgt</span></div><div class="d-flex justify-content-end mb-2"><button class="btn btn-outline-primary me-2" id="approveBtn">Godkend</button><button class="btn btn-primary" id="previewBtn"><i class="bi bi-eye me-1"></i>Godkend og send / afregn…</button></div><details class="queue-details d-none" id="manualBillingDetails"><summary>Justér afregning manuelt</summary><div class="row g-2 align-items-end"><div class="col-12 col-lg-3"><label class="form-label">Afregningstype</label><select class="form-select" id="settlementMethod"><option value="">Behold den enkelte tids afregningstype</option><option value="invoice">Faktura</option><option value="prepaid">Klippekort</option><option value="subscription">Abonnement / fast pris</option><option value="internal">Intern tid</option><option value="non_billable">Ikke-fakturerbar tid</option></select></div><div class="col-12 col-lg-3"><label class="form-label">Klippekort</label><select class="form-select" id="prepaidCard" disabled><option value="">Vælg først tider fra ét firma</option></select><div class="form-text" id="prepaidHelp">Vælg tidsregistreringer for at se kundens klippekort.</div></div><div class="col-12 col-lg-2"><label class="form-label">Aftale-ID</label><input id="agreementId" class="form-control" type="number" min="1" placeholder="Kun abonnement"></div><div class="col-12 col-lg-4 d-flex gap-2"><button class="btn btn-outline-primary flex-grow-1" id="setMethodBtn">Gem afregning</button></div></div></details></div></section>
|
||
<div id="workflowLineModeHost" class="mb-2 d-none"><label class="form-label d-flex align-items-center gap-2">Fakturalinjer<select id="workflowLineMode" class="form-select form-select-sm" style="width:auto"><option value="separate">En linje pr. tidsregistrering</option><option value="combined">Saml ens faktureringsvilkår</option></select></label><p class="small text-muted mb-0">Afregning ændres direkte på den enkelte registrering.</p></div><section id="previewPanel" class="card border-primary mb-3 d-none"><div class="card-header bg-primary-subtle d-flex justify-content-between align-items-center"><div><strong>Forhåndsvisning af afregning</strong><div class="small text-muted">Dette er en kontrol uden ændringer. Kontrollér resultatet, før du godkender og sender eller afregner.</div></div><button class="btn btn-sm btn-outline-secondary" id="closePreviewBtn">Luk</button></div><div class="card-body" id="previewBody"></div><div class="card-footer"><div class="small text-muted mb-2">Faktura opretter en lokal ordrekladde. Klippekort trækker timer. Fastpris knytter timer til aftalen. Intet sendes eller bogføres i e-conomic her.</div><div class="text-end"><button class="btn btn-success" id="settleBtn"><i class="bi bi-check2-circle me-1"></i>Godkend og gennemfør</button></div></div></section>
|
||
<div class="modal fade" id="caseDetailModal" tabindex="-1" aria-labelledby="caseDetailTitle" aria-hidden="true"><div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><div class="small text-primary text-uppercase fw-semibold">Sag uden at forlade tidskøen</div><h2 class="modal-title h4 mb-0" id="caseDetailTitle">Sagsdetaljer</h2></div><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button></div><div class="modal-body" id="caseDetailBody"></div><div class="modal-footer"><a class="btn btn-outline-primary" id="openCaseNewTab" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right me-1"></i>Åbn sag i ny fane</a><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button></div></div></div></div>
|
||
<div class="d-flex flex-wrap gap-2 align-items-center mb-3"><div class="btn-group triage-tabs" role="group" aria-label="Gennemgang"><button class="btn btn-primary triage-tab" data-triage="all">Alle <span id="countAll">0</span></button><button class="btn btn-outline-primary triage-tab" data-triage="ready">Klar <span id="countReady">0</span></button><button class="btn btn-outline-primary triage-tab" data-triage="action">Kræver handling <span id="countAction">0</span></button></div><button class="btn btn-outline-primary btn-sm" id="selectReadyBtn">Vælg alle klare</button></div>
|
||
<section id="queueContent" aria-live="polite"></section>
|
||
<div class="queue-actions card d-none" id="selectionActions"><div class="card-body py-2 d-flex flex-wrap justify-content-between align-items-center gap-2"><div><strong id="selectionText">0 tidsregistreringer valgt</strong></div><div class="d-flex gap-2"><button class="btn btn-sm btn-outline-secondary" id="clearSelectionBtn">Fravælg</button><button class="btn btn-sm btn-outline-primary" id="stickyApproveBtn">Godkend</button><button class="btn btn-sm btn-primary" id="stickyPreviewBtn">Godkend og send / afregn…</button></div></div></div>
|
||
</main>
|
||
<script src="/static/js/time-billing-review.js?v=20261009-1"></script>
|
||
<script>
|
||
(() => {
|
||
const state={items:[],selected:new Set(),preview:null,loading:false,cards:[],workflowEnabled:false,triage:'all',previewPayload:null,previewFingerprint:null,scope:'active'}; const $=id=>document.getElementById(id);
|
||
const labels={pending:'Afventer',approved:'Godkendt',rejected:'Afvist',invoice:'Faktura',prepaid:'Klippekort',subscription:'Fastpris',internal:'Intern tid',non_billable:'Ikke-fakturerbar tid',draft:'Kladde',operations:'Drift',development:'Udvikling',manual_review:'Manuel vurdering'};
|
||
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const h=v=>Number(v||0).toLocaleString('da-DK',{maximumFractionDigits:2});
|
||
const norm=v=>({fixed_price:'subscription',prepaid_card:'prepaid'})[String(v||'draft').toLowerCase()]||String(v||'draft').toLowerCase(); const ids=()=>[...state.selected];
|
||
function message(type,text){$('messageArea').innerHTML=`<div class="alert alert-${type} alert-dismissible fade show">${esc(text)}<button class="btn-close" data-bs-dismiss="alert"></button></div>`}
|
||
async function api(url,options={}){const r=await fetch(url,{headers:{'Content-Type':'application/json'},...options});const d=await r.json().catch(()=>({}));if(!r.ok){const e=new Error(typeof d.detail==='object'?(d.detail.message||'Handlingen kunne ikke gennemføres'):(d.detail||'Handlingen kunne ikke gennemføres'));e.payload=d;throw e}return d}
|
||
function actionable(row){return state.scope!=='history'&&(row.can_approve===true||row.can_send===true||row.can_settle===true);}
|
||
function approvalSnapshot(row){return {id:Number(row.id),updated_at:row.updated_at,entry_fingerprint:row.entry_fingerprint,billing_method:row.billing_method,prepaid_card_id:row.prepaid_card_id??null,fixed_price_agreement_id:row.fixed_price_agreement_id??null,workflow_revision:row.workflow_revision??row.workflow_decision?.revision??null,workflow_decision_id:row.workflow_decision?.id??null};}
|
||
function effectiveMethod(row){if(['sent','invoiced','settled'].includes(row.lifecycle_status))return norm(row.billing_method);if(row.workflow_active){const d=row.workflow_decision;if(row.workflow_stale||!d)return 'draft';if(d.status==='manual_review')return 'manual_review';if(!['decided','overridden','settled'].includes(d.status))return 'draft';return norm(d.method);}return norm(row.billing_method);}
|
||
function reviewIssue(row){if(['sent','invoiced','settled'].includes(row.lifecycle_status))return '';const m=effectiveMethod(row);if(row.workflow_active){if(row.workflow_stale)return 'Beslutningen er forældet. Genberegn under Hvorfor?.';if(!row.workflow_decision)return 'Afventer workflowbeslutning. Åbn Hvorfor?.';if(m==='manual_review'){const reasons={ambiguous_resource_coverage:'Flere aftaler eller kort matcher.',conflicting_rules:'Reglerne vælger forskellige metoder.',pending_tag_approval:'Tags afventer godkendelse.',insufficient_card_balance:'Klippekortet har ikke timer nok.',ambiguous_or_missing_cards:'Klippekort mangler eller er ikke entydigt.',ambiguous_or_missing_agreements:'Aftale mangler eller er ikke entydig.',no_matching_rule:'Ingen regel matcher.'};return (reasons[row.workflow_decision?.details?.reason]||'Afregningen kræver manuel vurdering.')+' Gennemgå under Hvorfor?.';}}if(m==='draft')return 'Afregning mangler.';if(['operations','development'].includes(m))return 'Afventer chefkontrol og valg af afregning.';if(!['invoice','prepaid','subscription','internal','non_billable','operations','development'].includes(m))return 'Afregningsmetoden skal gennemgås.';if(Number(row.approved_hours||row.original_hours||0)<=0)return 'Mangler positivt timeantal.';if(row.status==='rejected')return 'Tiden er afvist.';if(m==='invoice'&&row.billable===false)return 'Faktura er valgt på ikke-fakturerbar tid.';if(m==='invoice'&&!row.billing_customer_id&&!row.customer_id&&!row.hub_customer_id)return 'Mangler faktureringskunde.';if(m==='prepaid'&&!row.prepaid_card_id)return 'Mangler klippekort.';if(m==='subscription'&&!row.fixed_price_agreement_id)return 'Mangler aftale.';return '';}
|
||
function caseTags(row){return (row.case_tags||[]).map(tag=>`<span class="badge text-bg-light border me-1" title="${esc(tag.name)}"><i class="bi bi-tag me-1"></i>${esc(tag.name)}</span>`).join('');}
|
||
function workflowExplanation(row){if(!row.workflow_active)return '';if(reviewIssue(row))return '';const reasons={resource_coverage:'Valgt ud fra aftalens eller kortets dækning.',matched_rule:'Valgt af en workflowregel.',manual_override:'Manuelt overstyret med begrundelse.',no_matching_rule:'Ingen regel matcher.'};return reasons[row.workflow_decision?.details?.reason]||'Valgt af workflow.';}
|
||
function selectionMode(rows){const active=rows.filter(x=>x.workflow_active===true).length;return active===0?'legacy':active===rows.length?'workflow':'mixed';}
|
||
function allWorkflowSelected(){return selectedRows().length>0&&selectionMode(selectedRows())==='workflow'}
|
||
function invalidatePreview(){state.previewGeneration=(state.previewGeneration||0)+1;state.preview=null;state.previewPayload=null;state.previewFingerprint=null;$('previewPanel').classList.add('d-none');$('settleBtn').disabled=true;}
|
||
function syncWorkflowControls(){const mode=selectionMode(selectedRows()),active=mode!=='legacy'||selectedRows().some(x=>x.can_change_billing===false);$('workflowLineModeHost').classList.toggle('d-none',mode!=='workflow');for(const id of ['settlementMethod','prepaidCard','agreementId','setMethodBtn']){$(id).disabled=active;if(active&&id!=='setMethodBtn')$(id).value='';}$('approveBtn').disabled=!selectedRows().length||selectedRows().some(x=>x.can_approve!==true);$('stickyApproveBtn').disabled=$('approveBtn').disabled;const allApproved=selectedRows().length>0&&selectedRows().every(x=>x.lifecycle_status==='approved');$('previewBtn').innerHTML=allApproved?'Send til fakturering / afregn…':'Godkend og send / afregn…';$('stickyPreviewBtn').textContent=allApproved?'Send til fakturering / afregn…':'Godkend og send / afregn…';$('previewBtn').disabled=mode==='mixed'||!selectedRows().length||selectedRows().some(x=>x.can_settle!==true);$('stickyPreviewBtn').disabled=$('previewBtn').disabled;$('selectionText').textContent=mode==='mixed'?'Blandet udvalg: vælg workflowtider og øvrige tider hver for sig.':`${ids().length} tidsregistreringer valgt`;return active;}
|
||
function payload(){return {ids:ids(),line_mode:$('workflowLineMode').value||'separate'};}
|
||
function fingerprint(){return JSON.stringify({generation:state.previewGeneration||0,request:payload(),method:$('settlementMethod').value,card:$('prepaidCard').value,agreement:$('agreementId').value,line:$('workflowLineMode').value,triage:state.triage,filters:['filterMethod','filterCustomer','filterStatus','filterCaseState','filterBillable','filterQuery'].map(id=>$(id).value)});}
|
||
function lifecycle(row){return `<span class="badge ${['approved','invoiced','settled'].includes(row.lifecycle_status)?'text-bg-success':row.lifecycle_status==='sent'?'text-bg-info':'text-bg-warning'}">${esc(row.lifecycle_label||labels[row.status]||row.status||'Afventer godkendelse')}</span>`;}
|
||
function status(v){return `<span class="badge ${v==='approved'?'text-bg-success':v==='rejected'?'text-bg-danger':'text-bg-warning'}">${esc(labels[v]||v||'Ukendt')}</span>`} function method(v){const m=norm(v),cls='queue-method-badge';return `<span class="badge ${cls}">${esc(labels[m]||m)}</span>`}
|
||
function isClosedCase(value){return ['lukket','løst','closed','resolved','afsluttet','done'].includes(String(value||'').trim().toLocaleLowerCase('da-DK'))} function caseStatus(v){if(!v)return '<span class="badge text-bg-light border">Uden sag</span>';return `<span class="badge ${isClosedCase(v)?'case-closed-badge':'case-open-badge'}">Sag: ${esc(v)}</span>`}
|
||
function renderStats(){const selected=state.items.filter(x=>state.selected.has(Number(x.id)));$('statEntries').textContent=state.items.length;$('statHours').textContent=`${h(state.items.reduce((a,x)=>a+Number(x.approved_hours||x.original_hours||0),0))} t`;$('statInvoice').textContent=state.items.filter(x=>effectiveMethod(x)==='invoice'&&x.billable!==false).length;$('statClosedCases').textContent=state.items.filter(x=>isClosedCase(x.case_status)).length;$('statSelected').textContent=selected.length;$('selectedBadge').textContent=`${selected.length} valgt`;$('selectionText').textContent=`${selected.length} tidsregistrering${selected.length===1?'':'er'} valgt`;$('selectionActions').classList.toggle('d-none',!selected.length);$('billingSelectionPanel').classList.add('d-none')}
|
||
function selectedRows(){return state.items.filter(x=>state.selected.has(Number(x.id)))}
|
||
function selectedCardCustomerIds(){return [...new Set(selectedRows().map(x=>Number(x.billing_customer_id||x.hub_customer_id||0)).filter(Boolean))]}
|
||
function selectedHours(){return selectedRows().reduce((total,x)=>total+Number(x.approved_hours||x.original_hours||0),0)}
|
||
function actualHours(rows){return rows.reduce((total,x)=>total+Number(x.faktisk_tid_min!=null?x.faktisk_tid_min:Number(x.original_hours||0)*60)/60,0)}
|
||
function cardHours(rows,block){const safeBlock=Math.max(1,Number(block)||30);return rows.reduce((total,x)=>{const minutes=Math.max(0,Math.round(Number(x.faktisk_tid_min!=null?x.faktisk_tid_min:Number(x.original_hours||0)*60)));return total+(minutes?Math.ceil(minutes/safeBlock)*safeBlock/60:0)},0)}
|
||
function renderPrepaidHelp(){const selected=selectedRows(),customers=selectedCardCustomerIds(),card=state.cards.find(x=>Number(x.id)===Number($('prepaidCard').value)),storedHours=selectedHours();if(!selected.length){$('prepaidHelp').textContent='Vælg tidsregistreringer for at se kundens klippekort.';return}if(customers.length!==1){$('prepaidHelp').innerHTML='<span class="text-danger">Vælg kun tider fra ét firma for at bruge ét klippekort.</span>';return}const company=selected[0].customer_name||'den valgte kunde';if(!card){$('prepaidHelp').textContent=`${esc(company)} · Fakturerbar tid: ${h(storedHours)} timer. Vælg et klippekort for at se kortets afrunding.`;return}const block=Number(card.rounding_minutes)||30,actual=actualHours(selected),hours=cardHours(selected,block),remaining=Number(card.remaining_hours||0),after=remaining-hours;$('prepaidHelp').innerHTML=`${esc(company)} · Faktisk tid: <strong>${h(actual)} t</strong> → klippes som <strong>${h(hours)} t</strong> <span class="text-muted">(${block} min. afrunding pr. registrering)</span>. Kortet har ${h(remaining)} t tilbage → <strong class="${after<0?'text-danger':'text-success'}">${h(after)} t efter afregning</strong>.`}
|
||
function methodFilteredItems(){const m=$('filterMethod').value;return state.items.filter(x=>!m||effectiveMethod(x)===m)} function visibleItems(){return methodFilteredItems().filter(x=>state.triage==='all'||(state.triage==='ready'?!reviewIssue(x):!!reviewIssue(x)))} function render(){renderStats();syncWorkflowControls();const filtered=methodFilteredItems();$('countAll').textContent=filtered.length;$('countReady').textContent=filtered.filter(x=>!reviewIssue(x)).length;$('countAction').textContent=filtered.filter(x=>!!reviewIssue(x)).length;document.querySelectorAll('.triage-tab').forEach(x=>{x.classList.toggle('btn-primary',x.dataset.triage===state.triage);x.classList.toggle('btn-outline-primary',x.dataset.triage!==state.triage)});if(!visibleItems().length){$('queueContent').innerHTML='<div class="empty-state py-5 text-center text-muted"><i class="bi bi-check2-circle fs-2 d-block text-success mb-2"></i>Ingen tidsregistreringer matcher dine filtre.</div>';return}const groups={};visibleItems().forEach(x=>{const key=`${x.customer_id||0}:${x.customer_name||'Ukendt kunde'}`;(groups[key]??=[]).push(x)});$('queueContent').innerHTML=Object.values(groups).map(items=>{const f=items[0],total=items.reduce((a,x)=>a+Number(x.approved_hours||x.original_hours||0),0);return `<article class="time-group mb-3"><header class="px-3 py-2 d-flex justify-content-between flex-wrap gap-2"><div><strong>${esc(f.customer_name||'Ukendt kunde')}</strong><span class="small text-muted ms-2">${items.length} registreringer · ${h(total)} timer</span></div><button class="btn btn-sm btn-outline-primary customer-select" data-ids="${items.filter(actionable).map(x=>x.id).join(',')}" ${items.some(actionable)?'':'disabled'}>Vælg tider</button></header>${items.map(x=>{const id=Number(x.id),m=effectiveMethod(x),issue=reviewIssue(x);return `<div class="time-row p-3" data-method="${esc(m)}"><div class="row align-items-center g-2"><div class="col-auto"><input class="form-check-input row-check" type="checkbox" data-id="${id}" ${state.selected.has(id)?'checked':''} ${actionable(x)?'':'disabled'}></div><div class="col-12 col-lg-4"><div class="fw-semibold">${x.sag_id?`<button type="button" class="btn btn-link p-0 text-decoration-none fw-semibold case-details" data-sag-id="${Number(x.sag_id)}">#${Number(x.sag_id)} · ${esc(x.case_title)}</button>`:esc(x.case_title||'Ingen sag')}</div><div class="small text-muted">${caseStatus(x.case_status)} ${esc(x.contact_name||'Ingen kontakt')} · ${esc(x.employee_name||'Ukendt medarbejder')}</div></div><div class="col-6 col-lg-2"><div class="small text-muted">Dato og status</div>${esc(x.worked_date||'Ingen dato')} ${lifecycle(x)}</div><div class="col-6 col-lg-2"><div class="small text-muted">Tid</div><strong>${h(x.approved_hours||x.original_hours)} t</strong><span class="small text-muted"> · ${esc(x.work_type||x.entry_type||'arbejde')}</span></div><div class="col-8 col-lg-2"><div class="small text-muted">Afregning</div>${method(m)} ${state.scope!=='history'&&x.can_change_billing?`<button class="btn btn-link btn-sm p-0 ms-1 change-billing" data-id="${id}">Ændr</button>`:''} ${x.billable===false?'<span class="small text-muted">ikke fakturerbar</span>':''}</div><div class="col-4 col-lg-2 text-lg-end">${x.workflow_active?`<a class="btn btn-sm btn-outline-primary me-1" href="/workflows/time/${id}" target="_blank" rel="noopener">Hvorfor?</a>`:''}${x.order_url?`<a class="btn btn-sm btn-outline-secondary" href="${esc(x.order_url)}">Ordre</a>`:''}${x.invoice_url?`<a class="btn btn-sm btn-outline-secondary" href="${esc(x.invoice_url)}">Faktura</a>`:''}</div></div>${x.description?`<div class="queue-description small text-muted" title="${esc(x.description)}">${esc(x.description)}</div>`:''}${x.case_tags?.length?`<div class="small mt-2 ps-lg-4 queue-tags">${caseTags(x)}</div>`:''}${issue?`<div class="small text-warning queue-issue">${esc(issue)}</div>`:''}<details class="queue-row-details"><summary>Detaljer</summary><div class="small text-muted">${esc(x.employee_name||'Ukendt medarbejder')} · ${esc(x.contact_name||'Ingen kontakt')} · ${caseStatus(x.case_status)}${workflowExplanation(x)?`<div>${esc(workflowExplanation(x))}</div>`:''}${!x.description?'<div>Mangler beskrivelse</div>':''}</div></details></div>`}).join('')}</article>`}).join('');document.querySelectorAll('.row-check').forEach(e=>e.addEventListener('change',()=>{const id=Number(e.dataset.id);e.checked?state.selected.add(id):state.selected.delete(id);selectionChanged()}));document.querySelectorAll('.change-billing').forEach(e=>e.addEventListener('click',()=>changeBilling(Number(e.dataset.id))));document.querySelectorAll('.case-details').forEach(e=>e.addEventListener('click',()=>openCaseDetails(Number(e.dataset.sagId))));document.querySelectorAll('.select-one').forEach(e=>e.addEventListener('click',()=>{state.selected=new Set([Number(e.dataset.id)]);selectionChanged();$('selectionActions').scrollIntoView({behavior:'smooth',block:'nearest'})}));document.querySelectorAll('.customer-select').forEach(e=>e.addEventListener('click',()=>{const group=e.dataset.ids.split(',').map(Number),allSelected=group.every(id=>state.selected.has(id));group.forEach(id=>allSelected?state.selected.delete(id):state.selected.add(id));selectionChanged()}))}
|
||
function url(){const p=new URLSearchParams({limit:'500',scope:state.scope});if($('filterCaseState').value)p.set('case_state',$('filterCaseState').value);if($('filterCustomer').value)p.set('customer_id',$('filterCustomer').value);if($('filterStatus').value)p.set('status',$('filterStatus').value);if($('filterBillable').value)p.set('billable',$('filterBillable').value);if($('filterQuery').value.trim())p.set('q',$('filterQuery').value.trim());return `/api/v1/economy/time-queue?${p}`}
|
||
async function loadCustomers(){try{const d=await api('/api/v1/economy/time-queue/customers'),v=$('filterCustomer').value;$('filterCustomer').innerHTML=`<option value="">Alle firmaer</option>${(d.items||[]).map(x=>`<option value="${Number(x.customer_id)}">${esc(x.customer_name)} (${Number(x.open_count||0)})</option>`).join('')}`;$('filterCustomer').value=v}catch(_){message('warning','Firmafilteret kunne ikke hentes.') }}
|
||
async function loadCards(){const selectionKey=ids().join(',');if(selectionMode(selectedRows())!=='legacy'){$('prepaidHelp').textContent='Afregningen følger workflowbeslutningerne.';return;}const select=$('prepaidCard'),customers=selectedCardCustomerIds(),current=Number(select.value);state.cards=[];select.disabled=true;if(customers.length!==1){select.innerHTML=`<option value="">${customers.length>1?'Vælg tider fra ét firma':'Vælg først tider fra ét firma'}</option>`;renderPrepaidHelp();return}try{const d=await api(`/api/v1/economy/time-queue/prepaid-cards?customer_id=${customers[0]}`);if(selectionKey!==ids().join(',')||selectionMode(selectedRows())!=='legacy')return;state.cards=(d.items||[]).filter(x=>x.status==='active'&&Number(x.remaining_hours)>0);select.innerHTML=`<option value="">Vælg kundens klippekort</option>${state.cards.map(x=>`<option value="${Number(x.id)}">${esc(x.card_number)} · ${h(x.remaining_hours)} timer tilbage</option>`).join('')}`;select.disabled=!state.cards.length;if(state.cards.some(x=>Number(x.id)===current))select.value=String(current);renderPrepaidHelp()}catch(_){select.innerHTML='<option value="">Klippekort kunne ikke hentes</option>';renderPrepaidHelp()}}
|
||
async function load(){if(state.loading){state.reloadPending=true;return;}state.loading=true;const loadedScope=state.scope;invalidatePreview();$('queueContent').innerHTML='<div class="empty-state py-5 text-center text-muted"><span class="spinner-border spinner-border-sm me-2"></span>Henter tidsregistreringer…</div>';try{const d=await api(url());if(loadedScope!==state.scope){state.reloadPending=true;return;}state.items=d.items||[];state.workflowEnabled=!!d.workflow_enabled;state.selected=new Set(ids().filter(id=>state.items.some(x=>Number(x.id)===id&&actionable(x))));state.preview=null;$('previewPanel').classList.add('d-none');render();await loadCards()}catch(e){$('queueContent').innerHTML=`<div class="alert alert-danger">${esc(e.message)}</div>`}finally{state.loading=false;if(state.reloadPending){state.reloadPending=false;load();}}}
|
||
async function approveSelected(){const rows=selectedRows();if(!rows.length||rows.some(x=>!x.can_approve))return message('warning','Vælg tider, der afventer godkendelse.');invalidatePreview();$('approveBtn').disabled=true;$('stickyApproveBtn').disabled=true;try{await api('/api/v1/economy/time-queue/approve-reviewed',{method:'POST',body:JSON.stringify({ids:ids(),snapshots:rows.map(approvalSnapshot)})});message('success','Tiderne er godkendt. Ingen timer er trukket, og ingen ordre er oprettet.');state.selected.clear();await load()}catch(e){message('danger',e.message);render()}}
|
||
async function changeBilling(id){const row=state.items.find(x=>Number(x.id)===id);if(!row?.can_change_billing||state.scope==='history')return;invalidatePreview();try{if(row.workflow_active){if(await TimeBillingReview.choose(row))await load();return;}const options=await api(`/api/v1/timetracking/time/${id}/settlement-options`),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"><h2 class="modal-title h5">Ændr afregning</h2><button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Luk"></button></div><div class="modal-body"><label class="form-label">Afregning</label><select class="form-select" required></select><p class="small text-muted mt-2">Valget gemmes. Tiden godkendes først, når du vælger Godkend.</p><div class="text-danger small" 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 afregning</button></div></form></div>';const select=host.querySelector('select');for(const [value,label] of [['invoice','Faktura'],['internal','Intern tid'],['non_billable','Ikke fakturerbar']])select.add(new Option(label,value));(options.prepaid_cards||[]).forEach(c=>select.add(new Option('Klippekort '+(c.card_number||c.id),'card_'+c.id)));(options.agreements||[]).forEach(a=>select.add(new Option('Fastpris '+(a.agreement_number||a.id),'fpa_'+a.id)));select.value=effectiveMethod(row)==='prepaid'?'card_'+row.prepaid_card_id:effectiveMethod(row)==='subscription'?'fpa_'+row.fixed_price_agreement_id:effectiveMethod(row);document.body.append(host);const modal=new bootstrap.Modal(host);host.addEventListener('hidden.bs.modal',()=>{modal.dispose();host.remove()},{once:true});host.querySelector('form').onsubmit=async e=>{e.preventDefault();const value=select.value,body={ids:[id],billing_method:value,prepaid_card_id:null,fixed_price_agreement_id:null};if(value.startsWith('card_')){body.billing_method='prepaid';body.prepaid_card_id=Number(value.slice(5));}if(value.startsWith('fpa_')){body.billing_method='subscription';body.fixed_price_agreement_id=Number(value.slice(4));}body.billable=['invoice','prepaid'].includes(body.billing_method);const button=host.querySelector('[type=submit]');button.disabled=true;try{await api('/api/v1/economy/time-queue/bulk-update',{method:'PATCH',body:JSON.stringify(body)});modal.hide();await load()}catch(error){host.querySelector('[role=status]').textContent=error.message;button.disabled=false;}};modal.show();}catch(e){message('danger',e.message)}}
|
||
async function saveMethod(){if(selectedRows().some(x=>x.can_change_billing===false))return message('warning','Godkendt eller afregnet tid kan ikke ændres her.');if(selectionMode(selectedRows())!=='legacy')return message('warning','Brug Hvorfor? for en begrundet overstyring.');if(!ids().length)return message('warning','Vælg mindst én tidsregistrering først.');const m=$('settlementMethod').value;if(!m)return message('warning','Vælg en afregningstype, der skal gemmes.');try{await api('/api/v1/economy/time-queue/bulk-update',{method:'PATCH',body:JSON.stringify({ids:ids(),billing_method:m,billable:['invoice','prepaid'].includes(m)})});message('success','Afregningstypen er gemt.');$('settlementMethod').value='';$('prepaidCard').value='';$('agreementId').value='';await load()}catch(e){message('danger',e.message)}}
|
||
async function openCaseDetails(sagId){const row=state.items.find(x=>Number(x.sag_id)===sagId)||{},modal=bootstrap.Modal.getOrCreateInstance($('caseDetailModal'));$('caseDetailTitle').textContent=`#${sagId} · ${row.case_title||'Sag'}`;$('openCaseNewTab').href=`/sag/${sagId}/v5`;$('caseDetailBody').innerHTML='<div class="py-4 text-center text-muted"><span class="spinner-border spinner-border-sm me-2"></span>Henter sagsdetaljer…</div>';modal.show();try{const s=await api(`/api/v1/sag/${sagId}`),description=s.beskrivelse||row.description||'Ingen beskrivelse på sagen.';$('caseDetailBody').innerHTML=`<div class="d-flex flex-wrap gap-2 mb-3">${status(s.status)}<span class="badge text-bg-light border">${esc(s.type||'Sag')}</span><span class="badge text-bg-light border">${esc(s.prioritet||'Normal')}</span></div><div class="case-detail-meta mb-3"><div><small>Kunde</small><strong>${esc(row.customer_name||'Ikke tilknyttet')}</strong></div><div><small>Kontakt</small><strong>${esc(row.contact_name||'Ikke tilknyttet')}</strong></div><div><small>Ansvarlig</small><strong>${esc(s.ansvarlig_navn||'Ikke tildelt')}</strong></div><div><small>Senest opdateret</small><strong>${esc(s.updated_at||s.opdateret_at||'Ikke oplyst')}</strong></div></div><h3 class="h6">Beskrivelse</h3><div class="border rounded p-3 bg-light">${esc(description).replace(/\n/g,'<br>')}</div>`}catch(e){$('caseDetailBody').innerHTML=`<div class="alert alert-danger mb-0">${esc(e.message)}</div>`}}
|
||
function previewView(d){
|
||
state.preview=d;
|
||
$('previewPanel').classList.remove('d-none');
|
||
const errors=d.errors||[],invoice=d.invoice_groups||[],prepaid=d.prepaid_groups||[],sub=d.subscription_groups||[],items=d.items||[];
|
||
const itemRows=items.map(x=>`<div class="preview-item d-flex flex-wrap justify-content-between gap-2"><div><strong>${esc(x.title||`Tid #${x.id}`)}</strong> <span class="small text-muted">${esc(x.customer_name||'Ukendt kunde')}</span><div class="small mt-1">${caseStatus(x.case_status)} <span class="text-muted">Sagstatus er kun information; løst/lukket sag må gerne afregnes.</span></div></div><div class="text-end"><strong>${h(x.hours)} t</strong><div class="small text-muted">Faktisk ${h(x.actual_hours)} t · ${esc(labels[x.method]||x.method_label||x.method)}</div>${x.message?`<div class="small text-danger">${esc(x.message)}</div>`:''}</div></div>`).join('');
|
||
$('previewBody').innerHTML=`<div class="alert alert-info py-2"><strong>Hvad gør forhåndsvisning?</strong> Den kontrollerer valgte registreringer, kunde, klippekort/aftale, timer og beløb. Den gemmer ikke noget og trækker ikke timer. Det sker først, når du godkender og gennemfører nedenfor.</div>${errors.length?`<div class="alert alert-danger"><strong>Afregningen kan ikke gennemføres endnu.</strong><ul class="mb-0 mt-2">${errors.map(x=>`<li>Tid #${Number(x.id)}: ${esc(x.message)}</li>`).join('')}</ul></div>`:'<div class="alert alert-success"><i class="bi bi-check-circle me-1"></i>Alle valgte tider kan afregnes. Løst/lukket sagsstatus blokerer ikke.</div>'}<div class="row g-3">${invoice.map(x=>`<div class="col-12 col-lg-6"><div class="preview-group p-3 rounded bg-light"><strong>Faktura · ${esc(x.customer_name)}</strong><div class="small text-muted">${x.entries.length} tider · ${h(x.hours)} timer · ${h(x.hourly_rate)} kr./t</div><div class="fs-5 fw-semibold">${Number(x.amount_ex_vat).toLocaleString('da-DK',{minimumFractionDigits:2})} kr. ekskl. moms</div><div class="small text-muted">Gebyr og evt. fragt vises i ordrekladde.</div></div></div>`).join('')}${prepaid.map(x=>`<div class="col-12 col-lg-6"><div class="preview-group p-3 rounded bg-light"><strong>Klippekort · ${esc(x.card_number||'#'+x.card_id)}</strong><div class="small text-muted">Forbrug: ${h(x.hours)} t · Tilbage før træk: ${h(x.remaining_hours)} t · efter: ${h(Number(x.remaining_hours)-Number(x.hours))} t</div><div class="small text-muted">Afrunding: ${Number(x.rounding_minutes)||30} min. pr. registrering</div></div></div>`).join('')}${sub.map(x=>`<div class="col-12 col-lg-6"><div class="preview-group p-3 rounded bg-light"><strong>Abonnement / fast pris · aftale #${Number(x.agreement_id)}</strong><div class="small text-muted">${x.entries.length} tider · ${h(x.hours)} timer knyttes til aftalen uden særskilt fakturalinje.</div></div></div>`).join('')}</div><h3 class="h6 mt-4">Valgte tidsregistreringer (${items.length})</h3><div class="preview-items">${itemRows||'<div class="text-muted">Ingen registreringer.</div>'}</div>`;
|
||
const onlyInvoice=items.length>0&&items.every(x=>norm(x.method)==='invoice');const approved=selectedRows().every(x=>x.lifecycle_status==='approved');$('settleBtn').innerHTML=onlyInvoice?(approved?'Send til fakturering':'Godkend og send til fakturering'):(approved?'Afregn':'Godkend og afregn');
|
||
$('settleBtn').disabled=!d.valid;
|
||
$('previewPanel').scrollIntoView({behavior:'smooth',block:'center'});
|
||
}
|
||
async function preview(){if(selectedRows().some(x=>x.can_settle!==true))return message('warning','Disse tider kan ikke sendes eller afregnes.');if(!ids().length)return message('warning','Vælg mindst én tidsregistrering først.');if(selectionMode(selectedRows())==='mixed')return message('warning','Vælg workflowtider og øvrige tider hver for sig.');invalidatePreview();const request=payload(),key=fingerprint();try{const d=await api('/api/v1/economy/time-queue/preview-settlement',{method:'POST',body:JSON.stringify(request)});if(key!==fingerprint())return;state.previewPayload=JSON.parse(JSON.stringify({...request,snapshots:selectedRows().map(approvalSnapshot)}));state.previewFingerprint=key;previewView(d)}catch(e){message('danger',e.message)}}
|
||
async function settle(){if(!state.preview?.valid||!state.previewPayload)return;if(state.previewFingerprint!==fingerprint()){invalidatePreview();return message('warning','Udvalget eller afregningen er ændret. Lav en ny forhåndsvisning.');}const request=state.previewPayload;if(!confirm('Gennemfør den viste afregning? Fakturatid sendes til en lokal ordrekladde. Klippekort trækker timer, og øvrig tid afregnes som vist.'))return;$('settleBtn').disabled=true;$('settleBtn').innerHTML='<span class="spinner-border spinner-border-sm me-1"></span>Afregner…';try{const d=await api('/api/v1/economy/time-queue/send-reviewed',{method:'POST',body:JSON.stringify(request)});message('success',`Afregningen er gennemført.${d.created_drafts?.length?' Ordrekladder er oprettet.':''}`);state.selected.clear();invalidatePreview();if(d.orders_url)$('messageArea').insertAdjacentHTML('beforeend',`<p><a class="btn btn-outline-primary" href="${esc(d.orders_url)}">Åbn ordrekladde</a></p>`);await loadCustomers();await load()}catch(e){message('danger',e.message);invalidatePreview()}finally{$('settleBtn').disabled=!state.preview?.valid;$('settleBtn').innerHTML='<i class="bi bi-check2-circle me-1"></i>Godkend og gennemfør'}}
|
||
function selectionChanged(){invalidatePreview();render();loadCards()}
|
||
document.querySelectorAll('.scope-tab').forEach(x=>x.onclick=()=>{state.scope=x.dataset.scope;state.selected.clear();state.triage='all';document.querySelectorAll('.scope-tab').forEach(b=>{b.classList.toggle('btn-primary',b===x);b.classList.toggle('btn-outline-primary',b!==x)});$('selectReadyBtn').classList.toggle('d-none',state.scope==='history');load()});
|
||
document.querySelectorAll('.triage-tab').forEach(x=>x.onclick=()=>{state.triage=x.dataset.triage;invalidatePreview();render()});$('selectReadyBtn').onclick=()=>{state.selected=new Set(visibleItems().filter(x=>actionable(x)&&!reviewIssue(x)).map(x=>Number(x.id)));selectionChanged()};['workflowLineMode','settlementMethod','agreementId'].forEach(id=>{$(id).oninput=invalidatePreview;$(id).onchange=invalidatePreview});$('filterMethod').onchange=()=>{invalidatePreview();render()};$('reloadBtn').onclick=()=>{loadCustomers();load()};$('clearFiltersBtn').onclick=()=>{state.triage='all';invalidatePreview();$('filterMethod').value='';$('filterCustomer').value='';$('filterStatus').value='';$('filterBillable').value='';$('filterCaseState').value='';$('filterQuery').value='';load()};[$('filterCustomer'),$('filterStatus'),$('filterCaseState'),$('filterBillable')].forEach(x=>x.onchange=load);$('filterQuery').onkeydown=e=>e.key==='Enter'&&load();$('prepaidCard').onchange=()=>{invalidatePreview();renderPrepaidHelp()};$('filterQuery').oninput=invalidatePreview;$('setMethodBtn').onclick=saveMethod;$('approveBtn').onclick=approveSelected;$('stickyApproveBtn').onclick=approveSelected;$('previewBtn').onclick=preview;$('stickyPreviewBtn').onclick=preview;$('settleBtn').onclick=settle;$('closePreviewBtn').onclick=invalidatePreview;$('clearSelectionBtn').onclick=()=>{state.selected.clear();state.preview=null;$('previewPanel').classList.add('d-none');selectionChanged()};loadCustomers();load();
|
||
})();
|
||
</script>
|
||
{% endblock %}
|