Extract shared TFE form partial — single source of truth for add/edit/partage

Created templates/partials/form/form.php as the unified form template driven by
$mode ('add'|'edit'|'partage') and boolean flags for optional sections.

The three calling templates (templates/admin/add.php, templates/admin/edit.php,
partage/index.php renderShareLinkForm) now only set variables then include the
shared partial. ~200 lines of duplicated fieldset HTML eliminated.
This commit is contained in:
Pontoporeia
2026-05-07 22:48:18 +02:00
parent ac0008df6c
commit bdd95341b0
13 changed files with 833 additions and 778 deletions

View File

@@ -1,5 +1,12 @@
# XAMXAM TODO
## Extract shared TFE form partial (single source of truth)
- [x] Create `templates/partials/form/form.php` — unified form with `$mode`-driven conditionals
- [x] Refactor `templates/admin/add.php` → thin wrapper setting variables + including form partial
- [x] Refactor `templates/admin/edit.php` → thin wrapper with unified `$oldFn` + form partial
- [x] Refactor `partage/index.php``renderShareLinkForm()` delegates to form partial
- [x] Test all three forms render correctly (add, edit, partage) — syntax verified, logic reviewed
## Fix password-protected share links — form never loads after password entry
- [x] `partage/index.php` — main GET handler: check `$_SESSION['share_verified_' . $slug]` before showing password gate; skip to form if already verified
- [x] `partage/index.php` — add `error_log()` calls throughout password flow (gate entry, hash state, verification result, session check) for debugging

View File

