From b6ebdde2cf875678b974f4a973054764eda51f0e Mon Sep 17 00:00:00 2001
From: M_Viper
Date: Sun, 23 Aug 2026 15:23:16 +0000
Subject: [PATCH] Upload via GUI (40 Dateien)
---
includes/admin-menu.php | 1 +
includes/publication-stats.php | 696 +++++++++++++++++++++++++++++++++
2 files changed, 697 insertions(+)
create mode 100644 includes/publication-stats.php
diff --git a/includes/admin-menu.php b/includes/admin-menu.php
index e16522e..87a31d7 100644
--- a/includes/admin-menu.php
+++ b/includes/admin-menu.php
@@ -48,6 +48,7 @@ function wp_multi_register_admin_menus() {
// Analyse & Inhalte
$hooks[] = add_submenu_page($parent, __('Benutzer Analytics', 'wp-multi'), __('Benutzer-Analytics', 'wp-multi'), 'manage_options', 'wp_multi_analytics', 'wp_multi_display_user_analytics');
$hooks[] = add_submenu_page($parent, __('Gast-Autor Übersicht', 'wp-multi'), __('Gast-Autoren', 'wp-multi'), 'manage_options', 'guest_author_overview', 'wp_multi_guest_author_overview_page');
+ $hooks[] = add_submenu_page($parent, __('Veröffentlichungen', 'wp-multi'), __('Veröffentlichungen', 'wp-multi'), 'manage_options', 'wp-multi-publications', 'wp_multi_publication_stats_page');
$hooks[] = add_submenu_page($parent, __('Pinwand', 'wp-multi'), __('Pinwand', 'wp-multi'), 'manage_options', 'message-board', 'wp_multi_add_message_board');
$hooks[] = add_submenu_page($parent, __('Gemeldete Beiträge', 'wp-multi'), __('Gemeldete Beiträge', 'wp-multi'), 'manage_options', 'reported-posts', 'wp_stat_notice_reported_posts_page');
$hooks[] = add_submenu_page($parent, __('WP Stat & Notice', 'wp-multi'), __('Statistik & Notice', 'wp-multi'), 'manage_options', 'statistik_manager', 'statistik_manager_options_page');
diff --git a/includes/publication-stats.php b/includes/publication-stats.php
new file mode 100644
index 0000000..ba12f7f
--- /dev/null
+++ b/includes/publication-stats.php
@@ -0,0 +1,696 @@
+ __('Monat', 'wp-multi'),
+ 'year' => __('Jahr', 'wp-multi'),
+ 'timeline' => __('Autoren-Zeitlinie', 'wp-multi'),
+ ];
+}
+
+/**
+ * Beitragstyp, der gezählt wird.
+ */
+function wp_multi_get_stats_post_type() {
+ return apply_filters('wp_multi_stats_post_type', 'post');
+}
+
+/**
+ * Liefert alle Jahre mit veröffentlichten Beiträgen samt Anzahl.
+ *
+ * @return array Zeilen mit year und total, aufsteigend nach Jahr.
+ */
+function wp_multi_get_publication_years() {
+ static $years = null;
+
+ if ($years !== null) {
+ return $years;
+ }
+
+ global $wpdb;
+
+ $years = $wpdb->get_results($wpdb->prepare("
+ SELECT YEAR(post_date) AS year, COUNT(*) AS total
+ FROM {$wpdb->posts}
+ WHERE post_status = 'publish'
+ AND post_type = %s
+ GROUP BY YEAR(post_date)
+ ORDER BY year ASC
+ ", wp_multi_get_stats_post_type()));
+
+ return $years;
+}
+
+/**
+ * Callback für den Menüpunkt "Veröffentlichungen".
+ */
+function wp_multi_publication_stats_page() {
+ $tabs = wp_multi_get_publication_stats_tabs();
+ $current_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'month';
+ if (!isset($tabs[$current_tab])) {
+ $current_tab = 'month';
+ }
+
+ $base_url = add_query_arg(['page' => 'wp-multi-publications'], admin_url('admin.php'));
+
+ wpmt_admin_page_open(
+ __('Veröffentlichungen', 'wp-multi'),
+ __('Wie viele Geschichten pro Monat und pro Jahr veröffentlicht wurden', 'wp-multi')
+ );
+ ?>
+
+ $tab_label) : ?>
+
+
+
+
+
+ mit prozentualer Breite reicht,
+ * lädt nichts nach und funktioniert auch ohne JavaScript.
+ *
+ * @param string $label Beschriftung links.
+ * @param int $value Wert.
+ * @param int $max Größter Wert der Reihe (für die Balkenbreite).
+ */
+function wp_multi_render_stat_bar($label, $value, $max) {
+ $percent = $max > 0 ? round(($value / $max) * 100) : 0;
+ ?>
+
+
+
+
+
+
+
+ ' . esc_html__('Noch keine veröffentlichten Beiträge gefunden.', 'wp-multi') . '
';
+ wpmt_admin_card_close();
+ return;
+ }
+
+ $available_years = [];
+ foreach ($years as $row) {
+ $available_years[] = (int) $row->year;
+ }
+
+ // Jahr aus der URL, sonst das jüngste Jahr mit Beiträgen
+ $selected_year = isset($_GET['stats_year']) ? (int) $_GET['stats_year'] : 0;
+ if (!in_array($selected_year, $available_years, true)) {
+ $selected_year = end($available_years);
+ }
+
+ $months = $wpdb->get_results($wpdb->prepare("
+ SELECT MONTH(post_date) AS month, COUNT(*) AS total
+ FROM {$wpdb->posts}
+ WHERE post_status = 'publish'
+ AND post_type = %s
+ AND YEAR(post_date) = %d
+ GROUP BY MONTH(post_date)
+ ORDER BY month ASC
+ ", wp_multi_get_stats_post_type(), $selected_year));
+
+ $month_totals = array_fill(1, 12, 0);
+ $max_month_total = 0;
+ $year_total = 0;
+
+ foreach ((array) $months as $row) {
+ $month_totals[(int) $row->month] = (int) $row->total;
+ $year_total += (int) $row->total;
+ if ((int) $row->total > $max_month_total) {
+ $max_month_total = (int) $row->total;
+ }
+ }
+
+ $month_url = add_query_arg('tab', 'month', $base_url);
+
+ wpmt_admin_card_open(__('Veröffentlichungen pro Monat', 'wp-multi'));
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ get_month_abbrev($wp_locale->get_month($month)), $month_totals[$month], $max_month_total); ?>
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+ | get_month($month)); ?> |
+
+
+
+
+ 0
+
+ |
+
+
+
+
+
+ ' . esc_html__('Noch keine veröffentlichten Beiträge gefunden.', 'wp-multi') . '';
+ wpmt_admin_card_close();
+ return;
+ }
+
+ $max_total = 0;
+ $grand_total = 0;
+ $totals_by_year = [];
+
+ foreach ($years as $row) {
+ $totals_by_year[(int) $row->year] = (int) $row->total;
+ $grand_total += (int) $row->total;
+ if ((int) $row->total > $max_total) {
+ $max_total = (int) $row->total;
+ }
+ }
+
+ $year_keys = array_keys($totals_by_year);
+ $month_url = add_query_arg('tab', 'month', $base_url);
+ ?>
+
+
+
+
+ $total) : ?>
+
+
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+ $total) : ?>
+
+ |
+
+
+
+ |
+ |
+
+ —';
+ } else {
+ $diff = $total - $totals_by_year[$year - 1];
+ $sign = $diff > 0 ? '+' : '';
+ echo '' . esc_html($sign . number_format_i18n($diff)) . '';
+ }
+ ?>
+ |
+
+
+
+
+
+ '🔥 ' . __('Verlorene Vielschreiber', 'wp-multi'),
+ 'active' => '🟢 ' . __('Aktive Autoren', 'wp-multi'),
+ 'new' => '🆕 ' . __('Neue Autoren', 'wp-multi'),
+ 'quiet' => '🟡 ' . __('Stille Autoren', 'wp-multi'),
+ 'one_off' => '💤 ' . __('Einmalige Autoren', 'wp-multi'),
+ ];
+}
+
+/**
+ * Ordnet einen Autor anhand seiner Jahreswerte einer Gruppe zu.
+ *
+ * 🆕 Neue Autoren – erste Geschichte im aktuellen oder letzten Jahr
+ * 🔥 Verlorene Vielschreiber – früher ein starkes Jahr, seitdem so gut wie nichts
+ * 🟢 Aktive Autoren – im aktuellen oder letzten Jahr veröffentlicht
+ * 💤 Einmalige Autoren – höchstens zwei Geschichten und lange nichts mehr
+ * 🟡 Stille Autoren – alles dazwischen: mehr als zwei Geschichten, aber still
+ *
+ * @param array $per_year Zuordnung "Jahr => Anzahl".
+ * @param int $current_year Aktuelles Jahr.
+ * @return string Gruppenschlüssel.
+ */
+function wp_multi_get_author_timeline_group($per_year, $current_year) {
+ // Die letzten beiden Jahre gelten als "jetzt"
+ $recent_span = (int) apply_filters('wp_multi_timeline_recent_years', 2);
+ $recent_from = $current_year - ($recent_span - 1);
+
+ $total = 0;
+ $recent_total = 0;
+ $earlier_peak = 0;
+ $first_year = null;
+
+ foreach ($per_year as $year => $count) {
+ $year = (int) $year;
+ $count = (int) $count;
+ $total += $count;
+
+ if ($first_year === null || $year < $first_year) {
+ $first_year = $year;
+ }
+
+ if ($year >= $recent_from) {
+ $recent_total += $count;
+ } elseif ($count > $earlier_peak) {
+ $earlier_peak = $count;
+ }
+ }
+
+ // Wer erst im "Jetzt"-Fenster angefangen hat, ist neu
+ if ($first_year !== null && $first_year >= $recent_from) {
+ return 'new';
+ }
+
+ $prolific_min = (int) apply_filters('wp_multi_timeline_prolific_min', 5);
+ $recent_max = (int) apply_filters('wp_multi_timeline_recent_max', 1);
+
+ if ($earlier_peak >= $prolific_min && $recent_total <= $recent_max) {
+ return 'lost';
+ }
+
+ if ($recent_total > 0) {
+ return 'active';
+ }
+
+ if ($total <= 2) {
+ return 'one_off';
+ }
+
+ return 'quiet';
+}
+
+/**
+ * Liest die Veröffentlichungen je Autor und Jahr.
+ *
+ * @return array Zuordnung "Autor => [Jahr => Anzahl]".
+ */
+function wp_multi_get_author_timeline_data() {
+ static $data = null;
+
+ if ($data !== null) {
+ return $data;
+ }
+
+ global $wpdb;
+
+ $rows = $wpdb->get_results($wpdb->prepare("
+ SELECT pm.meta_value AS guest_author, YEAR(p.post_date) AS year, COUNT(*) AS total
+ 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'
+ AND p.post_type = %s
+ GROUP BY pm.meta_value, YEAR(p.post_date)
+ ", wp_multi_get_stats_post_type()));
+
+ $data = [];
+ foreach ((array) $rows as $row) {
+ $data[$row->guest_author][(int) $row->year] = (int) $row->total;
+ }
+
+ return $data;
+}
+
+/**
+ * Reiter "Autoren-Zeitlinie": wer wann geschrieben hat und wer abgewandert ist.
+ *
+ * @param string $base_url Basis-URL der Seite.
+ */
+function wp_multi_render_author_timeline($base_url) {
+ $years = wp_multi_get_publication_years();
+ $timeline = wp_multi_get_author_timeline_data();
+
+ if (!$years || !$timeline) {
+ wpmt_admin_card_open(__('Autoren-Zeitlinie', 'wp-multi'));
+ echo '' . esc_html__('Noch keine Daten für eine Zeitlinie vorhanden.', 'wp-multi') . '
';
+ wpmt_admin_card_close();
+ return;
+ }
+
+ $available_years = [];
+ foreach ($years as $row) {
+ $available_years[] = (int) $row->year;
+ }
+
+ $current_year = (int) end($available_years);
+
+ // Nur die letzten Jahre als Spalten, sonst wird die Tabelle unlesbar
+ $columns = (int) apply_filters('wp_multi_timeline_columns', 6);
+ $column_years = array_slice($available_years, -$columns);
+
+ $groups = wp_multi_get_timeline_groups();
+ $group_filter = isset($_GET['group']) ? sanitize_key($_GET['group']) : '';
+ if (!isset($groups[$group_filter])) {
+ $group_filter = '';
+ }
+
+ // Autoren aufbereiten: Gruppe, Gesamtzahl und Jahreswerte
+ $authors = [];
+ $group_counts = array_fill_keys(array_keys($groups), 0);
+
+ foreach ($timeline as $name => $per_year) {
+ $group = wp_multi_get_author_timeline_group($per_year, $current_year);
+ $group_counts[$group]++;
+
+ $total = 0;
+ foreach ($per_year as $count) {
+ $total += (int) $count;
+ }
+
+ if ($group_filter !== '' && $group !== $group_filter) {
+ continue;
+ }
+
+ $authors[] = [
+ 'name' => $name,
+ 'group' => $group,
+ 'total' => $total,
+ 'per_year' => $per_year,
+ ];
+ }
+
+ // Nach Gesamtzahl sortieren – die produktivsten zuerst
+ usort($authors, function ($a, $b) {
+ if ($a['total'] === $b['total']) {
+ return strcasecmp($a['name'], $b['name']);
+ }
+
+ return $b['total'] - $a['total'];
+ });
+
+ $timeline_url = add_query_arg('tab', 'timeline', $base_url);
+ $author_base_url = add_query_arg(['page' => 'guest_author_overview'], admin_url('admin.php'));
+
+ wpmt_admin_card_open(__('Autoren-Zeitlinie', 'wp-multi'));
+ ?>
+
+
+
+
+
+
+
+ $label) : ?>
+ |
+
+
+ ()
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+ |
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+ ·
+
+ |
+
+ |
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+ [Jahr => Anzahl]".
+ * @param string $author_base_url Basis-URL der Autoren-Detailansicht.
+ */
+function wp_multi_render_year_comparison($base_url, $available_years, $timeline, $author_base_url) {
+ if (count($available_years) < 2) {
+ return;
+ }
+
+ // Standard: die beiden letzten abgeschlossenen Jahre
+ $default_to = $available_years[count($available_years) - 2];
+ $default_from = $available_years[count($available_years) - 3] ?? $available_years[0];
+
+ $from = isset($_GET['cmp_from']) ? (int) $_GET['cmp_from'] : $default_from;
+ $to = isset($_GET['cmp_to']) ? (int) $_GET['cmp_to'] : $default_to;
+
+ if (!in_array($from, $available_years, true)) {
+ $from = $default_from;
+ }
+ if (!in_array($to, $available_years, true)) {
+ $to = $default_to;
+ }
+
+ $rows = [];
+ $sum_from = 0;
+ $sum_to = 0;
+
+ foreach ($timeline as $name => $per_year) {
+ $count_from = isset($per_year[$from]) ? (int) $per_year[$from] : 0;
+ $count_to = isset($per_year[$to]) ? (int) $per_year[$to] : 0;
+
+ $sum_from += $count_from;
+ $sum_to += $count_to;
+
+ if ($count_from === $count_to) {
+ continue;
+ }
+
+ $rows[] = [
+ 'name' => $name,
+ 'from' => $count_from,
+ 'to' => $count_to,
+ 'diff' => $count_to - $count_from,
+ ];
+ }
+
+ // Größte Rückgänge zuerst
+ usort($rows, function ($a, $b) {
+ if ($a['diff'] === $b['diff']) {
+ return strcasecmp($a['name'], $b['name']);
+ }
+
+ return $a['diff'] - $b['diff'];
+ });
+
+ $compare_url = add_query_arg('tab', 'timeline', $base_url);
+ $total_diff = $sum_to - $sum_from;
+
+ wpmt_admin_card_open(__('Jahresvergleich', 'wp-multi'));
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 ? '+' : '') . number_format_i18n($total_diff))
+ );
+ ?>
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+ +
+
+ |
+
+
+
+ |
+
+
+
+
+
+