refine: required confirmation_email field on both student forms, StudentEmail uses it directly

- Add dedicated 'confirmation_email' (type=email, required) field
  to student form at end of submission (partage + admin).
- ThesisCreateController now validates it is present and a valid
  email; form is rejected if missing/invalid.
- Autofocus mapping for confirmation_email errors.
- StudentEmail uses confirmation_email directly (removed extractEmail
  hack that mined email from free-form contact field).
This commit is contained in:
Pontoporeia
2026-04-20 15:02:28 +02:00
parent fa75ca4a65
commit e21a4d81a2
9 changed files with 189 additions and 1 deletions

16
TODO.md
View File

@@ -71,3 +71,19 @@
- [x] Update header.php action URLs - [x] Update header.php action URLs
- [x] Commit current state - [x] Commit current state
- [ ] Test all routes (/, /search, /tfe, /repertoire, /apropos, /licence, /media, /live-reload) - [ ] Test all routes (/, /search, /tfe, /repertoire, /apropos, /licence, /media, /live-reload)
# Now: Confirmation email on student form submission
- [x] Create src/StudentEmail.php — builds HTML recap email, extracts email from contact field, uses SmtpRelay to send
- [x] Wire StudentEmail::sendConfirmation() into partage/index.php handleShareLinkSubmission() after successful thesis creation
- [x] Pass email-sent flag via session to /partage/thanks.php
- [x] Update partage/thanks.php — show "email sent" notice with styled green badge when confirmation was sent
- [x] Add "Visiter" (👁 Visit) button to student link action row in acces-etudiante.php
- [x] Add link (target _blank) to /partage/<slug>
- [x] Add .admin-btn-visit / .admin-btn-visit:hover CSS in admin.css
- [x] Add required confirmation_email field to both student forms (partage/index.php + admin/add.php)
- [x] New fieldset at end of form with type="email", required
- [x] ThesisCreateController validates confirmation_email is present and valid
- [x] StudentEmail uses confirmation_email directly (no more extractEmail hack)
- [x] Autofocus mapping added for confirmation_email validation errors

View File

@@ -83,6 +83,10 @@ $bodyClass = 'admin-body';
<td><?= $created ?></td> <td><?= $created ?></td>
<td> <td>
<div class="admin-actions"> <div class="admin-actions">
<a href="/partage/<?= urlencode($link['slug']) ?>" target="_blank" rel="noopener"
class="admin-btn-sm admin-btn-visit" title="Visiter le formulaire">
👁 Visiter
</a>
<button type="button" class="admin-btn-sm admin-btn-view" <button type="button" class="admin-btn-sm admin-btn-view"
onclick="copyUrl(<?= $link['id'] ?>)" title="Copier l'URL"> onclick="copyUrl(<?= $link['id'] ?>)" title="Copier l'URL">
Copier Copier

View File

