bmc_hub/app/ticket/frontend/ticket_detail.html
Christian 3806c7d011 feat(ticket-module): Implement ticket system with comprehensive database schema, permissions, and testing suite
- 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.
2025-12-15 23:40:23 +01:00

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 %}