{% extends 'sam/layout.html.twig' %} {% block title %}SAM — Tableau de bord{% endblock %} {% block sam_body %} {# ── Macros tooltip réutilisables ── #} {% macro info(text) %} {{ text }} {% endmacro %} {% macro info_left(text) %} {{ text }} {% endmacro %} {% macro tooltip_delayed(text) %}{{ text }}{% endmacro %} {% macro tooltip_delayed_left(text) %}{{ text }}{% endmacro %}
{# ══════════════════════════════════════════════════════════════════════════════ EN-TÊTE ══════════════════════════════════════════════════════════════════════════════ #}

{{ greeting }}, {{ app.user.firstName }} 👋

{% if company %}{{ company.name }} — {% endif %}Qui progresse, qui décroche, qui excelle — toute la réalité de votre formation, en un coup d'œil.

{% if lastSyncAt %}
{% include '_atoms/icon/icon.html.twig' with {name: 'refresh', size: 'xs', color: 'gray'} only %} Sync. {{ lastSyncAt|date('d/m/Y à H\\hi') }}
{% endif %}
{# ══════════════════════════════════════════════════════════════════════════════ ÉTATS VIDES ══════════════════════════════════════════════════════════════════════════════ #} {% if isAdmin and companies|length == 0 %}
{% include '_atoms/icon/icon.html.twig' with {name: 'buildings', size: '2xl', color: 'gray'} only %}

Aucune entreprise

Aucune entreprise n'est enregistrée dans le système.

Gérer les entreprises
{% elseif not hasMoodleConfig and not isAdmin %}
{% include '_atoms/icon/icon.html.twig' with {name: 'warning', size: '2xl', color: 'warning'} only %}

Configuration requise

{% if company %}L'entreprise {{ company.name }} n'a pas de configuration Moodle. {% else %}Pour utiliser SAM, vous devez être rattaché à une entreprise.{% endif %}

{% if company and isAdmin %} Configurer Moodle {% elseif company %}

Pour configurer Moodle, contactez votre administrateur à l'adresse {{ contactEmail }}.

{% endif %}
{% else %} {# ══════════════════════════════════════════════════════════════════════════════ KPI CARDS ══════════════════════════════════════════════════════════════════════════════ #} {% set cs = chartData.connectionStats %} {% set inactiveOver30 = (cs.inactive5|default(0)) + (cs.inactive10|default(0)) %}
{# Macro card — tag=a si href, sinon div #} {% macro kpi(label, value, sub, icon, color, bg, href) %} {% set tag = href ? 'a' : 'div' %} <{{ tag }}{% if href %} href="{{ href }}"{% endif %} class="bg-white rounded-2xl p-5 shadow-md transition-all duration-200{% if href %} hover:shadow-md hover:-translate-y-0.5{% endif %}"> {# Icône #}
{% include '_atoms/icon/icon.html.twig' with {name: icon, size: 'md', color: color} only %}
{# Valeur #}
{{ value }}
{# Label #}
{{ label }}
{# Sous-texte #}
{{ sub }}
{% endmacro %} {{ _self.kpi( 'Apprenants', stats.users|default(0), stats.cohorts|default(0) ~ ' cohorte' ~ (stats.cohorts|default(0) > 1 ? 's' : ''), 'users-group', 'primary', '#0C81E4', path('app_sam_users') ) }} {{ _self.kpi( 'Cours', stats.courses|default(0), chartData.globalStats.total_activities|default(0) ~ ' activité' ~ (chartData.globalStats.total_activities|default(0) > 1 ? 's' : ''), 'book', 'cohorts', '#8B5CF6', path('app_sam_courses') ) }} {{ _self.kpi( 'Actifs (7j)', cs.active|default(0), cs.inactive2|default(0) ~ ' actifs 8–30j', 'check-circle', 'success', '#309344', path('app_sam_users') ) }} {{ _self.kpi( 'Jamais connectés', cs.neverConnected|default(0), 'aucune connexion enregistrée', 'user', 'error', '#D10000', path('app_sam_users') ) }}
{# ══════════════════════════════════════════════════════════════════════════════ SECTION — CONNEXIONS & UTILISATEURS ══════════════════════════════════════════════════════════════════════════════ #}
{% include '_molecules/section-divider/section-divider.html.twig' with { title: 'Suivi d\'assiduité', subtitle: 'Fréquence de connexion et profil d\'engagement de vos apprenants', icon: 'users-group', iconColor: 'cohorts' } only %}
{# ── Graphique connexions (chargement asynchrone) ── #}
{# Header : titre + sélecteur de période #}
{% include '_atoms/icon/icon.html.twig' with {name: 'chart', size: 'xs', color: 'cohorts'} only %}

{{ _self.tooltip_delayed('Nombre d\'utilisateurs uniques connectés chaque mois sur la période sélectionnée.') }}Évolution des connexions

{# Sélecteur de période #}
{% for period, label in {3: '3 mois', 6: '6 mois', 12: '12 mois'} %} {% endfor %}
{# Zone graphique #}
{# Skeleton de chargement #}
{# Canvas du graphique #} {# État vide #}
{# ── Profil d'engagement (donut, chargement asynchrone) ── #}
{% include '_atoms/icon/icon.html.twig' with {name: 'users-group', size: 'xs', color: 'cohorts'} only %}

{{ _self.tooltip_delayed('Répartition des apprenants selon leur fréquence de connexion : actifs réguliers, occasionnels, ou inactifs.') }}Profil d'engagement

Fréquence de connexion

{# Skeleton #}
{# Canvas + légende (révélés par le JS) #}
{# État vide #}
{# ══════════════════════════════════════════════════════════════════════════════ SECTION — PROGRESSION JOURNALIÈRE ══════════════════════════════════════════════════════════════════════════════ #}
{% include '_molecules/section-divider/section-divider.html.twig' with { title: 'Progression journalière', subtitle: 'Complétions d\'activités jour par jour — identifiez les pics et creux d\'apprentissage', icon: 'chart', iconColor: 'success' } only %} {% set hasCompletionData = chartData.monthlyCompletions.labels|length > 0 %} {% set allTypes = chartData.monthlyCompletions.datasets|keys %} {% set typeLabels = { 'quiz': 'Quiz', 'assign': 'Devoir', 'forum': 'Forum', 'scorm': 'SCORM', 'page': 'Page', 'url': 'URL', 'h5pactivity': 'H5P', 'resource': 'Fichier', 'lesson': 'Leçon', 'choice': 'Choix', 'data': 'Base de données', 'feedback': 'Feedback', 'glossary': 'Glossaire', 'wiki': 'Wiki', 'workshop': 'Atelier', 'chat': 'Chat', 'survey': 'Sondage', 'questionnaire': 'Questionnaire', 'lti': 'LTI', 'imscp': 'IMS CP', 'book': 'Livre', 'label': 'Étiquette', 'folder': 'Dossier', 'bigbluebuttonbn': 'BigBlueButton', 'zoom': 'Zoom', 'attendance': 'Présence', 'certificate': 'Certificat', 'hvp': 'H5P (hvp)', 'reengagement': 'Relance', 'customcert': 'Certificat perso' } %} {% set typeColors = { 'quiz': '#8b5cf6', 'assign': '#0ea5e9', 'forum': '#10b981', 'scorm': '#1E40AF', 'page': '#3B82F6', 'url': '#f97316', 'h5pactivity': '#ec4899', 'resource': '#f59e0b', 'lesson': '#6366f1', 'choice': '#06b6d4', 'data': '#84cc16', 'feedback': '#f43f5e', 'glossary': '#a78bfa', 'wiki': '#22d3ee', 'workshop': '#fb923c', 'chat': '#4ade80', 'survey': '#facc15', 'questionnaire': '#e879f9', 'lti': '#38bdf8', 'imscp': '#a3e635', 'book': '#818cf8', 'label': '#94a3b8', 'folder': '#fbbf24', 'bigbluebuttonbn': '#ef4444', 'zoom': '#2563eb', 'attendance': '#059669', 'certificate': '#d97706', 'hvp': '#db2777', 'reengagement': '#7c3aed', 'customcert': '#b45309' } %}
{% if hasCompletionData %} {# Header : filtres à gauche, période à droite #}
Filtrer
{% for type in allTypes %} {% set tColor = typeColors[type] ?? '#9CA3AF' %} {% endfor %}
{% for p in [{'v':7,'l':'7j'}, {'v':14,'l':'14j'}, {'v':30,'l':'30j'}] %} {% endfor %}
{% endif %} {# Canvas + tooltip #}
{% if hasCompletionData %}
{% else %}
{% include '_atoms/icon/icon.html.twig' with {name: 'chart', size: '2xl', color: 'gray'} only %}

Aucune donnée disponible

Les données apparaîtront après la prochaine synchronisation

{% endif %}
{# ══════════════════════════════════════════════════════════════════════════════ SECTION — FORMATION (accordion identique à /sam/courses) ══════════════════════════════════════════════════════════════════════════════ #}
{% embed '_molecules/section-divider/section-divider.html.twig' with { title: 'Formations à traiter', subtitle: 'Formations nécessitant une action — relancez vos apprenants en un clic', icon: 'book', iconColor: 'primary' } %} {% block right %} {% include '_atoms/button/button.html.twig' with { label: 'Voir tous', variant: 'cohorts', size: 'sm', href: path('app_sam_courses') } %} {% endblock %} {% endembed %} {% if dashboardCourses|length == 0 %}
{% include '_atoms/icon/icon.html.twig' with {name: 'book', size: '3xl', color: 'gray'} only %}

Tout est à jour ✓

Aucune formation n'a d'apprenant à relancer en ce moment.

{% else %}
{% for course in dashboardCourses %} {% set stats = dashCourseStats[course.id] ?? {enrolled: 0, completed: 0, inProgress: 0, atRisk: 0, percentage: 0} %} {% set pct = stats.percentage|default(0)|round %} {% set sections = dashCourseSections[course.id] ?? [] %} {% set cohorts = dashCourseCohorts[course.id] ?? [] %} {% set riskUsers = dashAtRiskUsers[course.id] ?? [] %} {% if pct >= 75 %} {% set statusLabel = 'Excellent' %} {% set statusClass = 'text-success bg-success/10' %} {% set barColor = '#22c55e' %} {% set courseStatus = 'excellent' %} {% elseif pct > 0 %} {% set statusLabel = 'En cours' %} {% set statusClass = 'text-warning bg-warning/10' %} {% set barColor = '#f59e0b' %} {% set courseStatus = 'in-progress' %} {% else %} {% set statusLabel = 'Pas commencé' %} {% set statusClass = 'text-error bg-error/10' %} {% set barColor = '#ef4444' %} {% set courseStatus = 'at-risk' %} {% endif %}
{% include '_atoms/icon/icon.html.twig' with {name: 'book', size: 'md', color: 'cohorts'} only %}

{{ course.fullname }}

{% if not course.visible %} Masqué {% endif %}
{% if cohorts|length > 0 %}
{% for cohort in cohorts %} {{ cohort.name }} {% endfor %}
{% endif %}
{{ course.sectionsCount }} module{{ course.sectionsCount > 1 ? 's' : '' }} · {{ course.activitiesCount }} activité{{ course.activitiesCount > 1 ? 's' : '' }}
{% include '_molecules/progress-ring/progress-ring.html.twig' with { value: pct, size: 'sm', variant: pct >= 75 ? 'success' : (pct > 0 ? 'warning' : 'error'), thickness: 'normal' } %}
{% include '_atoms/icon/icon.html.twig' with {name: 'chevron-down', size: 'sm', color: 'gray'} only %}
{% if sections|length > 0 %}

Modules {{ sections|length }}

{% for section in sections %} {% set secStats = dashSectionStats[course.id][section.id] ?? {completedUsers: 0, totalUsers: stats.enrolled, percentage: 0} %} {% set secPct = secStats.percentage|default(0)|round %} {% if secPct >= 90 %} {% set secLabel = 'Excellent' %} {% set secClass = 'text-success bg-success/10' %} {% set secColor = '#22c55e' %} {% elseif secPct >= 40 %} {% set secLabel = 'En cours' %} {% set secClass = 'text-warning bg-warning/10' %} {% set secColor = '#f59e0b' %} {% else %} {% set secLabel = 'Pas commencé' %} {% set secClass = 'text-error bg-error/10' %} {% set secColor = '#ef4444' %} {% endif %}
{{ loop.index }} {{ section.name }}
{{ secStats.completedUsers }}/{{ secStats.totalUsers }} {{ secLabel }}
{% endfor %}
{% endif %} {% if riskUsers|length > 0 %}

{{ _self.tooltip_delayed('Cliquez sur un apprenant ou sur "Relancer tous" pour lui envoyer un e-mail de rappel l\'invitant à reprendre sa formation.') }} Apprenants à relancer ({{ riskUsers|length }})

{% set lastSentDates = dashReminderSent[course.id] ?? {} %} {% set today = 'now'|date('Y-m-d') %} {% for user in riskUsers %} {% set lastSent = lastSentDates[user.id] ?? null %} {% set alreadySent = lastSent and lastSent|date('Y-m-d') == today %} {% set lastSentLabel = lastSent ? (alreadySent ? 'Envoyé aujourd\'hui à ' ~ lastSent|date('H\\hi') : 'Dernier envoi le ' ~ lastSent|date('d/m')) : null %}
{{ user.firstname|first|upper }}
{{ user.firstname }} {{ user.lastname }} {% if lastSentLabel %} {{ lastSentLabel }} {% endif %}
{% endfor %}
{% endif %}
{% endfor %} {# État vide si aucun cours ne correspond au filtre actif #}
{% endif %}
{# ══════════════════════════════════════════════════════════════════════════════ SECTION — SUIVI EN TEMPS RÉEL ══════════════════════════════════════════════════════════════════════════════ #}
{% include '_molecules/section-divider/section-divider.html.twig' with { title: 'Suivi en temps réel', subtitle: 'Activités récentes et apprenants à surveiller', icon: 'clock', iconColor: 'warning' } only %}
{% include '_atoms/icon/icon.html.twig' with {name: 'clock', size: 'xs', color: 'primary'} only %}

{{ _self.tooltip_delayed('Dernières complétions d\'activités par vos apprenants, toutes formations confondues.') }}Activités récentes

Dernières complétions

{% for i in 1..5 %}
{% endfor %}
{% if chartData.dropoutUsers|length > 0 %}
{% include '_atoms/icon/icon.html.twig' with {name: 'warning', size: 'xs', color: 'error'} only %}

{{ _self.tooltip_delayed('Apprenants n\'ayant complété aucune activité depuis plus de 7 jours, ou n\'en ayant jamais complété. Triés du plus inactif au moins inactif.') }}Apprenants en décrochage

Inactifs depuis 7+ jours

{% for user in chartData.dropoutUsers|slice(0, 7) %} {% set initials = user.fullname|split(' ')|map(w => w[0] ?? '')|join('')|upper|slice(0,2) %} {% set inactive = user.inactiveDays|default(0) %} {% set pct = user.completion|default(0)|round %}
{{ initials }}
{{ user.fullname }}
{% if user.cohort is defined and user.cohort %}
{{ user.cohort }}
{% endif %}
{{ pct }}%
{% endfor %}
{% else %}
{% include '_atoms/icon/icon.html.twig' with {name: 'check', size: '2xl', color: 'success'} only %}

Aucun apprenant en décrochage

Tout le monde est actif !

{% endif %}
{% endif %}{# fin hasMoodleConfig #}
{# /container #}
{# /px-6 py-8 #} {# ══════════════════════════════════════════════════════════════════════════════ MODAL DE CONFIRMATION — SYNCHRONISATION (HTML inline pour éviter le conflit {% block body %} avec base.html.twig) ══════════════════════════════════════════════════════════════════════════════ #} {% if hasMoodleConfig %} {% endif %} {% endblock %}