347 lines
14 KiB
Python
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'))
|