251 lines
12 KiB
HTML
251 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ ticket.ticket_number }}{% endblock %}
|
|
{% block page_title %}{{ ticket.ticket_number }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row g-4">
|
|
<!-- Main column -->
|
|
<div class="col-lg-8">
|
|
|
|
<!-- Ticket header -->
|
|
<div class="ticket-header mb-4">
|
|
<div class="d-flex align-items-start justify-content-between gap-3 mb-3">
|
|
<h2 style="font-size:20px;font-weight:700;line-height:1.3;margin:0;">{{ ticket.title }}</h2>
|
|
<div class="d-flex gap-2 flex-shrink-0">
|
|
<span class="badge badge-{{ ticket.priority }}">{{ ticket.priority.upper() }}</span>
|
|
<span class="badge badge-{{ ticket.status }}">{{ ticket.status.replace('_',' ').upper() }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-3" style="font-size:12px;color:var(--muted);">
|
|
<span><i class="bi bi-hash me-1"></i><span class="mono">{{ ticket.ticket_number }}</span></span>
|
|
<span><i class="bi bi-person me-1"></i>{{ ticket.creator.full_name }}</span>
|
|
<span><i class="bi bi-tag me-1"></i>{{ ticket.category.replace('_',' ').title() }}</span>
|
|
<span><i class="bi bi-calendar3 me-1"></i>{{ ticket.created_at.strftime('%b %d, %Y %H:%M') }}</span>
|
|
{% if ticket.location %}<span><i class="bi bi-geo-alt me-1"></i>{{ ticket.location }}</span>{% endif %}
|
|
{% if ticket.asset_tag %}<span><i class="bi bi-cpu me-1"></i>{{ ticket.asset_tag }}</span>{% endif %}
|
|
{% if ticket.ai_generated %}<span style="color:var(--accent3);"><i class="bi bi-robot me-1"></i>AI-generated</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="card mb-4">
|
|
<div class="card-header"><i class="bi bi-file-text me-2"></i>Description</div>
|
|
<div class="card-body" style="white-space:pre-wrap;font-size:14px;line-height:1.7;">{{ ticket.description }}</div>
|
|
</div>
|
|
|
|
<!-- Attachments on ticket -->
|
|
{% set ticket_atts = ticket.attachments.filter_by(comment_id=None).all() %}
|
|
{% if ticket_atts %}
|
|
<div class="card mb-4">
|
|
<div class="card-header"><i class="bi bi-paperclip me-2"></i>Attachments</div>
|
|
<div class="card-body d-flex flex-wrap gap-2">
|
|
{% for att in ticket_atts %}
|
|
<a href="{{ url_for('tickets.download_attachment', att_id=att.id) }}"
|
|
class="btn btn-secondary btn-sm">
|
|
<i class="bi bi-download me-1"></i>{{ att.filename }}
|
|
<span style="font-size:10px;color:var(--muted);">({{ (att.file_size/1024)|int }}KB)</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Resolution notes (IT only, or if resolved) -->
|
|
{% if ticket.resolution_notes %}
|
|
<div class="card mb-4" style="border-color:var(--success);">
|
|
<div class="card-header" style="background:rgba(45,212,191,.08);color:var(--success);">
|
|
<i class="bi bi-check-circle me-2"></i>Resolution Notes
|
|
</div>
|
|
<div class="card-body" style="white-space:pre-wrap;font-size:14px;">{{ ticket.resolution_notes }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Comments -->
|
|
<div class="mb-3 d-flex align-items-center justify-content-between">
|
|
<h5 style="font-size:16px;font-weight:700;margin:0;">
|
|
<i class="bi bi-chat-dots me-2"></i>Comments
|
|
<span style="font-size:13px;color:var(--muted);">({{ comments|length }})</span>
|
|
</h5>
|
|
</div>
|
|
|
|
{% for comment in comments %}
|
|
<div class="comment-card {% if comment.is_internal %}internal{% endif %}" id="comment-{{ comment.id }}">
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div style="width:28px;height:28px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">
|
|
{{ comment.author.full_name[0].upper() }}
|
|
</div>
|
|
<span class="comment-author">{{ comment.author.full_name }}</span>
|
|
{% if comment.author.is_it_staff %}
|
|
<span style="font-size:10px;background:rgba(0,180,216,.15);color:var(--accent3);padding:1px 7px;border-radius:4px;font-weight:600;">IT STAFF</span>
|
|
{% endif %}
|
|
{% if comment.is_internal %}
|
|
<span style="font-size:10px;background:rgba(251,191,36,.15);color:var(--warning);padding:1px 7px;border-radius:4px;font-weight:600;">INTERNAL NOTE</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="comment-time">{{ comment.created_at.strftime('%b %d, %Y %H:%M') }}</span>
|
|
{% if current_user.is_it_staff or comment.author_id == current_user.id %}
|
|
<form method="POST" action="{{ url_for('tickets.delete_comment', comment_id=comment.id) }}"
|
|
onsubmit="return confirm('Delete this comment?');">
|
|
<button type="submit" class="btn btn-sm" style="background:none;border:none;color:var(--muted);padding:2px 6px;"
|
|
title="Delete comment">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="comment-body">{{ comment.body }}</div>
|
|
<!-- Comment attachments -->
|
|
{% set c_atts = comment.attachments.all() %}
|
|
{% if c_atts %}
|
|
<div class="mt-2 d-flex flex-wrap gap-2">
|
|
{% for att in c_atts %}
|
|
<a href="{{ url_for('tickets.download_attachment', att_id=att.id) }}" class="btn btn-secondary btn-sm">
|
|
<i class="bi bi-download me-1"></i>{{ att.filename }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="p-4 text-center" style="color:var(--muted);font-size:13px;">
|
|
<i class="bi bi-chat" style="font-size:28px;display:block;margin-bottom:8px;"></i>
|
|
No comments yet. Be the first to add an update.
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<!-- Add comment -->
|
|
{% if ticket.status not in ('closed',) %}
|
|
<div class="card mt-4">
|
|
<div class="card-header"><i class="bi bi-chat-plus me-2"></i>Add Comment</div>
|
|
<div class="card-body">
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="mb-3">
|
|
<textarea class="form-control" name="body" rows="4" required
|
|
placeholder="Add your update, follow-up, or response here…"></textarea>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Attachments</label>
|
|
<input type="file" class="form-control" name="attachments" multiple
|
|
accept=".png,.jpg,.jpeg,.gif,.pdf,.doc,.docx,.txt,.zip,.log"/>
|
|
</div>
|
|
{% if current_user.is_it_staff %}
|
|
<div class="mb-3 d-flex align-items-center gap-2">
|
|
<input type="checkbox" id="is_internal" name="is_internal" style="accent-color:var(--warning);"/>
|
|
<label for="is_internal" style="font-size:13px;color:var(--warning);margin:0;cursor:pointer;">
|
|
<i class="bi bi-lock me-1"></i>Internal note (IT staff only)
|
|
</label>
|
|
</div>
|
|
{% endif %}
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-send me-2"></i>Post Comment
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Sidebar column -->
|
|
<div class="col-lg-4">
|
|
|
|
<!-- IT Update Panel -->
|
|
{% if current_user.is_it_staff %}
|
|
<div class="card mb-3">
|
|
<div class="card-header"><i class="bi bi-pencil-square me-2"></i>Update Ticket</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="{{ url_for('tickets.update_ticket', ticket_id=ticket.id) }}">
|
|
<div class="mb-3">
|
|
<label class="form-label">Status</label>
|
|
<select class="form-select" name="status">
|
|
{% for s in statuses %}
|
|
<option value="{{ s }}" {% if s == ticket.status %}selected{% endif %}>{{ s.replace('_',' ').title() }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Priority</label>
|
|
<select class="form-select" name="priority">
|
|
{% for p in priorities %}
|
|
<option value="{{ p }}" {% if p == ticket.priority %}selected{% endif %}>{{ p.title() }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Assign To</label>
|
|
<select class="form-select" name="assigned_to_id">
|
|
<option value="">— Unassigned —</option>
|
|
{% for staff in it_staff %}
|
|
<option value="{{ staff.id }}" {% if ticket.assigned_to_id == staff.id %}selected{% endif %}>
|
|
{{ staff.full_name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Due Date</label>
|
|
<input type="date" class="form-control" name="due_date"
|
|
value="{{ ticket.due_date.strftime('%Y-%m-%d') if ticket.due_date else '' }}"/>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Resolution Notes</label>
|
|
<textarea class="form-control" name="resolution_notes" rows="3"
|
|
placeholder="Describe how the issue was resolved…">{{ ticket.resolution_notes or '' }}</textarea>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Internal Notes <span style="color:var(--warning);">(IT only)</span></label>
|
|
<textarea class="form-control" name="internal_notes" rows="2"
|
|
placeholder="Private notes for IT team…">{{ ticket.internal_notes or '' }}</textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-check2 me-2"></i>Save Changes
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Ticket Info -->
|
|
<div class="card mb-3">
|
|
<div class="card-header"><i class="bi bi-info-circle me-2"></i>Ticket Details</div>
|
|
<div class="card-body" style="font-size:13px;">
|
|
{% macro info_row(icon, label, value) %}
|
|
<div style="display:flex;gap:10px;padding:7px 0;border-bottom:1px solid var(--border);">
|
|
<i class="bi {{ icon }}" style="color:var(--muted);width:16px;text-align:center;margin-top:1px;flex-shrink:0;"></i>
|
|
<div style="color:var(--muted);">{{ label }}</div>
|
|
<div style="margin-left:auto;text-align:right;max-width:55%;">{{ value }}</div>
|
|
</div>
|
|
{% endmacro %}
|
|
{{ info_row('bi-hash', 'Number', '<span class="mono" style="font-size:11px;color:var(--accent3);">'~ticket.ticket_number~'</span>') }}
|
|
{{ info_row('bi-tag', 'Category', ticket.category.replace('_',' ').title()) }}
|
|
{{ info_row('bi-person', 'Submitted by', ticket.creator.full_name) }}
|
|
{{ info_row('bi-person-check', 'Assigned to', ticket.assignee.full_name if ticket.assignee else '—') }}
|
|
{{ info_row('bi-calendar3', 'Created', ticket.created_at.strftime('%b %d, %Y')) }}
|
|
{{ info_row('bi-calendar-check', 'Updated', ticket.updated_at.strftime('%b %d, %Y')) }}
|
|
{% if ticket.due_date %}{{ info_row('bi-alarm', 'Due Date', ticket.due_date.strftime('%b %d, %Y')) }}{% endif %}
|
|
{% if ticket.resolved_at %}{{ info_row('bi-check-circle', 'Resolved', ticket.resolved_at.strftime('%b %d, %Y')) }}{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- History -->
|
|
{% if history %}
|
|
<div class="card">
|
|
<div class="card-header"><i class="bi bi-clock-history me-2"></i>Change History</div>
|
|
<div class="card-body p-2">
|
|
{% for h in history %}
|
|
<div class="history-item">
|
|
<strong>{{ h.field_name.replace('_',' ').title() }}</strong>
|
|
changed from <em>{{ h.old_value or '—' }}</em> to <em>{{ h.new_value or '—' }}</em>
|
|
<br>
|
|
<span style="font-size:10px;">by {{ h.changer.full_name }} · {{ h.changed_at.strftime('%b %d %H:%M') }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|