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
+123 -1
View File
@@ -11,7 +11,8 @@ from werkzeug.utils import secure_filename
import bleach
from app import db, limiter
from app.models import (User, Ticket, Comment, ActivityLog, KnowledgeBase,
KBAttachment, UserRole, TicketStatus, CannedResponse)
KBAttachment, UserRole, TicketStatus, CannedResponse,
TicketTemplate, TicketSatisfaction)
from app.services.log_service import log_action, log_ticket_history
from app.services.validation_service import validate_password, validate_file
@@ -1033,6 +1034,127 @@ def bulk_ticket_action():
))
# ─── Ticket Templates ────────────────────────────────────────────────────────
@admin_bp.route('/ticket-templates')
@login_required
@it_required
def ticket_templates():
templates = TicketTemplate.query.order_by(
TicketTemplate.sort_order, TicketTemplate.name
).all()
return render_template('admin/ticket_templates.html', templates=templates)
@admin_bp.route('/ticket-templates/new', methods=['GET', 'POST'])
@login_required
@it_required
def ticket_template_new():
if request.method == 'POST':
name = request.form.get('name', '').strip()
category = request.form.get('category', 'other')
priority = request.form.get('priority', 'medium')
title_hint = request.form.get('title_hint', '').strip()
description= request.form.get('description', '').strip()
icon = request.form.get('icon', 'bi-file-text').strip()
sort_order = request.form.get('sort_order', 0, type=int)
is_active = bool(request.form.get('is_active'))
if not name:
flash('Template name is required.', 'danger')
return render_template('admin/ticket_template_edit.html',
t=None, categories=_categories_list(),
priorities=_priorities_list())
t = TicketTemplate(
name=name, category=category, priority=priority,
title_hint=title_hint, description=description,
icon=icon, sort_order=sort_order, is_active=is_active,
created_by=current_user.id,
)
db.session.add(t)
db.session.flush()
log_action(current_user.id, 'ticket_template_create', 'ticket_template', t.id,
f'name={name}')
db.session.commit()
logger.info(f'[TEMPLATE CREATE] id={t.id} name={name} by user_id={current_user.id}')
flash(f'Template "{name}" created.', 'success')
return redirect(url_for('admin.ticket_templates'))
return render_template('admin/ticket_template_edit.html',
t=None, categories=_categories_list(),
priorities=_priorities_list())
@admin_bp.route('/ticket-templates/<int:tmpl_id>/edit', methods=['GET', 'POST'])
@login_required
@it_required
def ticket_template_edit(tmpl_id):
t = db.session.get(TicketTemplate, tmpl_id) or abort(404)
if request.method == 'POST':
t.name = request.form.get('name', t.name).strip()
t.category = request.form.get('category', t.category)
t.priority = request.form.get('priority', t.priority)
t.title_hint = request.form.get('title_hint', '').strip()
t.description = request.form.get('description', '').strip()
t.icon = request.form.get('icon', 'bi-file-text').strip()
t.sort_order = request.form.get('sort_order', 0, type=int)
t.is_active = bool(request.form.get('is_active'))
log_action(current_user.id, 'ticket_template_edit', 'ticket_template', t.id,
f'name={t.name}')
db.session.commit()
logger.info(f'[TEMPLATE EDIT] id={t.id} by user_id={current_user.id}')
flash(f'Template "{t.name}" updated.', 'success')
return redirect(url_for('admin.ticket_templates'))
return render_template('admin/ticket_template_edit.html',
t=t, categories=_categories_list(),
priorities=_priorities_list())
@admin_bp.route('/ticket-templates/<int:tmpl_id>/delete', methods=['POST'])
@login_required
@it_required
def ticket_template_delete(tmpl_id):
t = db.session.get(TicketTemplate, tmpl_id) or abort(404)
name = t.name
log_action(current_user.id, 'ticket_template_delete', 'ticket_template', t.id,
f'name={name}')
logger.info(f'[TEMPLATE DELETE] id={t.id} name={name} by user_id={current_user.id}')
db.session.delete(t)
db.session.commit()
flash(f'Template "{name}" deleted.', 'success')
return redirect(url_for('admin.ticket_templates'))
# ─── Satisfaction Survey Report ───────────────────────────────────────────────
@admin_bp.route('/satisfaction')
@login_required
@it_required
def satisfaction_report():
surveys = TicketSatisfaction.query.filter(
TicketSatisfaction.rating.isnot(None)
).order_by(TicketSatisfaction.submitted_at.desc()).all()
total = len(surveys)
avg_rating = round(sum(s.rating for s in surveys) / total, 2) if total else None
dist = {i: sum(1 for s in surveys if s.rating == i) for i in range(1, 6)}
pending = TicketSatisfaction.query.filter_by(rating=None).count()
return render_template('admin/satisfaction_report.html',
surveys=surveys, total=total,
avg_rating=avg_rating, dist=dist, pending=pending)
def _categories_list():
from app.models import TicketCategory
return [TicketCategory.HARDWARE, TicketCategory.SOFTWARE,
TicketCategory.NETWORK, TicketCategory.ACCESS,
TicketCategory.EMAIL, TicketCategory.PRINTER,
TicketCategory.PHONE, TicketCategory.SECURITY, TicketCategory.OTHER]
def _priorities_list():
from app.models import TicketPriority
return [TicketPriority.LOW, TicketPriority.MEDIUM,
TicketPriority.HIGH, TicketPriority.CRITICAL]
def _roles():
return [UserRole.EMPLOYEE, UserRole.IT_STAFF, UserRole.ADMIN]