""" ENT Routes - GMAO Collège Intégration ENT77 """ 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 app_new.ent.models import EntCredential, EntMessage, EntConfig from app_new.ent.interpretation_models import EntMessageInterpretation from app_new.lib_ext.ent_service import login_ent from app_new.lib_ext.ent_crypto import encrypt_value, decrypt_value from flask import current_app import requests ent_bp = Blueprint('ent', __name__, template_folder='templates') @ent_bp.route('/', methods=['GET', 'POST']) @login_required def index(): """Page principale ENT - formulaire de connexion avec vérification.""" credentials = EntCredential.query.first() connection_status = None # None, 'success', 'error' error_message = None if request.method == 'POST': action = request.form.get('action', 'create') username = request.form.get('ent_username') password = request.form.get('ent_password') if action == 'create': # Créer un nouveau credential - vérifier d'abord la connexion if not username or not password: flash('Veuillez remplir tous les champs.', 'danger') else: # Vérifier que les identifiants fonctionnent session = requests.Session() headers = login_ent(session, username, password) if headers is None: flash('Connexion échouée. Vérifiez vos identifiants ENT77.', 'danger') flash('Assurez-vous que le nom d\'utilisateur et le mot de passe sont corrects.', 'warning') else: # Connexion réussie - sauvegarder les identifiants chiffrés credential = EntCredential( user_id=current_user.id, ent_username_encrypted=encrypt_value(username, current_app._get_current_object()), ent_password_encrypted=encrypt_value(password, current_app._get_current_object()) ) db.session.add(credential) db.session.commit() flash('Connexion ENT77 réussie ! Identifiants sauvegardés.', 'success') return redirect(url_for('ent.index')) elif action == 'update': # Mettre à jour le credential existant - vérifier d'abord if credentials: new_password = password if password else None test_username = username if username else credentials.ent_username_encrypted.decode('utf-8') if new_password: # Nouveau mot de passe fourni - vérifier la connexion session = requests.Session() headers = login_ent(session, test_username, new_password) if headers is None: flash('Connexion échouée avec le nouveau mot de passe.', 'danger') else: if username: credentials.ent_username_encrypted = encrypt_value(username, current_app._get_current_object()) credentials.ent_password_encrypted = encrypt_value(new_password, current_app._get_current_object()) db.session.commit() flash('Identifiants ENT mis à jour avec succès.', 'success') else: # Juste changer le nom d'utilisateur (mot de passe inchangé) if username: credentials.ent_username_encrypted = encrypt_value(username, current_app._get_current_object()) db.session.commit() flash('Nom d\'utilisateur mis à jour.', 'success') return redirect(url_for('ent.index')) # Décoder les credentials pour l'affichage display_credentials = None if credentials: try: decrypted_username = decrypt_value(credentials.ent_username_encrypted, current_app._get_current_object()) if credentials.ent_username_encrypted else '' except: decrypted_username = credentials.ent_username_encrypted.decode('utf-8') if credentials.ent_username_encrypted else '' display_credentials = type('DisplayCredentials', (), { 'id': credentials.id, 'ent_username': decrypted_username })() return render_template('ent/index.html', credentials=display_credentials) @ent_bp.route('/messages') @login_required def messages(): """Liste des messages ENT.""" page = request.args.get('page', 1, type=int) messages = EntMessage.query.order_by(EntMessage.date.desc()).paginate(page=page, per_page=20) return render_template('ent/messages.html', messages=messages) @ent_bp.route('/inbox') @login_required def inbox(): """Boîte de réception ENT.""" messages = EntMessage.query.order_by(EntMessage.date.desc()).limit(50).all() return render_template('ent/messages.html', messages=messages) @ent_bp.route('/sync') @login_required def sync(): """Synchronisation des messages.""" flash('Synchronisation ENT non implémentée.', 'info') return redirect(url_for('ent.index')) @ent_bp.route('/staff') @login_required def staff(): """Liste du personnel depuis ENT.""" from app_new.core.models.user import Staff staff_list = Staff.query.order_by(Staff.last_name, Staff.first_name).all() return render_template('ent/staff.html', staff_list=staff_list) @ent_bp.route('/check', methods=['POST']) @login_required def check(): """Vérification manuelle des messages ENT.""" from app_new.lib_ext.ent_service import _process_credential from flask import current_app creds = EntCredential.query.filter_by(is_active=True).all() config = EntConfig.query.first() app = current_app._get_current_object() total = 0 for cred in creds: try: result = _process_credential(cred, config, app) if isinstance(result, int): total += result except Exception as e: flash(f'Erreur: {str(e)[:100]}', 'danger') if total > 0: flash(f'{total} nouveau(x) message(s) traite(s).', 'success') else: flash('Aucun nouveau message.', 'info') return redirect(url_for('ent.inbox')) @ent_bp.route('/check-all', methods=['POST']) @login_required def check_all(): """Vérification de TOUS les messages (lus et non lus).""" import re from datetime import datetime, timezone from app_new.lib_ext.ent_service import login_ent, extract_sender, is_intervention_request from app_new.lib_ext.ent_crypto import decrypt_value from app_new.core.models import Intervention credential = EntCredential.query.filter_by(user_id=current_user.id, is_active=True).first() if not credential: flash('Aucun identifiant ENT actif configuré.', 'danger') return redirect(url_for('ent.inbox')) # Login ENT try: username = credential.get_username() password = credential.get_password() except Exception: flash('Impossible de déchiffrer les identifiants ENT.', 'danger') return redirect(url_for('ent.index')) sess = requests.Session() headers = login_ent(sess, username, password) if not headers: flash('Connexion ENT échouée.', 'danger') return redirect(url_for('ent.inbox')) # Récupérer TOUS les messages all_messages = [] page = 0 while True: url_page = f'https://ent77.seine-et-marne.fr/conversation/list/inbox?page={page}' try: resp = sess.get(url_page, headers=headers, timeout=10) if resp.status_code != 200: break messages = resp.json() except Exception: break if not isinstance(messages, list) or len(messages) == 0: break for m in messages: if m.get('id') not in {x.get('id') for x in all_messages}: all_messages.append(m) if len(messages) < 25: break page += 1 if not all_messages: flash('Aucun message dans la boîte de réception.', 'info') return redirect(url_for('ent.inbox')) config = EntConfig.query.first() imported = 0 for msg in all_messages: msg_id = str(msg.get('id')) # Vérifier si le message existe déjà existing = EntMessage.query.get(msg_id) if existing: continue # Récupérer le détail try: detail = sess.get(f'https://ent77.seine-et-marne.fr/conversation/detail/{msg_id}', headers=headers, timeout=10) if detail.status_code != 200: continue detail_json = detail.json() except Exception: continue body_html = detail_json.get('body', '') body_text = re.sub('<[^<]+?>', '', body_html) sender_display, is_forwarded, original_sender = extract_sender(detail_json) subject = detail_json.get('subject', 'N/A') ts = detail_json.get('date') msg_date = datetime.fromtimestamp(ts / 1000) if ts else datetime.now(timezone.utc) # Créer le message ent_msg = EntMessage( id=msg_id, credential_id=credential.id, subject=subject, sender_name=sender_display, sender_id=detail_json.get('from', {}).get('id', '') if isinstance(detail_json.get('from'), dict) else '', date=msg_date, body=body_html, is_forwarded=is_forwarded, original_sender=original_sender, is_unread=msg.get('unread') is True or msg.get('unread') == 1, has_attachment=detail_json.get('has_attachment', False) ) db.session.add(ent_msg) imported += 1 db.session.commit() flash(f'{imported} message(s) importé(s).', 'success') return redirect(url_for('ent.inbox')) @ent_bp.route('/message/') @login_required def message_detail(msg_id): """Détail d'un message ENT.""" from .interpretation_models import EntMessageInterpretation message = EntMessage.query.get_or_404(msg_id) # Récupérer les interprétations interpretations = EntMessageInterpretation.query.filter_by(message_id=msg_id).order_by(EntMessageInterpretation.created_at.desc()).all() return render_template('ent/message_detail.html', message=message, attachments=[], interpretations=interpretations) @ent_bp.route('/interpretations') @login_required def interpretations(): """Liste des interprétations ENT.""" # Filtres status = request.args.get('status', 'pending') interpretation_type = request.args.get('type', '') # Requête de base query = EntMessageInterpretation.query.join(EntMessage) # Appliquer les filtres if status != 'all': query = query.filter(EntMessageInterpretation.status == status) if interpretation_type: query = query.filter(EntMessageInterpretation.analysis_type == interpretation_type) # Trier par date du message interpretations = query.order_by(EntMessage.date.desc()).limit(50).all() return render_template('ent/interpretations.html', interpretations=interpretations, status=status, interpretation_type=interpretation_type) # ─── API Interprétation ───────────────────────────────────────────────────── @ent_bp.route('/api/interpret/', methods=['POST']) @login_required def api_interpret_message(msg_id): """API pour interpréter un message ENT.""" from .interpretation_models import EntMessageInterpretation from app_new.outlook.models import GmaoContext from app_new.core.models.settings import AppSettings import requests message = EntMessage.query.get_or_404(msg_id) data = request.get_json() or {} user_context = data.get('context', '') # Récupérer le contexte GMAO gmao_context = GmaoContext.get_for_user(current_user.id) if not gmao_context: return jsonify({'error': 'Contexte GMAO non configuré'}), 400 # Récupérer la clé API api_key = AppSettings.get('openrouter_api_key') if not api_key: return jsonify({'error': 'Clé API non configurée'}), 400 # Construire le prompt prompt = f"""Analyse ce message de la plateforme ENT77 et propose une action GMAO. Contexte de l'établissement: {gmao_context.establishment_name or 'Non défini'} {gmao_context.technical_context or ''} {gmao_context.equipment_context or ''} Message: Sujet: {message.subject} Expéditeur: {message.sender_name} Contenu: {message.body[:1000] if message.body else ''} {"Contexte additionnel: " + user_context if user_context else ""} Réponds en JSON avec: - type: curative/preventive/administrative/formation/information - action: create/update/inform - priority: low/medium/high/critical - summary: résumé court - equipment: équipement concerné (optionnel) - description: description détaillée """ # Récupérer le modèle from app_new.core.models.settings import AppSettings as AS2 model = AS2.get('openrouter_model', 'poolside/laguna-m.1:free') # Appeler l'API try: response = requests.post( 'https://openrouter.ai/api/v1/chat/completions', headers={ 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', 'HTTP-Referer': 'https://gmao-college.fr', 'X-Title': 'GMAO College' }, json={ 'model': model, 'messages': [{'role': 'user', 'content': prompt}], 'max_tokens': 2000 }, timeout=30 ) content = response.json()['choices'][0]['message']['content'] # Parser le JSON import json content = content.strip() if content.startswith('```json'): content = content[7:] if content.startswith('```'): content = content[3:] if content.endswith('```'): content = content[:-3] data = json.loads(content) # Créer l'interprétation interpretation = EntMessageInterpretation( message_id=message.id, analysis_type=data.get('type', 'information'), analysis_action=data.get('action', 'inform'), analysis_priority=data.get('priority', 'medium'), analysis_summary=data.get('summary'), suggested_equipment=data.get('equipment'), suggested_description=data.get('description'), status='pending' ) db.session.add(interpretation) db.session.commit() return jsonify({'success': True, 'interpretation_id': interpretation.id}) except Exception as e: return jsonify({'error': str(e)}), 500 @ent_bp.route('/api/interpretation//reject', methods=['POST']) @login_required def reject_ent_interpretation(interp_id): """Rejette une interprétation ENT, déplace le message vers 'Traités' et supprime de la base.""" from app_new.lib_ext.ent_service import move_ent_message_to_processed interpretation = EntMessageInterpretation.query.get_or_404(interp_id) message = interpretation.message move_result = None # Déplacer le message vers "Traités" if message: move_result = move_ent_message_to_processed(interpretation.message_id) # Supprimer l'interprétation db.session.delete(interpretation) # Supprimer le message de la base GMAO if message: db.session.delete(message) db.session.commit() if move_result and move_result.get('success'): return jsonify({'success': True, 'message': 'Interprétation rejetée. Message déplacé et supprimé de la base.'}) elif move_result: return jsonify({'success': True, 'warning': f'Interprétation rejetée. Attention: {move_result.get("error")}'}) else: return jsonify({'success': True, 'message': 'Interprétation rejetée.'})