- Implemented subscription creation, updating, and rendering in script_9.js. - Added functions for handling subscription line items, product selection, and total calculations. - Integrated AnyDesk API for session management in test_anydesk.py. - Created REST client test requests for API endpoints in api.http. - Developed a script to check ESET machine status and save details in tmp_check_eset_machine.py.
1166 lines
48 KiB
HTML
1166 lines
48 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
||
{% block title %}AnyDesk Sessions - BMC Hub{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
/* ─── Layout ─────────────────────────────────────────────────── */
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.page-header h1 { font-size: 1.8rem; font-weight: 700; margin: 0; }
|
||
|
||
/* ─── Stat cards ─────────────────────────────────────────────── */
|
||
.stat-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.stat-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid rgba(0,0,0,.1);
|
||
border-radius: 12px;
|
||
padding: 1.1rem 1.4rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: .25rem;
|
||
}
|
||
.stat-card .label { font-size: .8rem; color: var(--text-secondary); font-weight:500; }
|
||
.stat-card .value { font-size: 1.7rem; font-weight: 700; color: var(--accent); line-height:1; }
|
||
.stat-card.warn .value { color: #e6a817; }
|
||
.stat-card.ok .value { color: #28a745; }
|
||
|
||
/* ─── Controls ───────────────────────────────────────────────── */
|
||
.controls-bar {
|
||
background: var(--bg-card);
|
||
border: 1px solid rgba(0,0,0,.1);
|
||
border-radius: 12px;
|
||
padding: 1rem 1.2rem;
|
||
margin-bottom: 1rem;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: .75rem;
|
||
align-items: center;
|
||
}
|
||
.controls-bar select,
|
||
.controls-bar input {
|
||
background: var(--bg-body);
|
||
color: var(--text-primary);
|
||
border: 1px solid rgba(0,0,0,.2);
|
||
border-radius: 6px;
|
||
padding: .4rem .7rem;
|
||
font-size: .9rem;
|
||
}
|
||
|
||
/* ─── Session table ──────────────────────────────────────────── */
|
||
.sessions-table-wrap {
|
||
background: var(--bg-card);
|
||
border: 1px solid rgba(0,0,0,.1);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
.sessions-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: .875rem;
|
||
}
|
||
.sessions-table th {
|
||
background: rgba(0,0,0,.04);
|
||
padding: .65rem .9rem;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
font-size: .8rem;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: .04em;
|
||
white-space: nowrap;
|
||
}
|
||
[data-theme="dark"] .sessions-table th { background: rgba(255,255,255,.05); }
|
||
.sessions-table td {
|
||
padding: .6rem .9rem;
|
||
border-top: 1px solid rgba(0,0,0,.06);
|
||
vertical-align: middle;
|
||
}
|
||
[data-theme="dark"] .sessions-table td { border-color: rgba(255,255,255,.06); }
|
||
.sessions-table tr:hover td { background: rgba(15,76,117,.04); }
|
||
|
||
/* badges */
|
||
.badge-link { display:inline-flex; align-items:center; gap:.3rem; font-size:.78rem; padding:.2rem .55rem; border-radius:5px; font-weight:500; white-space:nowrap; }
|
||
.badge-hw { background:rgba(15,76,117,.12); color:#0f4c75; }
|
||
.badge-cust{ background:rgba(40,167,69,.12); color:#1a6e30; }
|
||
.badge-cont{ background:rgba(255,140,0,.13); color:#b36200; }
|
||
.badge-sag { background:rgba(111,66,193,.13); color:#4b1d8c; }
|
||
[data-theme="dark"] .badge-hw { background:rgba(86,156,214,.15); color:#7bb8e8; }
|
||
[data-theme="dark"] .badge-cust{ background:rgba(78,201,176,.15); color:#4ec9b0; }
|
||
[data-theme="dark"] .badge-cont{ background:rgba(255,193,7,.15); color:#ffd54f; }
|
||
[data-theme="dark"] .badge-sag { background:rgba(180,120,255,.15);color:#c197ff; }
|
||
|
||
.badge-link i { font-size:.75rem; }
|
||
|
||
.badge-unreg { background:rgba(220,53,69,.1); color:#dc3545; font-size:.75rem; padding:.2rem .5rem; border-radius:4px; }
|
||
.badge-ok { background:rgba(40,167,69,.12); color:#1a6e30; font-size:.75rem; padding:.2rem .5rem; border-radius:4px; }
|
||
[data-theme="dark"] .badge-unreg { background:rgba(255,100,100,.15); color:#ff8080; }
|
||
[data-theme="dark"] .badge-ok { background:rgba(78,201,176,.15); color:#4ec9b0; }
|
||
|
||
.dur-pill {
|
||
display:inline-block;
|
||
background: rgba(15,76,117,.08);
|
||
color: var(--accent);
|
||
padding:.15rem .5rem;
|
||
border-radius:5px;
|
||
font-weight:600;
|
||
font-size:.82rem;
|
||
}
|
||
|
||
.row-unregistered td { background: rgba(220,53,69,.03); }
|
||
|
||
/* action buttons */
|
||
.btn-link-action {
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
color: var(--text-secondary);
|
||
padding: .2rem .4rem;
|
||
border-radius: 4px;
|
||
font-size: .85rem;
|
||
transition: all .15s;
|
||
}
|
||
.btn-link-action:hover { background: rgba(15,76,117,.1); color: var(--accent); }
|
||
|
||
/* ─── Sessions table: rows are clickable ────────────────────── */
|
||
.sessions-table tbody tr { cursor: pointer; }
|
||
.sessions-table tbody tr:hover td { background: rgba(15,76,117,.06); }
|
||
[data-theme="dark"] .sessions-table tbody tr:hover td { background: rgba(86,156,214,.08); }
|
||
|
||
/* ─── Right-side session drawer ─────────────────────────────── */
|
||
.drawer-backdrop {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,.35);
|
||
z-index: 1039;
|
||
}
|
||
.drawer-backdrop.open { display: block; }
|
||
|
||
.session-drawer {
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: min(500px, 100vw);
|
||
background: var(--bg-card);
|
||
z-index: 1040;
|
||
transform: translateX(105%);
|
||
transition: transform .28s cubic-bezier(.4, 0, .2, 1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: -12px 0 48px rgba(0,0,0,.2);
|
||
overflow: hidden;
|
||
}
|
||
.session-drawer.open { transform: translateX(0); }
|
||
|
||
.reg-header {
|
||
padding: 1.2rem 1.4rem .9rem;
|
||
border-bottom: 1px solid rgba(0,0,0,.08);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 1rem;
|
||
flex-shrink: 0;
|
||
}
|
||
[data-theme="dark"] .reg-header { border-color: rgba(255,255,255,.08); }
|
||
.reg-header h5 { font-weight:700; margin:0; font-size:1.1rem; }
|
||
.reg-close { background:none; border:none; cursor:pointer; color:var(--text-secondary); font-size:1.4rem; line-height:1; padding:0; }
|
||
.reg-close:hover { color: var(--text-primary); }
|
||
|
||
/* session context bar */
|
||
.reg-context {
|
||
background: rgba(15,76,117,.06);
|
||
border-bottom: 1px solid rgba(0,0,0,.07);
|
||
padding: .8rem 1.4rem;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: .6rem 1.2rem;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
[data-theme="dark"] .reg-context { background: rgba(86,156,214,.08); }
|
||
.ctx-item { display:flex; flex-direction:column; gap:.1rem; }
|
||
.ctx-label { font-size:.72rem; color:var(--text-secondary); font-weight:500; text-transform:uppercase; letter-spacing:.04em; }
|
||
.ctx-value { font-size:.9rem; font-weight:600; }
|
||
.ctx-badge { display:inline-flex; align-items:center; gap:.3rem; padding:.2rem .55rem; border-radius:5px; font-size:.78rem; font-weight:500; }
|
||
.ctx-tech { background:rgba(40,167,69,.12); color:#1a6e30; }
|
||
[data-theme="dark"] .ctx-tech { background:rgba(78,201,176,.15); color:#4ec9b0; }
|
||
.ctx-unknown { background:rgba(220,53,69,.1); color:#dc3545; }
|
||
[data-theme="dark"] .ctx-unknown { background:rgba(255,100,100,.15); color:#ff8080; }
|
||
|
||
/* form body */
|
||
.reg-body {
|
||
padding: 1.1rem 1.4rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: .9rem;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
.reg-label {
|
||
font-size:.8rem;
|
||
font-weight:600;
|
||
color:var(--text-secondary);
|
||
text-transform:uppercase;
|
||
letter-spacing:.04em;
|
||
margin-bottom:.3rem;
|
||
display:block;
|
||
}
|
||
.reg-body select,
|
||
.reg-body input,
|
||
.reg-body textarea {
|
||
width:100%;
|
||
background: var(--bg-body);
|
||
color: var(--text-primary);
|
||
border: 1px solid rgba(0,0,0,.18);
|
||
border-radius: 7px;
|
||
padding: .45rem .75rem;
|
||
font-size: .88rem;
|
||
}
|
||
[data-theme="dark"] .reg-body select,
|
||
[data-theme="dark"] .reg-body input,
|
||
[data-theme="dark"] .reg-body textarea { border-color: rgba(255,255,255,.15); }
|
||
.reg-body textarea { resize:vertical; min-height:60px; }
|
||
|
||
/* quick pick typeahead */
|
||
.quick-pick {
|
||
margin-top: .3rem;
|
||
border: 1px solid rgba(0,0,0,.12);
|
||
border-radius: 7px;
|
||
background: var(--bg-card);
|
||
max-height: 180px;
|
||
overflow-y: auto;
|
||
}
|
||
[data-theme="dark"] .quick-pick { border-color: rgba(255,255,255,.12); }
|
||
.quick-item {
|
||
padding: .45rem .65rem;
|
||
border-top: 1px solid rgba(0,0,0,.06);
|
||
cursor: pointer;
|
||
font-size: .84rem;
|
||
}
|
||
[data-theme="dark"] .quick-item { border-top-color: rgba(255,255,255,.06); }
|
||
.quick-item:first-child { border-top: none; }
|
||
.quick-item:hover { background: rgba(15,76,117,.08); }
|
||
.quick-item .sub { color: var(--text-secondary); font-size: .76rem; }
|
||
|
||
/* Sag search */
|
||
.sag-search-wrap { position:relative; }
|
||
.sag-results {
|
||
position:absolute;
|
||
top:100%;
|
||
left:0;
|
||
right:0;
|
||
background: var(--bg-card);
|
||
border: 1px solid rgba(0,0,0,.15);
|
||
border-radius: 0 0 8px 8px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
z-index: 10;
|
||
box-shadow: 0 8px 24px rgba(0,0,0,.15);
|
||
display:none;
|
||
}
|
||
.sag-results.open { display:block; }
|
||
.sag-result-item {
|
||
padding:.55rem .9rem;
|
||
cursor:pointer;
|
||
border-top: 1px solid rgba(0,0,0,.06);
|
||
font-size:.88rem;
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:center;
|
||
gap:.5rem;
|
||
}
|
||
.sag-result-item:hover { background:rgba(15,76,117,.07); }
|
||
.sag-result-item:first-child { border-top:none; }
|
||
.sag-status-badge { font-size:.72rem; padding:.15rem .5rem; border-radius:4px; white-space:nowrap; }
|
||
.sag-st-open { background:rgba(40,167,69,.12); color:#1a6e30; }
|
||
.sag-st-other { background:rgba(0,0,0,.08); color:var(--text-secondary); }
|
||
|
||
/* suggestion card inside body */
|
||
.hw-suggestion {
|
||
border: 1.5px solid rgba(15,76,117,.3);
|
||
border-radius: 10px;
|
||
padding: .75rem .9rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: .8rem;
|
||
background: rgba(15,76,117,.04);
|
||
cursor: pointer;
|
||
transition: background .15s;
|
||
}
|
||
.hw-suggestion:hover { background: rgba(15,76,117,.1); }
|
||
.hw-suggestion .sug-label { font-size:.73rem; color:var(--text-secondary); margin-bottom:.12rem; }
|
||
.hw-suggestion .sug-value { font-weight:600; font-size:.9rem; }
|
||
.hw-suggestion.confirmed { border-color:#28a745; background:rgba(40,167,69,.06); cursor:default; }
|
||
|
||
/* reg footer */
|
||
.reg-footer {
|
||
padding: .9rem 1.4rem;
|
||
border-top: 1px solid rgba(0,0,0,.08);
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: .6rem;
|
||
flex-shrink: 0;
|
||
}
|
||
[data-theme="dark"] .reg-footer { border-color: rgba(255,255,255,.08); }
|
||
|
||
/* ─── Loading state ──────────────────────────────────────────── */
|
||
.spinner { width:20px; height:20px; border:3px solid rgba(15,76,117,.2); border-top-color:var(--accent); border-radius:50%; animation:spin .7s linear infinite; display:inline-block; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* ─── Toast ──────────────────────────────────────────────────── */
|
||
.toast-container { position:fixed; bottom:1.5rem; right:1.5rem; z-index:9999; display:flex; flex-direction:column; gap:.5rem; }
|
||
.toast-msg { background:var(--accent); color:white; padding:.7rem 1.2rem; border-radius:8px; font-size:.9rem; font-weight:500; animation:fadeIn .2s; }
|
||
.toast-msg.error { background:#dc3545; }
|
||
.toast-msg.success { background:#28a745; }
|
||
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container-fluid px-3 px-md-4 py-3">
|
||
|
||
<!-- Header -->
|
||
<div class="page-header">
|
||
<h1><i class="bi bi-display me-2" style="color:var(--accent)"></i>AnyDesk Sessions</h1>
|
||
<div class="d-flex gap-2 flex-wrap">
|
||
<button class="btn btn-sm btn-outline-secondary" onclick="fetchFromApi()" id="btn-fetch">
|
||
<i class="bi bi-cloud-download me-1"></i>Hent fra AnyDesk
|
||
</button>
|
||
<button class="btn btn-sm btn-primary" onclick="autoLink()" id="btn-autolink">
|
||
<i class="bi bi-link-45deg me-1"></i>Auto-link hardware
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stats -->
|
||
<div class="stat-grid" id="stat-area">
|
||
<div class="stat-card"><div class="label">Sessions i alt</div><div class="value" id="s-total">–</div></div>
|
||
<div class="stat-card warn"><div class="label">Uden sag/kontakt</div><div class="value" id="s-unregistered">–</div></div>
|
||
<div class="stat-card ok"><div class="label">Total tid (min)</div><div class="value" id="s-duration">–</div></div>
|
||
<div class="stat-card"><div class="label">Unikke enheder</div><div class="value" id="s-devices">–</div></div>
|
||
</div>
|
||
|
||
<!-- Controls -->
|
||
<div class="controls-bar">
|
||
<label class="me-1" style="font-size:.85rem;font-weight:500">Periode:</label>
|
||
<select id="filter-days" onchange="loadSessions()">
|
||
<option value="30">Seneste 30 dage</option>
|
||
<option value="60">Seneste 60 dage</option>
|
||
<option value="90" selected>Seneste 90 dage</option>
|
||
<option value="365">Seneste år</option>
|
||
</select>
|
||
|
||
<label class="ms-2 me-1" style="font-size:.85rem;font-weight:500">Vis:</label>
|
||
<select id="filter-unreg" onchange="loadSessions()">
|
||
<option value="0">Alle sessions</option>
|
||
<option value="1">Kun uregistrerede</option>
|
||
</select>
|
||
|
||
<input type="text" id="filter-search" placeholder="Søg remote ID, hardware, navn…"
|
||
style="min-width:200px;" oninput="filterTable()" />
|
||
|
||
<span class="ms-auto" id="loading-indicator" style="display:none">
|
||
<span class="spinner"></span>
|
||
</span>
|
||
<span class="text-secondary" style="font-size:.82rem" id="row-count"></span>
|
||
</div>
|
||
|
||
<!-- Table -->
|
||
<div class="sessions-table-wrap">
|
||
<table class="sessions-table" id="sessions-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Tidspunkt</th>
|
||
<th>Varighed</th>
|
||
<th>Remote ID</th>
|
||
<th>Teknikker</th>
|
||
<th>Hardware</th>
|
||
<th>Kunde</th>
|
||
<th>Kontakt</th>
|
||
<th>Sag</th>
|
||
<th>Status</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sessions-tbody">
|
||
<tr><td colspan="9" class="text-center py-4 text-secondary">Indlæser…</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Drawer backdrop -->
|
||
<div class="drawer-backdrop" id="drawer-backdrop" onclick="closeRegModal(null)"></div>
|
||
|
||
<!-- Right-side session drawer -->
|
||
<div class="session-drawer" id="session-drawer">
|
||
|
||
<!-- Header -->
|
||
<div class="reg-header">
|
||
<div>
|
||
<h5 style="font-weight:700;margin:0;font-size:1rem">
|
||
<i class="bi bi-display me-2" style="color:var(--accent)"></i>Session
|
||
<span id="reg-session-id-label" style="color:var(--text-secondary);font-weight:500"></span>
|
||
</h5>
|
||
</div>
|
||
<button class="reg-close" onclick="closeRegModal(null)" title="Luk (Esc)">×</button>
|
||
</div>
|
||
|
||
<!-- Context bar -->
|
||
<div class="reg-context">
|
||
<div class="ctx-item">
|
||
<span class="ctx-label">Tidspunkt</span>
|
||
<span class="ctx-value" id="ctx-dt">–</span>
|
||
</div>
|
||
<div class="ctx-item">
|
||
<span class="ctx-label">Varighed</span>
|
||
<span class="ctx-value" id="ctx-dur">–</span>
|
||
</div>
|
||
<div class="ctx-item">
|
||
<span class="ctx-label">Maskine-ID</span>
|
||
<span class="ctx-value font-monospace" style="font-size:.82rem" id="ctx-machine">–</span>
|
||
</div>
|
||
<div class="ctx-item">
|
||
<span class="ctx-label">Teknikker</span>
|
||
<span id="ctx-tech"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Scrollable body -->
|
||
<div class="reg-body">
|
||
<input type="hidden" id="reg-session-id" />
|
||
<input type="hidden" id="reg-hw-id" />
|
||
<input type="hidden" id="reg-sag-id" />
|
||
|
||
<!-- Hardware suggestion -->
|
||
<div id="reg-hw-suggestion" style="display:none" onclick="confirmHwSuggestion()">
|
||
<div class="hw-suggestion" id="reg-hw-suggestion-inner">
|
||
<div>
|
||
<div class="sug-label"><i class="bi bi-magic me-1"></i>Foreslået hardware</div>
|
||
<div class="sug-value" id="reg-sug-hw-name"></div>
|
||
<div style="font-size:.76rem;color:var(--text-secondary)" id="reg-sug-hw-cust"></div>
|
||
</div>
|
||
<button class="btn btn-sm btn-primary" id="reg-sug-btn">Bekræft</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hardware (manual) -->
|
||
<div id="reg-hw-manual-wrap">
|
||
<label class="reg-label"><i class="bi bi-pc-display me-1"></i>Hardware</label>
|
||
<input type="text" id="reg-hw-search" placeholder="Søg hardware (brand/model/AnyDesk ID)" oninput="filterHwOptions()" onfocus="filterHwOptions()">
|
||
<div class="quick-pick" id="reg-hw-quick" style="display:none"></div>
|
||
<select id="reg-hw-select" onchange="onHwChange()">
|
||
<option value="">– vælg hardware –</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Kunde -->
|
||
<div>
|
||
<label class="reg-label"><i class="bi bi-building me-1"></i>Kunde</label>
|
||
<input type="text" id="reg-customer-search" placeholder="Søg firma" oninput="filterCustomerOptions()" onfocus="filterCustomerOptions()">
|
||
<div class="quick-pick" id="reg-customer-quick" style="display:none"></div>
|
||
<select id="reg-customer" onchange="onCustomerChange()">
|
||
<option value="">– vælg kunde –</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Kontakt -->
|
||
<div>
|
||
<label class="reg-label"><i class="bi bi-person me-1"></i>Kontakt</label>
|
||
<input type="text" id="reg-contact-search" placeholder="Søg kontakt" oninput="filterContactOptions()" onfocus="filterContactOptions()">
|
||
<div class="quick-pick" id="reg-contact-quick" style="display:none"></div>
|
||
<select id="reg-contact">
|
||
<option value="">– ingen / vælg –</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Sag live search -->
|
||
<div>
|
||
<label class="reg-label"><i class="bi bi-card-text me-1"></i>Sag</label>
|
||
<div class="sag-search-wrap">
|
||
<input type="text" id="reg-sag-search" placeholder="Søg sag efter titel eller #ID…"
|
||
autocomplete="off" oninput="onSagSearch()" onfocus="onSagSearch()">
|
||
<div class="sag-results" id="sag-results"></div>
|
||
</div>
|
||
<div id="reg-sag-selected" style="display:none;margin-top:.4rem">
|
||
<span class="badge-link badge-sag" style="font-size:.82rem">
|
||
<i class="bi bi-card-text"></i>
|
||
<span id="reg-sag-selected-label"></span>
|
||
<button type="button" onclick="clearSagSelection()"
|
||
style="background:none;border:none;cursor:pointer;opacity:.7;padding:0;color:inherit">×</button>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notat -->
|
||
<div>
|
||
<label class="reg-label"><i class="bi bi-chat-left-text me-1"></i>Notat</label>
|
||
<textarea id="reg-notes" placeholder="Hvad blev lavet? (valgfri)"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="reg-footer">
|
||
<button class="btn btn-secondary btn-sm" onclick="closeRegModal(null)">Luk</button>
|
||
<button class="btn btn-primary btn-sm" onclick="saveRegModal()">
|
||
<i class="bi bi-check-lg me-1"></i>Gem registrering
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast container -->
|
||
<div class="toast-container" id="toast-container"></div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
/* ─── State ──────────────────────────────────────────────────── */
|
||
let allSessions = [];
|
||
let hardwareList = [];
|
||
let customerList = [];
|
||
let currentContactList = [];
|
||
|
||
/* ─── Init ───────────────────────────────────────────────────── */
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
await Promise.all([loadSessions(), loadHardware(), loadCustomers()]);
|
||
});
|
||
|
||
/* ─── API helpers ────────────────────────────────────────────── */
|
||
async function api(url, opts = {}) {
|
||
const res = await fetch(url, { credentials: 'include', ...opts });
|
||
if (!res.ok) throw new Error(await res.text());
|
||
return res.json();
|
||
}
|
||
|
||
/* ─── Load sessions ──────────────────────────────────────────── */
|
||
async function loadSessions() {
|
||
const days = document.getElementById('filter-days').value;
|
||
const unreg = document.getElementById('filter-unreg').value;
|
||
document.getElementById('loading-indicator').style.display = '';
|
||
|
||
try {
|
||
const data = await api(
|
||
`/api/v1/anydesk/sessions-overview?days=${days}&unregistered_only=${unreg === '1'}&limit=500`
|
||
);
|
||
allSessions = data.sessions || [];
|
||
renderStats(allSessions);
|
||
filterTable();
|
||
} catch (e) {
|
||
toast('Kunne ikke hente sessions: ' + e.message, 'error');
|
||
} finally {
|
||
document.getElementById('loading-indicator').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/* ─── Stats ──────────────────────────────────────────────────── */
|
||
function renderStats(sessions) {
|
||
const unreg = sessions.filter(s => !s.sag && !s.contact && !s.hardware).length;
|
||
const totalMin = sessions.reduce((a, s) => a + (s.duration_minutes || 0), 0);
|
||
const devices = new Set(sessions.map(s => s.customer_machine_id || s.remote_id).filter(Boolean)).size;
|
||
|
||
document.getElementById('s-total').textContent = sessions.length;
|
||
document.getElementById('s-unregistered').textContent = unreg;
|
||
document.getElementById('s-duration').textContent = totalMin;
|
||
document.getElementById('s-devices').textContent = devices;
|
||
}
|
||
|
||
/* ─── Filter + render ────────────────────────────────────────── */
|
||
function filterTable() {
|
||
const q = document.getElementById('filter-search').value.toLowerCase().trim();
|
||
const rows = q
|
||
? allSessions.filter(s =>
|
||
(s.customer_machine_id || s.remote_id || '').includes(q) ||
|
||
(s.customer_alias || s.remote_alias || '').toLowerCase().includes(q) ||
|
||
(s.hardware?.model || '').toLowerCase().includes(q) ||
|
||
(s.hardware?.brand || '').toLowerCase().includes(q) ||
|
||
(s.customer?.name || '').toLowerCase().includes(q) ||
|
||
(s.contact?.name || '').toLowerCase().includes(q) ||
|
||
(s.sag?.titel || '').toLowerCase().includes(q)
|
||
)
|
||
: allSessions;
|
||
|
||
document.getElementById('row-count').textContent = `${rows.length} sessions`;
|
||
renderTable(rows);
|
||
}
|
||
|
||
/* ─── Render table ───────────────────────────────────────────── */
|
||
function renderTable(sessions) {
|
||
const tbody = document.getElementById('sessions-tbody');
|
||
if (!sessions.length) {
|
||
tbody.innerHTML = '<tr><td colspan="10" class="text-center py-5 text-secondary">Ingen sessions fundet</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = sessions.map(s => {
|
||
const isUnreg = !s.sag && !s.contact && !s.hardware;
|
||
const rowCls = isUnreg ? 'row-unregistered' : '';
|
||
|
||
const dt = s.started_at ? new Date(s.started_at) : null;
|
||
const dtStr = dt
|
||
? dt.toLocaleDateString('da-DK', {day:'2-digit',month:'short',year:'numeric'}) +
|
||
' ' + dt.toLocaleTimeString('da-DK', {hour:'2-digit',minute:'2-digit'})
|
||
: '–';
|
||
|
||
const dur = s.duration_minutes != null
|
||
? `<span class="dur-pill">${s.duration_minutes} min</span>`
|
||
: `<span class="text-secondary">–</span>`;
|
||
|
||
// customer_machine_id = to.cid (kundens maskine), remote_alias = fra.alias (teknikkerens alias)
|
||
const machineId = s.customer_machine_id || s.remote_id;
|
||
const machineAlias = s.customer_alias;
|
||
const remoteId = machineId
|
||
? `<code style="font-size:.78rem">${machineId}</code>${machineAlias ? `<br><small class="text-secondary">${machineAlias}</small>` : ''}`
|
||
: `<span class="text-secondary">–</span>`;
|
||
|
||
const techCell = s.technician_name
|
||
? `<span class="badge-link badge-cust"><i class="bi bi-person-badge"></i>${s.technician_name}</span>`
|
||
: s.remote_alias
|
||
? `<span style="font-size:.82rem">${s.remote_alias}</span>`
|
||
: s.technician_id
|
||
? `<code style="font-size:.78rem">${s.technician_id}</code>`
|
||
: `<span class="text-secondary" style="font-size:.78rem">–</span>`;
|
||
|
||
const hwCell = s.hardware
|
||
? `<a href="/hardware/${s.hardware.id}" class="badge-link badge-hw"><i class="bi bi-pc-display"></i>${s.hardware.brand||''} ${s.hardware.model||''}</a>`
|
||
: `<span class="text-secondary" style="font-size:.78rem">–</span>`;
|
||
|
||
const custCell = (s.customer)
|
||
? `<a href="/customers/${s.customer.id}" class="badge-link badge-cust"><i class="bi bi-building"></i>${s.customer.name}</a>`
|
||
: `<span class="text-secondary" style="font-size:.78rem">–</span>`;
|
||
|
||
const contCell = s.contact
|
||
? `<a href="/contacts/${s.contact.id}" class="badge-link badge-cont"><i class="bi bi-person"></i>${s.contact.name}</a>`
|
||
: `<span class="text-secondary" style="font-size:.78rem">–</span>`;
|
||
|
||
const sagCell = s.sag
|
||
? `<a href="/sag/${s.sag.id}" class="badge-link badge-sag"><i class="bi bi-card-text"></i>${s.sag.titel||'Sag #'+s.sag.id}</a>`
|
||
: `<span class="text-secondary" style="font-size:.78rem">–</span>`;
|
||
|
||
const statusBadge = isUnreg
|
||
? `<span class="badge-unreg">Uregistreret</span>`
|
||
: `<span class="badge-ok">Registreret</span>`;
|
||
|
||
return `<tr class="${rowCls}" data-id="${s.id}" onclick="openRegModal(${s.id})">
|
||
<td style="white-space:nowrap">${dtStr}</td>
|
||
<td>${dur}</td>
|
||
<td>${remoteId}</td>
|
||
<td>${techCell}</td>
|
||
<td>${hwCell}</td>
|
||
<td>${custCell}</td>
|
||
<td>${contCell}</td>
|
||
<td>${sagCell}</td>
|
||
<td>${statusBadge}</td>
|
||
<td style="text-align:right;padding-right:1rem">
|
||
<i class="bi bi-chevron-right" style="color:var(--text-secondary);font-size:.78rem"></i>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
/* ─── Fetch from AnyDesk API ─────────────────────────────────── */
|
||
async function fetchFromApi() {
|
||
const btn = document.getElementById('btn-fetch');
|
||
const origHtml = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner"></span> Henter…';
|
||
try {
|
||
const data = await api('/api/v1/anydesk/fetch-from-api?days=90&limit=1000', { method: 'POST' });
|
||
toast(`✅ ${data.imported} nye, ${data.updated} opdateret`, 'success');
|
||
await loadSessions();
|
||
} catch (e) {
|
||
toast('Fejl: ' + e.message, 'error');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = origHtml;
|
||
}
|
||
}
|
||
|
||
/* ─── Auto-link ──────────────────────────────────────────────── */
|
||
async function autoLink() {
|
||
const btn = document.getElementById('btn-autolink');
|
||
const origHtml = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner"></span> Linker…';
|
||
try {
|
||
const data = await api('/api/v1/anydesk/auto-link', { method: 'POST' });
|
||
toast(`✅ ${data.linked} sessions linket til hardware`, 'success');
|
||
await loadSessions();
|
||
} catch (e) {
|
||
toast('Fejl: ' + e.message, 'error');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = origHtml;
|
||
}
|
||
}
|
||
|
||
/* ─── Load hardware + customers for dropdowns ────────────────── */
|
||
async function loadHardware() {
|
||
try {
|
||
const data = await api('/api/v1/anydesk/hardware-assets');
|
||
hardwareList = data.assets || [];
|
||
} catch(e) { /* silent */ }
|
||
}
|
||
|
||
async function loadCustomers() {
|
||
try {
|
||
const data = await api('/api/v1/customers/?limit=500');
|
||
customerList = data.customers || data || [];
|
||
} catch(e) { /* silent */ }
|
||
}
|
||
|
||
function populateHwSelect(selectedId) {
|
||
const sel = document.getElementById('reg-hw-select');
|
||
sel.innerHTML = '<option value="">– vælg hardware –</option>';
|
||
hardwareList.forEach(a => {
|
||
const opt = document.createElement('option');
|
||
opt.value = a.id;
|
||
opt.textContent = `${a.brand||''} ${a.model||''} (AD: ${a.anydesk_id||'–'}) – ${a.customer_name||'–'}`;
|
||
if (a.id == selectedId) opt.selected = true;
|
||
sel.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
function filterHwOptions() {
|
||
const q = (document.getElementById('reg-hw-search').value || '').toLowerCase().trim();
|
||
const selectedId = document.getElementById('reg-hw-select').value;
|
||
const sel = document.getElementById('reg-hw-select');
|
||
sel.innerHTML = '<option value="">– vælg hardware –</option>';
|
||
|
||
const matches = hardwareList
|
||
.filter(a => {
|
||
if (!q) return true;
|
||
const hay = `${a.brand || ''} ${a.model || ''} ${a.anydesk_id || ''} ${a.customer_name || ''}`.toLowerCase();
|
||
return hay.includes(q);
|
||
});
|
||
|
||
matches.forEach(a => {
|
||
const opt = document.createElement('option');
|
||
opt.value = a.id;
|
||
opt.textContent = `${a.brand||''} ${a.model||''} (AD: ${a.anydesk_id||'–'}) – ${a.customer_name||'–'}`;
|
||
if (String(a.id) === String(selectedId)) opt.selected = true;
|
||
sel.appendChild(opt);
|
||
});
|
||
|
||
renderHwQuick(matches.slice(0, 8));
|
||
}
|
||
|
||
function renderHwQuick(items) {
|
||
const box = document.getElementById('reg-hw-quick');
|
||
if (!items.length) {
|
||
box.style.display = 'none';
|
||
box.innerHTML = '';
|
||
return;
|
||
}
|
||
box.innerHTML = items.map(a => `
|
||
<div class="quick-item" onclick="pickHardware(${a.id})">
|
||
<div>${a.brand || ''} ${a.model || ''}</div>
|
||
<div class="sub">AD: ${a.anydesk_id || '–'} · ${a.customer_name || '–'}</div>
|
||
</div>
|
||
`).join('');
|
||
box.style.display = '';
|
||
}
|
||
|
||
function pickHardware(hwId) {
|
||
const hw = hardwareList.find(a => String(a.id) === String(hwId));
|
||
if (!hw) return;
|
||
document.getElementById('reg-hw-select').value = hw.id;
|
||
document.getElementById('reg-hw-search').value = `${hw.brand || ''} ${hw.model || ''}`.trim();
|
||
document.getElementById('reg-hw-quick').style.display = 'none';
|
||
onHwChange();
|
||
}
|
||
|
||
function populateCustomerSelect(selectedId) {
|
||
const sel = document.getElementById('reg-customer');
|
||
sel.innerHTML = '<option value="">– vælg kunde –</option>';
|
||
customerList.forEach(c => {
|
||
const opt = document.createElement('option');
|
||
opt.value = c.id;
|
||
opt.textContent = c.name;
|
||
if (c.id == selectedId) opt.selected = true;
|
||
sel.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
function filterCustomerOptions() {
|
||
const q = (document.getElementById('reg-customer-search').value || '').toLowerCase().trim();
|
||
const selectedId = document.getElementById('reg-customer').value;
|
||
const sel = document.getElementById('reg-customer');
|
||
sel.innerHTML = '<option value="">– vælg kunde –</option>';
|
||
|
||
const matches = customerList
|
||
.filter(c => !q || (c.name || '').toLowerCase().includes(q))
|
||
;
|
||
|
||
matches.forEach(c => {
|
||
const opt = document.createElement('option');
|
||
opt.value = c.id;
|
||
opt.textContent = c.name;
|
||
if (String(c.id) === String(selectedId)) opt.selected = true;
|
||
sel.appendChild(opt);
|
||
});
|
||
|
||
renderCustomerQuick(matches.slice(0, 8));
|
||
}
|
||
|
||
function renderCustomerQuick(items) {
|
||
const box = document.getElementById('reg-customer-quick');
|
||
if (!items.length) {
|
||
box.style.display = 'none';
|
||
box.innerHTML = '';
|
||
return;
|
||
}
|
||
box.innerHTML = items.map(c => `
|
||
<div class="quick-item" onclick="pickCustomer(${c.id})">
|
||
<div>${c.name || '–'}</div>
|
||
</div>
|
||
`).join('');
|
||
box.style.display = '';
|
||
}
|
||
|
||
function pickCustomer(customerId) {
|
||
const customer = customerList.find(c => String(c.id) === String(customerId));
|
||
if (!customer) return;
|
||
document.getElementById('reg-customer').value = customer.id;
|
||
document.getElementById('reg-customer-search').value = customer.name || '';
|
||
document.getElementById('reg-customer-quick').style.display = 'none';
|
||
onCustomerChange();
|
||
}
|
||
|
||
/* ─── Smart registration modal ───────────────────────────────── */
|
||
let _sagSearchTimer = null;
|
||
let _regSuggestedHwId = null;
|
||
|
||
function openRegModal(sessionId) {
|
||
const s = allSessions.find(x => x.id === sessionId);
|
||
if (!s) return;
|
||
|
||
// Reset state
|
||
_regSuggestedHwId = null;
|
||
document.getElementById('reg-session-id').value = sessionId;
|
||
document.getElementById('reg-hw-id').value = '';
|
||
document.getElementById('reg-sag-id').value = '';
|
||
document.getElementById('reg-sag-search').value = '';
|
||
document.getElementById('sag-results').innerHTML = '';
|
||
document.getElementById('sag-results').classList.remove('open');
|
||
document.getElementById('reg-sag-selected').style.display = 'none';
|
||
document.getElementById('reg-sag-search').style.display = '';
|
||
document.getElementById('reg-notes').value = s.notes || '';
|
||
document.getElementById('reg-session-id-label').textContent = '#' + sessionId;
|
||
document.getElementById('reg-hw-search').value = '';
|
||
document.getElementById('reg-customer-search').value = '';
|
||
document.getElementById('reg-contact-search').value = '';
|
||
|
||
// Context bar
|
||
const dt = s.started_at ? new Date(s.started_at) : null;
|
||
document.getElementById('ctx-dt').textContent = dt
|
||
? dt.toLocaleDateString('da-DK', {day:'2-digit', month:'short', year:'numeric'}) +
|
||
' ' + dt.toLocaleTimeString('da-DK', {hour:'2-digit', minute:'2-digit'})
|
||
: '–';
|
||
document.getElementById('ctx-dur').textContent =
|
||
s.duration_minutes != null ? s.duration_minutes + ' min' : '–';
|
||
document.getElementById('ctx-machine').textContent =
|
||
s.customer_machine_id || s.remote_id || '–';
|
||
|
||
// Technician badge
|
||
const techEl = document.getElementById('ctx-tech');
|
||
if (s.technician_name) {
|
||
techEl.innerHTML = `<span class="ctx-badge ctx-tech">👤 ${s.technician_name}</span>`;
|
||
} else if (s.remote_alias) {
|
||
techEl.innerHTML = `<span class="ctx-badge ctx-tech">👤 ${s.remote_alias}</span>`;
|
||
} else if (s.technician_id) {
|
||
techEl.innerHTML = `<span class="ctx-badge ctx-unknown">ID ${s.technician_id}</span>`;
|
||
} else {
|
||
techEl.innerHTML = `<span class="ctx-badge ctx-unknown">Ukendt</span>`;
|
||
}
|
||
|
||
// Populate selects
|
||
populateHwSelect(s.hardware?.id);
|
||
populateCustomerSelect(s.customer?.id);
|
||
loadContactsForCustomer(s.customer?.id, s.contact?.id);
|
||
|
||
// Pre-fill sag if already linked
|
||
if (s.sag) {
|
||
document.getElementById('reg-sag-id').value = s.sag.id;
|
||
document.getElementById('reg-sag-selected-label').textContent =
|
||
'#' + s.sag.id + (s.sag.titel ? ' — ' + s.sag.titel : '');
|
||
document.getElementById('reg-sag-selected').style.display = '';
|
||
document.getElementById('reg-sag-search').style.display = 'none';
|
||
}
|
||
|
||
// Hardware suggestion
|
||
document.getElementById('reg-hw-suggestion').style.display = 'none';
|
||
document.getElementById('reg-hw-manual-wrap').style.display = '';
|
||
const machineId = s.customer_machine_id || s.remote_id;
|
||
if (machineId) {
|
||
const match = hardwareList.find(a => a.anydesk_id && a.anydesk_id === machineId);
|
||
if (match && !s.hardware) {
|
||
_regSuggestedHwId = match.id;
|
||
document.getElementById('reg-sug-hw-name').textContent =
|
||
(match.brand || '') + ' ' + (match.model || '');
|
||
document.getElementById('reg-sug-hw-cust').textContent =
|
||
match.customer_name ? '📍 ' + match.customer_name : '';
|
||
document.getElementById('reg-hw-suggestion').style.display = '';
|
||
// Hide manual picker when suggestion shown and approved
|
||
} else if (match && s.hardware && match.id === s.hardware.id) {
|
||
// Already linked to this hw — show as confirmed
|
||
_regSuggestedHwId = match.id;
|
||
document.getElementById('reg-sug-hw-name').textContent =
|
||
(match.brand || '') + ' ' + (match.model || '');
|
||
document.getElementById('reg-sug-hw-cust').textContent =
|
||
match.customer_name ? '📍 ' + match.customer_name : '';
|
||
document.getElementById('reg-hw-suggestion').style.display = '';
|
||
document.getElementById('reg-hw-suggestion-inner').classList.add('confirmed');
|
||
document.getElementById('reg-sug-btn').style.display = 'none';
|
||
document.getElementById('reg-hw-manual-wrap').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// If already linked hardware (no suggestion path)
|
||
if (s.hardware) {
|
||
document.getElementById('reg-hw-select').value = s.hardware.id;
|
||
document.getElementById('reg-hw-id').value = s.hardware.id;
|
||
}
|
||
|
||
document.getElementById('drawer-backdrop').classList.add('open');
|
||
document.getElementById('session-drawer').classList.add('open');
|
||
}
|
||
|
||
function confirmHwSuggestion() {
|
||
if (!_regSuggestedHwId) return;
|
||
const inner = document.getElementById('reg-hw-suggestion-inner');
|
||
if (inner.classList.contains('confirmed')) return;
|
||
|
||
inner.classList.add('confirmed');
|
||
document.getElementById('reg-sug-btn').style.display = 'none';
|
||
document.getElementById('reg-hw-manual-wrap').style.display = 'none';
|
||
document.getElementById('reg-hw-id').value = _regSuggestedHwId;
|
||
document.getElementById('reg-hw-select').value = _regSuggestedHwId;
|
||
|
||
// Auto-fill customer from hardware asset
|
||
const hw = hardwareList.find(a => a.id == _regSuggestedHwId);
|
||
if (hw && hw.customer_id) {
|
||
document.getElementById('reg-customer').value = hw.customer_id;
|
||
loadContactsForCustomer(hw.customer_id, null);
|
||
}
|
||
}
|
||
|
||
function onHwChange() {
|
||
const hwId = document.getElementById('reg-hw-select').value;
|
||
document.getElementById('reg-hw-id').value = hwId;
|
||
if (!hwId) return;
|
||
const hw = hardwareList.find(a => a.id == hwId);
|
||
if (hw && hw.customer_id) {
|
||
document.getElementById('reg-customer').value = hw.customer_id;
|
||
loadContactsForCustomer(hw.customer_id, null);
|
||
}
|
||
}
|
||
|
||
async function onCustomerChange() {
|
||
const customerId = document.getElementById('reg-customer').value;
|
||
await loadContactsForCustomer(customerId, null);
|
||
// Reset sag search filter
|
||
document.getElementById('reg-sag-search').value = '';
|
||
document.getElementById('sag-results').innerHTML = '';
|
||
document.getElementById('sag-results').classList.remove('open');
|
||
}
|
||
|
||
function filterContactOptions() {
|
||
const q = (document.getElementById('reg-contact-search').value || '').toLowerCase().trim();
|
||
const selectedId = document.getElementById('reg-contact').value;
|
||
const sel = document.getElementById('reg-contact');
|
||
sel.innerHTML = '<option value="">– ingen / vælg –</option>';
|
||
|
||
const matches = currentContactList
|
||
.filter(c => {
|
||
if (!q) return true;
|
||
const fullName = `${c.first_name || ''} ${c.last_name || ''}`.toLowerCase();
|
||
const email = (c.email || '').toLowerCase();
|
||
return fullName.includes(q) || email.includes(q);
|
||
});
|
||
|
||
matches.forEach(c => {
|
||
const opt = document.createElement('option');
|
||
opt.value = c.id;
|
||
opt.textContent = ((c.first_name||'') + ' ' + (c.last_name||'')).trim();
|
||
if (c.email) opt.textContent += ` (${c.email})`;
|
||
if (String(c.id) === String(selectedId)) opt.selected = true;
|
||
sel.appendChild(opt);
|
||
});
|
||
|
||
renderContactQuick(matches.slice(0, 8));
|
||
}
|
||
|
||
function renderContactQuick(items) {
|
||
const box = document.getElementById('reg-contact-quick');
|
||
if (!items.length) {
|
||
box.style.display = 'none';
|
||
box.innerHTML = '';
|
||
return;
|
||
}
|
||
box.innerHTML = items.map(c => {
|
||
const fullName = `${c.first_name || ''} ${c.last_name || ''}`.trim() || '(ingen navn)';
|
||
return `
|
||
<div class="quick-item" onclick="pickContact(${c.id})">
|
||
<div>${fullName}</div>
|
||
<div class="sub">${c.email || ''}</div>
|
||
</div>`;
|
||
}).join('');
|
||
box.style.display = '';
|
||
}
|
||
|
||
function pickContact(contactId) {
|
||
const contact = currentContactList.find(c => String(c.id) === String(contactId));
|
||
if (!contact) return;
|
||
document.getElementById('reg-contact').value = contact.id;
|
||
document.getElementById('reg-contact-search').value = `${contact.first_name || ''} ${contact.last_name || ''}`.trim();
|
||
document.getElementById('reg-contact-quick').style.display = 'none';
|
||
}
|
||
|
||
async function loadContactsForCustomer(customerId, selectedId) {
|
||
const sel = document.getElementById('reg-contact');
|
||
sel.innerHTML = '<option value="">– ingen / vælg –</option>';
|
||
try {
|
||
const url = customerId
|
||
? `/api/v1/contacts/?customer_id=${customerId}&limit=300`
|
||
: '/api/v1/contacts/?limit=300';
|
||
const data = await api(url);
|
||
currentContactList = data.contacts || data || [];
|
||
filterContactOptions();
|
||
if (selectedId) {
|
||
document.getElementById('reg-contact').value = selectedId;
|
||
}
|
||
} catch(e) { /* silent */ }
|
||
}
|
||
|
||
function onSagSearch() {
|
||
clearTimeout(_sagSearchTimer);
|
||
const q = document.getElementById('reg-sag-search').value.trim();
|
||
if (!q) {
|
||
document.getElementById('sag-results').innerHTML = '';
|
||
document.getElementById('sag-results').classList.remove('open');
|
||
return;
|
||
}
|
||
_sagSearchTimer = setTimeout(async () => {
|
||
const customerId = document.getElementById('reg-customer').value;
|
||
let url = `/api/v1/sag?q=${encodeURIComponent(q)}&limit=8`;
|
||
if (customerId) url += `&customer_id=${customerId}`;
|
||
try {
|
||
const data = await api(url);
|
||
const sager = data.sager || data || [];
|
||
const resultsEl = document.getElementById('sag-results');
|
||
if (!sager.length) {
|
||
resultsEl.innerHTML = '<div class="sag-result-item text-secondary">Ingen sager fundet</div>';
|
||
} else {
|
||
resultsEl.innerHTML = sager.map(sag => {
|
||
const statusClass = (sag.status||'').toLowerCase().includes('åben') ||
|
||
(sag.status||'').toLowerCase().includes('open')
|
||
? 'sag-st-open' : 'sag-st-other';
|
||
const titel = sag.titel || sag.title || '(ingen titel)';
|
||
return `<div class="sag-result-item"
|
||
onclick="selectSag(${sag.id}, ${JSON.stringify(titel)}, ${JSON.stringify(sag.status||'')})">
|
||
<span>#${sag.id} — ${titel}</span>
|
||
${sag.status ? `<span class="sag-status-badge ${statusClass}">${sag.status}</span>` : ''}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
resultsEl.classList.add('open');
|
||
} catch(e) { /* silent */ }
|
||
}, 280);
|
||
}
|
||
|
||
function selectSag(id, titel, status) {
|
||
document.getElementById('reg-sag-id').value = id;
|
||
const label = '#' + id + (titel ? ' — ' + titel : '');
|
||
document.getElementById('reg-sag-selected-label').textContent = label;
|
||
document.getElementById('reg-sag-selected').style.display = '';
|
||
document.getElementById('reg-sag-search').style.display = 'none';
|
||
document.getElementById('sag-results').classList.remove('open');
|
||
document.getElementById('reg-sag-search').value = '';
|
||
}
|
||
|
||
function clearSagSelection() {
|
||
document.getElementById('reg-sag-id').value = '';
|
||
document.getElementById('reg-sag-selected').style.display = 'none';
|
||
document.getElementById('reg-sag-search').style.display = '';
|
||
document.getElementById('reg-sag-search').value = '';
|
||
document.getElementById('reg-sag-search').focus();
|
||
}
|
||
|
||
function closeRegModal(event) {
|
||
document.getElementById('drawer-backdrop').classList.remove('open');
|
||
document.getElementById('session-drawer').classList.remove('open');
|
||
document.getElementById('sag-results').classList.remove('open');
|
||
document.getElementById('reg-hw-quick').style.display = 'none';
|
||
document.getElementById('reg-customer-quick').style.display = 'none';
|
||
document.getElementById('reg-contact-quick').style.display = 'none';
|
||
}
|
||
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape') closeRegModal(null);
|
||
});
|
||
|
||
document.addEventListener('click', e => {
|
||
if (!e.target.closest('#reg-hw-manual-wrap')) {
|
||
document.getElementById('reg-hw-quick').style.display = 'none';
|
||
}
|
||
if (!e.target.closest('#reg-customer') && !e.target.closest('#reg-customer-search') && !e.target.closest('#reg-customer-quick')) {
|
||
document.getElementById('reg-customer-quick').style.display = 'none';
|
||
}
|
||
if (!e.target.closest('#reg-contact') && !e.target.closest('#reg-contact-search') && !e.target.closest('#reg-contact-quick')) {
|
||
document.getElementById('reg-contact-quick').style.display = 'none';
|
||
}
|
||
});
|
||
|
||
async function saveRegModal() {
|
||
const id = document.getElementById('reg-session-id').value;
|
||
const hw = document.getElementById('reg-hw-id').value ||
|
||
document.getElementById('reg-hw-select').value;
|
||
const cust = document.getElementById('reg-customer').value;
|
||
const contact = document.getElementById('reg-contact').value;
|
||
const sag = document.getElementById('reg-sag-id').value;
|
||
const notes = document.getElementById('reg-notes').value;
|
||
|
||
const params = new URLSearchParams();
|
||
if (hw) params.append('hardware_asset_id', hw);
|
||
if (cust) params.append('customer_id', cust);
|
||
if (contact) params.append('contact_id', contact);
|
||
if (sag) params.append('sag_id', sag);
|
||
if (notes) params.append('notes', notes);
|
||
|
||
try {
|
||
await api(`/api/v1/anydesk/sessions/${id}/link?${params}`, { method: 'PATCH' });
|
||
toast('✅ Session registreret', 'success');
|
||
document.getElementById('drawer-backdrop').classList.remove('open');
|
||
document.getElementById('session-drawer').classList.remove('open');
|
||
await loadSessions();
|
||
} catch (e) {
|
||
toast('Fejl: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
/* ─── Toast ──────────────────────────────────────────────────── */
|
||
function toast(msg, type = '') {
|
||
const el = document.createElement('div');
|
||
el.className = 'toast-msg ' + type;
|
||
el.textContent = msg;
|
||
document.getElementById('toast-container').appendChild(el);
|
||
setTimeout(() => el.remove(), 4000);
|
||
}
|
||
</script>
|
||
{% endblock %}
|