Files
IT_Ticket_System/app/routes/api.py
T

191 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from flask import Blueprint, jsonify, request, abort
from flask_login import login_required, current_user
from flask_socketio import emit, join_room, leave_room
from app import db, socketio
from app.models import Notification, Ticket, TicketStatus, TicketPriority
from app.services.log_service import log_action
api_bp = Blueprint('api', __name__, url_prefix='/api')
logger = logging.getLogger(__name__)
# ─── Notifications API ────────────────────────────────────────────────────────
@api_bp.route('/notifications')
@login_required
def get_notifications():
"""Return the 20 most recent notifications for the current user as JSON."""
notifs = (Notification.query
.filter_by(user_id=current_user.id)
.order_by(Notification.created_at.desc())
.limit(20)
.all())
return jsonify({'notifications': [
{
'id' : n.id,
'type' : n.type,
'title' : n.title,
'message' : n.message or '',
'link' : n.link or '',
'is_read' : n.is_read,
'created_at': n.created_at.strftime('%b %d, %H:%M'),
}
for n in notifs
]})
@api_bp.route('/notifications/unread-count')
@login_required
def unread_count():
count = Notification.query.filter_by(user_id=current_user.id, is_read=False).count()
return jsonify({'count': count})
@api_bp.route('/notifications/<int:notif_id>/read', methods=['POST'])
@login_required
def mark_read(notif_id):
notif = Notification.query.filter_by(id=notif_id, user_id=current_user.id).first_or_404()
notif.is_read = True
db.session.commit()
return jsonify({'ok': True})
@api_bp.route('/notifications/mark-all-read', methods=['POST'])
@login_required
def mark_all_read():
Notification.query.filter_by(user_id=current_user.id, is_read=False).update({'is_read': True})
db.session.commit()
return jsonify({'ok': True})
# ─── Ticket Comments API ──────────────────────────────────────────────────────
@api_bp.route('/tickets/<int:ticket_id>/comments')
@login_required
def get_comments(ticket_id):
"""Return all visible comments for a ticket as JSON."""
from app.models import Ticket, Comment, UserRole
ticket = db.session.get(Ticket, ticket_id) or abort(404)
# Employees may only see their own tickets
if not current_user.is_it_staff and ticket.created_by_id != current_user.id:
return jsonify({'error': 'Forbidden'}), 403
q = Comment.query.filter_by(ticket_id=ticket_id)
if not current_user.is_it_staff:
q = q.filter_by(is_internal=False)
comments = q.order_by(Comment.created_at.asc()).all()
return jsonify({'comments': [
{
'id' : c.id,
'author_name': c.author.full_name,
'author_init': c.author.full_name[0].upper(),
'author_avatar': c.author.avatar_url or '',
'is_it_staff': c.author.is_it_staff,
'is_internal': c.is_internal,
'body' : c.body,
'created_at' : c.created_at.strftime('%b %d, %Y %H:%M'),
'can_delete' : current_user.is_it_staff or c.author_id == current_user.id,
'attachments': [
{
'id' : a.id,
'filename' : a.filename,
'mime_type': a.mime_type or '',
'is_image' : (a.mime_type or '').startswith('image/'),
}
for a in c.attachments.all()
],
}
for c in comments
]})
# ─── Ticket Stats API (IT) ────────────────────────────────────────────────────
@api_bp.route('/stats/tickets')
@login_required
def ticket_stats():
if not current_user.is_it_staff:
return jsonify({'error': 'Forbidden'}), 403
stats = {
'open' : Ticket.query.filter_by(status=TicketStatus.OPEN).count(),
'in_progress' : Ticket.query.filter_by(status=TicketStatus.IN_PROGRESS).count(),
'pending' : Ticket.query.filter_by(status=TicketStatus.PENDING).count(),
'resolved' : Ticket.query.filter_by(status=TicketStatus.RESOLVED).count(),
'closed' : Ticket.query.filter_by(status=TicketStatus.CLOSED).count(),
}
return jsonify(stats)
# ─── WebSocket Events ─────────────────────────────────────────────────────────
@socketio.on('connect')
def on_connect():
if current_user.is_authenticated:
join_room(f'user_{current_user.id}')
logger.info(f'[SOCKET CONNECT] user_id={current_user.id}')
@socketio.on('disconnect')
def on_disconnect():
if current_user.is_authenticated:
leave_room(f'user_{current_user.id}')
logger.info(f'[SOCKET DISCONNECT] user_id={current_user.id}')
@socketio.on('join_ticket')
def on_join_ticket(data):
if current_user.is_authenticated:
ticket_id = data.get('ticket_id')
join_room(f'ticket_{ticket_id}')
@socketio.on('leave_ticket')
def on_leave_ticket(data):
if current_user.is_authenticated:
ticket_id = data.get('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
]})