from flask import Blueprint, request, jsonify, g from datetime import datetime, timedelta, date from pydantic import ValidationError from app.services.reservation_service import ReservationService from app.schemas.reservation_dto import ReservationCreateDTO, ReservationUpdateDTO from app.utils.jwt_decorators import jwt_required, jwt_role_required api_reservations_bp = Blueprint('api_reservations', __name__, url_prefix='/api/v1/reservations') reservation_service = ReservationService() @api_reservations_bp.route('', methods=['GET']) @jwt_required def list_reservations(): """Consulta de reservas por rango de fechas y/o aula.""" start_str = request.args.get('start_date') end_str = request.args.get('end_date') classroom_id = request.args.get('classroom_id', type=int) now = datetime.utcnow() try: start_dt = datetime.strptime(start_str, '%Y-%m-%d') if start_str else now.replace(hour=0, minute=0, second=0) except ValueError: return jsonify({'error': 'BadRequest', 'message': 'Formato inválido de start_date (debe ser YYYY-MM-DD).'}), 400 try: end_dt = datetime.strptime(end_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59) if end_str else start_dt + timedelta(days=7) except ValueError: return jsonify({'error': 'BadRequest', 'message': 'Formato inválido de end_date (debe ser YYYY-MM-DD).'}), 400 hide_virtual = request.args.get('hide_virtual', '0') in ['1', 'true', 'True'] floor = request.args.get('floor') shift = request.args.get('shift') search = request.args.get('search') reservations = reservation_service.list_reservations(start_dt, end_dt, classroom_id=classroom_id) result = [] for r in reservations: is_virt = bool(r.is_virtual or (r.classroom and r.classroom.is_virtual)) if hide_virtual and is_virt: continue if floor and r.classroom and str(r.classroom.floor).strip().lower() != str(floor).strip().lower(): continue if shift and r.shift and shift.lower() not in r.shift.lower(): continue if search: s_lower = search.lower() subj_n = (r.subject_name or '').lower() subj_c = (r.subject_code or '').lower() c_name = (r.classroom.code if r.classroom else '').lower() u_name = (r.user.name if r.user else '').lower() if s_lower not in subj_n and s_lower not in subj_c and s_lower not in c_name and s_lower not in u_name: continue result.append({ 'id': r.id, 'classroom_id': r.classroom_id, 'classroom_name': r.classroom.code if r.classroom else None, 'classroom': { 'id': r.classroom.id, 'code': r.classroom.code, 'code_display': getattr(r.classroom, 'code_display', r.classroom.code), 'floor': r.classroom.floor, 'floor_display': getattr(r.classroom, 'floor_display', f"Piso {r.classroom.floor}"), 'building': getattr(r.classroom, 'building_name', r.classroom.building), 'is_virtual': getattr(r.classroom, 'is_virtual', False), 'location_display': getattr(r.classroom, 'location_display', r.classroom.code) } if r.classroom else None, 'commission_id': r.commission_id, 'commission': { 'id': r.commission.id, 'code': r.commission.code, 'virtual_link': r.commission.virtual_link, 'max_students': r.commission.max_students } if r.commission else None, 'teacher_name': r.user.name if r.user else None, 'user': { 'id': r.user.id, 'name': r.user.name, 'email': r.user.email } if r.user else None, 'start_time': r.start_time.isoformat(), 'end_time': r.end_time.isoformat(), 'purpose': r.purpose, 'subject_name': r.subject_name, 'subject_code': r.subject_code, 'status': str(r.status.value if hasattr(r.status, 'value') else r.status), 'shift': r.shift or 'Mañana', 'virtual_link': r.virtual_link, 'effective_virtual_link': r.effective_virtual_link, 'is_virtual': is_virt, 'expected_attendees': r.expected_attendees or 30, 'notes': r.notes or '' }) return jsonify({ 'total': len(result), 'start_date': start_dt.strftime('%Y-%m-%d'), 'end_date': end_dt.strftime('%Y-%m-%d'), 'reservations': result }), 200 @api_reservations_bp.route('/', methods=['GET']) @jwt_required def get_reservation(reservation_id: int): """Obtiene el detalle de una reserva específica.""" r = reservation_service.get_reservation(reservation_id) if not r: return jsonify({'error': 'NotFound', 'message': f'Reserva {reservation_id} no encontrada.'}), 404 return jsonify({ 'id': r.id, 'classroom_id': r.classroom_id, 'classroom_name': r.classroom.code if r.classroom else None, 'location_display': r.classroom.location_display if r.classroom else None, 'commission_id': r.commission_id, 'teacher_id': r.user_id, 'teacher_name': r.user.name if r.user else None, 'start_time': r.start_time.isoformat(), 'end_time': r.end_time.isoformat(), 'purpose': r.purpose, 'status': str(r.status), 'shift': r.shift, 'virtual_link': r.virtual_link, 'expected_attendees': r.expected_attendees, 'notes': r.notes }), 200 @api_reservations_bp.route('', methods=['POST']) @jwt_required def create_reservation(): """Crea una reserva aplicando validación DTO y detección de colisiones.""" data = request.get_json(silent=True) or {} # Inyectar usuario autenticado si no se especifica if 'user_id' not in data: data['user_id'] = g.jwt_user.id try: dto = ReservationCreateDTO(**data) except ValidationError as e: return jsonify({'error': 'ValidationError', 'details': e.errors()}), 400 try: reservation = reservation_service.create_reservation(dto) return jsonify({ 'message': 'Reserva creada exitosamente.', 'id': reservation.id, 'classroom_id': reservation.classroom_id, 'start_time': reservation.start_time.isoformat(), 'end_time': reservation.end_time.isoformat(), 'status': str(reservation.status) }), 201 except ValueError as e: return jsonify({'error': 'ConflictOrValidation', 'message': str(e)}), 409 @api_reservations_bp.route('/', methods=['PUT']) @jwt_required def update_reservation(reservation_id: int): """Modifica una reserva existente.""" data = request.get_json(silent=True) or {} try: dto = ReservationUpdateDTO(**data) except ValidationError as e: return jsonify({'error': 'ValidationError', 'details': e.errors()}), 400 try: reservation = reservation_service.update_reservation(reservation_id, dto) return jsonify({ 'message': 'Reserva actualizada exitosamente.', 'id': reservation.id, 'start_time': reservation.start_time.isoformat(), 'end_time': reservation.end_time.isoformat(), 'status': str(reservation.status) }), 200 except ValueError as e: return jsonify({'error': 'ConflictOrValidation', 'message': str(e)}), 409 @api_reservations_bp.route('/', methods=['DELETE']) @jwt_required def cancel_reservation(reservation_id: int): """Cancela una reserva.""" cancelled = reservation_service.cancel_reservation(reservation_id) if not cancelled: return jsonify({'error': 'NotFound', 'message': f'Reserva {reservation_id} no encontrada.'}), 404 return jsonify({'message': f'Reserva {reservation_id} cancelada correctamente.'}), 200