"""Routes salles - GMAO Collège""" from datetime import date, datetime, timedelta from flask import Blueprint, render_template, redirect, url_for, request, flash from flask_login import login_required from app_new.extensions import db from app_new.core.models.college import Room, Zone, Building, RoomType, RoomProfile, RoomSurface, RoomSchedule from app_new.core.models.equipment import Equipment rooms_bp = Blueprint('rooms', __name__, template_folder='templates') def _room_form_context(room=None): """Return all reference data used by the room form. The building list is deliberately loaded independently from the zone list. Previously the GET route only supplied ``zones`` which left the required building select empty and made every room creation impossible. """ return { 'room': room, 'buildings': Building.query.order_by(Building.name).all(), 'zones': Zone.query.join(Building).order_by(Building.name, Zone.name).all(), 'room_types': RoomType.query.order_by(RoomType.name).all(), 'room_profiles': RoomProfile.query.order_by(RoomProfile.name).all(), } @rooms_bp.route('/') @login_required def index(): """Liste des salles.""" zones_filter = request.args.getlist('zone') or None room_type_id = request.args.get('room_type', type=int) query = Room.query.join(Zone).join(Building) if zones_filter: query = query.filter(Zone.id.in_(zones_filter)) if room_type_id: query = query.filter_by(room_type_id=room_type_id) rooms = query.order_by(Building.name, Zone.name, Room.name).all() zones = Zone.query.join(Building).order_by(Building.name, Zone.name).all() room_types = RoomType.query.order_by(RoomType.name).all() return render_template('equipments/rooms.html', rooms=rooms, zones=zones, room_types=room_types, selected_zones=[int(z) for z in zones_filter] if zones_filter else []) @rooms_bp.route('/') @login_required def detail(id): """Détail d'une salle.""" room = Room.query.get_or_404(id) today = date.today() week_start = today - timedelta(days=today.weekday()) # La vue salle consomme la résolution interne (manuel/Pronote/import), # afin de ne pas réafficher un créneau supplanté par la synchronisation. from app_new.core.services.room_planning import active_room_schedules courses = active_room_schedules(room.id, today) current_time = datetime.now().time() current_course = next( (course for course in courses if course.start_time <= current_time <= course.end_time), None, ) next_course = next( (course for course in courses if course.start_time > current_time), None, ) if current_course: room_status = { 'status': 'occupee', 'message': 'Salle actuellement occupée', 'course': current_course, 'next_occupied': None, } elif courses: room_status = { 'status': 'libre', 'message': 'Salle libre actuellement', 'course': None, 'next_occupied': next_course.start_time.strftime('%H:%M') if next_course else None, } else: room_status = { 'status': 'inconnu', 'message': "Aucun emploi du temps importé pour cette salle aujourd'hui.", 'course': None, 'next_occupied': None, } equipments = Equipment.query.filter( Equipment.room_id == room.id, Equipment.is_deleted.is_(False), ).order_by(Equipment.name, Equipment.individual_number).all() return render_template('equipments/room_detail.html', room=room, equipments=equipments, courses=courses, room_status=room_status, surfaces=room.surfaces) @rooms_bp.route('//duplicate', methods=['GET', 'POST']) @login_required def duplicate(id): """Duplique explicitement un local, jamais son historique métier.""" source = Room.query.get_or_404(id) if request.method == 'POST': name = (request.form.get('name') or '').strip() if not name: flash('Le nom du nouveau local est obligatoire.', 'danger') return render_template('equipments/room_duplicate.html', room=source), 400 new_room = Room(name=name, code=request.form.get('code') or name, building_id=source.building_id, zone_id=source.zone_id, floor=source.floor, room_type_id=source.room_type_id, room_profile_id=source.room_profile_id if request.form.get('copy_profile') else None) db.session.add(new_room) db.session.flush() if request.form.get('copy_surfaces'): for surface in source.surfaces: db.session.add(RoomSurface(room_id=new_room.id, surface_type=surface.surface_type, material=surface.material, finish=surface.finish, label=surface.label, sort_order=surface.sort_order)) if request.form.get('copy_equipment'): from app_new.core.services.equipment_creation import create_equipment for equipment in source.equipments: if equipment.is_deleted: continue create_equipment(name=equipment.name, category_id=equipment.category_id, lot_id=equipment.lot_id, room_id=new_room.id, quantity=equipment.quantity or 1, is_group=equipment.is_group, tracked_individually=equipment.tracked_individually, mobility=equipment.mobility, status=equipment.status, management_mode=equipment.management_mode) db.session.commit() flash(f"Local '{new_room.name}' créé à partir de {source.name}.", 'success') return redirect(url_for('rooms.detail', id=new_room.id)) return render_template('equipments/room_duplicate.html', room=source) @rooms_bp.route('//surfaces', methods=['POST']) @login_required def add_surface(id): room = Room.query.get_or_404(id) surface_type = request.form.get('surface_type') material = (request.form.get('material') or '').strip() if surface_type not in {'mur', 'sol', 'plafond', 'autre'} or not material: flash('Type et matériau sont obligatoires.', 'danger') else: db.session.add(RoomSurface(room_id=room.id, surface_type=surface_type, material=material, finish=(request.form.get('finish') or '').strip() or None, label=(request.form.get('label') or '').strip() or None)) db.session.commit() flash('Composition du local ajoutée.', 'success') return redirect(url_for('rooms.detail', id=room.id)) @rooms_bp.route('/new', methods=['GET', 'POST']) @login_required def create(): """Créer une salle.""" if request.method == 'POST': name = (request.form.get('name') or '').strip() building_id = request.form.get('building_id', type=int) zone_id = request.form.get('zone_id', type=int) building = db.session.get(Building, building_id) if building_id else None zone = db.session.get(Zone, zone_id) if zone_id else None errors = [] if not name: errors.append('Le nom de la salle est obligatoire.') if not building: errors.append('Sélectionnez un bâtiment valide.') if zone and building and zone.building_id != building.id: errors.append('La zone sélectionnée n’appartient pas au bâtiment choisi.') if errors: for error in errors: flash(error, 'danger') context = _room_form_context() context['form_data'] = request.form return render_template('equipments/room_form.html', **context), 400 room = Room( name=name, code=request.form.get('code') or None, zone_id=zone_id, building_id=building.id, room_type_id=request.form.get('room_type_id', type=int), room_profile_id=request.form.get('room_profile_id', type=int) or None, floor=request.form.get('floor', 0, type=int) ) db.session.add(room) try: db.session.commit() except Exception: db.session.rollback() flash('Impossible d’enregistrer cette salle. Vérifiez les valeurs saisies.', 'danger') context = _room_form_context() context['form_data'] = request.form return render_template('equipments/room_form.html', **context), 400 flash(f"Salle '{room.name}' créée.", 'success') return redirect(url_for('rooms.index')) context = _room_form_context() context['form_data'] = {} context['title'] = 'Nouvelle salle' return render_template('equipments/room_form.html', **context) @rooms_bp.route('/bulk', methods=['GET', 'POST']) @login_required def bulk_create(): """Création rapide de locaux similaires, sans imposer d'équipement.""" context = _room_form_context() if request.method == 'POST': building_id = request.form.get('building_id', type=int) zone_id = request.form.get('zone_id', type=int) building = db.session.get(Building, building_id) if building_id else None zone = db.session.get(Zone, zone_id) if zone_id else None names = [line.strip() for line in (request.form.get('names') or '').splitlines() if line.strip()] if not building or (zone and (zone.building_id or (zone.building.id if zone.building else None)) != building.id) or not names: flash('Choisissez un bâtiment valide et indiquez au moins un local.', 'danger') return render_template('equipments/rooms_bulk.html', **context), 400 if request.form.get('preview') == '1': existing = {room.name for room in Room.query.filter_by(building_id=building.id).all()} return render_template('equipments/rooms_bulk_preview.html', building=building, zone=zone, names=names, existing=existing, room_type_id=request.form.get('room_type_id', type=int), room_profile_id=request.form.get('room_profile_id', type=int) or None, floor=request.form.get('floor', 0, type=int)) room_type_id = request.form.get('room_type_id', type=int) room_profile_id = request.form.get('room_profile_id', type=int) or None created = [] existing = {room.name for room in Room.query.filter_by(building_id=building.id).all()} for name in names: if name in existing: continue created.append(Room(name=name, code=name, building_id=building.id, zone_id=zone.id if zone else None, room_type_id=room_type_id, room_profile_id=room_profile_id, floor=request.form.get('floor', 0, type=int))) existing.add(name) db.session.add_all(created) db.session.commit() flash(f'{len(created)} local(aux) créé(s). Aucun équipement n’a été ajouté automatiquement.', 'success') return redirect(url_for('rooms.index')) return render_template('equipments/rooms_bulk.html', **context) @rooms_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit(id): """Modifier une salle.""" room = Room.query.get_or_404(id) if request.method == 'POST': name = (request.form.get('name') or '').strip() building_id = request.form.get('building_id', type=int) zone_id = request.form.get('zone_id', type=int) building = db.session.get(Building, building_id) if building_id else None zone = db.session.get(Zone, zone_id) if zone_id else None if not name or not building or (zone and zone.building_id != building.id): flash('Le nom, le bâtiment et une zone cohérente sont obligatoires.', 'danger') context = _room_form_context(room) context['form_data'] = request.form context['title'] = 'Modifier la salle' return render_template('equipments/room_form.html', **context), 400 room.name = name room.code = request.form.get('code') or None room.zone_id = zone_id room.building_id = building.id room.room_type_id = request.form.get('room_type_id', type=int) room.room_profile_id = request.form.get('room_profile_id', type=int) or None room.floor = request.form.get('floor', 0, type=int) try: db.session.commit() except Exception: db.session.rollback() flash('Impossible d’enregistrer cette salle. Vérifiez les valeurs saisies.', 'danger') context = _room_form_context(room) context['form_data'] = request.form context['title'] = 'Modifier la salle' return render_template('equipments/room_form.html', **context), 400 flash(f"Salle '{room.name}' mise à jour.", 'success') return redirect(url_for('rooms.index')) context = _room_form_context(room) context['form_data'] = {} context['title'] = 'Modifier la salle' return render_template('equipments/room_form.html', **context) @rooms_bp.route('//delete', methods=['POST']) @login_required def delete(id): """Supprimer une salle.""" room = Room.query.get_or_404(id) db.session.delete(room) db.session.commit() flash(f"Salle '{room.name}' supprimée.", 'success') return redirect(url_for('rooms.index'))