diff --git a/README.md b/README.md index 85e4890..34c0f25 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,16 @@ 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 -4. Champs **intro**, **conclusion**, **URL d'inscription**, **URL de désinscription** -5. Sauvegarde : crée un post WordPress dans la catégorie **Newsletter** (`20`) avec le HTML email complet en `post_content` -6. Bouton **Exporter en HTML** (`thalim_nl_export_html`) → téléchargement du fichier `newsletter-THALIM-{mois}.html` +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) +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` +7. Bouton **Exporter en HTML** (`thalim_nl_export_html`) → téléchargement du fichier `newsletter-THALIM-{mois}.html` Une liste des newsletters déjà sauvegardées permet de revenir éditer un mois passé. +> 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 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`) : @@ -37,12 +40,22 @@ Les contenus pertinents d'un mois donné ne sont pas seulement « les posts publ | Catégorie | Fenêtre | | -------------------------------------------------- | ---------------------- | | Appels (`8`), Soutenances (`14`) | `datetime_to_fin` (du `datetime` à `date_de_fin`) | -| Colloques (`10`), Séminaires (`11`) | `debut_minus35_to_fin` (de `date_de_debut - 35j` à `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`) | 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. +### 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. +- **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}`). +- **Ordre** : les **séminaires** sont réordonnables entre eux par glisser-déposer (poignée sur le titre) — leur ordre de premier appartenance dans la sélection stockée donne l'ordre de rendu. Les **séances** d'un séminaire sont toujours triées par `date_de_debut` croissante, dans l'UI comme à l'export. + ## Catégories couvertes La liste des catégories éligibles n'est **pas** codée en dur dans le plugin — elle est calculée dynamiquement via `Thalim_NL_Post_Query::get_eligible_categories()` (toutes les catégories WordPress, groupées par parent). Les constantes en haut de `thalim-newsletter.php` (`THALIM_NL_CAT_APPELS = 8`, etc.) ne servent qu'à associer les fenêtres temporelles spéciales aux catégories concernées : diff --git a/assets/admin.css b/assets/admin.css index 119995b..556c96a 100644 --- a/assets/admin.css +++ b/assets/admin.css @@ -138,6 +138,41 @@ margin-top: 2px; } +/* ---- Drag handle + reordering ---- */ +.thalim-nl-drag-handle { + flex-shrink: 0; + cursor: grab; + color: #c3c4c7; + font-size: 16px; + width: 16px; + height: 16px; + line-height: 1; + align-self: center; +} + +.thalim-nl-post-row:hover .thalim-nl-drag-handle, +.thalim-nl-seminar-title:hover .thalim-nl-drag-handle { + color: #8c8f94; +} + +.thalim-nl-drag-handle:active { + cursor: grabbing; +} + +.thalim-nl-sortable-placeholder { + border: 1px dashed #2271b1; + background: #f0f6fc; + border-radius: 3px; + height: 26px; + margin: 2px 0; +} + +.ui-sortable-helper { + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + border-radius: 3px; +} + .thalim-nl-post-title { flex: 1; font-size: 13px; @@ -166,29 +201,37 @@ white-space: nowrap; } -/* ---- Séances list under seminars ---- */ -.thalim-nl-seances-list { - margin: 2px 0 6px 28px; - padding: 0; - list-style: none; - border-left: 2px solid #dcdcde; - padding-left: 12px; +/* ---- Seminars: séances grouped under their seminar ---- */ +.thalim-nl-seminar-group { + margin-top: 18px; } -.thalim-nl-seance-item { - padding: 3px 0; - font-size: 12px; - color: #50575e; +.thalim-nl-seminar-group:first-child { + margin-top: 4px; } -.thalim-nl-seance-title { - font-weight: 500; +.thalim-nl-seminar-title { + display: flex; + align-items: baseline; + gap: 6px; + font-weight: 600; + font-size: 13px; + color: #1d2327; + padding: 5px 10px; + background: #f0f3f5; + border-radius: 3px; } -.thalim-nl-seance-meta { - display: block; - color: #888; - font-size: 11px; +.thalim-nl-seminar-title .thalim-nl-post-view { + margin-left: auto; +} + +/* Séances hang under their seminar title, connected by a left rail */ +.thalim-nl-seance-row { + margin-left: 9px; + padding-left: 17px; + border-bottom: none; + border-left: 2px solid #e2e6ea; } /* ---- Empty state ---- */ diff --git a/assets/admin.js b/assets/admin.js index 23b241c..076f324 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -2,6 +2,49 @@ (function ($) { 'use strict'; + // ------------------------------------------------------------------------- + // Drag-and-drop reordering of items within each category list. + // The submit order of checkboxes follows DOM order, and the exporter + // renders each section in stored order — so reordering rows here is enough + // for the new order to appear in the generated HTML after saving. + // ------------------------------------------------------------------------- + function makeSortable($container, itemSelector) { + if (!$.fn.sortable) return; + $container.sortable({ + items: itemSelector, + handle: '.thalim-nl-drag-handle', + axis: 'y', + containment: 'parent', + tolerance: 'pointer', + cursor: 'grabbing', + opacity: 0.9, + placeholder: 'thalim-nl-sortable-placeholder', + forcePlaceholderSize: true, + }); + } + + function initSortable() { + var $root = $('#thalim-nl-sections-wrap'); + + $root.find('.thalim-nl-section-body').each(function () { + var $body = $(this); + if ($body.find('.thalim-nl-seminar-group').length) { + // Seminars: reorder whole seminars (séances stay chronological). + makeSortable($body, '> .thalim-nl-seminar-group'); + } else { + // Normal categories: reorder post rows. + makeSortable($body, '> .thalim-nl-post-row'); + } + }); + } + + // A click on the drag handle must not toggle its enclosing checkbox label. + $(document).on('click', '.thalim-nl-drag-handle', function (e) { + e.preventDefault(); + }); + + $(initSortable); + // ------------------------------------------------------------------------- // Month change → AJAX reload sections + update TinyMCE intro // ------------------------------------------------------------------------- @@ -24,6 +67,7 @@ if (!response.success) return; $wrap.html(response.data.html); + initSortable(); // Update TinyMCE intro — do NOT re-render wp_editor var introEditor = typeof tinymce !== 'undefined' && tinymce.get('thalim_nl_intro'); diff --git a/includes/class-admin-page.php b/includes/class-admin-page.php index e9b34d2..1cac891 100644 --- a/includes/class-admin-page.php +++ b/includes/class-admin-page.php @@ -118,7 +118,7 @@ class Thalim_NL_Admin_Page {
- render_sections_html($month_data, $existing_sections); ?> + render_sections_html($month_data, $existing ? $existing_sections : null); ?>
@@ -213,9 +213,18 @@ class Thalim_NL_Admin_Page { // Sections HTML (shared between initial render and AJAX) // ------------------------------------------------------------------------- - public function render_sections_html(array $month_data, array $checked_ids = []): string { + /** + * @param array $month_data Posts grouped by category for the month. + * @param array|null $checked_ids Saved selection (cat_id => [post_id,...]) for an + * existing newsletter. Pass null for a fresh month: + * every item is then checked by default. + */ + public function render_sections_html(array $month_data, ?array $checked_ids = null): string { $groups = Thalim_NL_Post_Query::get_eligible_categories(); + // No saved newsletter yet → everything checked by default. + $check_all = ($checked_ids === null); + ob_start(); $has_content = false; @@ -249,8 +258,17 @@ class Thalim_NL_Admin_Page { continue; } $count = count($posts); - $checked_in_section = (array) ($checked_ids[$cat_id] ?? $checked_ids[(string) $cat_id] ?? []); - $has_checked = !empty($checked_in_section); + $checked_in_section = $check_all + ? [] + : (array) ($checked_ids[$cat_id] ?? $checked_ids[(string) $cat_id] ?? []); + $has_checked = $check_all || !empty($checked_in_section); + + // Restore the saved drag-and-drop order ($checked_in_section is + // stored in submit order). For seminars this also reorders the + // seminar groups, since grouping below follows first appearance. + if (!$check_all) { + $posts = $this->reorder_posts_by_saved($posts, $checked_in_section); + } ?>
> @@ -258,49 +276,67 @@ class Thalim_NL_Admin_Page { ()
- - - -
    - -
  • - - - - -
  • + + $seance['seminar_title'], + 'permalink' => $seance['seminar_permalink'], + 'seances' => [], + ]; + } + $by_seminar[$sem_id]['seances'][] = $seance; + } + ?> + +
    +
    + + + + + +
    + + -
