04/07 updated password reset, ticket templates, and satisfaction survey

This commit is contained in:
2026-04-07 17:32:11 -04:00
parent 36945f5976
commit 60db5c3f9a
18 changed files with 1309 additions and 105 deletions
@@ -0,0 +1,121 @@
{% extends "base.html" %}
{% block title %}Satisfaction Report{% endblock %}
{% block page_title %}Satisfaction Report{% endblock %}
{% block content %}
<!-- KPI row -->
<div class="row g-3 mb-4">
<div class="col-sm-4">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(251,191,36,.1);color:#f59e0b;">
<i class="bi bi-star-fill"></i>
</div>
<div>
<div class="stat-value">
{% if avg_rating %}{{ avg_rating }} <span style="font-size:16px;">/ 5</span>{% else %}—{% endif %}
</div>
<div class="stat-label">Average Rating</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(37,99,235,.1);color:var(--accent);">
<i class="bi bi-clipboard-check"></i>
</div>
<div>
<div class="stat-value">{{ total }}</div>
<div class="stat-label">Responses Received</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(100,116,139,.1);color:var(--muted);">
<i class="bi bi-hourglass-split"></i>
</div>
<div>
<div class="stat-value">{{ pending }}</div>
<div class="stat-label">Awaiting Response</div>
</div>
</div>
</div>
</div>
<!-- Rating distribution -->
{% if total %}
<div class="card mb-4">
<div class="card-header"><i class="bi bi-bar-chart-fill me-2"></i>Rating Distribution</div>
<div class="card-body">
{% for star in [5,4,3,2,1] %}
{% set count = dist[star] %}
{% set pct = ((count / total) * 100)|round(1) if total else 0 %}
<div class="d-flex align-items-center gap-3 mb-2">
<div style="width:60px;text-align:right;font-size:13px;font-weight:600;color:var(--text2);flex-shrink:0;">
{{ star }} ★
</div>
<div style="flex:1;background:var(--surface2);border-radius:4px;height:18px;overflow:hidden;">
<div style="width:{{ pct }}%;height:100%;background:{% if star >= 4 %}var(--success){% elif star == 3 %}var(--warning){% else %}var(--danger){% endif %};border-radius:4px;transition:width .4s;"></div>
</div>
<div style="width:70px;font-size:12px;color:var(--muted);font-family:'Space Mono',monospace;">
{{ count }} ({{ pct }}%)
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Individual responses -->
<div class="card">
<div class="card-header"><i class="bi bi-chat-square-text me-2"></i>Individual Responses</div>
<div class="card-body p-0">
{% if surveys %}
<table class="table mb-0">
<thead>
<tr>
<th>Ticket</th>
<th>Submitted By</th>
<th>Rating</th>
<th>Comment</th>
<th>Submitted</th>
</tr>
</thead>
<tbody>
{% for s in surveys %}
<tr>
<td>
<a href="{{ url_for('tickets.ticket_detail', ticket_id=s.ticket_id) }}"
class="mono" style="font-size:11px;color:var(--accent3);">
{{ s.ticket.ticket_number }}
</a>
<div style="font-size:12px;color:var(--muted);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
{{ s.ticket.title }}
</div>
</td>
<td style="font-size:13px;">{{ s.user.full_name }}</td>
<td>
<span style="font-size:18px;color:#f59e0b;">
{% for i in range(s.rating) %}★{% endfor %}{% for i in range(5 - s.rating) %}<span style="color:var(--border2);"></span>{% endfor %}
</span>
</td>
<td style="font-size:12px;color:var(--muted);max-width:220px;">
{{ s.comment or '—' }}
</td>
<td style="font-size:11px;color:var(--muted);">
{{ s.submitted_at | localtime("%b %d, %Y") }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-5 text-center" style="color:var(--muted);">
<i class="bi bi-star" style="font-size:40px;display:block;margin-bottom:12px;"></i>
No satisfaction responses yet. Surveys are sent automatically when tickets are resolved.
</div>
{% endif %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}{{ 'Edit' if t else 'New' }} Template{% endblock %}
{% block page_title %}{{ 'Edit' if t else 'New' }} Ticket Template{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-7">
<div class="card">
<div class="card-header">
<i class="bi bi-layout-text-window-reverse me-2"></i>
{{ 'Edit: ' + t.name if t else 'New Template' }}
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-3">
<label class="form-label">Template Name *</label>
<input type="text" class="form-control" name="name" required
value="{{ t.name if t else '' }}"
placeholder="e.g. VPN Access Issue"/>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Category</label>
<select class="form-select" name="category">
{% for cat in categories %}
<option value="{{ cat }}" {% if t and t.category == cat %}selected{% endif %}>
{{ cat.replace('_',' ').title() }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Default Priority</label>
<select class="form-select" name="priority">
{% for p in priorities %}
<option value="{{ p }}" {% if t and t.priority == p %}selected{% elif not t and p == 'medium' %}selected{% endif %}>
{{ p.upper() }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Title Hint
<span style="font-size:11px;color:var(--muted);font-weight:400;">
— pre-fills the ticket title field
</span>
</label>
<input type="text" class="form-control" name="title_hint"
value="{{ t.title_hint if t else '' }}"
placeholder="e.g. Cannot connect to VPN"/>
</div>
<div class="mb-3">
<label class="form-label">Description Template
<span style="font-size:11px;color:var(--muted);font-weight:400;">
— pre-fills the description textarea (use newlines as prompts)
</span>
</label>
<textarea class="form-control" name="description" rows="7"
placeholder="Steps to reproduce:&#10;1. &#10;&#10;Error message:&#10;&#10;Device / OS:">{{ t.description if t else '' }}</textarea>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Bootstrap Icon class
<span style="font-size:11px;color:var(--muted);font-weight:400;">
— e.g. bi-shield-lock
</span>
</label>
<div class="input-group">
<span class="input-group-text" id="icon-preview" style="font-size:18px;color:var(--accent);">
<i class="bi {{ t.icon if t else 'bi-file-text' }}" id="icon-el"></i>
</span>
<input type="text" class="form-control" name="icon" id="icon-input"
value="{{ t.icon if t else 'bi-file-text' }}"
oninput="document.getElementById('icon-el').className='bi '+this.value"/>
</div>
<div class="form-text">
Browse icons at
<a href="https://icons.getbootstrap.com/" target="_blank" style="color:var(--accent3);">
icons.getbootstrap.com
</a>
</div>
</div>
<div class="col-md-3">
<label class="form-label">Sort Order</label>
<input type="number" class="form-control" name="sort_order" min="0"
value="{{ t.sort_order if t else 0 }}"/>
<div class="form-text">Lower = shown first</div>
</div>
<div class="col-md-3 d-flex align-items-center" style="padding-top:28px;">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
name="is_active" id="is-active"
{% if not t or t.is_active %}checked{% endif %}
style="width:40px;height:22px;cursor:pointer;"/>
<label class="form-check-label ms-2" for="is-active"
style="font-size:13px;font-weight:600;">Active</label>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check2 me-2"></i>{{ 'Save Changes' if t else 'Create Template' }}
</button>
<a href="{{ url_for('admin.ticket_templates') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+78
View File
@@ -0,0 +1,78 @@
{% extends "base.html" %}
{% block title %}Ticket Templates{% endblock %}
{% block page_title %}Ticket Templates{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<p style="font-size:13px;color:var(--muted);margin:0;">
Pre-filled ticket scaffolds that employees can select on the New Ticket form to speed up submission.
</p>
<a href="{{ url_for('admin.ticket_template_new') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-1"></i>New Template
</a>
</div>
<div class="card">
<div class="card-header">
<i class="bi bi-layout-text-window-reverse me-2"></i>Templates
<span style="font-size:12px;color:var(--muted);font-family:'Space Mono',monospace;">({{ templates|length }})</span>
</div>
<div class="card-body p-0">
{% if templates %}
<table class="table mb-0">
<thead>
<tr>
<th style="width:36px;"></th>
<th>Name</th>
<th>Category</th>
<th>Priority</th>
<th>Title Hint</th>
<th>Order</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for t in templates %}
<tr>
<td style="color:var(--accent);font-size:18px;"><i class="bi {{ t.icon }}"></i></td>
<td style="font-weight:600;font-size:13px;">{{ t.name }}</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>
<td style="font-size:12px;color:var(--muted);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
{{ t.title_hint or '—' }}
</td>
<td style="font-size:12px;color:var(--muted);">{{ t.sort_order }}</td>
<td>
{% if t.is_active %}
<span style="font-size:12px;color:var(--success);"><i class="bi bi-check-circle-fill"></i> Active</span>
{% else %}
<span style="font-size:12px;color:var(--muted);"><i class="bi bi-dash-circle"></i> Inactive</span>
{% endif %}
</td>
<td>
<div class="d-flex gap-1">
<a href="{{ url_for('admin.ticket_template_edit', tmpl_id=t.id) }}"
class="btn btn-secondary btn-sm" title="Edit"><i class="bi bi-pencil"></i></a>
<form method="POST" action="{{ url_for('admin.ticket_template_delete', tmpl_id=t.id) }}"
onsubmit="return confirm('Delete template &quot;{{ t.name }}&quot;?');" style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-sm"
style="background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);color:var(--danger);"
title="Delete"><i class="bi bi-trash"></i></button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-5 text-center" style="color:var(--muted);">
<i class="bi bi-layout-text-window-reverse" style="font-size:40px;display:block;margin-bottom:12px;"></i>
No templates yet. <a href="{{ url_for('admin.ticket_template_new') }}" style="color:var(--accent3);">Create your first template →</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}