From 239d75ff06430930484a0c76a032a97e319a4562 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 9 Oct 2026 00:52:06 +0200 Subject: [PATCH] Simplify time queue layout to match Nordic Top design plan --- app/economy/frontend/time_queue.html | 59 +++++++++++++++------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/app/economy/frontend/time_queue.html b/app/economy/frontend/time_queue.html index 8d3d15f..847cf2b 100644 --- a/app/economy/frontend/time_queue.html +++ b/app/economy/frontend/time_queue.html @@ -2,34 +2,39 @@ {% block title %}Tidskø til afregning{% endblock %} {% block content %}
-
Økonomi

Tidskø til afregning

Gennemgå tid, vælg afregning og opret først ordrekladder efter en tydelig forhåndsvisning.

+
Økonomi

Tidskø til afregning

Gennemgå forslag og afregn efter forhåndsvisning.

-
Registreringer
–
Timer i alt
–
Faktura i køen
–
Sager løst/lukket
–
Udvalgt
0
Forhåndsvisning kontrollerer; den ændrer ikke data.
-
-

Afregning for valgte tidsregistreringer

Vælg type og eventuelt klippekort eller aftale. Løst/lukket sagsstatus forhindrer ikke afregning.
0 valgt
Vælg tidsregistreringer for at se kundens klippekort.
-

Afregningsmetoden er valgt af workflows. Brug Hvorfor? for at overstyre med begrundelse.

Forhåndsvisning af afregning
Dette er en kontrol uden ændringer. Først når du trykker “Gennemfør”, gemmes afregningen.
+
Registreringer
–
Timer
–
Valgt
0
Ingen afregning før din godkendelse.
+
Flere filtre
+

Valgte registreringer

Behold forslagene, eller justér afregningen.
0 valgt
Justér afregning manuelt
Vælg tidsregistreringer for at se kundens klippekort.
+

Workflowvalg ændres under Hvorfor?

Forhåndsvisning af afregning
Dette er en kontrol uden ændringer. Først når du trykker “Gennemfør”, gemmes afregningen.
-
Foreløbig vurdering. Forhåndsvisning kontrollerer endeligt.
+
Foreløbig vurdering. Forhåndsvisning kontrollerer endeligt.
0 tidsregistreringer valgtVælg forhåndsvisning før afregning.
@@ -51,9 +56,9 @@ function syncWorkflowControls(){const mode=selectionMode(selectedRows()),active=mode!=='legacy';$('workflowLineModeHost').classList.toggle('d-none',mode!=='workflow');for(const id of ['settlementMethod','prepaidCard','agreementId','setMethodBtn']){$(id).disabled=active;if(active&&id!=='setMethodBtn')$(id).value='';}$('previewBtn').disabled=mode==='mixed';$('stickyPreviewBtn').disabled=mode==='mixed';$('selectionText').textContent=mode==='mixed'?'Blandet udvalg: vælg workflowtider og øvrige tider hver for sig.':`${ids().length} tidsregistreringer valgt`;return active;} function payload(){if(allWorkflowSelected())return {ids:ids(),line_mode:$('workflowLineMode').value};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 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 status(v){return `${esc(labels[v]||v||'Ukendt')}`} 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 `${esc(labels[m]||m)}`} + function status(v){return `${esc(labels[v]||v||'Ukendt')}`} function method(v){const m=norm(v),cls='queue-method-badge';return `${esc(labels[m]||m)}`} function isClosedCase(value){return ['lukket','løst','closed','resolved','afsluttet','done'].includes(String(value||'').trim().toLocaleLowerCase('da-DK'))} function caseStatus(v){if(!v)return 'Uden sag';return `Sag: ${esc(v)}`} - 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)} + 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.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)} @@ -72,7 +77,7 @@ $('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=>`
${esc(x.title||`Tid #${x.id}`)} ${esc(x.customer_name||'Ukendt kunde')}
${caseStatus(x.case_status)} Sagstatus er kun information; løst/lukket sag må gerne afregnes.
${h(x.hours)} t
Faktisk ${h(x.actual_hours)} t · ${esc(labels[x.method]||x.method_label||x.method)}
${x.message?`
${esc(x.message)}
`:''}
`).join(''); - $('previewBody').innerHTML=`
Hvad gør forhåndsvisning? Den kontrollerer valgte registreringer, kunde, klippekort/aftale, timer og beløb. Den gemmer ikke noget og trækker ikke timer. Det sker først efter “Gennemfør afregning”.
${errors.length?`
Afregningen kan ikke gennemføres endnu.
`:'
Alle valgte tider kan afregnes. Løst/lukket sagsstatus blokerer ikke.
'}
${invoice.map(x=>`
Faktura · ${esc(x.customer_name)}
${x.entries.length} tider · ${h(x.hours)} timer · ${h(x.hourly_rate)} kr./t
${Number(x.amount_ex_vat).toLocaleString('da-DK',{minimumFractionDigits:2})} kr. ekskl. moms
Gebyr og evt. fragt vises i ordrekladde.
`).join('')}${prepaid.map(x=>`
Klippekort · ${esc(x.card_number||'#'+x.card_id)}
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
Afrunding: ${Number(x.rounding_minutes)||30} min. pr. registrering
`).join('')}${sub.map(x=>`
Abonnement / fast pris · aftale #${Number(x.agreement_id)}
${x.entries.length} tider · ${h(x.hours)} timer knyttes til aftalen uden særskilt fakturalinje.
`).join('')}

Valgte tidsregistreringer (${items.length})

${itemRows||'
Ingen registreringer.
'}
`; + $('previewBody').innerHTML=`
Hvad gør forhåndsvisning? Den kontrollerer valgte registreringer, kunde, klippekort/aftale, timer og beløb. Den gemmer ikke noget og trækker ikke timer. Det sker først efter “Gennemfør afregning”.
${errors.length?`
Afregningen kan ikke gennemføres endnu.
`:'
Alle valgte tider kan afregnes. Løst/lukket sagsstatus blokerer ikke.
'}
${invoice.map(x=>`
Faktura · ${esc(x.customer_name)}
${x.entries.length} tider · ${h(x.hours)} timer · ${h(x.hourly_rate)} kr./t
${Number(x.amount_ex_vat).toLocaleString('da-DK',{minimumFractionDigits:2})} kr. ekskl. moms
Gebyr og evt. fragt vises i ordrekladde.
`).join('')}${prepaid.map(x=>`
Klippekort · ${esc(x.card_number||'#'+x.card_id)}
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
Afrunding: ${Number(x.rounding_minutes)||30} min. pr. registrering
`).join('')}${sub.map(x=>`
Abonnement / fast pris · aftale #${Number(x.agreement_id)}
${x.entries.length} tider · ${h(x.hours)} timer knyttes til aftalen uden særskilt fakturalinje.
`).join('')}

Valgte tidsregistreringer (${items.length})

${itemRows||'
Ingen registreringer.
'}
`; $('settleBtn').disabled=!d.valid; $('previewPanel').scrollIntoView({behavior:'smooth',block:'center'}); }