@@ -37,7 +37,7 @@ try {
$isAdmin = true; $bodyClass = 'admin-body';
$extraCss = ['/assets/css/form.css'];
$extraJs = ['/assets/js/sortable.min.js', '/assets/js/file-upload-queue.js'];
$extraJs = ['/assets/js/sortable.min.js', '/assets/js/file-upload-queue.js', '/assets/js/beforeunload-guard.js'];
require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php';
include APP_ROOT . '/templates/admin/edit.php';

View File

@@ -75,18 +75,6 @@
/* ── Buttons ────────────────────────────────────────────────────────────── */
.admin-form-footer {
margin-top: var(--space-l);
padding-top: var(--space-m);
}
/* Sticky variant — pinned below admin header, top-right */
.admin-form-footer--sticky {
position: sticky;
top: 0;
z-index: 10;
margin: 0 0 var(--space-m);
display: flex;
justify-content: flex-end;
gap: var(--space-s);
}
/* ── Admin button aliases — see common.css .btn base class ────────────── */

View File

@@ -427,6 +427,7 @@ main {
.btn--primary {
background: var(--accent-primary);
color: var(--accent-foreground);
border: 1px solid transparent;
}
.btn--primary:hover {

View File

@@ -302,6 +302,10 @@
/* ── Submit / form footer ───────────────────────────────────────────────── */
.form-footer {
margin-top: var(--space-l);
margin-bottom: var(--space-l);
display: flex;
gap: var(--space-s);
align-items: center;
}
.form-footer button {

View File

@@ -0,0 +1,25 @@
/**
* Beforeunload guard — prompts the user before navigating away from unsaved changes.
*
* Attach to any form with a data-beforeunload-guard attribute.
* No effect when JavaScript is unavailable (form posts normally).
*/
(function () {
var forms = document.querySelectorAll('form[data-beforeunload-guard]');
if (!forms.length) return;
var dirty = false;
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
form.addEventListener('input', function () { dirty = true; });
form.addEventListener('change', function () { dirty = true; });
form.addEventListener('submit', function () { dirty = false; });
}
window.addEventListener('beforeunload', function (e) {
if (dirty) {
e.preventDefault();
}
});
})();

View File

@@ -254,6 +254,64 @@ function renderShareLinkForm(string $slug, array $link): void
// Load all form help blocks in one query.
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
// ── Shared form variables ──────────────────────────────────────────────
$mode = 'partage';
$formAction = '/partage/' . urlencode($slug) . '/submit';
$hiddenFields = '<input type="hidden" name="share_link_token" value="' . htmlspecialchars($shareCsrfToken) . '">';
$oldFn = $shareOldFn;
$withAutofocusFn = $shareWithAutofocusFn;
// Synopsis extra: inject fieldset_synopsis help block
ob_start();
$helpContent = $helpFn('fieldset_synopsis');
include APP_ROOT . '/templates/partials/form/form-help-block.php';
$synopsisExtra = ob_get_clean();
// Jury data from repopulation
$juryPromoteur = old($formData, 'jury_promoteur');
$juryPromoteurUlb = old($formData, 'jury_promoteur_ulb_name');
$lecteursInternes = [];
$lecteursExternes = [];
for ($i = 0; $i < 10; $i++) {
$n = old($formData, "jury_lecteur_interne:$i");
if ($n !== '') $lecteursInternes[] = ['name' => $n];
}
for ($i = 0; $i < 10; $i++) {
$n = old($formData, "jury_lecteur_externe:$i");
if ($n !== '') $lecteursExternes[] = ['name' => $n];
}
$juryPresident = null;
$showPresident = false;
$showPromoteurUlb = true;
$promoteurUlbConditional = true;
// Licence / access
$libreEnabled = ($siteSettings['access_type_libre_enabled'] ?? '0') === '1';
$interneEnabled = ($siteSettings['access_type_interne_enabled'] ?? '1') === '1';
$interditEnabled = ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1';
$generalitiesHtml = $helpFn('fieldset_generalites');
$defaultAccessTypeId = 2;
// Optional sections
$showFlash = true;
$showIntroHelp = true;
$showEmailConfirmation = true;
// Files: add mode
$filesMode = 'add';
// Website URL from repopulation
$existingWebsiteUrl = $formData['website_url'] ?? '';
$existingWebsiteLabel = $formData['website_label'] ?? '';
$checkedFormatsForSiteWeb = $formData['formats'] ?? [];
// Context / backoffice not shown in partage
$currentRaw = [];
$currentAuthorEmail = null;
$currentAuthorShowContact = false;
$currentContextNote = null;
?>
<!DOCTYPE html>
<html lang="fr">
@@ -277,195 +335,13 @@ function renderShareLinkForm(string $slug, array $link): void
<body class="student-body">
<main id="main-content">
<div class="thesis-add-header">
<h1>Soumettre un TFE</h1>
<h1><?= htmlspecialchars($pageTitle) ?></h1>
<?php if ($isVerified): ?>
<span class="share-badge">🔓 Accès partagé</span>
<?php endif; ?>
</div>
<?php
// Show flash messages from error redirect
$flashError = $_SESSION['_flash_error'] ?? null;
$flashWarning = $_SESSION['_flash_warning'] ?? null;
$flashSuccess = $_SESSION['_flash_success'] ?? null;
unset($_SESSION['_flash_error'], $_SESSION['_flash_warning'], $_SESSION['_flash_success']);
?>
<?php if ($flashError): ?>
<div class="flash-error" role="alert"><?= htmlspecialchars($flashError) ?></div>
<?php endif; ?>
<?php if ($flashWarning): ?>
<div class="flash-warning" id="flash-warning" role="alert" tabindex="-1"><?= htmlspecialchars($flashWarning) ?></div>
<script>document.addEventListener('DOMContentLoaded',function(){var el=document.getElementById('flash-warning');if(el){el.scrollIntoView({behavior:'smooth',block:'center'});el.focus();}});</script>
<?php endif; ?>
<?php if ($flashSuccess): ?>
<div class="flash-success" role="alert"><?= htmlspecialchars($flashSuccess) ?></div>
<?php endif; ?>
<?php $helpContent = $helpFn('partage_intro'); include APP_ROOT . '/templates/partials/form/form-help-block.php'; ?>
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
<form action="/partage/<?= urlencode($slug) ?>/submit" method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="share_link_token" value="<?= htmlspecialchars($shareCsrfToken) ?>">
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
<?php
$oldFn = $shareOldFn;
$withAutofocusFn = $shareWithAutofocusFn;
ob_start();
$helpContent = $helpFn('fieldset_synopsis');
include APP_ROOT . '/templates/partials/form/form-help-block.php';
$synopsisExtra = ob_get_clean();
$helpContent = $helpFn('fieldset_tfe_info');
include APP_ROOT . '/templates/partials/form/form-help-block.php';
include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php';
?>
<!-- ═══════════════════ Langue(s) ═══════════════════ -->
<fieldset>
<legend>Langue(s)</legend>
<?php $name = 'languages'; $label = 'Langue(s) du TFE :'; $options = $languages; $checked = $formData['languages'] ?? []; $required = true; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
<?php $name = 'language_autre'; $label = 'Autre(s) langue(s) :'; $value = old($formData, 'language_autre'); $hint = 'Si votre TFE contient une langue absente de la liste, précisez-la ici.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
</fieldset>
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
<fieldset>
<legend>Mots-clés</legend>
<?php
$name = 'tag'; $label = 'Mots-clés (max 10) :'; $value = old($formData, 'tag');
$placeholder = 'sociologie, anthropologie, ...';
$hint = 'Séparez par des virgules. Max 10 mots-clés.';
include APP_ROOT . '/templates/partials/form/text-field.php';
?>
</fieldset>
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
<?php
$oldFn = $shareOldFn;
$withAutofocusFn = $shareWithAutofocusFn;
$helpContent = $helpFn('fieldset_academic');
include APP_ROOT . '/templates/partials/form/form-help-block.php';
include APP_ROOT . '/templates/partials/form/fieldset-academic.php';
?>
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
<?php
$juryPromoteur = old($formData, 'jury_promoteur');
$juryPromoteurUlb = old($formData, 'jury_promoteur_ulb_name');
$lecteursInternes = [];
$lecteursExternes = [];
for ($i = 0; $i < 10; $i++) {
$n = old($formData, "jury_lecteur_interne:$i");
if ($n !== '') $lecteursInternes[] = ['name' => $n];
}
for ($i = 0; $i < 10; $i++) {
$n = old($formData, "jury_lecteur_externe:$i");
if ($n !== '') $lecteursExternes[] = ['name' => $n];
}
$juryPresident = null;
$showPresident = false;
$showPromoteurUlb = true;
$promoteurUlbConditional = true;
$helpContent = $helpFn('fieldset_jury');
include APP_ROOT . '/templates/partials/form/form-help-block.php';
require APP_ROOT . '/templates/partials/form/jury-fieldset.php';
?>
<!-- ═══════════════════ Format(s) ═══════════════════ -->
<fieldset>
<legend>Format(s)</legend>
<?php
$name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; $required = true;
$hxPost = '/partage/format-website-fragment';
$hxTarget = '#website-url-fieldset';
// Capture before include unsets it
$_checkedFormatsForSiteWeb = $checked;
include APP_ROOT . '/templates/partials/form/checkbox-list.php';
?>
</fieldset>
<!-- ═══════════════════ Fichiers ═══════════════════ -->
<?php
$helpContent = $helpFn('fieldset_files');
include APP_ROOT . '/templates/partials/form/form-help-block.php';
include APP_ROOT . '/templates/partials/form/fieldset-files.php';
?>
<!-- Website URL fieldset — shown/hidden via HTMX when "Site web" checked -->
<fieldset id="website-url-fieldset" style="display:none">
<legend>Site web</legend>
<div class="admin-form-group">
<label for="website_url">URL du site :</label>
<div class="admin-file-input">
<input type="url"
id="website_url"
name="website_url"
value="<?= htmlspecialchars($formData['website_url'] ?? '') ?>"
placeholder="https://mon-tfe.erg.be">
<small>Si le TFE est un site web, entrez son URL ici. Il sera affiché comme un site embarqué sur la page du TFE.</small>
</div>
</div>
<div class="admin-form-group">
<label for="website_label">Légende :</label>
<input type="text"
id="website_label"
name="website_label"
value="<?= htmlspecialchars($formData['website_label'] ?? '') ?>"
placeholder="Description du site (optionnel)"
class="admin-file-label-input"
style="max-width:400px;">
</div>
</fieldset>
<?php
// Server-side: show if Site web already checked (e.g. on error redirect)
$_stmt = Database::getInstance()->getConnection()->prepare('SELECT id FROM format_types WHERE name = ? LIMIT 1');
$_stmt->execute(['Site web']);
$_siteWebId = $_stmt->fetchColumn();
if ($_siteWebId && in_array((string)$_siteWebId, array_map('strval', $_checkedFormatsForSiteWeb), true)) {
echo '<script>document.getElementById("website-url-fieldset").style.display=""</script>';
}
?>
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
<?php
$oldFn = $shareOldFn;
$withAutofocusFn = $shareWithAutofocusFn;
include APP_ROOT . '/templates/partials/form/fieldset-metadata.php';
?>
<!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ -->
<?php
$libreEnabled = ($siteSettings['access_type_libre_enabled'] ?? '0') === '1';
$interneEnabled = ($siteSettings['access_type_interne_enabled'] ?? '1') === '1';
$interditEnabled = ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1';
$generalitiesHtml = $helpFn('fieldset_generalites');
$defaultAccessTypeId = 2;
$helpContent = $helpFn('fieldset_access');
include APP_ROOT . '/templates/partials/form/form-help-block.php';
include APP_ROOT . '/templates/partials/form/fieldset-licence-explanation.php';
?>
<!-- ═══════════════════ E-mail de confirmation ═══════════ -->
<fieldset>
<legend>E-mail de confirmation</legend>
<?php $helpContent = $helpFn('fieldset_email'); include APP_ROOT . '/templates/partials/form/form-help-block.php'; ?>
<?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">
<button type="submit" name="go" class="btn btn--primary">Soumettre</button>
</div>
</form>
<?php include APP_ROOT . '/templates/partials/form/form.php'; ?>
</main>
</body>
</html>

View File

@@ -1,204 +1,49 @@
<main id="main-content">
<div class="thesis-add-header">
<h1>Ajouter un TFE</h1>
</div>
<h1>Ajouter un TFE</h1>
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
<form action="actions/formulaire.php" method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<?php
// ── Variables for the shared form partial ─────────────────────────────────
$mode = 'add';
$formAction = 'actions/formulaire.php';
$hiddenFields = '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($_SESSION["csrf_token"]) . '">';
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
<?php
$oldFn = 'old';
$withAutofocusFn = 'withAutofocus';
$allowedObjet = [];
$synopsisExtra = '';
include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php';
?>
$synopsisExtra = '';
<!-- ═══════════════════ Contact ═══════════════════ -->
<fieldset>
<legend>Contact</legend>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="contact_public" value="1"
<?= !empty($formData['contact_public']) ? 'checked' : '' ?>>
Rendre le contact visible publiquement sur la fiche du TFE
</label>
<small>L'adresse est toujours conservée en interne comme contact de référence.</small>
</div>
</fieldset>
// Jury: fresh add (all empty)
$juryPromoteur = null;
$juryPromoteurUlb = null;
$lecteursInternes = [];
$lecteursExternes = [];
$juryPresident = null;
$showPresident = false;
$showPromoteurUlb = true;
$promoteurUlbConditional = false;
<!-- ═══════════════════ Langue(s) ═══════════════════ -->
<fieldset>
<legend>Langue(s)</legend>
<?php $name = 'languages'; $label = 'Langue(s) du TFE :'; $options = $languages; $checked = $formData['languages'] ?? []; $required = true; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
<?php $name = 'language_autre'; $label = 'Autre(s) langue(s) :'; $value = old('language_autre'); $hint = 'Si votre TFE contient une langue absente de la liste, précisez-la ici.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
</fieldset>
// Licence / access
$libreEnabled = ($siteSettings['access_type_libre_enabled'] ?? '0') === '1';
$interneEnabled = ($siteSettings['access_type_interne_enabled'] ?? '1') === '1';
$interditEnabled = ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1';
$generalitiesHtml = $helpFn('fieldset_generalites');
$defaultAccessTypeId = 2;
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
<fieldset>
<legend>Mots-clés</legend>
<?php
$name = 'tag'; $label = 'Mots-clés (max 10) :'; $value = old('tag');
$placeholder = 'sociologie, anthropologie, ...';
$hint = 'Séparez par des virgules. Max 10 mots-clés.';
$attrs = withAutofocus('tag');
include APP_ROOT . '/templates/partials/form/text-field.php';
?>
</fieldset>
// Optional sections
$showContact = true;
$showContextNote = true;
$showBackoffice = true;
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
<?php
$oldFn = 'old';
$withAutofocusFn = 'withAutofocus';
include APP_ROOT . '/templates/partials/form/fieldset-academic.php';
?>
// Files: add mode
$filesMode = 'add';
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
<?php
$juryPromoteur = null;
$juryPromoteurUlb = null;
$lecteursInternes = [];
$lecteursExternes = [];
$juryPresident = null;
$showPresident = false;
$showPromoteurUlb = true;
$promoteurUlbConditional = false;
require APP_ROOT . '/templates/partials/form/jury-fieldset.php';
?>
// Website URL (repopulation)
$existingWebsiteUrl = $formData['website_url'] ?? '';
$existingWebsiteLabel = $formData['website_label'] ?? '';
<!-- ═══════════════════ Format(s) ═══════════════════ -->
<fieldset>
<legend>Format(s)</legend>
<?php
$name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; $required = true;
$hxPost = '/partage/format-website-fragment';
$hxTarget = '#website-url-fieldset';
// Capture before include unsets it
$_checkedFormatsForSiteWeb = $checked;
include APP_ROOT . '/templates/partials/form/checkbox-list.php';
?>
</fieldset>
// Backoffice (add mode: null → falls back to formData)
$currentRaw = [];
$currentAuthorEmail = null;
$currentAuthorShowContact = false;
$currentContextNote = null;
<!-- ═══════════════════ Fichiers ═══════════════════ -->
<?php include APP_ROOT . '/templates/partials/form/fieldset-files.php'; ?>
<!-- Website URL fieldset — shown/hidden via HTMX when "Site web" checked -->
<fieldset id="website-url-fieldset" style="display:none">
<legend>Site web</legend>
<div class="admin-form-group">
<label for="website_url">URL du site :</label>
<div class="admin-file-input">
<input type="url"
id="website_url"
name="website_url"
value="<?= htmlspecialchars($formData['website_url'] ?? '') ?>"
placeholder="https://mon-tfe.erg.be">
<small>Si le TFE est un site web, entrez son URL ici. Il sera affiché comme un site embarqué sur la page du TFE.</small>
</div>
</div>
<div class="admin-form-group">
<label for="website_label">Légende :</label>
<input type="text"
id="website_label"
name="website_label"
value="<?= htmlspecialchars($formData['website_label'] ?? '') ?>"
placeholder="Description du site (optionnel)"
class="admin-file-label-input"
style="max-width:400px;">
</div>
</fieldset>
<?php
// Server-side: show if Site web already checked (e.g. on error redirect)
$_stmt = Database::getInstance()->getConnection()->prepare('SELECT id FROM format_types WHERE name = ? LIMIT 1');
$_stmt->execute(['Site web']);
$_siteWebId = $_stmt->fetchColumn();
if ($_siteWebId && in_array((string)$_siteWebId, array_map('strval', $_checkedFormatsForSiteWeb), true)) {
echo '<script>document.getElementById("website-url-fieldset").style.display=""</script>';
}
?>
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
<?php
$oldFn = 'old';
$withAutofocusFn = 'withAutofocus';
include APP_ROOT . '/templates/partials/form/fieldset-metadata.php';
?>
<!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ -->
<?php
$formData = $_SESSION['form_data'] ?? [];
$libreEnabled = ($siteSettings['access_type_libre_enabled'] ?? '0') === '1';
$interneEnabled = ($siteSettings['access_type_interne_enabled'] ?? '1') === '1';
$interditEnabled = ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1';
$generalitiesHtml = $helpBlocks['fieldset_generalites'] ?? '';
$defaultAccessTypeId = 2;
include APP_ROOT . '/templates/partials/form/fieldset-licence-explanation.php';
?>
<!-- ═══════════════════ Note contextuelle ═══════════════════ -->
<fieldset>
<legend>Note contextuelle</legend>
<div>
<label for="context_note">Note contextuelle :</label>
<div>
<textarea id="context_note" name="context_note"
rows="4" maxlength="1500"><?= htmlspecialchars($formData['context_note'] ?? '') ?></textarea>
<small>Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères.</small>
</div>
</div>
</fieldset>
<!-- ═══════════════════ Backoffice ═══════════════════ -->
<fieldset>
<legend>Backoffice</legend>
<div class="admin-form-group">
<label for="jury_points">Points :</label>
<input type="number" id="jury_points" name="jury_points"
value="<?= htmlspecialchars($formData['jury_points'] ?? '') ?>"
step="0.01" min="0" max="20" placeholder="sur 20">
<small>Note du jury (interne, non visible publiquement).</small>
</div>
<div class="admin-form-group">
<label for="remarks">Remarques :</label>
<textarea id="remarks" name="remarks" rows="4"><?= htmlspecialchars($formData['remarks'] ?? '') ?></textarea>
<small>Notes internes (non visibles publiquement).</small>
</div>
<div class="admin-form-group">
<label for="contact_interne">Contact interne :</label>
<input type="email" id="contact_interne" name="contact_interne"
value="<?= htmlspecialchars($formData['contact_interne'] ?? '') ?>"
placeholder="ton.email@exemple.be">
<small>Adresse de contact interne (non visible publiquement).</small>
</div>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="exemplaire_baiu" value="1"
<?= !empty($formData['exemplaire_baiu']) ? 'checked' : '' ?>>
Exemplaire physique BAIU
</label>
<small>Case logistique : cocher si un exemplaire physique est disponible à la BAIU.</small>
</div>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="exemplaire_erg" value="1"
<?= !empty($formData['exemplaire_erg']) ? 'checked' : '' ?>>
Exemplaire physique ERG
</label>
<small>Case logistique : cocher si un exemplaire physique est disponible à l'ERG.</small>
</div>
</fieldset>
<div class="form-footer">
<button type="submit" name="go" class="btn btn--primary">Soumettre</button>
</div>
</form>
include APP_ROOT . '/templates/partials/form/form.php';
?>
</main>

View File

@@ -1,402 +1,110 @@
<main id="main-content">
<h1>Modifier un TFE</h1>
<form method="post" action="/admin/actions/edit.php" class="admin-form" enctype="multipart/form-data">
<div class="admin-form-footer admin-form-footer--sticky">
<button type="submit" class="btn btn--primary">Enregistrer</button>
<a href="/admin/" class="btn btn--secondary admin-cancel-link">Annuler</a>
</div>
<?php
// ── Build a unified old() callable for the entire edit form ────────────────
$editFormData = array_merge($formData ?? [], [
'titre' => $thesis['title'],
'subtitle' => $thesis['subtitle'] ?? '',
'auteurice' => $thesis['authors'] ?? '',
'mail' => $currentAuthorEmail ?? '',
'synopsis' => $thesis['synopsis'] ?? '',
'tag' => $thesis['keywords'] ?? '',
'année' => $thesis['year'],
'orientation' => $thesis['orientation'],
'ap' => $thesis['ap_program'],
'finality' => $thesis['finality_type'],
'duration_pages' => $currentRaw['duration_pages'] ?? '',
'duration_minutes' => $currentRaw['duration_minutes'] ?? '',
'lien' => $thesis['baiu_link'] ?? '',
'contact_public' => $currentAuthorShowContact ?? false,
]);
$oldFn = fn(string $key, string $default = '') =>
isset($editFormData[$key]) && !is_array($editFormData[$key])
? htmlspecialchars((string)$editFormData[$key]) : $default;
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
$withAutofocusFn = function (string $field, array $attrs = []) use ($autofocusField) {
if ($autofocusField === $field) $attrs['autofocus'] = true;
return $attrs;
};
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
<?php
$formData = array_merge($formData ?? [], ['contact_public' => $currentAuthorShowContact ?? false]);
$editOldFn = function (string $key, string $default = '') use ($thesis, $formData, $currentAuthorEmail) {
if (!empty($formData[$key])) return htmlspecialchars($formData[$key]);
$map = [
'titre' => htmlspecialchars($thesis['title']),
'subtitle' => htmlspecialchars($thesis['subtitle'] ?? ''),
'auteurice'=> htmlspecialchars($thesis['authors'] ?? ''),
'mail' => htmlspecialchars($currentAuthorEmail ?? ''),
'synopsis' => htmlspecialchars($thesis['synopsis'] ?? ''),
];
return $map[$key] ?? $default;
};
$editWithAutofocusFn = function (string $field, array $attrs = []) use ($autofocusField) {
if ($autofocusField === $field) $attrs['autofocus'] = true;
return $attrs;
};
$allowedObjet = [];
$synopsisExtra = '';
$oldFn = $editOldFn;
$withAutofocusFn = $editWithAutofocusFn;
include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php';
$formData = $_SESSION['form_data'] ?? [];
?>
// ── Shared form variables ──────────────────────────────────────────────────
$mode = 'edit';
$formAction = '/admin/actions/edit.php';
$hiddenFields = '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($_SESSION['csrf_token']) . '">'
. '<input type="hidden" name="thesis_id" value="' . $thesisId . '">';
<!-- Contact public checkbox (admin) -->
<fieldset>
<legend>Contact</legend>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="contact_public" value="1"
<?= !empty($formData['contact_public']) || $currentAuthorShowContact ? 'checked' : '' ?>>
Rendre le contact visible publiquement sur la fiche du TFE
</label>
<small>L'adresse est toujours conservée en interne comme contact de référence.</small>
</div>
</fieldset>
$synopsisExtra = '';
$formData = $editFormData;
<!-- ═══════════════════ Langue(s) ═══════════════════ -->
<fieldset>
<legend>Langue(s)</legend>
<?php
$checkedLanguages = $formData['languages'] ?? $currentLanguages;
$name = 'languages'; $label = 'Langue(s) du TFE :'; $options = $languages; $checked = $checkedLanguages; $required = true;
include APP_ROOT . '/templates/partials/form/checkbox-list.php';
?>
<?php $name = 'language_autre'; $label = 'Autre(s) langue(s) :'; $value = old('language_autre'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
</fieldset>
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
<fieldset>
<legend>Mots-clés</legend>
<?php
$editKwFormData = ['tag' => $thesis['keywords'] ?? ''];
$editKwOldFn = fn(string $key, string $default = '') => isset($editKwFormData[$key]) ? htmlspecialchars((string)$editKwFormData[$key]) : $default;
$oldFn = $editKwOldFn; $withAutofocusFn = $editWithAutofocusFn; $formData = $editKwFormData;
$name = 'tag'; $label = 'Mots-clés (max 10) :'; $value = $editKwOldFn('tag');
$placeholder = 'sociologie, anthropologie, ...';
$hint = 'Séparez par des virgules. Max 10 mots-clés.';
include APP_ROOT . '/templates/partials/form/text-field.php';
?>
</fieldset>
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
<?php
$editFormData = [
'année' => $thesis['year'],
'orientation' => $thesis['orientation'],
'ap' => $thesis['ap_program'],
'finality' => $thesis['finality_type'],
];
$editAcademicOldFn = function (string $key, string $default = '') use ($editFormData) {
return isset($editFormData[$key]) && !is_array($editFormData[$key])
? htmlspecialchars((string)$editFormData[$key]) : $default;
};
$oldFn = $editAcademicOldFn;
$withAutofocusFn = $editWithAutofocusFn;
$formData = $editFormData;
include APP_ROOT . '/templates/partials/form/fieldset-academic.php';
?>
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
<?php
$juryPromoteur = null;
$juryPromoteurUlb = null;
$lecteursInternes = [];
$lecteursExternes = [];
$juryPresident = null;
foreach ($jury as $jm) {
if ($jm['role'] === 'president') {
$juryPresident = $jm['name'];
} elseif ($jm['role'] === 'promoteur') {
if (($jm['is_ulb'] ?? 0) == 1) {
$juryPromoteurUlb = $jm['name'];
} else {
$juryPromoteur = $jm['name'];
}
} elseif ($jm['role'] === 'lecteur') {
if (($jm['is_external'] ?? 0) == 1) {
$lecteursExternes[] = $jm;
} else {
$lecteursInternes[] = $jm;
}
// Jury data
$juryPromoteur = null;
$juryPromoteurUlb = null;
$lecteursInternes = [];
$lecteursExternes = [];
$juryPresident = null;
foreach ($jury as $jm) {
if ($jm['role'] === 'president') {
$juryPresident = $jm['name'];
} elseif ($jm['role'] === 'promoteur') {
if (($jm['is_ulb'] ?? 0) == 1) {
$juryPromoteurUlb = $jm['name'];
} else {
$juryPromoteur = $jm['name'];
}
} elseif ($jm['role'] === 'lecteur') {
if (($jm['is_external'] ?? 0) == 1) {
$lecteursExternes[] = $jm;
} else {
$lecteursInternes[] = $jm;
}
}
$showPresident = true;
$showPromoteurUlb = true;
$promoteurUlbConditional = false;
require APP_ROOT . '/templates/partials/form/jury-fieldset.php';
?>
}
$showPresident = true;
$showPromoteurUlb = true;
$promoteurUlbConditional = false;
<!-- ═══════════════════ Format(s) ═══════════════════ -->
<fieldset>
<legend>Format(s)</legend>
<?php
$checkedFormats = $formData['formats'] ?? $currentFormats ?? [];
$name = 'formats'; $label = 'Format(s) du TFE :'; $options = $formatTypes; $checked = $checkedFormats; $required = true;
$hxPost = '/partage/format-website-fragment';
$hxTarget = '#website-url-fieldset';
// Capture before include unsets it
$_checkedFormatsForSiteWeb = $checked;
include APP_ROOT . '/templates/partials/form/checkbox-list.php';
?>
</fieldset>
// Licence / access — always all enabled for admin
$libreEnabled = true;
$interneEnabled = true;
$interditEnabled = true;
$generalitiesHtml = $helpFn('fieldset_generalites');
$defaultAccessTypeId = $currentAccessTypeId ?? 2;
$formData['access_type_id'] = $currentAccessTypeId;
$formData['license_id'] = $currentLicenseId;
$formData['license_custom'] = $currentRaw['license_custom'] ?? '';
$formData['cc2r'] = $currentRaw['cc4r'] ?? false;
<!-- ═══════════════════ Fichiers ═══════════════════ -->
<fieldset>
<legend>Fichiers</legend>
// Optional sections
$showContact = true;
$showContextNote = true;
$showBackoffice = true;
$showPublish = true;
<!-- Cover image -->
<div class="admin-form-group">
<label>Image de couverture :</label>
<div class="admin-file-input">
<?php if (!empty($currentCover)): ?>
<div class="admin-banner-preview">
<img src="/media.php?path=<?= urlencode($currentCover['file_path']) ?>"
alt="Couverture actuelle" style="max-height:180px;">
<label class="admin-checkbox-label">
<input type="checkbox" name="remove_cover" value="1"> Supprimer la couverture
</label>
</div>
<?php endif; ?>
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png" data-preview="fp-couverture">
<div id="fp-couverture" class="file-preview-list" aria-live="polite"></div>
<small><?= empty($currentCover) ? 'JPG, PNG. Format 4:3 recommandé. Max 20 MB.' : 'Laisser vide pour conserver la couverture actuelle. JPG, PNG. Max 20 MB.' ?></small>
</div>
</div>
// Files: edit mode
$filesMode = 'edit';
$currentCover = $currentCover ?? null;
$currentFiles = $currentFiles ?? [];
$currentBannerPath = $thesis['banner_path'] ?? null;
$currentContextNote = $currentContextNote ?? null;
<!-- Existing thesis files — sortable, with labels -->
<?php $thesisFilesList = array_values(array_filter($currentFiles, fn($f) => $f['file_type'] !== 'cover')); ?>
<?php if (!empty($thesisFilesList)): ?>
<div class="admin-form-group">
<label>Fichiers du TFE existants :</label>
<small style="display:block;margin-bottom:var(--space-2xs);color:var(--text-tertiary)">
Glissez-déposez les lignes pour réordonner les fichiers sur la page publique.
</small>
<ul id="existing-files-sortable" class="admin-file-list sortable-list">
<?php foreach ($thesisFilesList as $f):
$fExt = strtolower(pathinfo($f['file_path'] ?? '', PATHINFO_EXTENSION));
$fType = $f['file_type'] ?? 'other';
$fIcon = match(true) {
$fType === 'main' || $fExt === 'pdf' => '📄',
in_array($fExt, ['jpg','jpeg','png','gif','webp']) => '🖼️',
$fType === 'video' || in_array($fExt, ['mp4','webm','mov','ogv']) => '🎬',
$fType === 'audio' || in_array($fExt, ['mp3','ogg','wav','flac','aac','m4a']) => '🔊',
$fType === 'caption' || $fExt === 'vtt' => '💬',
$fType === 'website' => '🌐',
default => '📎',
};
$isExternalUrl = str_starts_with($f['file_path'] ?? '', 'http://') || str_starts_with($f['file_path'] ?? '', 'https://');
$fLinkHref = $isExternalUrl
? htmlspecialchars($f['file_path'])
: ('/media.php?path=' . urlencode($f['file_path']));
?>
<li class="admin-file-list-item" data-file-id="<?= (int)$f['id'] ?>">
<input type="hidden" name="file_sort_order[]" value="<?= (int)$f['id'] ?>">
<span class="admin-file-drag-handle" title="Réordonner">⠿</span>
<span class="admin-file-icon-col"><?= $fIcon ?></span>
<span class="admin-file-info">
<a href="<?= $fLinkHref ?>" target="_blank" rel="noopener" class="admin-file-name">
<?= htmlspecialchars($f['file_name'] ?? basename($f['file_path'])) ?>
</a>
<span class="admin-file-meta-row">
<span class="admin-file-type-badge"><?= htmlspecialchars($fType) ?></span>
<?php if (!empty($f['file_size']) && $f['file_size'] > 0): ?>
<span class="admin-file-size"><?= number_format($f['file_size'] / 1024 / 1024, 2) ?> MB</span>
<?php endif; ?>
</span>
<input type="text" name="file_label[<?= (int)$f['id'] ?>]"
value="<?= htmlspecialchars($f['display_label'] ?? '') ?>"
placeholder="Légende / description (optionnel)"
class="admin-file-label-input">
</span>
<label class="admin-checkbox-label admin-file-delete">
<input type="checkbox" name="delete_files[]" value="<?= (int)$f['id'] ?>"> Supprimer
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- New thesis files -->
<div class="admin-form-group admin-files-fieldgroup">
<label>Ajouter des fichiers du TFE :</label>
<div class="admin-file-input">
<input type="file" id="tfe-files-input"
name="files[]" multiple
accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.mp4,.webm,.mov,.ogv,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a,.zip,.tar,.gz,.vtt"
class="tfe-file-picker">
<small class="admin-file-hint">
Types acceptés : PDF · JPG/PNG/GIF/WEBP · MP4/WebM/MOV (vidéo) · MP3/OGG/WAV/FLAC (audio) · ZIP/TAR (archives) · autres fichiers (téléchargement uniquement). Max 500 MB par fichier.
</small>
<ul id="tfe-file-queue" class="tfe-file-queue sortable-list" aria-label="Nouveaux fichiers (réordonnable)"></ul>
<p id="tfe-file-queue-empty" class="tfe-queue-empty">Aucun nouveau fichier sélectionné.</p>
</div>
</div>
<!-- Banner image -->
<div class="admin-form-group">
<label>Image bannière (accueil) :</label>
<div class="admin-file-input">
<?php if (!empty($thesis['banner_path'])): ?>
<div class="admin-banner-preview">
<img src="/media.php?path=<?= urlencode($thesis['banner_path']) ?>" alt="Bannière actuelle">
<label class="admin-checkbox-label">
<input type="checkbox" name="remove_banner" value="1"> Supprimer la bannière
</label>
</div>
<?php endif; ?>
<input type="file" name="banner" id="banner" accept="image/jpeg,image/png,image/webp" data-preview="fp-banner">
<div id="fp-banner" class="file-preview-list" aria-live="polite"></div>
<small><?= empty($thesis['banner_path']) ? 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB.' : 'Laisser vide pour conserver la bannière actuelle. JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB.' ?></small>
</div>
</div>
</fieldset>
<!-- Website URL fieldset — shown/hidden via HTMX when "Site web" checked -->
<?php
// Extract existing website URL/label from thesis_files for initial render
$existingWebsite = null;
$existingWebsiteLabel = '';
foreach ($currentFiles as $f) {
if ($f['file_type'] === 'website') {
$existingWebsite = $f['file_path'] ?? '';
$existingWebsiteLabel = $f['display_label'] ?? '';
break;
}
// Website URL from existing files
$existingWebsiteUrl = '';
$existingWebsiteLabel = '';
foreach ($currentFiles as $f) {
if ($f['file_type'] === 'website') {
$existingWebsiteUrl = $f['file_path'] ?? '';
$existingWebsiteLabel = $f['display_label'] ?? '';
break;
}
?>
<fieldset id="website-url-fieldset" style="display:none">
<legend>Site web</legend>
}
<div class="admin-form-group">
<label for="website_url">URL du site :</label>
<div class="admin-file-input">
<input type="url"
id="website_url"
name="website_url"
value="<?= htmlspecialchars($existingWebsite ?? '') ?>"
placeholder="https://mon-tfe.erg.be">
<small>Si le TFE est un site web, entrez son URL ici. Il sera affiché comme un site embarqué sur la page du TFE.</small>
</div>
</div>
// Formats — either from flash repopulation or current thesis data
$checkedFormats = $formData['formats'] ?? $currentFormats ?? [];
// Populate formData.formats for checkbox-list partial
$formData['formats'] = $checkedFormats;
$checkedFormatsForSiteWeb = $checkedFormats;
<div class="admin-form-group">
<label for="website_label">Légende :</label>
<input type="text"
id="website_label"
name="website_label"
value="<?= htmlspecialchars($existingWebsiteLabel) ?>"
placeholder="Description du site (optionnel)"
class="admin-file-label-input"
style="max-width:400px;">
</div>
</fieldset>
<?php
// Server-side: show if Site web already checked
$_stmt = Database::getInstance()->getConnection()->prepare('SELECT id FROM format_types WHERE name = ? LIMIT 1');
$_stmt->execute(['Site web']);
$_siteWebId = $_stmt->fetchColumn();
if ($_siteWebId && in_array((string)$_siteWebId, array_map('strval', $_checkedFormatsForSiteWeb), true)) {
echo '<script>document.getElementById("website-url-fieldset").style.display=""</script>';
}
?>
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
<?php
$editMetaFormData = [
'duration_pages' => $currentRaw['duration_pages'] ?? '',
'duration_minutes' => $currentRaw['duration_minutes'] ?? '',
'lien' => $thesis['baiu_link'] ?? '',
];
$editMetaOldFn = function (string $key, string $default = '') use ($editMetaFormData) {
return isset($editMetaFormData[$key]) ? htmlspecialchars((string)$editMetaFormData[$key]) : $default;
};
$oldFn = $editMetaOldFn;
$withAutofocusFn = $editWithAutofocusFn;
$formData = $editMetaFormData;
include APP_ROOT . '/templates/partials/form/fieldset-metadata.php';
?>
<!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ -->
<?php
$formData = $_SESSION['form_data'] ?? [];
$libreEnabled = true; // always shown in admin
$interneEnabled = true;
$interditEnabled = true;
$generalitiesHtml = $helpFn('fieldset_generalites');
$defaultAccessTypeId = $currentAccessTypeId ?? 2;
$formData['access_type_id'] = $currentAccessTypeId;
$formData['license_id'] = $currentLicenseId;
$formData['license_custom'] = $currentRaw['license_custom'] ?? '';
$formData['cc2r'] = $currentRaw['cc4r'] ?? false;
include APP_ROOT . '/templates/partials/form/fieldset-licence-explanation.php';
?>
<!-- ═══════════════════ Note contextuelle ═══════════════════ -->
<fieldset>
<legend>Note contextuelle</legend>
<div>
<label for="context_note">Note contextuelle :</label>
<div>
<textarea id="context_note" name="context_note"
rows="4" maxlength="1500"><?= htmlspecialchars($currentContextNote ?? '') ?></textarea>
<small>Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères.</small>
</div>
</div>
</fieldset>
<!-- ═══════════════════ Backoffice ═══════════════════ -->
<fieldset>
<legend>Backoffice</legend>
<div class="admin-form-group">
<label for="jury_points">Points :</label>
<input type="number" id="jury_points" name="jury_points"
value="<?= htmlspecialchars($currentRaw['jury_points'] ?? '') ?>"
step="0.01" min="0" max="20" placeholder="sur 20">
<small>Note du jury (interne, non visible publiquement).</small>
</div>
<div class="admin-form-group">
<label for="remarks">Remarques :</label>
<textarea id="remarks" name="remarks" rows="4"><?= htmlspecialchars($currentRaw['remarks'] ?? '') ?></textarea>
<small>Notes internes (non visibles publiquement).</small>
</div>
<div class="admin-form-group">
<label for="contact_interne">Contact interne :</label>
<input type="email" id="contact_interne" name="contact_interne"
value="<?= htmlspecialchars($currentRaw['contact_interne'] ?? $currentAuthorEmail ?? '') ?>"
placeholder="ton.email@exemple.be">
<small>Adresse de contact interne (non visible publiquement).</small>
</div>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="exemplaire_baiu" value="1"
<?= !empty($currentRaw['exemplaire_baiu']) ? 'checked' : '' ?>>
Exemplaire physique BAIU
</label>
<small>Case logistique : cocher si un exemplaire physique est disponible à la BAIU.</small>
</div>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="exemplaire_erg" value="1"
<?= !empty($currentRaw['exemplaire_erg']) ? 'checked' : '' ?>>
Exemplaire physique ERG
</label>
<small>Case logistique : cocher si un exemplaire physique est disponible à l'ERG.</small>
</div>
</fieldset>
<!-- ═══════════════════ Publication ════════════════════════ -->
<fieldset>
<legend>Publication</legend>
<div>
<label class="admin-checkbox-label">
<input type="checkbox" name="is_published" value="1"
<?= $thesis['is_published'] ? 'checked' : '' ?>>
Publier ce TFE sur le site public
</label>
</div>
</fieldset>
</form>
include APP_ROOT . '/templates/partials/form/form.php';
?>
</main>

View File

@@ -46,4 +46,3 @@ $formData = $formData ?? [];
?>
</fieldset>
<?php
unset($oldFn, $withAutofocusFn);

View File

@@ -46,4 +46,3 @@ $formData = $formData ?? [];
?>
</fieldset>
<?php
unset($oldFn, $withAutofocusFn);

View File

@@ -71,4 +71,4 @@ $synopsisExtra = $synopsisExtra ?? '';
</div>
</fieldset>
<?php
unset($oldFn, $withAutofocusFn, $allowedObjet, $synopsisExtra);
unset($allowedObjet, $synopsisExtra);

View File

@@ -0,0 +1,603 @@
<?php
/**
* Shared TFE form partial — single source of truth for add, edit, and partage forms.
*
* Required variables (set by the including page):
* string $mode — 'add' | 'edit' | 'partage'
* string $formAction — form action URL
* string $hiddenFields — raw HTML for hidden inputs (csrf, thesis_id, etc.)
*
* old/value callables:
* callable $oldFn — fn(string $key, string $default=''): string (with escaping)
* callable $withAutofocusFn — fn(string $field, array $attrs=[]): array
*
* Data shared across fieldsets:
* array $formData — raw form repopulation data (not pre-escaped)
* array $orientations, $apPrograms, $finalityTypes, $languages, $formatTypes, $licenseTypes
*
* Jury data:
* ?string $juryPromoteur, $juryPromoteurUlb, $juryPresident
* array $lecteursInternes, $lecteursExternes
* bool $showPresident, $showPromoteurUlb, $promoteurUlbConditional
*
* Licence / access:
* bool $libreEnabled, $interneEnabled, $interditEnabled
* string $generalitiesHtml
* int $defaultAccessTypeId
*
* Optional flags (all default to false):
* bool $showIntroHelp — render partage intro help block
* bool $showFlash — render flash banners (error/warning/success)
* bool $showContact — Contact checkbox fieldset
* bool $showCoverPreview — cover image preview + remove checkbox
* bool $showExistingFiles — existing thesis files list (sortable, deletable)
* bool $showBannerPreview — banner image preview + remove checkbox
* bool $showContextNote — Note contextuelle fieldset
* bool $showBackoffice — Backoffice fieldset (jury_points, remarks, contact_interne, exemplaires)
* bool $showEmailConfirmation — E-mail de confirmation fieldset
* bool $showPublish — Publication checkbox fieldset
* string $helpFn — fn(string $key): string (for help blocks)
* string $helpContent — current help block content (for help-block renders)
* array $helpBlocks — all help blocks (for intro)
*
* Files mode variables:
* string $filesMode — 'add' | 'edit' (determines which file inputs to show)
* ?string $currentCover — existing cover file info for edit mode
* array $currentFiles — existing thesis files for edit mode
* ?string $currentBannerPath — existing banner path for edit mode
* ?string $currentContextNote — existing context note for edit mode
* array $currentRaw — raw thesis row for edit mode
* ?string $currentAuthorShowContact — author show_contact flag for edit mode
* ?string $currentAuthorEmail — author email for edit mode
*
* Website:
* string $existingWebsiteUrl
* string $existingWebsiteLabel
* array $checkedFormatsForSiteWeb
*/
// ── Defaults ──────────────────────────────────────────────────────────────────
$mode = $mode ?? 'add';
$formAction = $formAction ?? '';
$hiddenFields = $hiddenFields ?? '';
$formData = $formData ?? [];
$synopsisExtra = $synopsisExtra ?? "";
$juryPromoteur = $juryPromoteur ?? null;
$juryPromoteurUlb = $juryPromoteurUlb ?? null;
$lecteursInternes = $lecteursInternes ?? [];
$lecteursExternes = $lecteursExternes ?? [];
$juryPresident = $juryPresident ?? null;
$showPresident = $showPresident ?? false;
$showPromoteurUlb = $showPromoteurUlb ?? true;
$promoteurUlbConditional = $promoteurUlbConditional ?? false;
$libreEnabled = $libreEnabled ?? true;
$interneEnabled = $interneEnabled ?? true;
$interditEnabled = $interditEnabled ?? true;
$generalitiesHtml = $generalitiesHtml ?? "";
$defaultAccessTypeId = $defaultAccessTypeId ?? 2;
// Optional flags
$showIntroHelp = $showIntroHelp ?? false;
$showFlash = $showFlash ?? false;
$showContact = $showContact ?? false;
$showCoverPreview = $showCoverPreview ?? false;
$showExistingFiles = $showExistingFiles ?? false;
$showBannerPreview = $showBannerPreview ?? false;
$showContextNote = $showContextNote ?? false;
$showBackoffice = $showBackoffice ?? false;
$showEmailConfirmation = $showEmailConfirmation ?? false;
$showPublish = $showPublish ?? false;
$oldFn = $oldFn ?? (function_exists('old') ? 'old' : fn($k, $d = '') => $d);
$withAutofocusFn = $withAutofocusFn ?? fn($field, $attrs = []) => $attrs;
$filesMode = $filesMode ?? 'add';
$existingWebsiteUrl = $existingWebsiteUrl ?? '';
$existingWebsiteLabel = $existingWebsiteLabel ?? '';
$checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
?>
<?php if ($showIntroHelp && isset($helpFn)): ?>
<?php
$helpContent = $helpFn("partage_intro");
include APP_ROOT . "/templates/partials/form/form-help-block.php";
?>
<?php endif; ?>
<?php if ($showFlash): ?>
<?php
$flashError = $_SESSION["_flash_error"] ?? null;
$flashWarning = $_SESSION["_flash_warning"] ?? null;
$flashSuccess = $_SESSION["_flash_success"] ?? null;
unset(
$_SESSION["_flash_error"],
$_SESSION["_flash_warning"],
$_SESSION["_flash_success"],
);
?>
<?php if ($flashError): ?>
<div class="flash-error" role="alert"><?= htmlspecialchars(
$flashError,
) ?></div>
<?php endif; ?>
<?php if ($flashWarning): ?>
<div class="flash-warning" id="flash-warning" role="alert" tabindex="-1"><?= htmlspecialchars(
$flashWarning,
) ?></div>
<script>document.addEventListener('DOMContentLoaded',function(){var el=document.getElementById('flash-warning');if(el){el.scrollIntoView({behavior:'smooth',block:'center'});el.focus();}});</script>
<?php endif; ?>
<?php if ($flashSuccess): ?>
<div class="flash-success" role="alert"><?= htmlspecialchars(
$flashSuccess,
) ?></div>
<?php endif; ?>
<?php endif; ?>
<form action="<?= $formAction ?>" method="post" enctype="multipart/form-data" class="admin-form"<?= $mode === 'edit' ? ' data-beforeunload-guard' : '' ?>>
<?= $hiddenFields ?>
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_tfe_info");
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
include APP_ROOT . "/templates/partials/form/fieldset-tfe-info.php";
?>
<?php if ($showContact): ?>
<!-- ═══════════════════ Contact ═══════════════════ -->
<fieldset>
<legend>Contact</legend>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="contact_public" value="1"
<?= !empty($formData["contact_public"]) ||
($currentAuthorShowContact ?? false)
? "checked"
: "" ?>>
Rendre le contact visible publiquement sur la fiche du TFE
</label>
<small>L'adresse est toujours conservée en interne comme contact de référence.</small>
</div>
</fieldset>
<?php endif; ?>
<!-- ═══════════════════ Langue(s) ═══════════════════ -->
<fieldset>
<legend>Langue(s)</legend>
<?php
$name = "languages";
$label = "Langue(s) du TFE :";
$options = $languages;
$checked = $formData["languages"] ?? [];
$required = true;
include APP_ROOT . "/templates/partials/form/checkbox-list.php";
?>
<?php
$name = "language_autre";
$label = "Autre(s) langue(s) :";
$value = $oldFn("language_autre");
$hint =
"Si votre TFE contient une langue absente de la liste, précisez-la ici.";
include APP_ROOT . "/templates/partials/form/text-field.php";
?>
</fieldset>
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
<fieldset>
<legend>Mots-clés</legend>
<?php
$name = "tag";
$label = "Mots-clés (max 10) :";
$value = $oldFn("tag");
$placeholder = "sociologie, anthropologie, ...";
$hint = "Séparez par des virgules. Max 10 mots-clés.";
$attrs = $withAutofocusFn("tag");
include APP_ROOT . "/templates/partials/form/text-field.php";
?>
</fieldset>
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
<?php include APP_ROOT .
"/templates/partials/form/fieldset-academic.php"; ?>
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_jury");
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
require APP_ROOT . "/templates/partials/form/jury-fieldset.php";
?>
<!-- ═══════════════════ Format(s) ═══════════════════ -->
<fieldset>
<legend>Format(s)</legend>
<?php
$name = "formats";
$label = "Format(s) du TFE :";
$options = $formatTypes;
$checked = $formData["formats"] ?? [];
$required = true;
$hxPost = "/partage/format-website-fragment";
$hxTarget = "#website-url-fieldset";
include APP_ROOT . "/templates/partials/form/checkbox-list.php";
?>
</fieldset>
<!-- ═══════════════════ Fichiers ═══════════════════ -->
<?php if ($filesMode === "edit"): ?>
<fieldset>
<legend>Fichiers</legend>
<!-- Cover image -->
<div class="admin-form-group">
<label>Image de couverture :</label>
<div class="admin-file-input">
<?php if (!empty($currentCover)): ?>
<div class="admin-banner-preview">
<img src="/media.php?path=<?= urlencode(
$currentCover["file_path"],
) ?>"
alt="Couverture actuelle" style="max-height:180px;">
<label class="admin-checkbox-label">
<input type="checkbox" name="remove_cover" value="1"> Supprimer la couverture
</label>
</div>
<?php endif; ?>
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png" data-preview="fp-couverture">
<div id="fp-couverture" class="file-preview-list" aria-live="polite"></div>
<small><?= empty($currentCover)
? "JPG, PNG. Format 4:3 recommandé. Max 20 MB."
: "Laisser vide pour conserver la couverture actuelle. JPG, PNG. Max 20 MB." ?></small>
</div>
</div>
<!-- Existing thesis files — sortable, with labels -->
<?php $thesisFilesList = array_values(
array_filter(
$currentFiles,
fn($f) => $f["file_type"] !== "cover",
),
); ?>
<?php if (!empty($thesisFilesList)): ?>
<div class="admin-form-group">
<label>Fichiers du TFE existants :</label>
<small style="display:block;margin-bottom:var(--space-2xs);color:var(--text-tertiary)">
Glissez-déposez les lignes pour réordonner les fichiers sur la page publique.
</small>
<ul id="existing-files-sortable" class="admin-file-list sortable-list">
<?php foreach ($thesisFilesList as $f):
$fExt = strtolower(
pathinfo($f["file_path"] ?? "", PATHINFO_EXTENSION),
);
$fType = $f["file_type"] ?? "other";
$fIcon = match (true) {
$fType === "main" || $fExt === "pdf" => "📄",
in_array($fExt, [
"jpg",
"jpeg",
"png",
"gif",
"webp",
])
=> "🖼️",
$fType === "video" ||
in_array($fExt, ["mp4", "webm", "mov", "ogv"])
=> "🎬",
$fType === "audio" ||
in_array($fExt, [
"mp3",
"ogg",
"wav",
"flac",
"aac",
"m4a",
])
=> "🔊",
$fType === "caption" || $fExt === "vtt" => "💬",
$fType === "website" => "🌐",
default => "📎",
};
$isExternalUrl =
str_starts_with($f["file_path"] ?? "", "http://") ||
str_starts_with($f["file_path"] ?? "", "https://");
$fLinkHref = $isExternalUrl
? htmlspecialchars($f["file_path"])
: "/media.php?path=" . urlencode($f["file_path"]);
?>
<li class="admin-file-list-item" data-file-id="<?= (int) $f[
"id"
] ?>">
<input type="hidden" name="file_sort_order[]" value="<?= (int) $f[
"id"
] ?>">
<span class="admin-file-drag-handle" title="Réordonner">⠿</span>
<span class="admin-file-icon-col"><?= $fIcon ?></span>
<span class="admin-file-info">
<a href="<?= $fLinkHref ?>" target="_blank" rel="noopener" class="admin-file-name">
<?= htmlspecialchars(
$f["file_name"] ??
basename($f["file_path"]),
) ?>
</a>
<span class="admin-file-meta-row">
<span class="admin-file-type-badge"><?= htmlspecialchars(
$fType,
) ?></span>
<?php if (
!empty($f["file_size"]) &&
$f["file_size"] > 0
): ?>
<span class="admin-file-size"><?= number_format(
$f["file_size"] / 1024 / 1024,
2,
) ?> MB</span>
<?php endif; ?>
</span>
<input type="text" name="file_label[<?= (int) $f[
"id"
] ?>]"
value="<?= htmlspecialchars(
$f["display_label"] ?? "",
) ?>"
placeholder="Légende / description (optionnel)"
class="admin-file-label-input">
</span>
<label class="admin-checkbox-label admin-file-delete">
<input type="checkbox" name="delete_files[]" value="<?= (int) $f[
"id"
] ?>"> Supprimer
</label>
</li>
<?php
endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- New thesis files -->
<div class="admin-form-group admin-files-fieldgroup">
<label>Ajouter des fichiers du TFE :</label>
<div class="admin-file-input">
<input type="file" id="tfe-files-input"
name="files[]" multiple
accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.mp4,.webm,.mov,.ogv,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a,.zip,.tar,.gz,.vtt"
class="tfe-file-picker">
<small class="admin-file-hint">
Types acceptés : PDF · JPG/PNG/GIF/WEBP · MP4/WebM/MOV (vidéo) · MP3/OGG/WAV/FLAC (audio) · ZIP/TAR (archives) · autres fichiers (téléchargement uniquement). Max 500 MB par fichier.
</small>
<ul id="tfe-file-queue" class="tfe-file-queue sortable-list" aria-label="Nouveaux fichiers (réordonnable)"></ul>
<p id="tfe-file-queue-empty" class="tfe-queue-empty">Aucun nouveau fichier sélectionné.</p>
</div>
</div>
<!-- Banner image -->
<div class="admin-form-group">
<label>Image bannière (accueil) :</label>
<div class="admin-file-input">
<?php if (!empty($currentBannerPath)): ?>
<div class="admin-banner-preview">
<img src="/media.php?path=<?= urlencode(
$currentBannerPath,
) ?>" alt="Bannière actuelle">
<label class="admin-checkbox-label">
<input type="checkbox" name="remove_banner" value="1"> Supprimer la bannière
</label>
</div>
<?php endif; ?>
<input type="file" name="banner" id="banner" accept="image/jpeg,image/png,image/webp" data-preview="fp-banner">
<div id="fp-banner" class="file-preview-list" aria-live="polite"></div>
<small><?= empty($currentBannerPath)
? "JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB."
: "Laisser vide pour conserver la bannière actuelle. JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 20 MB." ?></small>
</div>
</div>
</fieldset>
<?php else: ?>
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_files");
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
include APP_ROOT . "/templates/partials/form/fieldset-files.php";
?>
<?php endif; ?>
<!-- Website URL fieldset — shown/hidden via HTMX when "Site web" checked -->
<fieldset id="website-url-fieldset" style="display:none">
<legend>Site web</legend>
<div class="admin-form-group">
<label for="website_url">URL du site :</label>
<div class="admin-file-input">
<input type="url"
id="website_url"
name="website_url"
value="<?= htmlspecialchars($existingWebsiteUrl) ?>"
placeholder="https://mon-tfe.erg.be">
<small>Si le TFE est un site web, entrez son URL ici. Il sera affiché comme un site embarqué sur la page du TFE.</small>
</div>
</div>
<div class="admin-form-group">
<label for="website_label">Légende :</label>
<input type="text"
id="website_label"
name="website_label"
value="<?= htmlspecialchars($existingWebsiteLabel) ?>"
placeholder="Description du site (optionnel)"
class="admin-file-label-input"
style="max-width:400px;">
</div>
</fieldset>
<?php
// Server-side: show if Site web already checked (e.g. on error redirect)
$_stmt = Database::getInstance()
->getConnection()
->prepare("SELECT id FROM format_types WHERE name = ? LIMIT 1");
$_stmt->execute(["Site web"]);
$_siteWebId = $_stmt->fetchColumn();
if (
$_siteWebId &&
in_array(
(string) $_siteWebId,
array_map("strval", $checkedFormatsForSiteWeb),
true,
)
) {
echo '<script>document.getElementById("website-url-fieldset").style.display=""</script>';
}
?>
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
<?php include APP_ROOT .
"/templates/partials/form/fieldset-metadata.php"; ?>
<!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ -->
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_access");
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
include APP_ROOT .
"/templates/partials/form/fieldset-licence-explanation.php";
?>
<?php if ($showContextNote): ?>
<!-- ═══════════════════ Note contextuelle ═══════════════════ -->
<fieldset>
<legend>Note contextuelle</legend>
<div>
<label for="context_note">Note contextuelle :</label>
<div>
<textarea id="context_note" name="context_note"
rows="4" maxlength="1500"><?= htmlspecialchars(
$currentContextNote ??
($formData["context_note"] ?? ""),
) ?></textarea>
<small>Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères.</small>
</div>
</div>
</fieldset>
<?php endif; ?>
<?php if ($showBackoffice): ?>
<!-- ═══════════════════ Backoffice ═══════════════════ -->
<fieldset>
<legend>Backoffice</legend>
<div class="admin-form-group">
<label for="jury_points">Points :</label>
<input type="number" id="jury_points" name="jury_points"
value="<?= htmlspecialchars(
$currentRaw["jury_points"] ??
($formData["jury_points"] ?? ""),
) ?>"
step="0.01" min="0" max="20" placeholder="sur 20">
<small>Note du jury (interne, non visible publiquement).</small>
</div>
<div class="admin-form-group">
<label for="remarks">Remarques :</label>
<textarea id="remarks" name="remarks" rows="4"><?= htmlspecialchars(
$currentRaw["remarks"] ?? ($formData["remarks"] ?? ""),
) ?></textarea>
<small>Notes internes (non visibles publiquement).</small>
</div>
<div class="admin-form-group">
<label for="contact_interne">Contact interne :</label>
<input type="email" id="contact_interne" name="contact_interne"
value="<?= htmlspecialchars(
$currentRaw["contact_interne"] ??
($formData["contact_interne"] ??
($currentAuthorEmail ?? "")),
) ?>"
placeholder="ton.email@exemple.be">
<small>Adresse de contact interne (non visible publiquement).</small>
</div>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="exemplaire_baiu" value="1"
<?= !empty(
$currentRaw["exemplaire_baiu"] ??
($formData["exemplaire_baiu"] ?? false)
)
? "checked"
: "" ?>>
Exemplaire physique BAIU
</label>
<small>Case logistique : cocher si un exemplaire physique est disponible à la BAIU.</small>
</div>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="exemplaire_erg" value="1"
<?= !empty(
$currentRaw["exemplaire_erg"] ??
($formData["exemplaire_erg"] ?? false)
)
? "checked"
: "" ?>>
Exemplaire physique ERG
</label>
<small>Case logistique : cocher si un exemplaire physique est disponible à l'ERG.</small>
</div>
</fieldset>
<?php endif; ?>
<?php if ($showEmailConfirmation): ?>
<!-- ═══════════════════ E-mail de confirmation ═══════════ -->
<fieldset>
<legend>E-mail de confirmation</legend>
<?php if ($mode === "partage" && isset($helpFn)): ?>
<?php
$helpContent = $helpFn("fieldset_email");
include APP_ROOT . "/templates/partials/form/form-help-block.php";
?>
<?php endif; ?>
<?php
$name = "confirmation_email";
$label = "Adresse e-mail :";
$value = $oldFn("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>
<?php endif; ?>
<?php if ($showPublish): ?>
<!-- ═══════════════════ Publication ════════════════════════ -->
<fieldset>
<legend>Publication</legend>
<div>
<label class="admin-checkbox-label">
<input type="checkbox" name="is_published" value="1"
<?= !empty($currentRaw["is_published"] ?? false)
? "checked"
: "" ?>>
Publier ce TFE sur le site public
</label>
</div>
</fieldset>
<?php endif; ?>
<div class="form-footer admin-form-footer">
<button type="submit" name="go" class="btn btn--primary"><?= $mode === 'edit' ? 'Enregistrer' : 'Soumettre' ?></button>
<?php if ($mode === 'add' || $mode === 'edit'): ?>
<a href="/admin/" class="btn btn--secondary">Annuler</a>
<?php endif; ?>
</div>
</form>