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."""
|
2026-08-15 00:47:55 +02:00
|
|
|
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:
|
2026-08-15 00:47:55 +02:00
|
|
|
return jsonify({'success': True, 'message': 'Interprétation rejetée.'})
|