- 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.
410 lines
16 KiB
HTML
410 lines
16 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}{{ ticket.ticket_number }} - BMC Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.ticket-header {
|
|
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%);
|
|
padding: 2rem;
|
|
border-radius: var(--border-radius);
|
|
color: white;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.ticket-number {
|
|
font-family: 'Monaco', 'Courier New', monospace;
|
|
font-size: 1rem;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.ticket-title {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.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; }
|
|
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.info-item {
|
|
background: var(--bg-card);
|
|
padding: 1rem;
|
|
border-radius: var(--border-radius);
|
|
border: 1px solid var(--accent-light);
|
|
}
|
|
|
|
.info-item label {
|
|
display: block;
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.info-item .value {
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.comment {
|
|
padding: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
background: var(--accent-light);
|
|
border-radius: var(--border-radius);
|
|
border-left: 4px solid var(--accent);
|
|
}
|
|
|
|
.comment.internal {
|
|
background: #fff3cd;
|
|
border-left-color: #ffc107;
|
|
}
|
|
|
|
.comment-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.comment-author {
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.comment-date {
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.worklog-table th {
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
border-bottom: 2px solid var(--accent-light);
|
|
padding: 0.75rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.worklog-table td {
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid var(--accent-light);
|
|
}
|
|
|
|
.attachment {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.5rem 1rem;
|
|
background: var(--accent-light);
|
|
border-radius: var(--border-radius);
|
|
margin-right: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
text-decoration: none;
|
|
color: var(--accent);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.attachment:hover {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.attachment i {
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 2rem;
|
|
margin-bottom: 0.5rem;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.description-box {
|
|
background: var(--bg-body);
|
|
padding: 1.5rem;
|
|
border-radius: var(--border-radius);
|
|
white-space: pre-wrap;
|
|
line-height: 1.6;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid px-4">
|
|
<!-- Ticket Header -->
|
|
<div class="ticket-header">
|
|
<div class="ticket-number">{{ ticket.ticket_number }}</div>
|
|
<div class="ticket-title">{{ ticket.subject }}</div>
|
|
<div class="mt-3">
|
|
<span class="badge badge-status-{{ ticket.status }}">
|
|
{{ ticket.status.replace('_', ' ').title() }}
|
|
</span>
|
|
<span class="badge badge-priority-{{ ticket.priority }}">
|
|
{{ ticket.priority.title() }} Priority
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="action-buttons mb-4">
|
|
<a href="/api/v1/tickets/{{ ticket.id }}" class="btn btn-outline-primary">
|
|
<i class="bi bi-pencil"></i> Rediger
|
|
</a>
|
|
<button class="btn btn-outline-secondary" onclick="addComment()">
|
|
<i class="bi bi-chat"></i> Tilføj Kommentar
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="addWorklog()">
|
|
<i class="bi bi-clock"></i> Log Tid
|
|
</button>
|
|
<a href="/ticket/tickets" class="btn btn-outline-secondary">
|
|
<i class="bi bi-arrow-left"></i> Tilbage
|
|
</a>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Main Content -->
|
|
<div class="col-lg-8">
|
|
<!-- Description -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="section-title">
|
|
<i class="bi bi-file-text"></i> Beskrivelse
|
|
</div>
|
|
<div class="description-box">
|
|
{{ ticket.description or 'Ingen beskrivelse' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comments -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="section-title">
|
|
<i class="bi bi-chat-dots"></i> Kommentarer ({{ comments|length }})
|
|
</div>
|
|
{% if comments %}
|
|
{% for comment in comments %}
|
|
<div class="comment {% if comment.internal_note %}internal{% endif %}">
|
|
<div class="comment-header">
|
|
<span class="comment-author">
|
|
<i class="bi bi-person-circle"></i>
|
|
{{ comment.user_name or 'System' }}
|
|
{% if comment.internal_note %}
|
|
<span class="badge bg-warning text-dark ms-2">Internal</span>
|
|
{% endif %}
|
|
</span>
|
|
<span class="comment-date">
|
|
{{ comment.created_at.strftime('%d-%m-%Y %H:%M') if comment.created_at else '-' }}
|
|
</span>
|
|
</div>
|
|
<div class="comment-text">
|
|
{{ comment.comment_text }}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<i class="bi bi-chat"></i>
|
|
<p>Ingen kommentarer endnu</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Worklog -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="section-title">
|
|
<i class="bi bi-clock-history"></i> Worklog ({{ worklog|length }})
|
|
</div>
|
|
{% if worklog %}
|
|
<div class="table-responsive">
|
|
<table class="table worklog-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Dato</th>
|
|
<th>Timer</th>
|
|
<th>Type</th>
|
|
<th>Beskrivelse</th>
|
|
<th>Status</th>
|
|
<th>Medarbejder</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for entry in worklog %}
|
|
<tr>
|
|
<td>{{ entry.work_date.strftime('%d-%m-%Y') if entry.work_date else '-' }}</td>
|
|
<td><strong>{{ "%.2f"|format(entry.hours) }}t</strong></td>
|
|
<td>{{ entry.work_type }}</td>
|
|
<td>{{ entry.description or '-' }}</td>
|
|
<td>
|
|
<span class="badge {% if entry.status == 'billable' %}bg-success{% elif entry.status == 'draft' %}bg-warning{% else %}bg-secondary{% endif %}">
|
|
{{ entry.status }}
|
|
</span>
|
|
</td>
|
|
<td>{{ entry.user_name or '-' }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<i class="bi bi-clock"></i>
|
|
<p>Ingen worklog entries endnu</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Attachments -->
|
|
{% if attachments %}
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="section-title">
|
|
<i class="bi bi-paperclip"></i> Vedhæftninger ({{ attachments|length }})
|
|
</div>
|
|
{% for attachment in attachments %}
|
|
<a href="/api/v1/attachments/{{ attachment.id }}/download" class="attachment">
|
|
<i class="bi bi-file-earmark"></i>
|
|
{{ attachment.filename }}
|
|
<small class="ms-2">({{ (attachment.file_size / 1024)|round(1) }} KB)</small>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Ticket Info -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="section-title">
|
|
<i class="bi bi-info-circle"></i> Ticket Information
|
|
</div>
|
|
<div class="info-item mb-3">
|
|
<label>Kunde</label>
|
|
<div class="value">
|
|
{% if ticket.customer_name %}
|
|
<a href="/customers/{{ ticket.customer_id }}" style="text-decoration: none; color: var(--accent);">
|
|
{{ ticket.customer_name }}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-muted">Ikke angivet</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="info-item mb-3">
|
|
<label>Tildelt til</label>
|
|
<div class="value">
|
|
{{ ticket.assigned_to_name or 'Ikke tildelt' }}
|
|
</div>
|
|
</div>
|
|
<div class="info-item mb-3">
|
|
<label>Oprettet</label>
|
|
<div class="value">
|
|
{{ ticket.created_at.strftime('%d-%m-%Y %H:%M') if ticket.created_at else '-' }}
|
|
</div>
|
|
</div>
|
|
<div class="info-item mb-3">
|
|
<label>Senest opdateret</label>
|
|
<div class="value">
|
|
{{ ticket.updated_at.strftime('%d-%m-%Y %H:%M') if ticket.updated_at else '-' }}
|
|
</div>
|
|
</div>
|
|
{% if ticket.resolved_at %}
|
|
<div class="info-item mb-3">
|
|
<label>Løst</label>
|
|
<div class="value">
|
|
{{ ticket.resolved_at.strftime('%d-%m-%Y %H:%M') }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% if ticket.first_response_at %}
|
|
<div class="info-item mb-3">
|
|
<label>Første svar</label>
|
|
<div class="value">
|
|
{{ ticket.first_response_at.strftime('%d-%m-%Y %H:%M') }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
{% if ticket.tags %}
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="section-title">
|
|
<i class="bi bi-tags"></i> Tags
|
|
</div>
|
|
{% for tag in ticket.tags %}
|
|
<span class="badge bg-secondary me-1 mb-1">#{{ tag }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// Add comment (placeholder - integrate with API)
|
|
function addComment() {
|
|
alert('Add comment functionality - integrate with POST /api/v1/tickets/{{ ticket.id }}/comments');
|
|
}
|
|
|
|
// Add worklog (placeholder - integrate with API)
|
|
function addWorklog() {
|
|
alert('Add worklog functionality - integrate with POST /api/v1/tickets/{{ ticket.id }}/worklog');
|
|
}
|
|
</script>
|
|
{% endblock %}
|