2026-02-01 11:58:44 +01:00
{% extends "shared/frontend/base.html" %}
2026-02-01 00:38:10 +01:00
2026-02-01 11:58:44 +01:00
{% block title %}Sager - BMC Hub{% endblock %}
2026-01-29 23:07:33 +01:00
2026-02-01 11:58:44 +01:00
{% block extra_css %}
< style >
.search-bar {
2026-08-30 14:34:43 +02:00
position: relative;
2026-04-21 01:34:40 +02:00
margin-bottom: 0;
2026-08-30 14:34:43 +02:00
flex: 1 1 460px;
2026-04-21 01:34:40 +02:00
min-width: 240px;
2026-02-01 11:58:44 +01:00
}
2026-08-30 14:34:43 +02:00
2026-02-01 11:58:44 +01:00
.search-bar input {
2026-08-30 14:34:43 +02:00
min-height: 46px;
border-radius: 12px;
border: 1px solid rgba(15,76,117,.15);
padding: 0.6rem 4.5rem 0.6rem 2.7rem;
background: var(--bg-card);
box-shadow: 0 4px 16px rgba(15, 76, 117, .06);
transition: border-color .16s ease, box-shadow .16s ease;
}
.search-bar input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(15,76,117,.12), 0 5px 18px rgba(15,76,117,.08);
}
.search-bar .search-icon {
position: absolute; left: .95rem; top: 50%; transform: translateY(-50%);
color: var(--accent); pointer-events: none;
2026-04-21 01:34:40 +02:00
}
2026-08-30 14:34:43 +02:00
.search-clear-btn {
position: absolute; right: .45rem; top: 50%; transform: translateY(-50%);
width: 34px; height: 34px; border: 0; border-radius: 9px;
background: transparent; color: var(--text-secondary);
}
.search-clear-btn:hover { background: rgba(15,76,117,.09); color: var(--accent); }
.search-shortcut {
position:absolute; right:2.85rem; top:50%; transform:translateY(-50%);
padding:.1rem .35rem; border:1px solid rgba(0,0,0,.12); border-radius:5px;
color:var(--text-secondary); font-size:.68rem; background:rgba(255,255,255,.7);
}
.sag-toolbar-card { padding: .85rem; margin-bottom: .9rem; background: var(--bg-card); border:1px solid rgba(15,76,117,.09); border-radius:16px; box-shadow:0 5px 20px rgba(15,76,117,.055); }
.sag-quick-filters { display:flex; flex-wrap:wrap; align-items:center; gap:.48rem; padding-top:.7rem; margin-top:.7rem; border-top:1px solid rgba(15,76,117,.08); }
.sag-quick-filter { display:inline-flex; align-items:center; gap:.4rem; padding:.43rem .72rem; border:1px solid rgba(15,76,117,.14); border-radius:999px; background:rgba(15,76,117,.035); color:var(--text-primary); font-size:.8rem; font-weight:650; transition:all .16s ease; }
.sag-quick-filter:hover { border-color:rgba(15,76,117,.32); background:rgba(15,76,117,.09); color:var(--accent); transform:translateY(-1px); }
.sag-quick-filter.active { color:#fff; background:var(--accent); border-color:var(--accent); box-shadow:0 4px 12px rgba(15,76,117,.2); }
.sag-quick-filter[data-quick-filter="overdue"].active { background:#c92a2a; border-color:#c92a2a; }
.sag-advanced-filters { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; margin-bottom:1rem; padding:.6rem .7rem; border-radius:12px; background:rgba(15,76,117,.035); border:1px solid rgba(15,76,117,.07); }
2026-04-21 01:34:40 +02:00
.top-controls-row {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.9rem;
}
.top-controls-actions {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-left: auto;
flex-shrink: 0;
}
.top-controls-actions .btn {
white-space: nowrap;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
2026-02-01 11:58:44 +01:00
}
.table-wrapper {
background: var(--bg-card);
border-radius: 12px;
2026-03-20 00:24:58 +01:00
overflow-x: auto;
overflow-y: hidden;
2026-02-01 11:58:44 +01:00
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.sag-table {
width: 100%;
2026-03-20 18:43:45 +01:00
min-width: 1760px;
2026-02-01 11:58:44 +01:00
margin: 0;
}
.sag-table thead {
background: var(--accent);
color: white;
}
.sag-table thead th {
2026-03-20 00:24:58 +01:00
padding: 0.6rem 0.75rem;
2026-02-01 11:58:44 +01:00
font-weight: 600;
2026-03-20 00:24:58 +01:00
font-size: 0.78rem;
2026-02-01 11:58:44 +01:00
text-transform: uppercase;
2026-03-20 00:24:58 +01:00
letter-spacing: 0.3px;
2026-02-01 11:58:44 +01:00
border: none;
2026-03-20 00:24:58 +01:00
white-space: nowrap;
2026-02-01 11:58:44 +01:00
}
2026-06-18 22:20:39 +02:00
.sag-table thead th.col-expand,
.sag-table tbody td.col-expand {
width: 44px;
min-width: 44px;
max-width: 44px;
text-align: center;
padding-left: 0.45rem;
padding-right: 0.45rem;
}
2026-02-01 11:58:44 +01:00
.sag-table tbody tr {
border-bottom: 1px solid rgba(0,0,0,0.05);
transition: all 0.2s;
cursor: pointer;
}
2026-04-15 09:34:26 +02:00
.sag-table tbody tr:nth-child(even) {
background: rgba(15, 76, 117, 0.04);
}
2026-02-01 11:58:44 +01:00
.sag-table tbody tr:hover {
background: var(--accent-light);
}
2026-08-30 14:34:43 +02:00
.sag-table tbody tr.sag-deadline-overdue {
background: rgba(201, 42, 42, 0.045);
box-shadow: inset 5px 0 0 #c92a2a, inset 0 0 18px rgba(201, 42, 42, 0.11);
}
.sag-table tbody tr.sag-deadline-overdue:hover {
background: rgba(201, 42, 42, 0.09);
box-shadow: inset 5px 0 0 #b42318, inset 0 0 22px rgba(201, 42, 42, 0.16);
}
.sag-table tbody tr.sag-deadline-overdue td:last-child {
color: #b42318 !important;
font-weight: 700;
}
2026-02-01 11:58:44 +01:00
.sag-table tbody td {
2026-03-20 00:24:58 +01:00
padding: 0.5rem 0.75rem;
vertical-align: top;
font-size: 0.86rem;
white-space: nowrap;
}
.sag-table td.col-company,
.sag-table td.col-contact,
.sag-table td.col-owner,
.sag-table td.col-group,
.sag-table td.col-desc {
white-space: normal;
}
.sag-table td.col-company,
.sag-table td.col-contact,
.sag-table td.col-owner,
.sag-table td.col-group {
max-width: 180px;
}
.sag-table td.col-desc {
min-width: 260px;
max-width: 360px;
2026-02-01 11:58:44 +01:00
}
2026-08-30 14:34:43 +02:00
.sag-column-list { min-width: 260px; max-height: 420px; overflow-y: auto; }
.sag-column-item { display:flex; align-items:center; gap:.55rem; padding:.45rem .6rem; border-radius:8px; cursor:grab; }
.sag-column-item:hover, .sag-column-item.dragging { background:rgba(15,76,117,.1); }
.sag-column-item .drag-handle { color:var(--text-secondary); cursor:grab; }
2026-02-01 11:58:44 +01:00
.sag-id {
2026-08-30 14:34:43 +02:00
display: inline-flex;
align-items: center;
gap: 0.28rem;
padding: 0.22rem 0.48rem;
border: 1px solid rgba(15, 76, 117, 0.16);
border-radius: 7px;
background: rgba(15, 76, 117, 0.07);
color: var(--accent);
2026-02-01 11:58:44 +01:00
font-weight: 700;
2026-08-30 14:34:43 +02:00
font-size: 0.82rem;
line-height: 1.2;
text-decoration: none;
transition: background-color .16s ease, border-color .16s ease, transform .16s ease;
}
.sag-id:hover {
2026-02-01 11:58:44 +01:00
color: var(--accent);
2026-08-30 14:34:43 +02:00
background: rgba(15, 76, 117, 0.14);
border-color: rgba(15, 76, 117, 0.3);
transform: translateY(-1px);
text-decoration: none;
}
.sag-entity-link {
display: inline-flex;
align-items: center;
gap: 0.38rem;
max-width: 100%;
padding: 0.25rem 0.5rem;
border-radius: 7px;
color: var(--text-primary);
font-weight: 600;
line-height: 1.25;
text-decoration: none;
transition: color .16s ease, background-color .16s ease, transform .16s ease;
}
.sag-entity-link i {
flex: 0 0 auto;
color: var(--text-secondary);
font-size: 0.78rem;
}
.sag-entity-link span {
overflow: hidden;
text-overflow: ellipsis;
}
.sag-entity-link:hover {
color: var(--accent);
background: rgba(15, 76, 117, 0.09);
text-decoration: none;
transform: translateX(2px);
}
.sag-entity-link:hover i {
color: var(--accent);
}
.sag-id:focus-visible,
.sag-entity-link:focus-visible {
outline: 3px solid rgba(15, 76, 117, 0.22);
outline-offset: 2px;
2026-02-01 11:58:44 +01:00
}
2026-04-01 21:34:58 +02:00
.sag-unread-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.35rem;
height: 1.35rem;
padding: 0 0.35rem;
margin-left: 0.45rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
color: #fff;
vertical-align: middle;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85);
}
.sag-unread-fresh {
background: #2f9e44;
}
.sag-unread-warm {
background: #f08c00;
}
.sag-unread-hot {
background: #c92a2a;
animation: sagUnreadPulse 1.8s ease-in-out infinite;
}
@keyframes sagUnreadPulse {
0% { transform: scale(1); }
50% { transform: scale(1.08); }
100% { transform: scale(1); }
}
2026-02-01 11:58:44 +01:00
.sag-titel {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0;
}
.sag-beskrivelse {
color: var(--text-secondary);
font-size: 0.8rem;
margin-top: 0.2rem;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Tree view styles */
.tree-row {
position: relative;
}
.tree-row.has-children {
cursor: pointer;
}
.tree-row.has-children:before {
content: none;
}
.tree-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: var(--accent-light);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.75rem;
color: var(--accent);
font-weight: bold;
position: absolute;
left: 0.5rem;
top: 50%;
transform: translateY(-50%);
}
2026-06-18 22:20:39 +02:00
.col-expand .tree-toggle {
position: static;
transform: none;
}
2026-02-01 11:58:44 +01:00
.tree-toggle:hover {
background: var(--accent);
color: white;
}
.tree-child {
background: rgba(0,0,0,0.02);
}
.tree-child td {
padding: 0.5rem 1rem !important;
border-top: none !important;
}
2026-06-18 22:20:39 +02:00
.tree-child .child-branch {
color: rgba(0,0,0,0.4);
font-size: 1rem;
font-weight: 600;
line-height: 1;
2026-02-01 11:58:44 +01:00
}
.relation-badge {
display: inline-block;
font-size: 0.65rem;
padding: 0.25rem 0.6rem;
background: var(--accent);
color: white;
border-radius: 12px;
margin-right: 0.5rem;
font-weight: 600;
vertical-align: middle;
}
.status-badge {
display: inline-block;
padding: 0.35rem 0.8rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
2026-06-18 22:20:39 +02:00
background: #e9ecef;
color: #495057;
2026-02-01 11:58:44 +01:00
}
.status-åben {
background: #fff3cd;
color: #856404;
}
.status-lukket {
background: #d4edda;
color: #155724;
}
2026-06-18 22:20:39 +02:00
.status-under-behandling,
.status-i-gang,
.status-in-progress {
background: #dbeafe;
color: #1e3a8a;
}
.status-afventer,
.status-on-hold {
background: #fef3c7;
color: #92400e;
}
.status-løst,
.status-afsluttet,
.status-resolved,
.status-done,
.status-closed {
background: #d1fae5;
color: #065f46;
}
2026-08-30 14:34:43 +02:00
.sag-inline-select {
min-height: 34px;
border: 1px solid rgba(15, 76, 117, 0.16);
border-radius: 9px;
background-color: rgba(15, 76, 117, 0.045);
color: var(--text-primary);
font-size: 0.8rem;
font-weight: 600;
padding: 0.34rem 2rem 0.34rem 0.62rem;
box-shadow: none;
cursor: pointer;
transition: border-color .16s ease, background-color .16s ease, box-shadow .16s ease, transform .16s ease;
}
.sag-inline-select:hover:not(:disabled) {
border-color: rgba(15, 76, 117, 0.38);
background-color: rgba(15, 76, 117, 0.09);
}
.sag-inline-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(15, 76, 117, 0.14);
}
.sag-inline-select:disabled {
opacity: .62;
cursor: wait;
}
.sag-owner-select {
background-color: rgba(99, 102, 241, 0.055);
border-color: rgba(99, 102, 241, 0.17);
}
.sag-status-select.status-tone-open {
color: #8a5a00;
background-color: #fff8df;
border-color: #efd991;
}
.sag-status-select.status-tone-progress {
color: #174a89;
background-color: #eaf3ff;
border-color: #b8d5fa;
}
.sag-status-select.status-tone-waiting {
color: #8a4600;
background-color: #fff1df;
border-color: #f0c58e;
}
.sag-status-select.status-tone-done {
color: #087044;
background-color: #e5f8ef;
border-color: #acdcbc;
}
2026-02-01 11:58:44 +01:00
.filter-pills {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.filter-pill {
padding: 0.5rem 1rem;
border-radius: 20px;
border: 1px solid rgba(0,0,0,0.1);
background: var(--bg-card);
cursor: pointer;
transition: all 0.2s;
font-size: 0.85rem;
}
.filter-pill:hover {
background: var(--accent-light);
border-color: var(--accent);
}
.filter-pill.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.stats-bar {
2026-04-15 09:34:26 +02:00
display: inline-flex;
gap: 0.4rem;
2026-04-21 01:34:40 +02:00
margin-bottom: 0;
2026-04-15 09:34:26 +02:00
padding: 0.25rem;
2026-02-01 11:58:44 +01:00
background: var(--bg-card);
2026-04-15 09:34:26 +02:00
border-radius: 999px;
border: 1px solid rgba(0,0,0,0.08);
2026-04-21 01:34:40 +02:00
flex-shrink: 0;
2026-02-01 11:58:44 +01:00
}
.stat-item {
text-align: center;
2026-04-15 09:34:26 +02:00
padding: 0.2rem 0.5rem;
border-radius: 999px;
background: rgba(0,0,0,0.03);
line-height: 1.15;
2026-02-01 11:58:44 +01:00
}
.stat-value {
2026-04-15 09:34:26 +02:00
font-size: 0.92rem;
2026-02-01 11:58:44 +01:00
font-weight: 700;
color: var(--accent);
}
.stat-label {
2026-04-15 09:34:26 +02:00
font-size: 0.62rem;
2026-02-01 11:58:44 +01:00
color: var(--text-secondary);
text-transform: uppercase;
2026-04-15 09:34:26 +02:00
letter-spacing: 0.35px;
}
2026-06-18 22:20:39 +02:00
.type-filter-wrap {
min-width: 200px;
max-width: 280px;
}
.type-filter-header {
display: none;
}
.type-filter-dropdown .dropdown-toggle {
width: 100%;
text-align: left;
border-radius: 0.375rem;
border: 1px solid #ced4da;
background: #fff;
color: var(--text-primary);
font-weight: 400;
font-size: 0.86rem;
padding: 0.28rem 2rem 0.28rem 0.65rem;
min-height: calc(1.4em + 0.56rem + 2px);
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
box-shadow: none;
}
.type-filter-dropdown .dropdown-toggle:hover,
.type-filter-dropdown .dropdown-toggle:focus,
.type-filter-dropdown .dropdown-toggle:active,
.type-filter-dropdown .dropdown-toggle.show {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
color: var(--text-primary);
background: #fff;
}
.type-filter-dropdown .dropdown-toggle::after {
margin-left: 0.5rem;
}
.type-filter-dropdown .dropdown-menu {
width: min(340px, 92vw);
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.15);
padding: 0.45rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.type-filter-checkbox-list {
max-height: 12.5rem;
overflow-y: auto;
padding-right: 0.2rem;
}
.type-filter-checkbox-item {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.34rem 0.42rem;
border-radius: 10px;
cursor: pointer;
margin-bottom: 0.2rem;
transition: background-color 0.15s ease;
}
.type-filter-checkbox-item:hover {
background: rgba(15, 76, 117, 0.08);
}
.type-filter-checkbox-item input {
margin-top: 0;
cursor: pointer;
}
.type-filter-checkbox-label {
font-size: 0.8rem;
font-weight: 600;
color: #16384f;
user-select: none;
}
.type-filter-menu-actions {
border-top: 1px solid rgba(0, 0, 0, 0.08);
margin-top: 0.35rem;
padding-top: 0.38rem;
display: flex;
justify-content: flex-end;
gap: 0.3rem;
}
.type-filter-menu-actions .btn {
font-size: 0.72rem;
line-height: 1.2;
padding: 0.2rem 0.45rem;
}
.type-filter-footer {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.6rem;
margin-top: 0.25rem;
}
.type-filter-selection {
font-size: 0.73rem;
color: var(--text-secondary);
min-height: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.type-filter-empty {
font-size: 0.8rem;
color: var(--text-secondary);
padding: 0.22rem 0.1rem;
}
.mini-filter-wrap {
min-width: 190px;
max-width: 260px;
}
.mini-filter-dropdown .dropdown-toggle {
width: 100%;
text-align: left;
border-radius: 0.375rem;
border: 1px solid #ced4da;
background: #fff;
color: var(--text-primary);
font-weight: 400;
font-size: 0.86rem;
padding: 0.28rem 2rem 0.28rem 0.65rem;
min-height: calc(1.4em + 0.56rem + 2px);
2026-04-15 09:34:26 +02:00
display: flex;
align-items: center;
2026-06-18 22:20:39 +02:00
justify-content: space-between;
2026-04-15 09:34:26 +02:00
gap: 0.5rem;
2026-06-18 22:20:39 +02:00
box-shadow: none;
}
.mini-filter-dropdown .dropdown-toggle:hover,
.mini-filter-dropdown .dropdown-toggle:focus,
.mini-filter-dropdown .dropdown-toggle:active,
.mini-filter-dropdown .dropdown-toggle.show {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
color: var(--text-primary);
background: #fff;
}
.mini-filter-dropdown .dropdown-menu {
width: min(320px, 92vw);
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.15);
padding: 0.45rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.mini-filter-list {
max-height: 12.5rem;
overflow-y: auto;
padding-right: 0.2rem;
}
.mini-filter-item {
display: flex;
align-items: center;
gap: 0.52rem;
padding: 0.34rem 0.42rem;
border-radius: 10px;
cursor: pointer;
margin-bottom: 0.15rem;
transition: background-color 0.15s ease;
}
.mini-filter-item:hover {
background: rgba(15, 76, 117, 0.08);
}
.mini-filter-item input {
margin-top: 0;
cursor: pointer;
}
.mini-filter-label {
font-size: 0.8rem;
font-weight: 600;
color: #16384f;
user-select: none;
}
.mini-filter-footer {
border-top: 1px solid rgba(0, 0, 0, 0.08);
margin-top: 0.35rem;
padding-top: 0.35rem;
display: flex;
justify-content: flex-end;
}
.mini-filter-clear {
font-size: 0.72rem;
line-height: 1.2;
padding: 0.2rem 0.45rem;
}
.owner-cell {
display: inline-flex;
align-items: center;
justify-content: center;
2026-04-15 09:34:26 +02:00
}
.owner-avatar {
2026-06-18 22:20:39 +02:00
width: 1.75rem;
height: 1.75rem;
2026-04-15 09:34:26 +02:00
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
2026-06-18 22:20:39 +02:00
font-size: 0.68rem;
2026-04-15 09:34:26 +02:00
font-weight: 700;
letter-spacing: 0.02em;
color: #fff;
background: var(--accent);
flex-shrink: 0;
}
2026-06-18 22:20:39 +02:00
.owner-avatar.group-avatar {
background: #7c3aed;
}
.owner-avatar.empty-avatar {
background: #cbd5e1;
color: #475569;
}
2026-04-15 09:34:26 +02:00
.owner-name {
color: var(--text-secondary);
font-size: 0.85rem;
word-break: break-word;
2026-02-01 11:58:44 +01:00
}
2026-04-21 01:34:40 +02:00
@media (max-width: 1100px) {
.top-controls-row {
flex-wrap: wrap;
}
.top-controls-actions {
margin-left: 0;
}
}
2026-02-01 11:58:44 +01:00
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.3;
}
2026-04-01 21:34:58 +02:00
.sag-top-alerts {
margin-bottom: 1rem;
}
.sag-top-alerts .alert {
border-left: 6px solid;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.sag-top-alerts .alert-warning {
border-left-color: #f59f00;
}
.sag-top-alerts .alert-danger {
border-left-color: #e03131;
}
2026-02-01 11:58:44 +01:00
< / style >
{% endblock %}
2026-02-01 00:38:10 +01:00
2026-02-01 11:58:44 +01:00
{% block content %}
2026-06-18 22:20:39 +02:00
{% macro initials_bubble(label, group=false) -%}
{% if label %}
{% set norm = label.strip() %}
{% set parts = norm.split() %}
{% set initials = ((parts[0][0] if parts|length > 0 else norm[0]) ~ (parts[1][0] if parts|length > 1 else ''))|upper %}
< span class = "owner-avatar {% if group %}group-avatar{% endif %}" title = "{{ norm }}" > {{ initials }}< / span >
{% else %}
< span class = "owner-avatar empty-avatar" title = "Ikke sat" > -< / span >
{% endif %}
{%- endmacro %}
2026-04-15 09:34:26 +02:00
< div class = "container-fluid" style = "max-width: none; padding-top: 0.65rem;" >
2026-04-01 21:34:58 +02:00
< div id = "sagTopAlerts" class = "sag-top-alerts d-none" > < / div >
2026-08-30 14:34:43 +02:00
< div class = "sag-toolbar-card" >
< div class = "top-controls-row mb-0" >
2026-04-21 01:34:40 +02:00
< h1 style = "margin: 0; color: var(--accent); flex-shrink: 0;" >
2026-04-15 09:34:26 +02:00
< i class = "bi bi-list-check" > < / i >
2026-02-01 11:58:44 +01:00
< / h1 >
2026-04-21 01:34:40 +02:00
< div class = "stats-bar" >
< div class = "stat-item" >
< div class = "stat-value" > {{ sager|length }}< / div >
< div class = "stat-label" > Total< / div >
< / div >
< div class = "stat-item" >
< div class = "stat-value" > {{ sager|selectattr('status', 'equalto', 'åben')|list|length }}< / div >
< div class = "stat-label" > Åbne< / div >
< / div >
< / div >
< div class = "search-bar" >
2026-08-30 14:34:43 +02:00
< i class = "bi bi-search search-icon" > < / i >
2026-04-21 01:34:40 +02:00
< input type = "text"
class="form-control"
id="searchInput"
2026-08-30 14:34:43 +02:00
placeholder="Søg på sagsnr., firma, kontakt, titel eller ansvarlig..."
2026-04-21 01:34:40 +02:00
autocomplete="off">
2026-08-30 14:34:43 +02:00
< span class = "search-shortcut" > /< / span >
< button class = "search-clear-btn d-none" id = "clearSearchBtn" type = "button" title = "Ryd søgning" aria-label = "Ryd søgning" > < i class = "bi bi-x-lg" > < / i > < / button >
2026-04-21 01:34:40 +02:00
< / div >
< div class = "top-controls-actions" >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< button class = "btn btn-outline-primary" onclick = "window.location.href='/sag/varekob-salg'" >
< i class = "bi bi-basket3 me-2" > < / i > Varekøb & Salg
< / button >
< button class = "btn btn-primary" style = "background: var(--accent); border: none;" onclick = "window.location.href='/sag/new'" >
< i class = "bi bi-plus-lg me-2" > < / i > Ny Sag
< / button >
< / div >
2026-02-01 11:58:44 +01:00
< / div >
2026-08-30 14:34:43 +02:00
< div class = "sag-quick-filters" aria-label = "Hurtigfiltre" >
< button class = "sag-quick-filter {{ 'active' if current_quick_filter == 'all' else '' }}" type = "button" data-quick-filter = "all" > < i class = "bi bi-grid" > < / i > Alle aktive< / button >
< button class = "sag-quick-filter {{ 'active' if current_quick_filter == 'mine-open' else '' }}" type = "button" data-quick-filter = "mine-open" > < i class = "bi bi-person-check" > < / i > Mine åbne sager< / button >
< button class = "sag-quick-filter {{ 'active' if current_quick_filter == 'overdue' else '' }}" type = "button" data-quick-filter = "overdue" > < i class = "bi bi-alarm" > < / i > Overskredet deadline< / button >
< button class = "sag-quick-filter {{ 'active' if current_quick_filter == 'my-groups' else '' }}" type = "button" data-quick-filter = "my-groups" > < i class = "bi bi-people" > < / i > Mine grupper< / button >
< button class = "sag-quick-filter {{ 'active' if current_quick_filter == 'unassigned' else '' }}" type = "button" data-quick-filter = "unassigned" > < i class = "bi bi-person-dash" > < / i > Ikke tildelt< / button >
< button class = "sag-quick-filter {{ 'active' if current_quick_filter == 'closed' else '' }}" type = "button" data-quick-filter = "closed" > < i class = "bi bi-check2-circle" > < / i > Lukkede< / button >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
2026-08-30 14:34:43 +02:00
< / div >
< div class = "sag-advanced-filters" >
2026-06-18 22:20:39 +02:00
< div class = "type-filter-wrap" >
< div class = "dropdown type-filter-dropdown mb-1" >
< button class = "btn dropdown-toggle" type = "button" id = "typeFilterDropdownBtn" data-bs-toggle = "dropdown" data-bs-auto-close = "outside" aria-expanded = "false" >
< span id = "typeFilterDropdownLabel" > Vælg typer< / span >
< / button >
< div class = "dropdown-menu" aria-labelledby = "typeFilterDropdownBtn" >
< div class = "type-filter-checkbox-list" id = "typeFilterCheckboxList" > < / div >
< div class = "type-filter-menu-actions" >
< button class = "btn btn-sm btn-outline-secondary" type = "button" id = "clearTypeFilterBtn" > Ryd< / button >
< button class = "btn btn-sm btn-primary" type = "button" id = "saveTypeFilterDefaultBtn" > Gem< / button >
< / div >
< / div >
< / div >
< select class = "d-none" id = "typeFilter" multiple title = "Type filter data source" > < / select >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / div >
2026-06-18 22:20:39 +02:00
< div class = "mini-filter-wrap" >
< div class = "dropdown mini-filter-dropdown mb-1" >
< button class = "btn dropdown-toggle" type = "button" id = "assigneeDropdownBtn" data-bs-toggle = "dropdown" data-bs-auto-close = "outside" aria-expanded = "false" >
< span id = "assigneeDropdownLabel" > Ansvarlig< / span >
< / button >
< div class = "dropdown-menu" aria-labelledby = "assigneeDropdownBtn" >
< div class = "mini-filter-list" id = "assigneeFilterList" > < / div >
< div class = "mini-filter-footer" >
< button class = "btn btn-sm btn-outline-secondary mini-filter-clear" type = "button" id = "clearAssigneeFilterBtn" > Ryd< / button >
< / div >
< / div >
< / div >
< select class = "d-none" name = "ansvarlig_bruger_id" id = "assigneeFilter" multiple >
2026-02-17 08:29:05 +01:00
< option value = "" > Alle medarbejdere< / option >
2026-04-24 11:28:12 +02:00
< option value = "__UNASSIGNED__" { % if current_unassigned % } selected { % endif % } > Uden ansvarlig< / option >
2026-02-17 08:29:05 +01:00
{% for user in assignment_users or [] %}
< option value = "{{ user.user_id }}" { % if current_ansvarlig_bruger_id = = user . user_id % } selected { % endif % } > {{ user.display_name }}< / option >
{% endfor %}
2026-06-18 22:20:39 +02:00
< / select >
< / div >
< div class = "mini-filter-wrap" >
< div class = "dropdown mini-filter-dropdown mb-1" >
< button class = "btn dropdown-toggle" type = "button" id = "groupDropdownBtn" data-bs-toggle = "dropdown" data-bs-auto-close = "outside" aria-expanded = "false" >
< span id = "groupDropdownLabel" > Grupper< / span >
< / button >
< div class = "dropdown-menu" aria-labelledby = "groupDropdownBtn" >
< div class = "mini-filter-list" id = "groupFilterList" > < / div >
< div class = "mini-filter-footer" >
< button class = "btn btn-sm btn-outline-secondary mini-filter-clear" type = "button" id = "clearGroupFilterBtn" > Ryd< / button >
< / div >
< / div >
2026-02-17 08:29:05 +01:00
< / div >
2026-06-18 22:20:39 +02:00
< select class = "d-none" name = "assigned_group_id" id = "groupFilter" multiple >
2026-02-17 08:29:05 +01:00
< option value = "" > Alle grupper< / option >
{% for group in assignment_groups or [] %}
< option value = "{{ group.id }}" { % if current_assigned_group_id = = group . id % } selected { % endif % } > {{ group.name }}< / option >
{% endfor %}
2026-06-18 22:20:39 +02:00
< / select >
< / div >
2026-02-06 10:47:14 +01:00
< a class = "btn btn-sm btn-outline-secondary" href = "{{ toggle_include_deferred_url }}" >
{% if include_deferred %}Skjul udsatte{% else %}Vis udsatte{% endif %}
< / a >
2026-08-30 14:34:43 +02:00
< div class = "dropdown ms-auto" >
< button class = "btn btn-sm btn-outline-secondary dropdown-toggle" type = "button" data-bs-toggle = "dropdown" data-bs-auto-close = "outside" >
< i class = "bi bi-layout-three-columns me-1" > < / i > Kolonner
< / button >
< div class = "dropdown-menu dropdown-menu-end p-2 shadow" >
< div class = "small text-muted px-2 pb-2" > Træk for at ændre rækkefølge< / div >
< div id = "sagColumnList" class = "sag-column-list" > < / div >
< div class = "d-flex gap-2 border-top pt-2 mt-2" >
< button id = "resetSagColumnsBtn" class = "btn btn-sm btn-outline-secondary" type = "button" > Nulstil< / button >
< button id = "saveSagColumnsBtn" class = "btn btn-sm btn-primary ms-auto" type = "button" > Gem for mig< / button >
< / div >
< / div >
< / div >
2026-02-01 11:58:44 +01:00
< / div >
<!-- Table -->
< div class = "table-wrapper" >
{% if sager %}
< table class = "sag-table" >
< thead >
< tr >
2026-06-18 22:20:39 +02:00
< th class = "col-expand" > < / th >
2026-03-20 00:24:58 +01:00
< th style = "width: 90px;" > SagsID< / th >
< th style = "width: 180px;" > Virksom.< / th >
< th style = "width: 150px;" > Kontakt< / th >
< th style = "width: 300px;" > Beskr.< / th >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< th style = "width: 120px;" > Type< / th >
2026-03-20 00:24:58 +01:00
< th style = "width: 110px;" > Prioritet< / th >
2026-06-18 22:20:39 +02:00
< th style = "width: 120px;" > Status< / th >
2026-03-20 00:24:58 +01:00
< th style = "width: 160px;" > Ansvarl.< / th >
< th style = "width: 170px;" > Gruppe/Level< / th >
2026-03-20 18:43:45 +01:00
< th style = "width: 240px;" > Næste todo< / th >
2026-03-20 00:24:58 +01:00
< th style = "width: 120px;" > Opret.< / th >
2026-04-04 02:46:37 +02:00
< th style = "width: 120px;" > Arbejdsstart< / th >
< th style = "width: 140px;" > Start senest< / th >
2026-03-20 00:24:58 +01:00
< th style = "width: 120px;" > Deadline< / th >
2026-02-01 11:58:44 +01:00
< / tr >
< / thead >
< tbody id = "sagTableBody" >
{% for sag in sager %}
{% if sag.id not in child_ids %}
{% set has_relations = sag.id in relations_map and relations_map[sag.id]|length > 0 %}
< tr class = "tree-row {% if has_relations %}has-children{% endif %}"
data-sag-id="{{ sag.id }}"
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
data-status="{{ sag.status }}"
2026-06-18 22:20:39 +02:00
data-type="{{ sag.template_key or sag.type or 'ticket' }}"
data-assignee-id="{{ sag.ansvarlig_bruger_id if sag.ansvarlig_bruger_id else '' }}"
2026-08-30 14:34:43 +02:00
data-group-id="{{ sag.assigned_group_id if sag.assigned_group_id else '' }}"
data-deadline="{{ sag.deadline.isoformat() if sag.deadline else '' }}">
2026-06-18 22:20:39 +02:00
< td class = "col-expand" onclick = "event.stopPropagation();" >
2026-02-01 11:58:44 +01:00
{% if has_relations %}
< span class = "tree-toggle" onclick = "toggleTreeNode(event, {{ sag.id }})" > +< / span >
{% endif %}
2026-06-18 22:20:39 +02:00
< / td >
< td >
2026-08-30 14:34:43 +02:00
< a class = "sag-id" href = "/sag/{{ sag.id }}/v3" > < i class = "bi bi-folder2-open" > < / i > #{{ sag.id }}< / a >
2026-04-01 21:34:58 +02:00
{% if (sag.unread_email_count or 0) > 0 %}
{% set unread_level = sag.unread_email_level or 'fresh' %}
< span class = "sag-unread-badge sag-unread-{{ unread_level }}" title = "{{ sag.unread_email_count }} ulæste e-mails" >
{{ sag.unread_email_count if sag.unread_email_count < = 99 else '99+' }}
< / span >
{% endif %}
2026-02-01 11:58:44 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-company" onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem;" >
2026-08-30 14:34:43 +02:00
{% if sag.customer_id and sag.customer_name %}< a class = "sag-entity-link" href = "/customers/{{ sag.customer_id }}" onclick = "event.stopPropagation()" title = "Åbn {{ sag.customer_name }}" > < i class = "bi bi-building" > < / i > < span > {{ sag.customer_name }}< / span > < / a > {% else %}-{% endif %}
2026-03-20 00:24:58 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-contact" onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem;" >
2026-08-30 14:34:43 +02:00
{% if sag.kontakt_id and sag.kontakt_navn and sag.kontakt_navn.strip() %}< a class = "sag-entity-link" href = "/contacts/{{ sag.kontakt_id }}" onclick = "event.stopPropagation()" title = "Åbn {{ sag.kontakt_navn }}" > < i class = "bi bi-person" > < / i > < span > {{ sag.kontakt_navn }}< / span > < / a > {% else %}-{% endif %}
2026-03-20 00:24:58 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-desc" onclick = "window.location.href='/sag/{{ sag.id }}/v3'" >
2026-04-21 01:34:40 +02:00
< div class = "sag-titel" { % if sag . beskrivelse % } title = "{{ sag.beskrivelse }}" { % endif % } > {{ sag.titel }}< / div >
2026-02-01 11:58:44 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ sag.id }}/v3'" >
2026-02-17 08:29:05 +01:00
< span class = "badge bg-light text-dark border" > {{ sag.template_key or sag.type or 'ticket' }}< / span >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem; text-transform: capitalize;" >
2026-03-20 00:24:58 +01:00
{{ sag.priority if sag.priority else 'normal' }}
2026-02-01 11:58:44 +01:00
< / td >
2026-06-18 22:20:39 +02:00
< td onclick = "window.location.href='/sag/{{ sag.id }}/v3'" >
{% set status_raw = sag.status if sag.status else 'åben' %}
2026-08-30 14:34:43 +02:00
< select class = "form-select form-select-sm sag-inline-select sag-status-select" style = "min-width:120px" data-previous = "{{ status_raw }}" onclick = "event.stopPropagation()" onchange = "updateCaseListField({{ sag.id }}, 'status', this.value, this)" >
{% for status_option in status_options %}< option value = "{{ status_option }}" { % if status_option = = status_raw % } selected { % endif % } > {{ status_option }}< / option > {% endfor %}
< / select >
2026-06-18 22:20:39 +02:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-owner" onclick = "window.location.href='/sag/{{ sag.id }}/v3'" >
2026-08-30 14:34:43 +02:00
< select class = "form-select form-select-sm sag-inline-select sag-owner-select" style = "min-width:150px" data-previous = "{{ sag.ansvarlig_bruger_id or '' }}" onclick = "event.stopPropagation()" onchange = "updateCaseListField({{ sag.id }}, 'ansvarlig_bruger_id', this.value || null, this)" >
< option value = "" > Ikke tildelt< / option >
{% for user in assignment_users or [] %}< option value = "{{ user.user_id }}" { % if sag . ansvarlig_bruger_id = = user . user_id % } selected { % endif % } > {{ user.display_name }}< / option > {% endfor %}
< / select >
2026-02-17 08:29:05 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-group" onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem;" >
2026-06-18 22:20:39 +02:00
< div class = "owner-cell" >
{{ initials_bubble(sag.assigned_group_name, true) }}
< / div >
2026-02-17 08:29:05 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem; white-space: normal; max-width: 240px;" >
2026-03-20 18:43:45 +01:00
{% if sag.next_todo_title %}
< div > {{ sag.next_todo_title }}< / div >
{% if sag.next_todo_due_date %}
< div class = "small text-muted" > Forfald: {{ sag.next_todo_due_date.strftime('%d/%m-%Y') }}< / div >
{% endif %}
{% else %}
-
{% endif %}
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary);" >
2026-03-20 00:24:58 +01:00
{{ sag.created_at.strftime('%d/%m-%Y') if sag.created_at else '-' }}
2026-02-01 11:58:44 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary);" >
2026-03-20 00:24:58 +01:00
{{ sag.start_date.strftime('%d/%m-%Y') if sag.start_date else '-' }}
2026-02-06 10:47:14 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary);" >
2026-03-20 00:24:58 +01:00
{{ sag.deferred_until.strftime('%d/%m-%Y') if sag.deferred_until else '-' }}
2026-02-01 11:58:44 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ sag.id }}/v3'" style = "color: var(--text-secondary);" >
2026-03-20 00:24:58 +01:00
{{ sag.deadline.strftime('%d/%m-%Y') if sag.deadline else '-' }}
2026-02-01 11:58:44 +01:00
< / td >
< / tr >
{% if has_relations %}
{% set seen_targets = [] %}
{% for rel in relations_map[sag.id] %}
{% set related_sag = sager|selectattr('id', 'equalto', rel.target_id)|first %}
{% if related_sag and rel.target_id not in seen_targets %}
{% set _ = seen_targets.append(rel.target_id) %}
{% set all_rel_types = relations_map[sag.id]|selectattr('target_id', 'equalto', rel.target_id)|map(attribute='type')|list %}
2026-08-30 14:34:43 +02:00
< tr class = "tree-child" data-parent = "{{ sag.id }}" data-status = "{{ related_sag.status }}" data-type = "{{ related_sag.template_key or related_sag.type or 'ticket' }}" data-assignee-id = "{{ related_sag.ansvarlig_bruger_id if related_sag.ansvarlig_bruger_id else '' }}" data-group-id = "{{ related_sag.assigned_group_id if related_sag.assigned_group_id else '' }}" data-deadline = "{{ related_sag.deadline.isoformat() if related_sag.deadline else '' }}" style = "display: none;" >
2026-06-18 22:20:39 +02:00
< td class = "col-expand" > < span class = "child-branch" > └< / span > < / td >
2026-02-01 11:58:44 +01:00
< td >
2026-08-30 14:34:43 +02:00
< a class = "sag-id" href = "/sag/{{ related_sag.id }}/v3" > < i class = "bi bi-folder2-open" > < / i > #{{ related_sag.id }}< / a >
2026-04-01 21:34:58 +02:00
{% if (related_sag.unread_email_count or 0) > 0 %}
{% set child_unread_level = related_sag.unread_email_level or 'fresh' %}
< span class = "sag-unread-badge sag-unread-{{ child_unread_level }}" title = "{{ related_sag.unread_email_count }} ulæste e-mails" >
{{ related_sag.unread_email_count if related_sag.unread_email_count < = 99 else '99+' }}
< / span >
{% endif %}
2026-02-01 11:58:44 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-company" onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem;" >
2026-08-30 14:34:43 +02:00
{% if related_sag.customer_id and related_sag.customer_name %}< a class = "sag-entity-link" href = "/customers/{{ related_sag.customer_id }}" onclick = "event.stopPropagation()" title = "Åbn {{ related_sag.customer_name }}" > < i class = "bi bi-building" > < / i > < span > {{ related_sag.customer_name }}< / span > < / a > {% else %}-{% endif %}
2026-03-20 00:24:58 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-contact" onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem;" >
2026-08-30 14:34:43 +02:00
{% if related_sag.kontakt_id and related_sag.kontakt_navn and related_sag.kontakt_navn.strip() %}< a class = "sag-entity-link" href = "/contacts/{{ related_sag.kontakt_id }}" onclick = "event.stopPropagation()" title = "Åbn {{ related_sag.kontakt_navn }}" > < i class = "bi bi-person" > < / i > < span > {{ related_sag.kontakt_navn }}< / span > < / a > {% else %}-{% endif %}
2026-03-20 00:24:58 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-desc" onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" >
2026-02-01 11:58:44 +01:00
{% for rt in all_rel_types %}
< span class = "relation-badge" > {{ rt }}< / span >
{% endfor %}
2026-04-21 01:34:40 +02:00
< div class = "sag-titel" style = "display: inline;" { % if related_sag . beskrivelse % } title = "{{ related_sag.beskrivelse }}" { % endif % } > {{ related_sag.titel }}< / div >
2026-02-01 11:58:44 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" >
2026-02-17 08:29:05 +01:00
< span class = "badge bg-light text-dark border" > {{ related_sag.template_key or related_sag.type or 'ticket' }}< / span >
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem; text-transform: capitalize;" >
2026-03-20 00:24:58 +01:00
{{ related_sag.priority if related_sag.priority else 'normal' }}
2026-02-01 11:58:44 +01:00
< / td >
2026-06-18 22:20:39 +02:00
< td onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" >
{% set related_status_raw = related_sag.status if related_sag.status else 'åben' %}
2026-08-30 14:34:43 +02:00
< select class = "form-select form-select-sm sag-inline-select sag-status-select" style = "min-width:120px" data-previous = "{{ related_status_raw }}" onclick = "event.stopPropagation()" onchange = "updateCaseListField({{ related_sag.id }}, 'status', this.value, this)" >
{% for status_option in status_options %}< option value = "{{ status_option }}" { % if status_option = = related_status_raw % } selected { % endif % } > {{ status_option }}< / option > {% endfor %}
< / select >
2026-06-18 22:20:39 +02:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-owner" onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" >
2026-08-30 14:34:43 +02:00
< select class = "form-select form-select-sm sag-inline-select sag-owner-select" style = "min-width:150px" data-previous = "{{ related_sag.ansvarlig_bruger_id or '' }}" onclick = "event.stopPropagation()" onchange = "updateCaseListField({{ related_sag.id }}, 'ansvarlig_bruger_id', this.value || null, this)" >
< option value = "" > Ikke tildelt< / option >
{% for user in assignment_users or [] %}< option value = "{{ user.user_id }}" { % if related_sag . ansvarlig_bruger_id = = user . user_id % } selected { % endif % } > {{ user.display_name }}< / option > {% endfor %}
< / select >
2026-02-17 08:29:05 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td class = "col-group" onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem;" >
2026-06-18 22:20:39 +02:00
< div class = "owner-cell" >
{{ initials_bubble(related_sag.assigned_group_name, true) }}
< / div >
2026-02-17 08:29:05 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary); font-size: 0.85rem; white-space: normal; max-width: 240px;" >
2026-03-20 18:43:45 +01:00
{% if related_sag.next_todo_title %}
< div > {{ related_sag.next_todo_title }}< / div >
{% if related_sag.next_todo_due_date %}
< div class = "small text-muted" > Forfald: {{ related_sag.next_todo_due_date.strftime('%d/%m-%Y') }}< / div >
{% endif %}
{% else %}
-
{% endif %}
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary);" >
2026-03-20 00:24:58 +01:00
{{ related_sag.created_at.strftime('%d/%m-%Y') if related_sag.created_at else '-' }}
2026-02-01 11:58:44 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary);" >
2026-03-20 00:24:58 +01:00
{{ related_sag.start_date.strftime('%d/%m-%Y') if related_sag.start_date else '-' }}
2026-02-06 10:47:14 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary);" >
2026-03-20 00:24:58 +01:00
{{ related_sag.deferred_until.strftime('%d/%m-%Y') if related_sag.deferred_until else '-' }}
2026-02-01 11:58:44 +01:00
< / td >
feat: Update sag links to include versioning in URLs across multiple templates and services
- Updated links in index_old.html, varekob_salg.html, log.html, opportunities.html, detail.html, and various frontend files to point to the new versioned sag URLs.
- Modified reminder_notification_service.py to reflect the new sag URL structure in notifications.
- Added FedEx shipment management functionality, including API client, service layer, and router for handling FedEx bookings, tracking, and cancellations.
- Created database migration for FedEx shipments, including tables for shipments, packages, and tracking events.
2026-04-30 23:06:00 +02:00
< td onclick = "window.location.href='/sag/{{ related_sag.id }}/v3'" style = "color: var(--text-secondary);" >
2026-03-20 00:24:58 +01:00
{{ related_sag.deadline.strftime('%d/%m-%Y') if related_sag.deadline else '-' }}
2026-02-01 11:58:44 +01:00
< / td >
< / tr >
{% endif %}
2026-02-01 00:38:10 +01:00
{% endfor %}
{% endif %}
2026-02-01 11:58:44 +01:00
{% endif %}
{% endfor %}
< / tbody >
< / table >
{% else %}
< div class = "empty-state" >
< i class = "bi bi-inbox" > < / i >
< p > Ingen sager fundet< / p >
2026-02-01 00:38:10 +01:00
< / div >
2026-02-01 11:58:44 +01:00
{% endif %}
2026-01-29 23:07:33 +01:00
< / div >
2026-02-01 11:58:44 +01:00
< / div >
2026-08-30 14:34:43 +02:00
< div class = "modal fade" id = "closeCaseWithoutTimeModal" tabindex = "-1" aria-labelledby = "closeCaseWithoutTimeTitle" aria-hidden = "true" data-bs-backdrop = "static" data-bs-keyboard = "false" >
< div class = "modal-dialog modal-dialog-centered modal-lg" >
< div class = "modal-content border-0 shadow-lg" >
< div class = "modal-header bg-danger text-white border-0 py-4" >
< div class = "d-flex align-items-center gap-3" >
< i class = "bi bi-exclamation-triangle-fill" style = "font-size:3rem;line-height:1" > < / i >
< div >
< div class = "text-uppercase fw-bold small opacity-75 mb-1" > Vigtig advarsel< / div >
< h2 class = "modal-title fw-bold mb-0" id = "closeCaseWithoutTimeTitle" > Der er ikke registreret tid på sagen< / h2 >
< / div >
< / div >
< / div >
< div class = "modal-body p-4 p-md-5 text-center" >
< p class = "fs-4 fw-semibold mb-3" > Du er ved at lukke sag < span id = "closeWithoutTimeCaseNumber" > < / span > uden tidsregistrering.< / p >
< p class = "fs-5 text-muted mb-0" > Kontrollér, at det er korrekt. Sagen bliver kun lukket, hvis du aktivt bekræfter nedenfor.< / p >
< / div >
< div class = "modal-footer border-0 bg-light p-4 justify-content-center gap-2" >
< button type = "button" class = "btn btn-lg btn-outline-secondary px-4" data-bs-dismiss = "modal" >
< i class = "bi bi-arrow-left me-2" > < / i > Gå tilbage
< / button >
< button type = "button" class = "btn btn-lg btn-danger px-4" id = "confirmCloseCaseWithoutTimeBtn" >
< i class = "bi bi-check-circle-fill me-2" > < / i > Ja, luk sagen uden tid
< / button >
< / div >
< / div >
< / div >
< / div >
2026-02-01 11:58:44 +01:00
< script >
2026-04-01 21:34:58 +02:00
const topAlertCustomerId = {{ current_customer_id if current_customer_id else 'null' }};
2026-08-30 14:34:43 +02:00
function applyStatusSelectTone(control) {
if (!control) return;
const status = String(control.value || '').trim().toLowerCase();
control.classList.remove('status-tone-open', 'status-tone-progress', 'status-tone-waiting', 'status-tone-done');
let tone = 'status-tone-open';
if (['under behandling', 'i gang', 'in progress'].includes(status)) tone = 'status-tone-progress';
else if (['afventer', 'on hold'].includes(status)) tone = 'status-tone-waiting';
else if (['løst', 'lukket', 'afsluttet', 'resolved', 'closed', 'done'].includes(status)) tone = 'status-tone-done';
control.classList.add(tone);
}
document.querySelectorAll('.sag-status-select').forEach(applyStatusSelectTone);
function confirmCloseCaseWithoutTime(caseId, message) {
return new Promise(resolve => {
const modalElement = document.getElementById('closeCaseWithoutTimeModal');
const confirmButton = document.getElementById('confirmCloseCaseWithoutTimeBtn');
const caseNumber = document.getElementById('closeWithoutTimeCaseNumber');
if (!modalElement || !confirmButton || typeof bootstrap === 'undefined') {
resolve(window.confirm(`${message}\n\nVil du lukke sagen uden tidsregistrering?`));
return;
}
if (caseNumber) caseNumber.textContent = `#${caseId}`;
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
let confirmed = false;
const handleConfirm = () => {
confirmed = true;
modal.hide();
};
const handleHidden = () => {
confirmButton.removeEventListener('click', handleConfirm);
modalElement.removeEventListener('hidden.bs.modal', handleHidden);
resolve(confirmed);
};
confirmButton.addEventListener('click', handleConfirm, { once: true });
modalElement.addEventListener('hidden.bs.modal', handleHidden, { once: true });
modal.show();
});
}
async function updateCaseListField(caseId, field, value, control) {
const previous = control?.dataset?.previous ?? '';
if (control) control.disabled = true;
try {
const saveField = async (confirmedWithoutTime = false) => {
const body = { [field]: value === '' ? null : value };
if (confirmedWithoutTime) body.confirm_close_without_time = true;
const response = await fetch(`/api/v1/sag/${caseId}`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const payload = await response.json().catch(() => ({}));
return { response, payload };
};
let { response, payload } = await saveField();
const detail = payload?.detail;
if (response.status === 409 & & detail?.code === 'close_without_time_confirmation_required') {
const confirmed = await confirmCloseCaseWithoutTime(caseId, detail.message);
if (!confirmed) {
if (control) control.value = previous;
if (control & & field === 'status') applyStatusSelectTone(control);
return;
}
({ response, payload } = await saveField(true));
}
if (!response.ok) {
const message = typeof payload.detail === 'string' ? payload.detail : payload.detail?.message;
throw new Error(message || 'Ændringen kunne ikke gemmes');
}
if (control) control.dataset.previous = String(value ?? '');
if (control & & field === 'status') applyStatusSelectTone(control);
const row = control?.closest('tr');
if (row & & field === 'status') row.dataset.status = String(value || '');
if (row & & field === 'status') updateOverdueDeadlineMarker(row);
if (row & & field === 'ansvarlig_bruger_id') row.dataset.assigneeId = String(value || '');
if (typeof applyFilters === 'function') applyFilters();
} catch (error) {
if (control) control.value = previous;
if (control & & field === 'status') applyStatusSelectTone(control);
if (typeof showNotification === 'function') showNotification(error.message, 'error');
else window.alert(error.message);
} finally {
if (control) control.disabled = false;
}
}
2026-04-01 21:34:58 +02:00
function escapeTopAlertHtml(value) {
return String(value ?? '')
.replace(/& /g, '& ')
.replace(/< /g, '< ')
.replace(/>/g, '> ')
.replace(/"/g, '" ')
.replace(/'/g, '' ');
}
async function loadSagTopAlertsForCustomer(customerId) {
const container = document.getElementById('sagTopAlerts');
if (!container || !customerId) {
return;
}
container.classList.remove('d-none');
container.innerHTML = '< div class = "alert alert-info mb-0" > < span class = "spinner-border spinner-border-sm me-2" > < / span > Henter kunde-alerts...< / div > ';
try {
const response = await fetch(`/api/v1/alert-notes/check?entity_type=customer& entity_id=${customerId}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const alerts = (data?.alerts || []).filter((alert) => ['critical', 'warning'].includes(String(alert?.severity || '').toLowerCase()));
if (!alerts.length) {
container.classList.add('d-none');
container.innerHTML = '';
return;
}
const html = alerts.map((alert) => {
const isCritical = String(alert.severity || '').toLowerCase() === 'critical';
const klass = isCritical ? 'alert-danger' : 'alert-warning';
const label = isCritical ? 'KRITISK' : 'ADVARSEL';
const title = escapeTopAlertHtml(alert.title || 'Vigtig kundeinformation');
const message = escapeTopAlertHtml(alert.message || '');
return `
< div class = "alert ${klass} mb-2" role = "alert" >
< strong > ${label}:< / strong > ${title}
${message ? `< div class = "small mt-1" > ${message}< / div > ` : ''}
< / div >
`;
}).join('');
container.innerHTML = html;
container.classList.remove('d-none');
} catch (error) {
console.error('Failed to load customer alerts on sag list:', error);
container.innerHTML = '< div class = "alert alert-warning mb-0" role = "alert" > < strong > Advarsel:< / strong > Kunde-alerts kunne ikke hentes.< / div > ';
container.classList.remove('d-none');
}
}
2026-02-01 11:58:44 +01:00
// Tree toggle functionality
function toggleTreeNode(event, sagId) {
event.stopPropagation();
const toggle = event.currentTarget || event.target;
const row = toggle.closest('tr');
2026-01-31 23:16:24 +01:00
2026-02-01 11:58:44 +01:00
if (!row) {
return;
2026-02-01 00:38:10 +01:00
}
2026-02-01 11:58:44 +01:00
if (row.classList.contains('expanded')) {
row.classList.remove('expanded');
toggle.textContent = '+';
} else {
row.classList.add('expanded');
toggle.textContent = '-';
2026-02-01 00:38:10 +01:00
}
2026-02-01 11:58:44 +01:00
applyFilters();
}
// Search functionality
const searchInput = document.getElementById('searchInput');
const allRows = document.querySelectorAll('.tree-row');
let currentSearch = '';
let currentFilter = 'all';
2026-08-30 14:34:43 +02:00
let currentQuickFilter = {{ (current_quick_filter or 'all')|tojson }};
let currentEmployeeId = '';
let currentEmployeeGroupIds = new Set();
2026-06-18 22:20:39 +02:00
let currentTypes = new Set();
let currentAssignees = new Set();
let currentGroups = new Set();
const closedStatuses = new Set({{ (closed_statuses or ['lukket', 'løst', 'afsluttet', 'closed', 'resolved', 'done'])|tojson }});
2026-02-01 11:58:44 +01:00
2026-08-30 14:34:43 +02:00
function isDeadlinePast(deadlineValue) {
if (!deadlineValue) return false;
const deadline = new Date(deadlineValue);
if (Number.isNaN(deadline.getTime())) return false;
deadline.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
return deadline.getTime() < today.getTime ( ) ;
}
function updateOverdueDeadlineMarker(row) {
if (!row) return;
const status = String(row.dataset.status || '').trim().toLowerCase();
const deadlineValue = String(row.dataset.deadline || '').trim();
const isOverdue = isDeadlinePast(deadlineValue)
& & !closedStatuses.has(status);
row.classList.toggle('sag-deadline-overdue', isOverdue);
}
document.querySelectorAll('.sag-table tbody tr[data-deadline]').forEach(updateOverdueDeadlineMarker);
2026-02-17 08:29:05 +01:00
const assigneeFilter = document.getElementById('assigneeFilter');
const groupFilter = document.getElementById('groupFilter');
2026-06-18 22:20:39 +02:00
const assigneeFilterList = document.getElementById('assigneeFilterList');
const groupFilterList = document.getElementById('groupFilterList');
const assigneeDropdownLabel = document.getElementById('assigneeDropdownLabel');
const groupDropdownLabel = document.getElementById('groupDropdownLabel');
const clearAssigneeFilterBtn = document.getElementById('clearAssigneeFilterBtn');
const clearGroupFilterBtn = document.getElementById('clearGroupFilterBtn');
2026-02-17 08:29:05 +01:00
2026-02-01 11:58:44 +01:00
function applyFilters() {
const search = currentSearch;
2026-08-30 14:34:43 +02:00
const matchesQuickFilter = (row, isClosed) => {
const assigneeId = String(row.dataset.assigneeId || '').trim();
const groupId = String(row.dataset.groupId || '').trim();
if (currentQuickFilter === 'mine-open') return !isClosed & & !!currentEmployeeId & & assigneeId === currentEmployeeId;
if (currentQuickFilter === 'my-groups') return !isClosed & & currentEmployeeGroupIds.has(groupId);
if (currentQuickFilter === 'unassigned') return !isClosed & & !assigneeId;
if (currentQuickFilter === 'closed') return isClosed;
if (currentQuickFilter === 'overdue') {
const deadline = String(row.dataset.deadline || '');
return !isClosed & & isDeadlinePast(deadline);
}
return !isClosed;
};
2026-02-01 11:58:44 +01:00
allRows.forEach(row => {
const text = row.textContent.toLowerCase();
2026-06-18 22:20:39 +02:00
const status = String(row.dataset.status || '').toLowerCase();
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
const type = row.dataset.type || 'ticket';
2026-06-18 22:20:39 +02:00
const assigneeRaw = String(row.dataset.assigneeId || '').trim();
const groupRaw = String(row.dataset.groupId || '').trim();
const assigneeId = assigneeRaw || '__UNASSIGNED__';
const groupId = groupRaw;
2026-08-30 14:34:43 +02:00
const matchesSearch = search.split(/\s+/).filter(Boolean).every(term => text.includes(term));
2026-06-18 22:20:39 +02:00
const isClosed = closedStatuses.has(status);
const matchesFilter = currentFilter === 'all'
|| (currentFilter === 'åben' & & !isClosed)
|| (currentFilter === 'lukket' & & isClosed)
|| status === currentFilter;
const matchesType = currentTypes.size === 0 || currentTypes.has(type);
const matchesAssignee = currentAssignees.size === 0 || currentAssignees.has(assigneeId);
const matchesGroup = currentGroups.size === 0 || currentGroups.has(groupId);
2026-08-30 14:34:43 +02:00
const visible = matchesSearch & & matchesFilter & & matchesType & & matchesAssignee & & matchesGroup & & matchesQuickFilter(row, isClosed);
2026-02-01 11:58:44 +01:00
row.style.display = visible ? '' : 'none';
const sagId = row.dataset.sagId;
if (sagId) {
const children = document.querySelectorAll(`tr[data-parent="${sagId}"]`);
children.forEach(child => {
const childText = child.textContent.toLowerCase();
2026-06-18 22:20:39 +02:00
const childStatus = String(child.dataset.status || '').toLowerCase();
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
const childType = child.dataset.type || 'ticket';
2026-06-18 22:20:39 +02:00
const childAssigneeRaw = String(child.dataset.assigneeId || '').trim();
const childGroupRaw = String(child.dataset.groupId || '').trim();
const childAssigneeId = childAssigneeRaw || '__UNASSIGNED__';
const childGroupId = childGroupRaw;
2026-08-30 14:34:43 +02:00
const childMatchesSearch = search.split(/\s+/).filter(Boolean).every(term => childText.includes(term));
2026-06-18 22:20:39 +02:00
const childIsClosed = closedStatuses.has(childStatus);
const childMatchesFilter = currentFilter === 'all'
|| (currentFilter === 'åben' & & !childIsClosed)
|| (currentFilter === 'lukket' & & childIsClosed)
|| childStatus === currentFilter;
const childMatchesType = currentTypes.size === 0 || currentTypes.has(childType);
const childMatchesAssignee = currentAssignees.size === 0 || currentAssignees.has(childAssigneeId);
const childMatchesGroup = currentGroups.size === 0 || currentGroups.has(childGroupId);
2026-08-30 14:34:43 +02:00
const childVisible = visible & & row.classList.contains('expanded') & & childMatchesSearch & & childMatchesFilter & & childMatchesType & & childMatchesAssignee & & childMatchesGroup & & matchesQuickFilter(child, childIsClosed);
2026-02-01 11:58:44 +01:00
child.style.display = childVisible ? '' : 'none';
});
}
});
}
2026-06-18 22:20:39 +02:00
function renderMiniFilterOptions(selectEl, listEl, selectedSet, labelEl, defaultLabel) {
if (!selectEl || !listEl) return;
const options = Array.from(selectEl.options || []).filter((opt) => String(opt.value || '').trim() !== '');
if (options.length === 0) {
listEl.innerHTML = '< span class = "type-filter-empty" > Ingen muligheder< / span > ';
if (labelEl) labelEl.textContent = defaultLabel;
return;
}
listEl.innerHTML = options.map((opt) => {
const value = String(opt.value || '').trim();
const isChecked = selectedSet.has(value);
const safeValue = value.replace(/"/g, '" ');
const safeLabel = String(opt.textContent || value).replace(/"/g, '" ');
const safeId = `${selectEl.id}-opt-${value.replace(/[^a-zA-Z0-9_-]+/g, '-')}`;
return `
< label class = "mini-filter-item" for = "${safeId}" >
< input class = "form-check-input" type = "checkbox" id = "${safeId}" data-value = "${safeValue}" $ { isChecked ? ' checked ' : ' ' } >
< span class = "mini-filter-label" > ${safeLabel}< / span >
< / label >
`;
}).join('');
if (labelEl) {
labelEl.textContent = selectedSet.size === 0 ? defaultLabel : `${selectedSet.size} valgt`;
}
}
function syncMiniSelect(selectEl, selectedSet) {
if (!selectEl) return;
Array.from(selectEl.options || []).forEach((opt) => {
const value = String(opt.value || '').trim();
opt.selected = value !== '' & & selectedSet.has(value);
});
}
if (assigneeFilterList & & assigneeFilter) {
assigneeFilterList.addEventListener('change', function(event) {
const checkbox = event.target.closest('input[type="checkbox"][data-value]');
if (!checkbox) return;
const value = String(checkbox.dataset.value || '').trim();
if (!value) return;
if (checkbox.checked) currentAssignees.add(value);
else currentAssignees.delete(value);
syncMiniSelect(assigneeFilter, currentAssignees);
renderMiniFilterOptions(assigneeFilter, assigneeFilterList, currentAssignees, assigneeDropdownLabel, 'Ansvarlig');
applyFilters();
});
}
if (groupFilterList & & groupFilter) {
groupFilterList.addEventListener('change', function(event) {
const checkbox = event.target.closest('input[type="checkbox"][data-value]');
if (!checkbox) return;
const value = String(checkbox.dataset.value || '').trim();
if (!value) return;
if (checkbox.checked) currentGroups.add(value);
else currentGroups.delete(value);
syncMiniSelect(groupFilter, currentGroups);
renderMiniFilterOptions(groupFilter, groupFilterList, currentGroups, groupDropdownLabel, 'Grupper');
applyFilters();
});
}
if (clearAssigneeFilterBtn) {
clearAssigneeFilterBtn.addEventListener('click', function() {
currentAssignees = new Set();
syncMiniSelect(assigneeFilter, currentAssignees);
renderMiniFilterOptions(assigneeFilter, assigneeFilterList, currentAssignees, assigneeDropdownLabel, 'Ansvarlig');
applyFilters();
});
}
if (clearGroupFilterBtn) {
clearGroupFilterBtn.addEventListener('click', function() {
currentGroups = new Set();
syncMiniSelect(groupFilter, currentGroups);
renderMiniFilterOptions(groupFilter, groupFilterList, currentGroups, groupDropdownLabel, 'Grupper');
applyFilters();
});
}
function initMiniFilterSelections() {
if (assigneeFilter) {
currentAssignees = new Set(
Array.from(assigneeFilter.selectedOptions || [])
.map((opt) => String(opt.value || '').trim())
.filter((value) => value)
);
renderMiniFilterOptions(assigneeFilter, assigneeFilterList, currentAssignees, assigneeDropdownLabel, 'Ansvarlig');
}
if (groupFilter) {
currentGroups = new Set(
Array.from(groupFilter.selectedOptions || [])
.map((opt) => String(opt.value || '').trim())
.filter((value) => value)
);
renderMiniFilterOptions(groupFilter, groupFilterList, currentGroups, groupDropdownLabel, 'Grupper');
}
}
2026-02-01 11:58:44 +01:00
if (searchInput) {
searchInput.addEventListener('input', function(e) {
2026-08-30 14:34:43 +02:00
currentSearch = e.target.value.trim().toLowerCase();
document.getElementById('clearSearchBtn')?.classList.toggle('d-none', !currentSearch);
2026-02-01 11:58:44 +01:00
applyFilters();
});
}
2026-08-30 14:34:43 +02:00
document.getElementById('clearSearchBtn')?.addEventListener('click', () => {
searchInput.value = '';
currentSearch = '';
document.getElementById('clearSearchBtn')?.classList.add('d-none');
searchInput.focus();
applyFilters();
});
document.addEventListener('keydown', event => {
if (event.key === '/' & & !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName)) {
event.preventDefault();
searchInput?.focus();
}
if (event.key === 'Escape' & & document.activeElement === searchInput & & searchInput.value) {
document.getElementById('clearSearchBtn')?.click();
}
});
document.querySelectorAll('.sag-quick-filter').forEach(button => {
button.addEventListener('click', () => {
const quick = button.dataset.quickFilter || 'all';
const url = new URL(window.location.href);
if (quick === 'all') url.searchParams.delete('quick');
else url.searchParams.set('quick', quick);
url.searchParams.delete('status');
window.location.assign(url.toString());
});
});
async function loadQuickFilterContext() {
try {
const response = await fetch('/api/v1/sag/me/quick-filter-context', { credentials: 'include' });
if (!response.ok) return;
const data = await response.json();
currentEmployeeId = String(data.user_id || '');
currentEmployeeGroupIds = new Set((data.group_ids || []).map(String));
applyFilters();
} catch (error) {
console.error('Kunne ikke hente hurtigfilter-kontekst', error);
}
}
loadQuickFilterContext();
2026-02-01 11:58:44 +01:00
// Filter functionality
const filterPills = document.querySelectorAll('.filter-pill');
filterPills.forEach(pill => {
pill.addEventListener('click', function() {
// Update active state
filterPills.forEach(p => p.classList.remove('active'));
this.classList.add('active');
currentFilter = this.dataset.filter || 'all';
applyFilters();
2026-01-29 23:07:33 +01:00
});
2026-02-01 11:58:44 +01:00
});
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
const typeFilter = document.getElementById('typeFilter');
2026-06-18 22:20:39 +02:00
const typeFilterCheckboxList = document.getElementById('typeFilterCheckboxList');
const typeFilterDropdownLabel = document.getElementById('typeFilterDropdownLabel');
const typeFilterSelection = document.getElementById('typeFilterSelection');
const clearTypeFilterBtn = document.getElementById('clearTypeFilterBtn');
const saveTypeFilterDefaultBtn = document.getElementById('saveTypeFilterDefaultBtn');
function getSelectedTypesFromUi() {
return Array.from(currentTypes);
}
function renderTypeFilterOptions() {
if (!typeFilterCheckboxList || !typeFilter) return;
const options = Array.from(typeFilter.options || []);
if (options.length === 0) {
typeFilterCheckboxList.innerHTML = '< span class = "type-filter-empty" > Ingen typer fundet< / span > ';
2026-08-17 19:27:43 +02:00
if (typeFilterSelection) typeFilterSelection.textContent = 'Ingen typer tilgængelige';
2026-06-18 22:20:39 +02:00
if (typeFilterDropdownLabel) typeFilterDropdownLabel.textContent = 'Ingen typer';
return;
}
typeFilterCheckboxList.innerHTML = options.map((opt) => {
const value = String(opt.value || '').trim();
const key = value.toLowerCase();
const isActive = currentTypes.has(key);
const safeValue = value.replace(/"/g, '" ');
const safeId = `type-filter-opt-${key.replace(/[^a-z0-9_-]+/g, '-')}`;
return `
< label class = "type-filter-checkbox-item" for = "${safeId}" >
< input class = "form-check-input" type = "checkbox" id = "${safeId}" data-type = "${safeValue}" $ { isActive ? ' checked ' : ' ' } >
< span class = "type-filter-checkbox-label" > ${value}< / span >
< / label >
`;
}).join('');
if (typeFilterSelection) {
const selectedLabels = options
.filter((opt) => currentTypes.has(String(opt.value || '').trim().toLowerCase()))
.map((opt) => String(opt.value || '').trim())
.filter(Boolean);
if (selectedLabels.length === 0) {
typeFilterSelection.textContent = 'Ingen typer valgt';
if (typeFilterDropdownLabel) typeFilterDropdownLabel.textContent = 'Vælg typer';
} else {
typeFilterSelection.textContent = `Valgt: ${selectedLabels.join(', ')}`;
if (typeFilterDropdownLabel) typeFilterDropdownLabel.textContent = `${selectedLabels.length} valgt`;
}
}
}
function applySelectedTypesToUi() {
if (!typeFilter) return;
Array.from(typeFilter.options || []).forEach((opt) => {
opt.selected = currentTypes.has(String(opt.value || '').trim().toLowerCase());
});
renderTypeFilterOptions();
}
if (typeFilterCheckboxList) {
typeFilterCheckboxList.addEventListener('change', function(event) {
const checkbox = event.target.closest('input[type="checkbox"][data-type]');
if (!checkbox) return;
const typeValue = String(checkbox.dataset.type || '').trim().toLowerCase();
if (!typeValue) return;
if (checkbox.checked) {
currentTypes.add(typeValue);
} else {
currentTypes.delete(typeValue);
}
applySelectedTypesToUi();
applyFilters();
});
}
if (clearTypeFilterBtn) {
clearTypeFilterBtn.addEventListener('click', function() {
currentTypes = new Set();
applySelectedTypesToUi();
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
applyFilters();
});
}
async function loadTypeFilters() {
if (!typeFilter) return;
try {
2026-02-17 08:29:05 +01:00
const rowTypes = new Set(Array.from(document.querySelectorAll('.tree-row[data-type], .tree-child[data-type]'))
.map((row) => (row.dataset.type || '').trim())
.filter(Boolean));
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
const res = await fetch('/api/v1/settings/case_types');
2026-02-17 08:29:05 +01:00
let configuredTypes = [];
if (res.ok) {
const setting = await res.json();
const types = JSON.parse(setting.value || '[]');
if (Array.isArray(types)) {
configuredTypes = types;
}
}
configuredTypes.forEach((t) => rowTypes.add(String(t || '').trim()));
const mergedTypes = Array.from(rowTypes).filter(Boolean).sort((a, b) => a.localeCompare(b, 'da'));
2026-06-18 22:20:39 +02:00
if (mergedTypes.length === 0) {
typeFilter.innerHTML = '';
renderTypeFilterOptions();
return;
}
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
2026-06-18 22:20:39 +02:00
typeFilter.innerHTML = mergedTypes.map(type => `< option value = "${type}" > ${type}< / option > `).join('');
applySelectedTypesToUi();
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
} catch (err) {
console.error('Failed to load case types', err);
}
}
2026-08-30 14:34:43 +02:00
const SAG_COLUMN_DEFINITIONS = [
['id', 'SagsID'], ['company', 'Virksomhed'], ['contact', 'Kontakt'],
['description', 'Beskrivelse'], ['type', 'Type'], ['priority', 'Prioritet'],
['status', 'Status'], ['owner', 'Ansvarlig'], ['group', 'Gruppe/Level'],
['next_todo', 'Næste todo'], ['created', 'Oprettet'], ['start', 'Arbejdsstart'],
['deferred', 'Start senest'], ['deadline', 'Deadline']
];
const SAG_DEFAULT_COLUMN_ORDER = SAG_COLUMN_DEFINITIONS.map(([key]) => key);
let sagColumnOrder = [...SAG_DEFAULT_COLUMN_ORDER];
let sagHiddenColumns = new Set();
function normalizeSagColumnOrder(order) {
const allowed = new Set(SAG_DEFAULT_COLUMN_ORDER);
const normalized = Array.from(new Set((Array.isArray(order) ? order : []).filter(key => allowed.has(key))));
SAG_DEFAULT_COLUMN_ORDER.forEach(key => { if (!normalized.includes(key)) normalized.push(key); });
return normalized;
}
function applySagColumnPreferences() {
document.querySelectorAll('.sag-table tr').forEach(row => {
const cells = Array.from(row.children).filter(cell => cell.matches('th,td'));
if (!cells.length) return;
const expandCell = cells.find(cell => cell.dataset.columnKey === 'expand') || cells[0];
if (!expandCell.dataset.columnKey) {
expandCell.dataset.columnKey = 'expand';
cells.slice(1).forEach((cell, index) => {
if (SAG_DEFAULT_COLUMN_ORDER[index]) cell.dataset.columnKey = SAG_DEFAULT_COLUMN_ORDER[index];
});
}
const byKey = new Map(Array.from(row.children).filter(cell => cell.dataset.columnKey).map(cell => [cell.dataset.columnKey, cell]));
if (byKey.get('expand')) row.appendChild(byKey.get('expand'));
sagColumnOrder.forEach(key => {
const cell = byKey.get(key);
if (!cell) return;
cell.style.display = sagHiddenColumns.has(key) ? 'none' : '';
row.appendChild(cell);
});
});
}
function renderSagColumnChooser() {
const host = document.getElementById('sagColumnList');
if (!host) return;
const labels = Object.fromEntries(SAG_COLUMN_DEFINITIONS);
host.innerHTML = sagColumnOrder.map(key => `
< div class = "sag-column-item" draggable = "true" data-column-key = "${key}" >
< i class = "bi bi-grip-vertical drag-handle" > < / i >
< input class = "form-check-input sag-column-visible" type = "checkbox" $ { sagHiddenColumns . has ( key ) ? ' ' : ' checked ' } >
< span > ${labels[key]}< / span >
< / div > `).join('');
}
function applySagColumnPayload(data) {
sagColumnOrder = normalizeSagColumnOrder(data?.column_order);
sagHiddenColumns = new Set((Array.isArray(data?.hidden_columns) ? data.hidden_columns : []).filter(key => SAG_DEFAULT_COLUMN_ORDER.includes(key)));
renderSagColumnChooser();
applySagColumnPreferences();
}
async function saveSagColumnPreferences() {
const button = document.getElementById('saveSagColumnsBtn');
if (button) button.disabled = true;
try {
const response = await fetch('/api/v1/sag/me/list-preferences', {
method: 'PATCH', credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type_filters: getSelectedTypesFromUi(),
column_order: sagColumnOrder,
hidden_columns: Array.from(sagHiddenColumns)
})
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.detail || 'Kolonnerne kunne ikke gemmes');
applySagColumnPayload(payload);
if (typeof showNotification === 'function') showNotification('Kolonner gemt for din bruger', 'success');
} catch (error) {
if (typeof showNotification === 'function') showNotification(error.message, 'error');
} finally {
if (button) button.disabled = false;
}
}
const sagColumnList = document.getElementById('sagColumnList');
let draggedSagColumn = null;
sagColumnList?.addEventListener('change', event => {
const item = event.target.closest('.sag-column-item');
if (!item || !event.target.matches('.sag-column-visible')) return;
if (event.target.checked) sagHiddenColumns.delete(item.dataset.columnKey);
else sagHiddenColumns.add(item.dataset.columnKey);
applySagColumnPreferences();
});
sagColumnList?.addEventListener('dragstart', event => {
draggedSagColumn = event.target.closest('.sag-column-item')?.dataset.columnKey || null;
event.target.closest('.sag-column-item')?.classList.add('dragging');
});
sagColumnList?.addEventListener('dragend', event => event.target.closest('.sag-column-item')?.classList.remove('dragging'));
sagColumnList?.addEventListener('dragover', event => event.preventDefault());
sagColumnList?.addEventListener('drop', event => {
event.preventDefault();
const targetKey = event.target.closest('.sag-column-item')?.dataset.columnKey;
if (!draggedSagColumn || !targetKey || draggedSagColumn === targetKey) return;
sagColumnOrder = sagColumnOrder.filter(key => key !== draggedSagColumn);
sagColumnOrder.splice(sagColumnOrder.indexOf(targetKey), 0, draggedSagColumn);
draggedSagColumn = null;
renderSagColumnChooser();
applySagColumnPreferences();
});
document.getElementById('resetSagColumnsBtn')?.addEventListener('click', () => {
sagColumnOrder = [...SAG_DEFAULT_COLUMN_ORDER];
sagHiddenColumns = new Set();
renderSagColumnChooser();
applySagColumnPreferences();
});
document.getElementById('saveSagColumnsBtn')?.addEventListener('click', saveSagColumnPreferences);
2026-06-18 22:20:39 +02:00
async function loadTypeFilterPreferences() {
try {
const res = await fetch('/api/v1/sag/me/list-preferences', { credentials: 'include' });
if (!res.ok) return;
const data = await res.json();
const fromServer = Array.isArray(data?.type_filters) ? data.type_filters : [];
currentTypes = new Set(fromServer.map((v) => String(v || '').trim().toLowerCase()).filter(Boolean));
2026-08-30 14:34:43 +02:00
applySagColumnPayload(data);
2026-06-18 22:20:39 +02:00
applySelectedTypesToUi();
applyFilters();
} catch (err) {
console.error('Failed to load type filter preferences', err);
}
}
async function saveTypeFilterPreferences() {
if (!saveTypeFilterDefaultBtn) return;
const selected = getSelectedTypesFromUi();
saveTypeFilterDefaultBtn.disabled = true;
try {
const res = await fetch('/api/v1/sag/me/list-preferences', {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
2026-08-30 14:34:43 +02:00
body: JSON.stringify({ type_filters: selected, column_order: sagColumnOrder, hidden_columns: Array.from(sagHiddenColumns) }),
2026-06-18 22:20:39 +02:00
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (typeof showNotification === 'function') {
showNotification('Typefilter gemt som standard', 'success');
}
} catch (err) {
console.error('Failed to save type filter preferences', err);
if (typeof showNotification === 'function') {
showNotification('Kunne ikke gemme typefilter', 'error');
}
} finally {
saveTypeFilterDefaultBtn.disabled = false;
}
}
if (saveTypeFilterDefaultBtn) {
saveTypeFilterDefaultBtn.addEventListener('click', saveTypeFilterPreferences);
}
(async () => {
initMiniFilterSelections();
await loadTypeFilters();
await loadTypeFilterPreferences();
applyFilters();
})();
2026-04-01 21:34:58 +02:00
if (topAlertCustomerId) {
loadSagTopAlertsForCustomer(topAlertCustomerId);
}
2026-02-01 11:58:44 +01:00
< / script >
{% endblock %}