Initial commit

This commit is contained in:
2026-03-25 11:37:46 -04:00
commit dfdbb54875
41 changed files with 5079 additions and 0 deletions
+110
View File
@@ -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:&#10;- What were you trying to do?&#10;- What happened?&#10;- Any error messages?&#10;- 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 %}
+104
View File
@@ -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 %}
+250
View File
@@ -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 %}
+55
View File
@@ -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 %}
+60
View File
@@ -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 %}
+149
View File
@@ -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 %}
+80
View File
@@ -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 %}