gmao/app_new/ent/routes.py

438 lines
16 KiB
Python
Raw Normal View History

2026-08-14 18:02:25 +02:00
"""
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)
2026-08-14 18:02:25 +02:00
@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/<msg_id>')
@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/<msg_id>', 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/<int:interp_id>/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.'})