Simplify time queue layout to match Nordic Top design plan

This commit is contained in:
Christian 2026-10-09 00:52:06 +02:00
parent cdb9d8c178
commit 239d75ff06

View File

@ -2,34 +2,39 @@
{% block title %}Tidskø til afregning{% endblock %} {% block title %}Tidskø til afregning{% endblock %}
{% block content %} {% block content %}
<style> <style>
.time-queue { max-width: 1700px; } .time-queue{max-width:none;color:var(--text-primary)}
.queue-summary { background:linear-gradient(135deg,#f7fbff,#fff); border:1px solid #d8e3ed; border-radius:1rem; } .time-queue .text-primary,.time-queue .btn-link{color:var(--accent)!important}
.queue-stat { min-width:150px; }.queue-stat strong{font-size:1.5rem;color:#15476c} .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-group { border:1px solid #dce5ed;border-radius:.85rem;overflow:hidden }.time-group header{background:#f5f8fb;border-bottom:1px solid #dce5ed} .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)}
.time-group header { padding:.45rem .75rem!important; font-size:.85rem; } .queue-stat{min-width:110px}.queue-stat strong{font-size:1.35rem;color:var(--text-primary)}
.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} .queue-summary .queue-stat+.queue-stat{border-left:1px solid var(--bs-border-color);padding-left:1.25rem}
.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-group{overflow:hidden}.time-group header{padding:.65rem 1rem!important;background:var(--accent-light);border-bottom:1px solid var(--bs-border-color)}
.time-row .row>[class*="col"] { width:auto;max-width:none;flex:none;padding:0;min-width:0; } .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 .col-12.col-lg-4>div { display:inline; }.time-row .col-12.col-lg-4>div+div:before { content:' · ';color:#94a3b8; } .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 .small { font-size:.75rem;line-height:1.15; }.time-row .fw-semibold,.time-row strong { font-size:.85rem;line-height:1.15; } .time-row .row>[class*="col"]{width:auto;max-width:none;flex:none;padding:0;min-width:0}
.time-row .col-6 .small.text-muted { display:none; }.time-row .col-8 .small.text-muted { display:none; } .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-12.col-lg-4,.time-row .col-6,.time-row .col-8 { overflow:hidden;white-space:nowrap;text-overflow:ellipsis; } .time-row .col-6 .small.text-muted,.time-row .col-8 .small.text-muted{display:none}
.time-row > .mt-2 { display:block; margin-top:.2rem!important; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }.time-row > .queue-tags { white-space:normal; }.time-row .btn { padding:.18rem .48rem;font-size:.75rem; } .time-row > .mt-2{margin-top:.35rem!important}.time-row .btn{font-size:.82rem}.time-row .case-details{text-align:left;white-space:normal}
@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}} .time-row .badge{font-weight:500}.queue-tags .badge{margin-bottom:.2rem}
@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 .3rem 1rem rgba(0,0,0,.1)!important}
.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} .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}
.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:var(--theme-muted, #6c757d);margin-bottom:.12rem}@media(max-width:576px){.case-detail-meta{grid-template-columns:1fr}} .queue-details summary{cursor:pointer;font-weight:500}.queue-details[open] summary{margin-bottom:1rem}
.case-closed-badge{background:#e8f5ec;color:#176b36;border:1px solid #bee3c8}.case-open-badge{background:#fff5dc;color:#805b00;border:1px solid #f1dda0}.preview-items{max-height:320px;overflow:auto}.preview-item{border-bottom:1px solid #e7edf1;padding:.55rem 0}.preview-item:last-child{border-bottom:0} .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}}
</style> </style>
<main class="container-fluid py-4 time-queue"> <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 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å forslag og afregn efter 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> <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">Faktura i køen</div><strong id="statInvoice">–</strong></div><div class="queue-stat"><div class="small text-muted">Sager løst/lukket</div><strong id="statClosedCases">–</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>Forhåndsvisning kontrollerer; den ændrer ikke data.</div></div></section> <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</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 class="ms-md-auto small text-muted"><i class="bi bi-shield-check me-1"></i>Ingen afregning før din godkendelse.</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">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-md-2"><label class="form-label">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-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-2"><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="row g-3 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="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. Løst/lukket sagsstatus forhindrer ikke afregning.</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 class="card mb-3 d-none" 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-primary" id="previewBtn"><i class="bi bi-eye me-1"></i>Forhåndsvis</button></div><details class="queue-details" 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 class="mb-3"><label class="form-label" for="filterMethod">Afregningsmetode</label><select id="filterMethod" class="form-select" style="max-width:300px"><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 id="workflowLineModeHost" class="mb-3 d-none"><label class="form-label">Fakturalinjer<select id="workflowLineMode" class="form-select"><option value="separate">En linje pr. tidsregistrering</option><option value="combined">Saml ens faktureringsvilkår</option></select></label><p class="small text-muted">Afregningsmetoden er valgt af workflows. Brug Hvorfor? for at overstyre med begrundelse.</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. Først når du trykker “Gennemfør”, gemmes afregningen.</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>Gennemfør afregning</button></div></div></section> <div id="workflowLineModeHost" class="mb-3 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">Workflowvalg ændres under Hvorfor?</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. Først når du trykker “Gennemfør”, gemmes afregningen.</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>Gennemfør afregning</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="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" 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 til gennemgang <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 synlige klar til gennemgang</button><span class="small text-muted">Foreløbig vurdering. Forhåndsvisning kontrollerer endeligt.</span></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 til gennemgang <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 klar til gennemgang</button><span class="small text-muted">Foreløbig vurdering. Forhåndsvisning kontrollerer endeligt.</span></div>
<section id="queueContent" aria-live="polite"></section> <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> <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> </main>
@ -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 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 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 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 `<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 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 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)} 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 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 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 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'); $('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 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(''); 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 efter “Gennemfør afregning”.</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" 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 · 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" 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 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>`; $('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 efter “Gennemfør afregning”.</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>`;
$('settleBtn').disabled=!d.valid; $('settleBtn').disabled=!d.valid;
$('previewPanel').scrollIntoView({behavior:'smooth',block:'center'}); $('previewPanel').scrollIntoView({behavior:'smooth',block:'center'});
} }