{% extends 'layout/backEndLayout.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<style>
:root {
--ev-card : #ffffff;
--ev-border : rgba(0,0,0,.08);
--ev-border-hi: rgba(0,0,0,.15);
--ev-ink : #111827;
--ev-dim : #4b5563;
--ev-muted : #9ca3af;
--ev-blue : #1d4ed8;
--ev-blue-lt : rgba(29,78,216,.1);
--ev-teal : #0d9488;
--ev-teal-lt : rgba(13,148,136,.1);
--ev-amber : #b45309;
--ev-amber-lt: rgba(180,83,9,.1);
--ev-rose : #be123c;
--ev-rose-lt : rgba(190,18,60,.1);
--ev-green : #15803d;
--ev-green-lt: rgba(21,128,61,.1);
--ev-r : 12px;
--ev-rs : 8px;
--ev-shadow : 0 1px 4px rgba(0,0,0,.06), 0 6px 20px rgba(0,0,0,.07);
}
.ev-page { font-family:'DM Sans','Segoe UI',sans-serif; color:var(--ev-ink); }
/* ── Topbar ────────────────────────────────────────────────── */
.ev-topbar {
display:flex; align-items:center; justify-content:space-between;
flex-wrap:wrap; gap:12px; margin-bottom:20px;
}
.ev-topbar__title {
font-size:20px; font-weight:700; display:flex; align-items:center; gap:10px;
}
.ev-topbar__title i { color:var(--ev-blue); }
.ev-topbar__sub { font-size:13px; color:var(--ev-muted); margin-top:2px; }
/* ── Barre de filtre ───────────────────────────────────────── */
.ev-filter-card {
background:var(--ev-card);
border:1px solid var(--ev-border);
border-radius:var(--ev-r);
padding:16px 20px;
margin-bottom:20px;
box-shadow:var(--ev-shadow);
}
.ev-filter-card__title {
font-size:13px; font-weight:600;
color:var(--ev-dim); margin-bottom:12px;
display:flex; align-items:center; gap:8px;
}
.ev-filter-row {
display:grid;
grid-template-columns:repeat(auto-fill, minmax(180px,1fr));
gap:10px;
align-items:flex-end;
}
.ev-filter-row select,
.ev-filter-row input {
width:100%;
height:38px;
padding:6px 12px;
border:1px solid var(--ev-border);
border-radius:var(--ev-rs);
font-size:13px;
font-family:inherit;
color:var(--ev-ink);
background:#fff;
outline:none;
transition:border-color .15s;
}
.ev-filter-row select:focus,
.ev-filter-row input:focus { border-color:var(--ev-blue); box-shadow:0 0 0 3px rgba(29,78,216,.08); }
.ev-filter-actions {
display:flex; gap:8px; align-items:center;
}
.btn-filter {
display:inline-flex; align-items:center; gap:6px;
padding:8px 18px;
background:var(--ev-blue);
color:#fff;
border-radius:var(--ev-rs);
font-size:13px; font-weight:600;
border:none; cursor:pointer;
text-decoration:none;
transition:background .18s;
}
.btn-filter:hover { background:#1e40af; color:#fff; }
.btn-reset {
display:inline-flex; align-items:center; gap:6px;
padding:8px 14px;
background:transparent;
color:var(--ev-dim);
border-radius:var(--ev-rs);
font-size:13px; font-weight:500;
border:1px solid var(--ev-border);
cursor:pointer; text-decoration:none;
transition:background .15s;
}
.btn-reset:hover { background:rgba(0,0,0,.04); color:var(--ev-ink); }
/* ── KPIs ──────────────────────────────────────────────────── */
.ev-kpis {
display:grid;
grid-template-columns:repeat(auto-fill, minmax(150px,1fr));
gap:12px; margin-bottom:20px;
}
.ev-kpi {
background:var(--ev-card);
border:1px solid var(--ev-border);
border-radius:var(--ev-r);
padding:14px 16px;
display:flex; align-items:center; gap:12px;
box-shadow:var(--ev-shadow);
transition:transform .2s;
}
.ev-kpi:hover { transform:translateY(-2px); }
.ev-kpi__icon {
width:38px; height:38px; border-radius:var(--ev-rs);
display:flex; align-items:center; justify-content:center;
font-size:15px; flex-shrink:0;
}
.ki-blue { background:var(--ev-blue-lt); color:var(--ev-blue); }
.ki-teal { background:var(--ev-teal-lt); color:var(--ev-teal); }
.ki-green { background:var(--ev-green-lt); color:var(--ev-green); }
.ki-amber { background:var(--ev-amber-lt); color:var(--ev-amber); }
.ev-kpi__val { font-size:20px; font-weight:700; color:var(--ev-ink); line-height:1; }
.ev-kpi__lbl { font-size:11.5px; color:var(--ev-muted); margin-top:2px; }
/* ── Table ─────────────────────────────────────────────────── */
.ev-table-card {
background:var(--ev-card);
border:1px solid var(--ev-border);
border-radius:var(--ev-r);
overflow:hidden;
box-shadow:var(--ev-shadow);
}
.ev-table-head {
display:flex; align-items:center; justify-content:space-between;
flex-wrap:wrap; gap:10px;
padding:14px 20px;
border-bottom:1px solid var(--ev-border);
background:#fafbfc;
}
.ev-table-head__title {
font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px;
}
.ev-table-head__dot {
width:8px; height:8px; border-radius:50%;
background:var(--ev-blue); box-shadow:0 0 6px var(--ev-blue);
}
table.ev-tbl { width:100%; border-collapse:collapse; font-size:13px; }
table.ev-tbl thead th {
padding:10px 14px; color:var(--ev-muted);
font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.06em;
background:#fafbfc; border-bottom:1px solid var(--ev-border);
white-space:nowrap;
}
table.ev-tbl tbody td {
padding:11px 14px; border-bottom:1px solid rgba(0,0,0,.04);
vertical-align:middle;
}
table.ev-tbl tbody tr:last-child td { border-bottom:none; }
table.ev-tbl tbody tr:hover td { background:rgba(29,78,216,.025); }
/* Chips dans les cellules */
.course-chip {
display:inline-block;
padding:3px 10px; border-radius:20px;
font-size:12px; font-weight:600;
background:var(--ev-blue-lt); color:var(--ev-blue);
border:1px solid rgba(29,78,216,.2);
}
.class-chip {
display:inline-block;
padding:3px 10px; border-radius:20px;
font-size:12px; font-weight:600;
background:var(--ev-teal-lt); color:var(--ev-teal);
border:1px solid rgba(13,148,136,.2);
}
.seq-chip {
display:inline-block;
padding:2px 9px; border-radius:20px;
font-size:11px; font-weight:500;
background:var(--ev-amber-lt); color:var(--ev-amber);
border:1px solid rgba(180,83,9,.2);
}
/* Score badge */
.score-badge {
display:inline-flex; align-items:center; justify-content:center;
min-width:44px; padding:3px 8px;
border-radius:8px; font-size:13px; font-weight:700;
}
.sc-good { background:var(--ev-green-lt); color:var(--ev-green); }
.sc-mid { background:var(--ev-amber-lt); color:var(--ev-amber); }
.sc-low { background:var(--ev-rose-lt); color:var(--ev-rose); }
.sc-none { background:rgba(0,0,0,.05); color:var(--ev-muted); }
/* Barres mini succès/échec */
.mini-bar { width:80px; height:6px; background:rgba(0,0,0,.07); border-radius:3px; overflow:hidden; display:inline-block; vertical-align:middle; margin-left:4px; }
.mini-bar__fill { height:100%; border-radius:3px; background:var(--ev-green); }
/* Actions ligne */
.ev-actions { display:flex; align-items:center; gap:4px; justify-content:flex-end; }
.ev-btn {
display:inline-flex; align-items:center; gap:5px;
padding:5px 10px; border-radius:7px;
font-size:11.5px; font-weight:500;
text-decoration:none; border:1px solid var(--ev-border);
cursor:pointer; background:#fff; color:var(--ev-dim);
transition:background .15s, border-color .15s, color .15s;
}
.ev-btn:hover { border-color:var(--ev-border-hi); color:var(--ev-ink); }
.ev-btn-primary { background:var(--ev-blue-lt); color:var(--ev-blue); border-color:rgba(29,78,216,.25); }
.ev-btn-primary:hover { background:var(--ev-blue); color:#fff; }
.ev-btn-warning { background:var(--ev-amber-lt); color:var(--ev-amber); border-color:rgba(180,83,9,.25); }
.ev-btn-warning:hover { background:var(--ev-amber); color:#fff; }
.ev-btn-danger { background:var(--ev-rose-lt); color:var(--ev-rose); border-color:rgba(190,18,60,.25); }
.ev-btn-danger:hover { background:var(--ev-rose); color:#fff; }
.ev-btn-teal { background:var(--ev-teal-lt); color:var(--ev-teal); border-color:rgba(13,148,136,.25); }
.ev-btn-teal:hover { background:var(--ev-teal); color:#fff; }
/* ── Pagination ────────────────────────────────────────────── */
.ev-pag {
display:flex; align-items:center; justify-content:space-between;
flex-wrap:wrap; gap:10px;
padding:14px 20px; border-top:1px solid var(--ev-border); background:#fafbfc;
}
.ev-pag__info { font-size:12.5px; color:var(--ev-muted); }
.ev-pager {
display:flex; align-items:center; gap:4px;
list-style:none; margin:0; padding:0;
}
.ev-pager a, .ev-pager span {
display:inline-flex; align-items:center; justify-content:center;
min-width:34px; height:34px; padding:0 8px;
border-radius:8px; border:1px solid var(--ev-border);
font-size:13px; font-weight:500; color:var(--ev-dim);
text-decoration:none; background:#fff;
transition:background .15s, border-color .15s, color .15s;
}
.ev-pager a:hover { border-color:var(--ev-blue); color:var(--ev-blue); }
.ev-pager .pg-active span {
background:var(--ev-blue); color:#fff; border-color:var(--ev-blue); font-weight:700;
}
.ev-pager .pg-disabled span { opacity:.4; }
/* ── Vide ──────────────────────────────────────────────────── */
.ev-empty { text-align:center; padding:64px 24px; color:var(--ev-muted); }
.ev-empty i { font-size:40px; margin-bottom:12px; display:block; }
.ev-empty strong { display:block; font-size:15px; color:var(--ev-dim); margin-bottom:6px; }
/* ── Anim ──────────────────────────────────────────────────── */
@keyframes fadeUp { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:none} }
.ev-table-card, .ev-filter-card { animation:fadeUp .4s ease both; }
.ev-filter-card { animation-delay:.06s; }
</style>
{% endblock %}
{% block content %}
<div class="ev-page">
{# ── Topbar ──────────────────────────────────────────────────── #}
<div class="ev-topbar">
<div>
<div class="ev-topbar__title">
<i class="fa fa-file-text-o"></i> Évaluations
</div>
<div class="ev-topbar__sub">Suivi des évaluations séquentielles par classe et matière</div>
</div>
{% if is_granted('ROLE_PROF') or is_granted('ROLE_ADMIN') %}
<a href="{{ path('admin_evaluations_new') }}" class="btn-filter">
<i class="fa fa-plus"></i> Nouvelle évaluation
</a>
{% endif %}
</div>
{# ── Barre de filtre ─────────────────────────────────────────── #}
<div class="ev-filter-card">
<div class="ev-filter-card__title">
<i class="fa fa-filter" style="color:var(--ev-blue)"></i>
Filtrer les évaluations
</div>
{{ form_start(searchForm, {'method': 'GET', 'attr': {'class': ''}}) }}
<div class="ev-filter-row">
{{ form_widget(searchForm.room, {'attr': {'class': ''}}) }}
{{ form_widget(searchForm.sequence, {'attr': {'class': ''}}) }}
{{ form_widget(searchForm.course, {'attr': {'class': ''}}) }}
<div class="ev-filter-actions">
<button type="submit" class="btn-filter">
<i class="fa fa-search"></i> Filtrer
</button>
<a href="{{ path('admin_evaluations') }}" class="btn-reset">
<i class="fa fa-times"></i> Reset
</a>
</div>
</div>
{{ form_end(searchForm) }}
</div>
{# ── KPIs calculés depuis la pagination ─────────────────────── #}
{% set total_evals = pagination.totalItemCount %}
{% set total_success = 0 %}
{% set total_failures = 0 %}
{% set total_absents = 0 %}
{% for eval in pagination %}
{% set total_success = total_success + eval.successH + eval.successF %}
{% set total_failures = total_failures + eval.failluresH + eval.failluresF %}
{% set total_absents = total_absents + eval.abscent %}
{% endfor %}
<div class="ev-kpis">
<div class="ev-kpi">
<div class="ev-kpi__icon ki-blue"><i class="fa fa-file-text-o"></i></div>
<div>
<div class="ev-kpi__val">{{ total_evals }}</div>
<div class="ev-kpi__lbl">Évaluations</div>
</div>
</div>
<div class="ev-kpi">
<div class="ev-kpi__icon ki-green"><i class="fa fa-check-circle"></i></div>
<div>
<div class="ev-kpi__val">{{ total_success }}</div>
<div class="ev-kpi__lbl">Succès (page)</div>
</div>
</div>
<div class="ev-kpi">
<div class="ev-kpi__icon ki-amber"><i class="fa fa-times-circle"></i></div>
<div>
<div class="ev-kpi__val">{{ total_failures }}</div>
<div class="ev-kpi__lbl">Échecs (page)</div>
</div>
</div>
<div class="ev-kpi">
<div class="ev-kpi__icon ki-teal"><i class="fa fa-user-times"></i></div>
<div>
<div class="ev-kpi__val">{{ total_absents }}</div>
<div class="ev-kpi__lbl">Absents (page)</div>
</div>
</div>
</div>
{# ── Table des évaluations ───────────────────────────────────── #}
<div class="ev-table-card">
<div class="ev-table-head">
<div class="ev-table-head__title">
<span class="ev-table-head__dot"></span>
Liste des évaluations
<span style="font-size:11.5px;color:var(--ev-muted);font-weight:400;">
({{ pagination.totalItemCount }} résultat{{ pagination.totalItemCount > 1 ? 's' : '' }})
</span>
</div>
<span style="font-size:12px;color:var(--ev-muted)">
Page {{ pagination.currentPageNumber }} / {{ pagination.pageCount }}
</span>
</div>
{% if pagination.totalItemCount == 0 %}
<div class="ev-empty">
<i class="fa fa-search"></i>
<strong>Aucune évaluation trouvée</strong>
Modifiez les critères de filtre ou ajoutez une nouvelle évaluation.
</div>
{% else %}
<div style="overflow-x:auto">
<table class="ev-tbl">
<thead>
<tr>
<th style="width:4%">#</th>
<th style="width:16%">Matière</th>
<th style="width:14%">Classe</th>
<th style="width:13%">Séquence</th>
<th style="width:10%" class="text-center">Moy.</th>
<th style="width:8%" class="text-center">Min</th>
<th style="width:8%" class="text-center">Max</th>
<th style="width:18%">Réussite / Échec</th>
<th style="width:9%">Auteur</th>
<th style="width:10%" class="text-right">Actions</th>
</tr>
</thead>
<tbody>
{% for eval in pagination %}
{% set moy = eval.moyenne %}
{% set success = eval.successH + eval.successF %}
{% set failures = eval.failluresH + eval.failluresF %}
{% set effectif = success + failures %}
{% set pct = effectif > 0 ? (success / effectif * 100)|round : 0 %}
<tr>
<td style="color:var(--ev-muted);font-size:12px">
{{ (pagination.currentPageNumber - 1) * pagination.itemNumberPerPage + loop.index }} </td>
<td>
<span class="course-chip">{{ eval.course.wording }}</span>
</td>
<td>
<a href="{{ path('admin_classrooms_show', {id: eval.classRoom.id}) }}" style="text-decoration:none">
<span class="class-chip">{{ eval.classRoom.name }}</span>
</a>
</td>
<td>
<span class="seq-chip">{{ eval.sequence.wording }}</span>
</td>
<td class="text-center">
{% if moy > 0 %}
<span class="score-badge {{ moy >= 14 ? 'sc-good' : (moy >= 10 ? 'sc-mid' : 'sc-low') }}">
{{ moy|number_format(1) }}
</span>
{% else %}
<span class="score-badge sc-none">—</span>
{% endif %}
</td>
<td class="text-center" style="color:var(--ev-rose);font-weight:600;font-size:12.5px">
{{ eval.mini > 0 ? eval.mini|number_format(1) : '—' }}
</td>
<td class="text-center" style="color:var(--ev-green);font-weight:600;font-size:12.5px">
{{ eval.maxi > 0 ? eval.maxi|number_format(1) : '—' }}
</td>
<td>
<div style="display:flex;align-items:center;gap:6px;font-size:11.5px">
<span style="color:var(--ev-green);font-weight:600;">{{ success }}</span>
<span style="color:var(--ev-muted)">/</span>
<span style="color:var(--ev-rose);font-weight:600">{{ failures }}</span>
<div class="mini-bar">
<div class="mini-bar__fill" style="width:{{ pct }}%;background:{{ pct >= 60 ? 'var(--ev-green)' : (pct >= 40 ? 'var(--ev-amber)' : 'var(--ev-rose)') }}"></div>
</div>
<span style="color:var(--ev-muted)">{{ pct }}%</span>
</div>
</td>
<td style="font-size:12px;color:var(--ev-dim)">
{% if eval.author is defined and eval.author %}
{{ eval.author.username|split('.')|first|capitalize }}
{% else %}—{% endif %}
</td>
<td>
<div class="ev-actions">
<a class="ev-btn ev-btn-primary" href="{{ path('admin_evaluations_show', {id: eval.id}) }}" title="Voir">
<i class="fa fa-eye"></i>
</a>
{% if is_granted('ROLE_ADMIN') or (eval.author is defined and eval.author == app.user) %}
<a class="ev-btn ev-btn-warning" href="{{ path('admin_evaluations_edit', {id: eval.id}) }}" title="Modifier">
<i class="fa fa-pencil"></i>
</a>
{% endif %}
<a class="ev-btn ev-btn-teal" href="{{ path('admin_evaluations_pdf', {id: eval.id}) }}" title="PDF" target="_blank">
<i class="fa fa-file-pdf-o"></i>
</a>
{% if is_granted('ROLE_ADMIN') %}
<form method="post" action="{{ path('admin_evaluations_delete', {id: eval.id}) }}"
onsubmit="return confirm('Supprimer cette évaluation ?')" style="display:inline">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="csrf_token" value="{{ csrf_token('evaluations_deletion' ~ eval.id) }}">
<button class="ev-btn ev-btn-danger" type="submit" title="Supprimer">
<i class="fa fa-trash"></i>
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# ── Pagination KNP ──────────────────────────────────────────── #}
<div class="ev-pag">
<div class="ev-pag__info">
Affichage {{ (pagination.currentPageNumber - 1) * pagination.itemNumberPerPage + 1 }}–{{ min(pagination.currentPageNumber * pagination.itemNumberPerPage, pagination.totalItemCount) }}
sur {{ pagination.totalItemCount }} évaluation{{ pagination.totalItemCount > 1 ? 's' : '' }}
</div>
<ul class="ev-pager">
{# Prev #}
{% if pagination.currentPageNumber > 1 %}
<li><a href="{{ path('admin_evaluations', app.request.query.all|merge({page: pagination.currentPageNumber - 1})) }}"><i class="fa fa-chevron-left"></i></a></li>
{% else %}
<li class="pg-disabled"><span><i class="fa fa-chevron-left"></i></span></li>
{% endif %}
{# Pages #}
{% for p in 1..pagination.pageCount %}
{% if p == pagination.currentPageNumber %}
<li class="pg-active"><span>{{ p }}</span></li>
{% elseif p == 1 or p == pagination.pageCount or (p >= pagination.currentPageNumber - 2 and p <= pagination.currentPageNumber + 2) %}
<li><a href="{{ path('admin_evaluations', app.request.query.all|merge({page: p})) }}">{{ p }}</a></li>
{% elseif p == pagination.currentPageNumber - 3 or p == pagination.currentPageNumber + 3 %}
<li><span style="border:none;background:transparent;min-width:auto;padding:0 4px">…</span></li>
{% endif %}
{% endfor %}
{# Next #}
{% if pagination.currentPageNumber < pagination.pageCount %}
<li><a href="{{ path('admin_evaluations', app.request.query.all|merge({page: pagination.currentPageNumber + 1})) }}"><i class="fa fa-chevron-right"></i></a></li>
{% else %}
<li class="pg-disabled"><span><i class="fa fa-chevron-right"></i></span></li>
{% endif %}
</ul>
</div>
{% endif %}
</div>{# /.ev-table-card #}
</div>{# /.ev-page #}
{% endblock %}