mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 07:11:18 +02:00
refactor: combined duration (pages + minutes), has_annexes checkbox, remove Mo
- Remove Mo option from duration, keep only pages and minutes - Redesign duration fieldset: separate Pages input + Durée h:m inputs, both can be set together - Fix minutes input visibility: wider inputs (6ch), proper CSS layout - Add has_annexes checkbox to fichiers fragment + DB column + controllers - Display duration on admin backoffice recap page - Display duration on public partage recap page - Update public TFE page for new combined duration format - Migration 041: add duration_pages, duration_minutes, has_annexes columns; migrate data; recreate views
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,3 +1,6 @@
|
||||
- [x] Fix markdown cheatsheet modal 500 error — missing `require_once bootstrap.php` in `app/public/admin/markdown-cheatsheet-fragment.php`
|
||||
- [x] Add `padding-bottom` to `.md-cheatsheet-dialog` so last table row isn't flush against dialog edge
|
||||
- [x] Add independent scrollbar for TOC in charte/licence/apropos + admin pages (max-height + overflow-y: auto on .toc desktop)
|
||||
- [x] Remove "Mo" option from duration — keep only minutes and pages
|
||||
- [x] Combine pages and minutes as separate fields (both can be set simultaneously)
|
||||
- [x] Fix minutes input visibility (can't see what's typed)
|
||||
- [x] Add `has_annexes` checkbox to form + DB column
|
||||
- [x] Display duration on admin backoffice recap page
|
||||
- [x] Update public TFE page duration display for new combined format
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
-- 041_combined_duration.sql
|
||||
-- Replace single duration_value/duration_unit with separate pages + minutes fields.
|
||||
-- Allows a TFE to have both a page count and a time duration simultaneously.
|
||||
|
||||
-- Add new columns to theses table
|
||||
ALTER TABLE theses ADD COLUMN duration_pages INTEGER;
|
||||
ALTER TABLE theses ADD COLUMN duration_minutes INTEGER;
|
||||
|
||||
-- Add has_annexes flag for optional annexes checkbox
|
||||
ALTER TABLE theses ADD COLUMN has_annexes INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Migrate existing data:
|
||||
-- duration_unit='pages' → duration_pages = duration_value
|
||||
-- duration_unit='durée' → duration_minutes = ROUND(duration_value * 60)
|
||||
UPDATE theses SET duration_pages = CAST(duration_value AS INTEGER) WHERE duration_unit = 'pages' AND duration_value IS NOT NULL;
|
||||
UPDATE theses SET duration_minutes = CAST(ROUND(duration_value * 60) AS INTEGER) WHERE duration_unit = 'durée' AND duration_value IS NOT NULL;
|
||||
|
||||
-- Drop and recreate views to include the new columns
|
||||
DROP VIEW IF EXISTS v_theses_full;
|
||||
DROP VIEW IF EXISTS v_theses_public;
|
||||
|
||||
CREATE VIEW IF NOT EXISTS v_theses_full AS
|
||||
SELECT
|
||||
t.id,
|
||||
t.identifier,
|
||||
t.title,
|
||||
t.subtitle,
|
||||
t.year,
|
||||
t.is_doctoral,
|
||||
t.objet,
|
||||
o.name as orientation,
|
||||
ap.name as ap_program,
|
||||
ft.name as finality_type,
|
||||
t.synopsis,
|
||||
t.context_note,
|
||||
t.duration_pages,
|
||||
t.duration_minutes,
|
||||
t.has_annexes,
|
||||
at.name as access_type,
|
||||
lt.name as license_type,
|
||||
t.license_id,
|
||||
t.license_custom,
|
||||
t.access_type_id,
|
||||
t.jury_points,
|
||||
t.submitted_at,
|
||||
t.defense_date,
|
||||
t.published_at,
|
||||
t.is_published,
|
||||
t.baiu_link,
|
||||
t.exemplaire_baiu,
|
||||
t.exemplaire_erg,
|
||||
t.cc2r,
|
||||
t.remarks,
|
||||
t.jury_note_added,
|
||||
t.contact_visible,
|
||||
GROUP_CONCAT(DISTINCT a.name ORDER BY a.name ASC) as authors,
|
||||
GROUP_CONCAT(DISTINCT s.name) as supervisors,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'president' THEN s.name END) as jury_president,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 0 THEN s.name END) as jury_promoteurs,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 1 THEN s.name END) as jury_promoteurs_ulb,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 0 THEN s.name END) as jury_lecteurs_internes,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 1 THEN s.name END) as jury_lecteurs_externes,
|
||||
GROUP_CONCAT(DISTINCT l.name) as languages,
|
||||
GROUP_CONCAT(DISTINCT fmt.name) as formats,
|
||||
GROUP_CONCAT(DISTINCT tg.name) as keywords,
|
||||
(SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_email,
|
||||
(SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as author_show_contact
|
||||
FROM theses t
|
||||
LEFT JOIN orientations o ON t.orientation_id = o.id
|
||||
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
||||
LEFT JOIN finality_types ft ON t.finality_id = ft.id
|
||||
LEFT JOIN access_types at ON t.access_type_id = at.id
|
||||
LEFT JOIN license_types lt ON t.license_id = lt.id
|
||||
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
|
||||
LEFT JOIN authors a ON ta.author_id = a.id
|
||||
LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id
|
||||
LEFT JOIN supervisors s ON ts.supervisor_id = s.id
|
||||
LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id
|
||||
LEFT JOIN languages l ON tl.language_id = l.id
|
||||
LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id
|
||||
LEFT JOIN format_types fmt ON tf.format_id = fmt.id
|
||||
LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id
|
||||
LEFT JOIN tags tg ON tt.tag_id = tg.id
|
||||
GROUP BY t.id;
|
||||
|
||||
CREATE VIEW IF NOT EXISTS v_theses_public AS
|
||||
SELECT * FROM v_theses_full
|
||||
WHERE is_published = 1;
|
||||
@@ -45,6 +45,9 @@ $formData = array_merge($formData, [
|
||||
'lien' => $thesis['baiu_link'] ?? '',
|
||||
'contact_public' => $contactPublic ?? false,
|
||||
'contact_interne' => $contactInterne ?? '',
|
||||
'duration_pages' => $currentRaw['duration_pages'] ?? null,
|
||||
'duration_minutes' => $currentRaw['duration_minutes'] ?? null,
|
||||
'has_annexes' => $currentRaw['has_annexes'] ?? false,
|
||||
]);
|
||||
|
||||
// Build jury arrays
|
||||
@@ -144,8 +147,7 @@ extract(FormBootstrap::adminFormVariables(
|
||||
'currentFiles' => $currentFiles ?? [],
|
||||
'currentContextNote' => $currentContextNote ?? null,
|
||||
'currentContactVisible' => $currentContactVisible ?? null,
|
||||
'currentDurationValue' => $currentDurationValue ?? null,
|
||||
'currentDurationUnit' => $currentDurationUnit ?? 'pages',
|
||||
|
||||
'contactInterne' => $contactInterne ?? null,
|
||||
'contactPublic' => $contactPublic ?? false,
|
||||
'showCoverPreview' => true,
|
||||
@@ -161,8 +163,9 @@ $formData['license_custom'] = $currentRaw['license_custom'] ?? '';
|
||||
$formData['cc2r'] = $currentRaw['cc2r'] ?? false;
|
||||
|
||||
// Duration variables for the form template
|
||||
$durationValue = $currentDurationValue ?? null;
|
||||
$durationUnit = $currentDurationUnit ?? 'pages';
|
||||
$durationPages = $currentRaw['duration_pages'] ?? null;
|
||||
$durationMins = $currentRaw['duration_minutes'] ?? null;
|
||||
$hasAnnexes = $currentRaw['has_annexes'] ?? false;
|
||||
|
||||
// Asset arrays and page chrome
|
||||
$isAdmin = true;
|
||||
|
||||
@@ -61,6 +61,10 @@ if ($thesisId) {
|
||||
$existingFilesJsonForTfe = $buildQueueFilesJson($currentFiles, 'tfe');
|
||||
$existingFilesJsonForAnnexe = $buildQueueFilesJson($currentFiles, 'annexe');
|
||||
|
||||
// Load has_annexes flag for the checkbox
|
||||
$rawRow = $db->getThesisRawFields($thesisId);
|
||||
$_POST['has_annexes'] = !empty($rawRow['has_annexes']) ? '1' : null;
|
||||
|
||||
$_POST['edit_mode'] = '1';
|
||||
}
|
||||
|
||||
|
||||
@@ -2160,18 +2160,36 @@ th.admin-ap-col {
|
||||
100% { transform: scaleX(0); transform-origin: right; }
|
||||
}
|
||||
|
||||
/* ── Sidebar TOC (matches public .page-content alignment pattern) ────────── */
|
||||
/* ── Sidebar TOC (matches public .page-content grid layout) ────────────── */
|
||||
|
||||
.admin-main--toc {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
gap: var(--space-2xl);
|
||||
align-items: start;
|
||||
padding: var(--space-xl) var(--space-m) var(--space-2xl);
|
||||
}
|
||||
|
||||
.admin-main--toc > article {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* Desktop: article in second column */
|
||||
@media (min-width: 768px) {
|
||||
.admin-main--toc > article {
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: single column */
|
||||
@media (max-width: 767px) {
|
||||
.admin-main--toc {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-l);
|
||||
padding: var(--space-m) var(--space-s) var(--space-xl);
|
||||
}
|
||||
|
||||
.admin-main--toc > article {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Align first child of article with TOC heading (same as public) */
|
||||
@@ -2188,11 +2206,6 @@ th.admin-ap-col {
|
||||
}
|
||||
|
||||
/* Admin TOC: same <details class="toc"> as public pages, positioned sticky */
|
||||
#admin-toc {
|
||||
width: 180px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#admin-toc .toc-list a {
|
||||
font-size: var(--step--1);
|
||||
}
|
||||
|
||||
@@ -352,7 +352,11 @@
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.duration-time-inputs {
|
||||
.duration-field {
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.duration-time-field {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--space-xs);
|
||||
@@ -364,6 +368,11 @@
|
||||
gap: var(--space-3xs);
|
||||
}
|
||||
|
||||
.duration-time-fields input[type="number"] {
|
||||
width: 6ch;
|
||||
min-width: 6ch;
|
||||
}
|
||||
|
||||
.duration-time-fields span {
|
||||
font-size: var(--step--1);
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -45,21 +45,63 @@
|
||||
items.push({ section: sec, link: a });
|
||||
});
|
||||
|
||||
// Generate a spread of thresholds for smooth IntersectionObserver firing.
|
||||
function buildThresholds() {
|
||||
var t = [];
|
||||
for (var i = 0; i <= 20; i++) t.push(i / 20);
|
||||
return t;
|
||||
}
|
||||
|
||||
var observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
var best = null,
|
||||
bestRatio = 0;
|
||||
entries.forEach((e) => {
|
||||
if (e.intersectionRatio > bestRatio) {
|
||||
bestRatio = e.intersectionRatio;
|
||||
best = e.target;
|
||||
() => {
|
||||
// Active section = whose top is closest to the midpoint of the
|
||||
// viewport from above. At page bottom, the last section wins
|
||||
// even if its heading can't reach 50%.
|
||||
var line = window.innerHeight * 0.5;
|
||||
var best = null;
|
||||
var bestDist = Infinity;
|
||||
|
||||
// Pass 1: sections whose top is above/at the line
|
||||
items.forEach((item) => {
|
||||
var top = item.section.getBoundingClientRect().top;
|
||||
if (top <= line && (line - top) < bestDist) {
|
||||
bestDist = line - top;
|
||||
best = item;
|
||||
}
|
||||
});
|
||||
|
||||
// Pass 2: no section above the line — pick closest from below
|
||||
if (!best) {
|
||||
items.forEach((item) => {
|
||||
var top = item.section.getBoundingClientRect().top;
|
||||
var dist = top - line;
|
||||
if (dist >= 0 && dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = item;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Pass 3: at page bottom, if the last section is visible but
|
||||
// its heading never crossed 50%, promote it anyway.
|
||||
if (best) {
|
||||
var lastItem = items[items.length - 1];
|
||||
if (lastItem !== best) {
|
||||
var lastTop = lastItem.section.getBoundingClientRect().top;
|
||||
var atBottom = (window.innerHeight + window.scrollY + 5)
|
||||
>= document.documentElement.scrollHeight;
|
||||
if (atBottom && lastTop > line && lastTop < window.innerHeight) {
|
||||
best = lastItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!best && items.length > 0) best = items[0];
|
||||
items.forEach((item) => {
|
||||
item.link.classList.toggle('toc-active', item.section === best);
|
||||
item.link.classList.toggle('toc-active', item === best);
|
||||
});
|
||||
},
|
||||
{ rootMargin: '-10% 0px -70% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] }
|
||||
{ threshold: buildThresholds() }
|
||||
);
|
||||
|
||||
items.forEach((item) => {
|
||||
|
||||
@@ -1,51 +1,26 @@
|
||||
/**
|
||||
* form-duration-toggle.js — Duration unit toggle on TFE form.
|
||||
* form-duration-toggle.js — Combined duration helper for TFE form.
|
||||
*
|
||||
* Switches between integer input (pages/mo) and h/m/s time inputs (durée).
|
||||
* Updates hidden #duration_value on change.
|
||||
* Computes duration_minutes from h + m fields on form submit so the server
|
||||
* receives a single integer (total minutes).
|
||||
*/
|
||||
(() => {
|
||||
var unit = document.getElementById('duration_unit');
|
||||
var hidden = document.getElementById('duration_value');
|
||||
var intWrap = document.getElementById('duration-value-integer');
|
||||
var intInput = document.getElementById('duration_value_int');
|
||||
var intLabel = document.getElementById('duration-value-label');
|
||||
var timeWrap = document.getElementById('duration-value-time');
|
||||
var hInput = document.getElementById('duration_h');
|
||||
var mInput = document.getElementById('duration_m');
|
||||
var sInput = document.getElementById('duration_s');
|
||||
var form = document.querySelector('form.admin-form');
|
||||
if (!form) return;
|
||||
|
||||
var LABELS = { pages: 'Nombre :', mo: 'Taille :', durée: 'Durée :' };
|
||||
if (!unit || !hidden) return;
|
||||
form.addEventListener('submit', function() {
|
||||
var h = parseInt(document.getElementById('duration_h')?.value, 10) || 0;
|
||||
var m = parseInt(document.getElementById('duration_m')?.value, 10) || 0;
|
||||
var total = h * 60 + m;
|
||||
|
||||
function updateHidden() {
|
||||
if (unit.value === 'durée') {
|
||||
var h = parseInt(hInput.value, 10) || 0;
|
||||
var m = parseInt(mInput.value, 10) || 0;
|
||||
var s = parseInt(sInput.value, 10) || 0;
|
||||
var total = h + m / 60 + s / 3600;
|
||||
hidden.value = total > 0 ? total.toFixed(6) : '';
|
||||
} else {
|
||||
hidden.value = intInput.value;
|
||||
var hidden = document.getElementById('duration_minutes_hidden');
|
||||
if (!hidden) {
|
||||
hidden = document.createElement('input');
|
||||
hidden.type = 'hidden';
|
||||
hidden.name = 'duration_minutes';
|
||||
hidden.id = 'duration_minutes_hidden';
|
||||
form.appendChild(hidden);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFields() {
|
||||
if (unit.value === 'durée') {
|
||||
intWrap.style.display = 'none';
|
||||
timeWrap.style.display = '';
|
||||
} else {
|
||||
timeWrap.style.display = 'none';
|
||||
intWrap.style.display = '';
|
||||
if (intLabel) intLabel.textContent = LABELS[unit.value] || 'Valeur :';
|
||||
}
|
||||
updateHidden();
|
||||
}
|
||||
|
||||
unit.addEventListener('change', toggleFields);
|
||||
if (intInput) intInput.addEventListener('input', updateHidden);
|
||||
if (hInput) hInput.addEventListener('input', updateHidden);
|
||||
if (mInput) mInput.addEventListener('input', updateHidden);
|
||||
if (sInput) sInput.addEventListener('input', updateHidden);
|
||||
toggleFields();
|
||||
hidden.value = total > 0 ? String(total) : '';
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -137,6 +137,22 @@ $pageTitle = 'Merci — TFE enregistré';
|
||||
<dd><?= htmlspecialchars($thesis['keywords']) ?></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$_hasPages2 = isset($thesis['duration_pages']) && $thesis['duration_pages'] !== null;
|
||||
$_hasMins2 = isset($thesis['duration_minutes']) && $thesis['duration_minutes'] !== null;
|
||||
if ($_hasPages2 || $_hasMins2):
|
||||
$_parts2 = [];
|
||||
if ($_hasPages2) $_parts2[] = $thesis['duration_pages'] . ' pages';
|
||||
if ($_hasMins2) {
|
||||
$_h2 = (int)floor($thesis['duration_minutes'] / 60);
|
||||
$_m2 = $thesis['duration_minutes'] % 60;
|
||||
$_parts2[] = $_h2 . 'h' . str_pad((string)$_m2, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
?>
|
||||
<dt>Durée</dt>
|
||||
<dd><?= htmlspecialchars(implode(', ', $_parts2)) ?></dd>
|
||||
<?php unset($_hasPages2, $_hasMins2, $_parts2, $_h2, $_m2); endif; ?>
|
||||
|
||||
<?php if (!empty($thesis['jury_promoteurs'])): ?>
|
||||
<dt>Promoteur·ice(s) interne</dt>
|
||||
<dd><?= htmlspecialchars($thesis['jury_promoteurs']) ?></dd>
|
||||
|
||||
@@ -162,8 +162,9 @@ class ThesisCreateController
|
||||
'exemplaire_baiu' => $data['exemplaireBaiu'],
|
||||
'exemplaire_erg' => $data['exemplaireErg'],
|
||||
'cc2r' => $data['cc2r'],
|
||||
'duration_value' => $data['durationValue'],
|
||||
'duration_unit' => $data['durationUnit'],
|
||||
'duration_pages' => $data['durationPages'],
|
||||
'duration_minutes' => $data['durationMins'],
|
||||
'has_annexes' => $data['hasAnnexes'] ? 1 : 0,
|
||||
]);
|
||||
|
||||
$identifier = $this->db->getThesisIdentifier($thesisId);
|
||||
@@ -555,20 +556,22 @@ class ThesisCreateController
|
||||
$exemplaireErg = !empty($post['exemplaire_erg']);
|
||||
$cc2r = !empty($post['cc2r']);
|
||||
|
||||
// Duration: numeric value + unit (optional, admin-validated)
|
||||
$validDurationUnits = ['pages', 'mo', 'durée'];
|
||||
$durationValue = $post['duration_value'] ?? null;
|
||||
$durationUnit = $post['duration_unit'] ?? 'pages';
|
||||
if ($durationValue !== null && $durationValue !== '') {
|
||||
$durationValue = filter_var($durationValue, FILTER_VALIDATE_FLOAT);
|
||||
if ($durationValue === false || $durationValue <= 0) {
|
||||
$durationValue = null; // ignore invalid
|
||||
// Duration: separate pages (int) and time (h/m → total minutes)
|
||||
$durationPages = null;
|
||||
$durationPagesRaw = $post['duration_pages'] ?? null;
|
||||
if ($durationPagesRaw !== null && $durationPagesRaw !== '') {
|
||||
$durationPages = filter_var($durationPagesRaw, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]);
|
||||
if ($durationPages === false || $durationPages <= 0) {
|
||||
$durationPages = null;
|
||||
}
|
||||
} else {
|
||||
$durationValue = null;
|
||||
}
|
||||
if (!in_array($durationUnit, $validDurationUnits, true)) {
|
||||
$durationUnit = 'pages';
|
||||
$durationMins = null;
|
||||
$durationMinsRaw = $post['duration_minutes'] ?? null;
|
||||
if ($durationMinsRaw !== null && $durationMinsRaw !== '') {
|
||||
$durationMins = filter_var($durationMinsRaw, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]);
|
||||
if ($durationMins === false || $durationMins <= 0) {
|
||||
$durationMins = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Annexes are optional — no validation required
|
||||
@@ -601,8 +604,9 @@ class ThesisCreateController
|
||||
'exemplaireBaiu',
|
||||
'exemplaireErg',
|
||||
'cc2r',
|
||||
'durationValue',
|
||||
'durationUnit'
|
||||
'durationPages',
|
||||
'durationMins',
|
||||
'hasAnnexes'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,8 +109,8 @@ class ThesisEditController
|
||||
$currentAccessTypeId = $rawRow['access_type_id'] ?? null;
|
||||
$currentContextNote = $rawRow['context_note'] ?? '';
|
||||
$currentContactVisible = $rawRow['contact_visible'] ?? '';
|
||||
$currentDurationValue = $rawRow['duration_value'] ?? null;
|
||||
$currentDurationUnit = $rawRow['duration_unit'] ?? 'pages';
|
||||
$currentDurationPages = $rawRow['duration_pages'] ?? null;
|
||||
$currentDurationMins = $rawRow['duration_minutes'] ?? null;
|
||||
|
||||
// Author contact info (from view)
|
||||
$contactInterne = $thesis['contact_interne'] ?? '';
|
||||
@@ -134,8 +134,8 @@ class ThesisEditController
|
||||
'currentAccessTypeId' => $currentAccessTypeId,
|
||||
'currentContextNote' => $currentContextNote,
|
||||
'currentContactVisible' => $currentContactVisible,
|
||||
'currentDurationValue' => $currentDurationValue,
|
||||
'currentDurationUnit' => $currentDurationUnit,
|
||||
'currentDurationPages' => $currentDurationPages,
|
||||
'currentDurationMins' => $currentDurationMins,
|
||||
'contactInterne' => $contactInterne,
|
||||
'contactPublic' => $contactPublic,
|
||||
'currentRaw' => $rawRow,
|
||||
@@ -223,8 +223,9 @@ class ThesisEditController
|
||||
'exemplaire_erg' => !empty($post['exemplaire_erg']),
|
||||
'cc2r' => !empty($post['cc2r']),
|
||||
'license_custom' => trim($post['license_custom'] ?? ''),
|
||||
'duration_value' => isset($post['duration_value']) && $post['duration_value'] !== '' ? (float)$post['duration_value'] : null,
|
||||
'duration_unit' => !empty($post['duration_unit']) ? $post['duration_unit'] : 'pages',
|
||||
'duration_pages' => (isset($post['duration_pages']) && $post['duration_pages'] !== '') ? (int)$post['duration_pages'] : null,
|
||||
'duration_minutes' => (isset($post['duration_minutes']) && $post['duration_minutes'] !== '') ? (int)$post['duration_minutes'] : null,
|
||||
'has_annexes' => !empty($post['has_annexes']) ? 1 : 0,
|
||||
];
|
||||
// Regenerate identifier if year changed or if identifier prefix doesn't match year
|
||||
$oldThesis = $this->db->getThesis($thesisId);
|
||||
|
||||
+13
-9
@@ -2032,7 +2032,7 @@ class Database
|
||||
public function getThesisRawFields(int $thesisId): ?array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT license_id, license_custom, access_type_id, context_note, contact_visible, remarks, jury_points, exemplaire_baiu, exemplaire_erg, cc2r, duration_value, duration_unit, is_published FROM theses WHERE id = ? LIMIT 1'
|
||||
'SELECT license_id, license_custom, access_type_id, context_note, contact_visible, remarks, jury_points, exemplaire_baiu, exemplaire_erg, cc2r, duration_pages, duration_minutes, has_annexes, is_published FROM theses WHERE id = ? LIMIT 1'
|
||||
);
|
||||
$stmt->execute([$thesisId]);
|
||||
$row = $stmt->fetch();
|
||||
@@ -2176,8 +2176,9 @@ class Database
|
||||
exemplaire_baiu = ?,
|
||||
exemplaire_erg = ?,
|
||||
cc2r = ?,
|
||||
duration_value = ?,
|
||||
duration_unit = ?,
|
||||
duration_pages = ?,
|
||||
duration_minutes = ?,
|
||||
has_annexes = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
");
|
||||
@@ -2209,8 +2210,9 @@ class Database
|
||||
!empty($data['exemplaire_baiu']) ? 1 : 0,
|
||||
!empty($data['exemplaire_erg']) ? 1 : 0,
|
||||
!empty($data['cc2r']) ? 1 : 0,
|
||||
isset($data['duration_value']) && $data['duration_value'] !== '' ? (float)$data['duration_value'] : null,
|
||||
!empty($data['duration_unit']) ? $data['duration_unit'] : 'pages',
|
||||
isset($data['duration_pages']) && $data['duration_pages'] !== '' ? (int)$data['duration_pages'] : null,
|
||||
isset($data['duration_minutes']) && $data['duration_minutes'] !== '' ? (int)$data['duration_minutes'] : null,
|
||||
!empty($data['has_annexes']) ? 1 : 0,
|
||||
$thesisId,
|
||||
]);
|
||||
$stmt->execute($params);
|
||||
@@ -2261,9 +2263,10 @@ class Database
|
||||
remarks, jury_points,
|
||||
exemplaire_baiu, exemplaire_erg,
|
||||
cc2r,
|
||||
duration_value, duration_unit,
|
||||
duration_pages, duration_minutes,
|
||||
has_annexes,
|
||||
submitted_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, "draft", ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, "draft", ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
');
|
||||
|
||||
$validObjet = ['tfe', 'thèse', 'frart'];
|
||||
@@ -2296,8 +2299,9 @@ class Database
|
||||
!empty($data['exemplaire_baiu']) ? 1 : 0,
|
||||
!empty($data['exemplaire_erg']) ? 1 : 0,
|
||||
!empty($data['cc2r']) ? 1 : 0,
|
||||
isset($data['duration_value']) && $data['duration_value'] !== '' ? (float)$data['duration_value'] : null,
|
||||
!empty($data['duration_unit']) ? $data['duration_unit'] : 'pages',
|
||||
isset($data['duration_pages']) && $data['duration_pages'] !== '' ? (int)$data['duration_pages'] : null,
|
||||
isset($data['duration_minutes']) && $data['duration_minutes'] !== '' ? (int)$data['duration_minutes'] : null,
|
||||
!empty($data['has_annexes']) ? 1 : 0,
|
||||
]);
|
||||
|
||||
$newId = (int)$this->pdo->lastInsertId();
|
||||
|
||||
@@ -203,8 +203,7 @@ class FormBootstrap
|
||||
'contactPublic' => null,
|
||||
'currentContextNote' => null,
|
||||
'currentContactVisible' => null,
|
||||
'currentDurationValue' => null,
|
||||
'currentDurationUnit' => 'pages',
|
||||
|
||||
|
||||
// Files (edit mode)
|
||||
'currentCover' => null,
|
||||
|
||||
@@ -31,5 +31,8 @@ function icon(string $name, int $size = 0, string $class = ''): string {
|
||||
$svg = str_replace('<svg', '<svg class="' . $class . '"', $svg);
|
||||
}
|
||||
}
|
||||
// Collapse newlines: otherwise raw SVG markup breaks JS string literals
|
||||
// when icon() is used inside <script> tags (e.g., contenus.php inline rename)
|
||||
$svg = str_replace(["\n", "\r"], '', $svg);
|
||||
return $svg;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ CREATE TABLE IF NOT EXISTS theses (
|
||||
remarks TEXT,
|
||||
duration_value REAL,
|
||||
duration_unit TEXT DEFAULT 'pages',
|
||||
duration_pages INTEGER,
|
||||
duration_minutes INTEGER,
|
||||
has_annexes INTEGER NOT NULL DEFAULT 0,
|
||||
access_type_id INTEGER,
|
||||
license_id INTEGER,
|
||||
jury_points DECIMAL(4,2),
|
||||
@@ -416,8 +419,9 @@ SELECT
|
||||
t.synopsis,
|
||||
t.context_note,
|
||||
t.contact_visible,
|
||||
t.duration_value,
|
||||
t.duration_unit,
|
||||
t.duration_pages,
|
||||
t.duration_minutes,
|
||||
t.has_annexes,
|
||||
at.name as access_type,
|
||||
lt.name as license_type,
|
||||
t.license_id,
|
||||
|
||||
@@ -131,6 +131,25 @@
|
||||
<dt>Mots-clés</dt><dd><?= htmlspecialchars($thesis['keywords']) ?></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$_hasPages = isset($thesis['duration_pages']) && $thesis['duration_pages'] !== null;
|
||||
$_hasMins = isset($thesis['duration_minutes']) && $thesis['duration_minutes'] !== null;
|
||||
if ($_hasPages || $_hasMins):
|
||||
$_parts = [];
|
||||
if ($_hasPages) $_parts[] = $thesis['duration_pages'] . ' pages';
|
||||
if ($_hasMins) {
|
||||
$_h = (int)floor($thesis['duration_minutes'] / 60);
|
||||
$_m = $thesis['duration_minutes'] % 60;
|
||||
$_parts[] = $_h . 'h' . str_pad((string)$_m, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
?>
|
||||
<dt>Durée</dt><dd><?= htmlspecialchars(implode(', ', $_parts)) ?></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($thesis['has_annexes'])): ?>
|
||||
<dt>Annexes</dt><dd>Oui</dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($thesis['baiu_link']): ?>
|
||||
<dt>Lien BAIU</dt><dd><a href="<?= htmlspecialchars($thesis['baiu_link']) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($thesis['baiu_link']) ?></a></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -185,9 +185,15 @@ $websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
|
||||
|
||||
<!-- ── 4. Annexes ── -->
|
||||
<div id="annexes-input-block">
|
||||
<input type="hidden" name="has_annexes" value="0">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="has_annexes" value="1"
|
||||
<?= !empty($_POST['has_annexes']) ? 'checked' : '' ?>>
|
||||
Ce TFE contient des annexes
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-form-group admin-files-fieldgroup">
|
||||
<label for="annexe-files-input">Annexes (optionnel)</label>
|
||||
<label for="annexe-files-input">Fichiers annexes (optionnel)</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="annexe-files-input"
|
||||
name="queue_file[annexe][]"
|
||||
|
||||
@@ -103,8 +103,7 @@ $existingWebsiteLabel = $existingWebsiteLabel ?? '';
|
||||
$checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
||||
|
||||
// Duration (value + unit)
|
||||
$durationValue = $durationValue ?? null;
|
||||
$durationUnit = $durationUnit ?? 'pages';
|
||||
|
||||
|
||||
// WCAG 3.3.1: which field has a validation error (set by caller from App::consumeAutofocus())
|
||||
$errorFieldName = $errorFieldName ?? null;
|
||||
@@ -419,70 +418,46 @@ if ($filesMode === 'add'): ?>
|
||||
|
||||
<!-- ═══════════════════ Durée ═══════════════════ -->
|
||||
<?php
|
||||
$_durRaw = $durationValue ?? ($formData['duration_value'] ?? null);
|
||||
$_durUnit = $durationUnit ?? ($formData['duration_unit'] ?? 'pages');
|
||||
$_durFloat = $_durRaw !== null && $_durRaw !== '' ? (float)$_durRaw : null;
|
||||
// Pre-split stored hours into h/m/s for the time-input fields
|
||||
// Combined duration: pages (integer) + minutes (integer, stored as total minutes)
|
||||
$_pagesRaw = $formData['duration_pages'] ?? $currentRaw['duration_pages'] ?? null;
|
||||
$_minsRaw = $formData['duration_minutes'] ?? $currentRaw['duration_minutes'] ?? null;
|
||||
$_pages = $_pagesRaw !== null && $_pagesRaw !== '' ? (int)$_pagesRaw : null;
|
||||
$_totalMins = $_minsRaw !== null && $_minsRaw !== '' ? (int)$_minsRaw : null;
|
||||
// Pre-split total minutes into h/m for the time inputs
|
||||
$_durH = '';
|
||||
$_durM = '';
|
||||
$_durS = '';
|
||||
if ($_durFloat !== null && $_durUnit === 'durée') {
|
||||
$_durH = (int)floor($_durFloat);
|
||||
$_remaining = round(($_durFloat - $_durH) * 3600);
|
||||
$_durM = (int)floor($_remaining / 60);
|
||||
$_durS = $_remaining % 60;
|
||||
$_durH = (string)$_durH;
|
||||
$_durM = (string)$_durM;
|
||||
$_durS = (string)$_durS;
|
||||
if ($_totalMins !== null) {
|
||||
$_durH = (string)(int)floor($_totalMins / 60);
|
||||
$_durM = (string)($_totalMins % 60);
|
||||
}
|
||||
?>
|
||||
<fieldset id="duration-fieldset">
|
||||
<legend>Durée</legend>
|
||||
<div class="admin-form-group">
|
||||
<div>
|
||||
<label for="duration_unit">Unité :</label>
|
||||
<select id="duration_unit" name="duration_unit">
|
||||
<?php
|
||||
$_units = [
|
||||
'pages' => 'pages',
|
||||
'mo' => 'Mo',
|
||||
'durée' => 'durée (h:m:s)',
|
||||
];
|
||||
foreach ($_units as $_val => $_label): ?>
|
||||
<option value="<?= $_val ?>" <?= $_durUnit === $_val ? 'selected' : '' ?>><?= htmlspecialchars($_label) ?></option>
|
||||
<?php endforeach; unset($_units, $_val, $_label); ?>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Integer input for pages / Mo -->
|
||||
<div id="duration-value-integer"<?= $_durUnit === 'durée' ? ' style="display:none"' : '' ?>>
|
||||
<label for="duration_value_int" id="duration-value-label">Valeur :</label>
|
||||
<input type="number" id="duration_value_int"
|
||||
value="<?= htmlspecialchars($_durUnit !== 'durée' ? (string)($_durFloat ?? '') : '') ?>"
|
||||
step="1" min="0" placeholder="0"
|
||||
<!-- Pages -->
|
||||
<div class="duration-field">
|
||||
<label for="duration_pages">Pages :</label>
|
||||
<input type="number" id="duration_pages" name="duration_pages"
|
||||
value="<?= htmlspecialchars($_pages !== null ? (string)$_pages : '') ?>"
|
||||
step="1" min="0" placeholder="Ex: 88"
|
||||
style="width: 8ch;">
|
||||
</div>
|
||||
<!-- Time inputs for durée -->
|
||||
<div id="duration-value-time" class="duration-time-inputs"<?= $_durUnit !== 'durée' ? ' style="display:none"' : '' ?>>
|
||||
<!-- Time duration (h:m) -->
|
||||
<div class="duration-field duration-time-field">
|
||||
<label>Durée :</label>
|
||||
<span class="duration-time-fields">
|
||||
<input type="number" id="duration_h" value="<?= htmlspecialchars($_durH) ?>"
|
||||
<input type="number" id="duration_h" name="duration_h" value="<?= htmlspecialchars($_durH) ?>"
|
||||
step="1" min="0" placeholder="0" style="width: 5ch;" aria-label="Heures">
|
||||
<span>h</span>
|
||||
<input type="number" id="duration_m" value="<?= htmlspecialchars($_durM) ?>"
|
||||
<input type="number" id="duration_m" name="duration_m" value="<?= htmlspecialchars($_durM) ?>"
|
||||
step="1" min="0" max="59" placeholder="0" style="width: 5ch;" aria-label="Minutes">
|
||||
<span>m</span>
|
||||
<input type="number" id="duration_s" value="<?= htmlspecialchars($_durS) ?>"
|
||||
step="1" min="0" max="59" placeholder="0" style="width: 5ch;" aria-label="Secondes">
|
||||
<span>s</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hidden field: always submitted, populated by JS on unit change / submit -->
|
||||
<input type="hidden" id="duration_value" name="duration_value"
|
||||
value="<?= htmlspecialchars((string)($_durFloat ?? '')) ?>">
|
||||
<small>Optionnel. Exemples : 88 pages, 120 Mo, 1h30.</small>
|
||||
<small>Optionnel. Vous pouvez indiquer des pages, une durée, ou les deux.</small>
|
||||
</fieldset>
|
||||
<?php unset($_durRaw, $_durUnit, $_durFloat, $_durH, $_durM, $_durS, $_remaining); ?>
|
||||
<?php unset($_pagesRaw, $_minsRaw, $_pages, $_totalMins, $_durH, $_durM); ?>
|
||||
|
||||
<!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ -->
|
||||
<?php
|
||||
|
||||
@@ -46,29 +46,23 @@
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data["duration_value"]) && !empty($data["duration_unit"])): ?>
|
||||
<?php
|
||||
$_dVal = (float)$data["duration_value"];
|
||||
$_dUnit = $data["duration_unit"];
|
||||
$_label = match($_dUnit) {
|
||||
'pages' => 'pages',
|
||||
'mo' => 'Mo',
|
||||
'durée' => '',
|
||||
default => $_dUnit,
|
||||
};
|
||||
if ($_dUnit === 'durée') {
|
||||
$_hours = (int)floor($_dVal);
|
||||
$_mins = (int)round(($_dVal - $_hours) * 60);
|
||||
$_display = ($_mins > 0) ? "{$_hours}h{$_mins}" : "{$_hours}h";
|
||||
} else {
|
||||
$_display = ($_dVal == (int)$_dVal) ? (int)$_dVal : $_dVal;
|
||||
}
|
||||
$_hasPages = isset($data["duration_pages"]) && $data["duration_pages"] !== null;
|
||||
$_hasMins = isset($data["duration_minutes"]) && $data["duration_minutes"] !== null;
|
||||
if ($_hasPages || $_hasMins):
|
||||
$_parts = [];
|
||||
if ($_hasPages) $_parts[] = $data['duration_pages'] . ' pages';
|
||||
if ($_hasMins) {
|
||||
$_h = (int)floor($data['duration_minutes'] / 60);
|
||||
$_m = $data['duration_minutes'] % 60;
|
||||
$_parts[] = $_h . 'h' . str_pad((string)$_m, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
?>
|
||||
<p class="tfe-meta-item">
|
||||
<span class="tfe-meta-label">Durée :</span>
|
||||
<?= $_display ?><?= $_label ? ' ' . htmlspecialchars($_label) : '' ?>
|
||||
<?= htmlspecialchars(implode(', ', $_parts)) ?>
|
||||
</p>
|
||||
<?php unset($_dVal, $_dUnit, $_label, $_display, $_hours, $_mins); endif; ?>
|
||||
<?php unset($_hasPages, $_hasMins, $_parts, $_h, $_m); endif; ?>
|
||||
|
||||
<?php if (!empty($data["languages"])): ?>
|
||||
<p class="tfe-meta-item">
|
||||
|
||||
Reference in New Issue
Block a user