Files
xamxam/app/templates/partials/form/form.php
Pontoporeia 8f4f9d00b4 Refactor: Form improvements and cleanup: note contextuel, annexes, fichiers
1. fix: form improvements — multiple promoteurices, asterisks, contact dedup, bentopdf

- Multiple promoteurice (interne + ULB): both fieldsets now support dynamic
  add/remove rows (same pattern as lecteurs). field names changed to arrays
  (jury_promoteur[], jury_promoteur_ulb_name[]). Controllers accept both
  scalar and array forms for backwards compat.
- ULB promoteurice: when finality=Approfondi, asterisk appears on legend
  and first ULB input is marked required (JS toggle). Non-Approfondi hides
  the fieldset and clears values.
- Contact visibility duplication: removed redundant contact_public checkbox
  from admin add/edit forms (showContact=false). The 'mail' field in
  fieldset-tfe-info already serves this purpose.
- Asterisk fixes: website URL field now has asterisk+required when Site web
  format selected. Video/audio already had correct required handling.
- bentopdf link: clearer full URL 'https://bentopdf.com/' in both
  fichiers-fragment.php and form.php (edit mode)

2. refactor: merge Note contextuelle into Backoffice, add Lien BAIU, reorder fields

Backoffice fieldset now contains in order:
1. Note contextuelle (was standalone fieldset)
2. Points du jury
3. Remarques
4. Lien BAIU (moved from Métadonnées complémentaires)
5. Exemplaire physique BAIU
6. Exemplaire physique ERG
7. Contact interne


Métadonnées complémentaires now only has: pages, minutes, annexes checkbox.
Removed dead showContextNote variable from form.php, add.php, edit.php.
Controller baiu_link still mapped to input name "lien" (no migration needed).

3. refactor: move annexes checkbox from Métadonnées into Fichiers fieldset

- Removed 'Ce TFE comporte des annexes' checkbox from
  fieldset-metadata.php.
- Added annexes checkbox + conditional file input to
  fichiers-fragment.php. When checked, an HTMX swap reveals
  the 'annexes' file input (multiple, PDF or ZIP/TAR, max 500 MB).
- form.php seeds ['has_annexes'] for initial fragment render.
- Métadonnées complémentaires now only contains pages + minutes.
2026-05-13 17:59:13 +02:00

553 lines
26 KiB
PHP

<?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 $juryPromoteurs, $juryPromoteursUlb
* 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 $showBackoffice — Backoffice fieldset (context_note, jury_points, remarks, baiu_link, exemplaires, contact_interne)
* 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 $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';
// In admin add/edit, no field is required (admins can save partial records)
$adminMode = ($mode === 'add' || $mode === 'edit');
$formAction = $formAction ?? '';
$hiddenFields = $hiddenFields ?? '';
$formData = $formData ?? [];
$synopsisExtra = $synopsisExtra ?? "";
$juryPromoteur = $juryPromoteur ?? null;
$juryPromoteurs = $juryPromoteurs ?? [];
$juryPromoteurUlb = $juryPromoteurUlb ?? null;
$juryPromoteursUlb = $juryPromoteursUlb ?? [];
$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 = false; // Banners merged into covers — field removed
$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" data-beforeunload-guard>
<?= $hiddenFields ?>
<?php if (!$adminMode): ?>
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
<?php endif; ?>
<!-- ═══════════════════ 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 id="languages-fieldset">
<legend>Langue(s)</legend>
<?php
$name = "languages";
$label = "Langue(s) du TFE :";
$options = $languages;
$checked = $formData["languages"] ?? [];
$required = !$adminMode;
$hxPost = $mode === 'partage' ? "/partage/language-autre-fragment" : "/admin/language-autre-fragment.php";
$hxTarget = "#language-autre-row";
$hxSwap = "outerHTML";
include APP_ROOT . "/templates/partials/form/checkbox-list.php";
unset($hxSwap);
?>
<?php
$_langAutreRequired = empty($formData["languages"]);
$_langAutreValue = $oldFn("language_autre");
?>
<div id="language-autre-row">
<div>
<label for="language_autre">Autre(s) langue(s) :<?= $_langAutreRequired ? ' <span class="asterisk">*</span>' : '' ?></label>
<div>
<input type="text"
id="language_autre"
name="language_autre"
value="<?= $_langAutreValue ?>"
<?= (!$adminMode && $_langAutreRequired) ? 'required' : '' ?>>
<small>Si votre TFE contient une langue absente de la liste, précisez-la ici.</small>
</div>
</div>
</div>
<?php unset($_langAutreRequired, $_langAutreValue); ?>
</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) + Fichiers ═══════════════════ -->
<?php if ($filesMode === 'add'): ?>
<?php
// Add / partage mode: Format + Fichiers rendered as one HTMX-swappable block.
// Synthesise POST-like data so fichiers-fragment.php can render the initial state.
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_files");
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
// Temporarily populate $_POST so the fragment can read formats/website/annexes values.
$_savedPost = $_POST;
$_POST['formats'] = $checkedFormatsForSiteWeb;
$_POST['website_url'] = $existingWebsiteUrl;
$_POST['website_label'] = $existingWebsiteLabel;
$_POST['admin_mode'] = $adminMode ? '1' : '0';
$_POST['has_annexes'] = $formData['has_annexes'] ?? null;
include APP_ROOT . '/public/partage/fichiers-fragment.php';
$_POST = $_savedPost;
unset($_savedPost);
?>
<?php else: ?>
<!-- Edit mode: reuse the same Format + Fichiers HTMX fragment as add/partage -->
<?php
// Synthesise POST-like data so fichiers-fragment.php renders the initial state.
$_savedPost = $_POST;
$_POST['formats'] = $checkedFormatsForSiteWeb;
$_POST['website_url'] = $existingWebsiteUrl;
$_POST['website_label'] = $existingWebsiteLabel;
$_POST['admin_mode'] = $adminMode ? '1' : '0';
$_POST['has_annexes'] = $formData['has_annexes'] ?? null;
include APP_ROOT . '/public/partage/fichiers-fragment.php';
$_POST = $_savedPost;
unset($_savedPost);
?>
<!-- Edit-only: existing files management -->
<div id="edit-existing-files-block">
<!-- 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?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,image/webp" data-preview="fp-couverture">
<div id="fp-couverture" class="file-preview-list" aria-live="polite"></div>
<small><?= empty($currentCover)
? "JPG, PNG ou WEBP. Format 4:3 recommandé. Max 20 MB."
: "Laisser vide pour conserver la couverture actuelle. JPG, PNG ou WEBP. 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?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; ?>
</div><!-- #edit-existing-files-block -->
<?php endif; ?>
<!-- ═══════════════════ 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 ($showBackoffice): ?>
<!-- ═══════════════════ Backoffice ═══════════════════ -->
<fieldset>
<legend>Backoffice</legend>
<!-- 1. Note contextuelle -->
<div class="admin-form-group">
<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>
<!-- 2. Points du jury -->
<div class="admin-form-group">
<label for="jury_points">Points du jury :</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>
<!-- 3. Remarques -->
<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>
<!-- 4. Lien BAIU -->
<?php
$name = 'lien'; $label = 'Lien BAIU :'; $value = $oldFn('lien');
$type = 'url'; $placeholder = 'https://...'; $hint = '';
include APP_ROOT . '/templates/partials/form/text-field.php';
?>
<!-- 5. Exemplaire BAIU -->
<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>
<!-- 6. Exemplaire ERG -->
<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>
<!-- 7. Contact interne -->
<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>
</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 = !$adminMode;
$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>