2040 lines
90 KiB
PHP
2040 lines
90 KiB
PHP
<?php
|
||
/**
|
||
* WP Multi – Modul: Gast-Autoren
|
||
*
|
||
* Automatisch aus wp-multi.php ausgelagert. Wird über den Loader in
|
||
* wp-multi.php eingebunden (nur wenn das WP Multi Toolkit aktiv ist).
|
||
*/
|
||
|
||
if (!defined('ABSPATH')) { exit; }
|
||
|
||
/*
|
||
* Gast Autoren
|
||
*/
|
||
|
||
// Gast-Autor Eingabefeld in der Sidebar im Admin-Bereich hinzufügen
|
||
function wp_multi_add_guest_author_field() {
|
||
add_meta_box(
|
||
'guest_author_meta_box',
|
||
__('Gast-Autor', 'wp-multi'),
|
||
'wp_multi_guest_author_field',
|
||
['post', 'page', 'dein_custom_post_type'],
|
||
'side',
|
||
'high'
|
||
);
|
||
}
|
||
add_action('add_meta_boxes', 'wp_multi_add_guest_author_field');
|
||
|
||
// Callback-Funktion, die das Eingabefeld anzeigt
|
||
function wp_multi_guest_author_field($post) {
|
||
$guest_author = get_post_meta($post->ID, '_guest_author', true);
|
||
wp_nonce_field('wp_multi_guest_author_nonce', 'wp_multi_guest_author_nonce_field');
|
||
?>
|
||
<label for="guest_author"><?php _e('Gast-Autor Name:', 'wp-multi'); ?></label>
|
||
<input type="text" id="guest_author" name="guest_author" value="<?php echo esc_attr($guest_author); ?>" class="widefat" />
|
||
<?php
|
||
}
|
||
|
||
// Speichern der Gast-Autor Daten
|
||
function wp_multi_save_guest_author_meta($post_id) {
|
||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
|
||
if (!isset($_POST['wp_multi_guest_author_nonce_field']) || !wp_verify_nonce($_POST['wp_multi_guest_author_nonce_field'], 'wp_multi_guest_author_nonce')) return;
|
||
if (!current_user_can('edit_post', $post_id)) return;
|
||
if (isset($_POST['guest_author'])) {
|
||
$guest_author = sanitize_text_field(wp_unslash($_POST['guest_author']));
|
||
update_post_meta($post_id, '_guest_author', $guest_author);
|
||
} else {
|
||
delete_post_meta($post_id, '_guest_author');
|
||
}
|
||
}
|
||
add_action('save_post', 'wp_multi_save_guest_author_meta');
|
||
|
||
// Gast-Autor anzeigen anstelle des regulären Autors im Frontend
|
||
function wp_multi_display_guest_author($author_name) {
|
||
if ((is_single() || is_archive() || is_home()) && !is_admin()) {
|
||
$post = get_post();
|
||
if ($post) {
|
||
$guest_author = get_post_meta($post->ID, '_guest_author', true);
|
||
if (!empty($guest_author)) {
|
||
$author_name = $guest_author;
|
||
}
|
||
}
|
||
}
|
||
return $author_name;
|
||
}
|
||
add_filter('the_author', 'wp_multi_display_guest_author');
|
||
|
||
// Anzeige des Gast-Autors in der Beitragsübersicht (Backend)
|
||
function wp_multi_add_guest_author_column($columns) {
|
||
if (isset($columns['author'])) {
|
||
$columns['guest_author'] = __('Gast-Autor', 'wp-multi');
|
||
}
|
||
return $columns;
|
||
}
|
||
add_filter('manage_posts_columns', 'wp_multi_add_guest_author_column');
|
||
|
||
function wp_multi_display_guest_author_column($column_name, $post_id) {
|
||
if ($column_name == 'guest_author') {
|
||
$guest_author = get_post_meta($post_id, '_guest_author', true);
|
||
echo !empty($guest_author) ? esc_html($guest_author) : __('Kein Gast-Autor', 'wp-multi');
|
||
}
|
||
}
|
||
add_action('manage_posts_custom_column', 'wp_multi_display_guest_author_column', 10, 2);
|
||
|
||
// Der Gast-Autoren-Menüpunkt wird zentral in wp_multi_register_admin_menus() registriert.
|
||
|
||
/**
|
||
* Ermittelt den Aktivitätsstatus eines Gast-Autors anhand seiner letzten Geschichte.
|
||
*
|
||
* 🟢 Aktiv – letzte Geschichte innerhalb der letzten 60 Tage
|
||
* 🟡 Inaktiv – letzte Geschichte 61–180 Tage her
|
||
* 🔴 Stark inaktiv – letzte Geschichte länger als 180 Tage her
|
||
* ⭐ Ehemaliger Vielschreiber – mindestens 10 Geschichten, aber seit über 180 Tagen inaktiv
|
||
*
|
||
* Der Rang steigt mit dem "Rückhol-Potenzial": absteigend sortiert stehen die
|
||
* ehemaligen Vielschreiber ganz oben, die aktiven Autoren ganz unten.
|
||
*
|
||
* @param string $last_post_date Datum der letzten Geschichte (MySQL-Format, Seitenzeit).
|
||
* @param int $post_count Anzahl veröffentlichter Geschichten.
|
||
* @return array|null Array mit key, emoji, label, class, rank und days – oder null ohne Datum.
|
||
*/
|
||
function wp_multi_get_guest_author_activity($last_post_date, $post_count = 0) {
|
||
if (empty($last_post_date)) {
|
||
return null;
|
||
}
|
||
|
||
// Über GMT rechnen, damit die Zeitzone der Seite das Ergebnis nicht verschiebt
|
||
$last_ts = strtotime(get_gmt_from_date($last_post_date));
|
||
if (!$last_ts) {
|
||
return null;
|
||
}
|
||
|
||
$days = (int) floor((time() - $last_ts) / DAY_IN_SECONDS);
|
||
|
||
if ($days <= 60) {
|
||
$status = ['key' => 'active', 'emoji' => '🟢', 'label' => __('Aktiv', 'wp-multi'), 'class' => 'wpm-badge--success', 'rank' => 1];
|
||
} elseif ($days <= 180) {
|
||
$status = ['key' => 'inactive', 'emoji' => '🟡', 'label' => __('Inaktiv', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 2];
|
||
} elseif ((int) $post_count >= 10) {
|
||
$status = ['key' => 'former_prolific', 'emoji' => '⭐', 'label' => __('Ehemaliger Vielschreiber', 'wp-multi'), 'class' => 'wpm-badge--star', 'rank' => 4];
|
||
} else {
|
||
$status = ['key' => 'dormant', 'emoji' => '🔴', 'label' => __('Stark inaktiv', 'wp-multi'), 'class' => 'wpm-badge--danger', 'rank' => 3];
|
||
}
|
||
|
||
$status['days'] = $days;
|
||
|
||
return $status;
|
||
}
|
||
|
||
/**
|
||
* Liefert die Beschriftungen aller Aktivitätsstatus für Filter und Legende.
|
||
*/
|
||
function wp_multi_get_guest_author_status_labels() {
|
||
return [
|
||
'former_prolific' => '⭐ ' . __('Ehemaliger Vielschreiber', 'wp-multi'),
|
||
'dormant' => '🔴 ' . __('Stark inaktiv', 'wp-multi'),
|
||
'inactive' => '🟡 ' . __('Inaktiv', 'wp-multi'),
|
||
'active' => '🟢 ' . __('Aktiv', 'wp-multi'),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Liefert die Rangleiter der Autoren, absteigend nach Schwelle.
|
||
*
|
||
* 🎉 Newcomer ab 5 – ⭐ Vielschreiber ab 10 – 🏆 Etablierter Autor ab 25 –
|
||
* 👑 Star-Autor ab 50 – 💎 Legende ab 100 Geschichten.
|
||
*
|
||
* @return array Zuordnung "Schwelle => [emoji, Bezeichnung]".
|
||
*/
|
||
function wp_multi_get_guest_author_ranks() {
|
||
return [
|
||
100 => ['💎', __('Legende', 'wp-multi')],
|
||
50 => ['👑', __('Star-Autor', 'wp-multi')],
|
||
25 => ['🏆', __('Etablierter Autor', 'wp-multi')],
|
||
10 => ['⭐', __('Vielschreiber', 'wp-multi')],
|
||
5 => ['🎉', __('Newcomer', 'wp-multi')],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Ermittelt den höchsten erreichten Rang eines Autors.
|
||
*
|
||
* Es wird immer nur der höchste erreichte Rang zurückgegeben, nie mehrere.
|
||
*
|
||
* @param int $post_count Anzahl veröffentlichter Geschichten.
|
||
* @return array|null Array mit threshold, emoji, label und label_full – oder null ohne Rang.
|
||
*/
|
||
function wp_multi_get_guest_author_milestone($post_count) {
|
||
$post_count = (int) $post_count;
|
||
|
||
foreach (wp_multi_get_guest_author_ranks() as $threshold => $rank) {
|
||
if ($post_count >= $threshold) {
|
||
return [
|
||
'threshold' => $threshold,
|
||
'emoji' => $rank[0],
|
||
'label' => $rank[1],
|
||
/* translators: 1: Rangbezeichnung, 2: Schwelle des Rangs */
|
||
'label_full' => sprintf(__('%1$s (ab %2$d Geschichten)', 'wp-multi'), $rank[1], $threshold),
|
||
];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Ermittelt den nächsten noch nicht erreichten Rang.
|
||
*
|
||
* @param int $post_count Anzahl veröffentlichter Geschichten.
|
||
* @return array|null Array mit threshold, emoji, label und missing – oder null nach dem letzten Rang.
|
||
*/
|
||
function wp_multi_get_guest_author_next_milestone($post_count) {
|
||
$post_count = (int) $post_count;
|
||
|
||
// Aufsteigend, damit der nächste offene Rang zuerst greift
|
||
$ranks = array_reverse(wp_multi_get_guest_author_ranks(), true);
|
||
|
||
foreach ($ranks as $threshold => $rank) {
|
||
if ($post_count < $threshold) {
|
||
return [
|
||
'threshold' => $threshold,
|
||
'emoji' => $rank[0],
|
||
'label' => $rank[1],
|
||
'missing' => $threshold - $post_count,
|
||
];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Zählt die veröffentlichten Beiträge eines Gast-Autors.
|
||
*
|
||
* Nutzt dieselbe Regel wie die Autoren-Übersicht, damit Übersicht und
|
||
* Autorenprofil nie unterschiedliche Zahlen anzeigen.
|
||
*
|
||
* @param string $guest_author Name des Gast-Autors.
|
||
* @return int
|
||
*/
|
||
function wp_multi_get_guest_author_post_count($guest_author) {
|
||
if (empty($guest_author)) {
|
||
return 0;
|
||
}
|
||
|
||
global $wpdb;
|
||
|
||
return (int) $wpdb->get_var($wpdb->prepare("
|
||
SELECT COUNT(*)
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||
WHERE pm.meta_key = '_guest_author'
|
||
AND pm.meta_value = %s
|
||
AND p.post_status = 'publish'
|
||
", $guest_author));
|
||
}
|
||
|
||
/**
|
||
* Liefert die Aufrufe je Gast-Autor aus der Analytics-Tabelle.
|
||
*
|
||
* Gezählt werden alle Besucher (siehe wp_multi_post_view_activity()); Bots sind
|
||
* ausgenommen und ein Besucher zählt je Beitrag höchstens einmal pro halbe Stunde.
|
||
*
|
||
* @return array|null Zuordnung "Autorenname => Aufrufe" oder null ohne Analytics-Tabelle.
|
||
*/
|
||
function wp_multi_get_guest_author_views() {
|
||
if (!defined('WP_MULTI_ANALYTICS_TABLE')) {
|
||
return null;
|
||
}
|
||
|
||
global $wpdb;
|
||
$table = WP_MULTI_ANALYTICS_TABLE;
|
||
|
||
if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) {
|
||
return null;
|
||
}
|
||
|
||
$results = $wpdb->get_results("
|
||
SELECT pm.meta_value AS guest_author, COUNT(*) AS views
|
||
FROM {$table} a
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author'
|
||
WHERE a.action = 'view'
|
||
AND pm.meta_value != ''
|
||
GROUP BY pm.meta_value
|
||
");
|
||
|
||
$views = [];
|
||
foreach ((array) $results as $row) {
|
||
$views[$row->guest_author] = (int) $row->views;
|
||
}
|
||
|
||
return $views;
|
||
}
|
||
|
||
/**
|
||
* Liefert die Reiter der Gast-Autoren-Seite.
|
||
*/
|
||
function wp_multi_get_guest_author_tabs() {
|
||
return [
|
||
'authors' => __('Gast-Autoren', 'wp-multi'),
|
||
'series' => __('Liegengebliebene Serien', 'wp-multi'),
|
||
'views' => __('Aufrufe je Geschichte', 'wp-multi'),
|
||
];
|
||
}
|
||
|
||
// Callback-Funktion für die Gast-Autor-Übersicht
|
||
function wp_multi_guest_author_overview_page() {
|
||
$tabs = wp_multi_get_guest_author_tabs();
|
||
$current_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'authors';
|
||
if (!isset($tabs[$current_tab])) {
|
||
$current_tab = 'authors';
|
||
}
|
||
|
||
$base_url = add_query_arg(['page' => 'guest_author_overview'], admin_url('admin.php'));
|
||
|
||
wpmt_admin_page_open(
|
||
__('Gast-Autoren', 'wp-multi'),
|
||
__('Alle Gast-Autoren mit Anzahl ihrer veröffentlichten Beiträge und ihrem Aktivitätsstatus', 'wp-multi')
|
||
);
|
||
?>
|
||
<h2 class="nav-tab-wrapper wpm-tabs">
|
||
<?php foreach ($tabs as $tab_key => $tab_label) : ?>
|
||
<a href="<?php echo esc_url(add_query_arg('tab', $tab_key, $base_url)); ?>"
|
||
class="nav-tab<?php echo ($current_tab === $tab_key) ? ' nav-tab-active' : ''; ?>">
|
||
<?php echo esc_html($tab_label); ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</h2>
|
||
<?php
|
||
|
||
$detail_author = isset($_GET['guest_author']) ? sanitize_text_field(wp_unslash($_GET['guest_author'])) : '';
|
||
|
||
if ($detail_author !== '') {
|
||
wp_multi_render_guest_author_detail($base_url, $detail_author, $current_tab);
|
||
} elseif ($current_tab === 'series') {
|
||
// Ein Fehler in der Serien-Erkennung darf den Rest der Seite nicht mitreißen
|
||
try {
|
||
wp_multi_render_stalled_series($base_url);
|
||
} catch (Throwable $e) {
|
||
wpmt_admin_card_open(__('Liegengebliebene Serien', 'wp-multi'));
|
||
echo '<p class="wpm-muted">' . esc_html__('Die Serien-Erkennung konnte nicht ausgeführt werden.', 'wp-multi') . '</p>';
|
||
wpmt_admin_card_close();
|
||
}
|
||
} elseif ($current_tab === 'views') {
|
||
wp_multi_render_post_views_table($base_url);
|
||
} else {
|
||
wp_multi_render_guest_author_table($base_url);
|
||
}
|
||
|
||
wpmt_admin_page_close();
|
||
}
|
||
|
||
/**
|
||
* Rendert die Aufrufe einzelner Geschichten mit dem Datum des letzten Aufrufs.
|
||
*
|
||
* @param string $base_url Basis-URL der Übersichtsseite.
|
||
*/
|
||
function wp_multi_render_post_views_table($base_url) {
|
||
wpmt_admin_card_open(__('Aufrufe je Geschichte', 'wp-multi'));
|
||
|
||
if (!defined('WP_MULTI_ANALYTICS_TABLE')) {
|
||
echo '<p class="wpm-muted">' . esc_html__('Das Analytics-Modul ist nicht aktiv, deshalb liegen keine Aufrufe vor.', 'wp-multi') . '</p>';
|
||
wpmt_admin_card_close();
|
||
return;
|
||
}
|
||
|
||
global $wpdb;
|
||
$table = WP_MULTI_ANALYTICS_TABLE;
|
||
|
||
if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) {
|
||
echo '<p class="wpm-muted">' . esc_html__('Die Analytics-Tabelle existiert noch nicht. Sie wird bei der Aktivierung des Plugins angelegt.', 'wp-multi') . '</p>';
|
||
wpmt_admin_card_close();
|
||
return;
|
||
}
|
||
|
||
// Sortierparameter aus URL lesen (Whitelist gegen SQL-Injection)
|
||
$valid_orderby = ['views' => 'views', 'last_view' => 'last_view', 'title' => 'p.post_title'];
|
||
// Standard: zuletzt gelesene Geschichten oben
|
||
$orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'last_view';
|
||
if (!isset($valid_orderby[$orderby])) {
|
||
$orderby = 'last_view';
|
||
}
|
||
$order = isset($_GET['order']) && in_array(strtoupper($_GET['order']), ['ASC', 'DESC']) ? strtoupper($_GET['order']) : 'DESC';
|
||
$sql_orderby = $valid_orderby[$orderby];
|
||
|
||
// Lange Listen bremsen die Seite aus – Anzahl per Filter anpassbar
|
||
$limit = (int) apply_filters('wp_multi_post_views_limit', 100);
|
||
$sort_url = add_query_arg('tab', 'views', $base_url);
|
||
|
||
$posts = $wpdb->get_results($wpdb->prepare("
|
||
SELECT a.post_id, p.post_title, p.post_status,
|
||
COUNT(*) AS views,
|
||
MAX(a.timestamp) AS last_view,
|
||
pm.meta_value AS guest_author
|
||
FROM {$table} a
|
||
INNER JOIN {$wpdb->posts} p ON p.ID = a.post_id
|
||
LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author'
|
||
WHERE a.action = 'view'
|
||
GROUP BY a.post_id, p.post_title, p.post_status, pm.meta_value
|
||
ORDER BY {$sql_orderby} {$order}, p.post_title ASC
|
||
LIMIT %d
|
||
", $limit));
|
||
|
||
$total_views = 0;
|
||
foreach ((array) $posts as $row) {
|
||
$total_views += (int) $row->views;
|
||
}
|
||
|
||
$date_format = get_option('date_format') . ' ' . get_option('time_format');
|
||
?>
|
||
<p class="wpm-muted">
|
||
<?php
|
||
printf(
|
||
/* translators: 1: Anzahl Geschichten, 2: Summe der Aufrufe */
|
||
esc_html__('%1$s Geschichten mit insgesamt %2$s Aufrufen. Gezählt werden alle Besucher ohne Bots, je Besucher einmal pro halbe Stunde.', 'wp-multi'),
|
||
esc_html(number_format_i18n(count((array) $posts))),
|
||
esc_html(number_format_i18n($total_views))
|
||
);
|
||
?>
|
||
</p>
|
||
<div class="wpm-table-wrap">
|
||
<table class="wpm-table">
|
||
<thead>
|
||
<tr>
|
||
<th><?php wp_multi_render_sort_link(__('Geschichte', 'wp-multi'), 'title', $orderby, $order, $sort_url, 'ASC'); ?></th>
|
||
<th><?php _e('Autor', 'wp-multi'); ?></th>
|
||
<th><?php wp_multi_render_sort_link(__('Aufrufe', 'wp-multi'), 'views', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<th><?php wp_multi_render_sort_link(__('Letzter Aufruf', 'wp-multi'), 'last_view', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if ($posts) : ?>
|
||
<?php foreach ($posts as $row) : ?>
|
||
<tr>
|
||
<td>
|
||
<a href="<?php echo esc_url(get_permalink((int) $row->post_id)); ?>" target="_blank" rel="noopener">
|
||
<?php echo esc_html($row->post_title !== '' ? $row->post_title : __('(ohne Titel)', 'wp-multi')); ?>
|
||
</a>
|
||
<?php if ($row->post_status !== 'publish') : ?>
|
||
<span class="wpm-muted">(<?php echo esc_html($row->post_status); ?>)</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?php wp_multi_render_guest_author_link($row->guest_author, $base_url, 'views'); ?></td>
|
||
<td><span class="wpm-badge"><?php echo esc_html(number_format_i18n($row->views)); ?></span></td>
|
||
<td>
|
||
<?php
|
||
// Die Analytics-Tabelle speichert in GMT, deshalb vor der Ausgabe umrechnen
|
||
echo esc_html(mysql2date($date_format, get_date_from_gmt($row->last_view)));
|
||
?>
|
||
<span class="wpm-muted">(<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime($row->last_view)))); ?>)</span>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else : ?>
|
||
<tr><td colspan="4"><?php _e('Noch keine Aufrufe erfasst.', 'wp-multi'); ?></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php if (count((array) $posts) >= $limit) : ?>
|
||
<p class="wpm-muted">
|
||
<?php
|
||
/* translators: %d: Anzahl der angezeigten Geschichten */
|
||
printf(esc_html__('Angezeigt werden die ersten %d Einträge dieser Sortierung.', 'wp-multi'), $limit);
|
||
?>
|
||
</p>
|
||
<?php endif; ?>
|
||
<?php
|
||
wpmt_admin_card_close();
|
||
}
|
||
|
||
/**
|
||
* Liefert Serien-Kennzahlen je Gast-Autor.
|
||
*
|
||
* Gezählt werden die Teile, die der Autor selbst zu einer Serie beigetragen hat –
|
||
* nicht die Gesamtlänge der Serie. Wer zu einer fremden Serie einen Teil beisteuert,
|
||
* gilt deshalb nicht als Serienautor.
|
||
*
|
||
* @return array|null Zuordnung "Name => ['series_count' => int, 'max_parts' => int]" oder null ohne Serien.
|
||
*/
|
||
function wp_multi_get_guest_author_series_stats() {
|
||
$source = wp_multi_detect_series_source();
|
||
|
||
if (!$source) {
|
||
return null;
|
||
}
|
||
|
||
global $wpdb;
|
||
$parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : '';
|
||
|
||
$results = $wpdb->get_results($wpdb->prepare("
|
||
SELECT x.guest_author,
|
||
SUM(CASE WHEN x.parts >= 2 THEN 1 ELSE 0 END) AS series_count,
|
||
MAX(x.parts) AS max_parts
|
||
FROM (
|
||
SELECT pm.meta_value AS guest_author, tt.term_id, COUNT(DISTINCT p.ID) AS parts
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != ''
|
||
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 = %s {$parent_sql}
|
||
WHERE p.post_status = 'publish'
|
||
GROUP BY pm.meta_value, tt.term_id
|
||
) x
|
||
GROUP BY x.guest_author
|
||
", $source['taxonomy']));
|
||
|
||
$stats = [];
|
||
foreach ((array) $results as $row) {
|
||
$stats[$row->guest_author] = [
|
||
'series_count' => (int) $row->series_count,
|
||
'max_parts' => (int) $row->max_parts,
|
||
];
|
||
}
|
||
|
||
return $stats;
|
||
}
|
||
|
||
/**
|
||
* Durchschnittliche Aufrufe je Geschichte über alle Gast-Autoren hinweg.
|
||
*
|
||
* Maßstab für "gute Aufrufe" in der Reaktivierungsbewertung. Wird einmal je
|
||
* Seitenaufruf berechnet, damit Übersicht und Detailansicht identisch bewerten.
|
||
*
|
||
* @return float
|
||
*/
|
||
function wp_multi_get_average_guest_author_views() {
|
||
static $average = null;
|
||
|
||
if ($average !== null) {
|
||
return $average;
|
||
}
|
||
|
||
$table = wp_multi_get_analytics_table();
|
||
|
||
if (!$table) {
|
||
$average = 0.0;
|
||
return $average;
|
||
}
|
||
|
||
global $wpdb;
|
||
|
||
$views = (int) $wpdb->get_var("
|
||
SELECT COUNT(*)
|
||
FROM {$table} a
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author' AND pm.meta_value != ''
|
||
WHERE a.action = 'view'
|
||
");
|
||
|
||
$posts = (int) $wpdb->get_var("
|
||
SELECT COUNT(*)
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != ''
|
||
WHERE p.post_status = 'publish'
|
||
");
|
||
|
||
$average = $posts ? ($views / $posts) : 0.0;
|
||
|
||
return $average;
|
||
}
|
||
|
||
/**
|
||
* Bewertet, wie lohnend es ist, einen Autor zurückzuholen.
|
||
*
|
||
* 🔥 Sehr interessant – ab 10 Geschichten, mindestens ein halbes Jahr still,
|
||
* mit einer eigenen Serie von mindestens 3 Teilen
|
||
* ⭐ Interessant – ab 5 Geschichten, mindestens ein halbes Jahr still,
|
||
* und überdurchschnittlich gelesen
|
||
* 🟡 Beobachten – ab 3 Geschichten und mindestens ein halbes Jahr still
|
||
* ⚪ Keine Priorität – 1–2 Geschichten, lange still
|
||
* – Kein Bedarf – schreibt noch
|
||
*
|
||
* @param array $data Kennzahlen des Autors (post_count, days, views, series_count, max_parts).
|
||
* @param float $avg_views Durchschnittliche Aufrufe je Geschichte auf der ganzen Seite.
|
||
* @return array Array mit key, emoji, label, class und rank.
|
||
*/
|
||
function wp_multi_get_guest_author_reactivation($data, $avg_views = 0) {
|
||
$post_count = isset($data['post_count']) ? (int) $data['post_count'] : 0;
|
||
$days = isset($data['days']) ? (int) $data['days'] : 0;
|
||
$views = isset($data['views']) ? (int) $data['views'] : 0;
|
||
$series_count = isset($data['series_count']) ? (int) $data['series_count'] : 0;
|
||
$max_parts = isset($data['max_parts']) ? (int) $data['max_parts'] : 0;
|
||
|
||
$min_days = (int) apply_filters('wp_multi_reactivation_min_days', 180);
|
||
|
||
// Wer noch schreibt, muss nicht zurückgeholt werden
|
||
if ($days < $min_days) {
|
||
return ['key' => 'current', 'emoji' => '–', 'label' => __('Kein Bedarf', 'wp-multi'), 'class' => '', 'rank' => 0];
|
||
}
|
||
|
||
if ($post_count >= 10 && $series_count > 0 && $max_parts >= 3) {
|
||
return ['key' => 'hot', 'emoji' => '🔥', 'label' => __('Sehr interessant', 'wp-multi'), 'class' => 'wpm-badge--danger', 'rank' => 4];
|
||
}
|
||
|
||
// "Mehrere Geschichten mit guten Aufrufen": im Schnitt deutlich über dem
|
||
// Seitendurchschnitt gelesen. Ohne Analytics-Daten greift die Regel nicht.
|
||
$good_views_factor = (float) apply_filters('wp_multi_reactivation_views_factor', 1.5);
|
||
$has_good_views = ($avg_views > 0 && $post_count > 0 && ($views / $post_count) >= ($avg_views * $good_views_factor));
|
||
|
||
if ($post_count >= 5 && $has_good_views) {
|
||
return ['key' => 'interesting', 'emoji' => '⭐', 'label' => __('Interessant', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 3];
|
||
}
|
||
|
||
if ($post_count >= 3) {
|
||
return ['key' => 'watch', 'emoji' => '🟡', 'label' => __('Beobachten', 'wp-multi'), 'class' => 'wpm-badge--warning', 'rank' => 2];
|
||
}
|
||
|
||
return ['key' => 'low', 'emoji' => '⚪', 'label' => __('Keine Priorität', 'wp-multi'), 'class' => '', 'rank' => 1];
|
||
}
|
||
|
||
/**
|
||
* Liefert die Beschriftungen aller Reaktivierungsstufen für den Filter.
|
||
*/
|
||
function wp_multi_get_reactivation_labels() {
|
||
return [
|
||
'hot' => '🔥 ' . __('Sehr interessant', 'wp-multi'),
|
||
'interesting' => '⭐ ' . __('Interessant', 'wp-multi'),
|
||
'watch' => '🟡 ' . __('Beobachten', 'wp-multi'),
|
||
'low' => '⚪ ' . __('Keine Priorität', 'wp-multi'),
|
||
'current' => '– ' . __('Kein Bedarf', 'wp-multi'),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Gibt eine Filterzeile mit Zählern aus.
|
||
*
|
||
* @param string $title Beschriftung der Zeile.
|
||
* @param string $param Name des URL-Parameters.
|
||
* @param array $labels Zuordnung "Wert => Beschriftung".
|
||
* @param array $counts Zuordnung "Wert => Anzahl".
|
||
* @param string $active Aktuell aktiver Wert ('' = alle).
|
||
* @param int $total Gesamtzahl für "Alle".
|
||
* @param string $link_url Basis-URL für die Links.
|
||
*/
|
||
function wp_multi_render_filter_row($title, $param, $labels, $counts, $active, $total, $link_url) {
|
||
?>
|
||
<p class="wpm-muted">
|
||
<?php echo esc_html($title); ?>
|
||
<a href="<?php echo esc_url(remove_query_arg($param, $link_url)); ?>" class="wpm-filter-link<?php echo ($active === '') ? ' wpm-filter-active' : ''; ?>">
|
||
<?php if ($active === '') : ?><strong><?php endif; ?>
|
||
<?php printf(esc_html__('Alle (%d)', 'wp-multi'), (int) $total); ?>
|
||
<?php if ($active === '') : ?></strong><?php endif; ?>
|
||
</a>
|
||
<?php foreach ($labels as $key => $label) : ?>
|
||
|
|
||
<a href="<?php echo esc_url(add_query_arg($param, $key, $link_url)); ?>" class="wpm-filter-link<?php echo ($active === $key) ? ' wpm-filter-active' : ''; ?>">
|
||
<?php if ($active === $key) : ?><strong><?php endif; ?>
|
||
<?php echo esc_html($label); ?> (<?php echo isset($counts[$key]) ? (int) $counts[$key] : 0; ?>)
|
||
<?php if ($active === $key) : ?></strong><?php endif; ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</p>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Rendert die Autorentabelle inklusive Filter und Sortierung.
|
||
*
|
||
* @param string $base_url Basis-URL der Übersichtsseite.
|
||
*/
|
||
function wp_multi_render_guest_author_table($base_url) {
|
||
// Sortierparameter aus URL lesen
|
||
$orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'guest_author';
|
||
$order = isset($_GET['order']) && in_array(strtoupper($_GET['order']), ['ASC', 'DESC']) ? strtoupper($_GET['order']) : 'ASC';
|
||
|
||
// Statusfilter und Reaktivierungsfilter aus URL lesen ('' = alle anzeigen)
|
||
$status_labels = wp_multi_get_guest_author_status_labels();
|
||
$filter = isset($_GET['activity']) ? sanitize_key($_GET['activity']) : '';
|
||
if (!isset($status_labels[$filter])) {
|
||
$filter = '';
|
||
}
|
||
|
||
$reactivation_labels = wp_multi_get_reactivation_labels();
|
||
$potential = isset($_GET['potential']) ? sanitize_key($_GET['potential']) : '';
|
||
if (!isset($reactivation_labels[$potential])) {
|
||
$potential = '';
|
||
}
|
||
|
||
// Aufrufe und Serien je Autor – jeweils null, wenn die Quelle fehlt
|
||
$views = wp_multi_get_guest_author_views();
|
||
$show_views = is_array($views);
|
||
$series_stats = wp_multi_get_guest_author_series_stats();
|
||
$show_series = is_array($series_stats);
|
||
|
||
// Gültige Sortierfelder. 'status', 'views', 'series' und 'potential' werden in
|
||
// PHP sortiert, da sie nicht aus der Hauptabfrage stammen.
|
||
$valid_orderby = ['guest_author' => 'pm.meta_value', 'posts' => 'post_count', 'date' => 'last_post_date'];
|
||
$php_orderby = ['status', 'views', 'series', 'potential'];
|
||
if (!isset($valid_orderby[$orderby]) && !in_array($orderby, $php_orderby, true)) {
|
||
$orderby = 'guest_author';
|
||
}
|
||
$sql_orderby = isset($valid_orderby[$orderby]) ? $valid_orderby[$orderby] : 'pm.meta_value';
|
||
$sql_order = $order;
|
||
|
||
// URLs für Sortier- und Filter-Links (Reiter, Filter und Sortierung bleiben erhalten)
|
||
$base_url = add_query_arg('tab', 'authors', $base_url);
|
||
$sort_url = $base_url;
|
||
if ($filter) {
|
||
$sort_url = add_query_arg('activity', $filter, $sort_url);
|
||
}
|
||
if ($potential) {
|
||
$sort_url = add_query_arg('potential', $potential, $sort_url);
|
||
}
|
||
|
||
$filter_url = add_query_arg(['orderby' => $orderby, 'order' => $order], $base_url);
|
||
$status_url = $potential ? add_query_arg('potential', $potential, $filter_url) : $filter_url;
|
||
$potential_url = $filter ? add_query_arg('activity', $filter, $filter_url) : $filter_url;
|
||
|
||
global $wpdb;
|
||
$query = "
|
||
SELECT pm.meta_value AS guest_author, COUNT(*) AS post_count, MAX(p.post_date) AS last_post_date
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||
WHERE pm.meta_key = '_guest_author'
|
||
AND pm.meta_value != ''
|
||
AND p.post_status = 'publish'
|
||
GROUP BY pm.meta_value
|
||
ORDER BY {$sql_orderby} {$sql_order}, pm.meta_value ASC
|
||
";
|
||
$guest_authors = $wpdb->get_results($query);
|
||
|
||
// Durchschnittliche Aufrufe je Geschichte – Maßstab für "gute Aufrufe"
|
||
$avg_views = wp_multi_get_average_guest_author_views();
|
||
|
||
// Kennzahlen ergänzen, Autoren je Filterwert zählen und einschränken
|
||
$status_counts = array_fill_keys(array_keys($status_labels), 0);
|
||
$reactivation_counts = array_fill_keys(array_keys($reactivation_labels), 0);
|
||
$rows = [];
|
||
$position = 0;
|
||
|
||
foreach ((array) $guest_authors as $author) {
|
||
$author->activity = wp_multi_get_guest_author_activity($author->last_post_date, $author->post_count);
|
||
$author->views = $show_views && isset($views[$author->guest_author]) ? $views[$author->guest_author] : 0;
|
||
$author->series_count = $show_series && isset($series_stats[$author->guest_author]) ? $series_stats[$author->guest_author]['series_count'] : 0;
|
||
$author->max_parts = $show_series && isset($series_stats[$author->guest_author]) ? $series_stats[$author->guest_author]['max_parts'] : 0;
|
||
$author->position = $position++; // Reihenfolge aus der DB (korrekte Sortierung inkl. Umlauten)
|
||
|
||
$author->reactivation = wp_multi_get_guest_author_reactivation([
|
||
'post_count' => (int) $author->post_count,
|
||
'days' => $author->activity ? $author->activity['days'] : 0,
|
||
'views' => $author->views,
|
||
'series_count' => $author->series_count,
|
||
'max_parts' => $author->max_parts,
|
||
], $avg_views);
|
||
|
||
if ($author->activity) {
|
||
$status_counts[$author->activity['key']]++;
|
||
}
|
||
$reactivation_counts[$author->reactivation['key']]++;
|
||
|
||
if ($filter !== '' && (!$author->activity || $author->activity['key'] !== $filter)) {
|
||
continue;
|
||
}
|
||
if ($potential !== '' && $author->reactivation['key'] !== $potential) {
|
||
continue;
|
||
}
|
||
|
||
$rows[] = $author;
|
||
}
|
||
|
||
// Sortierung nach berechneten Spalten: bei Gleichstand die DB-Reihenfolge behalten
|
||
if (in_array($orderby, $php_orderby, true)) {
|
||
usort($rows, function ($a, $b) use ($orderby, $order) {
|
||
switch ($orderby) {
|
||
case 'views':
|
||
$value_a = (int) $a->views;
|
||
$value_b = (int) $b->views;
|
||
break;
|
||
case 'series':
|
||
$value_a = (int) $a->series_count;
|
||
$value_b = (int) $b->series_count;
|
||
break;
|
||
case 'potential':
|
||
$value_a = (int) $a->reactivation['rank'];
|
||
$value_b = (int) $b->reactivation['rank'];
|
||
break;
|
||
default:
|
||
$value_a = $a->activity ? $a->activity['rank'] : 0;
|
||
$value_b = $b->activity ? $b->activity['rank'] : 0;
|
||
}
|
||
|
||
if ($value_a === $value_b) {
|
||
return $a->position - $b->position;
|
||
}
|
||
|
||
$diff = $value_a - $value_b;
|
||
|
||
return ($order === 'DESC') ? -$diff : $diff;
|
||
});
|
||
}
|
||
|
||
$colspan = 4 + ($show_views ? 1 : 0) + ($show_series ? 1 : 0);
|
||
|
||
wpmt_admin_card_open();
|
||
|
||
wp_multi_render_filter_row(
|
||
__('Status:', 'wp-multi'),
|
||
'activity',
|
||
$status_labels,
|
||
$status_counts,
|
||
$filter,
|
||
count((array) $guest_authors),
|
||
$status_url
|
||
);
|
||
|
||
wp_multi_render_filter_row(
|
||
__('Reaktivierung:', 'wp-multi'),
|
||
'potential',
|
||
$reactivation_labels,
|
||
$reactivation_counts,
|
||
$potential,
|
||
count((array) $guest_authors),
|
||
$potential_url
|
||
);
|
||
?>
|
||
<div class="wpm-table-wrap">
|
||
<table class="wpm-table">
|
||
<thead>
|
||
<tr>
|
||
<th><?php wp_multi_render_sort_link(__('Gast-Autor', 'wp-multi'), 'guest_author', $orderby, $order, $sort_url, 'ASC'); ?></th>
|
||
<th><?php wp_multi_render_sort_link(__('Beiträge', 'wp-multi'), 'posts', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<?php if ($show_series) : ?>
|
||
<th title="<?php esc_attr_e('Serien mit mindestens zwei eigenen Teilen. Die Länge der längsten steht im Tooltip der Zahl.', 'wp-multi'); ?>"><?php wp_multi_render_sort_link(__('Serien', 'wp-multi'), 'series', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<?php endif; ?>
|
||
<?php if ($show_views) : ?>
|
||
<th title="<?php esc_attr_e('Aufrufe aller Besucher ohne Bots, je Besucher einmal pro halbe Stunde.', 'wp-multi'); ?>"><?php wp_multi_render_sort_link(__('Aufrufe', 'wp-multi'), 'views', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<?php endif; ?>
|
||
<th title="<?php esc_attr_e('Das Symbol zeigt den Aktivitätsstatus.', 'wp-multi'); ?>"><?php wp_multi_render_sort_link(__('Letzte Geschichte', 'wp-multi'), 'date', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<th><?php wp_multi_render_sort_link(__('Reaktivierung', 'wp-multi'), 'potential', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if ($rows) : ?>
|
||
<?php foreach ($rows as $author) : ?>
|
||
<?php $activity = $author->activity; ?>
|
||
<tr>
|
||
<td><?php wp_multi_render_guest_author_link($author->guest_author, $base_url, 'authors'); ?></td>
|
||
<td><?php echo esc_html(number_format_i18n($author->post_count)); ?></td>
|
||
<?php if ($show_series) : ?>
|
||
<td>
|
||
<?php if ($author->series_count) : ?>
|
||
<span title="<?php printf(esc_attr__('längste Serie: %d Teile', 'wp-multi'), (int) $author->max_parts); ?>"><?php echo esc_html(number_format_i18n($author->series_count)); ?></span>
|
||
<?php else : ?>
|
||
<span class="wpm-muted">—</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<?php endif; ?>
|
||
<?php if ($show_views) : ?>
|
||
<td><?php echo esc_html(number_format_i18n($author->views)); ?></td>
|
||
<?php endif; ?>
|
||
<td>
|
||
<?php if ($activity) : ?>
|
||
<span title="<?php echo esc_attr($activity['label']); ?>"><?php echo esc_html($activity['emoji']); ?></span>
|
||
<?php echo esc_html(mysql2date(get_option('date_format'), $author->last_post_date)); ?>
|
||
<span class="wpm-muted">(<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime(get_gmt_from_date($author->last_post_date))))); ?>)</span>
|
||
<?php else : ?>
|
||
<span class="wpm-muted">—</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td>
|
||
<?php $reactivation = $author->reactivation; ?>
|
||
<?php if ($reactivation['key'] === 'hot' || $reactivation['key'] === 'interesting') : ?>
|
||
<span class="wpm-badge <?php echo esc_attr($reactivation['class']); ?>">
|
||
<?php echo esc_html($reactivation['emoji'] . ' ' . $reactivation['label']); ?>
|
||
</span>
|
||
<?php elseif ($reactivation['key'] === 'watch') : ?>
|
||
<span class="wpm-muted"><?php echo esc_html($reactivation['emoji'] . ' ' . $reactivation['label']); ?></span>
|
||
<?php else : ?>
|
||
<span class="wpm-muted" title="<?php echo esc_attr($reactivation['label']); ?>"><?php echo esc_html($reactivation['emoji']); ?></span>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else : ?>
|
||
<tr><td colspan="<?php echo (int) $colspan; ?>"><?php _e('Keine Gast-Autoren gefunden.', 'wp-multi'); ?></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php if ($show_views) : ?>
|
||
<p class="wpm-muted"><?php _e('Aufrufe stammen aus dem Analytics-Modul: alle Besucher, Bots ausgenommen, je Besucher höchstens einmal pro halbe Stunde und Beitrag.', 'wp-multi'); ?></p>
|
||
<?php endif; ?>
|
||
<?php
|
||
wpmt_admin_card_close();
|
||
}
|
||
|
||
/**
|
||
* Baut die URL zur Detailansicht eines Gast-Autors.
|
||
*
|
||
* @param string $base_url Basis-URL der Übersichtsseite.
|
||
* @param string $name Name des Gast-Autors.
|
||
* @param string $tab Reiter, aus dem der Aufruf kommt (für den Zurück-Link).
|
||
* @return string
|
||
*/
|
||
function wp_multi_get_guest_author_detail_url($base_url, $name, $tab = 'authors') {
|
||
return add_query_arg(['tab' => $tab, 'guest_author' => $name], $base_url);
|
||
}
|
||
|
||
/**
|
||
* Gibt einen oder mehrere Autorennamen als Links zur Detailansicht aus.
|
||
*
|
||
* @param string $names Ein Name oder mehrere, mit ", " verbunden.
|
||
* @param string $base_url Basis-URL der Übersichtsseite.
|
||
* @param string $tab Aktueller Reiter.
|
||
* @param bool $split Bei true wird an ", " getrennt (GROUP_CONCAT-Listen).
|
||
*/
|
||
function wp_multi_render_guest_author_link($names, $base_url, $tab = 'authors', $split = false) {
|
||
if ($names === null || $names === '') {
|
||
echo '<span class="wpm-muted">—</span>';
|
||
return;
|
||
}
|
||
|
||
$list = $split ? array_filter(array_map('trim', explode(',', $names))) : [$names];
|
||
$links = [];
|
||
|
||
foreach ($list as $name) {
|
||
$links[] = '<a href="' . esc_url(wp_multi_get_guest_author_detail_url($base_url, $name, $tab)) . '">' . esc_html($name) . '</a>';
|
||
}
|
||
|
||
echo implode(', ', $links);
|
||
}
|
||
|
||
/**
|
||
* Zeigt alles zu einem Gast-Autor auf einer Seite: Kennzahlen, Serien und
|
||
* Einzelgeschichten. Grundlage für die Entscheidung, wen man kontaktiert.
|
||
*
|
||
* @param string $base_url Basis-URL der Übersichtsseite.
|
||
* @param string $guest_author Name des Gast-Autors.
|
||
* @param string $tab Reiter, aus dem der Aufruf kam.
|
||
*/
|
||
function wp_multi_render_guest_author_detail($base_url, $guest_author, $tab = 'authors') {
|
||
global $wpdb;
|
||
|
||
$back_url = add_query_arg('tab', $tab, $base_url);
|
||
|
||
// Kennzahlen des Autors
|
||
$summary = $wpdb->get_row($wpdb->prepare("
|
||
SELECT COUNT(*) AS post_count, MAX(p.post_date) AS last_post_date
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
|
||
WHERE pm.meta_key = '_guest_author'
|
||
AND pm.meta_value = %s
|
||
AND p.post_status = 'publish'
|
||
", $guest_author));
|
||
|
||
// Der Name stammt aus einem Meta-Feld und wird deshalb selbst escaped
|
||
// ausgegeben, statt ihn als Kartentitel durchzureichen.
|
||
wpmt_admin_card_open(__('Autoren-Details', 'wp-multi'));
|
||
?>
|
||
<p><a href="<?php echo esc_url($back_url); ?>">← <?php _e('Zurück zur Übersicht', 'wp-multi'); ?></a></p>
|
||
<h2 class="wpm-author-name"><?php echo esc_html($guest_author); ?></h2>
|
||
<?php
|
||
|
||
if (!$summary || !$summary->post_count) {
|
||
echo '<p class="wpm-muted">' . esc_html__('Zu diesem Namen wurden keine veröffentlichten Geschichten gefunden.', 'wp-multi') . '</p>';
|
||
wpmt_admin_card_close();
|
||
return;
|
||
}
|
||
|
||
$post_count = (int) $summary->post_count;
|
||
$activity = wp_multi_get_guest_author_activity($summary->last_post_date, $post_count);
|
||
$milestone = wp_multi_get_guest_author_milestone($post_count);
|
||
|
||
// Gesamtaufrufe aller Geschichten dieses Autors
|
||
$analytics_table = wp_multi_get_analytics_table();
|
||
$total_views = null;
|
||
|
||
if ($analytics_table) {
|
||
$total_views = (int) $wpdb->get_var($wpdb->prepare("
|
||
SELECT COUNT(*)
|
||
FROM {$analytics_table} a
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = a.post_id AND pm.meta_key = '_guest_author'
|
||
WHERE a.action = 'view'
|
||
AND pm.meta_value = %s
|
||
", $guest_author));
|
||
}
|
||
?>
|
||
<ul class="wpm-author-facts">
|
||
<li>
|
||
<strong><?php echo esc_html(number_format_i18n($post_count)); ?></strong>
|
||
<?php echo esc_html(_n('Geschichte', 'Geschichten', $post_count, 'wp-multi')); ?>
|
||
<?php if ($milestone) : ?>
|
||
<span title="<?php echo esc_attr($milestone['label_full']); ?>"><?php echo esc_html($milestone['emoji']); ?></span>
|
||
<?php endif; ?>
|
||
</li>
|
||
<li>
|
||
<?php _e('Letzte Veröffentlichung:', 'wp-multi'); ?>
|
||
<strong><?php echo esc_html(mysql2date(get_option('date_format'), $summary->last_post_date)); ?></strong>
|
||
<span class="wpm-muted">(<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime(get_gmt_from_date($summary->last_post_date))))); ?>)</span>
|
||
</li>
|
||
<?php if ($activity) : ?>
|
||
<li>
|
||
<?php _e('Status:', 'wp-multi'); ?>
|
||
<span class="wpm-badge <?php echo esc_attr($activity['class']); ?>"><?php echo esc_html($activity['emoji'] . ' ' . $activity['label']); ?></span>
|
||
</li>
|
||
<?php endif; ?>
|
||
<?php if ($total_views !== null) : ?>
|
||
<li><?php _e('Gesamtaufrufe:', 'wp-multi'); ?> <strong><?php echo esc_html(number_format_i18n($total_views)); ?></strong></li>
|
||
<?php endif; ?>
|
||
<?php
|
||
// Reaktivierungsbewertung mit denselben Kennzahlen wie in der Übersicht
|
||
$series_stats = wp_multi_get_guest_author_series_stats();
|
||
$author_series = (is_array($series_stats) && isset($series_stats[$guest_author])) ? $series_stats[$guest_author] : ['series_count' => 0, 'max_parts' => 0];
|
||
|
||
$reactivation = wp_multi_get_guest_author_reactivation([
|
||
'post_count' => $post_count,
|
||
'days' => $activity ? $activity['days'] : 0,
|
||
'views' => (int) $total_views,
|
||
'series_count' => $author_series['series_count'],
|
||
'max_parts' => $author_series['max_parts'],
|
||
], wp_multi_get_average_guest_author_views());
|
||
?>
|
||
<li>
|
||
<?php _e('Reaktivierung:', 'wp-multi'); ?>
|
||
<?php if ($reactivation['key'] === 'current') : ?>
|
||
<span class="wpm-muted"><?php echo esc_html($reactivation['label']); ?></span>
|
||
<?php else : ?>
|
||
<span class="wpm-badge <?php echo esc_attr($reactivation['class']); ?>"><?php echo esc_html($reactivation['emoji'] . ' ' . $reactivation['label']); ?></span>
|
||
<?php endif; ?>
|
||
</li>
|
||
</ul>
|
||
<?php
|
||
|
||
// Serien dieses Autors
|
||
$source = wp_multi_detect_series_source();
|
||
$series_post_ids = [];
|
||
|
||
if ($source) {
|
||
$parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : '';
|
||
|
||
$series = $wpdb->get_results($wpdb->prepare("
|
||
SELECT t.term_id, t.name AS series_name,
|
||
COUNT(DISTINCT p.ID) AS author_parts,
|
||
MAX(p.post_date) AS last_part_date
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value = %s
|
||
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 = %s {$parent_sql}
|
||
INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
|
||
WHERE p.post_status = 'publish'
|
||
GROUP BY t.term_id, t.name
|
||
ORDER BY last_part_date DESC
|
||
", $guest_author, $source['taxonomy']));
|
||
|
||
if ($series) {
|
||
// Gesamtteile der Serie (auch von anderen Autoren) und Aufrufe nachladen
|
||
$term_ids = [];
|
||
foreach ($series as $row) {
|
||
$term_ids[] = (int) $row->term_id;
|
||
}
|
||
$term_id_list = implode(',', $term_ids);
|
||
|
||
$totals = $wpdb->get_results($wpdb->prepare("
|
||
SELECT tt.term_id, COUNT(DISTINCT p.ID) AS total_parts
|
||
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 = %s
|
||
WHERE p.post_status = 'publish'
|
||
AND tt.term_id IN ({$term_id_list})
|
||
GROUP BY tt.term_id
|
||
", $source['taxonomy']));
|
||
|
||
$total_parts = [];
|
||
foreach ((array) $totals as $total) {
|
||
$total_parts[(int) $total->term_id] = (int) $total->total_parts;
|
||
}
|
||
|
||
wp_multi_add_series_details($series, $source['taxonomy']);
|
||
|
||
// Beiträge in Serien merken, damit sie unten nicht doppelt auftauchen
|
||
$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 = %s
|
||
WHERE tt.term_id IN ({$term_id_list})
|
||
", $source['taxonomy']));
|
||
|
||
foreach ((array) $ids as $id) {
|
||
$series_post_ids[] = (int) $id;
|
||
}
|
||
?>
|
||
<h3><?php _e('Serien', 'wp-multi'); ?></h3>
|
||
<div class="wpm-table-wrap">
|
||
<table class="wpm-table">
|
||
<thead>
|
||
<tr>
|
||
<th><?php _e('Serie', 'wp-multi'); ?></th>
|
||
<th><?php _e('Teile', 'wp-multi'); ?></th>
|
||
<th><?php _e('Letzter Teil', 'wp-multi'); ?></th>
|
||
<?php if ($analytics_table) : ?>
|
||
<th><?php _e('Aufrufe gesamt', 'wp-multi'); ?></th>
|
||
<?php endif; ?>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($series as $row) : ?>
|
||
<?php
|
||
$term_link = get_term_link((int) $row->term_id, $source['taxonomy']);
|
||
$parts = isset($total_parts[(int) $row->term_id]) ? $total_parts[(int) $row->term_id] : (int) $row->author_parts;
|
||
?>
|
||
<tr>
|
||
<td>
|
||
<?php if (!is_wp_error($term_link)) : ?>
|
||
<a href="<?php echo esc_url($term_link); ?>" target="_blank" rel="noopener"><?php echo esc_html($row->series_name); ?></a>
|
||
<?php else : ?>
|
||
<?php echo esc_html($row->series_name); ?>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td>
|
||
<span class="wpm-badge"><?php echo (int) $parts; ?></span>
|
||
<?php if ((int) $row->author_parts !== (int) $parts) : ?>
|
||
<span class="wpm-muted">
|
||
<?php
|
||
/* translators: %d: Teile dieses Autors */
|
||
printf(esc_html__('(davon %d von diesem Autor)', 'wp-multi'), (int) $row->author_parts);
|
||
?>
|
||
</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td>
|
||
<?php if ($row->last_part_id) : ?>
|
||
<a href="<?php echo esc_url(get_permalink($row->last_part_id)); ?>" target="_blank" rel="noopener">
|
||
<?php echo esc_html($row->last_part_title !== '' ? $row->last_part_title : __('(ohne Titel)', 'wp-multi')); ?>
|
||
</a><br />
|
||
<?php endif; ?>
|
||
<span class="wpm-muted">
|
||
<?php echo esc_html(mysql2date(get_option('date_format'), $row->last_part_date)); ?>
|
||
(<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime(get_gmt_from_date($row->last_part_date))))); ?>)
|
||
</span>
|
||
</td>
|
||
<?php if ($analytics_table) : ?>
|
||
<td><?php echo esc_html(number_format_i18n($row->series_views)); ?></td>
|
||
<?php endif; ?>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php
|
||
}
|
||
}
|
||
|
||
// Einzelgeschichten außerhalb von Serien
|
||
$exclude_sql = $series_post_ids ? 'AND p.ID NOT IN (' . implode(',', array_map('intval', $series_post_ids)) . ')' : '';
|
||
$limit = (int) apply_filters('wp_multi_author_detail_limit', 100);
|
||
|
||
$posts = $wpdb->get_results($wpdb->prepare("
|
||
SELECT p.ID, p.post_title, p.post_date
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author'
|
||
WHERE pm.meta_value = %s
|
||
AND p.post_status = 'publish'
|
||
{$exclude_sql}
|
||
ORDER BY p.post_date DESC
|
||
LIMIT %d
|
||
", $guest_author, $limit));
|
||
|
||
// Aufrufe der angezeigten Geschichten
|
||
$post_views = [];
|
||
if ($analytics_table && $posts) {
|
||
$post_ids = [];
|
||
foreach ($posts as $row) {
|
||
$post_ids[] = (int) $row->ID;
|
||
}
|
||
$post_id_list = implode(',', $post_ids);
|
||
|
||
$view_rows = $wpdb->get_results("
|
||
SELECT post_id, COUNT(*) AS views
|
||
FROM {$analytics_table}
|
||
WHERE action = 'view'
|
||
AND post_id IN ({$post_id_list})
|
||
GROUP BY post_id
|
||
");
|
||
|
||
foreach ((array) $view_rows as $view_row) {
|
||
$post_views[(int) $view_row->post_id] = (int) $view_row->views;
|
||
}
|
||
}
|
||
?>
|
||
<h3><?php _e('Weitere Geschichten', 'wp-multi'); ?></h3>
|
||
<div class="wpm-table-wrap">
|
||
<table class="wpm-table">
|
||
<thead>
|
||
<tr>
|
||
<th><?php _e('Geschichte', 'wp-multi'); ?></th>
|
||
<th><?php _e('Veröffentlicht', 'wp-multi'); ?></th>
|
||
<?php if ($analytics_table) : ?>
|
||
<th><?php _e('Aufrufe', 'wp-multi'); ?></th>
|
||
<?php endif; ?>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if ($posts) : ?>
|
||
<?php foreach ($posts as $row) : ?>
|
||
<tr>
|
||
<td>
|
||
<a href="<?php echo esc_url(get_permalink((int) $row->ID)); ?>" target="_blank" rel="noopener">
|
||
<?php echo esc_html($row->post_title !== '' ? $row->post_title : __('(ohne Titel)', 'wp-multi')); ?>
|
||
</a>
|
||
</td>
|
||
<td><?php echo esc_html(mysql2date(get_option('date_format'), $row->post_date)); ?></td>
|
||
<?php if ($analytics_table) : ?>
|
||
<td><?php echo esc_html(number_format_i18n(isset($post_views[(int) $row->ID]) ? $post_views[(int) $row->ID] : 0)); ?></td>
|
||
<?php endif; ?>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else : ?>
|
||
<tr><td colspan="<?php echo $analytics_table ? 3 : 2; ?>"><?php _e('Alle Geschichten dieses Autors gehören zu einer Serie.', 'wp-multi'); ?></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php
|
||
wpmt_admin_card_close();
|
||
}
|
||
|
||
/**
|
||
* Ermittelt, wo die Serien dieser Installation stecken.
|
||
*
|
||
* Unterstützt zwei übliche Modelle:
|
||
* 1. eigene Taxonomie (z. B. "series") – jeder Term ist eine Serie
|
||
* 2. Oberkategorie "Serien" – jede Unterkategorie ist eine Serie
|
||
*
|
||
* @return array|null ['taxonomy' => string, 'parent' => int, 'label' => string] oder null.
|
||
*/
|
||
function wp_multi_detect_series_source() {
|
||
// Feste Vorgabe per Filter, z. B. add_filter('wp_multi_series_taxonomy', fn() => 'meine-serien');
|
||
$forced = apply_filters('wp_multi_series_taxonomy', '');
|
||
if ($forced && taxonomy_exists($forced)) {
|
||
return ['taxonomy' => $forced, 'parent' => 0, 'label' => $forced];
|
||
}
|
||
|
||
// Mögliche Namen einer eigenen Serien-Taxonomie
|
||
$candidates = apply_filters('wp_multi_series_taxonomy_candidates', ['series', 'serie', 'serien', 'story-series']);
|
||
|
||
foreach ($candidates as $candidate) {
|
||
if (!taxonomy_exists($candidate)) {
|
||
continue;
|
||
}
|
||
|
||
$count = wp_count_terms(['taxonomy' => $candidate, 'hide_empty' => false]);
|
||
if (!is_wp_error($count) && (int) $count > 0) {
|
||
return ['taxonomy' => $candidate, 'parent' => 0, 'label' => $candidate];
|
||
}
|
||
}
|
||
|
||
// Zweites Modell: Oberkategorie mit einer Unterkategorie je Serie
|
||
foreach ($candidates as $candidate) {
|
||
$term = get_term_by('slug', $candidate, 'category');
|
||
if (!$term || is_wp_error($term)) {
|
||
continue;
|
||
}
|
||
|
||
$children = get_term_children($term->term_id, 'category');
|
||
if (!is_wp_error($children) && !empty($children)) {
|
||
return [
|
||
'taxonomy' => 'category',
|
||
'parent' => (int) $term->term_id,
|
||
/* translators: %s: Name der Oberkategorie */
|
||
'label' => sprintf(__('Unterkategorien von "%s"', 'wp-multi'), $term->name),
|
||
];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Gibt einen Spaltenkopf mit Sortier-Link aus.
|
||
*
|
||
* @param string $label Beschriftung der Spalte.
|
||
* @param string $key Sortierschlüssel dieser Spalte.
|
||
* @param string $orderby Aktuell aktive Sortierung.
|
||
* @param string $order Aktuelle Richtung (ASC/DESC).
|
||
* @param string $sort_url Basis-URL für den Link.
|
||
* @param string $default_order Richtung beim ersten Klick auf diese Spalte.
|
||
*/
|
||
function wp_multi_render_sort_link($label, $key, $orderby, $order, $sort_url, $default_order = 'ASC') {
|
||
$is_active = ($orderby === $key);
|
||
// Aktive Spalte kehrt die Richtung um, inaktive startet mit ihrem Standard
|
||
$next_order = $is_active ? (($order === 'ASC') ? 'DESC' : 'ASC') : $default_order;
|
||
$url = add_query_arg(['orderby' => $key, 'order' => $next_order], $sort_url);
|
||
|
||
echo '<a href="' . esc_url($url) . '">' . esc_html($label);
|
||
if ($is_active) {
|
||
echo ' <span>' . (($order === 'ASC') ? '↑' : '↓') . '</span>';
|
||
}
|
||
echo '</a>';
|
||
}
|
||
|
||
/**
|
||
* Liefert den Namen der Analytics-Tabelle, sofern sie existiert.
|
||
*
|
||
* @return string Tabellenname oder '' ohne Analytics-Modul.
|
||
*/
|
||
function wp_multi_get_analytics_table() {
|
||
static $table = null;
|
||
|
||
if ($table !== null) {
|
||
return $table;
|
||
}
|
||
|
||
if (!defined('WP_MULTI_ANALYTICS_TABLE')) {
|
||
$table = '';
|
||
return $table;
|
||
}
|
||
|
||
global $wpdb;
|
||
$name = WP_MULTI_ANALYTICS_TABLE;
|
||
$table = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $name)) ? $name : '';
|
||
|
||
return $table;
|
||
}
|
||
|
||
/**
|
||
* Ergänzt Serien um letzten Teil und Aufrufzahlen.
|
||
*
|
||
* Läuft in eigenen Abfragen statt als weiterer JOIN in der Hauptabfrage: Ein
|
||
* zusätzlicher JOIN würde die Zeilen vervielfachen und die Teile-Zählung
|
||
* verfälschen.
|
||
*
|
||
* @param array $rows Serienzeilen mit term_id.
|
||
* @param string $taxonomy Taxonomie der Serien.
|
||
*/
|
||
function wp_multi_add_series_details(&$rows, $taxonomy) {
|
||
if (empty($rows)) {
|
||
return;
|
||
}
|
||
|
||
global $wpdb;
|
||
|
||
$term_ids = [];
|
||
foreach ($rows as $row) {
|
||
$term_ids[] = (int) $row->term_id;
|
||
}
|
||
$term_id_list = implode(',', $term_ids);
|
||
|
||
// Letzter Teil je Serie: absteigend sortiert, der erste Treffer je Serie gewinnt
|
||
$parts = $wpdb->get_results($wpdb->prepare("
|
||
SELECT tt.term_id, p.ID AS post_id, p.post_title
|
||
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 = %s
|
||
WHERE p.post_status = 'publish'
|
||
AND tt.term_id IN ({$term_id_list})
|
||
ORDER BY p.post_date DESC
|
||
", $taxonomy));
|
||
|
||
$last_parts = [];
|
||
foreach ((array) $parts as $part) {
|
||
if (!isset($last_parts[$part->term_id])) {
|
||
$last_parts[$part->term_id] = $part;
|
||
}
|
||
}
|
||
|
||
// Aufrufe der ganzen Serie und des letzten Teils
|
||
$series_views = [];
|
||
$post_views = [];
|
||
$analytics_table = wp_multi_get_analytics_table();
|
||
|
||
if ($analytics_table) {
|
||
$totals = $wpdb->get_results($wpdb->prepare("
|
||
SELECT tt.term_id, COUNT(*) AS views
|
||
FROM {$analytics_table} a
|
||
INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = a.post_id
|
||
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s
|
||
WHERE a.action = 'view'
|
||
AND tt.term_id IN ({$term_id_list})
|
||
GROUP BY tt.term_id
|
||
", $taxonomy));
|
||
|
||
foreach ((array) $totals as $total) {
|
||
$series_views[(int) $total->term_id] = (int) $total->views;
|
||
}
|
||
|
||
$post_ids = [];
|
||
foreach ($last_parts as $part) {
|
||
$post_ids[] = (int) $part->post_id;
|
||
}
|
||
|
||
if ($post_ids) {
|
||
$post_id_list = implode(',', $post_ids);
|
||
$single = $wpdb->get_results("
|
||
SELECT post_id, COUNT(*) AS views
|
||
FROM {$analytics_table}
|
||
WHERE action = 'view'
|
||
AND post_id IN ({$post_id_list})
|
||
GROUP BY post_id
|
||
");
|
||
|
||
foreach ((array) $single as $entry) {
|
||
$post_views[(int) $entry->post_id] = (int) $entry->views;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($rows as $row) {
|
||
$term_id = (int) $row->term_id;
|
||
$last_part = isset($last_parts[$term_id]) ? $last_parts[$term_id] : null;
|
||
|
||
$row->last_part_id = $last_part ? (int) $last_part->post_id : 0;
|
||
$row->last_part_title = $last_part ? $last_part->post_title : '';
|
||
$row->series_views = isset($series_views[$term_id]) ? $series_views[$term_id] : 0;
|
||
$row->last_part_views = ($row->last_part_id && isset($post_views[$row->last_part_id])) ? $post_views[$row->last_part_id] : 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Zeigt Serien, die seit längerem keinen neuen Teil bekommen haben.
|
||
*
|
||
* Beispiel: "Serie mit 7 Teilen – letzter Teil vor 11 Monaten". Damit sieht man
|
||
* im Backend sofort, wo sich Nachfragen beim Autor lohnt.
|
||
*/
|
||
function wp_multi_render_stalled_series($base_url = '') {
|
||
// Mindestens so viele Teile und so viele Tage ohne Fortsetzung
|
||
$min_parts = (int) apply_filters('wp_multi_stalled_series_min_parts', 2);
|
||
$min_days = (int) apply_filters('wp_multi_stalled_series_min_days', 180);
|
||
|
||
if ($base_url === '') {
|
||
$base_url = add_query_arg(['page' => 'guest_author_overview'], admin_url('admin.php'));
|
||
}
|
||
$sort_url = add_query_arg('tab', 'series', $base_url);
|
||
|
||
// Sortierparameter aus URL lesen. 'series_views' und 'last_part_views' stammen
|
||
// aus den Nachfragen und werden deshalb in PHP sortiert.
|
||
$valid_orderby = ['name' => 't.name', 'parts' => 'part_count', 'date' => 'last_part_date'];
|
||
$php_orderby = ['series_views', 'last_part_views'];
|
||
$orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'date';
|
||
if (!isset($valid_orderby[$orderby]) && !in_array($orderby, $php_orderby, true)) {
|
||
$orderby = 'date';
|
||
}
|
||
// Standard: neueste Fortsetzung oben
|
||
$order = isset($_GET['order']) && in_array(strtoupper($_GET['order']), ['ASC', 'DESC']) ? strtoupper($_GET['order']) : 'DESC';
|
||
$sql_orderby = isset($valid_orderby[$orderby]) ? $valid_orderby[$orderby] : 'last_part_date';
|
||
|
||
wpmt_admin_card_open(__('Liegengebliebene Serien', 'wp-multi'));
|
||
|
||
$source = wp_multi_detect_series_source();
|
||
|
||
if (!$source) {
|
||
wp_multi_render_series_source_hint();
|
||
wpmt_admin_card_close();
|
||
return;
|
||
}
|
||
|
||
global $wpdb;
|
||
|
||
// Bei Unterkategorien nur die Kinder der Oberkategorie berücksichtigen
|
||
$parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : '';
|
||
|
||
$series = $wpdb->get_results($wpdb->prepare("
|
||
SELECT t.term_id, t.name AS series_name,
|
||
COUNT(DISTINCT p.ID) AS part_count,
|
||
MAX(p.post_date) AS last_part_date,
|
||
GROUP_CONCAT(DISTINCT pm.meta_value ORDER BY pm.meta_value SEPARATOR ', ') AS authors
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
|
||
INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = %s {$parent_sql}
|
||
INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
|
||
LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value != ''
|
||
WHERE p.post_status = 'publish'
|
||
GROUP BY t.term_id, t.name
|
||
HAVING part_count >= %d
|
||
ORDER BY {$sql_orderby} {$order}, last_part_date DESC
|
||
", $source['taxonomy'], $min_parts));
|
||
|
||
// Nur Serien behalten, deren letzter Teil lange genug zurückliegt
|
||
$stalled = [];
|
||
foreach ((array) $series as $row) {
|
||
$last_ts = strtotime(get_gmt_from_date($row->last_part_date));
|
||
if (!$last_ts) {
|
||
continue;
|
||
}
|
||
|
||
$row->days = (int) floor((time() - $last_ts) / DAY_IN_SECONDS);
|
||
if ($row->days > $min_days) {
|
||
$stalled[] = $row;
|
||
}
|
||
}
|
||
|
||
// Letzten Teil und Aufrufe nur für die tatsächlich angezeigten Serien nachladen
|
||
wp_multi_add_series_details($stalled, $source['taxonomy']);
|
||
|
||
// Sortierung nach Aufrufen: bei Gleichstand die DB-Reihenfolge behalten
|
||
if (in_array($orderby, $php_orderby, true)) {
|
||
$position = 0;
|
||
foreach ($stalled as $row) {
|
||
$row->position = $position++;
|
||
}
|
||
|
||
usort($stalled, function ($a, $b) use ($orderby, $order) {
|
||
$value_a = (int) $a->$orderby;
|
||
$value_b = (int) $b->$orderby;
|
||
|
||
if ($value_a === $value_b) {
|
||
return $a->position - $b->position;
|
||
}
|
||
|
||
$diff = $value_a - $value_b;
|
||
|
||
return ($order === 'DESC') ? -$diff : $diff;
|
||
});
|
||
}
|
||
|
||
$show_views = (bool) wp_multi_get_analytics_table();
|
||
$colspan = $show_views ? 6 : 4;
|
||
?>
|
||
<p class="wpm-muted">
|
||
<?php
|
||
printf(
|
||
/* translators: 1: Mindestanzahl Teile, 2: Anzahl Tage, 3: Quelle der Serien */
|
||
esc_html__('Serien mit mindestens %1$d Teilen, deren letzter Teil länger als %2$d Tage zurückliegt. Quelle: %3$s.', 'wp-multi'),
|
||
$min_parts,
|
||
$min_days,
|
||
esc_html($source['label'])
|
||
);
|
||
?>
|
||
</p>
|
||
<div class="wpm-table-wrap">
|
||
<table class="wpm-table">
|
||
<thead>
|
||
<tr>
|
||
<th><?php wp_multi_render_sort_link(__('Serie', 'wp-multi'), 'name', $orderby, $order, $sort_url, 'ASC'); ?></th>
|
||
<th><?php wp_multi_render_sort_link(__('Teile', 'wp-multi'), 'parts', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<th><?php wp_multi_render_sort_link(__('Letzter Teil', 'wp-multi'), 'date', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<?php if ($show_views) : ?>
|
||
<th><?php wp_multi_render_sort_link(__('Aufrufe gesamt', 'wp-multi'), 'series_views', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<th><?php wp_multi_render_sort_link(__('Aufrufe letzter Teil', 'wp-multi'), 'last_part_views', $orderby, $order, $sort_url, 'DESC'); ?></th>
|
||
<?php endif; ?>
|
||
<th><?php _e('Autor(en)', 'wp-multi'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if ($stalled) : ?>
|
||
<?php foreach ($stalled as $row) : ?>
|
||
<?php $term_link = get_term_link((int) $row->term_id, $source['taxonomy']); ?>
|
||
<tr>
|
||
<td>
|
||
<?php if (!is_wp_error($term_link)) : ?>
|
||
<a href="<?php echo esc_url($term_link); ?>" target="_blank" rel="noopener"><?php echo esc_html($row->series_name); ?></a>
|
||
<?php else : ?>
|
||
<?php echo esc_html($row->series_name); ?>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><span class="wpm-badge"><?php echo (int) $row->part_count; ?></span></td>
|
||
<td>
|
||
<?php if ($row->last_part_id) : ?>
|
||
<a href="<?php echo esc_url(get_permalink($row->last_part_id)); ?>" target="_blank" rel="noopener">
|
||
<?php echo esc_html($row->last_part_title !== '' ? $row->last_part_title : __('(ohne Titel)', 'wp-multi')); ?>
|
||
</a><br />
|
||
<?php endif; ?>
|
||
<span class="wpm-muted">
|
||
<?php echo esc_html(mysql2date(get_option('date_format'), $row->last_part_date)); ?>
|
||
(<?php printf(esc_html__('vor %s', 'wp-multi'), esc_html(human_time_diff(strtotime(get_gmt_from_date($row->last_part_date))))); ?>)
|
||
</span>
|
||
</td>
|
||
<?php if ($show_views) : ?>
|
||
<td><span class="wpm-badge"><?php echo esc_html(number_format_i18n($row->series_views)); ?></span></td>
|
||
<td><?php echo esc_html(number_format_i18n($row->last_part_views)); ?></td>
|
||
<?php endif; ?>
|
||
<td><?php wp_multi_render_guest_author_link($row->authors, $base_url, 'series', true); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php else : ?>
|
||
<tr><td colspan="<?php echo (int) $colspan; ?>"><?php _e('Keine liegengebliebenen Serien – alle Serien sind aktuell.', 'wp-multi'); ?></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php if ($show_views) : ?>
|
||
<p class="wpm-muted"><?php _e('Aufrufe stammen aus dem Analytics-Modul und werden erst seit dessen Aktivierung erfasst – alte Serien haben deshalb oft weniger Aufrufe, als sie tatsächlich hatten.', 'wp-multi'); ?></p>
|
||
<?php endif; ?>
|
||
<?php
|
||
wpmt_admin_card_close();
|
||
}
|
||
|
||
/**
|
||
* Hinweis, wenn keine Serien-Quelle gefunden wurde – inklusive Liste der
|
||
* vorhandenen Taxonomien, damit die richtige schnell eingetragen werden kann.
|
||
*/
|
||
function wp_multi_render_series_source_hint() {
|
||
$taxonomies = get_taxonomies(['public' => true], 'objects');
|
||
?>
|
||
<p><?php _e('Es wurde keine Quelle für Serien gefunden. Erwartet wird entweder eine eigene Taxonomie ("series", "serie", "serien") oder eine Oberkategorie gleichen Namens mit einer Unterkategorie je Serie.', 'wp-multi'); ?></p>
|
||
<p class="wpm-muted"><?php _e('Vorhandene Taxonomien dieser Seite:', 'wp-multi'); ?></p>
|
||
<div class="wpm-table-wrap">
|
||
<table class="wpm-table">
|
||
<thead>
|
||
<tr>
|
||
<th><?php _e('Taxonomie', 'wp-multi'); ?></th>
|
||
<th><?php _e('Name', 'wp-multi'); ?></th>
|
||
<th><?php _e('Einträge', 'wp-multi'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($taxonomies as $taxonomy) : ?>
|
||
<?php $count = wp_count_terms(['taxonomy' => $taxonomy->name, 'hide_empty' => false]); ?>
|
||
<tr>
|
||
<td><code><?php echo esc_html($taxonomy->name); ?></code></td>
|
||
<td><?php echo esc_html($taxonomy->labels->name); ?></td>
|
||
<td><span class="wpm-badge"><?php echo is_wp_error($count) ? 0 : (int) $count; ?></span></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="wpm-muted">
|
||
<?php _e('Die passende Taxonomie lässt sich per Filter festlegen:', 'wp-multi'); ?>
|
||
<code>add_filter('wp_multi_series_taxonomy', function () { return 'taxonomie-name'; });</code>
|
||
</p>
|
||
<?php
|
||
}
|
||
|
||
|
||
// Funktion zum Erstellen der Gastautoren-Beitragsseite
|
||
function wp_multi_create_guest_author_page() {
|
||
$page_title = __('Gastautor Beiträge', 'wp-multi');
|
||
$page_slug = 'gastautor-beitraege';
|
||
$page = get_page_by_path($page_slug);
|
||
|
||
if (!$page) {
|
||
$page_id = wp_insert_post(array(
|
||
'post_title' => $page_title,
|
||
'post_name' => $page_slug,
|
||
'post_content' => '[guest_author_posts]',
|
||
'post_status' => 'publish',
|
||
'post_type' => 'page',
|
||
));
|
||
|
||
if ($page_id && !is_wp_error($page_id)) {
|
||
update_option('wp_multi_guest_author_page_id', $page_id);
|
||
}
|
||
} else {
|
||
update_option('wp_multi_guest_author_page_id', $page->ID);
|
||
}
|
||
}
|
||
add_action('init', 'wp_multi_create_guest_author_page');
|
||
|
||
/**
|
||
* Liefert die Serien eines Gast-Autors.
|
||
*
|
||
* @param string $guest_author Name des Gast-Autors.
|
||
* @return array ['series' => Zeilen, 'post_ids' => IDs aller Teile dieser Serien]
|
||
*/
|
||
function wp_multi_get_guest_author_series($guest_author) {
|
||
$empty = ['series' => [], 'post_ids' => []];
|
||
$source = wp_multi_detect_series_source();
|
||
|
||
if (!$source || $guest_author === '') {
|
||
return $empty;
|
||
}
|
||
|
||
global $wpdb;
|
||
$parent_sql = $source['parent'] ? $wpdb->prepare('AND tt.parent = %d', $source['parent']) : '';
|
||
|
||
$series = $wpdb->get_results($wpdb->prepare("
|
||
SELECT t.term_id, t.name AS series_name,
|
||
COUNT(DISTINCT p.ID) AS author_parts,
|
||
MAX(p.post_date) AS last_part_date
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author' AND pm.meta_value = %s
|
||
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 = %s {$parent_sql}
|
||
INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
|
||
WHERE p.post_status = 'publish'
|
||
GROUP BY t.term_id, t.name
|
||
ORDER BY last_part_date DESC
|
||
", $guest_author, $source['taxonomy']));
|
||
|
||
if (!$series) {
|
||
return $empty;
|
||
}
|
||
|
||
$term_ids = [];
|
||
foreach ($series as $row) {
|
||
$term_ids[] = (int) $row->term_id;
|
||
}
|
||
$term_id_list = implode(',', $term_ids);
|
||
|
||
// Gesamtlänge der Serie – auch Teile anderer Autoren zählen dazu
|
||
$totals = $wpdb->get_results($wpdb->prepare("
|
||
SELECT tt.term_id, COUNT(DISTINCT p.ID) AS total_parts
|
||
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 = %s
|
||
WHERE p.post_status = 'publish'
|
||
AND tt.term_id IN ({$term_id_list})
|
||
GROUP BY tt.term_id
|
||
", $source['taxonomy']));
|
||
|
||
$total_parts = [];
|
||
foreach ((array) $totals as $total) {
|
||
$total_parts[(int) $total->term_id] = (int) $total->total_parts;
|
||
}
|
||
|
||
foreach ($series as $row) {
|
||
$row->total_parts = isset($total_parts[(int) $row->term_id]) ? $total_parts[(int) $row->term_id] : (int) $row->author_parts;
|
||
$row->taxonomy = $source['taxonomy'];
|
||
}
|
||
|
||
// Alle Beiträge dieser Serien – damit sie in der Einzelliste nicht doppelt stehen
|
||
$post_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 = %s
|
||
WHERE tt.term_id IN ({$term_id_list})
|
||
", $source['taxonomy']));
|
||
|
||
return [
|
||
'series' => $series,
|
||
'post_ids' => array_map('intval', (array) $post_ids),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Findet die Seite, auf der die Autorenübersicht steht.
|
||
*
|
||
* Gesucht wird die Seite mit dem Shortcode [guest_authors]; das Ergebnis wird
|
||
* zwischengespeichert, damit die Suche nicht bei jedem Profilaufruf läuft.
|
||
*
|
||
* @return string URL oder '' wenn keine Übersichtsseite existiert.
|
||
*/
|
||
function wp_multi_get_guest_author_overview_url() {
|
||
$url = apply_filters('wp_multi_author_overview_url', '');
|
||
|
||
if ($url) {
|
||
return $url;
|
||
}
|
||
|
||
$cached = get_transient('wp_multi_author_overview_page');
|
||
|
||
if ($cached === false) {
|
||
global $wpdb;
|
||
|
||
$cached = (int) $wpdb->get_var("
|
||
SELECT ID
|
||
FROM {$wpdb->posts}
|
||
WHERE post_type = 'page'
|
||
AND post_status = 'publish'
|
||
AND post_content LIKE '%[guest_authors%'
|
||
ORDER BY ID ASC
|
||
LIMIT 1
|
||
");
|
||
|
||
set_transient('wp_multi_author_overview_page', $cached, 12 * HOUR_IN_SECONDS);
|
||
}
|
||
|
||
return $cached ? get_permalink($cached) : '';
|
||
}
|
||
|
||
// Shortcode für Gastautoren-Liste
|
||
function wp_multi_guest_author_shortcode($atts = []) {
|
||
ob_start();
|
||
global $wpdb;
|
||
|
||
// Erlaubte Sortierfelder (Whitelist – verhindert SQL-Injection)
|
||
$valid_orderby = [
|
||
'name' => 'pm.meta_value',
|
||
'posts' => 'post_count',
|
||
'date' => 'last_post_date',
|
||
'new' => 'first_post_date',
|
||
];
|
||
// Sinnvolle Standardrichtung je Spalte
|
||
$default_order = ['name' => 'ASC', 'posts' => 'DESC', 'date' => 'DESC', 'new' => 'DESC'];
|
||
|
||
// Voreinstellung über Shortcode-Attribute, z. B. [guest_authors orderby="date" order="desc"]
|
||
$atts = shortcode_atts(['orderby' => 'name', 'order' => ''], $atts, 'guest_authors');
|
||
|
||
// URL-Parameter haben Vorrang vor den Shortcode-Attributen
|
||
$orderby = isset($_GET['ga_orderby']) ? sanitize_key(wp_unslash($_GET['ga_orderby'])) : sanitize_key($atts['orderby']);
|
||
if (!isset($valid_orderby[$orderby])) {
|
||
$orderby = 'name';
|
||
}
|
||
|
||
$order_raw = isset($_GET['ga_order']) ? sanitize_key(wp_unslash($_GET['ga_order'])) : sanitize_key($atts['order']);
|
||
$order = in_array(strtoupper($order_raw), ['ASC', 'DESC'], true) ? strtoupper($order_raw) : $default_order[$orderby];
|
||
|
||
$sql_orderby = $valid_orderby[$orderby];
|
||
|
||
// Alle Gastautoren mit Beitragsanzahl, erster und letzter Veröffentlichung
|
||
$query = "
|
||
SELECT pm.meta_value AS guest_author, COUNT(*) AS post_count,
|
||
MAX(p.post_date) AS last_post_date, MIN(p.post_date) AS first_post_date
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||
WHERE pm.meta_key = '_guest_author'
|
||
AND pm.meta_value != ''
|
||
AND p.post_status = 'publish'
|
||
GROUP BY pm.meta_value
|
||
ORDER BY {$sql_orderby} {$order}, pm.meta_value ASC
|
||
";
|
||
$guest_authors = $wpdb->get_results($query);
|
||
|
||
// Serien je Autor (leer, wenn es keine Serien-Taxonomie gibt)
|
||
$series_stats = wp_multi_get_guest_author_series_stats();
|
||
|
||
// URL der Gastautoren-Beitragsseite
|
||
$guest_author_page_id = get_option('wp_multi_guest_author_page_id');
|
||
$guest_author_page_url = $guest_author_page_id ? get_permalink($guest_author_page_id) : '#';
|
||
|
||
// Beschriftungen der Sortier-Schaltflächen
|
||
$sort_labels = [
|
||
'name' => __('Name', 'wp-multi'),
|
||
'posts' => __('Meiste Geschichten', 'wp-multi'),
|
||
'date' => __('Zuletzt veröffentlicht', 'wp-multi'),
|
||
'new' => __('Neueste Autoren', 'wp-multi'),
|
||
];
|
||
|
||
?>
|
||
<div id="guest-author-section" class="container mx-auto px-4 py-8">
|
||
<!-- Sortierung -->
|
||
<div class="guest-author-sort flex flex-wrap items-center gap-2 mb-6">
|
||
<span class="guest-author-sort-label text-sm text-gray-600"><?php _e('Sortieren nach:', 'wp-multi'); ?></span>
|
||
<?php foreach ($sort_labels as $key => $label) : ?>
|
||
<?php
|
||
$is_active = ($orderby === $key);
|
||
// Aktive Spalte kehrt die Richtung um, inaktive startet mit ihrem Standard
|
||
$next_order = $is_active ? (($order === 'ASC') ? 'DESC' : 'ASC') : $default_order[$key];
|
||
$sort_url = add_query_arg(['ga_orderby' => $key, 'ga_order' => strtolower($next_order)]) . '#guest-author-section';
|
||
?>
|
||
<a href="<?php echo esc_url($sort_url); ?>"
|
||
class="guest-author-sort-link<?php echo $is_active ? ' is-active' : ''; ?> px-3 py-1 rounded border text-sm <?php echo $is_active ? 'border-gray-800 font-semibold' : 'border-gray-300'; ?>">
|
||
<?php echo esc_html($label); ?>
|
||
<?php if ($is_active) : ?>
|
||
<span aria-hidden="true"><?php echo ($order === 'ASC') ? '↑' : '↓'; ?></span>
|
||
<?php endif; ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<!-- Gastautoren-Liste -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||
<?php if ($guest_authors) : ?>
|
||
<?php foreach ($guest_authors as $author) : ?>
|
||
<?php
|
||
$milestone = wp_multi_get_guest_author_milestone($author->post_count);
|
||
$series_count = (is_array($series_stats) && isset($series_stats[$author->guest_author])) ? $series_stats[$author->guest_author]['series_count'] : 0;
|
||
?>
|
||
<a href="<?php echo esc_url(add_query_arg('guest_author', $author->guest_author, $guest_author_page_url)); ?>" class="guest-author-card bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition">
|
||
<h3 class="text-xl font-semibold text-gray-800">
|
||
<?php echo esc_html($author->guest_author); ?>
|
||
<?php if ($milestone) : ?>
|
||
<span class="guest-author-milestone" title="<?php echo esc_attr($milestone['label_full']); ?>"><?php echo esc_html($milestone['emoji']); ?></span>
|
||
<?php endif; ?>
|
||
</h3>
|
||
<p class="text-gray-600">
|
||
<?php
|
||
/* translators: %s: Anzahl der Geschichten */
|
||
printf(esc_html(_n('%s Geschichte', '%s Geschichten', $author->post_count, 'wp-multi')), esc_html(number_format_i18n($author->post_count)));
|
||
?>
|
||
<?php if ($series_count) : ?>
|
||
·
|
||
<?php
|
||
/* translators: %s: Anzahl der Serien */
|
||
printf(esc_html(_n('%s Serie', '%s Serien', $series_count, 'wp-multi')), esc_html(number_format_i18n($series_count)));
|
||
?>
|
||
<?php endif; ?>
|
||
</p>
|
||
<?php if (!empty($author->last_post_date)) : ?>
|
||
<p class="guest-author-last-post text-sm text-gray-500"><?php printf(__('Zuletzt veröffentlicht: %s', 'wp-multi'), esc_html(mysql2date(get_option('date_format'), $author->last_post_date))); ?></p>
|
||
<?php endif; ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
<?php else : ?>
|
||
<p class="col-span-2 text-center text-gray-600"><?php _e('Keine Gastautoren gefunden.', 'wp-multi'); ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tailwind CSS CDN -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<?php
|
||
|
||
return ob_get_clean();
|
||
}
|
||
add_shortcode('guest_authors', 'wp_multi_guest_author_shortcode');
|
||
|
||
// Shortcode für das Autorenprofil
|
||
function wp_multi_guest_author_posts_shortcode() {
|
||
ob_start();
|
||
global $wpdb;
|
||
|
||
$guest_author = isset($_GET['guest_author']) ? sanitize_text_field(wp_unslash($_GET['guest_author'])) : '';
|
||
$post_count = $guest_author ? wp_multi_get_guest_author_post_count($guest_author) : 0;
|
||
|
||
$overview_url = wp_multi_get_guest_author_overview_url();
|
||
|
||
if (!$guest_author || !$post_count) {
|
||
?>
|
||
<div id="guest-author-posts" class="container mx-auto px-4 py-8">
|
||
<p class="text-gray-600">
|
||
<?php
|
||
if (!$guest_author) {
|
||
_e('Wählen Sie einen Gastautor aus, um die Geschichten anzuzeigen.', 'wp-multi');
|
||
} else {
|
||
_e('Von diesem Autor sind derzeit keine Geschichten veröffentlicht.', 'wp-multi');
|
||
}
|
||
?>
|
||
</p>
|
||
</div>
|
||
<?php
|
||
return ob_get_clean();
|
||
}
|
||
|
||
$milestone = wp_multi_get_guest_author_milestone($post_count);
|
||
$next_milestone = wp_multi_get_guest_author_next_milestone($post_count);
|
||
|
||
// Serien des Autors
|
||
$series_data = wp_multi_get_guest_author_series($guest_author);
|
||
$series = $series_data['series'];
|
||
$series_post_ids = $series_data['post_ids'];
|
||
|
||
// Neueste Einzelgeschichten (Serienteile stehen im eigenen Abschnitt)
|
||
$exclude_sql = $series_post_ids ? 'AND p.ID NOT IN (' . implode(',', $series_post_ids) . ')' : '';
|
||
$latest_limit = (int) apply_filters('wp_multi_author_profile_latest_limit', 12);
|
||
|
||
$latest = $wpdb->get_results($wpdb->prepare("
|
||
SELECT p.ID, p.post_title, p.post_date
|
||
FROM {$wpdb->posts} p
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author'
|
||
WHERE pm.meta_value = %s
|
||
AND p.post_status = 'publish'
|
||
{$exclude_sql}
|
||
ORDER BY p.post_date DESC
|
||
LIMIT %d
|
||
", $guest_author, $latest_limit));
|
||
|
||
// Beliebteste Geschichten – nur wenn Aufrufe vorliegen
|
||
$popular = [];
|
||
$analytics_table = wp_multi_get_analytics_table();
|
||
|
||
if ($analytics_table) {
|
||
$popular_limit = (int) apply_filters('wp_multi_author_profile_popular_limit', 5);
|
||
|
||
$popular = $wpdb->get_results($wpdb->prepare("
|
||
SELECT p.ID, p.post_title, COUNT(*) AS views
|
||
FROM {$analytics_table} a
|
||
INNER JOIN {$wpdb->posts} p ON p.ID = a.post_id
|
||
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_guest_author'
|
||
WHERE a.action = 'view'
|
||
AND pm.meta_value = %s
|
||
AND p.post_status = 'publish'
|
||
GROUP BY p.ID, p.post_title
|
||
ORDER BY views DESC
|
||
LIMIT %d
|
||
", $guest_author, $popular_limit));
|
||
}
|
||
?>
|
||
<div id="guest-author-posts" class="container mx-auto px-4 py-8">
|
||
<?php if ($overview_url) : ?>
|
||
<p class="mb-4"><a href="<?php echo esc_url($overview_url); ?>" class="text-sm">← <?php _e('Alle Autoren', 'wp-multi'); ?></a></p>
|
||
<?php endif; ?>
|
||
|
||
<h2 class="text-2xl font-semibold text-gray-800">
|
||
<?php echo esc_html($guest_author); ?>
|
||
<?php if ($milestone) : ?>
|
||
<span title="<?php echo esc_attr($milestone['label_full']); ?>"><?php echo esc_html($milestone['emoji']); ?></span>
|
||
<?php endif; ?>
|
||
</h2>
|
||
|
||
<p class="text-gray-600 mb-2">
|
||
<?php
|
||
/* translators: %s: Anzahl der Geschichten */
|
||
printf(esc_html(_n('%s Geschichte', '%s Geschichten', $post_count, 'wp-multi')), esc_html(number_format_i18n($post_count)));
|
||
?>
|
||
<?php if ($series) : ?>
|
||
·
|
||
<?php
|
||
/* translators: %s: Anzahl der Serien */
|
||
printf(esc_html(_n('%s Serie', '%s Serien', count($series), 'wp-multi')), esc_html(number_format_i18n(count($series))));
|
||
endif;
|
||
?>
|
||
</p>
|
||
|
||
<?php if ($milestone) : ?>
|
||
<p class="guest-author-milestone-badge text-lg font-semibold">
|
||
<?php echo esc_html($milestone['emoji'] . ' ' . $milestone['label']); ?>
|
||
<span class="guest-author-milestone-threshold text-sm font-normal text-gray-500">
|
||
<?php
|
||
/* translators: %d: Schwelle des Rangs */
|
||
printf(esc_html__('(ab %d Geschichten)', 'wp-multi'), (int) $milestone['threshold']);
|
||
?>
|
||
</span>
|
||
</p>
|
||
<?php endif; ?>
|
||
<?php if ($next_milestone) : ?>
|
||
<p class="guest-author-next-milestone text-sm text-gray-500 mb-6">
|
||
<?php
|
||
printf(
|
||
/* translators: 1: fehlende Geschichten, 2: Emoji des nächsten Meilensteins, 3: Anzahl des nächsten Meilensteins */
|
||
esc_html(_n('Noch %1$d Geschichte bis %2$s %3$s!', 'Noch %1$d Geschichten bis %2$s %3$s!', $next_milestone['missing'], 'wp-multi')),
|
||
$next_milestone['missing'],
|
||
esc_html($next_milestone['emoji']),
|
||
esc_html($next_milestone['label'])
|
||
);
|
||
?>
|
||
</p>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($series) : ?>
|
||
<h3 class="text-xl font-semibold text-gray-800 mt-8 mb-4"><?php _e('Serien', 'wp-multi'); ?></h3>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<?php foreach ($series as $row) : ?>
|
||
<?php $term_link = get_term_link((int) $row->term_id, $row->taxonomy); ?>
|
||
<div class="post-card bg-white p-6 rounded-lg shadow-md">
|
||
<h4 class="text-lg font-medium text-gray-800">
|
||
<?php if (!is_wp_error($term_link)) : ?>
|
||
<a href="<?php echo esc_url($term_link); ?>"><?php echo esc_html($row->series_name); ?></a>
|
||
<?php else : ?>
|
||
<?php echo esc_html($row->series_name); ?>
|
||
<?php endif; ?>
|
||
</h4>
|
||
<p class="text-gray-600">
|
||
<?php
|
||
/* translators: %s: Anzahl der Teile */
|
||
printf(esc_html(_n('%s Teil', '%s Teile', $row->total_parts, 'wp-multi')), esc_html(number_format_i18n($row->total_parts)));
|
||
?>
|
||
·
|
||
<?php printf(esc_html__('zuletzt %s', 'wp-multi'), esc_html(mysql2date(get_option('date_format'), $row->last_part_date))); ?>
|
||
</p>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($popular) : ?>
|
||
<h3 class="text-xl font-semibold text-gray-800 mt-8 mb-4"><?php _e('Beliebteste Geschichten', 'wp-multi'); ?></h3>
|
||
<ul class="guest-author-popular list-disc pl-5 text-gray-700">
|
||
<?php foreach ($popular as $row) : ?>
|
||
<li>
|
||
<a href="<?php echo esc_url(get_permalink((int) $row->ID)); ?>"><?php echo esc_html(get_the_title((int) $row->ID)); ?></a>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($latest) : ?>
|
||
<h3 class="text-xl font-semibold text-gray-800 mt-8 mb-4"><?php _e('Neueste Geschichten', 'wp-multi'); ?></h3>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<?php foreach ($latest as $row) : ?>
|
||
<div class="post-card bg-white p-6 rounded-lg shadow-md">
|
||
<h4 class="text-lg font-medium text-gray-800">
|
||
<a href="<?php echo esc_url(get_permalink((int) $row->ID)); ?>" class="hover:text-blue-600"><?php echo esc_html(get_the_title((int) $row->ID)); ?></a>
|
||
</h4>
|
||
<p class="text-gray-500 text-sm"><?php echo esc_html(mysql2date(get_option('date_format'), $row->post_date)); ?></p>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Tailwind CSS CDN -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<?php
|
||
|
||
return ob_get_clean();
|
||
}
|
||
add_shortcode('guest_author_posts', 'wp_multi_guest_author_posts_shortcode');
|
||
|