Files
thalim-plugin-newsletter/includes/class-admin-page.php

665 lines
30 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Admin Page Class — Tools > Newsletter
*/
if (!defined('ABSPATH')) {
exit;
}
class Thalim_NL_Admin_Page {
// -------------------------------------------------------------------------
// Main render
// -------------------------------------------------------------------------
public function render() {
if (!current_user_can('edit_others_posts')) {
wp_die('Unauthorized');
}
// Determine active month
$year_month = sanitize_text_field($_GET['nl_month'] ?? '');
if (!preg_match('/^\d{4}-\d{2}$/', $year_month)) {
$year_month = date('Y-m');
}
// Handle synchronous save
$save_message = null;
if (isset($_POST['thalim_nl_save']) && wp_verify_nonce($_POST['_wpnonce'] ?? '', 'thalim_nl_save')) {
$save_message = $this->handle_save_newsletter();
// Re-read month after save (may have been changed in form)
$posted_month = sanitize_text_field($_POST['nl_month'] ?? '');
if (preg_match('/^\d{4}-\d{2}$/', $posted_month)) {
$year_month = $posted_month;
}
}
// Load existing newsletter for this month (if any)
$existing = $this->get_newsletter_post_for_month($year_month);
$existing_sections = [];
$intro_content = '';
$conclusion_content = '';
$subscribe_url = '';
$unsubscribe_url = '';
if ($existing) {
$existing_sections = json_decode(get_post_meta($existing->ID, '_newsletter_sections', true), true) ?: [];
$intro_content = get_post_meta($existing->ID, '_newsletter_intro', true) ?: '';
$conclusion_content = get_post_meta($existing->ID, '_newsletter_conclusion', true) ?: '';
$subscribe_url = get_post_meta($existing->ID, '_newsletter_subscribe_url', true) ?: '';
$unsubscribe_url = get_post_meta($existing->ID, '_newsletter_unsubscribe_url', true) ?: '';
}
// Query posts for current month
$query = new Thalim_NL_Post_Query();
$month_data = $query->get_posts_for_month($year_month);
// Past newsletters
$past_newsletters = get_posts([
'post_type' => 'post',
'post_status' => ['draft', 'publish', 'pending'],
'posts_per_page' => -1,
'meta_key' => '_newsletter_month',
'orderby' => 'meta_value',
'order' => 'DESC',
]);
$past_newsletters = array_filter($past_newsletters, function ($p) {
return !empty(get_post_meta($p->ID, '_newsletter_month', true));
});
?>
<div class="wrap thalim-nl-wrap">
<h1>Newsletter THALIM</h1>
<?php if ($save_message): ?>
<div class="notice notice-<?php echo esc_attr($save_message[0]); ?> is-dismissible">
<p><?php echo esc_html($save_message[1]); ?></p>
</div>
<?php endif; ?>
<div class="thalim-nl-layout">
<!-- ============================================================
SECTION 1 — COMPOSE
============================================================ -->
<div class="thalim-nl-compose">
<h2>Composer</h2>
<form method="post" id="thalim-nl-form">
<?php wp_nonce_field('thalim_nl_save'); ?>
<input type="hidden" name="thalim_nl_save" value="1">
<div class="thalim-nl-month-row">
<label for="thalim-nl-month"><strong>Mois :</strong></label>
<input
type="month"
id="thalim-nl-month"
name="nl_month"
value="<?php echo esc_attr($year_month); ?>"
>
<span class="thalim-nl-spinner spinner"></span>
</div>
<div class="thalim-nl-intro">
<label><strong>Introduction :</strong></label>
<?php
wp_editor($intro_content, 'thalim_nl_intro', [
'textarea_name' => 'nl_intro',
'media_buttons' => false,
'tinymce' => [
'toolbar1' => 'bold,italic,bullist,numlist,blockquote,link,unlink',
'toolbar2' => '',
],
'quicktags' => ['buttons' => 'strong,em,ul,ol,li,link,close'],
'editor_height' => 200,
]);
?>
</div>
<div id="thalim-nl-sections-wrap">
<?php echo $this->render_sections_html($month_data, $existing ? $existing_sections : null); ?>
</div>
<div class="thalim-nl-conclusion">
<label><strong>Conclusion :</strong></label>
<?php
wp_editor($conclusion_content, 'thalim_nl_conclusion', [
'textarea_name' => 'nl_conclusion',
'media_buttons' => false,
'tinymce' => [
'toolbar1' => 'bold,italic,bullist,numlist,blockquote,link,unlink',
'toolbar2' => '',
],
'quicktags' => ['buttons' => 'strong,em,ul,ol,li,link,close'],
'editor_height' => 200,
]);
?>
</div>
<div class="thalim-nl-links-row">
<div class="thalim-nl-link-field">
<label for="thalim-nl-subscribe"><strong>Lien d'abonnement :</strong></label>
<input type="url" id="thalim-nl-subscribe" name="nl_subscribe_url"
value="<?php echo esc_attr($subscribe_url); ?>"
placeholder="https://…" class="regular-text">
</div>
<div class="thalim-nl-link-field">
<label for="thalim-nl-unsubscribe"><strong>Lien de désabonnement :</strong></label>
<input type="url" id="thalim-nl-unsubscribe" name="nl_unsubscribe_url"
value="<?php echo esc_attr($unsubscribe_url); ?>"
placeholder="https://…" class="regular-text">
</div>
</div>
<p class="submit">
<button type="submit" class="button button-primary">Enregistrer</button>
</p>
</form>
</div>
<!-- ============================================================
SECTION 2 — PAST NEWSLETTERS
============================================================ -->
<div class="thalim-nl-past">
<h2>Newsletters enregistrées</h2>
<?php if (empty($past_newsletters)): ?>
<p>Aucune newsletter enregistrée.</p>
<?php else: ?>
<table class="widefat thalim-nl-past-table">
<thead>
<tr>
<th>Mois</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($past_newsletters as $nl): ?>
<?php
$nl_month = get_post_meta($nl->ID, '_newsletter_month', true);
$label = $this->format_month_label($nl_month);
$edit_url = get_edit_post_link($nl->ID);
?>
<tr>
<td><?php echo esc_html($label); ?></td>
<td>
<a href="<?php echo esc_url($edit_url); ?>" class="button button-small">Modifier</a>
<button
type="button"
class="button button-small thalim-nl-preview"
data-post-id="<?php echo esc_attr($nl->ID); ?>"
>Prévisualisation</button>
<button
type="button"
class="button button-small thalim-nl-download"
data-post-id="<?php echo esc_attr($nl->ID); ?>"
data-filename="newsletter-THALIM-<?php echo esc_attr($this->format_month_label($nl_month)); ?>.html"
>Télécharger</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div><!-- .thalim-nl-layout -->
</div><!-- .wrap -->
<?php
}
// -------------------------------------------------------------------------
// Sections HTML (shared between initial render and AJAX)
// -------------------------------------------------------------------------
/**
* @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;
foreach ($groups as $parent_id => $group) {
// Collect all cat IDs in this group: parent + children
$cats_in_group = [$parent_id => $group['name']];
foreach ($group['children'] as $cid => $cname) {
$cats_in_group[$cid] = $cname;
}
// Check if any cat in this group has posts
$group_has_posts = false;
foreach ($cats_in_group as $cid => $cname) {
if (!empty($month_data[$cid])) {
$group_has_posts = true;
break;
}
}
if (!$group_has_posts) {
continue;
}
$has_content = true;
?>
<div class="thalim-nl-group">
<h3 class="thalim-nl-group-title"><?php echo esc_html($group['name']); ?></h3>
<?php foreach ($cats_in_group as $cat_id => $label):
$posts = $month_data[$cat_id] ?? [];
if (empty($posts)) {
continue;
}
$count = count($posts);
$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);
}
?>
<details class="thalim-nl-section" <?php if ($has_checked) echo 'open'; ?>>
<summary class="thalim-nl-section-summary">
<?php echo esc_html($label); ?>
<span class="thalim-nl-count">(<?php echo $count; ?>)</span>
</summary>
<div class="thalim-nl-section-body">
<?php if ((int) $cat_id === THALIM_NL_CAT_SEMINAIRES): ?>
<?php
// Seminars: posts are séance items carrying their parent
// seminar. Group them under a (non-selectable) seminar
// header; each séance is an individual checkbox.
$by_seminar = [];
foreach ($posts as $seance) {
$sem_id = (int) $seance['seminar_id'];
if (!isset($by_seminar[$sem_id])) {
$by_seminar[$sem_id] = [
'title' => $seance['seminar_title'],
'permalink' => $seance['seminar_permalink'],
'seances' => [],
];
}
$by_seminar[$sem_id]['seances'][] = $seance;
}
?>
<?php foreach ($by_seminar as $sem): ?>
<div class="thalim-nl-seminar-group">
<div class="thalim-nl-seminar-title">
<span class="thalim-nl-drag-handle dashicons dashicons-menu" title="Glisser pour réordonner les séminaires" aria-hidden="true"></span>
<span class="thalim-nl-seminar-name"><?php echo esc_html($sem['title']); ?></span>
<a href="<?php echo esc_url($sem['permalink']); ?>" target="_blank" rel="noopener"
class="thalim-nl-post-view" title="Voir sur le site">
<span class="dashicons dashicons-visibility"></span>
</a>
</div>
<?php foreach ($sem['seances'] as $seance): ?>
<label class="thalim-nl-post-row thalim-nl-seance-row">
<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)); ?>
>
<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>
</label>
<?php endforeach; ?>
</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 endforeach; ?>
<?php endif; ?>
</div>
</details>
<?php endforeach; ?>
</div>
<?php
}
$html = ob_get_clean();
if (!$has_content) {
$html = '<p class="thalim-nl-empty">Aucun contenu trouvé pour ce mois.</p>';
}
return $html;
}
/**
* 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);
}
}
}
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
// -------------------------------------------------------------------------
private function handle_save_newsletter(): array {
$year_month = sanitize_text_field($_POST['nl_month'] ?? '');
if (!preg_match('/^\d{4}-\d{2}$/', $year_month)) {
return ['error', 'Mois invalide.'];
}
$intro = wp_kses_post(wp_unslash($_POST['nl_intro'] ?? ''));
$conclusion = wp_kses_post(wp_unslash($_POST['nl_conclusion'] ?? ''));
$subscribe_url = esc_url_raw(wp_unslash($_POST['nl_subscribe_url'] ?? ''));
$unsubscribe_url = esc_url_raw(wp_unslash($_POST['nl_unsubscribe_url'] ?? ''));
// Build sections: cat_id => [post_id, ...]
$raw_sections = $_POST['nl_sections'] ?? [];
$sections = [];
foreach ($raw_sections as $cat_id => $post_ids) {
$cat_id = (int) $cat_id;
$clean_ids = array_map('intval', (array) $post_ids);
$clean_ids = array_filter($clean_ids);
if (!empty($clean_ids)) {
$sections[$cat_id] = array_values($clean_ids);
}
}
$post_id = $this->save_newsletter_post($year_month, $intro, $conclusion, $sections, $subscribe_url, $unsubscribe_url);
if (is_wp_error($post_id)) {
return ['error', $post_id->get_error_message()];
}
$label = $this->format_month_label($year_month);
return ['success', "Newsletter «\u{00A0}{$label}\u{00A0}» enregistrée et publiée (ID\u{00A0}: {$post_id})."];
}
private function save_newsletter_post(string $year_month, string $intro, string $conclusion, array $sections, string $subscribe_url = '', string $unsubscribe_url = ''): int|WP_Error {
global $wpdb;
$label = 'Newsletter — ' . $this->format_month_label($year_month);
$existing = $this->get_newsletter_post_for_month($year_month);
// First pass: insert/update with placeholder content (meta must exist before exporter runs)
$post_args = [
'post_title' => $label,
'post_content' => '',
'post_status' => 'publish',
'post_type' => 'post',
];
if ($existing) {
$post_args['ID'] = $existing->ID;
$post_id = wp_update_post($post_args, true);
} else {
$post_id = wp_insert_post($post_args, true);
}
if (is_wp_error($post_id)) {
return $post_id;
}
// Save all meta so the exporter can read them
update_post_meta($post_id, '_newsletter_month', $year_month);
update_post_meta($post_id, '_newsletter_intro', $intro);
update_post_meta($post_id, '_newsletter_conclusion', $conclusion);
update_post_meta($post_id, '_newsletter_sections', wp_json_encode($sections));
update_post_meta($post_id, '_newsletter_subscribe_url', $subscribe_url);
update_post_meta($post_id, '_newsletter_unsubscribe_url', $unsubscribe_url);
// Generate full rendered HTML and store as post_content
$exporter = new Thalim_NL_HTML_Exporter();
$full_html = $exporter->generate(get_post($post_id));
wp_update_post(['ID' => $post_id, 'post_content' => $full_html]);
$this->do_triple_storage_category($post_id, THALIM_NL_CAT_NEWSLETTER);
return $post_id;
}
/**
* Résout l'ID du pod `post` et celui de son champ `categorie` par NOM
* dans wp_posts (post_type _pods_pod / _pods_field) — les IDs en dur ne
* survivent pas à une réimportation de base.
*
* @return array{0:int,1:int} [pod_id, field_id] (0 si introuvable)
*/
private function resolve_pods_categorie_ids(): array {
static $ids = null;
if ($ids === null) {
global $wpdb;
$pod_id = (int) $wpdb->get_var(
"SELECT ID FROM {$wpdb->posts}
WHERE post_type = '_pods_pod' AND post_name = 'post' LIMIT 1"
);
$field_id = $pod_id ? (int) $wpdb->get_var($wpdb->prepare(
"SELECT ID FROM {$wpdb->posts}
WHERE post_type = '_pods_field' AND post_name = 'categorie' AND post_parent = %d
LIMIT 1",
$pod_id
)) : 0;
$ids = [$pod_id, $field_id];
}
return $ids;
}
/**
* Assign a category using the Pods quadruple-storage pattern (same as HAL importer).
*
* ⚠ DÉPENDANCE DURE à Pods 3.x : reproduit le stockage interne de Pods
* (postmeta + _pods_* sérialisé + wp_podsrel + catégorie WP native).
*/
private function do_triple_storage_category(int $post_id, int $cat_id): void {
global $wpdb;
// 1. Native WP category assignment
wp_set_post_categories($post_id, [$cat_id]);
// 2. Pods postmeta: single integer
update_post_meta($post_id, 'categorie', $cat_id);
// 3. Pods _pods_ meta: serialized array
update_post_meta($post_id, '_pods_categorie', [$cat_id]);
// 4. wp_podsrel row
[$pod_id, $field_id] = $this->resolve_pods_categorie_ids();
if (!$pod_id || !$field_id) {
error_log(sprintf(
'[thalim-newsletter] Pod/champ Pods "categorie" introuvable — wp_podsrel non écrit pour le post %d',
$post_id
));
return;
}
$wpdb->delete(
$wpdb->prefix . 'podsrel',
[
'pod_id' => $pod_id,
'field_id' => $field_id,
'item_id' => $post_id,
],
['%d', '%d', '%d']
);
$wpdb->insert(
$wpdb->prefix . 'podsrel',
[
'pod_id' => $pod_id,
'field_id' => $field_id,
'item_id' => $post_id,
'related_pod_id' => 0,
'related_field_id' => 0,
'related_item_id' => $cat_id,
'weight' => 0,
],
['%d', '%d', '%d', '%d', '%d', '%d', '%d']
);
}
// -------------------------------------------------------------------------
// AJAX handlers
// -------------------------------------------------------------------------
public function handle_ajax_load_month(): void {
check_ajax_referer('thalim_newsletter_ajax', '_wpnonce');
if (!current_user_can('edit_others_posts')) {
wp_send_json_error('Unauthorized', 403);
}
$year_month = sanitize_text_field($_POST['month'] ?? '');
if (!preg_match('/^\d{4}-\d{2}$/', $year_month)) {
wp_send_json_error('Invalid month format');
}
$query = new Thalim_NL_Post_Query();
$month_data = $query->get_posts_for_month($year_month);
$existing = $this->get_newsletter_post_for_month($year_month);
$existing_sections = [];
$intro_content = '';
$conclusion_content = '';
$subscribe_url = '';
$unsubscribe_url = '';
$has_existing = false;
if ($existing) {
$has_existing = true;
$existing_sections = json_decode(get_post_meta($existing->ID, '_newsletter_sections', true), true) ?: [];
$intro_content = get_post_meta($existing->ID, '_newsletter_intro', true) ?: '';
$conclusion_content = get_post_meta($existing->ID, '_newsletter_conclusion', true) ?: '';
$subscribe_url = get_post_meta($existing->ID, '_newsletter_subscribe_url', true) ?: '';
$unsubscribe_url = get_post_meta($existing->ID, '_newsletter_unsubscribe_url', true) ?: '';
}
$html = $this->render_sections_html($month_data, $existing ? $existing_sections : null);
wp_send_json_success([
'html' => $html,
'intro' => $intro_content,
'conclusion' => $conclusion_content,
'subscribe_url' => $subscribe_url,
'unsubscribe_url' => $unsubscribe_url,
'has_existing' => $has_existing,
]);
}
public function handle_ajax_export_html(): void {
check_ajax_referer('thalim_newsletter_ajax', '_wpnonce');
if (!current_user_can('edit_others_posts')) {
wp_send_json_error('Unauthorized', 403);
}
$post_id = (int) ($_POST['post_id'] ?? 0);
if (!$post_id) {
wp_send_json_error('Missing post_id');
}
$post = get_post($post_id);
if (!$post) {
wp_send_json_error('Post not found');
}
// post_content already holds the full rendered HTML (set on save)
wp_send_json_success(['html' => $post->post_content]);
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
public function get_newsletter_post_for_month(string $year_month): ?WP_Post {
$posts = get_posts([
'post_type' => 'post',
'post_status' => ['draft', 'publish', 'pending'],
'posts_per_page' => 1,
'meta_key' => '_newsletter_month',
'meta_value' => $year_month,
]);
return $posts[0] ?? null;
}
private function format_month_label(string $year_month): string {
$ts = strtotime($year_month . '-01');
if (!$ts) {
return $year_month;
}
// e.g. "Mars 2026"
return ucfirst(date_i18n('F Y', $ts));
}
}