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') + ); + ?> + + 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)) + ); + ?> +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+

+