2026-08-14 18:02:25 +02:00
{% extends "base.html" %}
{% block title %}Tableau de bord — GMAO Collège{% endblock %}
{% block extra_head %}
< style >
.bg-purple { background-color: #6f42c1 !important; }
.bg-purple.bg-opacity-10 { background-color: rgba(111, 66, 193, 0.1) !important; }
< / style >
{% endblock %}
{% block content %}
< h1 class = "mb-3 h3" > < i class = "bi bi-speedometer2" > < / i > Tableau de bord< / h1 >
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.ai %}<!-- Alerte modele IA -->
2026-08-14 18:02:25 +02:00
< div id = "ai-alert" style = "display:none;" class = "alert alert-danger mb-3" >
< i class = "bi bi-exclamation-triangle" > < / i >
< strong > Modele IA indisponible !< / strong >
< span id = "ai-alert-msg" > < / span >
< a href = "{{ url_for('ai_config.index') }}" class = "alert-link" > Configurer le modele< / a >
< / div >
2026-08-21 19:35:46 +02:00
{% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.planning %}<!-- Alerte retards -->
2026-08-14 18:02:25 +02:00
< div id = "overdue-alert" style = "display:none;" class = "alert alert-warning mb-3" >
< i class = "bi bi-clock-history" > < / i >
< strong > Retards detectes !< / strong >
< span id = "overdue-alert-msg" > < / span >
< a href = "{{ url_for('scheduler.index') }}" class = "alert-link" > Voir le planificateur< / a >
< / div >
2026-08-21 19:35:46 +02:00
{% endif %}
2026-08-14 18:02:25 +02:00
<!-- Stats rapides -->
< div class = "row g-2 g-md-3 mb-3" >
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.intervention_manage or dashboard_permissions.planning_manage %}< div class = "col-12" >
2026-08-14 18:02:25 +02:00
< form method = "POST" action = "{{ url_for('dashboard.create_month_interventions') }}" class = "d-inline" onsubmit = "return confirm('Créer les interventions pour le mois prochain?')" >
< button type = "submit" class = "btn btn-primary btn-sm" >
< i class = "bi bi-calendar-plus me-1" > < / i > Créer interventions (1 mois)
< / button >
< / form >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.intervention %}< div class = "col-6 col-md-3" >
2026-08-14 18:02:25 +02:00
< div class = "card stat-card text-white bg-primary" >
< div class = "card-body py-2 px-3" >
< small class = "card-title" > Interventions< / small >
< h3 class = "mb-0" > {{ stats.interventions_total }}< / h3 >
< small > {{ stats.interventions_en_cours }} en cours< / small >
< / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
{% if dashboard_permissions.intervention %}< div class = "col-6 col-md-3" >
2026-08-14 18:02:25 +02:00
< div class = "card stat-card text-white bg-success" >
< div class = "card-body py-2 px-3" >
< small class = "card-title" > Curatif / Préventif< / small >
< h3 class = "mb-0" > {{ stats.interventions_curatif }} / {{ stats.interventions_preventif }}< / h3 >
< small > {{ stats.interventions_recurrentes }} récurrentes< / small >
< / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
{% if dashboard_permissions.patrimoine %}< div class = "col-6 col-md-3" >
2026-08-14 18:02:25 +02:00
< div class = "card stat-card text-white bg-warning" >
< div class = "card-body py-2 px-3" >
< small class = "card-title" > Équipements< / small >
< h3 class = "mb-0" > {{ stats.equipments_total }}< / h3 >
< small > {{ stats.equipments_hors_service }} hors service< / small >
< / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
{% if dashboard_permissions.stock %}< div class = "col-6 col-md-3" >
2026-08-14 18:02:25 +02:00
< div class = "card stat-card {% if stats.parts_low_stock > 0 %}bg-danger{% else %}bg-info{% endif %} text-white" >
< div class = "card-body py-2 px-3" >
< small class = "card-title" > Stock bas< / small >
< h3 class = "mb-0" > {{ stats.parts_low_stock }}< / h3 >
< small > pièce(s)< / small >
< / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
2026-08-14 18:02:25 +02:00
< / div >
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.planning %}<!-- Échéances préventif -->
2026-08-14 18:02:25 +02:00
< div class = "row g-2 g-md-3 mb-3" >
< div class = "col-6 col-md-3" >
< a href = "{{ url_for('interventions_planning.planning') }}" class = "text-decoration-none" >
< div class = "card border-danger h-100" >
< div class = "card-body py-2 px-3 text-center" >
< h3 class = "text-danger mb-0" > {{ stats.preventif_overdue }}< / h3 >
< small class = "text-muted" > En retard< / small >
< / div >
< / div >
< / a >
< / div >
< div class = "col-6 col-md-3" >
< a href = "{{ url_for('interventions_planning.planning') }}" class = "text-decoration-none" >
< div class = "card border-warning h-100" >
< div class = "card-body py-2 px-3 text-center" >
< h3 class = "text-warning mb-0" > {{ stats.preventif_this_week }}< / h3 >
< small class = "text-muted" > Cette semaine< / small >
< / div >
< / div >
< / a >
< / div >
< div class = "col-6 col-md-3" >
< a href = "{{ url_for('interventions_planning.planning') }}" class = "text-decoration-none" >
< div class = "card border-info h-100" >
< div class = "card-body py-2 px-3 text-center" >
< h3 class = "text-info mb-0" > {{ stats.preventif_this_month }}< / h3 >
< small class = "text-muted" > Ce mois< / small >
< / div >
< / div >
< / a >
< / div >
< div class = "col-6 col-md-3" >
< a href = "{{ url_for('interventions.index', type='preventif', frequency='recurring') }}" class = "text-decoration-none" >
< div class = "card border-secondary h-100" >
< div class = "card-body py-2 px-3 text-center" >
< h3 class = "text-secondary mb-0" > {{ stats.interventions_recurrentes }}< / h3 >
< small class = "text-muted" > Récurrentes< / small >
< / div >
< / div >
< / a >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.patrimoine %}<!-- Contrôles des référentiels de maintenance -->
2026-08-20 01:46:52 +02:00
< div class = "row g-2 g-md-3 mb-3" >
< div class = "col-lg-6" >
< div class = "card h-100 {% if stats.lots_without_equipment %}border-warning{% else %}border-success{% endif %}" >
< div class = "card-header {% if stats.lots_without_equipment %}bg-warning text-dark{% else %}bg-success text-white{% endif %} py-2" >
< i class = "bi bi-box-seam me-1" > < / i > Lots présents sans équipement
< span class = "badge bg-light text-dark ms-1" > {{ stats.lots_without_equipment }}< / span >
< / div >
< div class = "card-body py-2" >
{% if lots_without_equipment %}
< div class = "list-group list-group-flush" >
{% for lot in lots_without_equipment[:8] %}
< a class = "list-group-item list-group-item-action px-0 py-1 d-flex justify-content-between" href = "{{ url_for('lots.detail', id=lot.id) }}" >
< span > {{ lot.name }}< / span > < i class = "bi bi-chevron-right" > < / i >
< / a >
{% endfor %}
< / div >
{% if lots_without_equipment|length > 8 %}< small class = "text-muted" > {{ lots_without_equipment|length - 8 }} autre(s)< / small > {% endif %}
{% else %}
< span class = "text-success" > < i class = "bi bi-check-circle me-1" > < / i > Tous les lots présents ont un équipement.< / span >
{% endif %}
< / div >
< / div >
< / div >
< div class = "col-lg-6" >
< div class = "card h-100 {% if stats.lots_missing_duration %}border-danger{% else %}border-success{% endif %}" >
< div class = "card-header {% if stats.lots_missing_duration %}bg-danger text-white{% else %}bg-success text-white{% endif %} py-2" >
< i class = "bi bi-stopwatch me-1" > < / i > Lots équipés avec durée manquante
< span class = "badge bg-light text-dark ms-1" > {{ stats.lots_missing_duration }}< / span >
< / div >
< div class = "card-body py-2" >
{% if lots_missing_duration %}
< div class = "list-group list-group-flush" >
{% for item in lots_missing_duration[:8] %}
< div class = "list-group-item px-0 py-1" >
< div class = "d-flex justify-content-between align-items-center" >
< a href = "{{ url_for('lots.detail', id=item.lot.id) }}" > < strong > {{ item.lot.name }}< / strong > < / a >
{% if item.no_task %}< span class = "badge bg-danger" > Aucune tâche< / span > {% else %}< span class = "badge bg-danger" > {{ item.tasks|length }} tâche(s)< / span > {% endif %}
< / div >
{% if item.no_task %}
< a class = "small text-decoration-none d-block ms-2" href = "{{ url_for('lots.detail', id=item.lot.id) }}" >
< i class = "bi bi-plus-circle me-1" > < / i > Ajouter une tâche et définir sa durée
< / a >
{% endif %}
{% for task in item.tasks[:3] %}
< a class = "small text-decoration-none d-block ms-2" href = "{{ url_for('lots.task_edit', lot_id=item.lot.id, task_id=task.id) }}" >
< i class = "bi bi-pencil me-1" > < / i > {{ task.tache or 'Tâche sans nom' }} — définir la durée
< / a >
{% endfor %}
< / div >
{% endfor %}
< / div >
{% else %}
< span class = "text-success" > < i class = "bi bi-check-circle me-1" > < / i > Toutes les tâches actives ont une durée définie.< / span >
{% endif %}
< / div >
< / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
2026-08-20 01:46:52 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.intervention or dashboard_permissions.patrimoine or dashboard_permissions.ent or dashboard_permissions.outlook %}<!-- Alertes -->
2026-08-14 18:02:25 +02:00
{% if alerts %}
< div class = "card border-warning mb-3" >
< div class = "card-header bg-warning text-dark py-2" >
< i class = "bi bi-exclamation-triangle" > < / i > Alertes récentes
< / div >
< div class = "card-body p-2" >
{% for alert in alerts[:5] %}
< div class = "d-flex justify-content-between align-items-center mb-1 p-2 rounded {% if alert.severity == 'critical' %}bg-danger text-white{% elif alert.severity == 'warning' %}bg-warning text-dark{% else %}bg-info text-white{% endif %}" style = "font-size:0.85rem;" >
< span > {{ alert.message[:100] }}{% if alert.message|length > 100 %}…{% endif %}< / span >
< / div >
{% endfor %}
{% if alerts|length > 5 %}
< a href = "{{ url_for('dashboard.alerts') }}" class = "btn btn-sm btn-outline-warning mt-1" > Voir tout ({{ alerts|length }})< / a >
{% endif %}
< / div >
< / div >
2026-08-21 19:35:46 +02:00
{% endif %}{% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.planning %}<!-- Maintenances du jour -->
2026-08-14 18:02:25 +02:00
{% if day_schedule and day_schedule.items %}
< div class = "card border-primary mb-3" >
< div class = "card-header bg-primary text-white py-2" >
< i class = "bi bi-calendar-check" > < / i > Maintenances du jour
< span class = "badge bg-light text-primary ms-2" > {{ day_schedule.items|length }}< / span >
< / div >
< div class = "card-body p-0" >
< div class = "table-responsive" >
< table class = "table table-hover mb-0" style = "font-size:0.85rem;" >
< thead >
< tr >
< th > Heure< / th >
< th > Type< / th >
< th > Description< / th >
< th > Salle< / th >
< th > Durée< / th >
< / tr >
< / thead >
< tbody >
{% for item in day_schedule.items %}
< tr >
< td >
{% if item.start_time %}
{{ item.start_time.strftime('%H:%M') if item.start_time.strftime else item.start_time }}
{% else %}
—
{% endif %}
< / td >
< td >
{% if item.type == 'admin' %}
< span class = "badge bg-success" > Admin< / span >
{% elif item.type == 'curative' %}
< span class = "badge bg-danger" > Curative< / span >
{% elif item.type == 'preventive' %}
< span class = "badge bg-primary" > Préventive< / span >
{% elif item.type == 'formation' %}
< span class = "badge bg-warning text-dark" > Formation< / span >
{% else %}
< span class = "badge bg-secondary" > {{ item.type }}< / span >
{% endif %}
< / td >
< td > {{ item.title }}< / td >
< td >
{% if item.room_name %}
< span class = "badge bg-info" > {{ item.room_name }}< / span >
{% else %}
—
{% endif %}
< / td >
< td >
{% if item.duration %}
{{ item.duration }} min
{% else %}
—
{% endif %}
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< / div >
< div class = "p-2 text-center" >
< a href = "{{ url_for('planning.day', date_str=today.strftime('%Y-%m-%d')) }}" class = "btn btn-sm btn-outline-primary" >
Voir le planning complet
< / a >
< / div >
< / div >
< / div >
{% elif day_schedule and day_schedule.is_working %}
< div class = "card border-secondary mb-3" >
< div class = "card-header bg-secondary text-white py-2" >
< i class = "bi bi-calendar-check" > < / i > Maintenances du jour
< / div >
< div class = "card-body p-3 text-center text-muted" >
< i class = "bi bi-check-circle fs-1" > < / i >
< p class = "mb-0" > Aucune maintenance planifiée pour aujourd'hui.< / p >
{% if day_schedule.working_hours %}
< small class = "text-muted" > Horaires: {{ day_schedule.working_hours.start.strftime('%H:%M') }} - {{ day_schedule.working_hours.end.strftime('%H:%M') }}< / small >
{% endif %}
< / div >
< / div >
{% elif day_schedule and day_schedule.closure %}
< div class = "card border-info mb-3" >
< div class = "card-header bg-info text-white py-2" >
< i class = "bi bi-calendar-x" > < / i > {{ day_schedule.closure }}
< / div >
< div class = "card-body p-3 text-center" >
< i class = "bi bi-snow fs-1" > < / i >
< p class = "mb-0" > Jour non travaillé< / p >
< / div >
< / div >
{% elif day_schedule and day_schedule.leave %}
< div class = "card border-warning mb-3" >
< div class = "card-header bg-warning text-dark py-2" >
< i class = "bi bi-calendar-x" > < / i > Congé
< / div >
< div class = "card-body p-3 text-center" >
< p class = "mb-0" > {{ day_schedule.leave }}< / p >
< / div >
< / div >
2026-08-21 19:35:46 +02:00
{% endif %}{% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.planning %}<!-- 2 colonnes : Maintenances + Fréquence -->
2026-08-14 18:02:25 +02:00
< div class = "row g-2 g-md-3 mb-3" >
<!-- Maintenances à venir -->
< div class = "col-12 col-md-6" >
< div class = "card h-100 border-info" >
< div class = "card-header bg-info text-white py-2" >
< i class = "bi bi-calendar-event" > < / i > Maintenances à venir
< / div >
< div class = "card-body p-0" >
{% if upcoming_maintenances %}
< div class = "list-group list-group-flush" >
{% for interv in upcoming_maintenances[:10] %}
{% set days_until = (interv.next_due_date - now).days %}
< div class = "list-group-item px-3 py-2 {% if days_until < 0 %}list-group-item-danger{% elif days_until < 7 %}list-group-item-warning{% endif %}" >
< div class = "d-flex justify-content-between align-items-start" >
< div class = "me-2" style = "min-width:0;" >
< a href = "{{ url_for('interventions.detail', id=interv.id) }}" class = "fw-bold text-decoration-none text-truncate d-block" > {{ interv.title }}< / a >
< small class = "text-muted d-block text-truncate" >
2026-08-22 15:10:39 +02:00
< i class = "bi bi-pc-display" > < / i > {% if interv.equipment %}{{ interv.equipment.name }}{% elif interv.room %}{{ interv.room.name }}{% else %}Aucune localisation{% endif %}
2026-08-14 18:02:25 +02:00
{% if interv.frequency_days %}
· < i class = "bi bi-arrow-repeat" > < / i > {{ frequency_labels.get(interv.frequency_days, interv.frequency_days ~ "j") }}
{% endif %}
< / small >
< / div >
< div class = "text-end flex-shrink-0" >
{% if days_until < 0 % }
< span class = "badge bg-danger" > En retard< / span >
{% elif days_until < 7 % }
< span class = "badge bg-warning text-dark" > ~{{ days_until }}j< / span >
{% else %}
< span class = "badge bg-primary" > ~{{ days_until }}j< / span >
{% endif %}
< / div >
< / div >
< / div >
{% endfor %}
< / div >
{% if upcoming_maintenances|length > 10 %}
< div class = "p-2 text-center" >
< a href = "{{ url_for('interventions_planning.planning') }}" class = "btn btn-sm btn-outline-info" > Voir tout le planning< / a >
< / div >
{% endif %}
{% else %}
< div class = "p-3 text-muted" > < i class = "bi bi-check-circle" > < / i > Aucune maintenance à venir.< / div >
{% endif %}
< / div >
< / div >
< / div >
<!-- Répartition fréquence -->
< div class = "col-12 col-md-6" >
< div class = "card h-100 border-secondary" >
< div class = "card-header bg-secondary text-white py-2" >
< i class = "bi bi-bar-chart" > < / i > Répartition par fréquence
< / div >
< div class = "card-body py-2" >
{% if frequency_data %}
{% for item in frequency_data %}
< div class = "d-flex justify-content-between align-items-center mb-1" style = "font-size:0.85rem;" >
< span class = "fw-bold me-2" > {{ item.label }}< / span >
< div class = "d-flex align-items-center" style = "min-width: 80px;" >
< div class = "progress flex-grow-1 me-1" style = "height: 16px;" >
{% set max_count = frequency_data|map(attribute='count')|max %}
{% set pct = (item.count / max_count * 100)|int if max_count > 0 else 0 %}
< div class = "progress-bar bg-primary" style = "width: {{ pct }}%" > {{ item.count }}< / div >
< / div >
< / div >
< / div >
{% endfor %}
{% else %}
< p class = "text-muted mb-0" > < i class = "bi bi-info-circle" > < / i > Aucune donnée.< / p >
{% endif %}
< / div >
< / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.intervention %}<!-- Workflow + Activité récente -->
2026-08-14 18:02:25 +02:00
< div class = "row g-2 g-md-3 mb-3" >
< div class = "col-12 col-md-6" >
< div class = "card h-100 border-warning" >
< div class = "card-header bg-warning text-dark py-2" >
< i class = "bi bi-hourglass-split" > < / i > Workflow en attente
< / div >
< div class = "card-body py-2" >
{% if workflow_pending_data %}
{% for item in workflow_pending_data %}
< div class = "d-flex justify-content-between align-items-center p-2 mb-1 rounded {% if item.color == 'purple' %}bg-purple bg-opacity-10{% else %}bg-{{ item.color }} bg-opacity-10{% endif %}" style = "font-size:0.85rem;" >
< div > < i class = "bi bi-{{ item.icon }}" > < / i > < span class = "fw-bold" > {{ item.label }}< / span > < / div >
< span class = "badge {% if item.color == 'purple' %}bg-purple{% else %}bg-{{ item.color }}{% endif %}" > {{ item.count }}< / span >
< / div >
{% endfor %}
{% else %}
< p class = "text-muted mb-0" > < i class = "bi bi-check-circle-fill" > < / i > Rien en attente.< / p >
{% endif %}
< / div >
< / div >
< / div >
< div class = "col-12 col-md-6" >
< div class = "card h-100 border-success" >
< div class = "card-header bg-success text-white py-2" >
< i class = "bi bi-activity" > < / i > Activité récente
< / div >
< div class = "card-body p-0" >
{% if recent_status_changes %}
< div class = "list-group list-group-flush" >
{% for change in recent_status_changes[:8] %}
< div class = "list-group-item px-3 py-2" style = "font-size:0.82rem;" >
< div class = "d-flex justify-content-between" >
< div style = "min-width:0;" >
< small >
< strong > {{ change.changed_by.full_name if change.changed_by else '?' }}< / strong >
· < a href = "{{ url_for('interventions.detail', id=change.intervention_id) }}" > #{{ change.intervention_id }}< / a >
< / small >
< div class = "mt-1" >
< span class = "badge {% if change.from_color == 'purple' %}bg-purple{% else %}bg-{{ change.from_color }}{% endif %}" style = "font-size:0.7rem;" > {{ change.from_label }}< / span >
< i class = "bi bi-arrow-right mx-1" style = "font-size:0.6rem;" > < / i >
< span class = "badge {% if change.to_color == 'purple' %}bg-purple{% else %}bg-{{ change.to_color }}{% endif %}" style = "font-size:0.7rem;" > {{ change.to_label }}< / span >
< / div >
< / div >
< small class = "text-muted text-nowrap" > {{ change.created_at|datetime_fmt }}< / small >
< / div >
< / div >
{% endfor %}
< / div >
{% else %}
< div class = "p-3 text-muted" > Aucun changement récent.< / div >
{% endif %}
< / div >
< / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.intervention or dashboard_permissions.patrimoine %}<!-- Graphiques -->
2026-08-14 18:02:25 +02:00
< div class = "row g-3 mb-3" >
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.intervention %}< div class = "col-md-6" >
2026-08-14 18:02:25 +02:00
< div class = "card" >
< div class = "card-header" > < h6 class = "mb-0" > < i class = "bi bi-bar-chart" > < / i > Interventions par mois< / h6 > < / div >
< div class = "card-body" > < canvas id = "chart-monthly" height = "200" > < / canvas > < / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
{% if dashboard_permissions.intervention %}< div class = "col-md-6" >
2026-08-14 18:02:25 +02:00
< div class = "card" >
< div class = "card-header" > < h6 class = "mb-0" > < i class = "bi bi-pie-chart" > < / i > Interventions par statut< / h6 > < / div >
< div class = "card-body" > < canvas id = "chart-status" height = "200" > < / canvas > < / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
< / div > {% endif %}
{% if dashboard_permissions.patrimoine or (dashboard_permissions.intervention and dashboard_permissions.patrimoine) %}< div class = "row g-3 mb-3" >
{% if dashboard_permissions.patrimoine %}< div class = "col-md-6" >
2026-08-14 18:02:25 +02:00
< div class = "card" >
< div class = "card-header" > < h6 class = "mb-0" > < i class = "bi bi-pie-chart" > < / i > Equipements par statut< / h6 > < / div >
< div class = "card-body" > < canvas id = "chart-equip" height = "200" > < / canvas > < / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
{% if dashboard_permissions.intervention and dashboard_permissions.patrimoine %}< div class = "col-md-6" >
2026-08-14 18:02:25 +02:00
< div class = "card" >
< div class = "card-header" > < h6 class = "mb-0" > < i class = "bi bi-bar-chart" > < / i > Top 5 equipements< / h6 > < / div >
< div class = "card-body" > < canvas id = "chart-top" height = "200" > < / canvas > < / div >
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
2026-08-14 18:02:25 +02:00
< / div >
2026-08-21 19:35:46 +02:00
{% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.intervention %}<!-- Dernières interventions -->
2026-08-14 18:02:25 +02:00
< div class = "card mb-3" >
< div class = "card-header d-flex justify-content-between align-items-center py-2" >
< span > < i class = "bi bi-clock-history" > < / i > Dernières interventions< / span >
< a href = "{{ url_for('interventions.create') }}" class = "btn btn-sm btn-primary" >
< i class = "bi bi-plus-lg" > < / i > < span class = "d-none d-sm-inline" > Nouvelle< / span >
< / a >
< / div >
< div class = "card-body p-0" >
{% if recent_interventions %}
<!-- Desktop : table -->
< div class = "table-responsive d-none d-md-block" >
< table class = "table table-sm table-hover mb-0" >
< thead > < tr > < th > #< / th > < th > Titre< / th > < th > Type< / th > < th > Statut< / th > < th > Équipement< / th > < / tr > < / thead >
< tbody >
{% for interv in recent_interventions %}
< tr >
< td > < a href = "{{ url_for('interventions.detail', id=interv.id) }}" > #{{ interv.id }}< / a > < / td >
< td > {{ interv.title[:50] }}{% if interv.title|length > 50 %}…{% endif %}< / td >
< td >
{% if interv.type == 'curatif' %}< span class = "badge bg-danger" > Curatif< / span >
{% else %}< span class = "badge bg-info" > Préventif< / span > {% endif %}
< / td >
< td > < span class = "badge {{ interv.status|status_badge }}" > {{ interv.status|format_status }}< / span > < / td >
2026-08-22 15:10:39 +02:00
< td > {% if interv.equipment %}{{ interv.equipment.name[:30] }}{% if interv.equipment.name|length > 30 %}…{% endif %}{% elif interv.room %}{{ interv.room.name }}{% else %}Aucun équipement{% endif %}< / td >
2026-08-14 18:02:25 +02:00
< / tr >
{% endfor %}
< / tbody >
< / table >
< / div >
<!-- Mobile : cards -->
< div class = "d-md-none" >
{% for interv in recent_interventions %}
< a href = "{{ url_for('interventions.detail', id=interv.id) }}" class = "list-group-item list-group-item-action px-3 py-2" >
< div class = "d-flex justify-content-between align-items-start" >
< div style = "min-width:0;" >
< strong > #{{ interv.id }}< / strong > {{ interv.title[:60] }}{% if interv.title|length > 60 %}…{% endif %}
< div class = "mt-1" >
{% if interv.type == 'curatif' %}< span class = "badge bg-danger" > Curatif< / span >
{% else %}< span class = "badge bg-info" > Préventif< / span > {% endif %}
< span class = "badge {{ interv.status|status_badge }}" > {{ interv.status|format_status }}< / span >
< / div >
< / div >
< / div >
< / a >
{% endfor %}
< / div >
{% else %}
< div class = "p-3 text-muted" > Aucune intervention.< / div >
{% endif %}
< / div >
2026-08-21 19:35:46 +02:00
< / div > {% endif %}
2026-08-14 18:02:25 +02:00
<!-- Interprétations en attente -->
2026-08-21 19:35:46 +02:00
{% if pending_interpretations and (dashboard_permissions.ent or dashboard_permissions.outlook) %}
2026-08-14 18:02:25 +02:00
< div class = "card mb-3 border-warning" >
< div class = "card-header bg-warning bg-opacity-10 d-flex justify-content-between align-items-center py-2" >
< span > < i class = "bi bi-robot" > < / i > Interprétations en attente < span class = "badge bg-warning text-dark" > {{ pending_interpretations|length }}< / span > < / span >
< a href = "{{ url_for('outlook_dashboard.all_interpretations') }}" class = "btn btn-sm btn-outline-warning" >
Voir tous
< / a >
< / div >
< div class = "card-body p-0" >
< div class = "table-responsive" >
< table class = "table table-sm table-hover mb-0" >
< thead > < tr > < th > Source< / th > < th > Type< / th > < th > Expéditeur< / th > < th > Sujet< / th > < th > Date< / th > < th > Actions< / th > < / tr > < / thead >
< tbody >
{% for interp in pending_interpretations %}
< tr >
< td >
{% if interp.source == 'ENT' %}
< span class = "badge bg-info text-dark" > < i class = "bi bi-broadcast" > < / i > ENT< / span >
{% else %}
< span class = "badge bg-primary" > < i class = "bi bi-microsoft-outlook" > < / i > Outlook< / span >
{% endif %}
< / td >
< td >
{% if interp.type == 'curative' %}
< span class = "badge bg-danger" > Curative< / span >
{% elif interp.type == 'preventive' %}
< span class = "badge bg-primary" > Préventive< / span >
{% elif interp.type == 'administrative' %}
< span class = "badge bg-warning text-dark" > Admin< / span >
{% elif interp.type == 'formation' %}
< span class = "badge bg-info text-dark" > Formation< / span >
{% elif interp.type == 'information' %}
< span class = "badge bg-secondary" > Information< / span >
{% else %}
< span class = "badge bg-secondary" > {{ interp.type or 'N/A' }}< / span >
{% endif %}
< / td >
< td > < small > {{ interp.sender[:20] }}{% if interp.sender|length > 20 %}…{% endif %}< / small > < / td >
< td > < small > {{ (interp.subject or 'Sans sujet')[:30] }}{% if interp.subject and interp.subject|length > 30 %}…{% endif %}< / small > < / td >
< td > < small > {{ interp.date.strftime('%d/%m %H:%M') if interp.date else '-' }}< / small > < / td >
< td >
< a href = "{{ interp.detail_url }}" class = "btn btn-sm btn-outline-primary me-1" title = "Voir le détail" >
< i class = "bi bi-eye" > < / i >
< / a >
< a href = "{{ interp.create_url }}" class = "btn btn-sm btn-success me-1" title = "Créer l'intervention" >
< i class = "bi bi-check-lg" > < / i >
< / a >
< button type = "button" class = "btn btn-sm btn-outline-danger reject-interp-btn"
data-id="{{ interp.id }}"
data-source="{{ interp.source|lower }}"
title="Refuser">
< i class = "bi bi-x-lg" > < / i >
< / button >
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< / div >
< / div >
< / div >
{% endif %}
< script >
document.addEventListener('DOMContentLoaded', function() {
// Gestion du bouton Refuser
document.querySelectorAll('.reject-interp-btn').forEach(btn => {
btn.addEventListener('click', function() {
const interpId = this.dataset.id;
const source = this.dataset.source;
const row = this.closest('tr');
if (!confirm('Refuser cette interprétation et déplacer le message vers Traités ?')) {
return;
}
// Désactiver le bouton
this.disabled = true;
this.innerHTML = '< i class = "bi bi-hourglass-split" > < / i > ';
// Appeler l'API de rejet
const url = source === 'ent'
? `/ent/api/interpretation/${interpId}/reject`
: `/outlook/interpretation/${interpId}/reject`;
fetch(url, { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.success) {
// Supprimer la ligne du tableau
row.style.opacity = '0.5';
row.style.textDecoration = 'line-through';
// Afficher message
const msg = data.message || 'Interprétation rejetée';
alert(msg);
// Recharger la page après 1s
setTimeout(() => location.reload(), 1000);
} else {
alert('Erreur: ' + (data.error || 'Impossible de rejeter'));
this.disabled = false;
this.innerHTML = '< i class = "bi bi-x-lg" > < / i > ';
}
})
.catch(err => {
alert('Erreur: ' + err);
this.disabled = false;
this.innerHTML = '< i class = "bi bi-x-lg" > < / i > ';
});
});
});
});
< / script >
< script src = "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" > < / script >
< script >
document.addEventListener('DOMContentLoaded', function() {
fetch('/api/stats').then(r => r.json()).then(data => {
// Interventions par mois
if (data.monthly & & data.monthly.length) {
new Chart(document.getElementById('chart-monthly'), {
type: 'bar',
data: {
labels: data.monthly.map(d => d.month),
datasets: [{ label: 'Interventions', data: data.monthly.map(d => d.count), backgroundColor: '#0d6efd' }]
},
options: { responsive: true, plugins: { legend: { display: false } } }
});
}
// Par statut
if (data.by_status & & Object.keys(data.by_status).length) {
new Chart(document.getElementById('chart-status'), {
type: 'doughnut',
data: {
labels: Object.keys(data.by_status),
datasets: [{ data: Object.values(data.by_status), backgroundColor: ['#0d6efd','#198754','#ffc107','#dc3545','#6f42c1','#0dcaf0'] }]
},
options: { responsive: true }
});
}
// Equipements par statut
if (data.equip_status & & Object.keys(data.equip_status).length) {
new Chart(document.getElementById('chart-equip'), {
type: 'doughnut',
data: {
labels: Object.keys(data.equip_status),
datasets: [{ data: Object.values(data.equip_status), backgroundColor: ['#198754','#dc3545','#ffc107','#6c757d'] }]
},
options: { responsive: true }
});
}
// Top 5 equipements
if (data.top_equipments & & data.top_equipments.length) {
new Chart(document.getElementById('chart-top'), {
type: 'bar',
data: {
labels: data.top_equipments.map(d => d.name.substring(0, 20)),
datasets: [{ label: 'Interventions', data: data.top_equipments.map(d => d.count), backgroundColor: '#6f42c1' }]
},
options: { responsive: true, indexAxis: 'y', plugins: { legend: { display: false } } }
});
}
}).catch(err => console.error('Chart data error:', err));
});
< / script >
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.ai or dashboard_permissions.planning %}< script >
{% if dashboard_permissions.ai %}
2026-08-14 18:02:25 +02:00
// Verifier le statut du modele IA au chargement
fetch('/ai-config/api/status').then(r => r.json()).then(d => {
if (d.status === 'error' || d.status === 'no_model' || d.status === 'no_key') {
const alert = document.getElementById('ai-alert');
const msg = document.getElementById('ai-alert-msg');
if (d.status === 'no_key') {
msg.textContent = ' Cle API non configuree.';
} else if (d.status === 'no_model') {
msg.textContent = ' Aucun modele configure.';
} else {
msg.textContent = ' Le modele "' + (d.model || '?') + '" ne repond pas : ' + (d.message || 'erreur');
}
alert.style.display = 'block';
}
}).catch(() => {});
2026-08-21 19:35:46 +02:00
{% endif %}
2026-08-14 18:02:25 +02:00
2026-08-21 19:35:46 +02:00
{% if dashboard_permissions.planning %}
2026-08-14 18:02:25 +02:00
// Verifier les retards
fetch('/scheduler/api/overdue-count').then(r => r.json()).then(d => {
const total = (d.overdue_tasks || 0) + (d.overdue_interventions || 0);
if (total > 0) {
const alert = document.getElementById('overdue-alert');
const msg = document.getElementById('overdue-alert-msg');
msg.textContent = ' ' + total + ' element(s) en retard (' + (d.overdue_tasks || 0) + ' taches, ' + (d.overdue_interventions || 0) + ' interventions).';
alert.style.display = 'block';
}
}).catch(() => {});
2026-08-21 19:35:46 +02:00
{% endif %}
2026-08-14 18:02:25 +02:00
< / script >
2026-08-21 19:35:46 +02:00
{% endif %}
2026-08-20 01:46:52 +02:00
{% endblock %}