2026-02-08 01:45:00 +01:00
|
|
|
{% extends "shared/frontend/base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block title %}Arkiverede Tickets - BMC Hub{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block extra_css %}
|
|
|
|
|
<style>
|
|
|
|
|
.filter-bar {
|
|
|
|
|
background: var(--bg-card);
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
border-radius: var(--border-radius);
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
box-shadow: 0 2px 15px rgba(0,0,0,0.05);
|
|
|
|
|
border: 1px solid var(--accent-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
align-items: end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ticket-table th {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
border-bottom: 2px solid var(--accent-light);
|
|
|
|
|
padding: 1rem 0.75rem;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ticket-table td {
|
|
|
|
|
padding: 1rem 0.75rem;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
border-bottom: 1px solid var(--accent-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ticket-row {
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ticket-row:hover {
|
|
|
|
|
background-color: var(--accent-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge {
|
|
|
|
|
padding: 0.4rem 0.8rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ticket-number {
|
|
|
|
|
font-family: 'Monaco', 'Courier New', monospace;
|
|
|
|
|
background: var(--accent-light);
|
|
|
|
|
padding: 0.2rem 0.5rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-box {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-box input {
|
|
|
|
|
padding-left: 2.5rem;
|
|
|
|
|
background: var(--bg-body);
|
|
|
|
|
border: 1px solid var(--accent-light);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-box i {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 1rem;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-label {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.4px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
margin-bottom: 0.35rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-chip {
|
|
|
|
|
background: var(--accent-light);
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
padding: 0.2rem 0.55rem;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
margin-left: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 4rem 2rem;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state i {
|
|
|
|
|
font-size: 4rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<div class="container-fluid px-4">
|
|
|
|
|
<!-- Page Header -->
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="mb-2">
|
|
|
|
|
<i class="bi bi-archive"></i> Arkiverede Tickets
|
|
|
|
|
</h1>
|
|
|
|
|
<p class="text-muted">Historiske tickets importeret fra Simply-CRM</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Filter Bar -->
|
|
|
|
|
<div class="filter-bar">
|
|
|
|
|
<form method="get" action="/ticket/archived" id="archivedFilters">
|
|
|
|
|
<div class="filter-grid">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="filter-label">Søg</div>
|
|
|
|
|
<div class="search-box">
|
|
|
|
|
<i class="bi bi-search"></i>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
name="search"
|
|
|
|
|
id="search"
|
|
|
|
|
class="form-control"
|
2026-02-17 08:29:05 +01:00
|
|
|
placeholder="Ticket nr, titel, løsning eller kommentar..."
|
2026-02-08 01:45:00 +01:00
|
|
|
value="{{ search_query or '' }}">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="filter-label">Organisation</div>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
name="organization"
|
|
|
|
|
id="organization"
|
|
|
|
|
class="form-control"
|
|
|
|
|
placeholder="fx Norva24"
|
|
|
|
|
value="{{ organization_query or '' }}">
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="filter-label">Kontakt</div>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
name="contact"
|
|
|
|
|
id="contact"
|
|
|
|
|
class="form-control"
|
|
|
|
|
placeholder="fx Kennie"
|
|
|
|
|
value="{{ contact_query or '' }}">
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="filter-label">Dato fra</div>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
name="date_from"
|
|
|
|
|
id="date_from"
|
|
|
|
|
class="form-control"
|
|
|
|
|
value="{{ date_from or '' }}">
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="filter-label">Dato til</div>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
name="date_to"
|
|
|
|
|
id="date_to"
|
|
|
|
|
class="form-control"
|
|
|
|
|
value="{{ date_to or '' }}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="filter-actions">
|
|
|
|
|
<button class="btn btn-primary" type="submit">
|
|
|
|
|
<i class="bi bi-funnel"></i> Filtrer
|
|
|
|
|
</button>
|
|
|
|
|
<a class="btn btn-outline-secondary" href="/ticket/archived">
|
|
|
|
|
<i class="bi bi-x-circle"></i> Nulstil
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Tickets Table -->
|
|
|
|
|
<div id="archivedResults">
|
|
|
|
|
{% if tickets %}
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table ticket-table mb-0">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Ticket</th>
|
|
|
|
|
<th>Organisation</th>
|
|
|
|
|
<th>Kontakt</th>
|
|
|
|
|
<th>Email From</th>
|
2026-02-17 08:29:05 +01:00
|
|
|
<th>Løsning</th>
|
|
|
|
|
<th>Kommentarer</th>
|
2026-02-08 01:45:00 +01:00
|
|
|
<th>Tid brugt</th>
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
<th>Oprettet</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{% for ticket in tickets %}
|
|
|
|
|
<tr class="ticket-row" onclick="window.location='/ticket/archived/{{ ticket.id }}'">
|
|
|
|
|
<td>
|
|
|
|
|
{% if ticket.ticket_number %}
|
|
|
|
|
<span class="ticket-number">{{ ticket.ticket_number }}</span>
|
|
|
|
|
<br>
|
|
|
|
|
{% endif %}
|
|
|
|
|
<strong>{{ ticket.title or '-' }}</strong>
|
|
|
|
|
</td>
|
|
|
|
|
<td>{{ ticket.organization_name or '-' }}</td>
|
|
|
|
|
<td>{{ ticket.contact_name or '-' }}</td>
|
|
|
|
|
<td>{{ ticket.email_from or '-' }}</td>
|
2026-02-17 08:29:05 +01:00
|
|
|
<td>
|
|
|
|
|
{% if ticket.solution and ticket.solution.strip() %}
|
|
|
|
|
{{ ticket.solution[:120] }}{% if ticket.solution|length > 120 %}...{% endif %}
|
|
|
|
|
{% else %}
|
|
|
|
|
<span class="text-muted">-</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<span class="badge" style="background: var(--accent-light); color: var(--accent);">
|
|
|
|
|
{{ ticket.message_count or 0 }}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
2026-02-08 01:45:00 +01:00
|
|
|
<td>
|
|
|
|
|
{% if ticket.time_spent_hours is not none %}
|
|
|
|
|
{{ '%.2f'|format(ticket.time_spent_hours) }} t
|
|
|
|
|
{% else %}
|
|
|
|
|
-
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
{% if ticket.status %}
|
|
|
|
|
<span class="badge" style="background: var(--accent-light); color: var(--accent);">
|
|
|
|
|
{{ ticket.status.replace('_', ' ').title() }}
|
|
|
|
|
</span>
|
|
|
|
|
{% else %}
|
|
|
|
|
<span class="text-muted">-</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
{% if ticket.source_created_at %}
|
|
|
|
|
{{ ticket.source_created_at.strftime('%Y-%m-%d') }}
|
|
|
|
|
{% else %}
|
|
|
|
|
-
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% else %}
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
<i class="bi bi-archive"></i>
|
|
|
|
|
<h4>Ingen arkiverede tickets fundet</h4>
|
|
|
|
|
<p>Prøv at justere din søgning eller importer data fra Simply-CRM.</p>
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block extra_js %}
|
|
|
|
|
<script>
|
|
|
|
|
const filterForm = document.getElementById('archivedFilters');
|
|
|
|
|
const debounceMs = 500;
|
|
|
|
|
let debounceTimer;
|
|
|
|
|
|
|
|
|
|
async function fetchResults() {
|
|
|
|
|
if (!filterForm) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const formData = new FormData(filterForm);
|
|
|
|
|
const params = new URLSearchParams(formData);
|
|
|
|
|
const url = `/ticket/archived?${params.toString()}`;
|
|
|
|
|
|
|
|
|
|
history.replaceState(null, '', url);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
|
|
|
});
|
|
|
|
|
if (response.redirected) {
|
|
|
|
|
window.location.href = response.url;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
filterForm.submit();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const html = await response.text();
|
|
|
|
|
const parser = new DOMParser();
|
|
|
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
|
|
|
const newResults = doc.getElementById('archivedResults');
|
|
|
|
|
const currentResults = document.getElementById('archivedResults');
|
|
|
|
|
if (newResults && currentResults) {
|
|
|
|
|
currentResults.replaceWith(newResults);
|
|
|
|
|
} else {
|
|
|
|
|
filterForm.submit();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
filterForm.submit();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function submitFilters() {
|
|
|
|
|
fetchResults();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function debounceSubmit() {
|
|
|
|
|
clearTimeout(debounceTimer);
|
|
|
|
|
debounceTimer = setTimeout(submitFilters, debounceMs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const inputs = [
|
|
|
|
|
document.getElementById('search'),
|
|
|
|
|
document.getElementById('organization'),
|
|
|
|
|
document.getElementById('contact'),
|
|
|
|
|
document.getElementById('date_from'),
|
|
|
|
|
document.getElementById('date_to')
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
inputs.forEach((input) => {
|
|
|
|
|
if (!input) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (input.type === 'date') {
|
|
|
|
|
input.addEventListener('change', debounceSubmit);
|
|
|
|
|
} else {
|
|
|
|
|
input.addEventListener('input', debounceSubmit);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
{% endblock %}
|