bmc_hub/app/ticket/frontend/archived_ticket_list.html
Christian e4b9091a1b feat: Implement fixed-price agreements frontend views and related templates
- Added views for listing fixed-price agreements, displaying agreement details, and a reporting dashboard.
- Created HTML templates for listing, detailing, and reporting on fixed-price agreements.
- Introduced API endpoint to fetch active customers for agreement creation.
- Added migration scripts for creating necessary database tables and views for fixed-price agreements, billing periods, and reporting.
- Implemented triggers for auto-generating agreement numbers and updating timestamps.
- Enhanced ticket management with archived ticket views and filtering capabilities.
2026-02-08 01:45:00 +01:00

342 lines
12 KiB
HTML

{% 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"
placeholder="Ticket nr, titel eller beskrivelse..."
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>
<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>
<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 %}