Annonces à venir proposées, et dates d'événement au lieu des dates de publication

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w7RzAHv4r1Yahnm2NwDDG
This commit is contained in:
2026-08-27 17:48:08 +02:00
co-authored by Claude Opus 5
parent 021f322719
commit 3cf09ea74f
6 changed files with 409 additions and 136 deletions
+84 -25
View File
@@ -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 {
</a>
</div>
<?php foreach ($sem['seances'] as $seance): ?>
<label class="thalim-nl-post-row thalim-nl-seance-row">
<label class="thalim-nl-post-row thalim-nl-seance-row<?php echo !empty($seance['is_future']) ? ' is-future' : ''; ?>">
<input
type="checkbox"
name="nl_sections[<?php echo (int) $cat_id; ?>][]"
value="<?php echo (int) $seance['id']; ?>"
<?php checked($check_all || in_array((int) $seance['id'], $checked_in_section)); ?>
<?php checked($this->is_item_checked($seance, $check_all, $checked_in_section)); ?>
>
<span class="thalim-nl-post-title"><?php echo esc_html($seance['title']); ?></span>
<span class="thalim-nl-post-date"><?php echo esc_html($this->format_seance_hint($seance)); ?></span>
@@ -318,23 +321,33 @@ class Thalim_NL_Admin_Page {
</div>
<?php endforeach; ?>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<label class="thalim-nl-post-row">
<span class="thalim-nl-drag-handle dashicons dashicons-menu" title="Glisser pour réordonner" aria-hidden="true"></span>
<input
type="checkbox"
name="nl_sections[<?php echo (int) $cat_id; ?>][]"
value="<?php echo (int) $post['id']; ?>"
<?php checked($check_all || in_array((int) $post['id'], $checked_in_section)); ?>
>
<span class="thalim-nl-post-title"><?php echo esc_html($post['title']); ?></span>
<a href="<?php echo esc_url($post['permalink']); ?>" target="_blank" rel="noopener"
class="thalim-nl-post-view" title="Voir sur le site">
<span class="dashicons dashicons-visibility"></span>
</a>
<span class="thalim-nl-post-date"><?php echo esc_html($this->format_post_date_hint($post)); ?></span>
</label>
<?php
// Les annonces dont la date d'événement dépasse le mois sont
// regroupées à part et décochées par défaut : elles restent
// disponibles à la coche sans encombrer la sélection du mois.
$posts_du_mois = [];
$posts_a_venir = [];
foreach ($posts as $post) {
if (!empty($post['is_future'])) {
$posts_a_venir[] = $post;
} else {
$posts_du_mois[] = $post;
}
}
?>
<?php foreach ($posts_du_mois as $post): ?>
<?php echo $this->render_post_row((int) $cat_id, $post, $check_all, $checked_in_section); ?>
<?php endforeach; ?>
<?php if (!empty($posts_a_venir)): ?>
<div class="thalim-nl-future">
<p class="thalim-nl-future-title">
À venir <span>(au-delà du mois — à cocher au besoin)</span>
</p>
<?php foreach ($posts_a_venir as $post): ?>
<?php echo $this->render_post_row((int) $cat_id, $post, $check_all, $checked_in_section); ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</details>
@@ -350,17 +363,59 @@ class Thalim_NL_Admin_Page {
return $html;
}
/**
* Un item est-il coché à l'ouverture ?
* - newsletter déjà enregistrée : la sélection sauvegardée fait foi ;
* - mois neuf : tout est coché sauf ce qui dépasse le mois (`is_future`).
*/
private function is_item_checked(array $item, bool $check_all, array $checked_in_section): bool {
if ($check_all) {
return empty($item['is_future']);
}
// Normalisation : la sélection vient d'un json_decode, tolérer les chaînes.
return in_array((int) $item['id'], array_map('intval', $checked_in_section), true);
}
/**
* Une ligne d'annonce cochable (hors séances, qui ont leur propre gabarit).
*/
private function render_post_row(int $cat_id, array $post, bool $check_all, array $checked_in_section): string {
ob_start();
?>
<label class="thalim-nl-post-row<?php echo !empty($post['is_future']) ? ' is-future' : ''; ?>">
<span class="thalim-nl-drag-handle dashicons dashicons-menu" title="Glisser pour réordonner" aria-hidden="true"></span>
<input
type="checkbox"
name="nl_sections[<?php echo $cat_id; ?>][]"
value="<?php echo (int) $post['id']; ?>"
<?php checked($this->is_item_checked($post, $check_all, $checked_in_section)); ?>
>
<span class="thalim-nl-post-title"><?php echo esc_html($post['title']); ?></span>
<a href="<?php echo esc_url($post['permalink']); ?>" target="_blank" rel="noopener"
class="thalim-nl-post-view" title="Voir sur le site">
<span class="dashicons dashicons-visibility"></span>
</a>
<span class="thalim-nl-post-date"><?php echo esc_html($this->format_post_date_hint($post)); ?></span>
</label>
<?php
return ob_get_clean();
}
/**
* Small date hint shown next to each post title in the checklist.
*/
private function format_post_date_hint(array $post): string {
$candidates = [$post['date_debut'], $post['datetime'], $post['post_date']];
foreach ($candidates as $raw) {
if ($raw && !str_starts_with($raw, '0000-00-00')) {
$ts = strtotime($raw);
if ($ts) {
return date_i18n('j M Y', $ts);
}
// Même libellé que l'export HTML (« Du X au Y », « Jusqu'au X »…) :
// ce que voit le rédacteur dans la liste est ce qui partira dans le mail.
$label = Thalim_NL_Post_Query::event_date_label((int) $post['id'], 'j M Y');
if ($label !== '') {
return $label;
}
$raw = $post['post_date'] ?? '';
if ($raw && !str_starts_with($raw, '0000-00-00')) {
$ts = strtotime($raw);
if ($ts) {
return date_i18n('j M Y', $ts);
}
}
return '';
@@ -605,6 +660,10 @@ class Thalim_NL_Admin_Page {
$unsubscribe_url = get_post_meta($existing->ID, '_newsletter_unsubscribe_url', true) ?: '';
}
if ($existing) {
$month_data = $query->merge_selected_items($month_data, $existing_sections);
}
$html = $this->render_sections_html($month_data, $existing ? $existing_sections : null);
wp_send_json_success([