From 3cf09ea74f4e230e642a218a54b669e60a74a93b Mon Sep 17 00:00:00 2001 From: Valentin Le Moign Date: Thu, 27 Aug 2026 17:48:08 +0200 Subject: [PATCH] =?UTF-8?q?Annonces=20=C3=A0=20venir=20propos=C3=A9es,=20e?= =?UTF-8?q?t=20dates=20d'=C3=A9v=C3=A9nement=20au=20lieu=20des=20dates=20d?= =?UTF-8?q?e=20publication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retour du commanditaire après le premier envoi réel (juillet 2026) : des annonces à venir n'étaient pas proposées à la coche, et les dates affichées étaient les dates de publication. - Fenêtre « à venir » de 12 mois (FUTURE_HORIZON_MONTHS), démarrant à la fin du mois de la newsletter ou à aujourd'hui si ce mois est passé, en remplacement du plafond « début de fenêtre <= fin du mois ». Idem pour les séances (SEANCE_WINDOW_MARGIN_DAYS de 5 jours supprimée). Ces items sont regroupés dans un bloc « À venir », décochés par défaut. - event_date_label() centralise les libellés du thème (« Le X de H1 à H2 », « Du X au Y », « Jusqu'au X », « X à H ») sur la convention date_de_debut > datetime > post_date. Utilisée par l'export et par la pastille de la liste à cocher, qui affichent donc la même chose. - Tri par date d'événement (sql_event_date_order) et non par date de publication. - Fenêtres de fin « +35 j » et « +3 mois » recalées sur la date d'événement : un événement annoncé plus de 35 jours à l'avance sortait de la fenêtre avant d'avoir eu lieu et manquait dans la newsletter de son propre mois. - merge_selected_items() : une sélection déjà enregistrée est réinjectée dans la liste même si ses items sont sortis de la fenêtre — sans ça, rouvrir puis réenregistrer une ancienne newsletter l'amputait silencieusement. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011w7RzAHv4r1Yahnm2NwDDG --- README.md | 44 ++++- assets/admin.css | 27 +++ assets/admin.js | 7 + includes/class-admin-page.php | 109 ++++++++--- includes/class-html-exporter.php | 31 ++- includes/class-post-query.php | 327 +++++++++++++++++++++++-------- 6 files changed, 409 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index 34c0f25..21d29cc 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Le workflow : 1. Sélection d'un mois (sélecteur année-mois) 2. Chargement AJAX (`thalim_nl_load_month`) des contenus éligibles, regroupés par catégorie parente -3. Cases à cocher pour inclure ou exclure chaque publication / séance. **Tout est coché par défaut** pour un mois sans newsletter existante (à l'ouverture d'une newsletter déjà enregistrée, c'est la sélection sauvegardée qui est restaurée) +3. Cases à cocher pour inclure ou exclure chaque publication / séance. Pour un mois sans newsletter existante, **tout est coché par défaut sauf le bloc « À venir »** (voir plus bas). À l'ouverture d'une newsletter déjà enregistrée, c'est la sélection sauvegardée qui est restaurée — y compris les items qui ne seraient plus dans la fenêtre du mois, réintégrés par `merge_selected_items()` pour qu'un réenregistrement ne les perde pas 4. **Réordonnancement par glisser-déposer** : chaque item porte une poignée (`⋮`). Pour les catégories normales, on réordonne les annonces dans leur liste. Pour les séminaires, la poignée est sur le **titre du séminaire** : on réordonne les **séminaires entre eux** (les séances, elles, restent toujours triées par ordre chronologique). L'ordre choisi est repris tel quel dans le rendu HTML après sauvegarde — l'ordre de soumission des cases suit l'ordre du DOM, et l'export rend chaque section dans l'ordre stocké 5. Champs **intro**, **conclusion**, **URL d'inscription**, **URL de désinscription** 6. Sauvegarde : crée un post WordPress dans la catégorie **Newsletter** (`20`) avec le HTML email complet en `post_content` @@ -33,24 +33,31 @@ Une liste des newsletters déjà sauvegardées permet de revenir éditer un mois > Les catégories **Vie du labo (intranet)** (`9`), **Séance de séminaire** (`12`), **Newsletter** (`20`) et **Non classé** (`31`) sont exclues de l'UI (`EXCLUDED_CATS` dans `includes/class-post-query.php`). Les séances (cat 12) restent listées, mais imbriquées sous leur séminaire — voir plus bas. -## Fenêtres d'éligibilité par catégorie +## Éligibilité : date d'événement, pas date de publication -Les contenus pertinents d'un mois donné ne sont pas seulement « les posts publiés ce mois-ci » — chaque catégorie a sa propre logique de fenêtre temporelle (cf. `WINDOW_TYPES` dans `includes/class-post-query.php`) : +Partout où il est question de « la date » d'un contenu, c'est la **date de l'événement annoncé** qui fait foi, dans l'ordre de priorité du thème : `date_de_debut` > `datetime` > `post_date`. Cet ordre gouverne l'éligibilité, le tri à l'intérieur de chaque section, et la date affichée dans le mail. -| Catégorie | Fenêtre | +Un contenu est proposé pour le mois M s'il remplit l'une des deux conditions : + +1. **Contenu du mois** — sa date d'événement est antérieure ou égale à la fin de M, et sa fenêtre de fin (ci-dessous) ne s'est pas achevée avant le début de M ; +2. **Contenu à venir** — sa date d'événement tombe entre la fin de M (ou aujourd'hui si M est déjà passé) et `FUTURE_HORIZON_MONTHS` (12 mois). Ces items sont regroupés dans un bloc **« À venir »**, **décochés par défaut**. + +La fenêtre de fin, qui détermine combien de temps un contenu reste proposé après son événement, dépend de la catégorie (`SPECIAL_WINDOW_TYPES` dans `includes/class-post-query.php`) : + +| Catégorie | Fin de fenêtre | | -------------------------------------------------- | ---------------------- | -| Appels (`8`), Soutenances (`14`) | `datetime_to_fin` (du `datetime` à `date_de_fin`) | -| Colloques (`10`), Communications (`13`) | `debut_minus35_to_fin` (de `date_de_debut - 35j` à `date_de_fin`) | -| Ouvrages (`15`), Articles (`16`) | `datetime_plus3m` (du `datetime` à `datetime + 3 mois`) | -| **Toutes les autres** | `datetime_plus35d` (du `datetime` à `datetime + 35 jours`) | +| Appels (`8`), Soutenances (`14`) | `date_de_fin` (date limite / date de soutenance) | +| Colloques (`10`), Communications (`13`) | `date_de_fin`, sinon `date_de_debut` | +| Ouvrages (`15`), Articles (`16`) | date d'événement + 3 mois | +| **Toutes les autres** | date d'événement + 35 jours | -Quand `datetime` (ou `date_de_debut`) est vide, le `post_date` sert de fallback. Cette logique permet par ex. à un appel à communication d'apparaître dans toutes les newsletters jusqu'à sa date de fin. +Un appel à contribution reste donc proposé dans toutes les newsletters jusqu'à sa date limite, et un événement annoncé six mois à l'avance apparaît à la fois dans le bloc « À venir » des newsletters intermédiaires et dans la liste principale de la newsletter de son propre mois. ### Cas particulier : Séminaires (`11`) → sélection par séance Le séminaire n'est **pas** sélectionnable en tant que tel. À la place, le plugin liste ses **séances** (cat 12) individuellement, chacune avec sa propre case à cocher, regroupées sous le titre (non cliquable) de leur séminaire parent. -- **Éligibilité** : une séance apparaît si sa `date_de_debut` tombe dans `[1er du mois, dernier jour du mois + 5 jours]` (marge `SEANCE_WINDOW_MARGIN_DAYS` dans `class-post-query.php`). Un séminaire sans séance dans cette fenêtre n'apparaît pas. +- **Éligibilité** : une séance apparaît si sa `date_de_debut` tombe dans le mois, ou dans la fenêtre « à venir » (jusqu'à 12 mois). Un séminaire sans séance dans ces fenêtres n'apparaît pas. Les séances à venir sont décochées par défaut et signalées visuellement. - **Découverte** : on parcourt les séminaires publiés (cat 11) et on lit leur meta `seances` (tableau d'IDs de séances). Le lien parent→séance vit donc sur le séminaire. - **Sélection stockée** : `_newsletter_sections[11]` contient des **IDs de séances**, plus des IDs de séminaires. - **Rendu HTML** (`class-html-exporter.php`) : les séances cochées sont regroupées par séminaire parent (lookup inverse via `Thalim_NL_Post_Query::get_seminar_id_for_seance()`, même requête que la redirection `#seance-{ID}` du thème). Le titre du séminaire est affiché **une seule fois**, suivi de la liste des séances sélectionnées (date · heure · lieu, lien vers `#seance-{id}`). @@ -73,6 +80,23 @@ La liste des catégories éligibles n'est **pas** codée en dur dans le plugin > IDs vérifiés en DB le 2026-03-20. À mettre à jour en cas de migration ou de réorganisation des taxonomies. +## Dates affichées dans le mail + +Le libellé est construit par `Thalim_NL_Post_Query::event_date_label()`, qui reprend les règles du thème (`thalim_get_agenda_card_data()`) : + +| Champs renseignés | Rendu | +| --- | --- | +| début et fin le même jour, avec horaires | `Le 9 octobre 2026 de 14:00 à 17:00` | +| début et fin sur plusieurs jours | `Du 9 octobre 2026 au 11 octobre 2026` | +| début seul (+ heure éventuelle) | `9 octobre 2026 à 14:00` | +| fin seule — cas des appels à contribution | `Jusqu'au 1 septembre 2026` | +| `datetime` seul | `9 octobre 2026` | +| aucune date | date de publication, en dernier recours | + +Les ouvrages (`15`) font exception : année seule, comme les cartes du site. + +La même fonction alimente la pastille de date de la liste à cocher — ce que voit le rédacteur est ce qui partira dans le mail. + ## Format HTML email `includes/class-html-exporter.php` génère un HTML compatible clients mail : diff --git a/assets/admin.css b/assets/admin.css index 556c96a..06f6771 100644 --- a/assets/admin.css +++ b/assets/admin.css @@ -304,3 +304,30 @@ grid-template-columns: 1fr; } } + +/* ---- Annonces à venir (au-delà du mois affiché) ---- */ +.thalim-nl-future { + margin-top: 10px; + padding-top: 6px; + border-top: 1px dashed #dcdcde; +} + +.thalim-nl-future-title { + margin: 0 0 2px 0; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #787c82; +} + +.thalim-nl-future-title span { + font-weight: normal; + text-transform: none; + letter-spacing: 0; + font-style: italic; +} + +.thalim-nl-post-row.is-future .thalim-nl-post-title { + color: #50575e; +} diff --git a/assets/admin.js b/assets/admin.js index 076f324..c0eecf6 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -36,6 +36,13 @@ makeSortable($body, '> .thalim-nl-post-row'); } }); + + // Le bloc « À venir » est une zone de tri à part, pour que ses lignes + // ne puissent pas se mélanger à celles du mois (l'ordre du DOM est + // l'ordre de soumission, donc l'ordre de rendu dans le mail). + $root.find('.thalim-nl-future').each(function () { + makeSortable($(this), '> .thalim-nl-post-row'); + }); } // A click on the drag handle must not toggle its enclosing checkbox label. diff --git a/includes/class-admin-page.php b/includes/class-admin-page.php index 64fa7de..6c713fe 100644 --- a/includes/class-admin-page.php +++ b/includes/class-admin-page.php @@ -53,6 +53,9 @@ class Thalim_NL_Admin_Page { // Query posts for current month $query = new Thalim_NL_Post_Query(); $month_data = $query->get_posts_for_month($year_month); + if ($existing) { + $month_data = $query->merge_selected_items($month_data, $existing_sections); + } // Past newsletters $past_newsletters = get_posts([ @@ -304,12 +307,12 @@ class Thalim_NL_Admin_Page { -