Initial commit
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}New Ticket{% endblock %}
|
||||
{% block page_title %}Submit a New Ticket{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle me-2"></i>New Support Request
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Issue Title *</label>
|
||||
<input type="text" class="form-control" name="title" required
|
||||
placeholder="Brief description of the issue (e.g. 'Cannot connect to VPN')"/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Category *</label>
|
||||
<select class="form-select" name="category" required>
|
||||
{% for cat in categories %}
|
||||
<option value="{{ cat }}">{{ cat.replace('_',' ').title() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Priority *</label>
|
||||
<select class="form-select" name="priority" required>
|
||||
{% for p in priorities %}
|
||||
<option value="{{ p }}" {% if p == 'medium' %}selected{% endif %}>{{ p.upper() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Location / Floor</label>
|
||||
<input type="text" class="form-control" name="location" placeholder="e.g. 3rd Floor, Room 302"/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Asset Tag / Device Serial</label>
|
||||
<input type="text" class="form-control" name="asset_tag" placeholder="e.g. ASSET-1234"/>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Detailed Description *</label>
|
||||
<textarea class="form-control" name="description" rows="6" required
|
||||
placeholder="Please describe the issue in detail: - What were you trying to do? - What happened? - Any error messages? - When did it start?"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Attachments (screenshots, logs, etc.)</label>
|
||||
<input type="file" class="form-control" name="attachments" multiple
|
||||
accept=".png,.jpg,.jpeg,.gif,.pdf,.doc,.docx,.txt,.zip,.log"/>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:5px;">
|
||||
Accepted: PNG, JPG, PDF, DOC, DOCX, TXT, ZIP, LOG. Max 16 MB per file.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Priority guide -->
|
||||
<div class="col-12">
|
||||
<div style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:14px 16px;">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;">Priority Guide</div>
|
||||
<div class="row g-2" style="font-size:12px;">
|
||||
<div class="col-sm-3"><span class="badge badge-low me-1">LOW</span> Minor inconvenience, no work stoppage</div>
|
||||
<div class="col-sm-3"><span class="badge badge-medium me-1">MEDIUM</span> Impacting productivity, workaround available</div>
|
||||
<div class="col-sm-3"><span class="badge badge-high me-1">HIGH</span> Significant impact, no workaround</div>
|
||||
<div class="col-sm-3"><span class="badge badge-critical me-1">CRITICAL</span> Complete outage or security incident</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-send me-2"></i>Submit Ticket
|
||||
</button>
|
||||
<a href="{{ url_for('tickets.dashboard') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 d-none d-lg-block">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><i class="bi bi-robot me-2"></i>Prefer Chatting?</div>
|
||||
<div class="card-body">
|
||||
<p style="font-size:13px;color:var(--muted);">Let our AI assistant guide you through reporting your issue conversationally.</p>
|
||||
<button class="btn btn-outline-primary w-100" onclick="toggleChat()">
|
||||
<i class="bi bi-chat-dots me-2"></i>Open IT Assistant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-lightbulb me-2"></i>Tips</div>
|
||||
<div class="card-body" style="font-size:13px;color:var(--muted);">
|
||||
<p>✅ Include error messages exactly as shown</p>
|
||||
<p>✅ Mention what changed recently</p>
|
||||
<p>✅ List affected users or devices</p>
|
||||
<p>✅ Attach relevant screenshots or logs</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,125 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
{% block page_title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Stats Row -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background:rgba(96,165,250,.1);color:var(--info);">
|
||||
<i class="bi bi-ticket-detailed"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value">{{ open_count }}</div>
|
||||
<div class="stat-label">Open Tickets</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background:rgba(0,180,216,.1);color:var(--accent3);">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value">{{ active_count }}</div>
|
||||
<div class="stat-label">In Progress</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background:rgba(45,212,191,.1);color:var(--success);">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value">{{ resolved_count }}</div>
|
||||
<div class="stat-label">Resolved</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- My Tickets -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-ticket-detailed me-2"></i>My Recent Tickets</span>
|
||||
<a href="{{ url_for('tickets.create_ticket') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg me-1"></i>New Ticket
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if my_tickets %}
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket #</th>
|
||||
<th>Title</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in my_tickets %}
|
||||
<tr style="cursor:pointer;" onclick="window.location='/tickets/{{ t.id }}'">
|
||||
<td><span class="mono" style="font-size:12px;color:var(--accent3);">{{ t.ticket_number }}</span></td>
|
||||
<td>{{ t.title[:45] }}{% if t.title|length > 45 %}…{% endif %}</td>
|
||||
<td><span class="badge badge-{{ t.status }}">{{ t.status.replace('_',' ').upper() }}</span></td>
|
||||
<td><span class="badge badge-{{ t.priority }}">{{ t.priority.upper() }}</span></td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.created_at.strftime('%b %d') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-inbox" style="font-size:36px;display:block;margin-bottom:12px;"></i>
|
||||
No tickets yet. <a href="{{ url_for('tickets.create_ticket') }}">Submit your first ticket →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions + KB -->
|
||||
<div class="col-lg-5">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><i class="bi bi-lightning-charge me-2"></i>Quick Actions</div>
|
||||
<div class="card-body d-grid gap-2">
|
||||
<a href="{{ url_for('tickets.create_ticket') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Report New Issue
|
||||
</a>
|
||||
<a href="{{ url_for('tickets.ticket_list') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-list-ul me-2"></i>View All My Tickets
|
||||
</a>
|
||||
<button class="btn btn-secondary" onclick="toggleChat()">
|
||||
<i class="bi bi-robot me-2"></i>Ask IT Assistant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-book me-2"></i>Knowledge Base</span>
|
||||
<a href="{{ url_for('tickets.knowledge_base') }}" style="font-size:12px;color:var(--accent3);">View all →</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if articles %}
|
||||
{% for art in articles %}
|
||||
<a href="{{ url_for('tickets.kb_article', article_id=art.id) }}" style="display:flex;align-items:center;gap:10px;padding:11px 16px;border-bottom:1px solid var(--border);color:var(--text);">
|
||||
<i class="bi bi-file-earmark-text" style="color:var(--accent3);font-size:16px;flex-shrink:0;"></i>
|
||||
<span style="font-size:13px;">{{ art.title[:55] }}</span>
|
||||
<span style="margin-left:auto;font-size:11px;color:var(--muted);">{{ art.view_count }} views</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="p-3 text-center" style="font-size:13px;color:var(--muted);">No articles yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,104 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}IT Dashboard{% endblock %}
|
||||
{% block page_title %}IT Operations Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Stats -->
|
||||
<div class="row g-3 mb-4">
|
||||
{% set stat_items = [
|
||||
('open', open_count, 'bi-circle', 'var(--info)', 'rgba(96,165,250,.1)', 'Open'),
|
||||
('in_progress', in_progress_count, 'bi-arrow-repeat', 'var(--accent3)', 'rgba(0,180,216,.1)', 'In Progress'),
|
||||
('pending', pending_count, 'bi-hourglass-split', 'var(--warning)', 'rgba(251,191,36,.1)', 'Pending'),
|
||||
('resolved', resolved_count, 'bi-check2-circle', 'var(--success)', 'rgba(45,212,191,.1)', 'Resolved'),
|
||||
] %}
|
||||
{% for status, count, icon, color, bg, label in stat_items %}
|
||||
<div class="col-6 col-xl-3">
|
||||
<a href="{{ url_for('admin.all_tickets', status=status) }}" style="text-decoration:none;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background:{{ bg }};color:{{ color }};">
|
||||
<i class="bi {{ icon }}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value" style="color:{{ color }};">{{ count }}</div>
|
||||
<div class="stat-label">{{ label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Assigned to me -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-person-check me-2"></i>Assigned to Me</span>
|
||||
<a href="{{ url_for('admin.all_tickets', assigned='me') }}" style="font-size:12px;color:var(--accent3);">View all →</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if my_tickets %}
|
||||
<table class="table mb-0">
|
||||
<thead><tr><th>Ticket #</th><th>Title</th><th>Priority</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for t in my_tickets %}
|
||||
<tr style="cursor:pointer;" onclick="window.location='/tickets/{{ t.id }}'">
|
||||
<td><span class="mono" style="font-size:11px;color:var(--accent3);">{{ t.ticket_number }}</span></td>
|
||||
<td style="font-size:13px;">{{ t.title[:40] }}{% if t.title|length>40 %}…{% endif %}</td>
|
||||
<td><span class="badge badge-{{ t.priority }}">{{ t.priority.upper() }}</span></td>
|
||||
<td><span class="badge badge-{{ t.status }}">{{ t.status.replace('_',' ').upper() }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="p-4 text-center" style="color:var(--muted);font-size:13px;"><i class="bi bi-inbox" style="font-size:28px;display:block;margin-bottom:8px;"></i>No tickets assigned to you.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent tickets -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-clock-history me-2"></i>Recent Tickets</span>
|
||||
<a href="{{ url_for('admin.all_tickets') }}" style="font-size:12px;color:var(--accent3);">All tickets →</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table mb-0">
|
||||
<thead><tr><th>Ticket #</th><th>User</th><th>Category</th><th>Priority</th></tr></thead>
|
||||
<tbody>
|
||||
{% for t in recent_tickets %}
|
||||
<tr style="cursor:pointer;" onclick="window.location='/tickets/{{ t.id }}'">
|
||||
<td><span class="mono" style="font-size:11px;color:var(--accent3);">{{ t.ticket_number }}</span></td>
|
||||
<td style="font-size:13px;">{{ t.creator.full_name.split()[0] }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.category.replace('_',' ').title() }}</td>
|
||||
<td><span class="badge badge-{{ t.priority }}">{{ t.priority.upper() }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick links -->
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-lightning-charge me-2"></i>Quick Actions</div>
|
||||
<div class="card-body d-flex flex-wrap gap-2">
|
||||
<a href="{{ url_for('admin.all_tickets', status='open') }}" class="btn btn-outline-primary btn-sm"><i class="bi bi-circle me-1"></i>Open Queue</a>
|
||||
<a href="{{ url_for('admin.all_tickets', assigned='unassigned') }}" class="btn btn-secondary btn-sm"><i class="bi bi-person-x me-1"></i>Unassigned</a>
|
||||
<a href="{{ url_for('admin.kb_new') }}" class="btn btn-secondary btn-sm"><i class="bi bi-plus-circle me-1"></i>New KB Article</a>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin.users') }}" class="btn btn-secondary btn-sm"><i class="bi bi-people me-1"></i>Manage Users</a>
|
||||
<a href="{{ url_for('admin.activity_logs') }}" class="btn btn-secondary btn-sm"><i class="bi bi-list-ul me-1"></i>Activity Logs</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,250 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ article.title }}{% endblock %}
|
||||
{% block page_title %}Knowledge Base{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-3">
|
||||
<a href="{{ url_for('tickets.knowledge_base') }}" style="font-size:13px;color:var(--accent3);">
|
||||
← Back to Knowledge Base
|
||||
</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-file-earmark-text me-2"></i>Article</span>
|
||||
{% if current_user.is_it_staff %}
|
||||
<a href="{{ url_for('admin.kb_edit', article_id=article.id) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-pencil me-1"></i>Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1 style="font-size:22px;font-weight:700;margin-bottom:12px;">{{ article.title }}</h1>
|
||||
<div style="font-size:12px;color:var(--muted);margin-bottom:24px;display:flex;gap:16px;">
|
||||
{% if article.category %}<span><i class="bi bi-tag me-1"></i>{{ article.category.replace('_',' ').title() }}</span>{% endif %}
|
||||
<span><i class="bi bi-person me-1"></i>{{ article.author.full_name if article.author else 'IT Team' }}</span>
|
||||
<span><i class="bi bi-calendar3 me-1"></i>{{ article.updated_at.strftime('%B %d, %Y') }}</span>
|
||||
<span><i class="bi bi-eye me-1"></i>{{ article.view_count }} views</span>
|
||||
</div>
|
||||
{% if article.tags %}
|
||||
<div class="mb-3">
|
||||
{% for tag in article.tags.split(',') %}
|
||||
<span style="background:var(--surface2);border:1px solid var(--border);border-radius:5px;padding:2px 10px;font-size:11px;color:var(--muted);margin-right:4px;">{{ tag.strip() }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div style="white-space:pre-wrap;font-size:14px;line-height:1.8;color:var(--text);">{{ article.body }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-body d-flex align-items-center justify-content-between">
|
||||
<span style="font-size:13px;color:var(--muted);">Did this article solve your issue?</span>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('tickets.knowledge_base') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
<a href="{{ url_for('tickets.create_ticket') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-ticket me-1"></i>Still need help
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Knowledge Base{% endblock %}
|
||||
{% block page_title %}Knowledge Base{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-book me-2"></i>Self-Service Articles</span>
|
||||
{% if current_user.is_it_staff %}
|
||||
<a href="{{ url_for('admin.kb_new') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg me-1"></i>New Article
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if articles %}
|
||||
{% for art in articles %}
|
||||
<a href="{{ url_for('tickets.kb_article', article_id=art.id) }}"
|
||||
style="display:flex;align-items:center;gap:14px;padding:14px 20px;border-bottom:1px solid var(--border);color:var(--text);">
|
||||
<div style="width:40px;height:40px;background:rgba(0,180,216,.1);border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||
<i class="bi bi-file-earmark-text" style="color:var(--accent3);font-size:18px;"></i>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:14px;font-weight:600;">{{ art.title }}</div>
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:2px;">
|
||||
{% if art.category %}{{ art.category.replace('_',' ').title() }} · {% endif %}
|
||||
{{ art.updated_at.strftime('%b %d, %Y') }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--muted);flex-shrink:0;">
|
||||
<i class="bi bi-eye me-1"></i>{{ art.view_count }}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-journal-x" style="font-size:40px;display:block;margin-bottom:12px;"></i>
|
||||
No articles published yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-lightbulb me-2"></i>Can't find an answer?</div>
|
||||
<div class="card-body d-grid gap-2">
|
||||
<a href="{{ url_for('tickets.create_ticket') }}" class="btn btn-primary">
|
||||
<i class="bi bi-ticket me-2"></i>Submit a Ticket
|
||||
</a>
|
||||
<button class="btn btn-secondary" onclick="toggleChat()">
|
||||
<i class="bi bi-robot me-2"></i>Ask IT Assistant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,149 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}My Tickets{% endblock %}
|
||||
{% block page_title %}Tickets{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="GET" class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" class="form-control" name="q" value="{{ search }}" placeholder="Search by title, ticket #, description…"/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
{% for s in statuses %}
|
||||
<option value="{{ s }}" {% if s == status %}selected{% endif %}>{{ s.replace('_',' ').title() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Priority</label>
|
||||
<select class="form-select" name="priority">
|
||||
<option value="">All Priorities</option>
|
||||
{% for p in priorities %}
|
||||
<option value="{{ p }}" {% if p == priority %}selected{% endif %}>{{ p.title() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select" name="category">
|
||||
<option value="">All Categories</option>
|
||||
{% for c in categories %}
|
||||
<option value="{{ c }}" {% if c == category %}selected{% endif %}>{{ c.replace('_',' ').title() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-fill"><i class="bi bi-search me-1"></i>Filter</button>
|
||||
<a href="{{ url_for('tickets.ticket_list') }}" class="btn btn-secondary"><i class="bi bi-x-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ticket table -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-ticket-detailed me-2"></i>
|
||||
Tickets
|
||||
<span style="font-size:12px;color:var(--muted);font-family:'Space Mono',monospace;">({{ tickets.total }})</span>
|
||||
</span>
|
||||
<a href="{{ url_for('tickets.create_ticket') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg me-1"></i>New Ticket
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if tickets.items %}
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket #</th>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
{% if current_user.is_it_staff %}<th>Submitted By</th>{% endif %}
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in tickets.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}"
|
||||
class="mono" style="font-size:12px;color:var(--accent3);">{{ t.ticket_number }}</a>
|
||||
{% if t.ai_generated %}
|
||||
<i class="bi bi-robot" style="font-size:11px;color:var(--muted);" title="Created via AI Assistant"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}" style="color:var(--text);font-size:14px;">
|
||||
{{ t.title[:55] }}{% if t.title|length > 55 %}…{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.category.replace('_',' ').title() }}</td>
|
||||
<td><span class="badge badge-{{ t.status }}">{{ t.status.replace('_',' ').upper() }}</span></td>
|
||||
<td><span class="badge badge-{{ t.priority }}">{{ t.priority.upper() }}</span></td>
|
||||
{% if current_user.is_it_staff %}
|
||||
<td style="font-size:13px;">{{ t.creator.full_name }}</td>
|
||||
{% endif %}
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.created_at.strftime('%b %d, %Y') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if tickets.pages > 1 %}
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
<nav>
|
||||
<ul class="pagination mb-0">
|
||||
{% if tickets.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('tickets.ticket_list', page=tickets.prev_num, status=status, priority=priority, category=category, q=search) }}">‹ Prev</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for p in tickets.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if p %}
|
||||
<li class="page-item {% if p == tickets.page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('tickets.ticket_list', page=p, status=status, priority=priority, category=category, q=search) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if tickets.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('tickets.ticket_list', page=tickets.next_num, status=status, priority=priority, category=category, q=search) }}">Next ›</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-inbox" style="font-size:40px;display:block;margin-bottom:12px;"></i>
|
||||
No tickets found.
|
||||
{% if search or status or priority or category %}
|
||||
<a href="{{ url_for('tickets.ticket_list') }}" style="color:var(--accent3);">Clear filters</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('tickets.create_ticket') }}" style="color:var(--accent3);">Submit your first ticket →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Notifications{% endblock %}
|
||||
{% block page_title %}Notifications{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-bell me-2"></i>All Notifications</span>
|
||||
<form method="POST" action="{{ url_for('tickets.mark_notifications_read') }}">
|
||||
<button type="submit" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-check2-all me-1"></i>Mark All Read
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if notifs.items %}
|
||||
{% for n in notifs.items %}
|
||||
<a href="{{ n.link or '#' }}"
|
||||
style="display:flex;gap:14px;padding:16px 20px;border-bottom:1px solid var(--border);color:var(--text);
|
||||
{% if not n.is_read %}border-left:3px solid var(--accent3);background:rgba(0,180,216,.03);{% endif %}">
|
||||
<div style="margin-top:2px;flex-shrink:0;">
|
||||
{% if not n.is_read %}
|
||||
<div style="width:8px;height:8px;border-radius:50%;background:var(--accent3);margin-top:3px;"></div>
|
||||
{% else %}
|
||||
<i class="bi bi-check2" style="color:var(--muted);font-size:14px;"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:14px;font-weight:{% if not n.is_read %}600{% else %}400{% endif %};">
|
||||
{{ n.title }}
|
||||
</div>
|
||||
{% if n.message %}
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:3px;">{{ n.message[:120] }}</div>
|
||||
{% endif %}
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:5px;font-family:'Space Mono',monospace;">
|
||||
{{ n.created_at.strftime('%b %d, %Y at %H:%M') }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex-shrink:0;align-self:center;">
|
||||
<i class="bi bi-chevron-right" style="color:var(--muted);font-size:12px;"></i>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if notifs.pages > 1 %}
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
<nav><ul class="pagination mb-0">
|
||||
{% if notifs.has_prev %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('tickets.notifications', page=notifs.prev_num) }}">‹</a></li>
|
||||
{% endif %}
|
||||
{% for p in notifs.iter_pages() %}
|
||||
{% if p %}
|
||||
<li class="page-item {% if p==notifs.page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('tickets.notifications', page=p) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if notifs.has_next %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('tickets.notifications', page=notifs.next_num) }}">›</a></li>
|
||||
{% endif %}
|
||||
</ul></nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-bell-slash" style="font-size:40px;display:block;margin-bottom:12px;"></i>
|
||||
No notifications yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user