"""Interventions Crud Routes - GMAO Collège""" from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify from flask_login import login_required, current_user from app_new.extensions import db from ..core.models.maintenance import Intervention, InterventionComment from ..core.models.equipment import Equipment from ..core.models.user import User from datetime import datetime interventions_bp = Blueprint("interventions", __name__, url_prefix="/interventions", template_folder="templates") """ Interventions Routes - GMAO Collège Gestion des interventions de maintenance """ from flask import Blueprint, render_template, redirect, url_for, request, flash from flask_login import login_required, current_user from datetime import datetime, timezone from app_new.extensions import db from ..core.models.maintenance import Intervention, StatusChange, InterventionComment, Lot from ..core.models.planning import ScheduledTask from ..core.models.equipment import Equipment from ..core.models.college import Room from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES interventions_bp = Blueprint('interventions', __name__) @interventions_bp.route('/') @login_required def index(): """Liste des interventions avec filtres avances et tri par colonne.""" page = request.args.get('page', 1, type=int) status = request.args.get('status', '') priority = request.args.get('priority', '') intervention_type = request.args.get('type', '') lot_id = request.args.get('lot', type=int) assigned_to_id = request.args.get('assigned_to_id', type=int) frequency = request.args.get('frequency', '') date_from = request.args.get('date_from', '') date_to = request.args.get('date_to', '') sort_col = request.args.get('sort', 'created_at') sort_dir = request.args.get('dir', 'desc') show_trashed = request.args.get('trashed', '0') == '1' query = Intervention.query if show_trashed: query = query.filter_by(is_deleted=True) else: query = query.filter_by(is_deleted=False) if status: query = query.filter_by(status=status) if priority: query = query.filter_by(priority=priority) if intervention_type: query = query.filter(Intervention.type == intervention_type) if lot_id: query = query.filter_by(lot_id=lot_id) if assigned_to_id: query = query.filter_by(assigned_to_id=assigned_to_id) if date_from: try: d = datetime.strptime(date_from, '%Y-%m-%d').date() query = query.filter(Intervention.scheduled_date >= d) except ValueError: pass if date_to: try: d = datetime.strptime(date_to, '%Y-%m-%d').date() query = query.filter(Intervention.scheduled_date <= d) except ValueError: pass if frequency: if frequency == 'recurring': query = query.filter(Intervention.is_recurring == True) elif frequency == 'ponctuel': query = query.filter(Intervention.is_recurring == False) elif frequency.isdigit(): query = query.filter(Intervention.frequency_days == int(frequency)) # Tri par colonne (avec validation pour eviter injection) valid_sort_cols = {'created_at', 'scheduled_date', 'priority', 'status', 'title'} if sort_col not in valid_sort_cols: sort_col = 'created_at' sort_dir = 'asc' if sort_dir == 'asc' else 'desc' order_clause = getattr(Intervention, sort_col).asc() if sort_dir == 'asc' else getattr(Intervention, sort_col).desc() query = query.order_by(order_clause) interventions = query.paginate(page=page, per_page=20) lots = Lot.query.order_by(Lot.name).all() users = User.query.filter_by(is_active=True).order_by(User.username).all() # Compter les interventions par statut status_counts = {} for status_key in INTERVENTION_STATUSES.keys(): status_counts[status_key] = Intervention.query.filter_by(status=status_key, is_deleted=False).count() # Compter les interventions à jeter trashed_count = Intervention.query.filter_by(is_deleted=True).count() return render_template('interventions/index.html', interventions=interventions, lots=lots, users=users, statuses=INTERVENTION_STATUSES, priorities=PRIORITIES, status_counts=status_counts, trashed_count=trashed_count, show_trashed=show_trashed, sort_col=sort_col, sort_dir=sort_dir) @interventions_bp.route('/new', methods=['GET', 'POST']) @login_required def create(): """Créer une intervention.""" from ..core.models.user import User from ..outlook.models import OutlookMailInterpretation if request.method == 'POST': # Une intervention générale peut cibler une salle sans équipement. equipment_value = request.form.get('equipment_id') equipment_id = int(equipment_value) if equipment_value and equipment_value.isdigit() else None # Déterminer le demandeur requester_name = request.form.get('requester_name', '').strip() if not requester_name: # Si pas de demandeur fourni, utiliser le nom de l'utilisateur connecté requester_name = current_user.username if hasattr(current_user, 'username') else 'Inconnu' intervention = Intervention( title=request.form.get('title'), description=request.form.get('description') or '', lot_id=request.form.get('lot_id') or None, equipment_id=equipment_id, room_id=request.form.get('room_id') or None, assigned_to_id=request.form.get('assigned_to_id') or None, author_id=current_user.id, status=request.form.get('status', 'brouillon'), priority=request.form.get('priority', 'normale'), type=request.form.get('intervention_type', 'curatif'), notes=request.form.get('notes') or '', requester_name=requester_name, scheduled_date=datetime.strptime(request.form.get('intervention_date'), '%Y-%m-%d').date() if request.form.get('intervention_date') else None, scheduled_start=datetime.strptime(request.form.get('scheduled_start'), '%H:%M').time() if request.form.get('scheduled_start') else None, scheduled_end=datetime.strptime(request.form.get('scheduled_end'), '%H:%M').time() if request.form.get('scheduled_end') else None ) db.session.add(intervention) db.session.commit() # Marquer l'interprétation comme acceptée si fournie from_interpretation_id = request.form.get('from_interpretation') source = request.form.get('source', 'outlook') # 'outlook' ou 'ent' if from_interpretation_id: if source == 'ent': from app_new.ent.interpretation_models import EntMessageInterpretation from app_new.lib_ext.ent_service import move_ent_message_to_processed interpretation = EntMessageInterpretation.query.get(from_interpretation_id) if interpretation: message = interpretation.message # Déplacer le message ENT vers "Traités" if message: move_result = move_ent_message_to_processed(interpretation.message_id) if move_result.get('success'): flash('Intervention créée. Message ENT déplacé vers Traités.', 'success') else: flash(f'Intervention créée. Erreur déplacement ENT: {move_result.get("error")}', 'warning') interpretation.status = 'accepted' if message: message.intervention_id = intervention.id db.session.commit() else: from app_new.outlook.models import OutlookMail from app_new.outlook.routes import move_mail_to_processed interpretation = OutlookMailInterpretation.query.get(from_interpretation_id) if interpretation: mail = interpretation.mail # Déplacer le mail Outlook vers "Traités" if mail: move_result = move_mail_to_processed(mail) if move_result.get('success'): flash('Intervention créée. Mail déplacé vers Traités.', 'success') else: flash(f'Intervention créée. Erreur déplacement mail: {move_result.get("error")}', 'warning') interpretation.status = 'accepted' if mail: mail.intervention_id = intervention.id db.session.commit() # Flash déjà envoyé ci-dessus else: flash('Intervention créée avec succès.', 'success') return redirect(url_for('interventions.detail', id=intervention.id)) # GET - Pré-remplir depuis interprétation ou tâche planifiée from_interpretation = request.args.get('from_interpretation') scheduled_task_id = request.args.get('scheduled_task_id', type=int) # Valeurs par défaut prefilled = { 'title': '', 'description': '', 'priority': 'normale', 'type': 'curatif', 'location': '', 'equipment': '', 'assignee': '', 'date': None, 'requester_name': '' } interpretation = None json_suggestions = None if from_interpretation: source = request.args.get('source', 'outlook') if source == 'ent': from app_new.ent.interpretation_models import EntMessageInterpretation interpretation = EntMessageInterpretation.query.get(from_interpretation) else: interpretation = OutlookMailInterpretation.query.get(from_interpretation) if interpretation: # Adapter selon la source if source == 'ent': json_suggestions = { 'title': interpretation.message.subject if interpretation.message else '', 'description': interpretation.suggested_description, 'priority': interpretation.analysis_priority, 'type': interpretation.analysis_type, 'location': '', 'equipment': interpretation.suggested_equipment, 'assignee': '', 'date': None } prefilled['title'] = interpretation.message.subject if interpretation.message else '' prefilled['description'] = interpretation.suggested_description or '' prefilled['priority'] = interpretation.analysis_priority or 'normale' # Demandeur = sender du message ENT prefilled['requester_name'] = interpretation.message.sender_name if interpretation.message else '' type_map = { 'curative': 'curatif', 'preventive': 'preventif', 'administrative': 'administratif', 'formation': 'formation', 'information': 'curatif' } prefilled['type'] = type_map.get(interpretation.analysis_type, 'curatif') prefilled['equipment'] = interpretation.suggested_equipment or '' else: json_suggestions = { 'title': interpretation.suggested_title, 'description': interpretation.suggested_description, 'priority': interpretation.suggested_urgency, 'type': interpretation.analysis_type, 'location': interpretation.suggested_location, 'equipment': interpretation.suggested_equipment, 'assignee': interpretation.suggested_assignee, 'date': interpretation.suggested_date.strftime('%d/%m/%Y') if interpretation.suggested_date else None } # Pré-remplir avec les valeurs de l'interprétation prefilled['title'] = interpretation.suggested_title or '' prefilled['description'] = interpretation.suggested_description or '' prefilled['priority'] = interpretation.suggested_urgency or 'normale' # Demandeur = sender du mail Outlook prefilled['requester_name'] = interpretation.mail.from_name if interpretation.mail else '' # Mapper le type type_map = { 'curative': 'curatif', 'preventive': 'preventif', 'administrative': 'administratif', 'formation': 'formation', 'information': 'curatif' } prefilled['type'] = type_map.get(interpretation.analysis_type, 'curatif') prefilled['location'] = interpretation.suggested_location or '' prefilled['equipment'] = interpretation.suggested_equipment or '' prefilled['assignee'] = interpretation.suggested_assignee or '' prefilled['date'] = interpretation.suggested_date # Chercher l'équipement correspondant si spécifié equipment_id = None equipment_match = None if prefilled['equipment']: eq = Equipment.query.filter(Equipment.name.ilike(f"%{prefilled['equipment']}%")).first() if eq: equipment_id = eq.id equipment_match = eq # Pré-remplir depuis tâche planifiée si fourni if scheduled_task_id and not from_interpretation: task = ScheduledTask.query.get(scheduled_task_id) if task: prefilled['title'] = f"Intervention: {task.preventive_task.name if task.preventive_task else task.lot_task.name if task.lot_task else 'Tâche planifiée'}" prefilled['description'] = task.notes or '' prefilled['date'] = task.scheduled_date.strftime('%Y-%m-%d') if task.scheduled_date else None prefilled['scheduled_start'] = task.scheduled_start.strftime('%H:%M') if task.scheduled_start else None prefilled['scheduled_end'] = task.scheduled_end.strftime('%H:%M') if task.scheduled_end else None # Chercher la salle correspondante si spécifiée room_id = None room_match = None if prefilled['location']: room = Room.query.filter(Room.name.ilike(f"%{prefilled['location']}%")).first() if room: room_id = room.id room_match = room lots = Lot.query.order_by(Lot.name).all() equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all() rooms = Room.query.order_by(Room.name).all() users = User.query.filter_by(is_active=True).order_by(User.username).all() return render_template('interventions/new.html', lots=lots, equipments=equipments, rooms=rooms, users=users, statuses=INTERVENTION_STATUSES, priorities=PRIORITIES, prefilled=prefilled, equipment_id=equipment_id, equipment_match=equipment_match, room_id=room_id, room_match=room_match, from_interpretation=from_interpretation, interpretation=interpretation, json_suggestions=json_suggestions, source=source if from_interpretation else 'outlook') @interventions_bp.route('/create-for-group', methods=['GET']) @login_required def create_for_group_select(): """Sélection d'équipements pour création groupée.""" equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all() return render_template('interventions/create_for_group_select.html', equipments=equipments) @interventions_bp.route('/create-for-group', methods=['POST']) @login_required def create_for_group(): """Créer des interventions pour plusieurs équipements.""" equipment_ids = request.form.getlist('equipment_ids') title = request.form.get('title') description = request.form.get('description', '') priority = request.form.get('priority', 'normale') lot_id = request.form.get('lot_id') if not equipment_ids: flash('Aucun équipement sélectionné.', 'error') return redirect(url_for('interventions.create_for_group_select')) created_count = 0 for eq_id in equipment_ids: equipment = Equipment.query.get(eq_id) if equipment: intervention = Intervention( title=title, description=description, priority=priority, lot_id=lot_id if lot_id else None, equipment_id=equipment.id, room_id=equipment.room_id, status='brouillon', intervention_type='corrective' ) db.session.add(intervention) created_count += 1 db.session.commit() flash(f'{created_count} intervention(s) créée(s) avec succès.', 'success') return redirect(url_for('interventions.index')) @interventions_bp.route('/') @login_required def detail(id): """Détail d'une intervention.""" intervention = Intervention.query.get_or_404(id) return render_template('interventions/detail.html', intervention=intervention, statuses=INTERVENTION_STATUSES) @interventions_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit(id): """Modifier une intervention.""" from ..core.models.user import User intervention = Intervention.query.get_or_404(id) if request.method == 'POST': old_status = intervention.status intervention.title = request.form.get('title') intervention.description = request.form.get('description') intervention.lot_id = request.form.get('lot_id') or None intervention.equipment_id = request.form.get('equipment_id') or None intervention.room_id = request.form.get('room_id') or None intervention.assigned_to_id = request.form.get('assigned_to_id') or None # Ne pas changer le statut si le formulaire ne le contient pas new_status = request.form.get('status') if new_status: intervention.status = new_status intervention.priority = request.form.get('priority') intervention.type = request.form.get('intervention_type', intervention.type) intervention.scheduled_date = request.form.get('scheduled_date') or None intervention.notes = request.form.get('notes') intervention.requester_name = request.form.get('requester_name') or None # Enregistrer le changement de statut seulement si le statut a changé if new_status and old_status != intervention.status: status_change = StatusChange( intervention_id=intervention.id, from_status=old_status, to_status=intervention.status, changed_by_id=current_user.id ) db.session.add(status_change) db.session.commit() flash('Intervention modifiée avec succès.', 'success') return redirect(url_for('interventions.detail', id=intervention.id)) lots = Lot.query.order_by(Lot.name).all() equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all() rooms = Room.query.order_by(Room.name).all() users = User.query.filter_by(is_active=True).order_by(User.username).all() # Pré-remplir avec les valeurs existantes pour l'édition prefilled = { 'title': intervention.title, 'description': intervention.description or '', 'priority': intervention.priority, 'type': intervention.type, 'location': intervention.equipment.room.name if intervention.equipment and intervention.equipment.room else '', 'equipment': intervention.equipment.name if intervention.equipment else '', 'assignee': intervention.assigned_to.username if intervention.assigned_to else '', 'date': intervention.scheduled_date, 'requester_name': intervention.requester_name or '' } return render_template('interventions/new.html', intervention=intervention, lots=lots, equipments=equipments, rooms=rooms, users=users, statuses=INTERVENTION_STATUSES, priorities=PRIORITIES, prefilled=prefilled, title="Modifier l'intervention") @interventions_bp.route('//status', methods=['POST']) @login_required def change_status(id): """Changer le statut d'une intervention.""" intervention = Intervention.query.get_or_404(id) old_status = intervention.status new_status = request.form.get('status') comment = request.form.get('comment', '') allowed = INTERVENTION_TRANSITIONS.get(old_status, set()) if new_status != old_status and new_status not in allowed: flash(f'Transition interdite : {old_status} → {new_status}.', 'danger') return redirect(url_for('interventions.detail', id=intervention.id)) if new_status == 'en_cours': from ..core.services.planning_service import PlanningService reason = PlanningService.maintenance_unavailability( datetime.now(timezone.utc).date(), user_id=current_user.id ) if reason: flash(f'Impossible de démarrer une maintenance aujourd’hui : {reason}.', 'danger') return redirect(url_for('interventions.detail', id=intervention.id)) if new_status != old_status: intervention.status = new_status status_change = StatusChange( intervention_id=intervention.id, from_status=old_status, to_status=new_status, changed_by_id=current_user.id, comment=comment ) db.session.add(status_change) if new_status == 'terminee': now = datetime.now(timezone.utc) intervention.completed_date = now.date() intervention.completed_at = now db.session.commit() flash('Statut modifié.', 'success') return redirect(url_for('interventions.detail', id=intervention.id)) @interventions_bp.route('//comment', methods=['POST']) @login_required def add_comment(id): """Ajouter un commentaire à une intervention.""" intervention = Intervention.query.get_or_404(id) content = request.form.get('content') if content: comment = InterventionComment( intervention_id=intervention.id, author_id=current_user.id, content=content ) db.session.add(comment) db.session.commit() flash('Commentaire ajouté.', 'success') return redirect(url_for('interventions.detail', id=intervention.id)) @interventions_bp.route('//soft-delete', methods=['POST']) @login_required def soft_delete(id): """Mettre une intervention en corbeille (soft delete).""" intervention = Intervention.query.get_or_404(id) intervention.is_deleted = True intervention.deleted_at = datetime.now(timezone.utc) intervention.deleted_by_id = current_user.id db.session.commit() flash('Intervention mise en corbeille.', 'success') return redirect(url_for('interventions.index')) @interventions_bp.route('//postpone', methods=['POST']) @login_required def postpone(id): """Reporter une intervention : motif, compteur, conservation date originale.""" intervention = Intervention.query.get_or_404(id) new_date_str = request.form.get('new_scheduled_date') reason = request.form.get('reason', '').strip() if not new_date_str: flash('Nouvelle date requise.', 'danger') return redirect(url_for('interventions.detail', id=id)) try: new_date = datetime.strptime(new_date_str, '%Y-%m-%d').date() except ValueError: flash('Format de date invalide.', 'danger') return redirect(url_for('interventions.detail', id=id)) # Conserver la date originale à la première reprogrammation if intervention.postpone_count == 0 and intervention.scheduled_date: intervention.original_scheduled_date = intervention.scheduled_date intervention.scheduled_date = new_date intervention.postpone_count += 1 intervention.last_postpone_reason = reason intervention.last_postpone_at = datetime.now(timezone.utc) # Commentaire d'historique comment_text = f"Report #{intervention.postpone_count}" if intervention.original_scheduled_date: comment_text += f" (date originale {intervention.original_scheduled_date.strftime('%d/%m/%Y')})" comment_text += f" → {new_date.strftime('%d/%m/%Y')}" if reason: comment_text += f" — {reason}" status_change = StatusChange( intervention_id=intervention.id, from_status=intervention.status, to_status='reportee', changed_by_id=current_user.id, comment=comment_text ) db.session.add(status_change) db.session.commit() flash(f"Intervention reportée au {new_date.strftime('%d/%m/%Y')}.", 'success') return redirect(url_for('interventions.detail', id=id))