""" 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/', 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'))