bmc_hub/app/modules/sag/templates/index.html
Christian d5dd958bf9 Refactor Sager module templates and functionality
- Updated index.html to extend base template and improve structure.
- Added new styles and search/filter functionality in the Sager list view.
- Created a backup of the old index.html as index_old.html.
- Updated navigation links in base.html for consistency.
- Included new dashboard API router in main.py.
- Added test scripts for customer and sag queries to validate database interactions.
2026-02-01 11:58:44 +01:00

470 lines
15 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: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.sag-table {
width: 100%;
margin: 0;
}
.sag-table thead {
background: var(--accent);
color: white;
}
.sag-table thead th {
padding: 0.8rem 1rem;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
border: none;
}
.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.6rem 1rem;
vertical-align: middle;
font-size: 0.9rem;
}
.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: 1400px; 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>
<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>
<!-- 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="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>
<!-- Table -->
<div class="table-wrapper">
{% if sager %}
<table class="sag-table">
<thead>
<tr>
<th style="width: 90px;">ID</th>
<th>Titel & Beskrivelse</th>
<th style="width: 180px;">Kunde</th>
<th style="width: 150px;">Hovedkontakt</th>
<th style="width: 100px;">Status</th>
<th style="width: 120px;">Oprettet</th>
<th style="width: 120px;">Opdateret</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 }}">
<td>
{% if has_relations %}
<span class="tree-toggle" onclick="toggleTreeNode(event, {{ sag.id }})">+</span>
{% endif %}
<span class="sag-id">#{{ sag.id }}</span>
</td>
<td 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 }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
{{ sag.customer_name if sag.customer_name else '-' }}
</td>
<td 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 onclick="window.location.href='/sag/{{ sag.id }}'">
<span class="status-badge status-{{ sag.status }}">{{ sag.status }}</span>
</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.updated_at.strftime('%d/%m-%Y') if sag.updated_at 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 }}" style="display: none;">
<td>
<span class="sag-id">#{{ related_sag.id }}</span>
</td>
<td 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 }}'" style="color: var(--text-secondary); font-size: 0.85rem;">
{{ related_sag.customer_name if related_sag.customer_name else '-' }}
</td>
<td 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 onclick="window.location.href='/sag/{{ related_sag.id }}'">
<span class="status-badge status-{{ related_sag.status }}">{{ related_sag.status }}</span>
</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.updated_at.strftime('%d/%m-%Y') if related_sag.updated_at 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';
function applyFilters() {
const search = currentSearch;
allRows.forEach(row => {
const text = row.textContent.toLowerCase();
const status = row.dataset.status;
const matchesSearch = text.includes(search);
const matchesFilter = currentFilter === 'all' || status === currentFilter;
const visible = matchesSearch && matchesFilter;
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 childMatchesSearch = childText.includes(search);
const childMatchesFilter = currentFilter === 'all' || childStatus === currentFilter;
const childVisible = visible && row.classList.contains('expanded') && childMatchesSearch && childMatchesFilter;
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();
});
});
</script>
{% endblock %}