- - +
+ + + + + +
@@ -331,6 +367,53 @@ class Thalim_NL_Admin_Page { return ''; } + /** + * Reorder $posts so items whose 'id' appears in $order come first, in + * $order's sequence; the rest keep their query order, appended after. + * Restores the editor's saved drag-and-drop order in the admin UI. + */ + private function reorder_posts_by_saved(array $posts, array $order): array { + if (empty($order)) { + return $posts; + } + $rank = array_flip(array_map('intval', array_values($order))); + $keyed = []; + foreach ($posts as $idx => $post) { + $keyed[] = [ + 'post' => $post, + 'rank' => $rank[(int) $post['id']] ?? PHP_INT_MAX, + 'idx' => $idx, + ]; + } + // Sort by saved rank, then original index (stable for un-ranked items). + usort($keyed, fn($a, $b) => [$a['rank'], $a['idx']] <=> [$b['rank'], $b['idx']]); + return array_map(fn($x) => $x['post'], $keyed); + } + + /** + * Date · heure · lieu hint shown next to each séance checkbox. + */ + private function format_seance_hint(array $seance): string { + $parts = []; + if (!empty($seance['date_debut']) && !str_starts_with($seance['date_debut'], '0000-00-00')) { + $ts = strtotime($seance['date_debut']); + if ($ts) { + $parts[] = date_i18n('j M Y', $ts); + } + } + if (!empty($seance['heure_de_debut'])) { + $time = $seance['heure_de_debut']; + if (!empty($seance['heure_de_fin'])) { + $time .= '–' . $seance['heure_de_fin']; + } + $parts[] = $time; + } + if (!empty($seance['lieu'])) { + $parts[] = $seance['lieu']; + } + return implode(' · ', $parts); + } + // ------------------------------------------------------------------------- // Save // ------------------------------------------------------------------------- @@ -490,7 +573,7 @@ class Thalim_NL_Admin_Page { $unsubscribe_url = get_post_meta($existing->ID, '_newsletter_unsubscribe_url', true) ?: ''; } - $html = $this->render_sections_html($month_data, $existing_sections); + $html = $this->render_sections_html($month_data, $existing ? $existing_sections : null); wp_send_json_success([ 'html' => $html, diff --git a/includes/class-html-exporter.php b/includes/class-html-exporter.php index e07770d..ffd8a86 100644 --- a/includes/class-html-exporter.php +++ b/includes/class-html-exporter.php @@ -44,10 +44,6 @@ class Thalim_NL_HTML_Exporter { /** * Generate a complete email-ready HTML document from a saved newsletter post. */ - /** Month window timestamps — set by generate(), used by format_seminar_line() */ - private int $month_start = 0; - private int $month_end = 0; - public function generate(WP_Post $post): string { $sections_json = get_post_meta($post->ID, '_newsletter_sections', true); $sections = json_decode($sections_json, true) ?: []; @@ -57,13 +53,6 @@ class Thalim_NL_HTML_Exporter { $subscribe_url = get_post_meta($post->ID, '_newsletter_subscribe_url', true) ?: ''; $unsubscribe_url = get_post_meta($post->ID, '_newsletter_unsubscribe_url', true) ?: ''; - // Compute month window from stored newsletter month - $year_month = get_post_meta($post->ID, '_newsletter_month', true) ?: ''; - if ($year_month) { - $this->month_start = strtotime($year_month . '-01 00:00:00') ?: 0; - $this->month_end = strtotime('last day of ' . $year_month . ' 23:59:59') ?: 0; - } - // Build ordered section blocks following the category hierarchy $groups = Thalim_NL_Post_Query::get_eligible_categories(); $section_blocks = []; @@ -83,10 +72,16 @@ class Thalim_NL_HTML_Exporter { continue; } $items = []; - foreach ($post_ids as $pid) { - $line = $this->format_post_line((int) $pid, $cat_id); - if ($line) { - $items[] = $line; + if ((int) $cat_id === THALIM_NL_CAT_SEMINAIRES) { + // Selected items are séance IDs: group them by parent seminar, + // rendering each seminar's title once above its séances. + $items = $this->format_seminar_blocks(array_map('intval', $post_ids)); + } else { + foreach ($post_ids as $pid) { + $line = $this->format_post_line((int) $pid, $cat_id); + if ($line) { + $items[] = $line; + } } } if (!empty($items)) { @@ -305,11 +300,6 @@ body { margin: 0 !important; padding: 0 !important; } * Optional image on the left, title in serif, meta below in sans-serif. */ public function format_post_line(int $post_id, int $cat_id): string { - // Seminars: special rendering with séances - if ($cat_id === THALIM_NL_CAT_SEMINAIRES) { - return $this->format_seminar_line($post_id); - } - $post = get_post($post_id); if (!$post) { return ''; @@ -394,11 +384,45 @@ body { margin: 0 !important; padding: 0 !important; } } /** - * Format a seminar post: title (no dates) + list of séances with date, time, location. - * Each séance links to the seminar page. + * Render the seminar section from a flat list of selected séance IDs. + * Séances are grouped by their parent seminar (first-appearance order), + * and each seminar is rendered as one block with its title shown once. + * + * @return string[] One HTML block per seminar. */ - private function format_seminar_line(int $post_id): string { - $post = get_post($post_id); + private function format_seminar_blocks(array $seance_ids): array { + // Group séance IDs by parent seminar, preserving order of first appearance. + $by_seminar = []; + foreach ($seance_ids as $sid) { + $sid = (int) $sid; + $s_post = get_post($sid); + if (!$s_post || $s_post->post_status !== 'publish') { + continue; + } + $sem_id = Thalim_NL_Post_Query::get_seminar_id_for_seance($sid); + if (!$sem_id) { + continue; + } + $by_seminar[$sem_id][] = $sid; + } + + $blocks = []; + foreach ($by_seminar as $sem_id => $sids) { + $block = $this->format_seminar_block((int) $sem_id, $sids); + if ($block) { + $blocks[] = $block; + } + } + return $blocks; + } + + /** + * Format one seminar block: title (no dates) shown once + the given + * séances with date, time, location. Each séance links to its anchor + * on the seminar page. + */ + private function format_seminar_block(int $seminar_id, array $seance_ids): string { + $post = get_post($seminar_id); if (!$post) { return ''; } @@ -406,12 +430,12 @@ body { margin: 0 !important; padding: 0 !important; } $font_serif = "Gelasio, Georgia, 'Times New Roman', serif"; $font_sans = "Arial, Helvetica, sans-serif"; - $url = get_permalink($post_id); - $title = get_the_title($post_id); + $url = get_permalink($seminar_id); + $title = get_the_title($seminar_id); - // Image + // Image (first image from the seminar's documents_joints) $image_url = ''; - $doc_ids = get_post_meta($post_id, 'documents_joints', false); + $doc_ids = get_post_meta($seminar_id, 'documents_joints', false); foreach ($doc_ids as $doc_id) { $mime = get_post_mime_type($doc_id); if ($mime && str_starts_with($mime, 'image/')) { @@ -423,28 +447,19 @@ body { margin: 0 !important; padding: 0 !important; } } } - // Fetch séances within the newsletter month window - $seance_ids = get_post_meta($post_id, 'seances', false); + // Build the selected séances. Within a seminar, séances always display + // in chronological order (seminars themselves are reorderable, séances + // are not). $seances = []; foreach ($seance_ids as $sid) { $sid = (int) $sid; $s_post = get_post($sid); if (!$s_post || $s_post->post_status !== 'publish') continue; - $raw_debut = get_post_meta($sid, 'date_de_debut', true) ?: ''; - $ts = $raw_debut ? strtotime($raw_debut) : false; - - // Filter: only séances within the month window - if ($this->month_start && $this->month_end) { - if (!$ts || $ts < $this->month_start || $ts > $this->month_end) { - continue; - } - } - $seances[] = [ 'id' => $sid, 'title' => get_the_title($sid), - 'date_debut' => $raw_debut, + 'date_debut' => get_post_meta($sid, 'date_de_debut', true) ?: '', 'date_fin' => get_post_meta($sid, 'date_de_fin', true) ?: '', 'heure_de_debut' => substr(get_post_meta($sid, 'heure_de_debut', true) ?: '', 0, 5), 'heure_de_fin' => substr(get_post_meta($sid, 'heure_de_fin', true) ?: '', 0, 5), diff --git a/includes/class-post-query.php b/includes/class-post-query.php index a559e53..0549884 100644 --- a/includes/class-post-query.php +++ b/includes/class-post-query.php @@ -21,7 +21,6 @@ class Thalim_NL_Post_Query { private const SPECIAL_WINDOW_TYPES = [ THALIM_NL_CAT_APPELS => 'datetime_to_fin', THALIM_NL_CAT_COLLOQUES => 'debut_minus35_to_fin', - THALIM_NL_CAT_SEMINAIRES => 'debut_minus35_to_fin', THALIM_NL_CAT_COMMS => 'debut_minus35_to_fin', THALIM_NL_CAT_SOUTENANCES => 'datetime_to_fin', THALIM_NL_CAT_OUVRAGES => 'datetime_plus3m', @@ -30,8 +29,17 @@ class Thalim_NL_Post_Query { private const DEFAULT_WINDOW_TYPE = 'datetime_plus35d'; + /** + * Séminaires (cat 11) are not selected as whole posts: instead, each of + * their séances (cat 12) is individually selectable. A séance is eligible + * when its date_de_debut falls within the newsletter month, extended by + * this margin (in days) past the end of the month. + */ + private const SEANCE_WINDOW_MARGIN_DAYS = 5; + /** Categories to exclude from the newsletter UI */ private const EXCLUDED_CATS = [ + 9, // Vie du labo (intranet) 12, // Séance de séminaire 20, // Newsletter 31, // Non classé @@ -119,6 +127,15 @@ class Thalim_NL_Post_Query { $result = []; foreach (self::get_all_eligible_cat_ids() as $cat_id) { + // Seminars: list individually-selectable séances grouped by seminar. + if ($cat_id === THALIM_NL_CAT_SEMINAIRES) { + $seances = $this->query_seminar_seances($month_start, $month_end); + if (!empty($seances)) { + $result[$cat_id] = $seances; + } + continue; + } + $window_type = self::get_window_type($cat_id); $posts = $this->query_category($cat_id, $window_type, $month_start, $month_end); if (!empty($posts)) { @@ -128,6 +145,100 @@ class Thalim_NL_Post_Query { return $result; } + /** + * Build the flat list of selectable séances for the seminar category. + * + * We walk every published seminar (cat 11), read its `seances` meta + * (array of séance post IDs), and keep the séances whose date_de_debut + * falls within [month_start, month_end + SEANCE_WINDOW_MARGIN_DAYS]. + * Each returned item carries its parent seminar (id/title/permalink) so + * the UI and the exporter can group séances under their seminar. + * + * @return array Flat list of séance items (see shape below). + */ + private function query_seminar_seances(int $month_start, int $month_end): array { + global $wpdb; + + $window_end = $month_end + (self::SEANCE_WINDOW_MARGIN_DAYS * DAY_IN_SECONDS); + + $seminar_ids = $wpdb->get_col($wpdb->prepare( + "SELECT DISTINCT p.ID + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID + INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id + AND tt.taxonomy = 'category' AND tt.term_id = %d + WHERE p.post_type = 'post' AND p.post_status = 'publish'", + THALIM_NL_CAT_SEMINAIRES + )); + + $items = []; + foreach ($seminar_ids as $seminar_id) { + $seminar_id = (int) $seminar_id; + $seminar_title = get_the_title($seminar_id); + $seminar_permalink = get_permalink($seminar_id); + + foreach (get_post_meta($seminar_id, 'seances', false) as $sid) { + $sid = (int) $sid; + $s_post = get_post($sid); + if (!$s_post || $s_post->post_status !== 'publish') { + continue; + } + + $raw_debut = get_post_meta($sid, 'date_de_debut', true) ?: ''; + $ts = $raw_debut ? strtotime($raw_debut) : false; + if (!$ts || $ts < $month_start || $ts > $window_end) { + continue; + } + + $items[] = [ + 'id' => $sid, + 'title' => get_the_title($sid), + // Links straight to the séance anchor on the seminar page. + 'permalink' => $seminar_permalink . '#seance-' . $sid, + 'datetime' => '', + 'date_debut' => $raw_debut, + 'date_fin' => get_post_meta($sid, 'date_de_fin', true) ?: '', + 'post_date' => $s_post->post_date, + 'heure_de_debut' => substr(get_post_meta($sid, 'heure_de_debut', true) ?: '', 0, 5), + 'heure_de_fin' => substr(get_post_meta($sid, 'heure_de_fin', true) ?: '', 0, 5), + 'lieu' => get_post_meta($sid, 'lieu', true) ?: '', + 'membres' => $this->get_post_membres($sid), + 'autrepersonnes' => get_post_meta($sid, 'autrepersonnes', true) ?: '', + 'seminar_id' => $seminar_id, + 'seminar_title' => $seminar_title, + 'seminar_permalink' => $seminar_permalink, + ]; + } + } + + // Sort by séance date: seminars naturally order by their earliest séance + // when grouped by first appearance (see render / export grouping). + usort($items, fn($a, $b) => strcmp($a['date_debut'], $b['date_debut'])); + + return $items; + } + + /** + * Resolve the parent seminar (cat 11) that lists a given séance in its + * `seances` meta. Mirrors the reverse lookup used by the theme's + * #seance-{ID} redirect. Returns 0 when none is found. + */ + public static function get_seminar_id_for_seance(int $seance_id): int { + global $wpdb; + + $parent_id = $wpdb->get_var($wpdb->prepare( + "SELECT pm.post_id + FROM {$wpdb->postmeta} pm + JOIN {$wpdb->posts} p ON p.ID = pm.post_id + WHERE pm.meta_key = 'seances' AND pm.meta_value = %s + AND p.post_status = 'publish' + LIMIT 1", + (string) $seance_id + )); + + return (int) $parent_id; + } + /** * Query a single category with the appropriate window expression. */ @@ -190,7 +301,7 @@ class Thalim_NL_Post_Query { $posts = []; foreach ($rows as $row) { - $posts[] = $this->build_post_data((int) $row->ID, $row->post_title, $row->post_date, $month_start, $month_end); + $posts[] = $this->build_post_data((int) $row->ID, $row->post_title, $row->post_date); } return $posts; } @@ -289,8 +400,8 @@ class Thalim_NL_Post_Query { // Post data builder // ------------------------------------------------------------------------- - private function build_post_data(int $post_id, string $post_title, string $post_date, int $month_start = 0, int $month_end = 0): array { - $data = [ + private function build_post_data(int $post_id, string $post_title, string $post_date): array { + return [ 'id' => $post_id, 'title' => $post_title, 'permalink' => get_permalink($post_id), @@ -300,57 +411,7 @@ class Thalim_NL_Post_Query { 'post_date' => $post_date, 'membres' => $this->get_post_membres($post_id), 'autrepersonnes' => get_post_meta($post_id, 'autrepersonnes', true) ?: '', - 'seances' => [], ]; - - // For seminars, fetch séances within the month window - if ($month_start && $month_end && has_category(THALIM_NL_CAT_SEMINAIRES, $post_id)) { - $data['seances'] = $this->get_seances_in_window($post_id, $month_start, $month_end); - } - - return $data; - } - - /** - * Fetch séances linked to a seminar that fall within the given time window. - */ - private function get_seances_in_window(int $seminar_id, int $month_start, int $month_end): array { - $seance_ids = get_post_meta($seminar_id, 'seances', false); - if (empty($seance_ids)) { - return []; - } - - $seances = []; - foreach ($seance_ids as $sid) { - $sid = (int) $sid; - $post = get_post($sid); - if (!$post || $post->post_status !== 'publish') { - continue; - } - - $raw_debut = get_post_meta($sid, 'date_de_debut', true) ?: ''; - $ts = $raw_debut ? strtotime($raw_debut) : false; - if (!$ts || $ts < $month_start || $ts > $month_end) { - continue; - } - - $seances[] = [ - 'id' => $sid, - 'title' => get_the_title($sid), - 'date_debut' => $raw_debut, - 'date_fin' => get_post_meta($sid, 'date_de_fin', true) ?: '', - 'heure_de_debut' => substr(get_post_meta($sid, 'heure_de_debut', true) ?: '', 0, 5), - 'heure_de_fin' => substr(get_post_meta($sid, 'heure_de_fin', true) ?: '', 0, 5), - 'lieu' => get_post_meta($sid, 'lieu', true) ?: '', - ]; - } - - // Sort by date_de_debut ascending - usort($seances, function ($a, $b) { - return strcmp($a['date_debut'], $b['date_debut']); - }); - - return $seances; } /** diff --git a/thalim-newsletter.php b/thalim-newsletter.php index 7b25c70..15db785 100644 --- a/thalim-newsletter.php +++ b/thalim-newsletter.php @@ -88,7 +88,7 @@ class Thalim_Newsletter { wp_enqueue_script( 'thalim-newsletter-admin', THALIM_NL_PLUGIN_URL . 'assets/admin.js', - ['jquery'], + ['jquery', 'jquery-ui-sortable'], THALIM_NL_VERSION, true );