- Added migration 025 for the Ticket System, creating tables for tickets, comments, attachments, worklogs, prepaid cards, and audit logs. - Introduced migration 026 to add ticket-related permissions to the auth system and assign them to user groups. - Developed a test suite for the Ticket Module, validating database schema, ticket number generation, prepaid card constraints, service logic, worklog creation, audit logging, and views.
273 lines
11 KiB
HTML
273 lines
11 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Alle 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);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.badge-status-open { background-color: #d1ecf1; color: #0c5460; }
|
|
.badge-status-in_progress { background-color: #fff3cd; color: #856404; }
|
|
.badge-status-pending_customer { background-color: #e2e3e5; color: #383d41; }
|
|
.badge-status-resolved { background-color: #d4edda; color: #155724; }
|
|
.badge-status-closed { background-color: #f8d7da; color: #721c24; }
|
|
|
|
.badge-priority-low { background-color: var(--accent-light); color: var(--accent); }
|
|
.badge-priority-normal { background-color: #e2e3e5; color: #383d41; }
|
|
.badge-priority-high { background-color: #fff3cd; color: #856404; }
|
|
.badge-priority-urgent, .badge-priority-critical { background-color: #f8d7da; color: #721c24; }
|
|
|
|
.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);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 4rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.meta-info {
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.meta-info i {
|
|
margin-right: 0.25rem;
|
|
}
|
|
</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-ticket-detailed"></i> Alle Tickets
|
|
</h1>
|
|
<p class="text-muted">Oversigt over alle tickets i systemet</p>
|
|
</div>
|
|
<a href="/api/v1/tickets" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle"></i> Ny Ticket
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Filter Bar -->
|
|
<div class="filter-bar">
|
|
<form method="get" action="/ticket/tickets" class="row g-3">
|
|
<div class="col-md-3">
|
|
<label for="status" class="form-label">Status:</label>
|
|
<select name="status" id="status" class="form-select" onchange="this.form.submit()">
|
|
<option value="">Alle</option>
|
|
<option value="open" {% if selected_status == 'open' %}selected{% endif %}>Open</option>
|
|
<option value="in_progress" {% if selected_status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
|
<option value="pending_customer" {% if selected_status == 'pending_customer' %}selected{% endif %}>Pending Customer</option>
|
|
<option value="resolved" {% if selected_status == 'resolved' %}selected{% endif %}>Resolved</option>
|
|
<option value="closed" {% if selected_status == 'closed' %}selected{% endif %}>Closed</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="priority" class="form-label">Prioritet:</label>
|
|
<select name="priority" id="priority" class="form-select" onchange="this.form.submit()">
|
|
<option value="">Alle</option>
|
|
<option value="low" {% if selected_priority == 'low' %}selected{% endif %}>Low</option>
|
|
<option value="normal" {% if selected_priority == 'normal' %}selected{% endif %}>Normal</option>
|
|
<option value="high" {% if selected_priority == 'high' %}selected{% endif %}>High</option>
|
|
<option value="urgent" {% if selected_priority == 'urgent' %}selected{% endif %}>Urgent</option>
|
|
<option value="critical" {% if selected_priority == 'critical' %}selected{% endif %}>Critical</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="customer_id" class="form-label">Kunde:</label>
|
|
<select name="customer_id" id="customer_id" class="form-select" onchange="this.form.submit()">
|
|
<option value="">Alle kunder</option>
|
|
{% for customer in customers %}
|
|
<option value="{{ customer.id }}" {% if customer.id == selected_customer_id %}selected{% endif %}>
|
|
{{ customer.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="search" class="form-label">Søg:</label>
|
|
<div class="search-box">
|
|
<i class="bi bi-search"></i>
|
|
<input
|
|
type="text"
|
|
name="search"
|
|
id="search"
|
|
class="form-control"
|
|
placeholder="Ticket nummer eller emne..."
|
|
value="{{ search_query or '' }}">
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Tickets Table -->
|
|
{% if tickets %}
|
|
<div class="card">
|
|
<div class="table-responsive">
|
|
<table class="table ticket-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Ticket</th>
|
|
<th>Kunde</th>
|
|
<th>Status</th>
|
|
<th>Prioritet</th>
|
|
<th>Tildelt</th>
|
|
<th>Aktivitet</th>
|
|
<th>Oprettet</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for ticket in tickets %}
|
|
<tr class="ticket-row" onclick="window.location='/ticket/tickets/{{ ticket.id }}'">
|
|
<td>
|
|
<span class="ticket-number">{{ ticket.ticket_number }}</span>
|
|
<br>
|
|
<strong>{{ ticket.subject }}</strong>
|
|
</td>
|
|
<td>
|
|
{% if ticket.customer_name %}
|
|
{{ ticket.customer_name }}
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-status-{{ ticket.status }}">
|
|
{{ ticket.status.replace('_', ' ').title() }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-priority-{{ ticket.priority }}">
|
|
{{ ticket.priority.title() }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
{% if ticket.assigned_to_name %}
|
|
{{ ticket.assigned_to_name }}
|
|
{% else %}
|
|
<span class="text-muted">Ikke tildelt</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="meta-info">
|
|
<i class="bi bi-chat"></i> {{ ticket.comment_count }}
|
|
<i class="bi bi-clock ms-2"></i> {{ ticket.worklog_count }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{{ ticket.created_at.strftime('%d-%m-%Y') if ticket.created_at else '-' }}
|
|
<br>
|
|
<small class="text-muted">{{ ticket.created_at.strftime('%H:%M') if ticket.created_at else '' }}</small>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="text-muted mt-3">
|
|
<small>Viser {{ tickets|length }} tickets</small>
|
|
</div>
|
|
{% else %}
|
|
<div class="card">
|
|
<div class="empty-state">
|
|
<i class="bi bi-inbox"></i>
|
|
<h3>Ingen tickets fundet</h3>
|
|
<p>Prøv at justere dine filtre eller opret en ny ticket</p>
|
|
{% if selected_status or selected_priority or selected_customer_id or search_query %}
|
|
<a href="/ticket/tickets" class="btn btn-outline-secondary mt-3">
|
|
<i class="bi bi-x-circle"></i> Ryd filtre
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// Submit search on Enter
|
|
document.getElementById('search').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
this.form.submit();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|