diff --git a/app/routes/api.py b/app/routes/api.py index f614054..4f19264 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -188,3 +188,46 @@ def search_users(): } for u in users ]}) + + +# ─── Ticket Search API ──────────────────────────────────────────────────────── + +@api_bp.route('/tickets/search') +@login_required +def search_tickets(): + """Return tickets matching a query string, used by the link-ticket picker. + + Searches ticket_number and title (case-insensitive). + ?q= — required, min 1 char + ?exclude= — ticket ID to exclude (the current ticket) + Restricted to IT staff only. + """ + if not current_user.is_it_staff: + return jsonify({'error': 'Forbidden'}), 403 + + q = request.args.get('q', '').strip() + exclude_id = request.args.get('exclude', 0, type=int) + + if not q: + return jsonify({'tickets': []}) + + from app.models import Ticket + query = Ticket.query.filter( + Ticket.ticket_number.ilike(f'%{q}%') | + Ticket.title.ilike(f'%{q}%') + ) + if exclude_id: + query = query.filter(Ticket.id != exclude_id) + + results = query.order_by(Ticket.created_at.desc()).limit(10).all() + logger.info(f'[TICKET SEARCH] q="{q}" results={len(results)} user_id={current_user.id}') + return jsonify({'tickets': [ + { + 'id' : t.id, + 'ticket_number': t.ticket_number, + 'title' : t.title, + 'status' : t.status, + 'priority' : t.priority, + } + for t in results + ]}) \ No newline at end of file diff --git a/app/routes/tickets.py b/app/routes/tickets.py index d99439d..1119e87 100644 --- a/app/routes/tickets.py +++ b/app/routes/tickets.py @@ -687,22 +687,32 @@ def link_ticket(ticket_id): abort(403) ticket = db.session.get(Ticket, ticket_id) or abort(404) - other_id = request.form.get('linked_ticket_id', type=int) + raw_input = request.form.get('linked_ticket_id', '').strip() link_type = request.form.get('link_type', 'related') - if not other_id: - flash('Please specify a ticket to link.', 'danger') + if not raw_input: + flash('Please search for and select a ticket to link.', 'danger') return redirect(url_for('tickets.ticket_detail', ticket_id=ticket_id)) + # Resolve by ticket_number (e.g. TKT-20260406-0001) or numeric DB id + if raw_input.upper().startswith('TKT-'): + other = Ticket.query.filter_by(ticket_number=raw_input.upper()).first() + else: + try: + other = db.session.get(Ticket, int(raw_input)) + except ValueError: + other = Ticket.query.filter_by(ticket_number=raw_input.upper()).first() + + if not other: + flash(f'Ticket "{raw_input}" not found.', 'danger') + return redirect(url_for('tickets.ticket_detail', ticket_id=ticket_id)) + + other_id = other.id + if other_id == ticket_id: flash('A ticket cannot be linked to itself.', 'danger') return redirect(url_for('tickets.ticket_detail', ticket_id=ticket_id)) - other = db.session.get(Ticket, other_id) - if not other: - flash(f'Ticket #{other_id} not found.', 'danger') - return redirect(url_for('tickets.ticket_detail', ticket_id=ticket_id)) - # Enforce canonical ordering (smaller id first) for the uniqueness constraint a, b = sorted([ticket_id, other_id]) existing = TicketLink.query.filter_by(ticket_id=a, linked_ticket_id=b).first() diff --git a/app/templates/tickets/detail.html b/app/templates/tickets/detail.html index d18ca6b..c21c1a6 100644 --- a/app/templates/tickets/detail.html +++ b/app/templates/tickets/detail.html @@ -548,6 +548,8 @@ function buildCommentEl(c) { `; return wrap; } + +