From 406752bc6f9829ff7d713b5280633e21117b4360 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Sun, 10 May 2026 22:36:28 +0200 Subject: [PATCH] Improve recap page + fix CSV import for jury roles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recapitulatif.php (partage): - Center .thanks-success and add bottom margin/padding - Display ALL fields: identifier, synopsis, languages, formats, jury (all roles), baiu link, license, access type - Add validation notice asking user to verify info, with xamxam@erg.be contact link (email obfuscated) StudentEmail: - Add 'Note contextuelle' and license_custom to email recap - Rename 'Promoteur·ice(s)' to 'Promoteur·ice(s) interne' - Change email message to ask student to verify info + contact for errors CSV export/import: - Add 3 new CSV columns: Lecteur·ice(s) interne, Lecteur·ice(s) externe, Promoteur·ice(s) ULB - Export splits supervisors by role/is_external/is_ulb into separate columns - Import inserts supervisors with correct role, is_external, and is_ulb flags (was: all treated as generic supervisors) - Add header matching for short distinguishers (ulb, externe) via str_contains fallback --- TODO.md | 5 ++ app/public/admin/index.php | 86 ++++++++++++++++++------ app/public/assets/css/form.css | 29 ++++++++ app/public/partage/recapitulatif.php | 61 +++++++++++++++++ app/src/Controllers/ExportController.php | 34 ++++++++-- app/src/Database.php | 2 +- app/src/StudentEmail.php | 8 ++- app/templates/admin/acces.php | 13 ++++ 8 files changed, 208 insertions(+), 30 deletions(-) diff --git a/TODO.md b/TODO.md index ad96266..a7d7dcd 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,10 @@ # TODO +- [x] Improve recapitulatif.php (partage): bottom margin/padding, center .thanks-success +- [x] Display ALL submitted info in recapitulatif page + email recap +- [x] Add "validate your info / contact xamxam@erg.be" note on recap page +- [x] Fix CSV import: lecteur interne/externe + promoteurice ULB not imported with correct role/is_external/is_ulb flags + - [x] Replace HTMX+PHP file upload queues with client-side JS - [x] Fix submit button on all forms — add JS/PHP debug logging - [x] Fix file-upload-queue.js: redirect detection broken due to opaque redirect (switched from fetch to XHR for reliable responseURL) diff --git a/app/public/admin/index.php b/app/public/admin/index.php index a1964de..3cdc5fa 100644 --- a/app/public/admin/index.php +++ b/app/public/admin/index.php @@ -42,7 +42,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { $headerRowNum = 0; $knownHeaders = [ 'identifiant', 'titre', 'sous-titre', 'auteur', 'contact', - 'promoteur', 'format', 'année', 'ap', 'orientation', 'finalité', + 'promoteur', 'lecteur', 'ulb', 'externe', 'format', 'année', 'ap', 'orientation', 'finalité', 'mots-clés', 'synopsis', 'contexte', 'remarques', 'langue', 'autorisation', 'licence', 'license', 'points', 'lien baiu', ]; @@ -68,10 +68,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { // Avoid short prefixes matching unrelated words if ($hlen >= 5 || $cell === $h) { $hits++; $map[$h] = $pos; $used[$pos] = true; break; } } + // Substring match for short distinguishers (ulb, externe) + if ($hlen >= 3 && $hlen <= 7 && str_contains($cell, $h)) { + $hits++; $map[$h] = $pos; $used[$pos] = true; break; + } } } - // Require at least 8 known headers to trust the row. - if ($hits >= 8) { $colIdx = $map; break; } + // Require at least 11 known headers to trust the row. + if ($hits >= 11) { $colIdx = $map; break; } } // If no header row found, rewind and fall back to positional (skip 4 rows). if ($colIdx === null) { @@ -238,27 +242,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { if ($contact !== '' && in_array(strtoupper(trim($contact)), ['NON', 'OUI'], true)) { $contact = ''; } - $supervisorsRaw = $cell($row, 'promoteur', 5); - $formatsRaw = $cell($row, 'format', 6); - $yearRaw = $cell($row, 'année', 7); + $supervisorsRaw = $cell($row, 'promoteur', 5); + $lecteursInternesRaw = $cell($row, 'lecteur', 6); // first "lecteur" col = interne + $lecteursExternesRaw = $cell($row, 'externe', 7); // contains "externe" + $promoteursUlbRaw = $cell($row, 'ulb', 8); // contains "ulb" + $formatsRaw = $cell($row, 'format', 9); + $yearRaw = $cell($row, 'année', 10); $year = $yearRaw !== '' ? intval($yearRaw) : 0; // Fallback: derive year from identifier (e.g. "2024-003" → 2024) if ($year === 0 && $identifier !== '' && preg_match('/^(\d{4})-/', $identifier, $m)) { $year = (int)$m[1]; } - $apCode = $cell($row, 'ap', 8); - $orientationCode = $cell($row, 'orientation', 9); - $finalityName = $cell($row, 'finalité', 10); - $keywordsRaw = $cell($row, 'mots-clés', 11); - $synopsis = $cell($row, 'synopsis', 12); - $context = $cell($row, 'contexte', 13); - $remarks = $cell($row, 'remarques', 14); - $languageRaw = $cell($row, 'langue', 15); - $access = $cell($row, 'autorisation', 16); - $license = $cell($row, 'license', 17); - $juryPointsRaw = $cell($row, 'points', 18); + $apCode = $cell($row, 'ap', 11); + $orientationCode = $cell($row, 'orientation', 12); + $finalityName = $cell($row, 'finalité', 13); + $keywordsRaw = $cell($row, 'mots-clés', 14); + $synopsis = $cell($row, 'synopsis', 15); + $context = $cell($row, 'contexte', 16); + $remarks = $cell($row, 'remarques', 17); + $languageRaw = $cell($row, 'langue', 18); + $access = $cell($row, 'autorisation', 19); + $license = $cell($row, 'license', 20); + $juryPointsRaw = $cell($row, 'points', 21); $juryPoints = $juryPointsRaw !== '' ? floatval($juryPointsRaw) : null; - $baiuLink = $cell($row, 'lien baiu', 19); + $baiuLink = $cell($row, 'lien baiu', 22); if ($title === '' || $year === 0) { $missing = []; @@ -328,12 +335,49 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) { } } } + // Insert supervisors with proper role/is_external/is_ulb flags + $juryOrder = 0; + // Promoteurs internes if (!empty($supervisorsRaw)) { - foreach (array_map('trim', explode(',', $supervisorsRaw)) as $idx => $name) { + foreach (array_map('trim', explode(',', $supervisorsRaw)) as $name) { if ($name) { + $juryOrder++; $sId = $importDb->findOrCreateSupervisor($name); - $s = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?,?,?)"); - $s->execute([$thesisId, $sId, $idx + 1]); + $stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)"); + $stmt->execute([$thesisId, $sId, 'promoteur', 0, 0, $juryOrder]); + } + } + } + // Lecteurs internes + if (!empty($lecteursInternesRaw)) { + foreach (array_map('trim', explode(',', $lecteursInternesRaw)) as $name) { + if ($name) { + $juryOrder++; + $sId = $importDb->findOrCreateSupervisor($name); + $stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)"); + $stmt->execute([$thesisId, $sId, 'lecteur', 0, 0, $juryOrder]); + } + } + } + // Lecteurs externes + if (!empty($lecteursExternesRaw)) { + foreach (array_map('trim', explode(',', $lecteursExternesRaw)) as $name) { + if ($name) { + $juryOrder++; + $sId = $importDb->findOrCreateSupervisor($name); + $stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)"); + $stmt->execute([$thesisId, $sId, 'lecteur', 1, 0, $juryOrder]); + } + } + } + // Promoteurs ULB + if (!empty($promoteursUlbRaw)) { + foreach (array_map('trim', explode(',', $promoteursUlbRaw)) as $name) { + if ($name) { + $juryOrder++; + $sId = $importDb->findOrCreateSupervisor($name); + $stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)"); + $stmt->execute([$thesisId, $sId, 'promoteur', 1, 1, $juryOrder]); } } } diff --git a/app/public/assets/css/form.css b/app/public/assets/css/form.css index 4ab566c..3d3f8be 100644 --- a/app/public/assets/css/form.css +++ b/app/public/assets/css/form.css @@ -797,12 +797,41 @@ a.recap-file-name:hover { .partage-recap { display: flex; flex-direction: column; + align-items: center; gap: var(--space-l); + padding-bottom: var(--space-3xl); + margin-bottom: var(--space-2xl); +} + +.partage-recap .thanks-success { + text-align: center; +} + +.recap-validate-notice { + text-align: center; + font-size: var(--step--1); + color: var(--text-secondary); + background: color-mix(in srgb, var(--accent-primary) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--accent-primary) 20%, transparent); + border-radius: 8px; + padding: var(--space-s) var(--space-m); + max-width: 560px; +} + +.recap-validate-notice p { + margin: 0; +} + +.recap-validate-notice a { + color: var(--accent-primary); + font-weight: 600; } .recap-section { border-top: 1px solid var(--border-primary); padding-top: var(--space-m); + width: 100%; + max-width: 620px; } .recap-section h2 { diff --git a/app/public/partage/recapitulatif.php b/app/public/partage/recapitulatif.php index 7011faf..a0e02f6 100644 --- a/app/public/partage/recapitulatif.php +++ b/app/public/partage/recapitulatif.php @@ -8,6 +8,8 @@ if (!defined('APP_ROOT')) { App::boot(); } +require_once APP_ROOT . '/src/EmailObfuscator.php'; + $thesisId = isset($_GET['id']) ? (int)$_GET['id'] : 0; if ($thesisId <= 0) { @@ -69,9 +71,18 @@ $pageTitle = 'Merci — TFE enregistré'; +
+

