66 lines
24 KiB
HTML
66 lines
24 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
||
{% block title %}Tidskø til afregning{% endblock %}
|
||
{% block content %}
|
||
<style>
|
||
.time-queue { max-width: 1700px; }
|
||
.queue-summary { background:linear-gradient(135deg,#f7fbff,#fff); border:1px solid #d8e3ed; border-radius:1rem; }
|
||
.queue-stat { min-width:150px; }.queue-stat strong{font-size:1.5rem;color:#15476c}
|
||
.time-group { border:1px solid #dce5ed;border-radius:.85rem;overflow:hidden }.time-group header{background:#f5f8fb;border-bottom:1px solid #dce5ed}
|
||
.time-group header { padding:.45rem .75rem!important; font-size:.85rem; }
|
||
.time-row{border-left:4px solid #0d6efd;padding:.42rem .7rem!important;min-height:43px}.time-row[data-method="prepaid"]{border-left-color:#6f42c1}.time-row[data-method="subscription"]{border-left-color:#198754}.time-row[data-method="internal"],.time-row[data-method="non_billable"]{border-left-color:#6c757d}.time-row+.time-row{border-top:1px solid #edf1f4}
|
||
.time-row .row { display:grid;grid-template-columns:22px minmax(290px,2.4fr) minmax(125px,.85fr) minmax(94px,.55fr) minmax(170px,.9fr) 82px;gap:.5rem;align-items:center;margin:0; }
|
||
.time-row .row>[class*="col"] { width:auto;max-width:none;flex:none;padding:0;min-width:0; }
|
||
.time-row .col-12.col-lg-4>div { display:inline; }.time-row .col-12.col-lg-4>div+div:before { content:' · ';color:#94a3b8; }
|
||
.time-row .small { font-size:.75rem;line-height:1.15; }.time-row .fw-semibold,.time-row strong { font-size:.85rem;line-height:1.15; }
|
||
.time-row .col-6 .small.text-muted { display:none; }.time-row .col-8 .small.text-muted { display:none; }
|
||
.time-row .col-12.col-lg-4,.time-row .col-6,.time-row .col-8 { overflow:hidden;white-space:nowrap;text-overflow:ellipsis; }
|
||
.time-row > .mt-2 { display:none; }.time-row .btn { padding:.18rem .48rem;font-size:.75rem; }
|
||
@media (max-width:1100px){.time-row .row{grid-template-columns:22px minmax(240px,2fr) minmax(115px,1fr) minmax(92px,.6fr) 78px}.time-row .col-8.col-lg-2{display:none}}
|
||
@media (max-width:720px){.time-row .row{display:flex;flex-wrap:wrap;gap:.35rem}.time-row .col-12.col-lg-4{flex:1 1 calc(100% - 30px)}.time-row .col-6,.time-row .col-8{flex:1 1 auto}.time-row .col-4{margin-left:auto}}
|
||
.queue-actions { position:sticky;bottom:1rem;z-index:20;box-shadow:0 .5rem 1.5rem rgba(27,56,84,.16) }.preview-group{border-left:4px solid #0d6efd}.empty-state{border:2px dashed #d7e0e8;border-radius:.9rem}
|
||
.case-detail-meta{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.case-detail-meta div{padding:.65rem .75rem;background:#f6f8fa;border-radius:.55rem}.case-detail-meta small{display:block;color:#6c757d;margin-bottom:.12rem}@media(max-width:576px){.case-detail-meta{grid-template-columns:1fr}}
|
||
</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">Gennemgå tid, vælg afregning og opret først ordrekladder efter en tydelig forhåndsvisning.</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>
|
||
<section class="queue-summary p-3 mb-3"><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 i alt</div><strong id="statHours">–</strong></div><div class="queue-stat"><div class="small text-muted">Klar til faktura</div><strong id="statInvoice">–</strong></div><div class="queue-stat"><div class="small text-muted">Udvalgt</div><strong id="statSelected">0</strong></div><div class="ms-md-auto small text-muted"><i class="bi bi-shield-check text-success me-1"></i>Intet afregnes uden forhåndsvisning.</div></div></section>
|
||
<section class="card mb-3"><div class="card-body"><div class="row g-2 align-items-end"><div class="col-12 col-md-3"><label class="form-label">Firma</label><select id="filterCustomer" class="form-select"><option value="">Alle firmaer</option></select></div><div class="col-6 col-md-2"><label class="form-label">Status</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-md-2"><label class="form-label">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-12 col-md-4"><label class="form-label">Søg</label><input id="filterQuery" class="form-control" placeholder="Kunde, sag eller beskrivelse"></div><div class="col-12 col-md-1 d-grid"><button class="btn btn-outline-secondary" id="clearFiltersBtn">Ryd</button></div></div></div></section>
|
||
<section class="card mb-3"><div class="card-body"><div class="d-flex justify-content-between flex-wrap gap-2 mb-3"><div><h2 class="h5 mb-0">Afregning for valgte tidsregistreringer</h2><div class="small text-muted">Vælg type og eventuelt klippekort eller aftale.</div></div><span class="badge text-bg-primary align-self-center" id="selectedBadge">0 valgt</span></div><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><button class="btn btn-primary flex-grow-1" id="previewBtn"><i class="bi bi-eye me-1"></i>Forhåndsvis</button></div></div></div></section>
|
||
<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">Kontrollér beløb og fejl før afregningen gennemføres.</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 text-end"><button class="btn btn-success" id="settleBtn"><i class="bi bi-check2-circle me-1"></i>Gennemfør afregning</button></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>
|
||
<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><span class="small text-muted ms-2">Vælg forhåndsvisning før afregning.</span></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-primary" id="stickyPreviewBtn">Forhåndsvis afregning</button></div></div></div>
|
||
</main>
|
||
<script>
|
||
(() => {
|
||
const state={items:[],selected:new Set(),preview:null,loading:false,cards:[]}; const $=id=>document.getElementById(id);
|
||
const labels={pending:'Afventer',approved:'Godkendt',rejected:'Afvist',invoice:'Faktura',prepaid:'Klippekort',subscription:'Abonnement / fast pris',internal:'Intern tid',non_billable:'Ikke-fakturerbar tid'};
|
||
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||'invoice').toLowerCase()]||String(v||'invoice').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 payload(){const method=$('settlementMethod').value,card=Number($('prepaidCard').value),agreement=Number($('agreementId').value);return {ids:ids(),...(method?{billing_method:method}:{}),...(card?{prepaid_card_id:card}:{}),...(agreement?{fixed_price_agreement_id:agreement}:{})}}
|
||
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=m==='invoice'?'text-bg-primary':m==='prepaid'?'text-bg-purple':m==='subscription'?'text-bg-success':'text-bg-secondary';return `<span class="badge ${cls}">${esc(labels[m]||m)}</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=>norm(x.billing_method)==='invoice'&&x.billable!==false).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)}
|
||
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 render(){renderStats();if(!state.items.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={};state.items.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-link customer-select" data-ids="${items.map(x=>x.id).join(',')}">Vælg kunde</button></header>${items.map(x=>{const id=Number(x.id),m=norm(x.billing_method);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':''}></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">${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')} ${status(x.status)}</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)} ${x.billable===false?'<span class="small text-muted">ikke fakturerbar</span>':''}</div><div class="col-4 col-lg-2 text-lg-end"><button class="btn btn-sm btn-outline-secondary select-one" data-id="${id}">Gennemgå</button></div></div>${x.description?`<div class="small text-muted mt-2 ps-lg-4">${esc(x.description)}</div>`:'<div class="small text-warning mt-2 ps-lg-4"><i class="bi bi-exclamation-triangle me-1"></i>Mangler beskrivelse</div>'}</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('.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();$('settlementMethod').scrollIntoView({behavior:'smooth',block:'center'})}));document.querySelectorAll('.customer-select').forEach(e=>e.addEventListener('click',()=>{e.dataset.ids.split(',').forEach(id=>state.selected.add(Number(id)));selectionChanged()}))}
|
||
function url(){const p=new URLSearchParams({limit:'500'});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 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]}`);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)return;state.loading=true;$('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());state.items=d.items||[];state.selected=new Set(ids().filter(id=>state.items.some(x=>Number(x.id)===id)));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}}
|
||
async function saveMethod(){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.');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}/v3`;$('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||[];$('previewBody').innerHTML=`${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 er klar til afregning.</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 ordrekladden.</div></div></div>`).join('')}${prepaid.map(x=>`<div class="col-12 col-lg-6"><div class="preview-group p-3 rounded bg-light" style="border-left-color:#6f42c1"><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</div></div></div>`).join('')}${sub.map(x=>`<div class="col-12 col-lg-6"><div class="preview-group p-3 rounded bg-light" style="border-left-color:#198754"><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 fakturalinje.</div></div></div>`).join('')}</div>`;$('settleBtn').disabled=!d.valid;$('previewPanel').scrollIntoView({behavior:'smooth',block:'center'})}
|
||
async function preview(){if(!ids().length)return message('warning','Vælg mindst én tidsregistrering først.');try{previewView(await api('/api/v1/economy/time-queue/preview-settlement',{method:'POST',body:JSON.stringify(payload())}))}catch(e){message('danger',e.message)}}
|
||
async function settle(){if(!state.preview?.valid)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/settle',{method:'POST',body:JSON.stringify(payload())});message('success',`Afregningen er gennemført.${d.created_drafts?.length?' Ordrekladder er oprettet.':''}`);state.selected.clear();$('previewPanel').classList.add('d-none');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);if(e.payload?.detail?.preview)previewView(e.payload.detail.preview)}finally{$('settleBtn').disabled=false;$('settleBtn').innerHTML='<i class="bi bi-check2-circle me-1"></i>Gennemfør afregning'}}
|
||
function selectionChanged(){state.preview=null;$('previewPanel').classList.add('d-none');render();loadCards()}
|
||
$('reloadBtn').onclick=()=>{loadCustomers();load()};$('clearFiltersBtn').onclick=()=>{$('filterCustomer').value='';$('filterStatus').value='';$('filterBillable').value='';$('filterQuery').value='';load()};[$('filterCustomer'),$('filterStatus'),$('filterBillable')].forEach(x=>x.onchange=load);$('filterQuery').onkeydown=e=>e.key==='Enter'&&load();$('prepaidCard').onchange=renderPrepaidHelp;$('setMethodBtn').onclick=saveMethod;$('previewBtn').onclick=preview;$('stickyPreviewBtn').onclick=preview;$('settleBtn').onclick=settle;$('closePreviewBtn').onclick=()=>{$('previewPanel').classList.add('d-none');state.preview=null};$('clearSelectionBtn').onclick=()=>{state.selected.clear();state.preview=null;$('previewPanel').classList.add('d-none');selectionChanged()};loadCustomers();load();
|
||
})();
|
||
</script>
|
||
{% endblock %}
|