templates/evaluation/index.html.twig line 1

Open in your IDE?
  1. {% extends 'layout/backEndLayout.html.twig' %}
  2. {% block stylesheets %}
  3. {{ parent() }}
  4. <style>
  5. :root {
  6. --ev-card : #ffffff;
  7. --ev-border : rgba(0,0,0,.08);
  8. --ev-border-hi: rgba(0,0,0,.15);
  9. --ev-ink : #111827;
  10. --ev-dim : #4b5563;
  11. --ev-muted : #9ca3af;
  12. --ev-blue : #1d4ed8;
  13. --ev-blue-lt : rgba(29,78,216,.1);
  14. --ev-teal : #0d9488;
  15. --ev-teal-lt : rgba(13,148,136,.1);
  16. --ev-amber : #b45309;
  17. --ev-amber-lt: rgba(180,83,9,.1);
  18. --ev-rose : #be123c;
  19. --ev-rose-lt : rgba(190,18,60,.1);
  20. --ev-green : #15803d;
  21. --ev-green-lt: rgba(21,128,61,.1);
  22. --ev-r : 12px;
  23. --ev-rs : 8px;
  24. --ev-shadow : 0 1px 4px rgba(0,0,0,.06), 0 6px 20px rgba(0,0,0,.07);
  25. }
  26. .ev-page { font-family:'DM Sans','Segoe UI',sans-serif; color:var(--ev-ink); }
  27. /* ── Topbar ────────────────────────────────────────────────── */
  28. .ev-topbar {
  29. display:flex; align-items:center; justify-content:space-between;
  30. flex-wrap:wrap; gap:12px; margin-bottom:20px;
  31. }
  32. .ev-topbar__title {
  33. font-size:20px; font-weight:700; display:flex; align-items:center; gap:10px;
  34. }
  35. .ev-topbar__title i { color:var(--ev-blue); }
  36. .ev-topbar__sub { font-size:13px; color:var(--ev-muted); margin-top:2px; }
  37. /* ── Barre de filtre ───────────────────────────────────────── */
  38. .ev-filter-card {
  39. background:var(--ev-card);
  40. border:1px solid var(--ev-border);
  41. border-radius:var(--ev-r);
  42. padding:16px 20px;
  43. margin-bottom:20px;
  44. box-shadow:var(--ev-shadow);
  45. }
  46. .ev-filter-card__title {
  47. font-size:13px; font-weight:600;
  48. color:var(--ev-dim); margin-bottom:12px;
  49. display:flex; align-items:center; gap:8px;
  50. }
  51. .ev-filter-row {
  52. display:grid;
  53. grid-template-columns:repeat(auto-fill, minmax(180px,1fr));
  54. gap:10px;
  55. align-items:flex-end;
  56. }
  57. .ev-filter-row select,
  58. .ev-filter-row input {
  59. width:100%;
  60. height:38px;
  61. padding:6px 12px;
  62. border:1px solid var(--ev-border);
  63. border-radius:var(--ev-rs);
  64. font-size:13px;
  65. font-family:inherit;
  66. color:var(--ev-ink);
  67. background:#fff;
  68. outline:none;
  69. transition:border-color .15s;
  70. }
  71. .ev-filter-row select:focus,
  72. .ev-filter-row input:focus { border-color:var(--ev-blue); box-shadow:0 0 0 3px rgba(29,78,216,.08); }
  73. .ev-filter-actions {
  74. display:flex; gap:8px; align-items:center;
  75. }
  76. .btn-filter {
  77. display:inline-flex; align-items:center; gap:6px;
  78. padding:8px 18px;
  79. background:var(--ev-blue);
  80. color:#fff;
  81. border-radius:var(--ev-rs);
  82. font-size:13px; font-weight:600;
  83. border:none; cursor:pointer;
  84. text-decoration:none;
  85. transition:background .18s;
  86. }
  87. .btn-filter:hover { background:#1e40af; color:#fff; }
  88. .btn-reset {
  89. display:inline-flex; align-items:center; gap:6px;
  90. padding:8px 14px;
  91. background:transparent;
  92. color:var(--ev-dim);
  93. border-radius:var(--ev-rs);
  94. font-size:13px; font-weight:500;
  95. border:1px solid var(--ev-border);
  96. cursor:pointer; text-decoration:none;
  97. transition:background .15s;
  98. }
  99. .btn-reset:hover { background:rgba(0,0,0,.04); color:var(--ev-ink); }
  100. /* ── KPIs ──────────────────────────────────────────────────── */
  101. .ev-kpis {
  102. display:grid;
  103. grid-template-columns:repeat(auto-fill, minmax(150px,1fr));
  104. gap:12px; margin-bottom:20px;
  105. }
  106. .ev-kpi {
  107. background:var(--ev-card);
  108. border:1px solid var(--ev-border);
  109. border-radius:var(--ev-r);
  110. padding:14px 16px;
  111. display:flex; align-items:center; gap:12px;
  112. box-shadow:var(--ev-shadow);
  113. transition:transform .2s;
  114. }
  115. .ev-kpi:hover { transform:translateY(-2px); }
  116. .ev-kpi__icon {
  117. width:38px; height:38px; border-radius:var(--ev-rs);
  118. display:flex; align-items:center; justify-content:center;
  119. font-size:15px; flex-shrink:0;
  120. }
  121. .ki-blue { background:var(--ev-blue-lt); color:var(--ev-blue); }
  122. .ki-teal { background:var(--ev-teal-lt); color:var(--ev-teal); }
  123. .ki-green { background:var(--ev-green-lt); color:var(--ev-green); }
  124. .ki-amber { background:var(--ev-amber-lt); color:var(--ev-amber); }
  125. .ev-kpi__val { font-size:20px; font-weight:700; color:var(--ev-ink); line-height:1; }
  126. .ev-kpi__lbl { font-size:11.5px; color:var(--ev-muted); margin-top:2px; }
  127. /* ── Table ─────────────────────────────────────────────────── */
  128. .ev-table-card {
  129. background:var(--ev-card);
  130. border:1px solid var(--ev-border);
  131. border-radius:var(--ev-r);
  132. overflow:hidden;
  133. box-shadow:var(--ev-shadow);
  134. }
  135. .ev-table-head {
  136. display:flex; align-items:center; justify-content:space-between;
  137. flex-wrap:wrap; gap:10px;
  138. padding:14px 20px;
  139. border-bottom:1px solid var(--ev-border);
  140. background:#fafbfc;
  141. }
  142. .ev-table-head__title {
  143. font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px;
  144. }
  145. .ev-table-head__dot {
  146. width:8px; height:8px; border-radius:50%;
  147. background:var(--ev-blue); box-shadow:0 0 6px var(--ev-blue);
  148. }
  149. table.ev-tbl { width:100%; border-collapse:collapse; font-size:13px; }
  150. table.ev-tbl thead th {
  151. padding:10px 14px; color:var(--ev-muted);
  152. font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.06em;
  153. background:#fafbfc; border-bottom:1px solid var(--ev-border);
  154. white-space:nowrap;
  155. }
  156. table.ev-tbl tbody td {
  157. padding:11px 14px; border-bottom:1px solid rgba(0,0,0,.04);
  158. vertical-align:middle;
  159. }
  160. table.ev-tbl tbody tr:last-child td { border-bottom:none; }
  161. table.ev-tbl tbody tr:hover td { background:rgba(29,78,216,.025); }
  162. /* Chips dans les cellules */
  163. .course-chip {
  164. display:inline-block;
  165. padding:3px 10px; border-radius:20px;
  166. font-size:12px; font-weight:600;
  167. background:var(--ev-blue-lt); color:var(--ev-blue);
  168. border:1px solid rgba(29,78,216,.2);
  169. }
  170. .class-chip {
  171. display:inline-block;
  172. padding:3px 10px; border-radius:20px;
  173. font-size:12px; font-weight:600;
  174. background:var(--ev-teal-lt); color:var(--ev-teal);
  175. border:1px solid rgba(13,148,136,.2);
  176. }
  177. .seq-chip {
  178. display:inline-block;
  179. padding:2px 9px; border-radius:20px;
  180. font-size:11px; font-weight:500;
  181. background:var(--ev-amber-lt); color:var(--ev-amber);
  182. border:1px solid rgba(180,83,9,.2);
  183. }
  184. /* Score badge */
  185. .score-badge {
  186. display:inline-flex; align-items:center; justify-content:center;
  187. min-width:44px; padding:3px 8px;
  188. border-radius:8px; font-size:13px; font-weight:700;
  189. }
  190. .sc-good { background:var(--ev-green-lt); color:var(--ev-green); }
  191. .sc-mid { background:var(--ev-amber-lt); color:var(--ev-amber); }
  192. .sc-low { background:var(--ev-rose-lt); color:var(--ev-rose); }
  193. .sc-none { background:rgba(0,0,0,.05); color:var(--ev-muted); }
  194. /* Barres mini succès/échec */
  195. .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; }
  196. .mini-bar__fill { height:100%; border-radius:3px; background:var(--ev-green); }
  197. /* Actions ligne */
  198. .ev-actions { display:flex; align-items:center; gap:4px; justify-content:flex-end; }
  199. .ev-btn {
  200. display:inline-flex; align-items:center; gap:5px;
  201. padding:5px 10px; border-radius:7px;
  202. font-size:11.5px; font-weight:500;
  203. text-decoration:none; border:1px solid var(--ev-border);
  204. cursor:pointer; background:#fff; color:var(--ev-dim);
  205. transition:background .15s, border-color .15s, color .15s;
  206. }
  207. .ev-btn:hover { border-color:var(--ev-border-hi); color:var(--ev-ink); }
  208. .ev-btn-primary { background:var(--ev-blue-lt); color:var(--ev-blue); border-color:rgba(29,78,216,.25); }
  209. .ev-btn-primary:hover { background:var(--ev-blue); color:#fff; }
  210. .ev-btn-warning { background:var(--ev-amber-lt); color:var(--ev-amber); border-color:rgba(180,83,9,.25); }
  211. .ev-btn-warning:hover { background:var(--ev-amber); color:#fff; }
  212. .ev-btn-danger { background:var(--ev-rose-lt); color:var(--ev-rose); border-color:rgba(190,18,60,.25); }
  213. .ev-btn-danger:hover { background:var(--ev-rose); color:#fff; }
  214. .ev-btn-teal { background:var(--ev-teal-lt); color:var(--ev-teal); border-color:rgba(13,148,136,.25); }
  215. .ev-btn-teal:hover { background:var(--ev-teal); color:#fff; }
  216. /* ── Pagination ────────────────────────────────────────────── */
  217. .ev-pag {
  218. display:flex; align-items:center; justify-content:space-between;
  219. flex-wrap:wrap; gap:10px;
  220. padding:14px 20px; border-top:1px solid var(--ev-border); background:#fafbfc;
  221. }
  222. .ev-pag__info { font-size:12.5px; color:var(--ev-muted); }
  223. .ev-pager {
  224. display:flex; align-items:center; gap:4px;
  225. list-style:none; margin:0; padding:0;
  226. }
  227. .ev-pager a, .ev-pager span {
  228. display:inline-flex; align-items:center; justify-content:center;
  229. min-width:34px; height:34px; padding:0 8px;
  230. border-radius:8px; border:1px solid var(--ev-border);
  231. font-size:13px; font-weight:500; color:var(--ev-dim);
  232. text-decoration:none; background:#fff;
  233. transition:background .15s, border-color .15s, color .15s;
  234. }
  235. .ev-pager a:hover { border-color:var(--ev-blue); color:var(--ev-blue); }
  236. .ev-pager .pg-active span {
  237. background:var(--ev-blue); color:#fff; border-color:var(--ev-blue); font-weight:700;
  238. }
  239. .ev-pager .pg-disabled span { opacity:.4; }
  240. /* ── Vide ──────────────────────────────────────────────────── */
  241. .ev-empty { text-align:center; padding:64px 24px; color:var(--ev-muted); }
  242. .ev-empty i { font-size:40px; margin-bottom:12px; display:block; }
  243. .ev-empty strong { display:block; font-size:15px; color:var(--ev-dim); margin-bottom:6px; }
  244. /* ── Anim ──────────────────────────────────────────────────── */
  245. @keyframes fadeUp { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:none} }
  246. .ev-table-card, .ev-filter-card { animation:fadeUp .4s ease both; }
  247. .ev-filter-card { animation-delay:.06s; }
  248. </style>
  249. {% endblock %}
  250. {% block content %}
  251. <div class="ev-page">
  252. {# ── Topbar ──────────────────────────────────────────────────── #}
  253. <div class="ev-topbar">
  254. <div>
  255. <div class="ev-topbar__title">
  256. <i class="fa fa-file-text-o"></i> Évaluations
  257. </div>
  258. <div class="ev-topbar__sub">Suivi des évaluations séquentielles par classe et matière</div>
  259. </div>
  260. {% if is_granted('ROLE_PROF') or is_granted('ROLE_ADMIN') %}
  261. <a href="{{ path('admin_evaluations_new') }}" class="btn-filter">
  262. <i class="fa fa-plus"></i> Nouvelle évaluation
  263. </a>
  264. {% endif %}
  265. </div>
  266. {# ── Barre de filtre ─────────────────────────────────────────── #}
  267. <div class="ev-filter-card">
  268. <div class="ev-filter-card__title">
  269. <i class="fa fa-filter" style="color:var(--ev-blue)"></i>
  270. Filtrer les évaluations
  271. </div>
  272. {{ form_start(searchForm, {'method': 'GET', 'attr': {'class': ''}}) }}
  273. <div class="ev-filter-row">
  274. {{ form_widget(searchForm.room, {'attr': {'class': ''}}) }}
  275. {{ form_widget(searchForm.sequence, {'attr': {'class': ''}}) }}
  276. {{ form_widget(searchForm.course, {'attr': {'class': ''}}) }}
  277. <div class="ev-filter-actions">
  278. <button type="submit" class="btn-filter">
  279. <i class="fa fa-search"></i> Filtrer
  280. </button>
  281. <a href="{{ path('admin_evaluations') }}" class="btn-reset">
  282. <i class="fa fa-times"></i> Reset
  283. </a>
  284. </div>
  285. </div>
  286. {{ form_end(searchForm) }}
  287. </div>
  288. {# ── KPIs calculés depuis la pagination ─────────────────────── #}
  289. {% set total_evals = pagination.totalItemCount %}
  290. {% set total_success = 0 %}
  291. {% set total_failures = 0 %}
  292. {% set total_absents = 0 %}
  293. {% for eval in pagination %}
  294. {% set total_success = total_success + eval.successH + eval.successF %}
  295. {% set total_failures = total_failures + eval.failluresH + eval.failluresF %}
  296. {% set total_absents = total_absents + eval.abscent %}
  297. {% endfor %}
  298. <div class="ev-kpis">
  299. <div class="ev-kpi">
  300. <div class="ev-kpi__icon ki-blue"><i class="fa fa-file-text-o"></i></div>
  301. <div>
  302. <div class="ev-kpi__val">{{ total_evals }}</div>
  303. <div class="ev-kpi__lbl">Évaluations</div>
  304. </div>
  305. </div>
  306. <div class="ev-kpi">
  307. <div class="ev-kpi__icon ki-green"><i class="fa fa-check-circle"></i></div>
  308. <div>
  309. <div class="ev-kpi__val">{{ total_success }}</div>
  310. <div class="ev-kpi__lbl">Succès (page)</div>
  311. </div>
  312. </div>
  313. <div class="ev-kpi">
  314. <div class="ev-kpi__icon ki-amber"><i class="fa fa-times-circle"></i></div>
  315. <div>
  316. <div class="ev-kpi__val">{{ total_failures }}</div>
  317. <div class="ev-kpi__lbl">Échecs (page)</div>
  318. </div>
  319. </div>
  320. <div class="ev-kpi">
  321. <div class="ev-kpi__icon ki-teal"><i class="fa fa-user-times"></i></div>
  322. <div>
  323. <div class="ev-kpi__val">{{ total_absents }}</div>
  324. <div class="ev-kpi__lbl">Absents (page)</div>
  325. </div>
  326. </div>
  327. </div>
  328. {# ── Table des évaluations ───────────────────────────────────── #}
  329. <div class="ev-table-card">
  330. <div class="ev-table-head">
  331. <div class="ev-table-head__title">
  332. <span class="ev-table-head__dot"></span>
  333. Liste des évaluations
  334. <span style="font-size:11.5px;color:var(--ev-muted);font-weight:400;">
  335. ({{ pagination.totalItemCount }} résultat{{ pagination.totalItemCount > 1 ? 's' : '' }})
  336. </span>
  337. </div>
  338. <span style="font-size:12px;color:var(--ev-muted)">
  339. Page {{ pagination.currentPageNumber }} / {{ pagination.pageCount }}
  340. </span>
  341. </div>
  342. {% if pagination.totalItemCount == 0 %}
  343. <div class="ev-empty">
  344. <i class="fa fa-search"></i>
  345. <strong>Aucune évaluation trouvée</strong>
  346. Modifiez les critères de filtre ou ajoutez une nouvelle évaluation.
  347. </div>
  348. {% else %}
  349. <div style="overflow-x:auto">
  350. <table class="ev-tbl">
  351. <thead>
  352. <tr>
  353. <th style="width:4%">#</th>
  354. <th style="width:16%">Matière</th>
  355. <th style="width:14%">Classe</th>
  356. <th style="width:13%">Séquence</th>
  357. <th style="width:10%" class="text-center">Moy.</th>
  358. <th style="width:8%" class="text-center">Min</th>
  359. <th style="width:8%" class="text-center">Max</th>
  360. <th style="width:18%">Réussite / Échec</th>
  361. <th style="width:9%">Auteur</th>
  362. <th style="width:10%" class="text-right">Actions</th>
  363. </tr>
  364. </thead>
  365. <tbody>
  366. {% for eval in pagination %}
  367. {% set moy = eval.moyenne %}
  368. {% set success = eval.successH + eval.successF %}
  369. {% set failures = eval.failluresH + eval.failluresF %}
  370. {% set effectif = success + failures %}
  371. {% set pct = effectif > 0 ? (success / effectif * 100)|round : 0 %}
  372. <tr>
  373. <td style="color:var(--ev-muted);font-size:12px">
  374. {{ (pagination.currentPageNumber - 1) * pagination.itemNumberPerPage + loop.index }} </td>
  375. <td>
  376. <span class="course-chip">{{ eval.course.wording }}</span>
  377. </td>
  378. <td>
  379. <a href="{{ path('admin_classrooms_show', {id: eval.classRoom.id}) }}" style="text-decoration:none">
  380. <span class="class-chip">{{ eval.classRoom.name }}</span>
  381. </a>
  382. </td>
  383. <td>
  384. <span class="seq-chip">{{ eval.sequence.wording }}</span>
  385. </td>
  386. <td class="text-center">
  387. {% if moy > 0 %}
  388. <span class="score-badge {{ moy >= 14 ? 'sc-good' : (moy >= 10 ? 'sc-mid' : 'sc-low') }}">
  389. {{ moy|number_format(1) }}
  390. </span>
  391. {% else %}
  392. <span class="score-badge sc-none">—</span>
  393. {% endif %}
  394. </td>
  395. <td class="text-center" style="color:var(--ev-rose);font-weight:600;font-size:12.5px">
  396. {{ eval.mini > 0 ? eval.mini|number_format(1) : '—' }}
  397. </td>
  398. <td class="text-center" style="color:var(--ev-green);font-weight:600;font-size:12.5px">
  399. {{ eval.maxi > 0 ? eval.maxi|number_format(1) : '—' }}
  400. </td>
  401. <td>
  402. <div style="display:flex;align-items:center;gap:6px;font-size:11.5px">
  403. <span style="color:var(--ev-green);font-weight:600;">{{ success }}</span>
  404. <span style="color:var(--ev-muted)">/</span>
  405. <span style="color:var(--ev-rose);font-weight:600">{{ failures }}</span>
  406. <div class="mini-bar">
  407. <div class="mini-bar__fill" style="width:{{ pct }}%;background:{{ pct >= 60 ? 'var(--ev-green)' : (pct >= 40 ? 'var(--ev-amber)' : 'var(--ev-rose)') }}"></div>
  408. </div>
  409. <span style="color:var(--ev-muted)">{{ pct }}%</span>
  410. </div>
  411. </td>
  412. <td style="font-size:12px;color:var(--ev-dim)">
  413. {% if eval.author is defined and eval.author %}
  414. {{ eval.author.username|split('.')|first|capitalize }}
  415. {% else %}—{% endif %}
  416. </td>
  417. <td>
  418. <div class="ev-actions">
  419. <a class="ev-btn ev-btn-primary" href="{{ path('admin_evaluations_show', {id: eval.id}) }}" title="Voir">
  420. <i class="fa fa-eye"></i>
  421. </a>
  422. {% if is_granted('ROLE_ADMIN') or (eval.author is defined and eval.author == app.user) %}
  423. <a class="ev-btn ev-btn-warning" href="{{ path('admin_evaluations_edit', {id: eval.id}) }}" title="Modifier">
  424. <i class="fa fa-pencil"></i>
  425. </a>
  426. {% endif %}
  427. <a class="ev-btn ev-btn-teal" href="{{ path('admin_evaluations_pdf', {id: eval.id}) }}" title="PDF" target="_blank">
  428. <i class="fa fa-file-pdf-o"></i>
  429. </a>
  430. {% if is_granted('ROLE_ADMIN') %}
  431. <form method="post" action="{{ path('admin_evaluations_delete', {id: eval.id}) }}"
  432. onsubmit="return confirm('Supprimer cette évaluation ?')" style="display:inline">
  433. <input type="hidden" name="_method" value="DELETE">
  434. <input type="hidden" name="csrf_token" value="{{ csrf_token('evaluations_deletion' ~ eval.id) }}">
  435. <button class="ev-btn ev-btn-danger" type="submit" title="Supprimer">
  436. <i class="fa fa-trash"></i>
  437. </button>
  438. </form>
  439. {% endif %}
  440. </div>
  441. </td>
  442. </tr>
  443. {% endfor %}
  444. </tbody>
  445. </table>
  446. </div>
  447. {# ── Pagination KNP ──────────────────────────────────────────── #}
  448. <div class="ev-pag">
  449. <div class="ev-pag__info">
  450. Affichage {{ (pagination.currentPageNumber - 1) * pagination.itemNumberPerPage + 1 }}–{{ min(pagination.currentPageNumber * pagination.itemNumberPerPage, pagination.totalItemCount) }}
  451. sur {{ pagination.totalItemCount }} évaluation{{ pagination.totalItemCount > 1 ? 's' : '' }}
  452. </div>
  453. <ul class="ev-pager">
  454. {# Prev #}
  455. {% if pagination.currentPageNumber > 1 %}
  456. <li><a href="{{ path('admin_evaluations', app.request.query.all|merge({page: pagination.currentPageNumber - 1})) }}"><i class="fa fa-chevron-left"></i></a></li>
  457. {% else %}
  458. <li class="pg-disabled"><span><i class="fa fa-chevron-left"></i></span></li>
  459. {% endif %}
  460. {# Pages #}
  461. {% for p in 1..pagination.pageCount %}
  462. {% if p == pagination.currentPageNumber %}
  463. <li class="pg-active"><span>{{ p }}</span></li>
  464. {% elseif p == 1 or p == pagination.pageCount or (p >= pagination.currentPageNumber - 2 and p <= pagination.currentPageNumber + 2) %}
  465. <li><a href="{{ path('admin_evaluations', app.request.query.all|merge({page: p})) }}">{{ p }}</a></li>
  466. {% elseif p == pagination.currentPageNumber - 3 or p == pagination.currentPageNumber + 3 %}
  467. <li><span style="border:none;background:transparent;min-width:auto;padding:0 4px">…</span></li>
  468. {% endif %}
  469. {% endfor %}
  470. {# Next #}
  471. {% if pagination.currentPageNumber < pagination.pageCount %}
  472. <li><a href="{{ path('admin_evaluations', app.request.query.all|merge({page: pagination.currentPageNumber + 1})) }}"><i class="fa fa-chevron-right"></i></a></li>
  473. {% else %}
  474. <li class="pg-disabled"><span><i class="fa fa-chevron-right"></i></span></li>
  475. {% endif %}
  476. </ul>
  477. </div>
  478. {% endif %}
  479. </div>{# /.ev-table-card #}
  480. </div>{# /.ev-page #}
  481. {% endblock %}