145 lines
5.3 KiB
Python
145 lines
5.3 KiB
Python
import logging
|
|
from flask import Blueprint, jsonify, request
|
|
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 = Ticket.query.get_or_404(ticket_id)
|
|
# 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(),
|
|
'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,
|
|
}
|
|
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}') |