Files
thalim-plugin-newsletter/includes/class-admin-page.php
2026-05-12 23:34:00 +02:00

551 lines
24 KiB
PHP
Raw Permalink 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',
'lang' => '',
]);
$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_sections); ?>
</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)
// -------------------------------------------------------------------------
public function render_sections_html(array $month_data, array $checked_ids = []): string {
$groups = Thalim_NL_Post_Query::get_eligible_categories();
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 = (array) ($checked_ids[$cat_id] ?? $checked_ids[(string) $cat_id] ?? []);
$has_checked = !empty($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 foreach ($posts as $post): ?>
<label class="thalim-nl-post-row">
<input
type="checkbox"
name="nl_sections[<?php echo (int) $cat_id; ?>][]"
value="<?php echo (int) $post['id']; ?>"
<?php checked(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 if (!empty($post['seances'])): ?>
<ul class="thalim-nl-seances-list">
<?php foreach ($post['seances'] as $seance): ?>
<li class="thalim-nl-seance-item">
<span class="thalim-nl-seance-title"><?php echo esc_html($seance['title']); ?></span>
<span class="thalim-nl-seance-meta">
<?php
$parts = [];
if ($seance['date_debut']) {
$ts = strtotime($seance['date_debut']);
$parts[] = $ts ? date_i18n('j M Y', $ts) : '';
}
if ($seance['heure_de_debut']) {
$time = $seance['heure_de_debut'];
if ($seance['heure_de_fin']) $time .= '' . $seance['heure_de_fin'];
$parts[] = $time;
}
if ($seance['lieu']) {
$parts[] = $seance['lieu'];
}
echo esc_html(implode(' · ', array_filter($parts)));
?>
</span>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php endforeach; ?>
</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 '';
}
// -------------------------------------------------------------------------
// 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);
if (function_exists('pll_set_post_language')) {
pll_set_post_language($post_id, 'fr');
}
return $post_id;
}
/**
* Assign a category using the Pods triple-storage pattern (same as HAL importer).
*/
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
$wpdb->delete(
$wpdb->prefix . 'podsrel',
[
'pod_id' => THALIM_NL_POD_ID_POST,
'field_id' => THALIM_NL_FIELD_ID_CAT,
'item_id' => $post_id,
],
['%d', '%d', '%d']
);
$wpdb->insert(
$wpdb->prefix . 'podsrel',
[
'pod_id' => THALIM_NL_POD_ID_POST,
'field_id' => THALIM_NL_FIELD_ID_CAT,
'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_sections);
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,
'lang' => '', // Polylang: all languages
]);
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));
}
}