gmao/app_new/core/routes/dashboard.py
root a18e0f2713
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Ajouter les alertes de lots au tableau de bord
2026-08-19 23:46:52 +00:00

347 lines
14 KiB
Python

"""
Core Routes - Dashboard
GMAO Collège
"""
from flask import Blueprint, render_template, request, url_for, flash, redirect, jsonify
from flask_login import login_required, current_user
from datetime import datetime
from ...extensions import db
from ..models.maintenance import Intervention
from ..models.equipment import Equipment
from ..models.company import Company
from ..models.college import Building, Room
from ..models.company import Part
from app_new.constants import INTERVENTION_STATUSES, EQUIPMENT_STATUSES
from sqlalchemy import func
dashboard_bp = Blueprint('dashboard', __name__)
@dashboard_bp.route('/')
@login_required
def index():
"""Tableau de bord principal."""
from datetime import date
from ..services.planning_service import PlanningService
from ..models.planning import ScheduledTask, AdminTask
from ..models.maintenance import Lot
# Statistiques
stats = {
'interventions_total': Intervention.query.count(),
'interventions_en_cours': Intervention.query.filter_by(status='en_cours').count(),
'interventions_en_attente': Intervention.query.filter_by(status='en_attente').count(),
'interventions_urgentes': Intervention.query.filter_by(priority='urgente', is_deleted=False).count(),
'equipments_total': Equipment.query.count(),
'equipments_actifs': Equipment.query.filter_by(status='en_service', is_deleted=False).count(),
'equipments_panne': Equipment.query.filter(Equipment.status.in_(['en_panne', 'hors_service', 'hs']), Equipment.is_deleted.is_(False)).count(),
'buildings_total': Building.query.count(),
'rooms_total': Room.query.count(),
'companies_total': Company.query.count(),
'parts_low_stock': Part.query.filter(Part.quantity <= Part.min_quantity).count(),
}
# Contrôle des lots : un lot présent doit être rattaché à au moins un
# équipement, sauf s'il a été explicitement marqué « non présent ».
equipment_counts = dict(
db.session.query(Equipment.lot_id, func.count(Equipment.id))
.filter(
Equipment.lot_id.isnot(None),
Equipment.is_deleted.is_(False),
db.or_(Equipment.status.is_(None), Equipment.status != 'jete'),
)
.group_by(Equipment.lot_id)
.all()
)
lots = Lot.query.order_by(Lot.name).all()
lots_without_equipment = [
lot for lot in lots
if lot.is_present and equipment_counts.get(lot.id, 0) == 0
]
lots_missing_duration = []
for lot in lots:
if not lot.is_present or equipment_counts.get(lot.id, 0) == 0:
continue
active_tasks = [task for task in lot.tasks if task.is_active]
missing_tasks = [
task for task in lot.tasks
if task.is_active and not task.duree_minutes
]
if missing_tasks or not active_tasks:
lots_missing_duration.append({
'lot': lot,
'tasks': missing_tasks,
'no_task': not active_tasks,
})
stats['lots_without_equipment'] = len(lots_without_equipment)
stats['lots_missing_duration'] = len(lots_missing_duration)
# Dernières interventions
recent_interventions = Intervention.query.order_by(Intervention.created_at.desc()).limit(10).all()
# Interventions urgentes
urgent_interventions = Intervention.query.filter_by(priority='urgente', is_deleted=False).limit(5).all()
# Interventions du jour (planning)
today = date.today()
day_schedule = PlanningService.get_day_schedule(today)
# Interprétations en attente (non traitées)
from ...outlook.models import OutlookMailInterpretation
from ...ent.interpretation_models import EntMessageInterpretation
# Récupérer les interprétations Outlook
outlook_interpretations = OutlookMailInterpretation.query.filter_by(
status='pending',
user_id=current_user.id
).order_by(OutlookMailInterpretation.created_at.desc()).limit(10).all()
# Récupérer les interprétations ENT
ent_interpretations = EntMessageInterpretation.query.filter_by(
status='pending'
).order_by(EntMessageInterpretation.created_at.desc()).limit(10).all()
# Combiner les deux listes avec une source
pending_interpretations = []
for interp in outlook_interpretations:
pending_interpretations.append({
'id': interp.id,
'source': 'Outlook',
'date': interp.mail.received_at if interp.mail else None,
'sender': interp.mail.from_name if interp.mail else 'Inconnu',
'subject': interp.mail.subject if interp.mail else 'N/A',
'type': interp.analysis_type,
'action': interp.analysis_action,
'description': interp.suggested_description,
'equipment': interp.suggested_equipment,
'confidence': getattr(interp, 'analysis_confidence', 0.5),
'detail_url': url_for('outlook_pages.mail_view', account_id=interp.mail.account_id, mail_id=interp.mail_id) if interp.mail else url_for('outlook_dashboard.all_interpretations'),
'create_url': url_for('outlook_dashboard.create_intervention_from_interpretation', interp_id=interp.id)
})
for interp in ent_interpretations:
pending_interpretations.append({
'id': interp.id,
'source': 'ENT',
'date': interp.message.date if interp.message else None,
'sender': interp.message.sender_name if interp.message else 'Inconnu',
'subject': interp.message.subject if interp.message else 'N/A',
'type': interp.analysis_type,
'action': interp.analysis_action,
'description': interp.suggested_description,
'equipment': interp.suggested_equipment,
'confidence': getattr(interp, 'analysis_confidence', 0.5),
'detail_url': url_for('ent.message_detail', msg_id=interp.message_id) if interp.message else url_for('ent.interpretations'),
'create_url': url_for('interventions.create', from_interpretation=interp.id, source='ent')
})
# Trier par date décroissante
pending_interpretations.sort(key=lambda x: x['date'] if x['date'] else datetime.min, reverse=True)
pending_interpretations = pending_interpretations[:15] # Limiter à 15
return render_template('dashboard/index.html',
stats=stats,
recent_interventions=recent_interventions,
urgent_interventions=urgent_interventions,
pending_interpretations=pending_interpretations,
day_schedule=day_schedule,
lots_without_equipment=lots_without_equipment,
lots_missing_duration=lots_missing_duration,
today=today,
statuses=INTERVENTION_STATUSES)
@dashboard_bp.route('/api/stats')
@login_required
def api_stats():
"""API JSON pour les graphiques du dashboard."""
from datetime import date, timedelta
from sqlalchemy import func
# Interventions par mois (6 derniers mois)
six_months_ago = date.today() - timedelta(days=180)
monthly = db.session.query(
func.date_format(Intervention.created_at, '%Y-%m').label('month'),
func.count().label('count')
).filter(Intervention.created_at >= six_months_ago).group_by('month').order_by('month').all()
monthly_data = [{'month': r.month, 'count': r.count} for r in monthly]
# Interventions par statut
status_data = {}
for status_val, info in INTERVENTION_STATUSES.items():
count = Intervention.query.filter_by(status=status_val).count()
if count > 0:
status_data[info['label']] = count
# Equipements par statut
equip_status = {}
for s in EQUIPMENT_STATUSES:
count = Equipment.query.filter_by(status=s).count()
if count > 0:
equip_status[s] = count
# Top 5 equipements les plus intervenus
top_equip = db.session.query(
Equipment.name,
func.count(Intervention.id).label('count')
).join(Intervention, Intervention.equipment_id == Equipment.id)\
.group_by(Equipment.id)\
.order_by(func.count(Intervention.id).desc())\
.limit(5).all()
top_data = [{'name': r.name, 'count': r.count} for r in top_equip]
return jsonify({
'monthly': monthly_data,
'by_status': status_data,
'equip_status': equip_status,
'top_equipments': top_data
})
@dashboard_bp.route('/search')
@login_required
def search():
"""Recherche globale."""
from flask import request
query = request.args.get('q', '')
results = {
'interventions': [],
'equipments': [],
'rooms': [],
}
if query:
# Recherche interventions
results['interventions'] = Intervention.query.filter(
db.or_(
Intervention.title.ilike(f'%{query}%'),
Intervention.description.ilike(f'%{query}%')
)
).limit(10).all()
# Recherche équipements
results['equipments'] = Equipment.query.filter(
db.or_(
Equipment.name.ilike(f'%{query}%'),
Equipment.serial_number.ilike(f'%{query}%')
)
).limit(10).all()
# Recherche salles
results['rooms'] = Room.query.filter(
Room.name.ilike(f'%{query}%')
).limit(10).all()
return render_template('dashboard/search.html', query=query, results=results)
@dashboard_bp.route('/alerts')
@login_required
def alerts():
"""Liste des alertes."""
from app_new.core.models.maintenance import Intervention
from app_new.core.models.company import Alert
alerts_list = Alert.query.filter_by(is_read=False).order_by(Alert.created_at.desc()).all()
# Interventions urgentes (statut haute priorité)
urgent_interventions = Intervention.query.filter(
Intervention.priority == 'haute',
Intervention.status.in_(['planifiee', 'en_cours', 'en_attente'])
).order_by(Intervention.scheduled_date.desc()).all()
# Équipements en panne
from app_new.core.models.equipment import Equipment
equipments_panne = Equipment.query.filter(
Equipment.status.in_(['en_panne', 'hors_service', 'hs']),
Equipment.is_deleted.is_(False),
).all()
# Équipements à jeter
equipments_to_trash = Equipment.query.filter_by(status='a_jeter').all()
# Interprétations en attente
from ...outlook.models import OutlookMailInterpretation
from ...ent.interpretation_models import EntMessageInterpretation
pending_outlook = OutlookMailInterpretation.query.filter_by(
status='pending', user_id=current_user.id
).order_by(OutlookMailInterpretation.created_at.desc()).all()
pending_ent = EntMessageInterpretation.query.filter_by(
status='pending'
).order_by(EntMessageInterpretation.created_at.desc()).all()
return render_template('dashboard/alerts.html',
alerts=alerts_list,
urgent_interventions=urgent_interventions,
equipments_panne=equipments_panne,
equipments_to_trash=equipments_to_trash,
pending_outlook=pending_outlook,
pending_ent=pending_ent)
@dashboard_bp.route('/alerts/mark-read/<int:alert_id>', methods=['POST'])
@login_required
def mark_alert_read(alert_id):
"""Marque une alerte comme lue."""
from app_new.core.models.company import Alert
alert = Alert.query.get(alert_id)
if alert:
alert.is_read = True
db.session.commit()
flash('Alerte marquée comme lue.', 'success')
return redirect(url_for('dashboard.alerts'))
@dashboard_bp.route('/create-month-interventions', methods=['POST'])
@login_required
def create_month_interventions():
"""Créer les interventions pour les 30 prochains jours à partir des tâches planifiées."""
from datetime import date, timedelta
from ..models.planning import ScheduledTask
from ..models.maintenance import Intervention
from ..models.company import Company
from flask_login import current_user
today = date.today()
one_month_later = today + timedelta(days=30)
# Récupérer les tâches planifiées dans le mois (uniquement avec équipement)
tasks = db.session.query(ScheduledTask).filter(
ScheduledTask.equipment_id.isnot(None),
ScheduledTask.scheduled_date >= today,
ScheduledTask.scheduled_date <= one_month_later,
ScheduledTask.status.in_(['planned', 'scheduled'])
).all()
created = 0
for task in tasks:
# Vérifier si l'intervention existe déjà
existing = db.session.query(Intervention).filter(
Intervention.equipment_id == task.equipment_id,
Intervention.scheduled_date == task.scheduled_date,
Intervention.scheduled_start == task.scheduled_start
).first()
if not existing:
intervention = Intervention(
title=f"Intervention: {task.preventive_task.name if task.preventive_task else (task.lot_task.tache if task.lot_task else 'Tâche planifiée')}",
description=task.notes or '',
equipment_id=task.equipment_id,
room_id=task.room_id,
scheduled_date=task.scheduled_date,
scheduled_start=task.scheduled_start,
scheduled_end=task.scheduled_end,
estimated_duration=task.estimated_duration or 30,
status='en_attente',
type='preventif',
assigned_to_id=task.assigned_to_id,
company_id=task.company_id,
author_id=current_user.id,
requester_name='Système automatique'
)
db.session.add(intervention)
created += 1
db.session.commit()
flash(f'{created} interventions créées pour les 30 prochains jours', 'success')
return redirect(url_for('dashboard.index'))