- Created migration 146 to seed case type tags with various categories and keywords. - Created migration 147 to seed brand and type tags, including a comprehensive list of brands and case types. - Added migration 148 to introduce a new column `is_next` in `sag_todo_steps` for persistent next-task selection. - Implemented a new script `run_migrations.py` to facilitate running SQL migrations against the PostgreSQL database with options for dry runs and error handling.
620 lines
23 KiB
HTML
620 lines
23 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Sager - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.search-bar {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.search-bar input {
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
padding: 0.6rem 1rem;
|
|
}
|
|
|
|
.table-wrapper {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.sag-table {
|
|
width: 100%;
|
|
min-width: 1550px;
|
|
margin: 0;
|
|
}
|
|
|
|
.sag-table thead {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.sag-table thead th {
|
|
padding: 0.6rem 0.75rem;
|
|
font-weight: 600;
|
|
font-size: 0.78rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
border: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sag-table tbody tr {
|
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sag-table tbody tr:hover {
|
|
background: var(--accent-light);
|
|
}
|
|
|
|
.sag-table tbody td {
|
|
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;
|
|
}
|
|
|
|
.sag-id {
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.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-row.has-children td:first-child {
|
|
position: relative;
|
|
padding-left: 2.5rem !important;
|
|
}
|
|
|
|
.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%);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.tree-child td:first-child {
|
|
position: relative;
|
|
padding-left: 2.5rem !important;
|
|
}
|
|
|
|
.tree-child td:first-child:before {
|
|
content: '└';
|
|
position: absolute;
|
|
left: 0.5rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: rgba(0,0,0,0.3);
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.status-åben {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.status-lukket {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.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 {
|
|
display: flex;
|
|
gap: 2rem;
|
|
margin-bottom: 1.5rem;
|
|
padding: 1rem;
|
|
background: var(--bg-card);
|
|
border-radius: 8px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 3rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.3;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid" style="max-width: none; padding-top: 2rem;">
|
|
<!-- Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h1 style="margin: 0; color: var(--accent);">
|
|
<i class="bi bi-list-check me-2"></i>Sager
|
|
</h1>
|
|
<div class="d-flex gap-2">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Stats Bar -->
|
|
<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 class="stat-item">
|
|
<div class="stat-value">{{ sager|selectattr('status', 'equalto', 'lukket')|list|length }}</div>
|
|
<div class="stat-label">Lukkede</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search & Filters -->
|
|
<div class="search-bar">
|
|
<input type="text"
|
|
class="form-control"
|
|
id="searchInput"
|
|
placeholder="🔍 Søg efter sag ID, titel, beskrivelse..."
|
|
autocomplete="off">
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap align-items-center gap-3 mb-3">
|
|
<div class="filter-pills">
|
|
<div class="filter-pill active" data-filter="all">Alle</div>
|
|
<div class="filter-pill" data-filter="åben">Åbne</div>
|
|
<div class="filter-pill" data-filter="lukket">Lukkede</div>
|
|
</div>
|
|
<div style="min-width: 200px;">
|
|
<select class="form-select" id="typeFilter">
|
|
<option value="all">Alle typer</option>
|
|
</select>
|
|
</div>
|
|
<form id="assignmentFilterForm" class="d-flex flex-wrap gap-2 align-items-center" method="get" action="/sag">
|
|
<div style="min-width: 220px;">
|
|
<select class="form-select" name="ansvarlig_bruger_id" id="assigneeFilter">
|
|
<option value="">Alle medarbejdere</option>
|
|
{% 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 %}
|
|
</select>
|
|
</div>
|
|
<div style="min-width: 220px;">
|
|
<select class="form-select" name="assigned_group_id" id="groupFilter">
|
|
<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 %}
|
|
</select>
|
|
</div>
|
|
{% if include_deferred %}
|
|
<input type="hidden" name="include_deferred" value="1">
|
|
{% endif %}
|
|
</form>
|
|
<a class="btn btn-sm btn-outline-secondary" href="{{ toggle_include_deferred_url }}">
|
|
{% if include_deferred %}Skjul udsatte{% else %}Vis udsatte{% endif %}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="table-wrapper">
|
|
{% if sager %}
|
|
<table class="sag-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 90px;">SagsID</th>
|
|
<th style="width: 180px;">Virksom.</th>
|
|
<th style="width: 150px;">Kontakt</th>
|
|
<th style="width: 300px;">Beskr.</th>
|
|
<th style="width: 120px;">Type</th>
|
|
<th style="width: 110px;">Prioritet</th>
|
|
<th style="width: 160px;">Ansvarl.</th>
|
|
<th style="width: 170px;">Gruppe/Level</th>
|
|
<th style="width: 120px;">Opret.</th>
|
|
<th style="width: 120px;">Start arbejde</th>
|
|
<th style="width: 140px;">Start inden</th>
|
|
<th style="width: 120px;">Deadline</th>
|
|
</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 }}"
|
|
data-status="{{ sag.status }}"
|
|
data-type="{{ sag.template_key or sag.type or 'ticket' }}">
|
|
<td>
|
|
{% if has_relations %}
|
|
<span class="tree-toggle" onclick="toggleTreeNode(event, {{ sag.id }})">+</span>
|
|
{% endif %}
|
|
<span class="sag-id">#{{ sag.id }}</span>
|
|
</td>
|
|
<td class="col-company" onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
{{ sag.customer_name if sag.customer_name else '-' }}
|
|
</td>
|
|
<td class="col-contact" onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
{{ sag.kontakt_navn if sag.kontakt_navn and sag.kontakt_navn.strip() else '-' }}
|
|
</td>
|
|
<td class="col-desc" onclick="window.location.href='/sag/{{ sag.id }}'">
|
|
<div class="sag-titel">{{ sag.titel }}</div>
|
|
{% if sag.beskrivelse %}
|
|
<div class="sag-beskrivelse">{{ sag.beskrivelse }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ sag.id }}'">
|
|
<span class="badge bg-light text-dark border">{{ sag.template_key or sag.type or 'ticket' }}</span>
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem; text-transform: capitalize;">
|
|
{{ sag.priority if sag.priority else 'normal' }}
|
|
</td>
|
|
<td class="col-owner" onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
{{ sag.ansvarlig_navn if sag.ansvarlig_navn else '-' }}
|
|
</td>
|
|
<td class="col-group" onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
{{ sag.assigned_group_name if sag.assigned_group_name else '-' }}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary);">
|
|
{{ sag.created_at.strftime('%d/%m-%Y') if sag.created_at else '-' }}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary);">
|
|
{{ sag.start_date.strftime('%d/%m-%Y') if sag.start_date else '-' }}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary);">
|
|
{{ sag.deferred_until.strftime('%d/%m-%Y') if sag.deferred_until else '-' }}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ sag.id }}'" style="color: var(--text-secondary);">
|
|
{{ sag.deadline.strftime('%d/%m-%Y') if sag.deadline else '-' }}
|
|
</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 %}
|
|
<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' }}" style="display: none;">
|
|
<td>
|
|
<span class="sag-id">#{{ related_sag.id }}</span>
|
|
</td>
|
|
<td class="col-company" onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
{{ related_sag.customer_name if related_sag.customer_name else '-' }}
|
|
</td>
|
|
<td class="col-contact" onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
{{ related_sag.kontakt_navn if related_sag.kontakt_navn and related_sag.kontakt_navn.strip() else '-' }}
|
|
</td>
|
|
<td class="col-desc" onclick="window.location.href='/sag/{{ related_sag.id }}'">
|
|
{% for rt in all_rel_types %}
|
|
<span class="relation-badge">{{ rt }}</span>
|
|
{% endfor %}
|
|
<div class="sag-titel" style="display: inline;">{{ related_sag.titel }}</div>
|
|
{% if related_sag.beskrivelse %}
|
|
<div class="sag-beskrivelse">{{ related_sag.beskrivelse }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ related_sag.id }}'">
|
|
<span class="badge bg-light text-dark border">{{ related_sag.template_key or related_sag.type or 'ticket' }}</span>
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem; text-transform: capitalize;">
|
|
{{ related_sag.priority if related_sag.priority else 'normal' }}
|
|
</td>
|
|
<td class="col-owner" onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
{{ related_sag.ansvarlig_navn if related_sag.ansvarlig_navn else '-' }}
|
|
</td>
|
|
<td class="col-group" onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
{{ related_sag.assigned_group_name if related_sag.assigned_group_name else '-' }}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary);">
|
|
{{ related_sag.created_at.strftime('%d/%m-%Y') if related_sag.created_at else '-' }}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary);">
|
|
{{ related_sag.start_date.strftime('%d/%m-%Y') if related_sag.start_date else '-' }}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary);">
|
|
{{ related_sag.deferred_until.strftime('%d/%m-%Y') if related_sag.deferred_until else '-' }}
|
|
</td>
|
|
<td onclick="window.location.href='/sag/{{ related_sag.id }}'" style="color: var(--text-secondary);">
|
|
{{ related_sag.deadline.strftime('%d/%m-%Y') if related_sag.deadline else '-' }}
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<i class="bi bi-inbox"></i>
|
|
<p>Ingen sager fundet</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Tree toggle functionality
|
|
function toggleTreeNode(event, sagId) {
|
|
event.stopPropagation();
|
|
const toggle = event.currentTarget || event.target;
|
|
const row = toggle.closest('tr');
|
|
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
if (row.classList.contains('expanded')) {
|
|
row.classList.remove('expanded');
|
|
toggle.textContent = '+';
|
|
} else {
|
|
row.classList.add('expanded');
|
|
toggle.textContent = '-';
|
|
}
|
|
|
|
applyFilters();
|
|
}
|
|
|
|
// Search functionality
|
|
const searchInput = document.getElementById('searchInput');
|
|
const allRows = document.querySelectorAll('.tree-row');
|
|
let currentSearch = '';
|
|
let currentFilter = 'all';
|
|
let currentType = 'all';
|
|
|
|
const assigneeFilter = document.getElementById('assigneeFilter');
|
|
const groupFilter = document.getElementById('groupFilter');
|
|
const assignmentFilterForm = document.getElementById('assignmentFilterForm');
|
|
|
|
if (assigneeFilter && assignmentFilterForm) {
|
|
assigneeFilter.addEventListener('change', () => assignmentFilterForm.submit());
|
|
}
|
|
if (groupFilter && assignmentFilterForm) {
|
|
groupFilter.addEventListener('change', () => assignmentFilterForm.submit());
|
|
}
|
|
|
|
function applyFilters() {
|
|
const search = currentSearch;
|
|
|
|
allRows.forEach(row => {
|
|
const text = row.textContent.toLowerCase();
|
|
const status = row.dataset.status;
|
|
const type = row.dataset.type || 'ticket';
|
|
const matchesSearch = text.includes(search);
|
|
const matchesFilter = currentFilter === 'all' || status === currentFilter;
|
|
const matchesType = currentType === 'all' || type === currentType;
|
|
const visible = matchesSearch && matchesFilter && matchesType;
|
|
|
|
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();
|
|
const childStatus = child.dataset.status;
|
|
const childType = child.dataset.type || 'ticket';
|
|
const childMatchesSearch = childText.includes(search);
|
|
const childMatchesFilter = currentFilter === 'all' || childStatus === currentFilter;
|
|
const childMatchesType = currentType === 'all' || childType === currentType;
|
|
const childVisible = visible && row.classList.contains('expanded') && childMatchesSearch && childMatchesFilter && childMatchesType;
|
|
child.style.display = childVisible ? '' : 'none';
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', function(e) {
|
|
currentSearch = e.target.value.toLowerCase();
|
|
applyFilters();
|
|
});
|
|
}
|
|
|
|
// 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();
|
|
});
|
|
});
|
|
|
|
const typeFilter = document.getElementById('typeFilter');
|
|
if (typeFilter) {
|
|
typeFilter.addEventListener('change', function() {
|
|
currentType = this.value || 'all';
|
|
applyFilters();
|
|
});
|
|
}
|
|
|
|
async function loadTypeFilters() {
|
|
if (!typeFilter) return;
|
|
try {
|
|
const rowTypes = new Set(Array.from(document.querySelectorAll('.tree-row[data-type], .tree-child[data-type]'))
|
|
.map((row) => (row.dataset.type || '').trim())
|
|
.filter(Boolean));
|
|
|
|
const res = await fetch('/api/v1/settings/case_types');
|
|
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'));
|
|
if (mergedTypes.length === 0) return;
|
|
|
|
typeFilter.innerHTML = `<option value="all">Alle typer</option>` +
|
|
mergedTypes.map(type => `<option value="${type}">${type}</option>`).join('');
|
|
} catch (err) {
|
|
console.error('Failed to load case types', err);
|
|
}
|
|
}
|
|
|
|
loadTypeFilters();
|
|
</script>
|
|
{% endblock %}
|