@@ -151,6 +151,12 @@ include APP_ROOT . '/templates/header.php';
?> ?>
</fieldset> </fieldset>
<!-- ═══════════════════ E-mail de confirmation ═══════════ -->
<fieldset>
<legend>E-mail de confirmation</legend>
<?php $name = 'confirmation_email'; $label = 'Adresse e-mail * :'; $value = old('confirmation_email'); $type = 'email'; $required = true; $placeholder = 'ton.email@exemple.be'; $hint = 'Nécessaire pour recevoir le récapitulatif de ta soumission.'; $attrs = withAutofocus('confirmation_email'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
</fieldset>
<div class="form-footer"> <div class="form-footer">
<button type="submit" name="go">Soumettre</button> <button type="submit" name="go">Soumettre</button>
</div> </div>

View File

@@ -638,6 +638,15 @@
background: var(--blue-muted-bg-hover); background: var(--blue-muted-bg-hover);
} }
.admin-btn-visit {
background: var(--success-muted-bg);
color: var(--success);
border-color: var(--success-muted-border);
}
.admin-btn-visit:hover {
background: var(--green-muted-bg-hover);
}
.admin-btn-edit { .admin-btn-edit {
background: var(--yellow-muted-bg); background: var(--yellow-muted-bg);
color: var(--accent-yellow); color: var(--accent-yellow);

View File

@@ -467,6 +467,12 @@ function renderShareLinkForm(string $slug, array $link): void
</div> </div>
</fieldset> </fieldset>
<!-- ═══════════════════ E-mail de confirmation ═══════════ -->
<fieldset>
<legend>E-mail de confirmation</legend>
<?php $name = 'confirmation_email'; $label = 'Adresse e-mail * :'; $value = old($formData, 'confirmation_email'); $type = 'email'; $required = true; $placeholder = 'ton.email@exemple.be'; $hint = 'Nécessaire pour recevoir le récapitulatif de ta soumission.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
</fieldset>
<div class="form-footer"> <div class="form-footer">
<button type="submit" name="go">Soumettre</button> <button type="submit" name="go">Soumettre</button>
</div> </div>
@@ -542,11 +548,15 @@ function handleShareLinkSubmission(string $slug): void
} }
require_once APP_ROOT . '/src/Controllers/ThesisCreateController.php'; require_once APP_ROOT . '/src/Controllers/ThesisCreateController.php';
require_once APP_ROOT . '/src/StudentEmail.php';
try { try {
$ctrl = ThesisCreateController::make(); $ctrl = ThesisCreateController::make();
$thesisId = $ctrl->submit($_POST, $_FILES); $thesisId = $ctrl->submit($_POST, $_FILES);
// Send confirmation e-mail (non-blocking; failure doesn't stop redirect)
$emailSent = StudentEmail::sendConfirmation(Database::getInstance(), $thesisId, $_POST);
// Mark the link as used // Mark the link as used
$shareLinkModel = new ShareLink(Database::getInstance()); $shareLinkModel = new ShareLink(Database::getInstance());
$shareLinkModel->incrementUsage($link['id']); $shareLinkModel->incrementUsage($link['id']);
@@ -554,6 +564,7 @@ function handleShareLinkSubmission(string $slug): void
// Clean up share-specific session data // Clean up share-specific session data
unset($_SESSION[$shareCsrfKey]); unset($_SESSION[$shareCsrfKey]);
unset($_SESSION['share_verified_' . $slug]); unset($_SESSION['share_verified_' . $slug]);
$_SESSION['share_email_sent'] = $emailSent;
// Redirect to thanks page // Redirect to thanks page
header('Location: /partage/thanks.php?id=' . urlencode((string)$thesisId)); header('Location: /partage/thanks.php?id=' . urlencode((string)$thesisId));

View File

@@ -22,6 +22,10 @@ if (!$thesis) {
die('TFE introuvable.'); die('TFE introuvable.');
} }
// Was the confirmation e-mail sent?
$emailSent = !empty($_SESSION['share_email_sent']);
unset($_SESSION['share_email_sent']);
// Get the share link slug from the referer path // Get the share link slug from the referer path
$pathParts = explode('/', trim($_SERVER['REQUEST_URI'] ?? '', '/')); $pathParts = explode('/', trim($_SERVER['REQUEST_URI'] ?? '', '/'));
$slug = count($pathParts) >= 2 ? $pathParts[0] : null; $slug = count($pathParts) >= 2 ? $pathParts[0] : null;
@@ -70,12 +74,27 @@ $pageTitle = 'Merci — TFE enregistré';
.thanks-center .btn-add-another:hover { .thanks-center .btn-add-another:hover {
background: #555; background: #555;
} }
.email-sent-notice {
display: inline-block;
padding: 0.75rem 1.5rem;
background: #e8f5e9;
border: 1px solid #a5d6a7;
border-radius: 6px;
font-size: 0.95rem;
color: #2e7d32;
margin: 0 0 1.5rem;
}
</style> </style>
</head> </head>
<body> <body>
<div class="thanks-center"> <div class="thanks-center">
<h1>✅ Merci !</h1> <h1>✅ Merci !</h1>
<p>Votre TFE a bien été enregistré sur la plateforme.</p> <p>Votre TFE a bien été enregistré sur la plateforme.</p>
<?php if ($emailSent): ?>
<div class="email-sent-notice">
📧 Un e-mail de confirmation a été envoyé avec un récapitulatif de votre soumission.
</div>
<?php endif; ?>
<?php if ($thesis): ?> <?php if ($thesis): ?>
<div class="thesis-title"><?= htmlspecialchars($thesis['title']) ?> — <?= htmlspecialchars($thesis['authors'] ?? '') ?></div> <div class="thesis-title"><?= htmlspecialchars($thesis['title']) ?> — <?= htmlspecialchars($thesis['authors'] ?? '') ?></div>
<?php endif; ?> <?php endif; ?>

View File

@@ -173,6 +173,7 @@ class ThesisCreateController
if (str_contains($message, 'langue')) return 'languages'; if (str_contains($message, 'langue')) return 'languages';
if (str_contains($message, 'mots-clés')) return 'tag'; if (str_contains($message, 'mots-clés')) return 'tag';
if (str_contains($message, 'Lien URL')) return 'lien'; if (str_contains($message, 'Lien URL')) return 'lien';
if (str_contains($message, 'e-mail de confirmation')) return 'confirmation_email';
return null; return null;
} }
@@ -283,8 +284,18 @@ class ThesisCreateController
} }
} }
// Confirmation e-mail (required)
$confirmationEmail = trim($post['confirmation_email'] ?? '');
if ($confirmationEmail === '') {
throw new Exception("L'adresse e-mail de confirmation est requise.");
}
$confirmationEmail = filter_var($confirmationEmail, FILTER_VALIDATE_EMAIL);
if ($confirmationEmail === false) {
throw new Exception("L'adresse e-mail de confirmation n'est pas valide.");
}
return compact( return compact(
'auteurName', 'mail', 'showContact', 'annee', 'orientationId', 'apProgramId', 'auteurName', 'mail', 'showContact', 'confirmationEmail', 'annee', 'orientationId', 'apProgramId',
'finalityId', 'titre', 'subtitle', 'synopsis', 'durationInfo', 'finalityId', 'titre', 'subtitle', 'synopsis', 'durationInfo',
'juryMembers', 'keywords', 'languageIds', 'formatIds', 'juryMembers', 'keywords', 'languageIds', 'formatIds',
'licenseId', 'lien', 'accessTypeId' 'licenseId', 'lien', 'accessTypeId'

112
app/src/StudentEmail.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
/**
* StudentEmail — builds and sends confirmation e-mails to students
* after a successful TFE submission via a share-link form.
*
* The e-mail is addressed to the confirmation e-mail field provided
* by the student and contains a recap of every field submitted.
*/
class StudentEmail {
/**
* Build the HTML body for the confirmation e-mail.
*
* @param array $thesis Thesis row (from getThesis / v_theses_full)
* @return string HTML body
*/
private static function buildHtml(array $thesis): string {
$rows = '';
$fields = [
'Identifiant' => $thesis['identifier'] ?? '',
'Titre' => $thesis['title'] ?? '',
'Sous-titre' => $thesis['subtitle'] ?? '',
'Auteur·ice(s)'=> $thesis['authors'] ?? '',
'Année' => $thesis['year'] ?? '',
'Orientation' => $thesis['orientation'] ?? '',
'Atelier pluridisciplinaire' => $thesis['ap_program'] ?? '',
'Finalité' => $thesis['finality_type'] ?? '',
'Synopsis' => $thesis['synopsis'] ?? '',
'Langue(s)' => $thesis['languages'] ?? '',
'Format(s)' => $thesis['formats'] ?? '',
'Mots-clés' => $thesis['keywords'] ?? '',
'Promoteur·ice(s)' => $thesis['jury_promoteurs'] ?? '',
'Président·e du jury' => $thesis['jury_president'] ?? '',
'Lecteurs·rices' => $thesis['jury_lecteurs'] ?? '',
'Durée / Taille' => $thesis['file_size_info'] ?? '',
'Lien' => $thesis['baiu_link'] ?? '',
'Type d\'accès' => $thesis['access_type'] ?? '',
'Licence' => $thesis['license_type'] ?? '',
];
foreach ($fields as $label => $value) {
$v = $value === '' ? '' : htmlspecialchars((string)$value);
$rows .= "<tr><th style='text-align:left;padding:6px 10px;border-bottom:1px solid #eee'>"
. htmlspecialchars($label) . "</th>"
. "<td style='padding:6px 10px;border-bottom:1px solid #eee'>{$v}</td></tr>\n";
}
return <<<HTML
<div style="font-family:system-ui,sans-serif;max-width:600px;margin:0 auto;color:#333">
<h1 style="font-size:1.4rem;color:#222">Merci — ton TFE a bien été enregistré 🎉</h1>
<p style="color:#555;font-size:0.95rem">
Voici un récapitulatif de ta soumission. Tu n'as pas besoin de répondre à cet e-mail.
</p>
<table style="width:100%;border-collapse:collapse;margin-top:1.5rem">
{$rows}
</table>
<p style="margin-top:2rem;font-size:0.85rem;color:#999">
Plateforme xamxam · erg Bruxelles
</p>
</div>
HTML;
}
/**
* Send a confirmation e-mail for a given thesis.
*
* @param Database $db
* @param int $thesisId
* @param array $postData Raw $_POST (must contain 'confirmation_email')
* @return bool True when sent, false otherwise
*/
public static function sendConfirmation(
Database $db,
int $thesisId,
array $postData
): bool {
// ── 1. Read e-mail from confirmation field ─────────────────────────
$to = trim($postData['confirmation_email'] ?? '');
if ($to === '' || filter_var($to, FILTER_VALIDATE_EMAIL) === false) {
error_log('[StudentEmail] No valid confirmation e-mail — skipping thesis #' . $thesisId);
return false;
}
// ── 2. Check SMTP config ────────────────────────────────────────────
if (!SmtpRelay::isConfigured($db)) {
error_log('[StudentEmail] SMTP not configured — skipping thesis #' . $thesisId);
return false;
}
// ── 3. Fetch thesis data ────────────────────────────────────────────
$thesis = $db->getThesis($thesisId);
if (!$thesis) {
error_log('[StudentEmail] Thesis #' . $thesisId . ' not found after creation');
return false;
}
// ── 4. Send ─────────────────────────────────────────────────────────
$subject = 'Merci — ton TFE a bien été enregistré';
$htmlBody = self::buildHtml($thesis);
$result = SmtpRelay::send($db, $to, $subject, $htmlBody);
if ($result) {
error_log("[StudentEmail] Confirmation sent to {$to} for thesis #{$thesisId}");
} else {
error_log("[StudentEmail] Failed to send to {$to} for thesis #{$thesisId}");
}
return $result;
}
}

Binary file not shown.