bmc_hub/app/modules/locations/templates/list.html

941 lines
36 KiB
HTML

{% extends "shared/frontend/base.html" %}
{% block title %}Lokaliteter - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.locations-list-page {
--loc-accent: #0f4c75;
--loc-accent-soft: rgba(15, 76, 117, 0.06);
--loc-border: rgba(15, 76, 117, 0.12);
--loc-surface: #f7f9fc;
}
.locations-list-page .locations-hero {
border: 1px solid rgba(0, 0, 0, 0.08);
background: var(--bg-card);
border-radius: 0.9rem;
box-shadow: 0 10px 28px rgba(15, 76, 117, 0.05);
}
.locations-list-page .stat-tile {
background: linear-gradient(180deg, #ffffff 0%, #f8fbfd 100%);
border: 1px solid rgba(15, 76, 117, 0.08);
border-radius: 0.8rem;
padding: 0.75rem 0.9rem;
}
.locations-list-page .section-label {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--loc-accent);
}
.locations-list-page .stat-tile .stat-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--loc-accent);
line-height: 1.1;
}
.locations-list-page .stat-tile .stat-note {
font-size: 0.76rem;
color: var(--text-secondary);
}
.locations-list-page .table thead th {
font-size: 0.76rem;
letter-spacing: 0.05em;
text-transform: uppercase;
font-weight: 700;
color: var(--text-secondary);
white-space: nowrap;
border-bottom: 1px solid rgba(15, 76, 117, 0.1);
background: #f8fafc;
}
.locations-list-page .location-row {
transition: background-color 0.16s ease;
}
.locations-list-page .location-row:hover {
background-color: rgba(15, 76, 117, 0.03);
}
.locations-list-page .table > :not(caption) > * > * {
padding-top: 0.95rem;
padding-bottom: 0.95rem;
vertical-align: middle;
}
.locations-list-page .table tbody tr:last-child td {
border-bottom: 0;
}
.locations-list-page .toggle-row,
.locations-list-page .location-tree-spacer {
color: var(--loc-accent);
width: 1.2rem;
display: inline-flex;
justify-content: center;
flex: 0 0 1.2rem;
}
.locations-list-page .filter-shell,
.locations-list-page .content-shell {
background: var(--bg-card);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 0.9rem;
box-shadow: 0 10px 28px rgba(15, 76, 117, 0.04);
}
.locations-list-page .filter-shell {
padding: 1.05rem 1.1rem;
}
.locations-list-page .content-shell-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1rem 1.1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.locations-list-page .content-shell-toolbar,
.locations-list-page .filter-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.locations-list-page .content-shell-title {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 700;
color: var(--text-secondary);
}
.locations-list-page .content-shell-subtitle {
font-size: 0.92rem;
color: var(--text-primary);
font-weight: 600;
}
.locations-list-page .content-shell-subtitle span {
color: var(--text-secondary);
font-weight: 500;
}
.locations-list-page .location-name-link {
color: var(--text-primary);
font-weight: 600;
font-size: 0.98rem;
}
.locations-list-page .location-name-link:hover {
color: var(--loc-accent);
}
.locations-list-page .location-primary {
min-width: 0;
}
.locations-list-page .location-summary {
display: flex;
align-items: flex-start;
gap: 0.7rem;
min-width: 0;
}
.locations-list-page .location-indent {
display: inline-flex;
align-items: stretch;
flex: 0 0 auto;
}
.locations-list-page .location-name-block {
min-width: 0;
}
.locations-list-page .location-meta {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.25rem;
}
.locations-list-page .location-meta-chip {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.18rem 0.5rem;
border-radius: 999px;
background: rgba(15, 76, 117, 0.07);
color: #2d5670;
font-size: 0.73rem;
font-weight: 600;
}
.locations-list-page .type-pill {
display: inline-flex;
align-items: center;
padding: 0.32rem 0.62rem;
border-radius: 999px;
font-size: 0.74rem;
font-weight: 700;
line-height: 1;
color: #fff;
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.18);
}
.locations-list-page .status-pill {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.28rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
}
.locations-list-page .status-pill.active {
background: rgba(25, 135, 84, 0.12);
color: #146c43;
}
.locations-list-page .status-pill.inactive {
background: rgba(108, 117, 125, 0.12);
color: #495057;
}
.locations-list-page .shortcut-hint {
border: 1px dashed rgba(0, 0, 0, 0.12);
border-radius: 0.55rem;
padding: 0.3rem 0.45rem;
font-size: 0.72rem;
color: var(--text-secondary);
}
.locations-list-page .hero-actions {
align-items: center;
}
.locations-list-page .list-status-note {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.4rem 0.65rem;
border-radius: 999px;
background: var(--loc-surface);
color: var(--text-secondary);
font-size: 0.78rem;
font-weight: 600;
}
.locations-list-page .actions-inline {
display: inline-flex;
align-items: center;
gap: 0.4rem;
flex-wrap: nowrap;
}
.locations-list-page .actions-inline .btn {
border-radius: 0.7rem;
}
.locations-list-page .empty-state-soft {
max-width: 420px;
margin: 0 auto;
}
@media (max-width: 767.98px) {
.locations-list-page {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.locations-list-page .stat-tile { padding: 0.65rem 0.75rem; }
.locations-list-page .content-shell-header { align-items: flex-start; flex-direction: column; }
.locations-list-page .content-shell-toolbar,
.locations-list-page .filter-toolbar { align-items: flex-start; flex-direction: column; }
.locations-list-page .hero-actions { width: 100%; }
.locations-list-page .hero-actions .btn { flex: 1 1 auto; }
.locations-list-page .location-summary { gap: 0.55rem; }
.locations-list-page .location-name-link { font-size: 0.94rem; }
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-4 py-4 locations-list-page">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/" class="text-decoration-none">Hjem</a></li>
<li class="breadcrumb-item active">Lokaliteter</li>
</ol>
</nav>
<!-- Header Section -->
<div class="locations-hero p-3 p-lg-4 mb-4">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div>
<div class="section-label mb-2">
<i class="bi bi-diagram-3"></i>
Lokationsstruktur
</div>
<h1 class="h2 fw-700 mb-1">Lokaliteter</h1>
<p class="text-muted small mb-0">Få overblik over steder, underlokationer og status uden at miste hierarkiet.</p>
</div>
<div class="d-flex gap-2 flex-wrap hero-actions">
<a href="/app/locations/create" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-2"></i>Opret lokation
</a>
<a href="/app/locations/wizard" class="btn btn-outline-primary btn-sm">
<i class="bi bi-diagram-3 me-2"></i>Wizard
</a>
<a href="/app/locations/outlets" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-ethernet me-2"></i>Vægstik
</a>
<span class="shortcut-hint">Tip: Tryk / for søgning</span>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-6 col-lg-3">
<div class="stat-tile">
<div class="small text-muted">Total</div>
<div class="stat-value" id="statTotal">{{ total or 0 }}</div>
<div class="stat-note">Alle registrerede lokationer</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-tile">
<div class="small text-muted">Aktive</div>
<div class="stat-value" id="statActive">0</div>
<div class="stat-note">Klar til daglig brug</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-tile">
<div class="small text-muted">Inaktive</div>
<div class="stat-value" id="statInactive">0</div>
<div class="stat-note">Skjulte eller lukkede</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="stat-tile">
<div class="small text-muted">Synlige nu</div>
<div class="stat-value" id="statVisible">{{ locations|length if locations else 0 }}</div>
<div class="stat-note">Efter søgning og foldning</div>
</div>
</div>
</div>
</div>
<!-- Filter Card -->
<div class="filter-shell mb-4">
<div class="filter-toolbar mb-3">
<div>
<div class="content-shell-title">Filtre</div>
<div class="text-muted small">Søg i navn og by, eller afgræns efter type og status.</div>
</div>
<div class="list-status-note">
<i class="bi bi-lightning-charge"></i>
Hierarkiet kan foldes direkte i listen
</div>
</div>
<form id="filterForm" method="get" class="row g-3 align-items-end">
<div class="col-lg-5">
<label for="locationSearch" class="form-label small text-muted">Søg</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="locationSearch" placeholder="Søg efter lokation eller by...">
</div>
</div>
<div class="col-md-4 col-lg-3">
<label for="locationTypeFilter" class="form-label small text-muted">Type</label>
<select class="form-select" id="locationTypeFilter" name="location_type">
<option value="">Alle typer</option>
{% if location_types %}
{% for type_option in location_types %}
{% set option_value = type_option['value'] if type_option is mapping else type_option %}
{% set option_label = type_option['label'] if type_option is mapping else type_option %}
<option value="{{ option_value }}" {% if location_type == option_value %}selected{% endif %}>
{% if option_value == 'kompleks' %}Kompleks{% elif option_value == 'bygning' %}Bygning{% elif option_value == 'etage' %}Etage{% elif option_value == 'customer_site' %}Kundesite{% elif option_value == 'rum' %}Rum{% elif option_value == 'kantine' %}Kantine{% elif option_value == 'moedelokale' %}Mødelokale{% elif option_value == 'vehicle' %}Køretøj{% else %}{{ option_label }}{% endif %}
</option>
{% endfor %}
{% endif %}
</select>
</div>
<div class="col-md-3 col-lg-2">
<label for="statusFilter" class="form-label small text-muted">Status</label>
<select class="form-select" id="statusFilter" name="is_active">
<option value="">Alle</option>
<option value="true" {% if is_active == True %}selected{% endif %}>Aktive</option>
<option value="false" {% if is_active == False %}selected{% endif %}>Inaktive</option>
</select>
</div>
<div class="col-md-5 col-lg-2 d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-funnel me-2"></i>Filtrer
</button>
<a href="/app/locations" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-counterclockwise"></i>
</a>
</div>
</form>
</div>
<!-- Main Content Section -->
<div class="content-shell">
<div class="content-shell-header">
<div class="content-shell-toolbar w-100">
<div>
<div class="content-shell-title">Lokationsliste</div>
<div class="content-shell-subtitle">
{% if total %}
Viser <strong id="visibleCount">{{ locations|length }}</strong> af <strong>{{ total }}</strong> lokationer
<span>med tydeligt hierarki og status</span>
{% else %}
Ingen lokationer endnu
{% endif %}
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<span class="list-status-note">
<i class="bi bi-check2-circle"></i>
Klik kun på navn eller handlinger for at åbne
</span>
<button type="button" class="btn btn-outline-danger btn-sm" id="bulkDeleteBtn" disabled>
<i class="bi bi-trash me-2"></i>Slet valgte
</button>
</div>
</div>
</div>
<div class="table-responsive">
{% if location_tree %}
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 40px;">
<input type="checkbox" class="form-check-input" id="selectAllCheckbox">
</th>
<th>Navn</th>
<th>Type</th>
<th class="d-none d-md-table-cell">By</th>
<th>Status</th>
<th style="width: 120px;">Handlinger</th>
</tr>
</thead>
<tbody>
{% macro render_row(node, depth, parent_id=None) %}
{% set type_label = {
'kompleks': 'Kompleks',
'bygning': 'Bygning',
'etage': 'Etage',
'customer_site': 'Kundesite',
'rum': 'Rum',
'kantine': 'Kantine',
'moedelokale': 'Mødelokale',
'vehicle': 'Køretøj'
}.get(node.location_type, node.location_type) %}
{% set type_color = {
'kompleks': '#0f4c75',
'bygning': '#1abc9c',
'etage': '#3498db',
'customer_site': '#9b59b6',
'rum': '#e67e22',
'kantine': '#d35400',
'moedelokale': '#16a085',
'vehicle': '#8e44ad'
}.get(node.location_type, '#6c757d') %}
{% set child_count = node.children|length if node.children else 0 %}
<tr class="location-row{% if node.children %} has-children{% endif %}" data-location-id="{{ node.id }}" data-parent-id="{{ parent_id if parent_id else '' }}" data-depth="{{ depth }}" data-has-children="{{ 'true' if node.children else 'false' }}">
<td>
<input type="checkbox" class="form-check-input location-checkbox" value="{{ node.id }}">
</td>
<td>
<div class="location-summary">
<div class="location-indent" style="padding-left: {{ depth * 18 }}px;">
{% if node.children %}
<button type="button" class="btn btn-link btn-sm p-0 toggle-row" data-target-id="{{ node.id }}" aria-expanded="false" title="Fold ud/ind">
<i class="bi bi-caret-right-fill"></i>
</button>
{% else %}
<span class="location-tree-spacer text-muted"></span>
{% endif %}
</div>
<div class="location-name-block">
<a href="/app/locations/{{ node.id }}" class="text-decoration-none location-name-link">
{{ node.name }}
</a>
<div class="location-meta">
{% if depth > 0 %}
<span class="location-meta-chip"><i class="bi bi-diagram-3"></i>Niveau {{ depth + 1 }}</span>
{% endif %}
{% if child_count %}
<span class="location-meta-chip"><i class="bi bi-collection"></i>{{ child_count }} underlokationer</span>
{% endif %}
{% if node.address_city %}
<span class="location-meta-chip"><i class="bi bi-geo-alt"></i>{{ node.address_city }}</span>
{% endif %}
<span class="location-meta-chip"><i class="bi bi-hash"></i>ID {{ node.id }}</span>
</div>
</div>
</div>
</td>
<td>
<span class="type-pill" style="background-color: {{ type_color }};">
{{ type_label }}
</span>
</td>
<td class="d-none d-md-table-cell text-muted small">
{{ node.address_city | default('—') }}
</td>
<td>
{% if node.is_active %}
<span class="status-pill active"><span></span>Aktiv</span>
{% else %}
<span class="status-pill inactive"><span></span>Inaktiv</span>
{% endif %}
</td>
<td>
<div class="actions-inline" role="group" aria-label="Handlinger">
<a href="/app/locations/{{ node.id }}" class="btn btn-outline-secondary btn-sm" title="Vis">
<i class="bi bi-eye"></i>
</a>
<a href="/app/locations/{{ node.id }}/edit" class="btn btn-outline-secondary btn-sm" title="Rediger">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-outline-danger btn-sm delete-location-btn"
data-location-id="{{ node.id }}"
data-location-name="{{ node.name }}"
title="Slet">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
{% if node.children %}
{% for child in node.children %}
{{ render_row(child, depth + 1, node.id) }}
{% endfor %}
{% endif %}
{% endmacro %}
{% for node in location_tree %}
{{ render_row(node, 0) }}
{% endfor %}
</tbody>
</table>
{% else %}
<!-- Empty State -->
<div class="text-center py-5">
<div class="empty-state-soft">
<div class="mb-3">
<i class="bi bi-pin-map" style="font-size: 3rem; color: var(--text-secondary);"></i>
</div>
<h5 class="text-muted">Ingen lokationer endnu</h5>
<p class="text-muted small mb-3">Opret din første lokation ved at klikke på knappen nedenfor</p>
<a href="/app/locations/create" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-2"></i>Opret lokation
</a>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Pagination Section -->
{% if total_pages and total_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_number > 1 %}
<li class="page-item">
<a class="page-link" href="?skip={{ (page_number - 2) * limit }}&limit={{ limit }}{% if location_type %}&location_type={{ location_type }}{% endif %}{% if is_active is not none %}&is_active={{ is_active }}{% endif %}">
<i class="bi bi-chevron-left"></i> Forrige
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link"><i class="bi bi-chevron-left"></i> Forrige</span>
</li>
{% endif %}
{% for page_num in range(1, total_pages + 1) %}
{% if page_num == page_number %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% elif page_num == 1 or page_num == total_pages or (page_num >= page_number - 1 and page_num <= page_number + 1) %}
<li class="page-item">
<a class="page-link" href="?skip={{ (page_num - 1) * limit }}&limit={{ limit }}{% if location_type %}&location_type={{ location_type }}{% endif %}{% if is_active is not none %}&is_active={{ is_active }}{% endif %}">
{{ page_num }}
</a>
</li>
{% elif page_num == page_number - 2 or page_num == page_number + 2 %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if page_number < total_pages %}
<li class="page-item">
<a class="page-link" href="?skip={{ page_number * limit }}&limit={{ limit }}{% if location_type %}&location_type={{ location_type }}{% endif %}{% if is_active is not none %}&is_active={{ is_active }}{% endif %}">
Næste <i class="bi bi-chevron-right"></i>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Næste <i class="bi bi-chevron-right"></i></span>
</li>
{% endif %}
</ul>
</nav>
<div class="text-center text-muted small mt-3">
Side {{ page_number }} af {{ total_pages }}
</div>
{% endif %}
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-bottom-0">
<h5 class="modal-title">Slet lokation?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button>
</div>
<div class="modal-body">
<p class="mb-0">Er du sikker på, at du vil slette <strong id="deleteLocationName"></strong>?</p>
<p class="text-muted small mt-2">Denne handling kan ikke fortrydes. Lokationen vil blive soft-deleted og kan gendannes af en administrator.</p>
</div>
<div class="modal-footer border-top-0">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-danger btn-sm" id="confirmDeleteBtn">
<i class="bi bi-trash me-2"></i>Slet
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('locationSearch');
const visibleCount = document.getElementById('visibleCount');
const statTotal = document.getElementById('statTotal');
const statActive = document.getElementById('statActive');
const statInactive = document.getElementById('statInactive');
const statVisible = document.getElementById('statVisible');
const rows = Array.from(document.querySelectorAll('.location-row'));
const rowById = new Map();
const parentById = new Map();
const childrenById = new Map();
function updateSummaryStats() {
const total = rows.length;
const active = rows.filter(row => {
const statusText = row.querySelector('td:nth-child(5)')?.innerText?.toLowerCase() || '';
return statusText.includes('aktiv') && !statusText.includes('inaktiv');
}).length;
const inactive = Math.max(total - active, 0);
const visible = rows.filter(row => !row.classList.contains('d-none')).length;
if (statTotal) statTotal.textContent = String(total);
if (statActive) statActive.textContent = String(active);
if (statInactive) statInactive.textContent = String(inactive);
if (statVisible) statVisible.textContent = String(visible);
if (visibleCount) visibleCount.textContent = String(visible);
}
rows.forEach(row => {
const id = row.getAttribute('data-location-id');
const parentId = row.getAttribute('data-parent-id') || null;
if (id) {
rowById.set(id, row);
parentById.set(id, parentId);
if (parentId) {
const list = childrenById.get(parentId) || [];
list.push(id);
childrenById.set(parentId, list);
}
}
});
function updateToggleIcon(row, expanded) {
const icon = row.querySelector('.toggle-row i');
if (!icon) return;
icon.className = expanded ? 'bi bi-caret-down-fill' : 'bi bi-caret-right-fill';
const btn = row.querySelector('.toggle-row');
if (btn) {
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
}
}
function hideDescendants(rootId) {
const stack = (childrenById.get(rootId) || []).slice();
while (stack.length) {
const childId = stack.pop();
const childRow = rowById.get(childId);
if (childRow) {
childRow.classList.add('d-none');
updateToggleIcon(childRow, false);
}
const grandchildren = childrenById.get(childId) || [];
stack.push(...grandchildren);
}
}
function showDescendants(rootId) {
const stack = (childrenById.get(rootId) || []).slice();
while (stack.length) {
const childId = stack.pop();
const childRow = rowById.get(childId);
if (childRow) {
childRow.classList.remove('d-none');
}
const grandchildren = childrenById.get(childId) || [];
stack.push(...grandchildren);
}
}
function collapseAll() {
rows.forEach(row => {
const depth = parseInt(row.getAttribute('data-depth') || '0', 10);
if (depth > 0) {
row.classList.add('d-none');
}
updateToggleIcon(row, false);
});
}
function showAncestors(id) {
let current = parentById.get(id);
while (current) {
const row = rowById.get(current);
if (row) {
row.classList.remove('d-none');
updateToggleIcon(row, true);
}
current = parentById.get(current);
}
}
collapseAll();
updateSummaryStats();
function toggleNode(targetId) {
const row = rowById.get(targetId);
if (!row) return;
const btn = row.querySelector('.toggle-row');
const expanded = btn?.getAttribute('aria-expanded') === 'true';
if (expanded) {
hideDescendants(targetId);
updateToggleIcon(row, false);
} else {
showDescendants(targetId);
updateToggleIcon(row, true);
}
}
document.querySelectorAll('.toggle-row').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const targetId = this.getAttribute('data-target-id');
toggleNode(targetId);
});
});
document.querySelectorAll('.location-row.has-children').forEach(row => {
row.addEventListener('click', function(e) {
const target = e.target;
if (target.closest('a') || target.closest('button') || target.closest('input')) {
return;
}
const targetId = row.getAttribute('data-location-id');
toggleNode(targetId);
});
});
function applySearchFilter() {
const query = (searchInput?.value || '').trim().toLowerCase();
let visible = 0;
if (!query) {
collapseAll();
}
rows.forEach(row => {
const name = row.querySelector('td:nth-child(2)')?.innerText || '';
const city = row.querySelector('td:nth-child(4)')?.innerText || '';
const haystack = `${name} ${city}`.toLowerCase();
if (!query) {
if (!row.classList.contains('d-none')) {
visible += 1;
}
return;
}
if (haystack.includes(query)) {
row.classList.remove('d-none');
showAncestors(row.getAttribute('data-location-id'));
visible += 1;
} else {
row.classList.add('d-none');
}
});
if (visibleCount) {
visibleCount.textContent = String(visible);
}
if (statVisible) {
statVisible.textContent = String(visible);
}
}
if (searchInput) {
let debounceTimer;
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(applySearchFilter, 150);
});
document.addEventListener('keydown', function(e) {
if (e.key === '/' && document.activeElement !== searchInput) {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
});
}
// Select all checkbox
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const locationCheckboxes = document.querySelectorAll('.location-checkbox');
const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
let currentDeleteId = null;
// Select all functionality
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
locationCheckboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBulkDeleteButton();
});
}
// Individual checkbox functionality
locationCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateBulkDeleteButton();
updateSelectAllCheckbox();
});
});
function updateBulkDeleteButton() {
const selectedCount = document.querySelectorAll('.location-checkbox:checked').length;
bulkDeleteBtn.disabled = selectedCount === 0;
if (selectedCount > 0) {
bulkDeleteBtn.innerHTML = `<i class="bi bi-trash me-2"></i>Slet valgte (${selectedCount})`;
}
}
function updateSelectAllCheckbox() {
if (!selectAllCheckbox) return;
const allChecked = Array.from(locationCheckboxes).every(cb => cb.checked);
const someChecked = Array.from(locationCheckboxes).some(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = someChecked && !allChecked;
}
// Delete individual location
document.querySelectorAll('.delete-location-btn').forEach(btn => {
btn.addEventListener('click', function() {
currentDeleteId = this.getAttribute('data-location-id');
const locationName = this.getAttribute('data-location-name');
document.getElementById('deleteLocationName').textContent = locationName;
deleteModal.show();
});
});
document.getElementById('confirmDeleteBtn').addEventListener('click', function() {
if (currentDeleteId) {
fetch(`/api/v1/locations/${currentDeleteId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(async response => {
if (response.ok) {
deleteModal.hide();
setTimeout(() => location.reload(), 300);
} else {
const err = await response.json().catch(() => ({}));
if (response.status === 404) {
alert(err.detail || 'Lokationen er allerede slettet. Siden opdateres.');
setTimeout(() => location.reload(), 300);
return;
}
alert(err.detail || 'Fejl ved sletning af lokation');
}
})
.catch(error => {
console.error('Error:', error);
alert('Fejl ved sletning af lokation');
});
}
});
// Bulk delete
if (bulkDeleteBtn) {
bulkDeleteBtn.addEventListener('click', function() {
const selectedIds = Array.from(locationCheckboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
if (selectedIds.length === 0) return;
if (confirm(`Slet ${selectedIds.length} lokation(er)? Dette kan ikke fortrydes.`)) {
Promise.all(selectedIds.map(id =>
fetch(`/api/v1/locations/${id}`, { method: 'DELETE' })
))
.then(() => location.reload())
.catch(error => {
console.error('Error:', error);
alert('Fejl ved sletning af lokationer');
});
}
});
}
});
</script>
{% endblock %}