04/06 implement creating ticket on behalf of another user for IT staff

This commit is contained in:
2026-04-06 17:13:53 -04:00
parent 3b17705911
commit bc236a3d19
8 changed files with 738 additions and 14 deletions
+43 -1
View File
@@ -145,4 +145,46 @@ def on_join_ticket(data):
def on_leave_ticket(data):
if current_user.is_authenticated:
ticket_id = data.get('ticket_id')
leave_room(f'ticket_{ticket_id}')
leave_room(f'ticket_{ticket_id}')
# ─── User Search API (IT Only) ────────────────────────────────────────────────
@api_bp.route('/users/search')
@login_required
def search_users():
"""Return active employees matching a search query.
Used by the 'create on behalf' form to populate the employee selector.
Restricted to IT staff to prevent employees from enumerating all users.
Query params
------------
q : str search term matched against full_name, email, department
limit : int max results (default 20, max 50)
"""
if not current_user.is_it_staff:
return jsonify({'error': 'Forbidden'}), 403
q = request.args.get('q', '').strip()
limit = min(request.args.get('limit', 20, type=int), 50)
from app.models import User, UserRole
query = User.query.filter(User.is_active == True)
if q:
query = query.filter(
User.full_name.ilike(f'%{q}%') |
User.email.ilike(f'%{q}%') |
User.department.ilike(f'%{q}%')
)
users = query.order_by(User.full_name).limit(limit).all()
return jsonify({'users': [
{
'id' : u.id,
'full_name' : u.full_name,
'email' : u.email,
'department': u.department or '',
'role' : u.role,
}
for u in users
]})
+103
View File
@@ -188,6 +188,109 @@ def create_ticket():
categories=_categories(), priorities=_priorities())
# ─── Create Ticket on Behalf of Employee (IT Staff Only) ─────────────────────
@tickets_bp.route('/tickets/behalf', methods=['GET', 'POST'])
@login_required
def create_ticket_behalf():
"""Allow IT staff to create a ticket on behalf of an employee.
Use case: an employee calls or emails to report an issue and cannot or
does not create a ticket themselves. IT staff completes the form,
selecting the employee from a searchable dropdown.
Data model
----------
ticket.created_by_id = employee's user ID (ticket shows as theirs)
ticket.created_by_staff_id = IT staff's user ID (audit trail)
The employee sees this ticket in their own dashboard and receives the
same new-ticket confirmation notification they would if self-submitted.
"""
if not current_user.is_it_staff:
abort(403)
employees = User.query.filter(
User.is_active == True,
).order_by(User.full_name).all()
if request.method == 'POST':
employee_id = request.form.get('employee_id', type=int)
title = request.form.get('title', '').strip()
description = request.form.get('description', '').strip()
category = request.form.get('category', TicketCategory.OTHER)
priority = request.form.get('priority', TicketPriority.MEDIUM)
location = request.form.get('location', '').strip()
asset_tag = request.form.get('asset_tag', '').strip()
# Validate employee selection
employee = db.session.get(User, employee_id) if employee_id else None
if not employee or not employee.is_active:
flash('Please select a valid active employee.', 'danger')
return render_template('tickets/create_behalf.html',
employees=employees,
categories=_categories(),
priorities=_priorities())
if not title or not description:
flash('Title and description are required.', 'danger')
return render_template('tickets/create_behalf.html',
employees=employees,
categories=_categories(),
priorities=_priorities(),
selected_employee_id=employee_id)
ticket = Ticket(
title = title,
description = description,
category = category,
priority = priority,
location = location,
asset_tag = asset_tag,
created_by_id = employee.id, # ticket belongs to the employee
created_by_staff_id = current_user.id, # IT staff who filed it
status = TicketStatus.OPEN,
)
ticket.ticket_number = ticket.generate_ticket_number()
db.session.add(ticket)
db.session.flush() # get ticket.id before attachments
# Handle file uploads
for f in request.files.getlist('attachments'):
if f and f.filename:
file_error = validate_file(f, ALLOWED_EXT)
if file_error:
logger.warning(
f'[BEHALF UPLOAD REJECTED] {file_error} '
f'filename="{f.filename}" staff_id={current_user.id}'
)
continue
save_attachment(f, ticket_id=ticket.id, uploader_id=current_user.id)
log_action(
current_user.id, 'ticket_create_behalf', 'ticket', ticket.id,
f'ticket_number={ticket.ticket_number} on_behalf_of=user_id:{employee.id} '
f'({employee.full_name}) priority={priority} category={category}'
)
db.session.commit()
logger.info(
f'[TICKET CREATE BEHALF] ticket_id={ticket.id} '
f'number={ticket.ticket_number} '
f'employee_id={employee.id} staff_id={current_user.id}'
)
notify_new_ticket(ticket)
flash(
f'Ticket {ticket.ticket_number} created on behalf of {employee.full_name}.',
'success'
)
return redirect(url_for('tickets.ticket_detail', ticket_id=ticket.id))
return render_template('tickets/create_behalf.html',
employees=employees,
categories=_categories(),
priorities=_priorities())
# ─── Ticket List ──────────────────────────────────────────────────────────────
@tickets_bp.route('/tickets')