Files
wp-multi/includes/guest-authors.php
T
2026-08-23 13:35:10 +00:00

2040 lines
90 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 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 61180 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 12 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); ?>">&larr; <?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) : ?>
&middot;
<?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">&larr; <?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) : ?>
&middot;
<?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)));
?>
&middot;
<?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');