191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
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
|
||
]})
|