Veuillez vérifier les informations ci-dessous. Si vous constatez une erreur, contactez-nous à .

+
+

Récapitulatif de votre soumission

+ +
Identifiant
+
+ +
Titre
@@ -105,10 +116,60 @@ $pageTitle = 'Merci — TFE enregistré';
+ +
Synopsis
+
+ + + +
Langue(s)
+
+ + + +
Format(s)
+
+ +
Mots-clés
+ + +
Promoteur·ice(s) interne
+
+ + + +
Promoteur·ice(s) ULB
+
+ + + +
Lecteur·ice(s) interne
+
+ + + +
Lecteur·ice(s) externe
+
+ + + +
Lien
+
+ + + +
Licence
+
+ + + +
Type d'accès
+
+
diff --git a/app/src/Controllers/ExportController.php b/app/src/Controllers/ExportController.php index 6e5464b..2992711 100644 --- a/app/src/Controllers/ExportController.php +++ b/app/src/Controllers/ExportController.php @@ -188,7 +188,10 @@ class ExportController 'Sous-titre', 'Auteur·ice(s)', 'Contact', - 'Promoteur·ice(s)', + 'Promoteur·ice(s) interne', + 'Lecteur·ice(s) interne', + 'Lecteur·ice(s) externe', + 'Promoteur·ice(s) ULB', 'Format(s)', 'Année', 'AP', @@ -252,10 +255,28 @@ class ExportController } } - // Supervisors - $supList = []; + // Supervisors — split by role + $promoteursInternes = []; + $lecteursInternes = []; + $lecteursExternes = []; + $promoteursUlb = []; foreach (($supervisors[$tid] ?? []) as $s) { - $supList[] = $s['name']; + $role = $s['role'] ?? ''; + $isExternal = (int)($s['is_external'] ?? 0); + $isUlb = (int)($s['is_ulb'] ?? 0); + $name = $s['name']; + if ($role === 'promoteur' && $isUlb) { + $promoteursUlb[] = $name; + } elseif ($role === 'promoteur') { + $promoteursInternes[] = $name; + } elseif ($role === 'lecteur' && $isExternal) { + $lecteursExternes[] = $name; + } elseif ($role === 'lecteur') { + $lecteursInternes[] = $name; + } else { + // Legacy rows with no role: treat as promoteur interne + $promoteursInternes[] = $name; + } } // Tags @@ -282,7 +303,10 @@ class ExportController $t['subtitle'] ?? '', implode(', ', $authorList), $contact, - implode(', ', $supList), + implode(', ', $promoteursInternes), + implode(', ', $lecteursInternes), + implode(', ', $lecteursExternes), + implode(', ', $promoteursUlb), implode(', ', $fmtList), $t['year'] ?? '', $t['ap_program'] ?? '', diff --git a/app/src/Database.php b/app/src/Database.php index edffb52..969c5d9 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -2340,7 +2340,7 @@ class Database public function getAllThesisSupervisorsForExport(): array { return $this->pdo->query(' - SELECT ts.thesis_id, s.name + SELECT ts.thesis_id, s.name, ts.role, ts.is_external, ts.is_ulb FROM thesis_supervisors ts JOIN supervisors s ON s.id = ts.supervisor_id ORDER BY ts.thesis_id, ts.supervisor_order diff --git a/app/src/StudentEmail.php b/app/src/StudentEmail.php index 7584be1..96a842d 100644 --- a/app/src/StudentEmail.php +++ b/app/src/StudentEmail.php @@ -28,17 +28,18 @@ class StudentEmail 'Atelier pluridisciplinaire' => $thesis['ap_program'] ?? '', 'Finalité' => $thesis['finality_type'] ?? '', 'Synopsis' => $thesis['synopsis'] ?? '', + 'Note contextuelle' => $thesis['context_note'] ?? '', 'Langue(s)' => $thesis['languages'] ?? '', 'Format(s)' => $thesis['formats'] ?? '', 'Mots-clés' => $thesis['keywords'] ?? '', - 'Promoteur·ice(s)' => $thesis['jury_promoteurs'] ?? '', + 'Promoteur·ice(s) interne' => $thesis['jury_promoteurs'] ?? '', 'Promoteur·ice(s) ULB' => $thesis['jury_promoteurs_ulb'] ?? '', 'Président·e du jury' => $thesis['jury_president'] ?? '', 'Lecteurs·rices (interne)' => $thesis['jury_lecteurs_internes'] ?? '', 'Lecteurs·rices (externe)' => $thesis['jury_lecteurs_externes'] ?? '', 'Lien' => $thesis['baiu_link'] ?? '', 'Type d\'accès' => $thesis['access_type'] ?? '', - 'Licence' => $thesis['license_type'] ?? '', + 'Licence' => trim(($thesis['license_type'] ?? '') . (!empty($thesis['license_custom']) ? ' — ' . $thesis['license_custom'] : '')), ]; foreach ($fields as $label => $value) { @@ -52,7 +53,8 @@ class StudentEmail

Merci, ton TFE a bien été enregistré.

- Voici un récapitulatif de ta soumission. Tu n'as pas besoin de répondre à cet e-mail. + Voici un récapitulatif de ta soumission. Vérifie bien toutes les informations ci-dessous. + Si tu constates une erreur, écris à xamxam@erg.be.

{$rows} diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index e249e09..636e7fe 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -480,6 +480,19 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: vqxpnkox f51d447c "fix migration 028, promoteurice repopulation, DB bootstrap" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: vqxpnkox f51d447c "fix migration 028, promoteurice repopulation, DB bootstrap" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: zxryvzkv 9d3f6b53 "Improve recap page + fix CSV import for jury roles" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: zxryvzkv 27814cea "Improve recap page + fix CSV import for jury roles" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?>