from flask import current_app, session from app_new.extensions import db from app_new.core.models import Alert, Company, Equipment, Part, Intervention, INTERVENTION_STATUSES, User import json from datetime import datetime, timezone, timedelta def format_datetime(value): """Formate une date/heure pour l'affichage""" if not value: return "" try: return value.strftime('%d/%m/%Y %H:%M') except: return str(value) def format_date(value): """Formate une date pour l'affichage""" if not value: return "" try: return value.strftime('%d/%m/%Y') except: return str(value) def get_user_session(): """Récupérer l'utilisateur de la session""" if 'user_id' in session: return User.query.get(session['user_id']) return None def requires_permission(user, permission): """Vérifier si l'utilisateur a une permission""" if not user: return False # Compatibilité pour les anciens appelants : la décision est déléguée au # RBAC SQL centralisé et ne lit plus User.role. from app_new.core.authorization import has_permission normalized = permission.replace(':', '.').replace('_', '.') return has_permission(normalized, user) def log_action(user, action, data=None): """Loguer une action utilisateur""" try: # Log simple pour l'instant current_app.logger.info(f"User action: {user.username if user else 'anonymous'} - {action} - {data}") except Exception as e: current_app.logger.error(f"Error logging action: {str(e)}") def log_user_action(user, action, data=None): """Loguer une action utilisateur avec plus de détails""" try: log_data = { 'user_id': user.id if user else None, 'username': user.username if user else 'anonymous', 'action': action, 'data': data, 'timestamp': datetime.now(timezone.utc).isoformat() } current_app.logger.info(f"User action: {json.dumps(log_data)}") except Exception as e: current_app.logger.error(f"Error logging user action: {str(e)}") def generate_alerts(): """Génère les alertes automatiques : récurrence, stock bas, échéances préventif, workflow.""" new_alerts = [] # 1) Alertes récurrence : 3+ interventions curatives sur un même équipement equipments = Equipment.query.all() for eq in equipments: curative_count = eq.interventions.filter_by(type="curatif").count() existing = Alert.query.filter_by( equipment_id=eq.id, type="recurrence_equipement" ).first() if curative_count >= 3: msg = (f"⚠️ {eq.name} : {curative_count} interventions curatives. " f"Travaux profonds recommandés.") if existing: existing.message = msg existing.severity = "critical" else: alert = Alert(type="recurrence_equipement", severity="critical", message=msg, equipment_id=eq.id) new_alerts.append(alert) elif existing: db.session.delete(existing) # 2) Alertes stock bas parts = Part.query.all() for part in parts: if part.is_low_stock: existing = Alert.query.filter_by( part_id=part.id, type="stock_bas" ).first() msg = f"📦 Stock bas : {part.name} — {part.quantity}/{part.min_quantity} {part.unit}" if existing: existing.message = msg existing.severity = "warning" else: alert = Alert(type="stock_bas", severity="warning", message=msg, part_id=part.id) new_alerts.append(alert) # 3) Alertes échéances préventif (uniquement si échéance dans les 7 prochains jours) now = datetime.now(timezone.utc) week_later = now + timedelta(days=7) upcoming = Intervention.query.filter( Intervention.type == "preventif", Intervention.is_recurring == True, Intervention.next_due_date != None, Intervention.next_due_date <= week_later, Intervention.status.in_(["brouillon", "en_cours", "attente_chef"]), ).all() for interv in upcoming: existing = Alert.query.filter_by( intervention_id=interv.id, type="preventive_echeance" ).first() if not existing: msg = (f"📅 Maintenance préventive à échéance : {interv.title} " f"(équipement : {interv.equipment.name})") alert = Alert(type="preventive_echeance", severity="info", message=msg, intervention_id=interv.id, equipment_id=interv.equipment_id) new_alerts.append(alert) # 4) Alertes interventions en attente chef depuis +3 jours three_days_ago = datetime.now(timezone.utc) - timedelta(days=3) pending_chef = Intervention.query.filter( Intervention.status == "attente_chef", Intervention.requested_at <= three_days_ago ).all() for interv in pending_chef: existing = Alert.query.filter_by( intervention_id=interv.id, type="attente_chef" ).first() if not existing: msg = (f"⏳ Intervention #{interv.id} en attente du chef depuis +3 jours : {interv.title}") alert = Alert(type="attente_chef", severity="warning", message=msg, intervention_id=interv.id) new_alerts.append(alert) # 5) Alertes interventions en demande GIMA depuis +7 jours seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7) pending_gima = Intervention.query.filter( Intervention.status == "demande_gima", Intervention.updated_at <= seven_days_ago ).all() for interv in pending_gima: existing = Alert.query.filter_by( intervention_id=interv.id, type="gima_relance" ).first() if not existing: msg = (f"🏛️ Relance GIMA : Intervention #{interv.id} en demande GIMA depuis +7 jours : {interv.title}") alert = Alert(type="gima_relance", severity="info", message=msg, intervention_id=interv.id) new_alerts.append(alert) db.session.add_all(new_alerts) db.session.commit() return new_alerts def get_dashboard_stats(): """Statistiques pour le tableau de bord.""" stats = {} stats["interventions_total"] = Intervention.query.count() # Compter par statut du nouveau workflow for status_key in INTERVENTION_STATUSES: stats[f"interventions_{status_key}"] = Intervention.query.filter_by(status=status_key).count() # Stats rapides pour les cartes stats["interventions_en_attente_chef"] = Intervention.query.filter_by(status="attente_chef").count() stats["interventions_en_cours"] = Intervention.query.filter_by(status="en_cours").count() stats["interventions_demande_gima"] = Intervention.query.filter_by(status="demande_gima").count() stats["interventions_curatif"] = Intervention.query.filter_by(type="curatif").count() stats["interventions_preventif"] = Intervention.query.filter_by(type="preventif").count() stats["interventions_recurrentes"] = Intervention.query.filter_by(is_recurring=True).count() # Échéances préventif now = datetime.now(timezone.utc) week_later = now + timedelta(days=7) month_later = now + timedelta(days=30) stats["preventif_overdue"] = Intervention.query.filter( Intervention.is_recurring == True, Intervention.next_due_date < now ).count() stats["preventif_this_week"] = Intervention.query.filter( Intervention.is_recurring == True, Intervention.next_due_date >= now, Intervention.next_due_date <= week_later ).count() stats["preventif_this_month"] = Intervention.query.filter( Intervention.is_recurring == True, Intervention.next_due_date > week_later, Intervention.next_due_date <= month_later ).count() stats["equipments_total"] = Equipment.query.count() stats["equipments_hors_service"] = Equipment.query.filter( Equipment.status.in_(["hors_service", "out_of_service"]) ).count() stats["parts_low_stock"] = Part.query.filter(Part.quantity <= Part.min_quantity).count() stats["alerts_unread"] = Alert.query.filter_by(is_read=False).count() stats["alerts_total"] = Alert.query.count() stats["entreprises_actives"] = Company.query.filter_by(is_active=True).count() return stats def format_status(status): """Convertit le statut technique en label lisible.""" from app_new.core.models import INTERVENTION_STATUSES, EQUIPMENT_STATUSES if status in INTERVENTION_STATUSES: return INTERVENTION_STATUSES[status]["label"] if status in EQUIPMENT_STATUSES: return EQUIPMENT_STATUSES[status]["label"] # Fallback pour statuts inconnus mapping = { "en_service": "En service", "hors_service": "Hors service", "en_reparation": "En réparation", "remplace": "Remplacé", } return mapping.get(status, status) def status_badge_class(status): """Retourne la classe CSS Bootstrap pour un statut.""" from app_new.core.models import INTERVENTION_STATUSES, EQUIPMENT_STATUSES if status in INTERVENTION_STATUSES: color = INTERVENTION_STATUSES[status].get("color", "secondary") if color == "purple": return "bg-purple" return f"bg-{color}" if status in EQUIPMENT_STATUSES: color = EQUIPMENT_STATUSES[status].get("color", "secondary") return f"bg-{color}" mapping = { "en_service": "bg-success", "hors_service": "bg-danger", "en_reparation": "bg-warning text-dark", "remplace": "bg-secondary", } return mapping.get(status, "bg-secondary") def priority_badge_class(priority): """Retourne la classe CSS Bootstrap pour une priorité.""" from app_new.core.models import PRIORITIES if priority in PRIORITIES: return f"bg-{PRIORITIES[priority]['color']}" mapping = { "basse": "bg-light text-dark", "normale": "bg-info", "haute": "bg-warning text-dark", "urgente": "bg-danger", } return mapping.get(priority, "bg-secondary")