Files
WP-Business-Forum/admin/forum-admin.php
2026-03-22 00:40:15 +01:00

3651 lines
298 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
/**
* forum-admin.php
* Backend-Verwaltung für WP Business Forum.
* Enthält: Dashboard, Rollen, Kategorien, Mitglieder, Meldungen.
* Einstellungen (wbf_admin_settings + wbf_get_settings) → admin/forum-settings.php
*/
if ( ! defined( 'ABSPATH' ) ) exit;
// ── Menü ──────────────────────────────────────────────────────────────────────
add_action( 'admin_menu', function() {
add_menu_page(
'Business Forum', 'Business Forum', 'manage_options',
'wbf-admin', 'wbf_admin_page', 'dashicons-format-chat', 30
);
add_submenu_page( 'wbf-admin', 'Übersicht', 'Übersicht', 'manage_options', 'wbf-admin', 'wbf_admin_page' );
add_submenu_page( 'wbf-admin', 'Kategorien', 'Kategorien', 'manage_options', 'wbf-categories', 'wbf_admin_categories' );
add_submenu_page( 'wbf-admin', 'Rollen', 'Rollen', 'manage_options', 'wbf-roles', 'wbf_admin_roles' );
add_submenu_page( 'wbf-admin', 'Level', 'Level', 'manage_options', 'wbf-levels', 'wbf_admin_levels' );
add_submenu_page( 'wbf-admin', 'Mitglieder', 'Mitglieder', 'manage_options', 'wbf-members', 'wbf_admin_members' );
add_submenu_page( 'wbf-admin', 'Meldungen', 'Meldungen', 'manage_options', 'wbf-reports', 'wbf_admin_reports' );
add_submenu_page( 'wbf-admin', 'Profilfelder', 'Profilfelder', 'manage_options', 'wbf-profile-fields', 'wbf_admin_profile_fields' );
add_submenu_page( 'wbf-admin', 'Einstellungen', 'Einstellungen', 'manage_options', 'wbf-settings', 'wbf_admin_settings' );
add_submenu_page( 'wbf-admin', 'Reaktionen', 'Reaktionen', 'manage_options', 'wbf-reactions', 'wbf_admin_reactions' );
add_submenu_page( 'wbf-admin', 'Einladungen', 'Einladungen', 'manage_options', 'wbf-invites', 'wbf_admin_invites' );
add_submenu_page( 'wbf-admin', 'Statistiken', 'Statistiken', 'manage_options', 'wbf-stats', 'wbf_admin_stats' );
add_submenu_page( 'wbf-admin', 'Papierkorb', 'Papierkorb', 'manage_options', 'wbf-trash', 'wbf_admin_trash' );
add_submenu_page( 'wbf-admin', 'Thread-Präfixe','Thread-Präfixe','manage_options', 'wbf-prefixes', 'wbf_admin_prefixes' );
add_submenu_page( 'wbf-admin', 'Wortfilter', 'Wortfilter', 'manage_options', 'wbf-wordfilter', 'wbf_admin_wordfilter' );
add_submenu_page( 'wbf-admin', 'Export / Import','Export / Import','manage_options', 'wbf-export', 'wbf_admin_export' );
add_submenu_page( 'wbf-admin', '⚠️ Deinstallieren', '⚠️ Deinstallieren', 'manage_options', 'wbf-uninstall', 'wbf_admin_uninstall' );
add_submenu_page( 'wbf-admin', '🔔 Updates', '🔔 Updates', 'manage_options', 'wbf-updates', 'wbf_admin_updates' );
}, 10 );
// Meldungs-Badge im Menü (separater Hook mit Priorität 999, läuft nach der Registrierung)
add_action( 'admin_menu', function() {
$count = WBF_DB::count_open_reports();
if ( $count < 1 ) return;
global $submenu;
if ( ! isset( $submenu['wbf-admin'] ) ) return;
foreach ( $submenu['wbf-admin'] as &$item ) {
if ( $item[2] === 'wbf-reports' ) {
$item[0] .= ' <span class="awaiting-mod">' . (int) $count . '</span>';
break;
}
}
}, 999 );
// ── Wartungsmodus-Toggle via admin_init (läuft vor WordPress-Output/Headern) ──
add_action( 'admin_init', function() {
if ( ! isset( $_POST['wbf_dash_toggle_maint'] ) ) return;
if ( ! check_admin_referer( 'wbf_dash_maint_nonce' ) ) return;
if ( ! current_user_can( 'manage_options' ) ) return;
$s = wbf_get_settings();
$s['maintenance_mode'] = ( ( $s['maintenance_mode'] ?? '0' ) === '1' ) ? '0' : '1';
update_option( 'wbf_settings', $s );
wp_safe_redirect( add_query_arg( 'page', 'wbf-admin', admin_url( 'admin.php' ) ) );
exit;
} );
// ── Export via admin_init (läuft vor WordPress-Output/Headern) ────────────────
add_action( 'admin_init', function() {
if ( ! isset( $_POST['wbf_do_export'] ) ) return;
if ( ! check_admin_referer( 'wbf_export_nonce' ) ) return;
if ( ! current_user_can( 'manage_options' ) ) return;
$sections = array_keys( array_filter( $_POST['export_sections'] ?? [] ) );
$data = [
'_meta' => [
'plugin' => 'WP Business Forum',
'version' => WBF_VERSION,
'exported' => date('c'),
'site' => get_bloginfo('url'),
'sections' => $sections,
],
];
global $wpdb;
foreach ( $sections as $sec ) {
switch ( $sec ) {
case 'settings':
$data['settings'] = get_option('wbf_settings', []);
$data['profile_fields'] = get_option('wbf_profile_fields', []);
$data['reactions_cfg'] = get_option('wbf_reactions', []);
break;
case 'roles':
$data['roles'] = get_option('wbf_custom_roles', []);
break;
case 'levels':
$data['levels'] = [
'config' => get_option('wbf_level_config', []),
'enabled' => get_option('wbf_levels_enabled', true),
];
break;
case 'categories':
$data['categories'] = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}forum_categories ORDER BY parent_id ASC, sort_order ASC",
ARRAY_A
);
break;
case 'users':
$data['users'] = $wpdb->get_results(
"SELECT id, username, email, password, display_name, avatar_url, bio, signature,
role, pre_ban_role, ban_reason, ban_until, post_count,
registered, last_active, profile_public
FROM {$wpdb->prefix}forum_users ORDER BY id ASC",
ARRAY_A
);
$data['user_meta'] = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}forum_user_meta ORDER BY user_id ASC",
ARRAY_A
);
break;
case 'threads':
$data['threads'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_threads ORDER BY id ASC", ARRAY_A );
$data['posts'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_posts ORDER BY id ASC", ARRAY_A );
$data['thread_tags'] = $wpdb->get_results(
"SELECT tt.*, t.name, t.slug, t.use_count
FROM {$wpdb->prefix}forum_thread_tags tt
JOIN {$wpdb->prefix}forum_tags t ON t.id = tt.tag_id
ORDER BY tt.thread_id ASC",
ARRAY_A
);
$data['subscriptions'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_subscriptions ORDER BY id ASC", ARRAY_A );
break;
case 'polls':
$data['polls'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_polls ORDER BY id ASC", ARRAY_A );
$data['poll_votes'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_poll_votes ORDER BY id ASC", ARRAY_A );
break;
case 'bookmarks':
$data['bookmarks'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_bookmarks ORDER BY id ASC", ARRAY_A );
break;
case 'prefixes':
$data['prefixes'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_prefixes ORDER BY sort_order ASC", ARRAY_A );
break;
case 'interactions':
$data['likes'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_likes ORDER BY id ASC", ARRAY_A );
$data['reactions'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_reactions ORDER BY id ASC", ARRAY_A );
$data['notifications'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_notifications ORDER BY id ASC", ARRAY_A );
break;
case 'messages':
$data['messages'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_messages ORDER BY id ASC", ARRAY_A );
break;
case 'reports':
$data['reports'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_reports ORDER BY id ASC", ARRAY_A );
break;
case 'invites':
$data['invites'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_invites ORDER BY id ASC", ARRAY_A );
break;
case 'ignore_list':
$data['ignore_list'] = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}forum_ignore_list ORDER BY id ASC", ARRAY_A );
break;
}
}
$json = wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
$filename = 'wbf-backup-' . date('Y-m-d-His') . '.json';
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Content-Length: ' . strlen( $json ) );
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
echo $json;
exit;
} );
// ── Inline Styles ─────────────────────────────────────────────────────────────
add_action( 'admin_head', function() {
if ( ! isset( $_GET['page'] ) || strpos( $_GET['page'], 'wbf-' ) === false ) return;
echo '<style>
/* ─── Shared (other pages) ───────────────────────────── */
.wbf-role-preview{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:4px;border:1px solid;font-size:12px;font-weight:700}
.wbf-perm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:6px;margin-top:6px}
.wbf-perm-item{display:flex;align-items:center;gap:6px;font-size:13px}
.wbf-color-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
/* ─── Dashboard Root ──────────────────────────────────── */
body.wp-admin{overflow-x:hidden!important}
.wbf-d{--accent:#0ea5e9;--accent2:#6366f1;--green:#10b981;--red:#ef4444;--orange:#f97316;--purple:#a855f7;--gap:18px;box-sizing:border-box;width:100%;padding-bottom:40px;padding-right:30px}
.wbf-d *{box-sizing:border-box}
/* ─── Top bar ────────────────────────────────────────── */
.wbf-topbar{background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);border-radius:14px;padding:20px 24px;margin-bottom:var(--gap);display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
.wbf-topbar__left{display:flex;align-items:center;gap:14px}
.wbf-topbar__logo{width:42px;height:42px;border-radius:10px;background:linear-gradient(135deg,var(--accent),#6366f1);display:flex;align-items:center;justify-content:center;font-size:1.1rem;color:#fff;flex-shrink:0}
.wbf-topbar__title{color:#f8fafc;font-size:1.15rem;font-weight:800;letter-spacing:-.02em;margin:0}
.wbf-topbar__sub{color:#94a3b8;font-size:.75rem;margin-top:1px}
.wbf-topbar__actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.wbf-topbar__btn{display:inline-flex;align-items:center;gap:7px;padding:7px 14px;border-radius:8px;font-size:.8rem;font-weight:600;text-decoration:none;border:1.5px solid;transition:all .15s;cursor:pointer}
.wbf-topbar__btn--outline{color:#cbd5e1;border-color:#334155;background:transparent}
.wbf-topbar__btn--outline:hover{background:#1e293b;color:#fff;border-color:#475569}
.wbf-topbar__btn--primary{color:#fff;border-color:var(--accent);background:var(--accent)}
.wbf-topbar__btn--primary:hover{background:#0284c7;border-color:#0284c7;color:#fff}
.wbf-topbar__btn--danger{color:#fca5a5;border-color:#7f1d1d;background:rgba(239,68,68,.15)}
.wbf-topbar__btn--danger:hover{background:rgba(239,68,68,.25)}
.wbf-topbar__badge{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:6px;font-size:.72rem;font-weight:700}
.wbf-topbar__badge--warn{background:rgba(239,68,68,.2);color:#fca5a5;border:1px solid rgba(239,68,68,.3)}
.wbf-topbar__badge--ok{background:rgba(16,185,129,.15);color:#6ee7b7;border:1px solid rgba(16,185,129,.2)}
/* ─── System bar ─────────────────────────────────────── */
.wbf-sysbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:9px 16px;margin-bottom:var(--gap)}
.wbf-sysbar__label{font-size:.65rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-right:6px;white-space:nowrap}
.wbf-sbadge{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:20px;font-size:.72rem;font-weight:600;border:1px solid;white-space:nowrap}
.wbf-sbadge--ok{color:#15803d;background:#f0fdf4;border-color:#86efac}
.wbf-sbadge--warn{color:#b45309;background:#fffbeb;border-color:#fde68a}
.wbf-sbadge--err{color:#dc2626;background:#fef2f2;border-color:#fca5a5}
.wbf-sbadge--inf{color:#0369a1;background:#f0f9ff;border-color:#bae6fd}
.wbf-sdivider{width:1px;height:14px;background:#e2e8f0;flex-shrink:0}
/* ─── KPI row ────────────────────────────────────────── */
.wbf-kpi-row{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:var(--gap);margin-bottom:var(--gap)}
.wbf-kpi{position:relative;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:18px 20px;overflow:hidden;transition:box-shadow .15s,transform .12s;text-decoration:none;display:block}
.wbf-kpi:hover{box-shadow:0 6px 24px rgba(0,0,0,.09);transform:translateY(-1px)}
.wbf-kpi::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;border-radius:12px 12px 0 0;background:var(--kc,#e2e8f0)}
.wbf-kpi__top{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.wbf-kpi__icon{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.95rem}
.wbf-kpi__trend{font-size:.68rem;font-weight:700;padding:2px 7px;border-radius:20px;display:inline-flex;align-items:center;gap:3px}
.wbf-kpi__trend--up{background:#f0fdf4;color:#15803d}
.wbf-kpi__trend--down{background:#fef2f2;color:#dc2626}
.wbf-kpi__trend--flat{background:#f8fafc;color:#64748b}
.wbf-kpi__val{font-size:2rem;font-weight:800;line-height:1;color:#0f172a;letter-spacing:-.03em}
.wbf-kpi__label{font-size:.74rem;color:#64748b;margin-top:4px;font-weight:500}
/* ─── Main layout ────────────────────────────────────── */
.wbf-main{display:grid;grid-template-columns:200px minmax(0,1fr) minmax(0,1fr) 260px;gap:var(--gap);margin-bottom:var(--gap);align-items:start;grid-auto-rows:min-content}
.wbf-bottom{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap)}
/* ─── Card ───────────────────────────────────────────── */
.wbf-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden}
.wbf-card__head{padding:13px 18px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:8px;background:#fafafa}
.wbf-card__head-title{font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:#374151;flex:1}
.wbf-card__head-link{font-size:.72rem;color:var(--accent);font-weight:600;text-decoration:none;white-space:nowrap}
.wbf-card__head-link:hover{text-decoration:underline}
.wbf-card__head-icon{color:var(--accent)}
.wbf-card__body{padding:14px 18px}
.wbf-card__body--tight{padding:8px 18px}
/* ─── Nav links ──────────────────────────────────────── */
.wbf-nav{display:flex;flex-direction:column;gap:3px}
.wbf-nav__item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;text-decoration:none;font-size:.83rem;font-weight:500;color:#374151;transition:background .1s,color .1s;border:1px solid transparent}
.wbf-nav__item:hover{background:#f0f9ff;color:var(--accent);border-color:#e0f2fe}
.wbf-nav__item i{width:16px;text-align:center;font-size:.85rem;color:#94a3b8;flex-shrink:0;transition:color .1s}
.wbf-nav__item:hover i{color:var(--accent)}
.wbf-nav__badge{margin-left:auto;background:#ef4444;color:#fff;font-size:.62rem;font-weight:700;padding:1px 6px;border-radius:20px;line-height:1.4}
.wbf-nav__sep{height:1px;background:#f1f5f9;margin:6px 0}
.wbf-nav__danger{color:#dc2626!important;border-color:#fecaca!important;background:#fef2f2!important}
.wbf-nav__danger i{color:#dc2626!important}
/* ─── Role chips ─────────────────────────────────────── */
.wbf-roles{display:flex;flex-wrap:wrap;gap:6px;padding:12px 18px 14px}
.wbf-rchip{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;border:1.5px solid;font-size:.75rem;font-weight:700;text-decoration:none;transition:opacity .15s}
.wbf-rchip:hover{opacity:.75}
/* ─── Activity feed ──────────────────────────────────── */
.wbf-feed{list-style:none;margin:0;padding:0}
.wbf-feed__item{display:flex;align-items:flex-start;gap:10px;padding:9px 0;border-bottom:1px solid #f1f5f9}
.wbf-feed__item:last-child{border-bottom:none;padding-bottom:0}
.wbf-feed__dot{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.78rem;flex-shrink:0;margin-top:1px}
.wbf-feed__dot--post{background:#eff6ff;color:#3b82f6}
.wbf-feed__dot--thread{background:#f0fdf4;color:#22c55e}
.wbf-feed__dot--register{background:#fdf4ff;color:var(--purple)}
.wbf-feed__dot--report{background:#fff7ed;color:var(--orange)}
.wbf-feed__body{flex:1;min-width:0}
.wbf-feed__name{font-weight:600;font-size:.82rem;color:#0f172a}
.wbf-feed__action{font-size:.82rem;color:#64748b}
.wbf-feed__sub{font-size:.72rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;margin-top:1px}
.wbf-feed__time{font-size:.68rem;color:#94a3b8;flex-shrink:0;padding-top:3px;white-space:nowrap}
/* ─── Threads / Members ──────────────────────────────── */
.wbf-list{list-style:none;margin:0;padding:0}
.wbf-list__item{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid #f1f5f9;font-size:.82rem}
.wbf-list__item:last-child{border-bottom:none;padding-bottom:0}
.wbf-list__body{flex:1;min-width:0}
.wbf-list__title{font-weight:600;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;text-decoration:none}
.wbf-list__title:hover{color:var(--accent)}
.wbf-list__meta{font-size:.7rem;color:#94a3b8;margin-top:1px}
.wbf-list__aside{font-size:.71rem;color:#94a3b8;white-space:nowrap;flex-shrink:0}
/* ─── Reports ────────────────────────────────────────── */
.wbf-report-item{padding:10px 0;border-bottom:1px solid #f1f5f9}
.wbf-report-item:last-child{border-bottom:none;padding-bottom:0}
.wbf-report-item__head{display:flex;align-items:center;gap:6px;margin-bottom:5px}
.wbf-rtag{font-size:.67rem;font-weight:700;padding:1px 7px;border-radius:10px;background:#fff7ed;color:var(--orange);border:1px solid #fed7aa;white-space:nowrap}
.wbf-report-item__text{font-size:.75rem;color:#64748b;background:#f8fafc;border-left:3px solid #e2e8f0;padding:3px 8px;border-radius:0 4px 4px 0;margin-bottom:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wbf-report-item__actions{display:flex;gap:5px}
.wbf-ra{font-size:.68rem;padding:2px 8px;border-radius:5px;border:1px solid;text-decoration:none;font-weight:600;transition:opacity .12s}
.wbf-ra:hover{opacity:.8}
.wbf-ra--ok{color:#15803d;border-color:#86efac;background:#f0fdf4}
.wbf-ra--skip{color:#64748b;border-color:#cbd5e1;background:#f8fafc}
.wbf-ra--go{color:#0369a1;border-color:#bae6fd;background:#f0f9ff}
.wbf-noreports{text-align:center;padding:28px 0;color:#94a3b8}
.wbf-noreports i{font-size:2rem;opacity:.25;display:block;margin-bottom:8px}
/* ─── Bottom panels ──────────────────────────────────── */
.wbf-cats{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:9px}
.wbf-cats__bar-head{display:flex;justify-content:space-between;font-size:.78rem;color:#374151;margin-bottom:4px}
.wbf-cats__track{height:5px;background:#f1f5f9;border-radius:4px}
.wbf-cats__fill{height:100%;border-radius:4px}
.wbf-trend-mini{display:flex;flex-direction:column;gap:10px}
.wbf-trend-mini__item{background:#f8fafc;border:1px solid #f1f5f9;border-radius:9px;padding:11px 14px}
.wbf-trend-mini__label{font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;margin-bottom:5px}
.wbf-trend-mini__row{display:flex;align-items:baseline;gap:8px}
.wbf-trend-mini__val{font-size:1.5rem;font-weight:800;color:#0f172a;line-height:1}
.wbf-trend-mini__badge{font-size:.68rem;font-weight:700;padding:2px 7px;border-radius:20px;display:inline-flex;align-items:center;gap:3px}
.wbf-tu{background:#f0fdf4;color:#15803d}
.wbf-td{background:#fef2f2;color:#dc2626}
.wbf-tf{background:#f8fafc;color:#64748b}
.wbf-maint-box{background:#f8fafc;border:1px solid #f1f5f9;border-radius:9px;padding:14px;margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;gap:10px}
.wbf-maint-state{font-size:.86rem;font-weight:700}
.wbf-maint-hint{font-size:.72rem;color:#94a3b8;margin-top:2px}
.wbf-toggle{position:relative;display:inline-block;width:40px;height:22px;flex-shrink:0}
.wbf-toggle input{opacity:0;width:0;height:0}
.wbf-toggle__track{position:absolute;cursor:pointer;inset:0;background:#cbd5e1;border-radius:22px;transition:.2s}
.wbf-toggle__track:before{content:"";position:absolute;width:16px;height:16px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s;box-shadow:0 1px 4px rgba(0,0,0,.25)}
input:checked + .wbf-toggle__track{background:#ef4444}
input:checked + .wbf-toggle__track:before{transform:translateX(18px)}
.wbf-pinfo{display:flex;flex-direction:column;gap:0}
.wbf-pinfo__row{display:flex;justify-content:space-between;align-items:center;font-size:.78rem;padding:6px 0;border-bottom:1px solid #f1f5f9}
.wbf-pinfo__row:last-child{border-bottom:none}
.wbf-pinfo__row span:first-child{color:#64748b}
.wbf-pinfo__row strong{color:#0f172a}
.wbf-ver{display:inline-flex;align-items:center;gap:4px;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;font-size:.7rem;font-weight:700;padding:2px 8px;border-radius:20px}
.wbf-banned-row{display:flex;align-items:center;gap:9px;font-size:.8rem;padding:7px 0;border-bottom:1px solid #f1f5f9}
.wbf-banned-row:last-child{border-bottom:none}
.wbf-banned-row img{border-radius:50%;flex-shrink:0;object-fit:cover}
</style>';
} );
// ── Dashboard ─────────────────────────────────────────────────────────────────
function wbf_admin_page() {
global $wpdb;
$stats = WBF_DB::get_stats();
$roles = WBF_Roles::get_sorted();
$open_reports = WBF_DB::count_open_reports();
$recent = WBF_DB::get_recent_threads(8);
$members = WBF_DB::get_all_users(1);
$admin_url = admin_url('admin.php');
$maint_active = ( wbf_get_settings()['maintenance_mode'] ?? '0' ) === '1';
$furl = wbf_get_forum_url();
// ── Counts ───────────────────────────────────────────────────────────────
$tag_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_tags");
$deleted_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE deleted_at IS NOT NULL")
+ (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NOT NULL");
$sub_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_subscriptions");
$online_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users WHERE last_active >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)");
$invite_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_invites WHERE use_count < max_uses AND (expires_at IS NULL OR expires_at > NOW())");
$banned_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users WHERE role='banned'");
$update_info = wbf_update_available(); // null oder array mit Versionsdaten
// ── System ────────────────────────────────────────────────────────────────
$php_ver = PHP_VERSION;
$php_rec = version_compare($php_ver,'8.0','>=');
$php_ok = version_compare($php_ver,'7.4','>=');
$mail_ok = function_exists('wp_mail');
$mysql_ver = $wpdb->get_var('SELECT VERSION()') ?: '?';
$exp_tables= ['forum_users','forum_categories','forum_threads','forum_posts','forum_likes','forum_reports','forum_tags','forum_thread_tags','forum_messages','forum_reactions','forum_remember_tokens','forum_subscriptions','forum_notifications'];
$existing = $wpdb->get_col("SHOW TABLES LIKE '{$wpdb->prefix}forum_%'");
$missing = array_filter($exp_tables, fn($t) => !in_array($wpdb->prefix.$t, $existing));
// ── Trends ────────────────────────────────────────────────────────────────
$pt = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)");
$pl = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_posts WHERE deleted_at IS NULL AND created_at BETWEEN DATE_SUB(NOW(), INTERVAL 14 DAY) AND DATE_SUB(NOW(), INTERVAL 7 DAY)");
$tht = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE deleted_at IS NULL AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)");
$thl = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_threads WHERE deleted_at IS NULL AND created_at BETWEEN DATE_SUB(NOW(), INTERVAL 14 DAY) AND DATE_SUB(NOW(), INTERVAL 7 DAY)");
$ut = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users WHERE registered >= DATE_SUB(NOW(), INTERVAL 7 DAY)");
$ul = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_users WHERE registered BETWEEN DATE_SUB(NOW(), INTERVAL 14 DAY) AND DATE_SUB(NOW(), INTERVAL 7 DAY)");
$tr = function($now,$prev) {
if ($prev==0) return $now>0 ? ['+∞','wbf-tu','fa-arrow-trend-up'] : ['—','wbf-tf','fa-minus'];
$p=round(($now-$prev)/$prev*100);
if ($p>0) return ['+'.$p.'%','wbf-tu','fa-arrow-trend-up'];
if ($p<0) return [$p.'%','wbf-td','fa-arrow-trend-down'];
return ['±0%','wbf-tf','fa-minus'];
};
[$pp,$pc,$pi] = $tr($pt,$pl);
[$tp,$tc,$ti] = $tr($tht,$thl);
[$up,$uc,$ui] = $tr($ut,$ul);
// ── Top cats ──────────────────────────────────────────────────────────────
$top_cats = $wpdb->get_results("SELECT c.name,c.icon,COUNT(t.id) AS cnt FROM {$wpdb->prefix}forum_categories c LEFT JOIN {$wpdb->prefix}forum_threads t ON t.category_id=c.id AND t.deleted_at IS NULL GROUP BY c.id ORDER BY cnt DESC LIMIT 5");
$max_cnt = $top_cats ? max(1,max(array_map(fn($c)=>(int)$c->cnt,$top_cats))) : 1;
// ── Banned ────────────────────────────────────────────────────────────────
$banned_users = $wpdb->get_results("SELECT display_name,avatar_url,ban_reason,ban_until FROM {$wpdb->prefix}forum_users WHERE role='banned' ORDER BY id DESC LIMIT 5");
// ── Activity ──────────────────────────────────────────────────────────────
$activity = $wpdb->get_results("
(SELECT 'post' AS type,p.id AS oid,u.display_name,SUBSTRING(p.content,1,70) AS sub,p.created_at,u.avatar_url FROM {$wpdb->prefix}forum_posts p JOIN {$wpdb->prefix}forum_users u ON u.id=p.user_id WHERE p.deleted_at IS NULL ORDER BY p.created_at DESC LIMIT 6)
UNION ALL
(SELECT 'thread',t.id,u.display_name,t.title,t.created_at,u.avatar_url FROM {$wpdb->prefix}forum_threads t JOIN {$wpdb->prefix}forum_users u ON u.id=t.user_id WHERE t.deleted_at IS NULL ORDER BY t.created_at DESC LIMIT 5)
UNION ALL
(SELECT 'register',fu.id,fu.display_name,fu.username,fu.registered,fu.avatar_url FROM {$wpdb->prefix}forum_users fu ORDER BY fu.registered DESC LIMIT 4)
UNION ALL
(SELECT 'report',r.id,u.display_name,r.reason,r.created_at,u.avatar_url FROM {$wpdb->prefix}forum_reports r JOIN {$wpdb->prefix}forum_users u ON u.id=r.reporter_id WHERE r.status='open' ORDER BY r.created_at DESC LIMIT 3)
ORDER BY created_at DESC LIMIT 15
");
// ── Open reports ──────────────────────────────────────────────────────────
$reports_preview = $wpdb->get_results("
SELECT r.*,u.display_name AS rname,p.content AS pc,t.title AS tt,t.id AS tid
FROM {$wpdb->prefix}forum_reports r
LEFT JOIN {$wpdb->prefix}forum_users u ON u.id=r.reporter_id
LEFT JOIN {$wpdb->prefix}forum_posts p ON p.id=r.object_id AND r.object_type='post'
LEFT JOIN {$wpdb->prefix}forum_threads t ON t.id=p.thread_id
WHERE r.status='open' ORDER BY r.created_at DESC LIMIT 6
");
$rlabels = ['spam'=>'Spam','harassment'=>'Belästigung','inappropriate'=>'Unangemessen','misinformation'=>'Fehlinformation','off-topic'=>'Offtopic','other'=>'Sonstiges'];
$acfg = ['post'=>['fas fa-comment-dots','post','antwortete'],'thread'=>['fas fa-comments','thread','erstellte Thread:'],'register'=>['fas fa-user-plus','register','registrierte sich'],'report'=>['fas fa-flag','report','meldete Beitrag:']];
$nonce_url = fn($action,$id) => esc_url(wp_nonce_url(add_query_arg(['page'=>'wbf-reports','report_id'=>$id,'report_action'=>$action,'filter'=>'open'],admin_url('admin.php')),'wbf_report_action'));
$ago = function($t) {
$d=time()-strtotime($t);
if($d<60) return 'jetzt';
if($d<3600) return (int)($d/60).' Min.';
if($d<86400)return (int)($d/3600).' Std.';
return date_i18n('d.m.',$d<604800?time()-$d:strtotime($t));
};
?>
<div class="wrap wbf-d" style="overflow-x:hidden;max-width:100%">
<!-- ═══════════ TOP BAR ═══════════════════════════════════════════════════ -->
<div class="wbf-topbar">
<div class="wbf-topbar__left">
<div class="wbf-topbar__logo" style="background:none;padding:0;overflow:hidden"><img src="data:image/png;base64,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" width="42" height="42" style="border-radius:10px;object-fit:cover;display:block" alt="Logo"></div>
<div>
<div class="wbf-topbar__title">WP Business Forum</div>
<div class="wbf-topbar__sub">Admin Dashboard · v<?php echo esc_html(WBF_VERSION); ?> · <?php echo esc_html(get_bloginfo('name')); ?></div>
</div>
</div>
<div class="wbf-topbar__actions">
<?php if ($maint_active): ?>
<span class="wbf-topbar__badge wbf-topbar__badge--warn"><i class="fas fa-screwdriver-wrench"></i> Wartungsmodus aktiv</span>
<?php else: ?>
<span class="wbf-topbar__badge wbf-topbar__badge--ok"><i class="fas fa-circle-check"></i> Forum online</span>
<?php endif; ?>
<a href="<?php echo esc_url(add_query_arg('page','wbf-settings',$admin_url)); ?>" class="wbf-topbar__btn wbf-topbar__btn--outline">
<i class="fas fa-gear"></i> Einstellungen
</a>
<?php if ($furl): ?>
<a href="<?php echo esc_url($furl); ?>" target="_blank" class="wbf-topbar__btn wbf-topbar__btn--primary">
<i class="fas fa-arrow-up-right-from-square"></i> Forum öffnen
</a>
<?php endif; ?>
</div>
</div>
<!-- ═══════════ PLUGIN INFO STRIP ════════════════════════════════════════ -->
<?php $pf=WBF_PATH.'wp-business-forum.php'; ?>
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:0;background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:0;margin-bottom:18px;overflow:hidden">
<div style="display:flex;align-items:center;gap:10px;padding:11px 18px;border-right:1px solid #f1f5f9;flex-shrink:0">
<i class="fas fa-puzzle-piece" style="color:#6366f1;font-size:.9rem"></i>
<span style="font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:#374151">WP Business Forum</span>
<span style="background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;font-size:.7rem;font-weight:700;padding:2px 8px;border-radius:20px;display:inline-flex;align-items:center;gap:4px">
<i class="fas fa-tag"></i> v<?php echo esc_html(WBF_VERSION);?>
</span>
</div>
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:0;flex:1">
<?php foreach ([
['fas fa-user-pen', 'Autor', '<a href="https://m-viper.de" target="_blank" style="color:#0ea5e9;text-decoration:none;font-weight:600">M_Viper</a>'],
['fas fa-calendar', 'Update', esc_html(file_exists($pf)?date_i18n('d.m.Y',filemtime($pf)):'—')],
['fas fa-code-branch','Repo', '<a href="https://git.viper.ipv64.net/M_Viper/WP-Business-Forum" target="_blank" style="color:#0ea5e9;text-decoration:none;font-weight:600">git.viper.ipv64.net</a>'],
['fas fa-layer-group','Shortcode','<code style="background:#f1f5f9;padding:1px 6px;border-radius:4px;font-size:.75rem">[business_forum]</code>'],
] as [$ico,$lbl,$val]):?>
<div style="display:flex;align-items:center;gap:6px;padding:11px 16px;border-right:1px solid #f1f5f9;font-size:.78rem;white-space:nowrap">
<i class="<?php echo esc_attr($ico);?>" style="color:#94a3b8;font-size:.78rem"></i>
<span style="color:#94a3b8"><?php echo $lbl;?>:</span>
<span style="color:#374151"><?php echo $val;?></span>
</div>
<?php endforeach;?>
</div>
</div>
<!-- ═══════════ SYSTEM BAR ════════════════════════════════════════════════ -->
<div class="wbf-sysbar">
<span class="wbf-sysbar__label">System</span>
<span class="wbf-sbadge <?php echo $php_rec?'wbf-sbadge--ok':($php_ok?'wbf-sbadge--warn':'wbf-sbadge--err'); ?>">
<i class="fas fa-<?php echo $php_rec?'check':($php_ok?'triangle-exclamation':'xmark'); ?>"></i> PHP <?php echo esc_html($php_ver); ?>
</span>
<span class="wbf-sbadge wbf-sbadge--inf"><i class="fas fa-database"></i> MySQL <?php echo esc_html($mysql_ver); ?></span>
<span class="wbf-sbadge wbf-sbadge--inf"><i class="fab fa-wordpress"></i> WP <?php echo esc_html(get_bloginfo('version')); ?></span>
<div class="wbf-sdivider"></div>
<span class="wbf-sbadge <?php echo $mail_ok?'wbf-sbadge--ok':'wbf-sbadge--err'; ?>">
<i class="fas fa-<?php echo $mail_ok?'envelope-circle-check':'xmark'; ?>"></i> wp_mail
</span>
<div class="wbf-sdivider"></div>
<?php if (empty($missing)): ?>
<span class="wbf-sbadge wbf-sbadge--ok"><i class="fas fa-table-columns"></i> <?php echo count($exp_tables); ?> Tabellen OK</span>
<?php else: ?>
<span class="wbf-sbadge wbf-sbadge--err"><i class="fas fa-triangle-exclamation"></i> Fehlende Tabellen: <?php echo esc_html(implode(', ',$missing)); ?></span>
<?php endif; ?>
<span style="margin-left:auto;font-size:.7rem;color:#94a3b8"><?php echo $online_count; ?> gerade online</span>
</div>
<!-- ═══════════ KPI ROW ═══════════════════════════════════════════════════ -->
<div class="wbf-kpi-row">
<?php
$kpis = [
['#3b82f6','#eff6ff','💬', $stats['threads'], 'Threads', $tp,$tc,$ti,'wbf-categories'],
['#22c55e','#f0fdf4','📝', $stats['posts'], 'Beiträge', $pp,$pc,$pi,''],
['#a855f7','#fdf4ff','👥', $stats['members'], 'Mitglieder', $up,$uc,$ui,'wbf-members'],
['#f97316','#fff7ed','🚩', $open_reports, 'Offene Meldungen','','','','wbf-reports'],
['#10b981','#f0fdf4','🟢', $online_count, 'Jetzt online', '','','','wbf-members'],
];
foreach ($kpis as [$col,$bg,$ico,$val,$label,$tpct,$tcss,$tico,$page]):
$href = $page ? esc_url(add_query_arg('page',$page,$admin_url)) : '';
?>
<?php if($href):?><a href="<?php echo $href;?>" class="wbf-kpi" style="--kc:<?php echo $col;?>"><?php else:?><div class="wbf-kpi" style="--kc:<?php echo $col;?>"><?php endif;?>
<div class="wbf-kpi__top">
<div class="wbf-kpi__icon" style="background:<?php echo $bg;?>;color:<?php echo $col;?>;font-size:1.2rem"><?php echo $ico;?></div>
<?php if ($tpct): ?>
<span class="wbf-kpi__trend wbf-kpi__trend--<?php echo $tcss==='wbf-tu'?'up':($tcss==='wbf-td'?'down':'flat');?>">
<i class="fas <?php echo esc_attr($tico);?>"></i> <?php echo esc_html($tpct);?>
</span>
<?php endif;?>
</div>
<div class="wbf-kpi__val" style="color:<?php echo $col;?>"><?php echo number_format((int)$val);?></div>
<div class="wbf-kpi__label"><?php echo esc_html($label);?></div>
<?php if($href):?></a><?php else:?></div><?php endif;?>
<?php endforeach;?>
</div>
<!-- ═══════════ MAIN GRID ═════════════════════════════════════════════════ -->
<div class="wbf-main">
<!-- ── Col 1: Navigation only ────────────────────── -->
<div class="wbf-card" style="height:100%">
<div class="wbf-card__head">
<i class="fas fa-bolt wbf-card__head-icon"></i>
<span class="wbf-card__head-title">Navigation</span>
</div>
<div class="wbf-card__body--tight" style="padding-top:10px;padding-bottom:10px">
<nav class="wbf-nav">
<?php
$nav = [
['wbf-categories','fas fa-folder-open', 'Kategorien'],
['wbf-members', 'fas fa-users', 'Mitglieder', $banned_count>0?$banned_count:0],
['wbf-reports', 'fas fa-flag', 'Meldungen', $open_reports>0?$open_reports:0],
['wbf-levels', 'fas fa-star', 'Level-System'],
['wbf-reactions', 'fas fa-face-smile', 'Reaktionen'],
['wbf-invites', 'fas fa-envelope-open-text','Einladungen', $invite_count>0?$invite_count:0],
['wbf-stats', 'fas fa-chart-line', 'Statistiken'],
null,
['wbf-trash', 'fas fa-trash-can', 'Papierkorb', $deleted_count>0?$deleted_count:0, true],
['wbf-export', 'fas fa-database', 'Export / Import'],
['wbf-settings', 'fas fa-gear', 'Einstellungen'],
['wbf-updates', 'fas fa-arrow-up-from-bracket', '🔔 Updates', $update_info ? 1 : 0],
];
foreach ($nav as $n):
if ($n===null) { echo '<div class="wbf-nav__sep"></div>'; continue; }
[$pg,$ico,$lbl] = $n;
$badge = $n[3] ?? 0;
$danger = $n[4] ?? false;
?>
<a href="<?php echo esc_url(add_query_arg('page',$pg,$admin_url));?>"
class="wbf-nav__item<?php echo ($danger&&$badge)?' wbf-nav__danger':'';?>">
<i class="<?php echo esc_attr($ico);?>"></i>
<?php echo esc_html($lbl);?>
<?php if($badge):?><span class="wbf-nav__badge"><?php echo (int)$badge;?></span><?php endif;?>
</a>
<?php endforeach;?>
</nav>
</div>
</div>
<!-- ── Col 2: Aktivitätsfeed ─────────────────────── -->
<div class="wbf-card" style="height:480px;display:flex;flex-direction:column">
<div class="wbf-card__head">
<i class="fas fa-wave-square wbf-card__head-icon"></i>
<span class="wbf-card__head-title">Live-Aktivität</span>
<span style="font-size:.68rem;color:#94a3b8;font-weight:400">letzte 15 Aktionen</span>
</div>
<div class="wbf-card__body--tight" style="flex:1;overflow-y:auto">
<?php if(empty($activity)):?>
<p style="color:#94a3b8;font-size:.82rem;padding:16px 0;text-align:center;margin:0">Noch keine Aktivität.</p>
<?php else:?>
<ul class="wbf-feed">
<?php foreach($activity as $ev):
[$ico,$css,$act] = $acfg[$ev->type] ?? $acfg['post'];
$t = $ago($ev->created_at);
?>
<li class="wbf-feed__item" style="padding:5px 0;gap:8px">
<div style="position:relative;flex-shrink:0">
<img src="<?php echo esc_url($ev->avatar_url ?: 'https://www.gravatar.com/avatar/0?d=identicon&s=28'); ?>"
width="28" height="28" style="border-radius:50%;object-fit:cover;display:block">
<span style="position:absolute;bottom:-2px;right:-2px;width:13px;height:13px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-size:.5rem;line-height:1">
<?php
$dot_emoji = ['post'=>'💬','thread'=>'📝','register'=>'✨','report'=>'🚩'];
echo $dot_emoji[$ev->type] ?? '💬';
?>
</span>
</div>
<div class="wbf-feed__body" style="min-width:0">
<span class="wbf-feed__name" style="font-size:.78rem"><?php echo esc_html($ev->display_name);?></span>
<span class="wbf-feed__action" style="font-size:.78rem"> <?php echo $act;?></span>
<span class="wbf-feed__sub" style="font-size:.68rem"><?php echo esc_html(mb_substr(strip_tags($ev->sub),0,45));?></span>
</div>
<span class="wbf-feed__time" style="font-size:.65rem"><?php echo esc_html($t);?></span>
</li>
<?php endforeach;?>
</ul>
<?php endif;?>
</div>
</div>
<!-- ── Col 3: Letzte Threads ─────────────────────── -->
<div class="wbf-card" style="height:480px;display:flex;flex-direction:column">
<div class="wbf-card__head">
<i class="fas fa-clock wbf-card__head-icon"></i>
<span class="wbf-card__head-title">Letzte Threads</span>
</div>
<div class="wbf-card__body--tight" style="flex:1;overflow-y:auto">
<?php if(empty($recent)):?>
<p style="color:#94a3b8;font-size:.82rem;padding:12px 0;margin:0;text-align:center">Keine Threads.</p>
<?php else:?>
<ul class="wbf-list">
<?php foreach($recent as $r):?>
<li class="wbf-list__item">
<div style="width:7px;height:7px;border-radius:50%;background:#e2e8f0;flex-shrink:0"></div>
<div class="wbf-list__body">
<a href="<?php echo esc_url(wbf_get_forum_url().'?forum_thread='.(int)$r->id);?>" target="_blank" class="wbf-list__title"><?php echo esc_html(mb_substr($r->title,0,42));?></a>
<div class="wbf-list__meta"><?php echo esc_html($r->cat_name);?> · <?php echo esc_html($r->display_name);?></div>
</div>
<span class="wbf-list__aside"><?php echo esc_html(date_i18n('d.m.',strtotime($r->created_at)));?></span>
</li>
<?php endforeach;?>
</ul>
<?php endif;?>
</div>
</div>
<!-- ── Col 4: Rollen + Meldungen + Mitglieder ───── -->
<div style="display:flex;flex-direction:column;gap:12px;height:480px">
<!-- Rollen -->
<div class="wbf-card">
<div class="wbf-card__head">
<i class="fas fa-shield-halved wbf-card__head-icon"></i>
<span class="wbf-card__head-title">Rollen</span>
<a href="<?php echo esc_url(add_query_arg('page','wbf-roles',$admin_url));?>" class="wbf-card__head-link">Bearbeiten →</a>
</div>
<div class="wbf-roles">
<?php foreach ($roles as $key=>$role):
$rc=esc_attr($role['color']); $rb=esc_attr($role['bg_color']);?>
<a href="<?php echo esc_url(add_query_arg(['page'=>'wbf-roles','edit_role'=>$key],$admin_url));?>"
class="wbf-rchip" style="color:<?php echo $rc;?>;background:<?php echo $rb;?>;border-color:<?php echo $rc;?>">
<i class="<?php echo esc_attr($role['icon']??'fas fa-user');?>"></i>
<?php echo esc_html($role['label']);?>
</a>
<?php endforeach;?>
</div>
</div>
<!-- Wartungsmodus -->
<div class="wbf-card">
<div class="wbf-card__head">
<i class="fas fa-screwdriver-wrench wbf-card__head-icon" style="color:#f97316"></i>
<span class="wbf-card__head-title">Wartungsmodus</span>
</div>
<div class="wbf-card__body">
<form method="post" action="<?php echo esc_url(admin_url('admin.php?page=wbf-admin')); ?>">
<?php wp_nonce_field('wbf_dash_maint_nonce');?>
<input type="hidden" name="wbf_dash_toggle_maint" value="1">
<div class="wbf-maint-box">
<div>
<div class="wbf-maint-state" style="color:<?php echo $maint_active?'#dc2626':'#15803d';?>"><?php echo $maint_active?'🔧 Aktiv':'✅ Online';?></div>
<div class="wbf-maint-hint"><?php echo $maint_active?'Besucher sehen Wartungsseite.':'Alle haben Zugriff.';?></div>
</div>
<label class="wbf-toggle">
<input type="checkbox" onchange="this.closest('form').submit()" <?php echo $maint_active?'checked':'';?>>
<span class="wbf-toggle__track"></span>
</label>
</div>
</form>
<p style="font-size:.72rem;color:#94a3b8;margin:0"><i class="fas fa-info-circle"></i> Admins haben immer Zugriff. <a href="<?php echo esc_url(add_query_arg('page','wbf-settings',$admin_url)); ?>" style="color:#0ea5e9">Einstellungen →</a></p>
</div>
</div>
<!-- Neueste Mitglieder -->
<div class="wbf-card">
<div class="wbf-card__head">
<i class="fas fa-user-plus wbf-card__head-icon"></i>
<span class="wbf-card__head-title">Neue Mitglieder</span>
<a href="<?php echo esc_url(add_query_arg('page','wbf-members',$admin_url));?>" class="wbf-card__head-link">Alle →</a>
</div>
<div class="wbf-card__body--tight">
<?php if(empty($members)):?>
<p style="color:#94a3b8;font-size:.82rem;padding:12px 0;margin:0;text-align:center">Keine Mitglieder.</p>
<?php else:?>
<ul class="wbf-list">
<?php foreach($members as $m):
$mr=WBF_Roles::get($m->role); $mc=esc_attr($mr['color']); $mb=esc_attr($mr['bg_color']);?>
<li class="wbf-list__item">
<img src="<?php echo esc_url($m->avatar_url?:'https://www.gravatar.com/avatar/0?d=identicon&s=26');?>" width="26" height="26" style="border-radius:50%;object-fit:cover;flex-shrink:0">
<div class="wbf-list__body">
<a href="<?php echo esc_url(add_query_arg('page','wbf-members',$admin_url));?>" class="wbf-list__title"><?php echo esc_html($m->display_name);?></a>
<div style="margin-top:2px"><span style="font-size:.67rem;font-weight:700;color:<?php echo $mc;?>;background:<?php echo $mb;?>;padding:1px 6px;border-radius:20px;border:1px solid <?php echo $mc;?>"><i class="<?php echo esc_attr($mr['icon']??'fas fa-user');?>"></i> <?php echo esc_html($mr['label']);?></span></div>
</div>
<span class="wbf-list__aside"><?php echo (int)$m->post_count;?> Beitr.</span>
</li>
<?php endforeach;?>
</ul>
<?php endif;?>
</div>
</div>
</div>
</div><!-- /.wbf-main -->
<!-- ═══════════ BOTTOM ROW ════════════════════════════════════════════════ -->
<div class="wbf-bottom">
<!-- Wachstumstrend -->
<div class="wbf-card">
<div class="wbf-card__head">
<i class="fas fa-chart-line wbf-card__head-icon" style="color:#10b981"></i>
<span class="wbf-card__head-title">Wachstum 7 Tage</span>
</div>
<div class="wbf-card__body">
<div class="wbf-trend-mini">
<?php foreach ([['fas fa-comment-dots','Beiträge',$pt,$pl,$pp,$pc,$pi],['fas fa-comments','Threads',$tht,$thl,$tp,$tc,$ti],['fas fa-user-plus','Mitglieder',$ut,$ul,$up,$uc,$ui]] as [$i,$lbl,$now,$prev,$pct,$css,$ico]):?>
<div class="wbf-trend-mini__item">
<div class="wbf-trend-mini__label"><i class="<?php echo esc_attr($i);?>"></i> <?php echo $lbl;?></div>
<div class="wbf-trend-mini__row">
<span class="wbf-trend-mini__val"><?php echo $now;?></span>
<span class="wbf-trend-mini__badge <?php echo $css;?>"><i class="fas <?php echo esc_attr($ico);?>"></i> <?php echo esc_html($pct);?></span>
</div>
<div style="font-size:.68rem;color:#94a3b8;margin-top:3px">Vorwoche: <?php echo $prev;?></div>
</div>
<?php endforeach;?>
</div>
</div>
</div>
<!-- Top-Kategorien -->
<div class="wbf-card">
<div class="wbf-card__head">
<i class="fas fa-fire wbf-card__head-icon" style="color:#f97316"></i>
<span class="wbf-card__head-title">Top-Kategorien</span>
<a href="<?php echo esc_url(add_query_arg('page','wbf-categories',$admin_url));?>" class="wbf-card__head-link">Alle →</a>
</div>
<div class="wbf-card__body">
<?php if(empty($top_cats)):?>
<p style="color:#94a3b8;font-size:.82rem;text-align:center;padding:12px 0;margin:0">Keine Kategorien.</p>
<?php else:?>
<ul class="wbf-cats">
<?php $rk=['#fbbf24','#94a3b8','#cd7c2f','#0ea5e9','#a78bfa']; foreach($top_cats as $i=>$cat):
$bpct=$max_cnt>0?round($cat->cnt/$max_cnt*100):0; $rc=$rk[$i]??'#94a3b8';?>
<li>
<div class="wbf-cats__bar-head">
<span><span style="color:<?php echo $rc;?>;font-size:.68rem;font-weight:800;margin-right:4px"><?php echo $i+1;?></span><i class="<?php echo esc_attr($cat->icon?:'fas fa-comments');?>" style="color:#94a3b8;font-size:.78rem;margin-right:3px"></i><?php echo esc_html(mb_substr($cat->name,0,20));?></span>
<span style="font-size:.75rem;color:#64748b;font-weight:600"><?php echo (int)$cat->cnt;?></span>
</div>
<div class="wbf-cats__track"><div class="wbf-cats__fill" style="width:<?php echo $bpct;?>%;background:<?php echo $rc;?>"></div></div>
</li>
<?php endforeach;?>
</ul>
<?php endif;?>
</div>
</div>
<!-- Gesperrte Nutzer -->
<div class="wbf-card">
<div class="wbf-card__head">
<i class="fas fa-ban wbf-card__head-icon" style="color:<?php echo $banned_count>0?'#ef4444':'#94a3b8';?>"></i>
<span class="wbf-card__head-title">Gesperrt</span>
<?php if($banned_count>0):?><span style="background:#ef4444;color:#fff;font-size:.62rem;font-weight:700;padding:1px 7px;border-radius:20px"><?php echo $banned_count;?></span><?php endif;?>
<a href="<?php echo esc_url(add_query_arg('page','wbf-members',$admin_url));?>" class="wbf-card__head-link">Liste →</a>
</div>
<div class="wbf-card__body">
<?php if($banned_count===0):?>
<div style="text-align:center;padding:20px 0;color:#94a3b8"><i class="fas fa-shield-check" style="font-size:1.8rem;opacity:.2;display:block;margin-bottom:8px"></i><span style="font-size:.82rem">Keine gesperrten Nutzer</span></div>
<?php else:?>
<?php foreach($banned_users as $bu):?>
<div class="wbf-banned-row">
<img src="<?php echo esc_url($bu->avatar_url?:'https://www.gravatar.com/avatar/0?d=identicon&s=26');?>" width="26" height="26">
<div style="min-width:0;flex:1">
<div style="font-weight:600;font-size:.8rem;color:#0f172a"><?php echo esc_html($bu->display_name);?></div>
<?php if($bu->ban_reason):?><div style="font-size:.7rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"><?php echo esc_html($bu->ban_reason);?></div><?php endif;?>
<?php if(!empty($bu->ban_until)):
$rem = strtotime($bu->ban_until) - time();
$days_r = floor($rem/86400); $hrs_r = floor(($rem%86400)/3600);
$rem_str = $rem > 0 ? '⏱ ' . ($days_r>0?$days_r.'T ':''). $hrs_r.'h verbl.' : 'läuft ab';
?><div style="font-size:.68rem;color:#f97316;font-weight:600"><?php echo esc_html($rem_str);?> · <?php echo esc_html(date_i18n('d.m.Y H:i',strtotime($bu->ban_until)));?></div>
<?php else:?><div style="font-size:.68rem;color:#ef4444;font-weight:600">Permanent</div><?php endif;?>
</div>
</div>
<?php endforeach;?>
<?php if($banned_count>5):?><div style="text-align:center;font-size:.72rem;color:#94a3b8;padding-top:8px">… und <?php echo $banned_count-5;?> weitere</div><?php endif;?>
<?php endif;?>
</div>
</div>
<!-- Offene Meldungen -->
<div class="wbf-card">
<div class="wbf-card__head">
<i class="fas fa-flag wbf-card__head-icon" style="color:#f97316"></i>
<span class="wbf-card__head-title">Offene Meldungen</span>
<?php if($open_reports>0):?><span style="background:#ef4444;color:#fff;font-size:.62rem;font-weight:700;padding:1px 7px;border-radius:20px"><?php echo (int)$open_reports;?></span><?php endif;?>
<?php if($open_reports>6):?><a href="<?php echo esc_url(add_query_arg('page','wbf-reports',$admin_url));?>" class="wbf-card__head-link">Alle →</a><?php endif;?>
</div>
<div class="wbf-card__body--tight">
<?php if(empty($reports_preview)):?>
<div class="wbf-noreports"><i class="fas fa-shield-check"></i>Keine offenen Meldungen</div>
<?php else:?>
<?php foreach($reports_preview as $rp):
$goto = $rp->tid ? esc_url(wbf_get_forum_url().'?forum_thread='.(int)$rp->tid.'#post-'.(int)$rp->object_id) : '';
?>
<div class="wbf-report-item">
<div class="wbf-report-item__head">
<strong style="font-size:.8rem;color:#0f172a"><?php echo esc_html($rp->rname??'?');?></strong>
<span class="wbf-rtag"><?php echo esc_html($rlabels[$rp->reason]??$rp->reason);?></span>
<span style="margin-left:auto;font-size:.68rem;color:#94a3b8"><?php echo date_i18n('d.m. H:i',strtotime($rp->created_at));?></span>
</div>
<?php if($rp->pc):?><div class="wbf-report-item__text">"<?php echo esc_html(mb_substr(strip_tags($rp->pc),0,80));?>"</div><?php endif;?>
<div class="wbf-report-item__actions">
<a href="<?php echo $nonce_url('resolved',$rp->id);?>" class="wbf-ra wbf-ra--ok">✔ Erledigt</a>
<a href="<?php echo $nonce_url('dismissed',$rp->id);?>" class="wbf-ra wbf-ra--skip">✖ Verwerfen</a>
<?php if($goto):?><a href="<?php echo $goto;?>" target="_blank" class="wbf-ra wbf-ra--go">↗ Beitrag</a><?php endif;?>
</div>
</div>
<?php endforeach;?>
<?php endif;?>
</div>
</div>
</div><!-- /.wbf-bottom -->
</div><!-- /.wbf-d -->
<?php
}
// ── Rollen ────────────────────────────────────────────────────────────────────
function wbf_admin_roles() {
$all_perms = WBF_Roles::all_permissions();
if ( isset( $_POST['wbf_save_role'] ) && check_admin_referer( 'wbf_role_edit_nonce' ) ) {
$key = sanitize_key( $_POST['role_key'] ?? '' );
if ( $key && $key !== WBF_Roles::SUPERADMIN ) {
$perms = [];
foreach ( $all_perms as $p => $label ) {
if ( ! empty( $_POST['perm_' . $p] ) ) $perms[] = $p;
}
WBF_Roles::save( $key, [
'label' => sanitize_text_field( $_POST['role_label'] ),
'level' => max( -1, min( 99, (int) $_POST['role_level'] ) ),
'color' => sanitize_hex_color( $_POST['role_color'] ) ?: '#94a3b8',
'bg_color' => sanitize_text_field( $_POST['role_bg'] ) ?: 'rgba(148,163,184,.1)',
'icon' => sanitize_text_field( $_POST['role_icon'] ) ?: 'fas fa-user',
'permissions' => $perms,
'locked' => false,
'description' => sanitize_textarea_field( $_POST['role_desc'] ),
] );
echo '<div class="notice notice-success is-dismissible"><p>Rolle gespeichert!</p></div>';
}
}
if ( isset( $_POST['wbf_create_role'] ) && check_admin_referer( 'wbf_role_create_nonce' ) ) {
$new_key = sanitize_key( $_POST['new_role_key'] ?? '' );
if ( $new_key && $new_key !== WBF_Roles::SUPERADMIN ) {
$perms = [];
foreach ( $all_perms as $p => $label ) {
if ( ! empty( $_POST['new_perm_' . $p] ) ) $perms[] = $p;
}
WBF_Roles::save( $new_key, [
'label' => sanitize_text_field( $_POST['new_role_label'] ?: ucfirst( $new_key ) ),
'level' => max( 1, min( 79, (int) ( $_POST['new_role_level'] ?? 10 ) ) ),
'color' => sanitize_hex_color( $_POST['new_role_color'] ) ?: '#94a3b8',
'bg_color' => 'rgba(148,163,184,.1)',
'icon' => sanitize_text_field( $_POST['new_role_icon'] ) ?: 'fas fa-user',
'permissions' => $perms,
'locked' => false,
'description' => sanitize_textarea_field( $_POST['new_role_desc'] ),
] );
echo '<div class="notice notice-success is-dismissible"><p>Neue Rolle erstellt!</p></div>';
}
}
if ( isset( $_GET['delete_role'] ) && check_admin_referer( 'delete_role_' . $_GET['delete_role'] ) ) {
$del = sanitize_key( $_GET['delete_role'] );
if ( WBF_Roles::delete( $del ) ) {
echo '<div class="notice notice-success is-dismissible"><p>Rolle gelöscht. Nutzer auf "member" gesetzt.</p></div>';
} else {
echo '<div class="notice notice-error"><p>Diese Rolle kann nicht gelöscht werden.</p></div>';
}
}
$roles = WBF_Roles::get_sorted();
$edit_key = isset( $_GET['edit_role'] ) ? sanitize_key( $_GET['edit_role'] ) : null;
$edit_role = $edit_key ? WBF_Roles::get( $edit_key ) : null;
echo '<div class="wrap"><h1>Rollen-Verwaltung <a href="?page=wbf-roles#new-role" class="page-title-action">+ Neue Rolle</a></h1>';
echo '<table class="widefat striped" style="margin-bottom:2rem">
<thead><tr><th style="width:180px">Rolle</th><th>Level</th><th>Permissions</th><th>Beschreibung</th><th>Aktionen</th></tr></thead><tbody>';
foreach ( $roles as $key => $role ) {
$color = esc_attr( $role['color'] ); $bg = esc_attr( $role['bg_color'] );
$badge = "<span class='wbf-role-preview' style='color:{$color};background:{$bg};border-color:{$color}'>
<i class='" . esc_attr( $role['icon'] ?? 'fas fa-user' ) . "'></i> " . esc_html( $role['label'] ) . "</span>";
$perms = implode( ', ', array_map( fn($p) => esc_html( $all_perms[$p] ?? $p ), $role['permissions'] ?? [] ) );
if ( in_array( 'all', $role['permissions'] ?? [] ) ) $perms = '<em>Alle Rechte</em>';
$actions = '';
if ( $key !== WBF_Roles::SUPERADMIN ) {
$actions .= "<a href='?page=wbf-roles&edit_role={$key}'>Bearbeiten</a>";
if ( ! in_array( $key, ['member'] ) ) {
$del_url = wp_nonce_url( "?page=wbf-roles&delete_role={$key}", "delete_role_{$key}" );
$actions .= " | <a href='" . esc_url( $del_url ) . "' style='color:#dc2626' onclick='return confirm(\"Rolle löschen?\")'>Löschen</a>";
}
} else {
$actions = '<em style="color:#999">Systemrolle — unveränderlich</em>';
}
echo "<tr>
<td>$badge " . ( ( $role['locked'] ?? false ) ? '<span style="color:#999;font-size:.8em">(🔒)</span>' : '' ) . "</td>
<td><strong>" . esc_html( $role['level'] ) . "</strong></td>
<td style='font-size:.82em;color:#555'>$perms</td>
<td style='font-size:.82em;color:#666'>" . esc_html( $role['description'] ?? '' ) . "</td>
<td>$actions</td>
</tr>";
}
echo '</tbody></table>';
if ( $edit_role && $edit_key !== WBF_Roles::SUPERADMIN ) {
echo '<h2>Bearbeiten: <em>' . esc_html( $edit_role['label'] ) . '</em></h2>';
echo '<form method="post"><table class="form-table">';
wp_nonce_field( 'wbf_role_edit_nonce' );
echo '<input type="hidden" name="role_key" value="' . esc_attr( $edit_key ) . '">';
wbf_role_form_fields( $edit_role, $all_perms, '' );
echo '</table>';
submit_button( 'Änderungen speichern', 'primary', 'wbf_save_role' );
echo '</form><hr>';
}
echo '<h2 id="new-role">Neue Rolle erstellen</h2><form method="post"><table class="form-table">';
wp_nonce_field( 'wbf_role_create_nonce' );
echo '<tr><th>Rollen-Schlüssel *</th><td>
<input name="new_role_key" placeholder="z.B. trusted_member" class="regular-text" pattern="[a-z0-9_]+" required>
<p class="description">Nur Kleinbuchstaben, Zahlen, Unterstriche. Unveränderlich.</p>
</td></tr>';
wbf_role_form_fields( null, $all_perms, 'new_' );
echo '</table>';
submit_button( 'Rolle erstellen', 'primary', 'wbf_create_role' );
echo '</form></div>';
}
function wbf_role_form_fields( $role, $all_perms, $prefix ) {
$v = fn($k, $d = '') => esc_attr( $role[$k] ?? $d );
$perms = $role['permissions'] ?? [];
echo "
<tr><th>Anzeigename *</th><td><input name='{$prefix}role_label' value='" . $v('label') . "' class='regular-text' required></td></tr>
<tr><th>Level</th><td>
<input name='{$prefix}role_level' type='number' value='" . $v('level','10') . "' class='small-text' min='-1' max='99'>
<p class='description'>Superadmin=100, Admin≤80, Mod≤50, Member=10, Banned=-1</p>
</td></tr>
<tr><th>Farbe</th><td class='wbf-color-row'>
<label>Textfarbe <input name='{$prefix}role_color' type='color' value='" . $v('color','#94a3b8') . "'></label>
<label>Hintergrund <input name='{$prefix}role_bg' value='" . $v('bg_color','rgba(148,163,184,.1)') . "' style='width:220px' placeholder='rgba(…)'></label>
</td></tr>
<tr><th>Icon</th><td>
<input name='{$prefix}role_icon' value='" . $v('icon','fas fa-user') . "' class='regular-text'>
<p class='description'>FontAwesome 6, z.B. <code>fas fa-crown</code></p>
</td></tr>
<tr><th>Beschreibung</th><td><textarea name='{$prefix}role_desc' rows='2' class='large-text'>" . $v('description') . "</textarea></td></tr>
<tr><th>Permissions</th><td><div class='wbf-perm-grid'>";
foreach ( $all_perms as $p => $label ) {
$checked = in_array( $p, $perms ) ? 'checked' : '';
echo "<label class='wbf-perm-item'><input type='checkbox' name='{$prefix}perm_{$p}' $checked> " . esc_html( $label ) . "</label>";
}
echo "</div></td></tr>";
}
// ── Kategorien ────────────────────────────────────────────────────────────────
function wbf_admin_categories() {
global $wpdb;
$roles = WBF_Roles::get_sorted();
// ── Reihenfolge verschieben ──────────────────────────────────────────────
if ( isset( $_POST['wbf_reorder_cat'] ) && check_admin_referer( 'wbf_reorder_nonce' ) ) {
$move_id = (int) $_POST['cat_id'];
$dir = ( $_POST['direction'] ?? '' ) === 'up' ? 'up' : 'down';
$parent = (int) $_POST['parent_id'];
$siblings = $wpdb->get_results( $wpdb->prepare(
"SELECT id, sort_order FROM {$wpdb->prefix}forum_categories WHERE parent_id=%d ORDER BY sort_order ASC, id ASC",
$parent
) );
$ids = array_column( (array) $siblings, 'id' );
$pos = array_search( $move_id, $ids );
if ( $dir === 'up' && $pos > 0 ) {
$swap_id = $ids[ $pos - 1 ];
} elseif ( $dir === 'down' && $pos !== false && $pos < count($ids) - 1 ) {
$swap_id = $ids[ $pos + 1 ];
}
if ( isset( $swap_id ) ) {
$so_a = $siblings[$pos]->sort_order;
$so_b = $siblings[ $dir === 'up' ? $pos - 1 : $pos + 1 ]->sort_order;
if ( $so_a === $so_b ) { $so_b = $dir === 'up' ? $so_a - 1 : $so_a + 1; }
$wpdb->update( "{$wpdb->prefix}forum_categories", ['sort_order' => $so_b], ['id' => $move_id] );
$wpdb->update( "{$wpdb->prefix}forum_categories", ['sort_order' => $so_a], ['id' => $swap_id] );
}
// Gleiche Seite neu laden (kein redirect_to nötig)
$tree = WBF_DB::get_categories_tree();
}
if ( isset( $_POST['wbf_save_cat'] ) && check_admin_referer( 'wbf_cat_nonce' ) ) {
$data = [
'parent_id' => (int) ( $_POST['parent_id'] ?? 0 ),
'name' => sanitize_text_field( $_POST['name'] ),
'slug' => sanitize_title( $_POST['name'] ),
'description' => sanitize_textarea_field( $_POST['description'] ),
'icon' => sanitize_text_field( $_POST['icon'] ),
'sort_order' => (int) ( $_POST['sort_order'] ?? 0 ),
'min_role' => sanitize_key( $_POST['min_role'] ?? 'member' ),
'guest_visible' => isset($_POST['guest_visible']) ? 1 : 0,
];
if ( ! empty( $_POST['cat_id'] ) ) {
$wpdb->update( "{$wpdb->prefix}forum_categories", $data, ['id' => (int) $_POST['cat_id']] );
echo '<div class="notice notice-success is-dismissible"><p>Gespeichert!</p></div>';
} else {
$wpdb->insert( "{$wpdb->prefix}forum_categories", $data );
echo '<div class="notice notice-success is-dismissible"><p>Erstellt!</p></div>';
}
}
if ( isset( $_GET['delete_cat'] ) && current_user_can( 'manage_options' ) ) {
check_admin_referer( 'delete_cat_' . (int) $_GET['delete_cat'] );
$wpdb->delete( "{$wpdb->prefix}forum_categories", ['id' => (int) $_GET['delete_cat']] );
}
$tree = WBF_DB::get_categories_tree();
$all = WBF_DB::get_categories_flat();
$edit = isset( $_GET['edit'] ) ? WBF_DB::get_category( (int) $_GET['edit'] ) : null;
echo '<div class="wrap"><h1>Kategorien <a href="?page=wbf-categories" class="page-title-action">+ Neue Kategorie</a></h1>';
echo '<table class="widefat fixed striped" style="margin-bottom:2rem">
<thead><tr><th>Kategorie</th><th>Threads</th><th>Min. Rolle</th><th>Aktionen</th></tr></thead><tbody>';
foreach ( $tree as $p ) {
$d = wp_nonce_url( "?page=wbf-categories&delete_cat={$p->id}", "delete_cat_{$p->id}" );
$role = WBF_Roles::get( $p->min_role ?? 'member' );
$lockbadge = ( $p->min_role ?? 'member' ) !== 'member'
? " <span style='color:" . esc_attr( $role['color'] ) . ";font-size:.8em'>🔒 " . esc_html( $role['label'] ) . "</span>" : '';
$guestbadge = ( (int)($p->guest_visible ?? 1) === 0 )
? " <span style='color:#f59e0b;font-size:.8em'>👁 Nur eingeloggt</span>" : '';
$reorder_p =
'<form method="post" style="display:inline-flex;gap:2px">'
. wp_nonce_field( 'wbf_reorder_nonce', '_wpnonce', true, false )
. '<input type="hidden" name="cat_id" value="' . (int)$p->id . '">'
. '<input type="hidden" name="parent_id" value="0">'
. '<input type="hidden" name="direction" value="up">'
. '<button type="submit" name="wbf_reorder_cat" value="1" style="background:none;border:1px solid #ccc;border-radius:3px;cursor:pointer;padding:1px 6px;font-size:13px">↑</button>'
. '</form>'
. '<form method="post" style="display:inline-flex;gap:2px">'
. wp_nonce_field( 'wbf_reorder_nonce', '_wpnonce', true, false )
. '<input type="hidden" name="cat_id" value="' . (int)$p->id . '">'
. '<input type="hidden" name="parent_id" value="0">'
. '<input type="hidden" name="direction" value="down">'
. '<button type="submit" name="wbf_reorder_cat" value="1" style="background:none;border:1px solid #ccc;border-radius:3px;cursor:pointer;padding:1px 6px;font-size:13px">↓</button>'
. '</form>';
echo "<tr style='background:#f0f0f0;font-weight:600'>
<td><i class='dashicons dashicons-category'></i> " . esc_html( $p->name ) . "$lockbadge</td>
<td>" . esc_html( $p->thread_count ) . "</td>
<td>" . esc_html( $role['label'] ) . "</td>
<td style='white-space:nowrap'>{$reorder_p} <a href='?page=wbf-categories&edit={$p->id}'>Bearbeiten</a> | <a href='" . esc_url( $d ) . "' onclick='return confirm(\"Löschen?\")'>Löschen</a></td>
</tr>";
foreach ( $p->children as $c ) {
$cd = wp_nonce_url( "?page=wbf-categories&delete_cat={$c->id}", "delete_cat_{$c->id}" );
$crole = WBF_Roles::get( $c->min_role ?? 'member' );
$reorder_c =
'<form method="post" style="display:inline-flex;gap:2px">'
. wp_nonce_field( 'wbf_reorder_nonce', '_wpnonce', true, false )
. '<input type="hidden" name="cat_id" value="' . (int)$c->id . '">'
. '<input type="hidden" name="parent_id" value="' . (int)$p->id . '">'
. '<input type="hidden" name="direction" value="up">'
. '<button type="submit" name="wbf_reorder_cat" value="1" style="background:none;border:1px solid #ccc;border-radius:3px;cursor:pointer;padding:1px 6px;font-size:13px">↑</button>'
. '</form>'
. '<form method="post" style="display:inline-flex;gap:2px">'
. wp_nonce_field( 'wbf_reorder_nonce', '_wpnonce', true, false )
. '<input type="hidden" name="cat_id" value="' . (int)$c->id . '">'
. '<input type="hidden" name="parent_id" value="' . (int)$p->id . '">'
. '<input type="hidden" name="direction" value="down">'
. '<button type="submit" name="wbf_reorder_cat" value="1" style="background:none;border:1px solid #ccc;border-radius:3px;cursor:pointer;padding:1px 6px;font-size:13px">↓</button>'
. '</form>';
echo "<tr>
<td style='padding-left:2.5rem'>↳ " . esc_html( $c->name ) . "</td>
<td>" . esc_html( $c->thread_count ) . "</td>
<td>" . esc_html( $crole['label'] ) . "$clockbadge$cguestbadge</td>
<td style='white-space:nowrap'>{$reorder_c} <a href='?page=wbf-categories&edit={$c->id}'>Bearbeiten</a> | <a href='" . esc_url( $cd ) . "' onclick='return confirm(\"Löschen?\")'>Löschen</a></td>
</tr>";
}
}
echo '</tbody></table>';
$parent_opts = '<option value="0">— Keine (Elternkategorie) —</option>';
foreach ( $all as $c ) {
if ( $edit && $c->id == $edit->id ) continue;
$sel = ( $edit && (int) $edit->parent_id === (int) $c->id ) ? ' selected' : '';
$indent = (int) $c->parent_id > 0 ? '&nbsp;&nbsp;↳ ' : '';
$parent_opts .= "<option value='{$c->id}'$sel>$indent" . esc_html( $c->name ) . "</option>";
}
$role_opts = '';
foreach ( $roles as $k => $r ) {
if ( $k === 'banned' || $k === WBF_Roles::SUPERADMIN ) continue;
$sel = ( $edit && ( $edit->min_role ?? 'member' ) === $k ) ? ' selected' : '';
$role_opts .= "<option value='$k'$sel>" . esc_html( $r['label'] ) . " (Lvl {$r['level']})</option>";
}
echo '<h2>' . ( $edit ? 'Bearbeiten: ' . esc_html( $edit->name ) : 'Neue Kategorie' ) . '</h2>';
echo '<form method="post"><table class="form-table">';
wp_nonce_field( 'wbf_cat_nonce' );
if ( $edit ) echo '<input type="hidden" name="cat_id" value="' . esc_attr( $edit->id ) . '">';
echo '
<tr><th>Elternkategorie</th><td><select name="parent_id">' . $parent_opts . '</select></td></tr>
<tr><th>Name *</th><td><input name="name" value="' . esc_attr( $edit ? $edit->name : '' ) . '" class="regular-text" required></td></tr>
<tr><th>Beschreibung</th><td><textarea name="description" rows="2" class="large-text">' . esc_textarea( $edit ? $edit->description : '' ) . '</textarea></td></tr>
<tr><th>Icon</th><td><input name="icon" value="' . esc_attr( $edit ? $edit->icon : 'fas fa-comments' ) . '" class="regular-text">
<p class="description">z.B. <code>fas fa-home</code>, <code>fas fa-bug</code></p></td></tr>
<tr><th>Min. Rolle</th><td><select name="min_role">' . $role_opts . '</select>
<p class="description">Mindest-Rolle zum <em>Posten</em> in dieser Kategorie.</p></td></tr>
<tr><th>Für Gäste sichtbar</th><td>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" name="guest_visible" value="1"' . ( !$edit || (int)($edit->guest_visible ?? 1) ? " checked" : "" ) . ' style="width:16px;height:16px;accent-color:#0ea5e9">
<span>Nicht eingeloggte Besucher können diese Kategorie sehen</span>
</label>
<p class="description">Deaktivieren = Kategorie und ihre Threads sind nur für eingeloggte Nutzer sichtbar.</p>
</td></tr>
<tr><th>Reihenfolge</th><td><input name="sort_order" type="number" value="' . esc_attr( $edit ? $edit->sort_order : 0 ) . '" class="small-text"></td></tr>';
echo '</table>';
submit_button( $edit ? 'Speichern' : 'Erstellen', 'primary', 'wbf_save_cat' );
echo '</form></div>';
}
// ── Mitglieder ────────────────────────────────────────────────────────────────
function wbf_admin_members() {
$roles = WBF_Roles::get_sorted();
// ── Neuen Nutzer anlegen ──────────────────────────────────────────────────
if ( isset( $_POST['wbf_create_user'] ) && check_admin_referer( 'wbf_create_user_nonce' ) ) {
$username = sanitize_user( $_POST['username'] ?? '' );
$email = sanitize_email( $_POST['email'] ?? '' );
$display_name = sanitize_text_field( $_POST['display_name'] ?? '' );
$password = $_POST['password'] ?? '';
$role = sanitize_key( $_POST['role'] ?? 'member' );
$errors = [];
if ( strlen($username) < 3 ) $errors[] = 'Benutzername mindestens 3 Zeichen.';
if ( ! is_email($email) ) $errors[] = 'Ungültige E-Mail-Adresse.';
if ( strlen($password) < 6 ) $errors[] = 'Passwort mindestens 6 Zeichen.';
if ( empty($display_name) ) $errors[] = 'Anzeigename darf nicht leer sein.';
if ( WBF_DB::get_user_by('username', $username) ) $errors[] = 'Benutzername bereits vergeben.';
if ( WBF_DB::get_user_by('email', $email) ) $errors[] = 'E-Mail bereits registriert.';
if ( $role === WBF_Roles::SUPERADMIN ) $errors[] = 'Superadmin kann nicht manuell vergeben werden.';
if ( empty($errors) ) {
$avatar = 'https://www.gravatar.com/avatar/' . md5(strtolower($email)) . '?d=identicon&s=80';
$new_id = WBF_DB::create_user([
'username' => $username,
'email' => $email,
'password' => password_hash($password, PASSWORD_DEFAULT),
'display_name' => $display_name,
'avatar_url' => $avatar,
'role' => $role,
]);
echo '<div class="notice notice-success is-dismissible"><p>✅ Nutzer <strong>' . esc_html($display_name) . '</strong> wurde angelegt (ID: ' . (int)$new_id . ').</p></div>';
} else {
echo '<div class="notice notice-error is-dismissible"><p>❌ ' . implode('<br>', array_map('esc_html', $errors)) . '</p></div>';
}
}
// ── Rolle ändern ──────────────────────────────────────────────────────────
if ( isset( $_POST['wbf_change_role'] ) && check_admin_referer( 'wbf_member_role_nonce' ) ) {
$uid = (int) $_POST['user_id'];
$role = sanitize_key( $_POST['new_role'] );
if ( $uid && $role !== WBF_Roles::SUPERADMIN && array_key_exists( $role, WBF_Roles::get_all() ) ) {
$update = ['role' => $role];
if ( $role === 'banned' ) {
$update['ban_reason'] = sanitize_textarea_field( $_POST['ban_reason'] ?? '' );
// Zeitlich begrenzte Sperre
$ban_until_raw = sanitize_text_field( $_POST['ban_until'] ?? '' );
if ( $ban_until_raw ) {
$ts = strtotime( $ban_until_raw );
if ( $ts && $ts > time() ) {
// temp_ban speichert pre_ban_role automatisch
WBF_DB::temp_ban( $uid, date('Y-m-d H:i:s', $ts), $update['ban_reason'] );
echo '<div class="notice notice-success is-dismissible"><p>✅ Nutzer temporär gesperrt bis ' . esc_html(date_i18n('d.m.Y H:i', $ts)) . '.</p></div>';
goto after_role_save;
}
}
// Permanente Sperre — pre_ban_role sichern
$existing = WBF_DB::get_user( $uid );
if ( $existing && $existing->role !== 'banned' ) {
$update['pre_ban_role'] = $existing->role;
}
$update['ban_until'] = null;
} else {
$update['ban_reason'] = '';
$update['ban_until'] = null;
$update['pre_ban_role'] = '';
}
WBF_DB::update_user( $uid, $update );
echo '<div class="notice notice-success is-dismissible"><p>Rolle aktualisiert!</p></div>';
after_role_save:;
}
}
// ── Nutzer löschen (DSGVO Art. 17 / Hard Delete) ─────────────────────────
if ( isset( $_POST['wbf_delete_member'] ) && check_admin_referer( 'wbf_delete_member_nonce' ) ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
$mode = sanitize_key( $_POST['delete_mode'] ?? 'anonymize' );
if ( $uid ) {
$target = WBF_DB::get_user( $uid );
if ( ! $target ) {
echo '<div class="notice notice-error is-dismissible"><p>&#10060; Nutzer nicht gefunden.</p></div>';
} elseif ( $target->role === WBF_Roles::SUPERADMIN ) {
echo '<div class="notice notice-error is-dismissible"><p>&#10060; Der Superadmin kann nicht gelöscht werden.</p></div>';
} else {
global $wpdb;
$name = esc_html( $target->display_name );
if ( $mode === 'hard' ) {
// Alle nutzerbezogenen Daten entfernen
$dep_tables = [
'forum_messages' => [ 'from_id', 'to_id' ],
'forum_notifications' => [ 'user_id', 'actor_id' ],
'forum_subscriptions' => [ 'user_id' ],
'forum_bookmarks' => [ 'user_id' ],
'forum_likes' => [ 'user_id' ],
'forum_reactions' => [ 'user_id' ],
'forum_reports' => [ 'reporter_id' ],
'forum_poll_votes' => [ 'user_id' ],
'forum_remember_tokens' => [ 'user_id' ],
'forum_user_meta' => [ 'user_id' ],
'forum_ignore_list' => [ 'user_id', 'ignored_id' ],
];
foreach ( $dep_tables as $tbl => $cols ) {
$full = $wpdb->prefix . $tbl;
if ( $wpdb->get_var( "SHOW TABLES LIKE '$full'" ) !== $full ) continue;
foreach ( $cols as $col ) {
$wpdb->delete( $full, [ $col => $uid ], [ '%d' ] );
}
}
// Threads/Posts auf user_id=0 setzen (Inhalt bleibt)
$wpdb->update( "{$wpdb->prefix}forum_threads", [ 'user_id' => 0 ], [ 'user_id' => $uid ], [ '%d' ], [ '%d' ] );
$wpdb->update( "{$wpdb->prefix}forum_posts", [ 'user_id' => 0 ], [ 'user_id' => $uid ], [ '%d' ], [ '%d' ] );
// Nutzer-Datensatz hart loeschen
$wpdb->delete( "{$wpdb->prefix}forum_users", [ 'id' => $uid ], [ '%d' ] );
echo "<div class='notice notice-success is-dismissible'><p>&#128465; Nutzer <strong>{$name}</strong> dauerhaft geloescht. Threads/Posts anonym gesetzt.</p></div>";
} else {
// DSGVO Art. 17 Anonymisierung (Standard)
$ok = WBF_DB::delete_user_gdpr( $uid );
if ( $ok ) {
echo "<div class='notice notice-success is-dismissible'><p>&#9989; Nutzer <strong>{$name}</strong> anonymisiert (DSGVO Art. 17). Threads und Beitraege bleiben erhalten.</p></div>";
} else {
echo "<div class='notice notice-error is-dismissible'><p>&#10060; Anonymisierung fehlgeschlagen.</p></div>";
}
}
delete_transient( 'wbf_flood_' . $uid );
delete_transient( 'wbf_flood_ts_' . $uid );
}
}
}
// ── Profil bearbeiten ─────────────────────────────────────────────────────
if ( isset( $_POST['wbf_edit_user'] ) && check_admin_referer( 'wbf_edit_user_nonce' ) ) {
$uid = (int) $_POST['user_id'];
$display_name = sanitize_text_field( $_POST['display_name'] ?? '' );
$email = sanitize_email( $_POST['email'] ?? '' );
$new_password = $_POST['new_password'] ?? '';
if ( $uid && $uid !== 0 ) {
$user = WBF_DB::get_user( $uid );
if ( $user && $user->role !== WBF_Roles::SUPERADMIN ) {
$update = [];
if ( ! empty($display_name) ) $update['display_name'] = $display_name;
if ( is_email($email) ) $update['email'] = $email;
if ( strlen($new_password) >= 6 ) $update['password'] = password_hash( $new_password, PASSWORD_DEFAULT );
// Bio + Signatur
if ( isset($_POST['bio']) ) $update['bio'] = sanitize_textarea_field( $_POST['bio'] );
if ( isset($_POST['signature']) ) $update['signature'] = mb_substr( sanitize_textarea_field( $_POST['signature'] ), 0, 300 );
if ( ! empty($update) ) WBF_DB::update_user( $uid, $update );
// Benutzerdefinierte Profilfelder speichern
$cf_defs = WBF_DB::get_profile_field_defs();
foreach ( $cf_defs as $def ) {
$key = sanitize_key( $def['key'] );
if ( ! isset($_POST[ 'cf_' . $key ]) ) continue;
$raw = $_POST[ 'cf_' . $key ];
$value = $def['type'] === 'url' ? esc_url_raw(trim($raw))
: ($def['type'] === 'textarea' ? sanitize_textarea_field($raw)
: ($def['type'] === 'number' ? (is_numeric($raw) ? (string)(float)$raw : '')
: sanitize_text_field($raw)));
WBF_DB::set_user_meta( $uid, $key, $value );
}
echo '<div class="notice notice-success is-dismissible"><p>Profil von <strong>' . esc_html($user->display_name) . '</strong> aktualisiert!</p></div>';
}
}
}
// ── Admin: einzelnen Ignore-Eintrag entfernen ─────────────────────────────
if ( isset( $_POST['wbf_admin_remove_ignore'] ) && check_admin_referer( 'wbf_admin_ignore_nonce' ) ) {
if ( current_user_can('manage_options') ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
$ignored_id = (int) ( $_POST['ignored_id'] ?? 0 );
if ( $uid && $ignored_id ) {
global $wpdb;
$wpdb->delete(
"{$wpdb->prefix}forum_ignore_list",
[ 'user_id' => $uid, 'ignored_id' => $ignored_id ],
[ '%d', '%d' ]
);
echo '<div class="notice notice-success is-dismissible"><p>Ignore-Eintrag entfernt.</p></div>';
}
}
}
// ── Admin: gesamte Ignore-Liste eines Users leeren ────────────────────────
if ( isset( $_POST['wbf_admin_clear_ignores'] ) && check_admin_referer( 'wbf_admin_ignore_nonce' ) ) {
if ( current_user_can('manage_options') ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
if ( $uid ) {
global $wpdb;
$wpdb->delete( "{$wpdb->prefix}forum_ignore_list", [ 'user_id' => $uid ], [ '%d' ] );
echo '<div class="notice notice-success is-dismissible"><p>Ignore-Liste vollständig geleert.</p></div>';
}
}
}
$members = WBF_DB::get_all_users( 200 );
?>
<div class="wrap">
<h1 style="display:flex;align-items:center;justify-content:space-between">
<span>Mitglieder</span>
<button type="button" class="button button-primary" onclick="document.getElementById('wbf-create-user-box').style.display=document.getElementById('wbf-create-user-box').style.display==='none'?'block':'none'">
+ Neuen Nutzer anlegen
</button>
</h1>
<!-- Neuen Nutzer anlegen -->
<div id="wbf-create-user-box" style="display:none;background:#fff;border:1px solid #ddd;border-radius:8px;padding:18px;margin-bottom:20px;max-width:600px">
<h3 style="margin:0 0 14px;font-size:.95rem">Neuen Forum-Nutzer anlegen</h3>
<form method="post">
<?php wp_nonce_field('wbf_create_user_nonce'); ?>
<table class="form-table" style="margin:0">
<tr>
<th style="width:140px"><label>Benutzername *</label></th>
<td><input type="text" name="username" class="regular-text" required minlength="3" placeholder="min. 3 Zeichen"></td>
</tr>
<tr>
<th><label>Anzeigename *</label></th>
<td><input type="text" name="display_name" class="regular-text" required placeholder="Vor- und Nachname"></td>
</tr>
<tr>
<th><label>E-Mail *</label></th>
<td><input type="email" name="email" class="regular-text" required></td>
</tr>
<tr>
<th><label>Passwort *</label></th>
<td>
<input type="text" name="password" class="regular-text" required minlength="6"
value="<?php echo esc_attr(wp_generate_password(12, false)); ?>">
<p class="description">Automatisch generiert — kannst du ändern.</p>
</td>
</tr>
<tr>
<th><label>Rolle</label></th>
<td>
<select name="role">
<?php foreach ($roles as $rk => $rv):
if ($rk === WBF_Roles::SUPERADMIN) continue; ?>
<option value="<?php echo esc_attr($rk); ?>"<?php echo $rk==='member'?' selected':''; ?>>
<?php echo esc_html($rv['label']); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
</table>
<div style="margin-top:12px;display:flex;gap:8px">
<button type="submit" name="wbf_create_user" class="button button-primary">Nutzer anlegen</button>
<button type="button" class="button" onclick="document.getElementById('wbf-create-user-box').style.display='none'">Abbrechen</button>
</div>
</form>
</div>
<table class="widefat striped">
<thead>
<tr>
<th>#</th><th>Nutzer</th><th>E-Mail</th>
<th>Aktuelle Rolle</th><th>Beiträge</th>
<th>Registriert</th><th>Rolle ändern</th>
</tr>
</thead>
<tbody>
<?php foreach ( $members as $m ) :
$role = WBF_Roles::get( $m->role );
$color = esc_attr( $role['color'] );
$bg = esc_attr( $role['bg_color'] );
$icon = esc_attr( $role['icon'] ?? 'fas fa-user' );
$is_sa = ( $m->role === WBF_Roles::SUPERADMIN );
$ban_reason = esc_attr( $m->ban_reason ?? '' );
$opts = '';
foreach ( $roles as $k => $r ) {
if ( $k === WBF_Roles::SUPERADMIN ) continue;
$sel = $m->role === $k ? ' selected' : '';
$opts .= '<option value="' . esc_attr($k) . '"' . $sel . '>' . esc_html( $r['label'] ) . '</option>';
}
?>
<tr>
<td><?php echo esc_html( $m->id ); ?></td>
<td>
<strong><?php echo esc_html( $m->display_name ); ?></strong><br>
<small style="color:#999">@<?php echo esc_html( $m->username ); ?></small>
</td>
<td><?php echo esc_html( $m->email ); ?></td>
<td>
<span class="wbf-role-preview" style="color:<?php echo $color; ?>;background:<?php echo $bg; ?>;border-color:<?php echo $color; ?>">
<i class="<?php echo $icon; ?>"></i> <?php echo esc_html( $role['label'] ); ?>
</span>
<?php if ( $is_sa ) : ?><em style="color:#999;font-size:.8em">(WP-Admin)</em><?php endif; ?>
</td>
<td><?php echo esc_html( $m->post_count ); ?></td>
<td><?php echo esc_html( date( 'd.m.Y', strtotime( $m->registered ) ) ); ?></td>
<td>
<?php if ( $is_sa ) : ?>
<em style="color:#999">Automatisch (WP-Admin)</em>
<?php else : ?>
<form method="post" style="display:flex;flex-direction:column;gap:5px">
<?php wp_nonce_field( 'wbf_member_role_nonce' ); ?>
<input type="hidden" name="user_id" value="<?php echo esc_attr( $m->id ); ?>">
<div style="display:flex;gap:6px;align-items:center">
<select name="new_role" style="height:28px"
onchange="var r=this.closest('form').querySelector('.wbf-ban-reason');r.style.display=this.value==='banned'?'block':'none'">
<?php echo $opts; ?>
</select>
<button type="submit" name="wbf_change_role" class="button button-small">Speichern</button>
<button type="button" class="button button-small"
onclick="var d=document.getElementById('wbf-edit-user-<?php echo (int)$m->id; ?>');d.style.display=d.style.display==='none'?'block':'none'">
Profil bearbeiten
</button>
<button type="button" class="button button-small"
style="color:#dc2626;border-color:#dc2626"
onclick="var d=document.getElementById('wbf-delete-user-<?php echo (int)$m->id; ?>');d.style.display=d.style.display==='none'?'block':'none'">
🗑️ Löschen
</button>
</div>
<div class="wbf-ban-reason" style="display:<?php echo $m->role === 'banned' ? 'block' : 'none'; ?>;margin-top:3px">
<input type="text" name="ban_reason" value="<?php echo $ban_reason; ?>"
placeholder="Sperrgrund (wird dem Nutzer beim Login angezeigt)"
style="width:100%;max-width:340px;font-size:12px;height:26px;margin-bottom:4px">
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
<label style="font-size:11px;color:#555;white-space:nowrap">Automatisch entsperren am:</label>
<input type="datetime-local" name="ban_until"
value="<?php echo $m->ban_until ? esc_attr(date('Y-m-d\TH:i', strtotime($m->ban_until))) : ''; ?>"
min="<?php echo date('Y-m-d\TH:i'); ?>"
style="font-size:11px;height:26px;border:1px solid #ccc;border-radius:3px;padding:0 4px">
<span style="font-size:11px;color:#999">Leer = permanente Sperre</span>
</div>
<?php if ( $m->role === 'banned' && ! empty($m->ban_until) ): ?>
<div style="margin-top:4px;font-size:11px;color:#f97316;font-weight:600">
<i class="dashicons dashicons-clock" style="font-size:13px;width:13px;height:13px;vertical-align:-2px"></i>
Temporäre Sperre — läuft ab am <?php echo esc_html(date_i18n('d.m.Y \u\m H:i \U\h\r', strtotime($m->ban_until))); ?>
<?php
$diff = strtotime($m->ban_until) - time();
if ($diff > 0) {
$days = floor($diff / 86400);
$hours = floor(($diff % 86400) / 3600);
$mins = floor(($diff % 3600) / 60);
$parts = [];
if ($days) $parts[] = $days . ' Tag' . ($days > 1 ? 'e' : '');
if ($hours) $parts[] = $hours . ' Std.';
if ($mins && !$days) $parts[] = $mins . ' Min.';
echo ' (' . esc_html(implode(', ', $parts)) . ' verbleibend)';
}
?>
</div>
<?php elseif ( $m->role === 'banned' && empty($m->ban_until) ): ?>
<div style="margin-top:4px;font-size:11px;color:#ef4444;font-weight:600">
<i class="dashicons dashicons-no" style="font-size:13px;width:13px;height:13px;vertical-align:-2px"></i>
Permanente Sperre
</div>
<?php endif; ?>
</div>
</form>
<!-- Inline Profil-Editor -->
<div id="wbf-edit-user-<?php echo (int)$m->id; ?>" style="display:none;margin-top:8px;padding:12px;background:#f9f9f9;border:1px solid #ddd;border-radius:4px;max-width:480px">
<strong style="font-size:13px">Profil bearbeiten: <?php echo esc_html($m->display_name); ?></strong>
<form method="post" style="margin-top:8px">
<?php wp_nonce_field( 'wbf_edit_user_nonce' ); ?>
<input type="hidden" name="user_id" value="<?php echo esc_attr( $m->id ); ?>">
<table style="width:100%;font-size:13px;border-collapse:collapse">
<tr>
<td style="padding:4px 8px 4px 0;white-space:nowrap;width:130px"><label>Anzeigename</label></td>
<td><input type="text" name="display_name" value="<?php echo esc_attr($m->display_name); ?>" style="width:100%;height:26px;font-size:12px"></td>
</tr>
<tr>
<td style="padding:4px 8px 4px 0;white-space:nowrap"><label>E-Mail</label></td>
<td><input type="email" name="email" value="<?php echo esc_attr($m->email); ?>" style="width:100%;height:26px;font-size:12px"></td>
</tr>
<tr>
<td style="padding:4px 8px 4px 0;white-space:nowrap"><label>Neues Passwort</label></td>
<td>
<input type="password" name="new_password" placeholder="Leer = nicht ändern" style="width:100%;height:26px;font-size:12px">
<p style="margin:2px 0 0;font-size:11px;color:#666">Min. 6 Zeichen</p>
</td>
</tr>
<tr>
<td style="padding:4px 8px 4px 0;vertical-align:top;padding-top:6px"><label>Bio</label></td>
<td><textarea name="bio" rows="2" style="width:100%;font-size:12px;resize:vertical"><?php echo esc_textarea($m->bio ?? ''); ?></textarea></td>
</tr>
<tr>
<td style="padding:4px 8px 4px 0;vertical-align:top;padding-top:6px"><label>Signatur</label></td>
<td>
<textarea name="signature" rows="2" maxlength="300" style="width:100%;font-size:12px;resize:vertical"><?php echo esc_textarea($m->signature ?? ''); ?></textarea>
<p style="margin:2px 0 0;font-size:11px;color:#666">Max. 300 Zeichen</p>
</td>
</tr>
<?php
// Benutzerdefinierte Profilfelder
$cf_admin_defs = WBF_DB::get_profile_field_defs();
if ( ! empty($cf_admin_defs) ):
$cf_admin_vals = WBF_DB::get_user_meta( $m->id );
?>
<tr><td colspan="2" style="padding:8px 0 4px">
<strong style="font-size:12px;color:#555;text-transform:uppercase;letter-spacing:.04em">Weitere Profilangaben</strong>
<hr style="margin:4px 0">
</td></tr>
<?php foreach ( $cf_admin_defs as $def ):
$cf_k = esc_attr($def['key']);
$cf_lbl = esc_html($def['label']);
$cf_val = $cf_admin_vals[$def['key']] ?? '';
$cf_ph = esc_attr($def['placeholder'] ?? '');
?>
<tr>
<td style="padding:4px 8px 4px 0;white-space:nowrap;vertical-align:top;padding-top:6px">
<label><?php echo $cf_lbl; ?></label>
</td>
<td>
<?php if ($def['type'] === 'textarea'): ?>
<textarea name="cf_<?php echo $cf_k; ?>" rows="2"
placeholder="<?php echo $cf_ph; ?>"
style="width:100%;font-size:12px;resize:vertical"><?php echo esc_textarea($cf_val); ?></textarea>
<?php elseif ($def['type'] === 'select'):
$cf_opts = array_filter(array_map('trim', explode("\n", $def['options'] ?? '')));
?>
<select name="cf_<?php echo $cf_k; ?>" style="width:100%;height:26px;font-size:12px">
<option value="">— Bitte wählen —</option>
<?php foreach ($cf_opts as $opt): ?>
<option value="<?php echo esc_attr($opt); ?>" <?php selected($cf_val,$opt); ?>><?php echo esc_html($opt); ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<input type="<?php echo $def['type']==='url'?'url':($def['type']==='number'?'number':'text'); ?>"
name="cf_<?php echo $cf_k; ?>"
value="<?php echo esc_attr($cf_val); ?>"
placeholder="<?php echo $cf_ph; ?>"
style="width:100%;height:26px;font-size:12px">
<?php endif; ?>
</td>
</tr>
<?php endforeach; endif; ?>
</table>
<div style="margin-top:10px">
<button type="submit" name="wbf_edit_user" class="button button-primary button-small">Änderungen speichern</button>
</div>
</form>
<!-- Ignore-Liste des Nutzers -->
<?php
$admin_ignore_list = WBF_DB::get_ignore_list( $m->id );
?>
<div style="margin-top:14px;border-top:1px solid #e0e0e0;padding-top:12px">
<strong style="font-size:12px;color:#555;text-transform:uppercase;letter-spacing:.04em">
<span class="dashicons dashicons-hidden" style="font-size:14px;width:14px;height:14px;vertical-align:-2px"></span>
Ignorierte Nutzer (<?php echo count($admin_ignore_list); ?>)
</strong>
<?php if ( empty($admin_ignore_list) ): ?>
<p style="font-size:12px;color:#999;margin:6px 0 0">Dieser Nutzer ignoriert niemanden.</p>
<?php else: ?>
<table style="width:100%;font-size:12px;margin-top:8px;border-collapse:collapse">
<thead>
<tr style="background:#f0f0f0">
<th style="padding:4px 6px;text-align:left;font-weight:600">Nutzer</th>
<th style="padding:4px 6px;text-align:left;font-weight:600">Ignoriert seit</th>
<th style="padding:4px 6px;text-align:center;font-weight:600">Entfernen</th>
</tr>
</thead>
<tbody>
<?php foreach ( $admin_ignore_list as $ign ): ?>
<tr style="border-bottom:1px solid #eee">
<td style="padding:4px 6px">
<strong><?php echo esc_html($ign->display_name); ?></strong>
<span style="color:#999"> @<?php echo esc_html($ign->username); ?></span>
</td>
<td style="padding:4px 6px;color:#666">
<?php echo esc_html(date_i18n('d.m.Y H:i', strtotime($ign->ignored_since))); ?>
</td>
<td style="padding:4px 6px;text-align:center">
<form method="post" style="display:inline">
<?php wp_nonce_field('wbf_admin_ignore_nonce'); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<input type="hidden" name="ignored_id" value="<?php echo (int)$ign->id; ?>">
<button type="submit" name="wbf_admin_remove_ignore"
class="button button-small"
style="color:#c00;border-color:#c00"
onclick="return confirm('Ignore-Eintrag für <?php echo esc_js($ign->display_name); ?> entfernen?')">
<span class="dashicons dashicons-no" style="font-size:13px;width:13px;height:13px;vertical-align:-2px"></span>
Entfernen
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<form method="post" style="margin-top:8px">
<?php wp_nonce_field('wbf_admin_ignore_nonce'); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<button type="submit" name="wbf_admin_clear_ignores"
class="button button-small"
style="color:#c00;border-color:#c00"
onclick="return confirm('Gesamte Ignore-Liste von <?php echo esc_js($m->display_name); ?> leeren?')">
<span class="dashicons dashicons-trash" style="font-size:13px;width:13px;height:13px;vertical-align:-2px"></span>
Alle Einträge löschen
</button>
</form>
<?php endif; ?>
</div>
</div><!-- /#wbf-edit-user -->
<!-- Löschen-Dialog -->
<div id="wbf-delete-user-<?php echo (int)$m->id; ?>"
style="display:none;margin-top:8px;padding:14px 16px;background:#fff5f5;border:1px solid #fca5a5;border-radius:6px;max-width:480px">
<p style="margin:0 0 10px;font-weight:700;color:#dc2626;font-size:.9rem">
🗑️ Nutzer löschen: <?php echo esc_html($m->display_name); ?>
</p>
<p style="font-size:.82rem;color:#6b7280;margin:0 0 12px">
Wähle wie der Account behandelt werden soll:
</p>
<!-- Option A: DSGVO Anonymisierung -->
<form method="post" style="margin-bottom:8px">
<?php wp_nonce_field( 'wbf_delete_member_nonce' ); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<input type="hidden" name="delete_mode" value="anonymize">
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:10px 12px;margin-bottom:6px">
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
<span>
<strong style="font-size:.83rem;color:#374151">📋 DSGVO Anonymisieren</strong>
<span style="display:block;font-size:.77rem;color:#9ca3af;margin-top:2px">
Account wird anonymisiert, Threads &amp; Beiträge bleiben unter „Gelöschter Nutzer" erhalten.
</span>
</span>
</label>
</div>
<button type="submit" name="wbf_delete_member" class="button"
style="font-size:.8rem;height:28px;line-height:28px;background:#fff8f0;border-color:#f97316;color:#c2410c"
onclick="return confirm('Nutzer <?php echo esc_js($m->display_name); ?> anonymisieren?\n\nDer Account wird nach DSGVO Art. 17 anonymisiert. Alle Inhalte bleiben anonym erhalten.')">
📋 Anonymisieren
</button>
</form>
<!-- Option B: Hard Delete -->
<form method="post">
<?php wp_nonce_field( 'wbf_delete_member_nonce' ); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<input type="hidden" name="delete_mode" value="hard">
<div style="background:#fff;border:1px solid #fca5a5;border-radius:6px;padding:10px 12px;margin-bottom:6px">
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
<span>
<strong style="font-size:.83rem;color:#dc2626">⚠️ Dauerhaft löschen</strong>
<span style="display:block;font-size:.77rem;color:#9ca3af;margin-top:2px">
Account + alle nutzerbezogenen Daten werden unwiderruflich gelöscht. Threads/Posts bleiben anonym erhalten.
</span>
</span>
</label>
</div>
<button type="submit" name="wbf_delete_member" class="button"
style="font-size:.8rem;height:28px;line-height:28px;background:#fef2f2;border-color:#dc2626;color:#dc2626"
onclick="return confirm('⚠️ ACHTUNG: Nutzer <?php echo esc_js($m->display_name); ?> DAUERHAFT löschen?\n\nDieser Vorgang kann NICHT rückgängig gemacht werden!\n\nAlle persönlichen Daten, Nachrichten und Einstellungen werden unwiderruflich entfernt.')">
🗑️ Dauerhaft löschen
</button>
</form>
<button type="button" class="button" style="margin-top:8px;font-size:.78rem"
onclick="document.getElementById('wbf-delete-user-<?php echo (int)$m->id; ?>').style.display='none'">
Abbrechen
</button>
</div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php
}
// ── Meldungen ─────────────────────────────────────────────────────────────────
function wbf_admin_reports() {
if ( isset( $_GET['report_action'], $_GET['report_id'] ) && check_admin_referer( 'wbf_report_action' ) ) {
$rid = (int) $_GET['report_id'];
$action = sanitize_key( $_GET['report_action'] );
if ( in_array( $action, ['resolved', 'dismissed', 'open'] ) ) {
WBF_DB::update_report( $rid, $action );
echo '<div class="notice notice-success is-dismissible"><p>Meldung aktualisiert.</p></div>';
}
}
$filter = sanitize_key( $_GET['filter'] ?? 'open' );
$reports = WBF_DB::get_reports( $filter, 100 );
$count = WBF_DB::count_open_reports();
echo '<div class="wrap"><h1>Meldungen ' . ( $count > 0 ? '<span class="awaiting-mod">' . (int) $count . '</span>' : '' ) . '</h1>';
$tabs = ['open' => 'Offen', 'resolved' => 'Erledigt', 'dismissed' => 'Verworfen', 'all' => 'Alle'];
echo '<ul class="subsubsub" style="margin-bottom:1rem">';
foreach ( $tabs as $k => $label ) {
$url = esc_url( add_query_arg( ['page' => 'wbf-reports', 'filter' => $k] ) );
$active = ( $filter === $k ) ? ' class="current"' : '';
echo "<li><a href='$url'$active>$label</a> | </li>";
}
echo '</ul>';
if ( empty( $reports ) ) {
echo '<p style="color:#999">Keine Meldungen vorhanden.</p></div>';
return;
}
$reason_labels = [
'spam' => 'Spam / Werbung', 'harassment' => 'Belästigung',
'inappropriate' => 'Unangemessen', 'misinformation' => 'Fehlinformation',
'off-topic' => 'Offtopic', 'other' => 'Sonstiges',
];
$status_colors = ['open' => '#f59e0b', 'resolved' => '#56cf7e', 'dismissed' => '#6b7a99'];
echo '<table class="widefat striped">
<thead><tr><th>#</th><th>Gemeldet von</th><th>Grund</th><th>Vorschau</th><th>Thread</th><th>Datum</th><th>Status</th><th>Aktionen</th></tr></thead><tbody>';
foreach ( $reports as $r ) {
$reason_label = esc_html( $reason_labels[ $r->reason ] ?? $r->reason );
$preview = esc_html( mb_substr( strip_tags( $r->post_content ?? '' ), 0, 80 ) );
$sc = $status_colors[ $r->status ] ?? '#999';
$status_badge = "<span style='color:{$sc};font-weight:600'>" . esc_html( ucfirst( $r->status ) ) . "</span>";
$note_cell = $r->note ? '<br><small style="color:#999">' . esc_html( $r->note ) . '</small>' : '';
$base = ['page' => 'wbf-reports', 'report_id' => $r->id, 'filter' => $filter];
if ( $r->status === 'open' ) {
$res_url = esc_url( wp_nonce_url( add_query_arg( array_merge( $base, ['report_action' => 'resolved'] ), admin_url('admin.php') ), 'wbf_report_action' ) );
$dis_url = esc_url( wp_nonce_url( add_query_arg( array_merge( $base, ['report_action' => 'dismissed'] ), admin_url('admin.php') ), 'wbf_report_action' ) );
$actions = "<a href='$res_url' class='button button-small' style='color:#56cf7e'>✔ Erledigt</a> ";
$actions .= "<a href='$dis_url' class='button button-small' style='color:#6b7a99'>✖ Verwerfen</a>";
} else {
$reopen_url = esc_url( wp_nonce_url( add_query_arg( array_merge( $base, ['report_action' => 'open'] ), admin_url('admin.php') ), 'wbf_report_action' ) );
$actions = "<a href='$reopen_url' class='button button-small'>↩ Wieder öffnen</a>";
}
echo "<tr>
<td>" . esc_html( $r->id ) . "</td>
<td><strong>" . esc_html( $r->reporter_name ?? '—' ) . "</strong><br><small style='color:#999'>@" . esc_html( $r->reporter_username ?? '' ) . "</small></td>
<td><span style='background:#f59e0b22;color:#f59e0b;padding:2px 8px;border-radius:4px;font-size:.8em;font-weight:600'>$reason_label</span>$note_cell</td>
<td style='font-size:.82em;color:#555;max-width:200px'>$preview…<br>
<a href='" . esc_url( wbf_get_forum_url() ) . "?forum_thread=" . esc_attr( $r->thread_id ) . "#post-" . esc_attr( $r->object_id ) . "' target='_blank' style='font-size:.78em'>→ Zum Beitrag</a></td>
<td style='font-size:.82em'>" . ( $r->thread_id ? esc_html( mb_substr( $r->thread_title ?? '', 0, 35 ) ) : '—' ) . "</td>
<td style='font-size:.82em;color:#666'>" . esc_html( date( 'd.m.Y H:i', strtotime( $r->created_at ) ) ) . "</td>
<td>$status_badge</td>
<td style='white-space:nowrap'>$actions</td>
</tr>";
}
echo '</tbody></table></div>';
}
// ── Level-System ──────────────────────────────────────────────────────────────
function wbf_admin_levels() {
// ── Toggle on/off ────────────────────────────────────────────────────────
if ( isset( $_POST['wbf_toggle_levels'] ) && check_admin_referer( 'wbf_levels_toggle_nonce' ) ) {
WBF_Levels::set_enabled( ! empty( $_POST['levels_enabled'] ) );
echo '<div class="notice notice-success is-dismissible"><p>Einstellung gespeichert!</p></div>';
}
// ── Reset ────────────────────────────────────────────────────────────────
if ( isset( $_POST['wbf_reset_levels'] ) && check_admin_referer( 'wbf_levels_reset_nonce' ) ) {
WBF_Levels::reset_to_defaults();
echo '<div class="notice notice-success is-dismissible"><p>Level auf Standard zurückgesetzt.</p></div>';
}
// ── Level löschen ────────────────────────────────────────────────────────
if ( isset( $_GET['delete_level'] ) && check_admin_referer( 'delete_level_' . (int) $_GET['delete_level'] ) ) {
$idx = (int) $_GET['delete_level'];
$levels = WBF_Levels::get_all();
if ( count($levels) > 1 ) {
unset( $levels[$idx] );
WBF_Levels::save( array_values($levels) );
echo '<div class="notice notice-success is-dismissible"><p>Level gelöscht.</p></div>';
} else {
echo '<div class="notice notice-error"><p>Mindestens ein Level muss vorhanden sein.</p></div>';
}
}
// ── Level speichern (Bearbeiten) ─────────────────────────────────────────
if ( isset( $_POST['wbf_save_level'] ) && check_admin_referer( 'wbf_levels_edit_nonce' ) ) {
$idx = (int) $_POST['level_index'];
$levels = WBF_Levels::get_all();
$levels[$idx] = [
'min' => max( 0, (int) $_POST['level_min'] ),
'label' => sanitize_text_field( $_POST['level_label'] ),
'icon' => sanitize_text_field( $_POST['level_icon'] ),
'color' => sanitize_hex_color( $_POST['level_color'] ) ?: '#94a3b8',
];
WBF_Levels::save( array_values($levels) );
echo '<div class="notice notice-success is-dismissible"><p>Level aktualisiert!</p></div>';
}
// ── Neues Level erstellen ────────────────────────────────────────────────
if ( isset( $_POST['wbf_create_level'] ) && check_admin_referer( 'wbf_levels_create_nonce' ) ) {
$levels = WBF_Levels::get_all();
$levels[] = [
'min' => max( 0, (int) $_POST['new_level_min'] ),
'label' => sanitize_text_field( $_POST['new_level_label'] ),
'icon' => sanitize_text_field( $_POST['new_level_icon'] ),
'color' => sanitize_hex_color( $_POST['new_level_color'] ) ?: '#94a3b8',
];
WBF_Levels::save( $levels );
echo '<div class="notice notice-success is-dismissible"><p>Neues Level erstellt!</p></div>';
}
$levels = WBF_Levels::get_all();
$enabled = WBF_Levels::is_enabled();
$edit_i = isset( $_GET['edit_level'] ) ? (int) $_GET['edit_level'] : null;
$edit_l = ( $edit_i !== null && isset( $levels[$edit_i] ) ) ? $levels[$edit_i] : null;
echo '<div class="wrap">';
echo '<h1>⭐ Level-System</h1>';
// ── Status-Toggle ────────────────────────────────────────────────────────
$status_color = $enabled ? '#56cf7e' : '#f05252';
$status_label = $enabled ? '✅ Aktiviert' : '❌ Deaktiviert';
echo "<div style='display:flex;align-items:center;gap:16px;background:#fff;border:1px solid #ddd;border-radius:8px;padding:16px 20px;margin-bottom:1.5rem;max-width:500px'>
<div>
<strong>Level-System ist:</strong>
<span style='color:{$status_color};font-weight:700;margin-left:8px'>{$status_label}</span>
<p style='margin:.4rem 0 0;color:#666;font-size:.85em'>Wenn deaktiviert, werden keine Level-Badges im Forum angezeigt.</p>
</div>
<form method='post' style='margin-left:auto;flex-shrink:0'>
" . wp_nonce_field( 'wbf_levels_toggle_nonce', '_wpnonce', true, false ) . "
<input type='hidden' name='levels_enabled' value='" . ( $enabled ? '' : '1' ) . "'>
<button type='submit' name='wbf_toggle_levels' class='button " . ( $enabled ? 'button-secondary' : 'button-primary' ) . "'>
" . ( $enabled ? '⏸ Deaktivieren' : '▶ Aktivieren' ) . "
</button>
</form>
</div>";
// ── Level-Tabelle ────────────────────────────────────────────────────────
echo '<h2>Aktuelle Level</h2>';
echo '<table class="widefat striped" style="margin-bottom:2rem;max-width:760px">
<thead><tr>
<th style="width:60px">#</th>
<th>Badge-Vorschau</th>
<th>Ab Beiträgen</th>
<th>Icon</th>
<th>Aktionen</th>
</tr></thead><tbody>';
foreach ( $levels as $i => $l ) {
$color = esc_attr( $l['color'] );
$icon = esc_attr( $l['icon'] );
$label = esc_html( $l['label'] );
$del_url = wp_nonce_url( "?page=wbf-levels&delete_level={$i}", "delete_level_{$i}" );
$badge_html = "<span style='display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:700;
color:{$color};border:1.5px solid {$color};background:rgba(0,0,0,.07);
padding:2px 9px;border-radius:20px;'>
<i class='{$icon}'></i> {$label}
</span>";
echo "<tr>
<td><strong>" . ( $i + 1 ) . "</strong></td>
<td>{$badge_html}</td>
<td><strong>≥ " . esc_html( $l['min'] ) . "</strong> Beiträge</td>
<td><code style='font-size:.8em'>{$icon}</code></td>
<td style='white-space:nowrap'>
<a href='?page=wbf-levels&edit_level={$i}'>Bearbeiten</a>
" . ( count($levels) > 1 ? " | <a href='" . esc_url($del_url) . "' style='color:#dc2626' onclick='return confirm(\"Level wirklich löschen?\")'>Löschen</a>" : '' ) . "
</td>
</tr>";
}
echo '</tbody></table>';
// ── Reset-Button ─────────────────────────────────────────────────────────
echo "<form method='post' style='margin-bottom:2rem'>
" . wp_nonce_field( 'wbf_levels_reset_nonce', '_wpnonce', true, false ) . "
<button type='submit' name='wbf_reset_levels' class='button'
onclick='return confirm(\"Alle Level auf Standard zurücksetzen?\")'>
↺ Auf Standard zurücksetzen
</button>
</form>";
// ── Bearbeiten-Formular ──────────────────────────────────────────────────
if ( $edit_l !== null ) {
echo '<h2>Level bearbeiten: <em>' . esc_html( $edit_l['label'] ) . '</em></h2>';
echo '<form method="post" style="max-width:500px"><table class="form-table">';
wp_nonce_field( 'wbf_levels_edit_nonce' );
echo '<input type="hidden" name="level_index" value="' . esc_attr( $edit_i ) . '">';
echo wbf_level_form_fields( $edit_l, '' );
echo '</table>';
submit_button( 'Änderungen speichern', 'primary', 'wbf_save_level' );
echo '</form><hr style="margin:2rem 0">';
}
// ── Neues Level erstellen ────────────────────────────────────────────────
echo '<h2 id="new-level">Neues Level erstellen</h2>';
echo '<form method="post" style="max-width:500px"><table class="form-table">';
wp_nonce_field( 'wbf_levels_create_nonce' );
echo wbf_level_form_fields( null, 'new_' );
echo '</table>';
submit_button( '+ Level erstellen', 'primary', 'wbf_create_level' );
echo '</form>';
echo '</div>';
}
// ── Hilfsformular für Level-Felder ────────────────────────────────────────────
function wbf_level_form_fields( $level, $prefix ) {
$min = esc_attr( $level['min'] ?? '' );
$label = esc_attr( $level['label'] ?? '' );
$icon = esc_attr( $level['icon'] ?? 'fas fa-star' );
$color = esc_attr( $level['color'] ?? '#94a3b8' );
return "
<tr>
<th>Ab Beiträgen *</th>
<td>
<input name='{$prefix}level_min' type='number' value='{$min}' class='small-text' min='0' required>
<p class='description'>Level ab dieser Beitragsanzahl (0 = für alle).</p>
</td>
</tr>
<tr>
<th>Bezeichnung *</th>
<td><input name='{$prefix}level_label' value='{$label}' class='regular-text' required></td>
</tr>
<tr>
<th>Farbe</th>
<td><input name='{$prefix}level_color' type='color' value='{$color}'></td>
</tr>
<tr>
<th>Icon</th>
<td>
<input name='{$prefix}level_icon' value='{$icon}' class='regular-text'>
<p class='description'>FontAwesome 6, z.B. <code>fas fa-crown</code>, <code>fas fa-fire</code>, <code>fas fa-seedling</code></p>
</td>
</tr>";
}
// ── Reaktionen ─────────────────────────────────────────────────────────────────
function wbf_admin_reactions() {
if ( ! current_user_can('manage_options') ) return;
$defaults = ['👍','❤️','😂','😮','😢','😡'];
// Speichern
if ( isset($_POST['wbf_save_reactions']) && check_admin_referer('wbf_reactions_nonce') ) {
$raw = $_POST['wbf_reactions_list'] ?? '';
$emojis = [];
// Split by whitespace or comma, keep only single emoji characters
$parts = preg_split('/[\s,]+/', trim($raw), -1, PREG_SPLIT_NO_EMPTY);
foreach ($parts as $p) {
// Accept any character sequence 1-8 chars (covers multi-codepoint emojis)
$p = trim($p);
if ( mb_strlen($p) >= 1 && mb_strlen($p) <= 8 ) {
$emojis[] = $p;
}
}
$emojis = array_values(array_unique($emojis));
if ( empty($emojis) ) $emojis = $defaults; // Fallback
$emojis = array_slice($emojis, 0, 20); // Max 20 Reaktionen
update_option('wbf_reactions', $emojis);
echo '<div class="notice notice-success is-dismissible"><p>✅ Reaktionen gespeichert!</p></div>';
}
// Reset
if ( isset($_POST['wbf_reset_reactions']) && check_admin_referer('wbf_reactions_nonce') ) {
delete_option('wbf_reactions');
echo '<div class="notice notice-success is-dismissible"><p>✅ Reaktionen auf Standard zurückgesetzt!</p></div>';
}
$current = WBF_DB::get_allowed_reactions();
// Emoji-Kategorien für die Schnellauswahl
$emoji_groups = [
'Gesichter' => ['😀','😃','😄','😁','😆','😅','😂','🤣','😊','😇','🥰','😍','🤩','😘','😗','😙','😚','🙂','🤗','🤭','🤫','🤔','🤐','🤨','😐','😑','😶','😏','😒','😞','😔','😟','😕','🙃','😣','😖','😫','😩','🥺','😢','😭','😤','😠','😡','🤬','😈','👿','💀','☠️','💩','🤡','👹','👺','👻','👽','👾','🤖'],
'Gesten' => ['👍','👎','👏','🙌','🤝','🤜','🤛','✊','👊','🤚','✋','🖐','👋','🤙','💪','🦾','🙏','🤲','🫶','❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❣️','💕','💞','💓','💗','💖','💘','💝'],
'Symbole' => ['🔥','⭐','✨','💫','💥','💢','❗','❓','💯','✅','❌','⚠️','🚀','🎉','🎊','🏆','🥇','🎯','💡','💎','🔑','🛡️','⚔️','🗡️','🔔','📢','📣','🎵','🎶','💻','📱','🖥️','📷','🎮'],
'Natur' => ['🌟','⚡','🌈','❄️','🌊','🔮','🌸','🌺','🌻','🌹','🍀','🌿','🌱','🌲','🌳','🦋','🐉','🦄','🐺','🦊','🐸','🐧','🦅','🦁'],
'Essen' => ['🍕','🍔','🍟','🌮','🌯','🍜','🍝','🍣','🍱','🍩','🎂','🍺','🥂','☕','🧋','🍭'],
];
?>
<div class="wrap" style="max-width:900px">
<h1 style="display:flex;align-items:center;gap:10px">
<span style="font-size:1.5rem">💬</span> Reaktionen verwalten
</h1>
<p style="color:#666;margin-bottom:1.5rem">
Lege fest welche Emoji-Reaktionen Nutzer unter Posts verwenden können. Maximal 20.
</p>
<div style="display:grid;grid-template-columns:1fr 380px;gap:20px">
<!-- ── Aktuelle Reaktionen + Eingabe ──────────────── -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden">
<div style="padding:14px 18px;background:#f0f9ff;border-bottom:1px solid #bae6fd;display:flex;align-items:center;gap:8px">
<strong style="color:#0369a1;font-size:.9rem;text-transform:uppercase;letter-spacing:.04em">
✏️ Aktuelle Konfiguration
</strong>
</div>
<div style="padding:18px">
<!-- Vorschau -->
<p style="font-size:.82rem;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.04em;margin:0 0 10px">
Aktive Reaktionen (<?php echo count($current); ?>/20)
</p>
<div id="wbf-reaction-preview" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px;min-height:42px;padding:8px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:7px">
<?php foreach ($current as $emoji): ?>
<span class="wbf-r-chip" data-emoji="<?php echo esc_attr($emoji); ?>"
style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:#fff;border:1.5px solid #e5e7eb;border-radius:20px;font-size:1.3rem;cursor:pointer;transition:all .15s"
title="Klicken zum Entfernen">
<?php echo esc_html($emoji); ?>
<span style="font-size:.7rem;color:#9ca3af;line-height:1">✕</span>
</span>
<?php endforeach; ?>
</div>
<form method="post" id="wbf-reactions-form">
<?php wp_nonce_field('wbf_reactions_nonce'); ?>
<label style="display:block;font-size:.82rem;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.04em;margin-bottom:6px">
Reaktionen (durch Leerzeichen getrennt)
</label>
<textarea name="wbf_reactions_list" id="wbfReactionsList" rows="3"
style="width:100%;font-size:1.4rem;line-height:2;padding:8px;border:1.5px solid #d1d5db;border-radius:6px;resize:vertical;box-sizing:border-box;letter-spacing:4px"><?php echo esc_textarea(implode(' ', $current)); ?></textarea>
<p style="font-size:.78rem;color:#9ca3af;margin:.4rem 0 1rem">
Emojis direkt eingeben oder unten aus der Bibliothek auswählen. Max. 20 Stück.
</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button type="submit" name="wbf_save_reactions" class="button button-primary">
💾 Speichern
</button>
<button type="submit" name="wbf_reset_reactions" class="button"
onclick="return confirm('Wirklich auf Standard zurücksetzen?')">
↺ Standard wiederherstellen
</button>
</div>
</form>
</div>
</div>
<!-- ── Emoji-Bibliothek ────────────────────────────── -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden">
<div style="padding:14px 18px;background:#fafaf9;border-bottom:1px solid #e7e5e4;display:flex;align-items:center;gap:8px">
<strong style="color:#57534e;font-size:.9rem;text-transform:uppercase;letter-spacing:.04em">
📚 Emoji-Bibliothek
</strong>
</div>
<div style="padding:12px;max-height:520px;overflow-y:auto">
<!-- Kategorie-Tabs -->
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<?php $first = true; foreach ($emoji_groups as $group_name => $group_emojis): ?>
<button type="button" class="wbf-eg-tab button<?php echo $first ? ' button-primary' : ''; ?>"
data-group="<?php echo esc_attr($group_name); ?>"
style="font-size:.72rem;padding:2px 8px;height:auto">
<?php echo esc_html($group_name); ?>
</button>
<?php $first = false; endforeach; ?>
</div>
<?php $first = true; foreach ($emoji_groups as $group_name => $group_emojis): ?>
<div class="wbf-eg-panel" data-group="<?php echo esc_attr($group_name); ?>"
style="display:<?php echo $first ? 'flex' : 'none'; ?>;flex-wrap:wrap;gap:4px">
<?php foreach ($group_emojis as $e): ?>
<button type="button" class="wbf-emoji-add-btn"
data-emoji="<?php echo esc_attr($e); ?>"
title="<?php echo esc_attr($e); ?> hinzufügen"
style="background:none;border:1px solid transparent;border-radius:6px;padding:4px 6px;font-size:1.3rem;cursor:pointer;transition:all .12s">
<?php echo esc_html($e); ?>
</button>
<?php endforeach; ?>
</div>
<?php $first = false; endforeach; ?>
</div>
</div>
</div>
<!-- ── Vorschau wie im Forum ───────────────────────────────── -->
<div style="margin-top:20px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:16px 20px">
<h3 style="margin:0 0 12px;font-size:.9rem;color:#374151">🔍 Vorschau (wie im Forum)</h3>
<div id="wbf-forum-preview" style="display:flex;gap:6px;flex-wrap:wrap">
<?php foreach ($current as $emoji): ?>
<button style="background:rgba(0,0,0,.06);border:1.5px solid rgba(0,0,0,.1);border-radius:20px;padding:4px 12px;font-size:.9rem;cursor:default">
<?php echo esc_html($emoji); ?> <span style="color:#888;font-size:.8em">0</span>
</button>
<?php endforeach; ?>
</div>
</div>
</div>
<script>
(function($) {
// Kategorie-Tabs
$(document).on('click', '.wbf-eg-tab', function() {
var g = $(this).data('group');
$('.wbf-eg-tab').removeClass('button-primary');
$(this).addClass('button-primary');
$('.wbf-eg-panel').hide();
$('.wbf-eg-panel[data-group="' + g + '"]').show();
});
// Emoji zu Liste hinzufügen
$(document).on('click', '.wbf-emoji-add-btn', function() {
var emoji = $(this).data('emoji');
var $list = $('#wbfReactionsList');
var parts = $list.val().trim().split(/\s+/).filter(Boolean);
if (parts.length >= 20) { alert('Maximal 20 Reaktionen erlaubt.'); return; }
if (parts.indexOf(emoji) !== -1) { alert('Diese Reaktion ist bereits in der Liste.'); return; }
parts.push(emoji);
$list.val(parts.join(' '));
updatePreview(parts);
});
// Chip aus Vorschau entfernen
$(document).on('click', '.wbf-r-chip', function() {
var emoji = $(this).data('emoji');
var $list = $('#wbfReactionsList');
var parts = $list.val().trim().split(/\s+/).filter(function(e){ return e !== emoji; });
$list.val(parts.join(' '));
updatePreview(parts);
});
// Textarea → Vorschau synchronisieren
$('#wbfReactionsList').on('input', function() {
var parts = $(this).val().trim().split(/\s+/).filter(Boolean);
updatePreview(parts);
});
function updatePreview(parts) {
var chips = parts.map(function(e) {
return '<span class="wbf-r-chip" data-emoji="' + e + '" '
+ 'style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:#fff;border:1.5px solid #e5e7eb;border-radius:20px;font-size:1.3rem;cursor:pointer;transition:all .15s" '
+ 'title="Klicken zum Entfernen">' + e
+ '<span style="font-size:.7rem;color:#9ca3af;line-height:1">✕</span></span>';
}).join('');
$('#wbf-reaction-preview').html(chips || '<span style="color:#9ca3af;font-size:.82rem">Keine Reaktionen</span>');
var pills = parts.map(function(e) {
return '<button style="background:rgba(0,0,0,.06);border:1.5px solid rgba(0,0,0,.1);border-radius:20px;padding:4px 12px;font-size:.9rem;cursor:default">' + e + ' <span style="color:#888;font-size:.8em">0</span></button>';
}).join('');
$('#wbf-forum-preview').html(pills || '<span style="color:#9ca3af;font-size:.82rem">Keine Reaktionen</span>');
}
// Hover-Effekt für Emoji-Buttons
$(document).on('mouseenter', '.wbf-emoji-add-btn', function() {
$(this).css({background:'#f3f4f6', borderColor:'#d1d5db'});
}).on('mouseleave', '.wbf-emoji-add-btn', function() {
$(this).css({background:'none', borderColor:'transparent'});
});
}(jQuery));
</script>
<?php
}
// ── Einladungen ────────────────────────────────────────────────────────────────
function wbf_admin_invites() {
if ( ! current_user_can('manage_options') ) return;
global $wpdb;
// Einladung erstellen
if ( isset($_POST['wbf_create_inv']) && check_admin_referer('wbf_invite_nonce') ) {
$max_uses = max(1, min(100, (int)($_POST['max_uses'] ?? 1)));
$note = sanitize_text_field($_POST['note'] ?? '');
$expires = sanitize_text_field($_POST['expires_at'] ?? '');
$expires_at = null;
if ($expires) {
$ts = strtotime($expires);
if ($ts > time()) $expires_at = date('Y-m-d H:i:s', $ts);
}
// Als Superadmin-Forum-User erstellen
$wp_user = wp_get_current_user();
$forum_user = WBF_DB::get_user_by('email', $wp_user->user_email);
$creator_id = $forum_user ? $forum_user->id : 0;
$code = WBF_DB::create_invite($creator_id, $max_uses, $note, $expires_at);
echo '<div class="notice notice-success is-dismissible"><p>✅ Einladungscode <strong>' . esc_html($code) . '</strong> erstellt!</p></div>';
}
// Einladung löschen
if ( isset($_GET['del_invite'], $_GET['_wpnonce']) && wp_verify_nonce($_GET['_wpnonce'], 'del_invite_' . (int)$_GET['del_invite']) ) {
WBF_DB::delete_invite((int)$_GET['del_invite']);
echo '<div class="notice notice-success is-dismissible"><p>Einladung gelöscht.</p></div>';
}
$invites = WBF_DB::get_all_invites(200);
$forum_url = wbf_get_forum_url();
$reg_mode = wbf_get_settings()['registration_mode'] ?? 'open';
?>
<div class="wrap" style="max-width:960px">
<h1 style="display:flex;align-items:center;gap:10px">
<span style="font-size:1.4rem">📨</span> Einladungen
</h1>
<?php if ($reg_mode !== 'invite'): ?>
<div class="notice notice-warning" style="margin-bottom:1rem">
<p>
<strong>Hinweis:</strong> Der Registrierungsmodus ist aktuell auf
<strong><?php echo $reg_mode === 'open' ? 'Offen' : 'Gesperrt'; ?></strong> gestellt —
Einladungscodes werden erst überprüft wenn der Modus auf <em>Nur Einladung</em> steht.
<a href="<?php echo admin_url('admin.php?page=wbf-settings'); ?>">→ Jetzt ändern</a>
</p>
</div>
<?php endif; ?>
<div style="display:grid;grid-template-columns:340px 1fr;gap:20px;align-items:start">
<!-- ── Neue Einladung ──────────────────────────── -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden">
<div style="padding:12px 16px;background:#f0fdf4;border-bottom:1px solid #dcfce7">
<strong style="color:#15803d;font-size:.85rem;text-transform:uppercase;letter-spacing:.04em">
+ Neue Einladung erstellen
</strong>
</div>
<form method="post" style="padding:16px">
<?php wp_nonce_field('wbf_invite_nonce'); ?>
<table class="form-table" style="margin:0">
<tr>
<th style="padding:6px 10px 6px 0;width:130px;font-size:.82rem">Max. Nutzungen</th>
<td style="padding:4px 0">
<select name="max_uses" style="height:28px;font-size:.82rem">
<?php foreach ([1,3,5,10,25,50,100] as $n): ?>
<option value="<?php echo $n; ?>"><?php echo $n; ?>x</option>
<?php endforeach; ?>
</select>
<span style="font-size:.75rem;color:#9ca3af;margin-left:4px">Wie oft kann der Code verwendet werden</span>
</td>
</tr>
<tr>
<th style="padding:6px 10px 6px 0;font-size:.82rem">Läuft ab am</th>
<td style="padding:4px 0">
<input type="datetime-local" name="expires_at"
min="<?php echo date('Y-m-d\TH:i'); ?>"
style="height:28px;font-size:.82rem">
<span style="font-size:.75rem;color:#9ca3af;display:block;margin-top:2px">Leer lassen = kein Ablauf</span>
</td>
</tr>
<tr>
<th style="padding:6px 10px 6px 0;font-size:.82rem">Notiz</th>
<td style="padding:4px 0">
<input type="text" name="note" placeholder="z.B. Für M_Viper — Discord"
style="width:100%;height:28px;font-size:.82rem">
</td>
</tr>
</table>
<button type="submit" name="wbf_create_inv" class="button button-primary" style="margin-top:10px;width:100%">
Einladungslink generieren
</button>
</form>
</div>
<!-- ── Bestehende Einladungen ──────────────────── -->
<div>
<?php if (empty($invites)): ?>
<div style="padding:2rem;text-align:center;color:#9ca3af;background:#fff;border:1px solid #e5e7eb;border-radius:10px">
<p style="font-size:1.5rem;margin:0 0 .5rem">📭</p>
<p style="margin:0">Noch keine Einladungen erstellt.</p>
</div>
<?php else: ?>
<table class="widefat striped" style="border-radius:8px;overflow:hidden">
<thead>
<tr>
<th>Code / Link</th>
<th>Notiz</th>
<th>Nutzungen</th>
<th>Ablauf</th>
<th>Erstellt von</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($invites as $inv):
$inv_url = $forum_url . '?wbf_invite=' . urlencode($inv->code);
$expired = $inv->expires_at && strtotime($inv->expires_at) < time();
$used_up = $inv->use_count >= $inv->max_uses;
$status_ok = !$expired && !$used_up;
$del_url = wp_nonce_url(
admin_url('admin.php?page=wbf-invites&del_invite=' . $inv->id),
'del_invite_' . $inv->id
);
?>
<tr style="<?php echo ($expired || $used_up) ? 'opacity:.5' : ''; ?>">
<td>
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
<code style="font-size:.9rem;font-weight:700;letter-spacing:.1em;background:<?php echo $status_ok ? '#f0fdf4' : '#f9fafb'; ?>;padding:2px 8px;border-radius:4px;border:1px solid <?php echo $status_ok ? '#86efac' : '#e5e7eb'; ?>">
<?php echo esc_html($inv->code); ?>
</code>
<?php if ($status_ok): ?>
<button type="button" onclick="navigator.clipboard.writeText('<?php echo esc_js($inv_url); ?>').then(function(){this.textContent='✓ Kopiert!';setTimeout(function(){document.querySelectorAll('.wbf-copy-btn').forEach(function(b){b.textContent='Link kopieren';});},2000);}.bind(this))" class="button button-small wbf-copy-btn">
Link kopieren
</button>
<?php endif; ?>
<?php if ($expired): ?>
<span style="font-size:.72rem;background:#fef2f2;color:#dc2626;padding:1px 6px;border-radius:4px">Abgelaufen</span>
<?php elseif ($used_up): ?>
<span style="font-size:.72rem;background:#f3f4f6;color:#6b7280;padding:1px 6px;border-radius:4px">Aufgebraucht</span>
<?php else: ?>
<span style="font-size:.72rem;background:#f0fdf4;color:#16a34a;padding:1px 6px;border-radius:4px">Aktiv</span>
<?php endif; ?>
</div>
</td>
<td style="font-size:.82rem;color:#6b7280"><?php echo esc_html($inv->note ?: '—'); ?></td>
<td style="text-align:center">
<span style="font-size:.9rem;font-weight:700"><?php echo (int)$inv->use_count; ?></span>
<span style="color:#9ca3af"> / <?php echo (int)$inv->max_uses; ?></span>
<?php if ($inv->used_name): ?>
<br><span style="font-size:.72rem;color:#9ca3af"><?php echo esc_html($inv->used_name); ?></span>
<?php endif; ?>
</td>
<td style="font-size:.78rem;color:#6b7280">
<?php echo $inv->expires_at ? date('d.m.Y H:i', strtotime($inv->expires_at)) : '∞ Nie'; ?>
</td>
<td style="font-size:.82rem"><?php echo esc_html($inv->creator_name ?? '—'); ?></td>
<td>
<a href="<?php echo esc_url($del_url); ?>"
onclick="return confirm('Einladung löschen?')"
class="button button-small"
style="color:#dc2626;border-color:#fca5a5">
✕ Löschen
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<!-- ── Wie Einladungen funktionieren ─────────────── -->
<div style="margin-top:20px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:14px 18px">
<strong style="font-size:.85rem"> So funktionieren Einladungen</strong>
<ol style="margin:.6rem 0 0;padding-left:1.4rem;font-size:.82rem;color:#6b7280;line-height:1.8">
<li>Setze den Registrierungsmodus in <a href="<?php echo admin_url('admin.php?page=wbf-settings'); ?>">Einstellungen</a> auf <strong>Nur Einladung</strong></li>
<li>Erstelle hier einen Einladungslink — wähle wie oft er genutzt werden kann und ob er abläuft</li>
<li>Teile den Link mit deinen gewünschten Nutzern (z.B. per Discord, E-Mail, etc.)</li>
<li>Besucher die den Link öffnen, sehen das Registrierungsformular mit vorausgefülltem Code</li>
<li>Nach erfolgreicher Registrierung wird der Code als verwendet markiert</li>
</ol>
</div>
</div>
<?php
}
// ── Statistiken ────────────────────────────────────────────────────────────────
function wbf_admin_stats() {
if (!current_user_can('manage_options')) return;
$days = (int)($_GET['days'] ?? 30);
if (!in_array($days,[7,14,30,90])) $days = 30;
$stats = WBF_DB::get_activity_stats($days);
// Build chart data
$post_data = wp_json_encode(array_map(fn($r)=>['x'=>$r->day,'y'=>(int)$r->count], $stats['posts_per_day']));
$thread_data = wp_json_encode(array_map(fn($r)=>['x'=>$r->day,'y'=>(int)$r->count], $stats['threads_per_day']));
$reg_data = wp_json_encode(array_map(fn($r)=>['x'=>$r->day,'y'=>(int)$r->count], $stats['registrations']));
$hour_labels = wp_json_encode(array_map(fn($r)=>$r->hour.':00', $stats['active_hours']));
$hour_data = wp_json_encode(array_map(fn($r)=>(int)$r->count, $stats['active_hours']));
?>
<div class="wrap" style="max-width:1100px">
<h1 style="display:flex;align-items:center;gap:10px">📊 Forum-Statistiken</h1>
<div style="margin-bottom:1rem">
<?php foreach([7=>'7 Tage',14=>'14 Tage',30=>'30 Tage',90=>'90 Tage'] as $d=>$l): ?>
<a href="?page=wbf-stats&days=<?php echo $d; ?>"
class="button<?php echo $d===$days?' button-primary':''; ?>" style="margin-right:4px">
<?php echo $l; ?>
</a>
<?php endforeach; ?>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px">
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:16px">
<h3 style="margin:0 0 12px;font-size:.9rem">Beiträge & Threads pro Tag</h3>
<canvas id="wbfActivityChart" height="200"></canvas>
</div>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:16px">
<h3 style="margin:0 0 12px;font-size:.9rem">Aktivste Stunden</h3>
<canvas id="wbfHoursChart" height="200"></canvas>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:16px">
<h3 style="margin:0 0 12px;font-size:.9rem">Neue Registrierungen</h3>
<canvas id="wbfRegChart" height="180"></canvas>
</div>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:16px">
<h3 style="margin:0 0 12px;font-size:.9rem">Top Poster (letzte <?php echo $days; ?> Tage)</h3>
<table style="width:100%;font-size:.82rem;border-collapse:collapse">
<thead><tr style="border-bottom:2px solid #e5e7eb">
<th style="text-align:left;padding:4px 0">#</th>
<th style="text-align:left;padding:4px 0">Nutzer</th>
<th style="text-align:right;padding:4px 0">Beiträge</th>
</tr></thead>
<tbody>
<?php foreach ($stats['top_posters'] as $i=>$p): ?>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:5px 0;color:#9ca3af"><?php echo $i+1; ?></td>
<td style="padding:5px 0">
<strong><?php echo esc_html($p->display_name); ?></strong>
<?php echo WBF_Roles::badge($p->role); ?>
</td>
<td style="text-align:right;padding:5px 0;font-weight:700"><?php echo (int)$p->post_count; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script>
(function() {
var postData = <?php echo $post_data; ?>;
var threadData = <?php echo $thread_data; ?>;
var regData = <?php echo $reg_data; ?>;
var hourLabels = <?php echo $hour_labels; ?>;
var hourData = <?php echo $hour_data; ?>;
new Chart(document.getElementById('wbfActivityChart'), {
type:'line',
data:{ datasets:[
{label:'Beiträge',data:postData,borderColor:'#0ea5e9',backgroundColor:'rgba(14,165,233,.1)',fill:true,tension:.3},
{label:'Threads', data:threadData,borderColor:'#22c55e',backgroundColor:'rgba(34,197,94,.1)',fill:true,tension:.3}
]},
options:{scales:{x:{type:'time',time:{unit:'day'},adapters:{date:{locale:'de'}}}},plugins:{legend:{position:'top'}},responsive:true}
});
new Chart(document.getElementById('wbfHoursChart'), {
type:'bar',
data:{labels:hourLabels,datasets:[{label:'Posts',data:hourData,backgroundColor:'rgba(14,165,233,.6)'}]},
options:{responsive:true,plugins:{legend:{display:false}}}
});
new Chart(document.getElementById('wbfRegChart'), {
type:'bar',
data:{datasets:[{label:'Registrierungen',data:regData,backgroundColor:'rgba(168,85,247,.6)'}]},
options:{scales:{x:{type:'time',time:{unit:'day'}}},responsive:true,plugins:{legend:{display:false}}}
});
})();
</script>
<?php
}
// ── Papierkorb ─────────────────────────────────────────────────────────────────
function wbf_admin_trash() {
if (!current_user_can('manage_options')) return;
if (isset($_POST['wbf_restore']) && check_admin_referer('wbf_trash_nonce')) {
$type = sanitize_key($_POST['content_type']);
$id = (int)$_POST['content_id'];
if ($type==='thread') WBF_DB::restore_thread($id);
else WBF_DB::restore_post($id);
echo '<div class="notice notice-success is-dismissible"><p>✅ Wiederhergestellt.</p></div>';
}
if (isset($_POST['wbf_perm_delete']) && check_admin_referer('wbf_trash_nonce')) {
global $wpdb;
$type = sanitize_key($_POST['content_type']);
$id = (int)$_POST['content_id'];
if ($type==='thread') $wpdb->delete("{$wpdb->prefix}forum_threads", ['id'=>$id]);
else $wpdb->delete("{$wpdb->prefix}forum_posts", ['id'=>$id]);
echo '<div class="notice notice-success is-dismissible"><p>✅ Endgültig gelöscht.</p></div>';
}
$items = WBF_DB::get_deleted_content(100);
?>
<div class="wrap">
<h1>🗑️ Papierkorb</h1>
<p style="color:#666">Gelöschte Inhalte können hier wiederhergestellt oder endgültig entfernt werden.</p>
<?php if (empty($items)): ?>
<div style="text-align:center;padding:3rem;color:#9ca3af">
<p style="font-size:2rem;margin:0">✓</p>
<p>Papierkorb ist leer.</p>
</div>
<?php else: ?>
<table class="widefat striped">
<thead><tr>
<th>Typ</th><th>Inhalt</th><th>Kategorie / Thread</th><th>Autor</th><th>Gelöscht am</th><th>Aktionen</th>
</tr></thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo $item->type==='thread' ? '<span style="color:#0ea5e9">🧵 Thread</span>' : '<span style="color:#9ca3af">💬 Post</span>'; ?></td>
<td style="max-width:300px;word-break:break-word;font-size:.82rem"><?php echo esc_html($item->content_preview); ?></td>
<td style="font-size:.78rem;color:#6b7280"><?php echo esc_html($item->cat_name); ?></td>
<td style="font-size:.82rem"><?php echo esc_html($item->display_name); ?></td>
<td style="font-size:.78rem;color:#9ca3af"><?php echo date('d.m.Y H:i', strtotime($item->deleted_at)); ?></td>
<td style="white-space:nowrap">
<form method="post" style="display:inline">
<?php wp_nonce_field('wbf_trash_nonce'); ?>
<input type="hidden" name="content_type" value="<?php echo esc_attr($item->type); ?>">
<input type="hidden" name="content_id" value="<?php echo (int)$item->id; ?>">
<button type="submit" name="wbf_restore" class="button button-small button-primary">↺ Wiederherstellen</button>
</form>
<form method="post" style="display:inline;margin-left:4px">
<?php wp_nonce_field('wbf_trash_nonce'); ?>
<input type="hidden" name="content_type" value="<?php echo esc_attr($item->type); ?>">
<input type="hidden" name="content_id" value="<?php echo (int)$item->id; ?>">
<button type="submit" name="wbf_perm_delete" class="button button-small"
style="color:#dc2626;border-color:#fca5a5"
onclick="return confirm('Endgültig löschen? Nicht rückgängig!')">✕ Löschen</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
// ── Export / Import ───────────────────────────────────────────────────────────
function wbf_admin_export() {
if ( ! current_user_can('manage_options') ) return;
$notice = '';
// Export wird von admin_init verarbeitet (vor HTTP-Header-Output)
// ── IMPORT ────────────────────────────────────────────────────────────────
if ( isset( $_POST['wbf_do_import'] ) && check_admin_referer('wbf_import_nonce') ) {
if ( empty( $_FILES['import_file']['tmp_name'] ) ) {
$notice = ['error', 'Keine Datei hochgeladen.'];
} else {
$raw = file_get_contents( $_FILES['import_file']['tmp_name'] );
$data = json_decode( $raw, true );
if ( ! $data || ! isset($data['_meta']) ) {
$notice = ['error', 'Ungültige Datei — kein gültiges WBF-Backup.'];
} else {
global $wpdb;
$imported = [];
// ── Einstellungen & Profilfeld-Definitionen ───────────────
if ( ! empty($data['settings']) ) {
update_option('wbf_settings', $data['settings']);
$imported[] = 'Einstellungen';
}
if ( isset($data['profile_fields']) ) {
update_option('wbf_profile_fields', $data['profile_fields']);
$imported[] = 'Profilfelder-Definitionen (' . count($data['profile_fields']) . ')';
}
// Reaktionen-Konfiguration
if ( isset($data['reactions_cfg']) && is_array($data['reactions_cfg']) ) {
update_option('wbf_reactions', $data['reactions_cfg']);
$imported[] = 'Reaktionen-Konfiguration';
}
// ── Rollen ────────────────────────────────────────────────
if ( ! empty($data['roles']) ) {
$roles = $data['roles'];
unset($roles['superadmin']); // Superadmin nie überschreiben
$current = get_option('wbf_custom_roles', []);
$current['superadmin'] = WBF_Roles::get('superadmin');
update_option('wbf_custom_roles', array_merge($current, $roles));
$imported[] = 'Rollen';
}
// ── Level-System ──────────────────────────────────────────
if ( ! empty($data['levels']) ) {
if ( isset($data['levels']['config']) ) update_option('wbf_level_config', $data['levels']['config']);
if ( isset($data['levels']['enabled']) ) update_option('wbf_levels_enabled', $data['levels']['enabled']);
$imported[] = 'Level';
}
// ── Kategorien ────────────────────────────────────────────
if ( ! empty($data['categories']) ) {
$force = ! empty($_POST['import_force_cats']);
$existing = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}forum_categories");
if ( $existing === 0 || $force ) {
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_categories");
foreach ($data['categories'] as $cat) {
// BUGFIX: Original-ID behalten! Threads zeigen direkt auf diese IDs.
$wpdb->query( $wpdb->prepare(
"INSERT INTO {$wpdb->prefix}forum_categories
(id, parent_id, name, slug, description, icon, sort_order, thread_count, post_count, min_role, guest_visible)
VALUES (%d,%d,%s,%s,%s,%s,%d,%d,%d,%s,%d)
ON DUPLICATE KEY UPDATE parent_id=%d, name=%s, slug=%s, description=%s, icon=%s, sort_order=%d, thread_count=%d, post_count=%d, min_role=%s, guest_visible=%d",
(int)($cat['id']), (int)($cat['parent_id'] ?? 0), $cat['name'] ?? '', $cat['slug'] ?? '',
$cat['description'] ?? '', $cat['icon'] ?? 'fas fa-comments',
(int)($cat['sort_order'] ?? 0), (int)($cat['thread_count'] ?? 0),
(int)($cat['post_count'] ?? 0), $cat['min_role'] ?? 'member', (int)($cat['guest_visible'] ?? 1),
(int)($cat['parent_id'] ?? 0), $cat['name'] ?? '', $cat['slug'] ?? '',
$cat['description'] ?? '', $cat['icon'] ?? 'fas fa-comments',
(int)($cat['sort_order'] ?? 0), (int)($cat['thread_count'] ?? 0),
(int)($cat['post_count'] ?? 0), $cat['min_role'] ?? 'member', (int)($cat['guest_visible'] ?? 1)
) );
}
$imported[] = 'Kategorien (' . count($data['categories']) . ')';
} else {
$imported[] = 'Kategorien übersprungen (bereits vorhanden — „Überschreiben" aktivieren)';
}
}
// ── Benutzer + User-Meta ──────────────────────────────────
if ( ! empty($data['users']) ) {
$force = ! empty($_POST['import_force_users']);
$id_map = []; // old_id => new_id für User-Meta
$count = 0;
foreach ($data['users'] as $u) {
$old_id = (int)$u['id'];
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}forum_users WHERE username=%s OR email=%s",
$u['username'], $u['email']
));
if ($exists && !$force) {
$id_map[$old_id] = (int)$exists;
continue;
}
// Superadmin-Schutz
$u['role'] = ($u['role'] === 'superadmin') ? 'member' : $u['role'];
if ($exists && $force) {
$uid = (int)$exists;
unset($u['id']);
$wpdb->update("{$wpdb->prefix}forum_users", $u, ['id' => $uid]);
} else {
unset($u['id']);
$wpdb->insert("{$wpdb->prefix}forum_users", $u);
$uid = (int)$wpdb->insert_id;
}
$id_map[$old_id] = $uid;
$count++;
}
$imported[] = "Benutzer ($count importiert)";
// User-Meta (Profilfeld-Werte) mit gemappten IDs importieren
if ( ! empty($data['user_meta']) ) {
$force_meta = $force; // gleiche Option wie Benutzer
if ($force_meta) $wpdb->query("DELETE FROM {$wpdb->prefix}forum_user_meta WHERE user_id IN (" . implode(',', array_values($id_map) ?: [0]) . ")");
$meta_count = 0;
foreach ($data['user_meta'] as $row) {
$new_uid = $id_map[(int)$row['user_id']] ?? null;
if (!$new_uid) continue;
unset($row['id']);
$row['user_id'] = $new_uid;
$wpdb->replace("{$wpdb->prefix}forum_user_meta", $row);
$meta_count++;
}
if ($meta_count) $imported[] = "Profilfeld-Werte ($meta_count)";
}
}
// ── Threads, Posts, Tags, Abonnements ────────────────────
if ( ! empty($data['threads']) ) {
$force = ! empty($_POST['import_force_threads']);
if ($force) {
// Alle abhängigen Tabellen mitbereinigen (verhindert verwaiste Einträge)
$wpdb->query("SET FOREIGN_KEY_CHECKS=0");
foreach ([
'forum_posts', 'forum_threads', 'forum_thread_tags', 'forum_tags',
'forum_subscriptions', 'forum_polls', 'forum_poll_votes',
'forum_bookmarks', 'forum_prefixes', 'forum_likes', 'forum_reactions',
'forum_notifications', 'forum_ignore_list',
] as $_tbl) {
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}$_tbl");
}
$wpdb->query("SET FOREIGN_KEY_CHECKS=1");
}
foreach ($data['threads'] ?? [] as $t) { $wpdb->insert("{$wpdb->prefix}forum_threads", $t); }
foreach ($data['posts'] ?? [] as $p) { $wpdb->insert("{$wpdb->prefix}forum_posts", $p); }
// Tags re-importieren
if (!empty($data['thread_tags'])) {
$tag_map = [];
foreach ($data['thread_tags'] as $tt) {
if (!isset($tag_map[$tt['slug']])) {
$etag = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}forum_tags WHERE slug=%s", $tt['slug']
));
if (!$etag) {
$wpdb->insert("{$wpdb->prefix}forum_tags", ['name'=>$tt['name'],'slug'=>$tt['slug'],'use_count'=>$tt['use_count']]);
$tag_map[$tt['slug']] = $wpdb->insert_id;
} else {
$tag_map[$tt['slug']] = $etag;
}
}
$wpdb->replace("{$wpdb->prefix}forum_thread_tags", ['thread_id'=>$tt['thread_id'],'tag_id'=>$tag_map[$tt['slug']]]);
}
}
// Abonnements
if (!empty($data['subscriptions'])) {
$sc = 0;
foreach ($data['subscriptions'] as $row) {
unset($row['id']);
$wpdb->replace("{$wpdb->prefix}forum_subscriptions", $row);
$sc++;
}
if ($sc) $imported[] = "Abonnements ($sc)";
}
$imported[] = 'Threads + Posts (' . count($data['threads']) . ' / ' . count($data['posts'] ?? []) . ')';
}
// ── Thread-Präfixe ────────────────────────────────────────
// ── Thread-Präfixe ────────────────────────────────────────
if ( ! empty($data['prefixes']) ) {
$force = ! empty($_POST['import_force_prefixes']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_prefixes");
$pc = 0;
foreach ($data['prefixes'] as $row) {
$wpdb->query( $wpdb->prepare(
"INSERT INTO {$wpdb->prefix}forum_prefixes (id, label, color, bg_color, sort_order)
VALUES (%d,%s,%s,%s,%d)
ON DUPLICATE KEY UPDATE label=%s, color=%s, bg_color=%s, sort_order=%d",
(int)$row['id'], $row['label'], $row['color'] ?? '#ffffff',
$row['bg_color'] ?? '#475569', (int)($row['sort_order'] ?? 0),
$row['label'], $row['color'] ?? '#ffffff',
$row['bg_color'] ?? '#475569', (int)($row['sort_order'] ?? 0)
) );
$pc++;
}
if ($pc) $imported[] = "Thread-Präfixe ($pc)";
}
// ── Umfragen + Abstimmungen ───────────────────────────────
if ( ! empty($data['polls']) ) {
$force = ! empty($_POST['import_force_polls']);
if ($force) {
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_poll_votes");
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_polls");
}
$pc = 0;
foreach ($data['polls'] as $row) {
$wpdb->query( $wpdb->prepare(
"INSERT INTO {$wpdb->prefix}forum_polls (id, thread_id, question, options, multi, ends_at, created_at)
VALUES (%d,%d,%s,%s,%d,%s,%s)
ON DUPLICATE KEY UPDATE thread_id=%d, question=%s, options=%s, multi=%d, ends_at=%s",
(int)$row['id'], (int)$row['thread_id'], $row['question'] ?? '',
$row['options'] ?? '[]', (int)($row['multi'] ?? 0),
$row['ends_at'] ?? null, $row['created_at'] ?? current_time('mysql'),
(int)$row['thread_id'], $row['question'] ?? '',
$row['options'] ?? '[]', (int)($row['multi'] ?? 0), $row['ends_at'] ?? null
) );
$pc++;
}
foreach ($data['poll_votes'] ?? [] as $row) { unset($row['id']); $wpdb->replace("{$wpdb->prefix}forum_poll_votes", $row); }
$imported[] = "Umfragen ($pc)";
}
// ── Lesezeichen ───────────────────────────────────────────
if ( ! empty($data['bookmarks']) ) {
$force = ! empty($_POST['import_force_bookmarks']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_bookmarks");
$bc = 0;
foreach ($data['bookmarks'] as $row) { unset($row['id']); $wpdb->replace("{$wpdb->prefix}forum_bookmarks", $row); $bc++; }
if ($bc) $imported[] = "Lesezeichen ($bc)";
}
// ── Likes, Reaktionen, Benachrichtigungen ─────────────────
if ( ! empty($data['likes']) ) {
$force = ! empty($_POST['import_force_threads']);
if ($force) $wpdb->query("DELETE FROM {$wpdb->prefix}forum_likes");
$count = 0;
foreach ($data['likes'] as $row) {
unset($row['id']);
if (!$wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}forum_likes WHERE user_id=%d AND object_id=%d AND object_type=%s",
$row['user_id'], $row['object_id'], $row['object_type']
))) { $wpdb->insert("{$wpdb->prefix}forum_likes", $row); $count++; }
}
$imported[] = "Likes ($count)";
}
if ( ! empty($data['reactions']) ) {
$force = ! empty($_POST['import_force_threads']);
if ($force) $wpdb->query("DELETE FROM {$wpdb->prefix}forum_reactions");
foreach ($data['reactions'] as $row) { unset($row['id']); $wpdb->replace("{$wpdb->prefix}forum_reactions", $row); }
$imported[] = 'Reaktionen (' . count($data['reactions']) . ')';
}
if ( ! empty($data['notifications']) ) {
$force = ! empty($_POST['import_force_threads']);
if ($force) $wpdb->query("DELETE FROM {$wpdb->prefix}forum_notifications");
$count = 0;
foreach ($data['notifications'] as $row) { unset($row['id']); $wpdb->insert("{$wpdb->prefix}forum_notifications", $row); $count++; }
$imported[] = "Benachrichtigungen ($count)";
}
// ── Privatnachrichten ─────────────────────────────────────
if ( ! empty($data['messages']) ) {
$force = ! empty($_POST['import_force_messages']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_messages");
$count = 0;
foreach ($data['messages'] as $row) { unset($row['id']); $wpdb->insert("{$wpdb->prefix}forum_messages", $row); $count++; }
$imported[] = "Nachrichten ($count)";
}
// ── Meldungen ─────────────────────────────────────────────
if ( ! empty($data['reports']) ) {
$force = ! empty($_POST['import_force_reports']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_reports");
$count = 0;
foreach ($data['reports'] as $row) { unset($row['id']); $wpdb->insert("{$wpdb->prefix}forum_reports", $row); $count++; }
$imported[] = "Meldungen ($count)";
}
// ── Einladungen ───────────────────────────────────────────
if ( ! empty($data['invites']) ) {
$force = ! empty($_POST['import_force_invites']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_invites");
$count = 0;
foreach ($data['invites'] as $row) {
unset($row['id']);
// Duplicate code skip
if ($wpdb->get_var($wpdb->prepare("SELECT id FROM {$wpdb->prefix}forum_invites WHERE code=%s", $row['code']))) continue;
$wpdb->insert("{$wpdb->prefix}forum_invites", $row);
$count++;
}
if ($count) $imported[] = "Einladungen ($count)";
}
// ── Ignore-Liste ──────────────────────────────────────────
if ( ! empty($data['ignore_list']) ) {
$force = ! empty($_POST['import_force_ignore']);
if ($force) $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}forum_ignore_list");
$count = 0;
foreach ($data['ignore_list'] as $row) {
unset($row['id']);
$wpdb->replace("{$wpdb->prefix}forum_ignore_list", $row);
$count++;
}
if ($count) $imported[] = "Ignore-Einträge ($count)";
}
if ( empty($imported) ) {
$notice = ['warning', 'Nichts importiert — Datei enthielt keine gültigen Abschnitte.'];
} else {
$notice = ['success', '✅ Importiert: ' . implode(', ', $imported) . '. Erstellt: ' . esc_html($data['_meta']['exported']) . ' · Site: ' . esc_html($data['_meta']['site'] ?? '?')];
}
}
}
}
// ── UI ─────────────────────────────────────────────────────────────────────
?>
<div class="wrap" style="max-width:860px">
<h1 style="display:flex;align-items:center;gap:10px">
<span class="dashicons dashicons-database-import" style="font-size:1.5rem;width:auto;color:#00b4d8"></span>
Export / Import
</h1>
<?php if ($notice): ?>
<div class="notice notice-<?php echo $notice[0]; ?> is-dismissible"><p><?php echo esc_html($notice[1]); ?></p></div>
<?php endif; ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px">
<!-- ── EXPORT ─────────────────────────────────────────── -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06)">
<div style="padding:14px 18px;background:#f0fdf4;border-bottom:1px solid #dcfce7;display:flex;align-items:center;gap:8px">
<span class="dashicons dashicons-download" style="color:#16a34a;font-size:1.1rem;width:auto"></span>
<strong style="color:#15803d;font-size:.9rem;text-transform:uppercase;letter-spacing:.04em">Export</strong>
</div>
<div style="padding:18px">
<p style="color:#6b7280;font-size:.85rem;margin:0 0 14px">Wähle was exportiert werden soll. Die Datei wird als <code>.json</code> heruntergeladen.</p>
<form method="post" action="<?php echo esc_url( admin_url('admin.php?page=wbf-export') ); ?>">
<?php wp_nonce_field('wbf_export_nonce'); ?>
<table style="width:100%;border-collapse:collapse">
<?php
$export_options = [
'settings' => ['⚙️', 'Einstellungen & Profilfelder', 'Forum-Texte, Regeln, Labels + Felddefinitionen'],
'roles' => ['🛡️', 'Rollen', 'Alle Rollen inkl. Berechtigungen & Farben'],
'levels' => ['⭐', 'Level-System', 'Level-Konfiguration & Status'],
'categories' => ['📂', 'Kategorien', 'Alle Kategorien inkl. Hierarchie'],
'users' => ['👥', 'Benutzer & Profilfelder', 'Accounts, Ban-Status, Profilfeld-Werte'],
'threads' => ['💬', 'Threads, Posts & Abos', 'Alle Inhalte, Tags & Abonnements'],
'polls' => ['📊', 'Umfragen', 'Alle Umfragen inkl. Abstimmungen'],
'bookmarks' => ['🔖', 'Lesezeichen', 'Alle gespeicherten Thread-Lesezeichen'],
'prefixes' => ['🏷️', 'Thread-Präfixe', 'Alle konfigurierten Präfix-Labels & Farben'],
'interactions' => ['❤️', 'Likes & Reaktionen', 'Likes, Reaktionen, Benachrichtigungen'],
'messages' => ['✉️', 'Privatnachrichten', 'Alle DM-Konversationen'],
'ignore_list' => ['🚫', 'Ignore-Liste', 'Alle Nutzer-Blockierungen'],
'reports' => ['🚩', 'Meldungen', 'Gemeldete Beiträge inkl. Status'],
'invites' => ['📨', 'Einladungen', 'Alle Einladungscodes inkl. Nutzungsstand'],
];
foreach ($export_options as $key => [$icon, $label, $desc]):
?>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;width:36px;text-align:center;font-size:1.1rem"><?php echo $icon; ?></td>
<td style="padding:10px 8px">
<label style="display:block;font-weight:600;font-size:.875rem;color:#111827;cursor:pointer">
<?php echo esc_html($label); ?>
</label>
<span style="font-size:.78rem;color:#9ca3af"><?php echo esc_html($desc); ?></span>
</td>
<td style="text-align:right;padding:10px 0">
<input type="checkbox" name="export_sections[<?php echo $key; ?>]" value="1" checked
style="width:16px;height:16px;accent-color:#16a34a;cursor:pointer">
</td>
</tr>
<?php endforeach; ?>
</table>
<div style="margin-top:14px;padding-top:12px;border-top:1px solid #f3f4f6;display:flex;align-items:center;gap:10px">
<button type="submit" name="wbf_do_export" class="button button-primary" style="background:#16a34a;border-color:#16a34a">
<span class="dashicons dashicons-download" style="margin-top:3px"></span>
Als JSON exportieren
</button>
<span style="font-size:.78rem;color:#9ca3af">Datei wird sofort heruntergeladen</span>
</div>
</form>
</div>
</div>
<!-- ── IMPORT ─────────────────────────────────────────── -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06)">
<div style="padding:14px 18px;background:#eff6ff;border-bottom:1px solid #dbeafe;display:flex;align-items:center;gap:8px">
<span class="dashicons dashicons-upload" style="color:#2563eb;font-size:1.1rem;width:auto"></span>
<strong style="color:#1d4ed8;font-size:.9rem;text-transform:uppercase;letter-spacing:.04em">Import</strong>
</div>
<div style="padding:18px">
<p style="color:#6b7280;font-size:.85rem;margin:0 0 14px">Lade eine zuvor exportierte <code>.json</code>-Datei hoch.</p>
<div style="background:#fff7ed;border:1px solid #fed7aa;border-radius:7px;padding:10px 12px;margin-bottom:14px;font-size:.82rem;color:#92400e">
<strong>⚠️ Hinweis:</strong> Benutzer-Import enthält Passwort-Hashes. Threads-Import kann bestehende Daten überschreiben. Mache vorher ein Backup.
</div>
<form method="post" enctype="multipart/form-data" action="<?php echo esc_url( admin_url('admin.php?page=wbf-export') ); ?>">
<?php wp_nonce_field('wbf_import_nonce'); ?>
<div style="margin-bottom:12px">
<label style="display:block;font-size:.82rem;font-weight:700;color:#374151;margin-bottom:5px;text-transform:uppercase;letter-spacing:.04em">
Backup-Datei (.json)
</label>
<input type="file" name="import_file" accept=".json,application/json"
style="width:100%;padding:6px;border:1.5px solid #d1d5db;border-radius:6px;font-size:.875rem">
</div>
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:7px;padding:12px;margin-bottom:12px">
<p style="font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin:0 0 8px">Überschreiben-Optionen</p>
<?php
$overwrite_opts = [
'import_force_cats' => 'Kategorien überschreiben (löscht bestehende)',
'import_force_users' => 'Benutzer aktualisieren (bei gleichem Username) inkl. Profilfeld-Werte',
'import_force_threads' => 'Threads, Posts, Likes, Reaktionen & Abonnements überschreiben',
'import_force_polls' => 'Umfragen & Abstimmungen überschreiben',
'import_force_bookmarks'=> 'Lesezeichen überschreiben',
'import_force_prefixes' => 'Thread-Präfixe überschreiben',
'import_force_ignore' => 'Ignore-Liste überschreiben',
'import_force_messages' => 'Privatnachrichten überschreiben',
'import_force_reports' => 'Meldungen überschreiben',
'import_force_invites' => 'Einladungen überschreiben',
];
foreach ($overwrite_opts as $name => $label): ?>
<label style="display:flex;align-items:center;gap:8px;font-size:.82rem;color:#374151;cursor:pointer;margin-bottom:5px">
<input type="checkbox" name="<?php echo $name; ?>" value="1"
style="width:15px;height:15px;accent-color:#dc2626;cursor:pointer">
<?php echo esc_html($label); ?>
</label>
<?php endforeach; ?>
</div>
<div style="display:flex;align-items:center;gap:10px">
<button type="submit" name="wbf_do_import" class="button button-primary"
onclick="return confirm('Import starten? Bestehende Daten können überschrieben werden.')"
style="background:#2563eb;border-color:#2563eb">
<span class="dashicons dashicons-upload" style="margin-top:3px"></span>
Importieren
</button>
<span style="font-size:.78rem;color:#9ca3af">Nur kompatible WBF-Backups</span>
</div>
</form>
</div>
</div>
</div>
<!-- ── Info-Box ────────────────────────────────────────────── -->
<div style="margin-top:20px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:16px 20px">
<h3 style="margin:0 0 10px;font-size:.9rem;display:flex;align-items:center;gap:7px">
<span class="dashicons dashicons-info" style="color:#00b4d8;width:auto"></span>
Export-Inhalte im Überblick
</h3>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px">
<?php
$info = [
['⚙️ Einstellungen', 'Forum-Texte, Labels, Regeln, Auto-Logout + Profilfeld-Definitionen'],
['🛡️ Rollen', 'Alle benutzerdefinierten Rollen mit Permissions & Design (Superadmin nie überschrieben)'],
['⭐ Level', 'Level-Bezeichnungen, Schwellenwerte, Icons, Farben, An/Aus-Status'],
['📂 Kategorien', 'Kategoriestruktur inkl. Eltern-Kind-Hierarchie, Icons, Min-Rolle'],
['👥 Benutzer', 'Accounts inkl. Passwort-Hashes, Ban-Status, Zeitsperre + Profilfeld-Werte'],
['💬 Threads & Posts', 'Alle Inhalte inkl. Tag-Zuordnungen & Thread-Abonnements'],
['❤️ Likes & Reaktionen', 'Likes auf Threads/Posts + Emoji-Reaktionen + Benachrichtigungen'],
['✉️ Privatnachrichten', 'Alle DM-Konversationen zwischen Nutzern'],
['🚩 Meldungen', 'Gemeldete Beiträge inkl. Status (offen/erledigt/verworfen)'],
['📨 Einladungen', 'Alle Einladungscodes inkl. Nutzungsanzahl & Ablaufdatum'],
];
foreach ($info as [$title, $desc]): ?>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:7px;padding:10px 12px">
<strong style="font-size:.82rem;display:block;margin-bottom:3px"><?php echo $title; ?></strong>
<span style="font-size:.76rem;color:#9ca3af;line-height:1.4"><?php echo esc_html($desc); ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php
}
// ── Benutzerdefinierte Profilfelder ───────────────────────────────────────────
function wbf_admin_profile_fields() {
// ── Speichern ────────────────────────────────────────────────────────────
if ( isset($_POST['wbf_save_profile_fields']) && check_admin_referer('wbf_profile_fields_nonce') ) {
$keys = $_POST['field_key'] ?? [];
$labels = $_POST['field_label'] ?? [];
$types = $_POST['field_type'] ?? [];
$placeholders = $_POST['field_placeholder'] ?? [];
$required = $_POST['field_required'] ?? [];
$public = $_POST['field_public'] ?? [];
$options = $_POST['field_options'] ?? [];
$fields = [];
foreach ( $keys as $i => $raw_key ) {
$key = sanitize_key( $raw_key );
if ( ! $key ) continue;
// Reservierte Keys sperren
if ( in_array($key, ['username','email','password','display_name','bio','signature','avatar_url','role']) ) continue;
$fields[] = [
'key' => $key,
'label' => sanitize_text_field( $labels[$i] ?? $key ),
'type' => in_array($types[$i] ?? '', ['text','url','textarea','select','number']) ? $types[$i] : 'text',
'placeholder' => sanitize_text_field( $placeholders[$i] ?? '' ),
'required' => ! empty( $required[$i] ) ? 1 : 0,
'public' => ! empty( $public[$i] ) ? 1 : 0,
'options' => sanitize_textarea_field( $options[$i] ?? '' ),
];
}
WBF_DB::save_profile_field_defs( $fields );
echo '<div class="notice notice-success is-dismissible"><p>✅ Profilfelder gespeichert!</p></div>';
}
$fields = WBF_DB::get_profile_field_defs();
$types = ['text'=>'Text','url'=>'URL/Link','textarea'=>'Mehrzeiliger Text','select'=>'Auswahlliste','number'=>'Zahl'];
?>
<div class="wrap">
<h1>👤 Benutzerdefinierte Profilfelder</h1>
<p style="color:#666;margin-bottom:1.5rem">Felder die Nutzer in ihrem Profil ausfüllen können — z.B. Website, Ort, Discord-Name, etc.</p>
<form method="post" id="wbfFieldsForm">
<?php wp_nonce_field('wbf_profile_fields_nonce'); ?>
<table class="widefat" style="margin-bottom:1rem">
<thead>
<tr>
<th style="width:140px">Schlüssel</th>
<th style="width:160px">Bezeichnung</th>
<th style="width:110px">Typ</th>
<th>Platzhalter</th>
<th style="width:110px">Optionen <small>(bei Auswahl)</small></th>
<th style="width:70px;text-align:center">Pflicht</th>
<th style="width:70px;text-align:center">Öffentlich</th>
<th style="width:50px"></th>
</tr>
</thead>
<tbody id="wbfFieldRows">
<?php
$rows = $fields;
// Mindestens eine leere Zeile wenn noch keine Felder
if ( empty($rows) ) $rows[] = ['key'=>'','label'=>'','type'=>'text','placeholder'=>'','required'=>0,'public'=>1,'options'=>''];
foreach ( $rows as $i => $f ):
?>
<tr class="wbf-field-row" style="background:#fff">
<td>
<input type="text" name="field_key[]"
value="<?php echo esc_attr($f['key']); ?>"
placeholder="z.b. website"
style="width:100%;font-family:monospace;font-size:.82rem"
<?php echo $f['key'] ? 'readonly' : ''; ?>>
<?php if ($f['key']): ?>
<p style="font-size:.7rem;color:#999;margin:2px 0 0">Schlüssel kann nach Erstellung nicht geändert werden.</p>
<?php endif; ?>
</td>
<td><input type="text" name="field_label[]" value="<?php echo esc_attr($f['label']); ?>" placeholder="Website" style="width:100%"></td>
<td>
<select name="field_type[]" style="width:100%" onchange="wbfToggleOptions(this)">
<?php foreach ($types as $val=>$lbl): ?>
<option value="<?php echo $val; ?>" <?php selected($f['type']??'text',$val); ?>><?php echo $lbl; ?></option>
<?php endforeach; ?>
</select>
</td>
<td><input type="text" name="field_placeholder[]" value="<?php echo esc_attr($f['placeholder']??''); ?>" placeholder="https://..." style="width:100%"></td>
<td>
<textarea name="field_options[]" rows="2"
placeholder="Option 1&#10;Option 2&#10;Option 3"
style="width:100%;font-size:.78rem;<?php echo ($f['type']??'text')==='select'?'':'display:none'; ?>"
class="wbf-options-field"><?php echo esc_textarea($f['options']??''); ?></textarea>
<span class="wbf-options-placeholder" style="color:#ccc;font-size:.75rem;<?php echo ($f['type']??'text')==='select'?'display:none':''; ?>">—</span>
</td>
<td style="text-align:center">
<input type="checkbox" name="field_required[<?php echo $i; ?>]" value="1" <?php checked($f['required']??0,1); ?>>
</td>
<td style="text-align:center">
<input type="checkbox" name="field_public[<?php echo $i; ?>]" value="1" <?php checked($f['public']??1,1); ?>>
</td>
<td>
<button type="button" class="button" onclick="this.closest('tr').remove()" style="color:#dc2626;border-color:#fca5a5">✕</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div style="display:flex;gap:.75rem;align-items:center;margin-bottom:1.5rem">
<button type="button" class="button" id="wbfAddField">+ Feld hinzufügen</button>
<?php submit_button('💾 Felder speichern','primary','wbf_save_profile_fields',false); ?>
</div>
</form>
<hr>
<h3> Hinweise</h3>
<ul style="color:#555;line-height:1.8;font-size:.9rem">
<li><strong>Schlüssel</strong>: Einmalig, nur Kleinbuchstaben/Zahlen/Unterstrich. Kann nach dem ersten Speichern nicht mehr geändert werden.</li>
<li><strong>Typ URL</strong>: Wird automatisch als klickbarer Link dargestellt.</li>
<li><strong>Typ Auswahlliste</strong>: Trage die Optionen zeilenweise ins Optionen-Feld ein.</li>
<li><strong>Öffentlich</strong>: Wenn deaktiviert, sieht nur der Nutzer selbst das Feld in seinem Profil.</li>
<li><strong>Pflicht</strong>: Verhindert das Speichern wenn das Feld leer ist.</li>
</ul>
</div>
<script>
var wbfRowCount = <?php echo count($fields) ?: 1; ?>;
document.getElementById('wbfAddField').addEventListener('click', function() {
var i = wbfRowCount++;
var tr = document.createElement('tr');
tr.className = 'wbf-field-row';
tr.style.background = '#fff';
tr.innerHTML = `
<td><input type="text" name="field_key[]" placeholder="mein_feld" style="width:100%;font-family:monospace;font-size:.82rem"></td>
<td><input type="text" name="field_label[]" placeholder="Mein Feld" style="width:100%"></td>
<td><select name="field_type[]" style="width:100%" onchange="wbfToggleOptions(this)">
<option value="text">Text</option>
<option value="url">URL/Link</option>
<option value="textarea">Mehrzeiliger Text</option>
<option value="select">Auswahlliste</option>
<option value="number">Zahl</option>
</select></td>
<td><input type="text" name="field_placeholder[]" placeholder="..." style="width:100%"></td>
<td>
<textarea name="field_options[]" rows="2" placeholder="Option 1&#10;Option 2" style="width:100%;font-size:.78rem;display:none" class="wbf-options-field"></textarea>
<span class="wbf-options-placeholder" style="color:#ccc;font-size:.75rem">—</span>
</td>
<td style="text-align:center"><input type="checkbox" name="field_required[${i}]" value="1"></td>
<td style="text-align:center"><input type="checkbox" name="field_public[${i}]" value="1" checked></td>
<td><button type="button" class="button" onclick="this.closest('tr').remove()" style="color:#dc2626;border-color:#fca5a5">✕</button></td>`;
document.getElementById('wbfFieldRows').appendChild(tr);
});
function wbfToggleOptions(sel) {
var tr = sel.closest('tr');
var ta = tr.querySelector('.wbf-options-field');
var ph = tr.querySelector('.wbf-options-placeholder');
if (sel.value === 'select') {
ta.style.display = ''; ph.style.display = 'none';
} else {
ta.style.display = 'none'; ph.style.display = '';
}
}
</script>
<?php
}
// ── Deinstallations-Seite ─────────────────────────────────────────────────────
// ── Update-Seite ──────────────────────────────────────────────────────────────
function wbf_admin_updates() {
if ( ! current_user_can('manage_options') ) return;
$latest = wbf_get_latest_release();
$update = wbf_update_available();
$cur_ver = WBF_VERSION;
$admin_url = admin_url('admin.php?page=wbf-updates');
// Manuelles Refresh
$refresh_url = wp_nonce_url( add_query_arg('wbf_refresh_update', '1', $admin_url), 'wbf_refresh_update' );
?>
<div class="wrap" style="max-width:860px">
<h1 style="display:flex;align-items:center;gap:10px">
<span style="font-size:1.4rem">🔔</span> WP Business Forum — Updates
</h1>
<!-- ── Status-Karte ─────────────────────────────────── -->
<div style="margin-top:20px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.06)">
<?php if ( $update ): ?>
<!-- UPDATE VERFÜGBAR -->
<div style="background:linear-gradient(135deg,#fffbeb,#fef3c7);border-bottom:2px solid #fcd34d;padding:18px 24px;display:flex;align-items:center;gap:14px">
<span style="font-size:2.2rem">📦</span>
<div>
<strong style="font-size:1.1rem;color:#92400e">Update verfügbar!</strong>
<p style="margin:.2rem 0 0;color:#78350f">
Version <strong><?php echo esc_html($update['version']); ?></strong> wurde veröffentlicht.
Du verwendest aktuell <strong><?php echo esc_html($cur_ver); ?></strong>.
</p>
</div>
<a href="<?php echo esc_url($update['url']); ?>" target="_blank" rel="noopener"
class="button button-primary"
style="margin-left:auto;background:#f59e0b;border-color:#d97706;white-space:nowrap;font-size:.9rem;padding:6px 16px">
📥 Zum Download
</a>
</div>
<?php else: ?>
<!-- AKTUELL -->
<div style="background:linear-gradient(135deg,#f0fdf4,#dcfce7);border-bottom:2px solid #86efac;padding:18px 24px;display:flex;align-items:center;gap:14px">
<span style="font-size:2.2rem">✅</span>
<div>
<strong style="font-size:1.1rem;color:#166534">Du verwendest die neueste Version</strong>
<p style="margin:.2rem 0 0;color:#15803d">Version <?php echo esc_html($cur_ver); ?> ist aktuell.</p>
</div>
</div>
<?php endif; ?>
<div style="padding:22px 24px">
<!-- ── Versionen-Tabelle ──────────────────────── -->
<table style="width:100%;border-collapse:collapse;margin-bottom:20px">
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;color:#6b7280;font-size:.875rem;width:220px">Installierte Version</td>
<td style="padding:10px 0;font-weight:700"><?php echo esc_html($cur_ver); ?></td>
</tr>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;color:#6b7280;font-size:.875rem">Neueste Version</td>
<td style="padding:10px 0;font-weight:700">
<?php if ($latest && !empty($latest['version'])): ?>
<?php echo esc_html($latest['version']); ?>
<?php if ($update): ?>
<span style="margin-left:6px;background:#fef3c7;color:#92400e;border:1px solid #fcd34d;border-radius:12px;padding:2px 8px;font-size:.75rem;font-weight:600">NEU</span>
<?php endif; ?>
<?php else: ?>
<span style="color:#9ca3af">Nicht abrufbar</span>
<?php endif; ?>
</td>
</tr>
<?php if ($latest && !empty($latest['published'])): ?>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;color:#6b7280;font-size:.875rem">Veröffentlicht am</td>
<td style="padding:10px 0"><?php echo esc_html(date_i18n('d.m.Y H:i', strtotime($latest['published']))); ?> Uhr</td>
</tr>
<?php endif; ?>
<tr style="border-bottom:1px solid #f3f4f6">
<td style="padding:10px 0;color:#6b7280;font-size:.875rem">Update-Quelle</td>
<td style="padding:10px 0">
<a href="<?php echo esc_url(WBF_RELEASES_PAGE); ?>" target="_blank" rel="noopener"
style="color:#2563eb;text-decoration:none">
<i class="fas fa-code-branch" style="margin-right:5px"></i>
git.viper.ipv64.net — WP-Business-Forum
</a>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6b7280;font-size:.875rem">Letzter Check</td>
<td style="padding:10px 0;display:flex;align-items:center;gap:12px">
<?php
$cached = get_transient(WBF_UPDATE_TRANSIENT);
echo ($cached !== false) ? '<span style="color:#059669">✔ Cache aktiv (12h)</span>' : '<span style="color:#9ca3af">—</span>';
?>
<a href="<?php echo esc_url($refresh_url); ?>"
style="color:#6b7280;font-size:.8rem;text-decoration:none;border:1px solid #e5e7eb;border-radius:5px;padding:3px 9px">
🔄 Jetzt prüfen
</a>
</td>
</tr>
</table>
<?php if ($latest && !empty($latest['body'])): ?>
<!-- ── Changelog ──────────────────────────────── -->
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:16px 18px">
<h3 style="margin:0 0 10px;font-size:.9rem;color:#374151;display:flex;align-items:center;gap:7px">
<span>📋</span> Release Notes — v<?php echo esc_html($latest['version']); ?>
</h3>
<pre style="white-space:pre-wrap;font-family:inherit;font-size:.82rem;color:#4b5563;margin:0;line-height:1.6;max-height:300px;overflow-y:auto"><?php echo esc_html(mb_substr($latest['body'], 0, 3000)); ?></pre>
<div style="margin-top:12px;padding-top:10px;border-top:1px solid #e2e8f0">
<a href="<?php echo esc_url($latest['url']); ?>" target="_blank" rel="noopener"
class="button" style="font-size:.82rem">
Vollständige Release-Seite öffnen →
</a>
</div>
</div>
<?php endif; ?>
<!-- ── Update-Anleitung ───────────────────────── -->
<div style="margin-top:18px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:14px 16px">
<strong style="font-size:.85rem;color:#1e40af">📖 Update-Anleitung</strong>
<ol style="margin:.6rem 0 0 1.2rem;padding:0;font-size:.82rem;color:#1e3a8a;line-height:1.8">
<li>Lade die neue <code>.zip</code> von der <a href="<?php echo esc_url(WBF_RELEASES_PAGE); ?>" target="_blank" rel="noopener" style="color:#2563eb">Release-Seite</a> herunter.</li>
<li>Erstelle vorher ein Backup über <a href="<?php echo esc_url(admin_url('admin.php?page=wbf-export')); ?>" style="color:#2563eb">Export / Import</a>.</li>
<li>Lade die neue Version per FTP/SFTP hoch und überschreibe die alten Dateien — oder nutze den WP-Plugins-Bereich (Plugin deaktivieren → löschen → neu hochladen).</li>
<li>Aktiviere das Plugin. Das DB-Schema wird automatisch aktualisiert.</li>
</ol>
</div>
</div>
</div>
</div>
<?php
}
// ── Deinstallations-Seite ─────────────────────────────────────────────────────
function wbf_admin_uninstall() {
if ( ! current_user_can('manage_options') ) return;
$did_uninstall = false;
$error = '';
// Aktion ausführen wenn Formular abgeschickt + Nonce + Bestätigungstext korrekt
if (
isset( $_POST['wbf_do_uninstall'] ) &&
check_admin_referer( 'wbf_uninstall_nonce' ) &&
( $_POST['wbf_confirm_text'] ?? '' ) === 'ALLES LÖSCHEN'
) {
global $wpdb;
// 1. Tabellen
$tables = [
'forum_poll_votes','forum_polls','forum_reactions','forum_notifications',
'forum_subscriptions','forum_invites','forum_thread_tags','forum_tags',
'forum_reports','forum_likes','forum_messages','forum_remember_tokens',
'forum_user_meta','forum_posts','forum_threads','forum_categories','forum_users',
];
foreach ( $tables as $t ) {
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}{$t}`" );
}
// 2. Optionen
$opts = [
'wbf_settings','wbf_custom_roles','wbf_level_config','wbf_levels_enabled',
'wbf_profile_fields','wbf_reactions','wbf_forum_page_id','wbf_superadmin_email',
'wbf_db_version',
];
foreach ( $opts as $o ) { delete_option( $o ); }
// 3. Transients
delete_transient( 'wbf_activation_redirect' );
delete_transient( 'wbf_stats_cache' );
$wpdb->query(
"DELETE FROM `{$wpdb->options}`
WHERE `option_name` LIKE '_transient_wbf_%'
OR `option_name` LIKE '_transient_timeout_wbf_%'"
);
// 4. Cron
wp_clear_scheduled_hook( 'wbf_check_expired_bans' );
// 5. Forum-Seite
$page_id = get_option( 'wbf_forum_page_id' );
if ( $page_id ) wp_delete_post( (int) $page_id, true );
// 6. Upload-Verzeichnis
$upload_dir = wp_upload_dir();
$wbf_dir = trailingslashit( $upload_dir['basedir'] ) . 'wbf-avatars';
if ( is_dir( $wbf_dir ) ) {
// Rekursiv löschen
$it = new RecursiveDirectoryIterator( $wbf_dir, RecursiveDirectoryIterator::SKIP_DOTS );
$ri = new RecursiveIteratorIterator( $it, RecursiveIteratorIterator::CHILD_FIRST );
foreach ( $ri as $file ) {
$file->isDir() ? rmdir( $file->getRealPath() ) : unlink( $file->getRealPath() );
}
rmdir( $wbf_dir );
}
$did_uninstall = true;
} elseif ( isset( $_POST['wbf_do_uninstall'] ) ) {
$error = 'Bitte gib <strong>ALLES LÖSCHEN</strong> korrekt ein um fortzufahren.';
}
?>
<div class="wrap">
<h1 style="color:#ef4444"><span class="dashicons dashicons-trash" style="font-size:1.6rem;width:auto;height:auto;vertical-align:middle;margin-right:.3rem"></span> Forum deinstallieren</h1>
<?php if ( $did_uninstall ): ?>
<div class="notice notice-success" style="border-left-color:#22c55e">
<p><strong>✅ Alle Forumsdaten wurden erfolgreich gelöscht.</strong><br>
Tabellen, Optionen, Transients, Cron-Jobs und der Avatar-Ordner wurden entfernt.<br>
Du kannst das Plugin jetzt unter <a href="<?php echo admin_url('plugins.php'); ?>">Plugins</a> deaktivieren und löschen.</p>
</div>
<?php return; endif; ?>
<div style="max-width:680px;margin-top:1.5rem">
<!-- Warnung -->
<div style="background:#fff5f5;border:2px solid #ef4444;border-radius:8px;padding:1.25rem 1.5rem;margin-bottom:1.5rem">
<h2 style="color:#ef4444;margin-top:0;font-size:1.1rem"><span class="dashicons dashicons-warning"></span> Achtung — dieser Vorgang ist nicht rückgängig zu machen!</h2>
<p style="margin:0 0 .5rem">Folgendes wird <strong>dauerhaft gelöscht</strong>:</p>
<ul style="margin:.5rem 0 0 1.2rem;line-height:1.9">
<li>🗄️ Alle Datenbanktabellen (Nutzer, Threads, Posts, Nachrichten, Umfragen, …)</li>
<li>⚙️ Alle Plugin-Einstellungen &amp; Optionen</li>
<li>🖼️ Avatar-Upload-Verzeichnis (<code>wp-content/uploads/wbf-avatars/</code>)</li>
<li>📄 Die Forum-Seite (erstellt vom Setup-Wizard)</li>
<li>⏰ Alle geplanten Cron-Jobs</li>
</ul>
<p style="margin:.75rem 0 0;color:#b91c1c;font-weight:600">Das Plugin selbst bleibt installiert — du kannst es danach neu einrichten oder manuell löschen.</p>
</div>
<?php if ( $error ): ?>
<div class="notice notice-error" style="margin-bottom:1rem"><p><?php echo $error; ?></p></div>
<?php endif; ?>
<!-- Bestätigungsformular -->
<form method="post" onsubmit="return wbfConfirmUninstall(event)">
<?php wp_nonce_field( 'wbf_uninstall_nonce' ); ?>
<table class="form-table" style="background:#fff;border:1px solid #ddd;border-radius:8px;padding:.5rem">
<tr>
<th style="width:220px;padding:1rem 1rem 1rem 1.25rem">
<label for="wbf_confirm_text">Bestätigung</label>
</th>
<td style="padding:1rem">
<p style="margin:0 0 .5rem;color:#555">Gib <strong style="color:#ef4444;font-family:monospace;font-size:1rem">ALLES LÖSCHEN</strong> ein um fortzufahren:</p>
<input type="text" id="wbf_confirm_text" name="wbf_confirm_text"
placeholder="ALLES LÖSCHEN"
autocomplete="off"
style="width:280px;border:2px solid #ef4444;border-radius:5px;padding:.4rem .6rem;font-size:1rem;font-weight:700;letter-spacing:.05em">
</td>
</tr>
</table>
<p style="margin-top:1.25rem">
<button type="submit" name="wbf_do_uninstall" value="1"
style="background:#ef4444;color:#fff;border:none;border-radius:6px;padding:.65rem 1.4rem;font-size:.95rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:.4rem">
<span class="dashicons dashicons-trash" style="margin-top:2px"></span>
Alle Forumsdaten unwiderruflich löschen
</button>
&nbsp;
<a href="<?php echo admin_url('admin.php?page=wbf-admin'); ?>" class="button">Abbrechen</a>
</p>
</form>
</div>
<script>
function wbfConfirmUninstall(e) {
var val = document.getElementById('wbf_confirm_text').value;
if (val !== 'ALLES LÖSCHEN') {
alert('Bitte gib genau "ALLES LÖSCHEN" ein.');
e.preventDefault();
return false;
}
return confirm('⚠️ Letzte Warnung:\n\nAlle Forumsdaten werden dauerhaft gelöscht.\nDieser Vorgang kann NICHT rückgängig gemacht werden!\n\nWirklich fortfahren?');
}
</script>
</div>
<?php
}
// ── Thread-Präfixe Verwaltung ─────────────────────────────────────────────────
function wbf_admin_prefixes() {
if ( ! current_user_can('manage_options') ) return;
// Löschen
if ( isset($_GET['delete_prefix']) && check_admin_referer('delete_prefix_'.(int)$_GET['delete_prefix']) ) {
WBF_DB::delete_prefix( (int)$_GET['delete_prefix'] );
echo '<div class="notice notice-success is-dismissible"><p>Präfix gelöscht.</p></div>';
}
// Speichern (neu oder bearbeiten)
if ( isset($_POST['wbf_save_prefix']) && check_admin_referer('wbf_prefix_nonce') ) {
$data = [
'label' => sanitize_text_field($_POST['label'] ?? ''),
'color' => sanitize_hex_color($_POST['color'] ?? '') ?: '#ffffff',
'bg_color' => sanitize_hex_color($_POST['bg_color'] ?? '') ?: '#475569',
'sort_order' => (int)($_POST['sort_order'] ?? 0),
];
if ( !empty($data['label']) ) {
if ( !empty($_POST['prefix_id']) ) {
WBF_DB::update_prefix( (int)$_POST['prefix_id'], $data );
echo '<div class="notice notice-success is-dismissible"><p>Präfix aktualisiert!</p></div>';
} else {
WBF_DB::create_prefix( $data );
echo '<div class="notice notice-success is-dismissible"><p>Präfix erstellt!</p></div>';
}
}
}
$prefixes = WBF_DB::get_prefixes();
$edit_id = isset($_GET['edit_prefix']) ? (int)$_GET['edit_prefix'] : 0;
$edit = $edit_id ? WBF_DB::get_prefix($edit_id) : null;
?>
<div class="wrap">
<h1>🏷️ Thread-Präfixe</h1>
<p style="color:#666">Präfixe erscheinen farbig vor dem Thread-Titel. Nutzer können beim Erstellen eines Threads einen Präfix wählen.</p>
<div style="display:grid;grid-template-columns:1fr 360px;gap:20px;margin-top:1.5rem;align-items:start">
<!-- Tabelle -->
<div>
<?php if (empty($prefixes)): ?>
<div style="padding:2rem;text-align:center;color:#94a3b8;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px">
Noch keine Präfixe erstellt.
</div>
<?php else: ?>
<table class="widefat striped">
<thead><tr><th>Vorschau</th><th>Reihenfolge</th><th>Aktionen</th></tr></thead>
<tbody>
<?php foreach ($prefixes as $px):
$del_url = wp_nonce_url("?page=wbf-prefixes&delete_prefix={$px->id}", "delete_prefix_{$px->id}"); ?>
<tr>
<td>
<span style="display:inline-flex;align-items:center;padding:2px 10px;border-radius:4px;font-size:.82rem;font-weight:700;color:<?php echo esc_attr($px->color); ?>;background:<?php echo esc_attr($px->bg_color); ?>">
<?php echo esc_html($px->label); ?>
</span>
</td>
<td><?php echo (int)$px->sort_order; ?></td>
<td>
<a href="?page=wbf-prefixes&edit_prefix=<?php echo (int)$px->id; ?>">Bearbeiten</a> |
<a href="<?php echo esc_url($del_url); ?>" style="color:#dc2626" onclick="return confirm('Präfix löschen?')">Löschen</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Formular -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:18px">
<h3 style="margin:0 0 14px;font-size:.95rem"><?php echo $edit ? 'Bearbeiten: '.esc_html($edit->label) : 'Neuer Präfix'; ?></h3>
<form method="post">
<?php wp_nonce_field('wbf_prefix_nonce'); ?>
<?php if ($edit): ?><input type="hidden" name="prefix_id" value="<?php echo (int)$edit->id; ?>"><?php endif; ?>
<table class="form-table" style="margin:0">
<tr>
<th style="padding:6px 10px 6px 0;width:110px">Label *</th>
<td><input type="text" name="label" value="<?php echo esc_attr($edit->label ?? ''); ?>" class="regular-text" required placeholder="z.B. GELÖST"></td>
</tr>
<tr>
<th style="padding:6px 10px 6px 0">Textfarbe</th>
<td><input type="color" name="color" value="<?php echo esc_attr($edit->color ?? '#ffffff'); ?>"></td>
</tr>
<tr>
<th style="padding:6px 10px 6px 0">Hintergrund</th>
<td><input type="color" name="bg_color" value="<?php echo esc_attr($edit->bg_color ?? '#475569'); ?>"></td>
</tr>
<tr>
<th style="padding:6px 10px 6px 0">Reihenfolge</th>
<td><input type="number" name="sort_order" value="<?php echo (int)($edit->sort_order ?? 0); ?>" class="small-text"></td>
</tr>
</table>
<div style="margin-top:12px;display:flex;gap:8px">
<button type="submit" name="wbf_save_prefix" class="button button-primary"><?php echo $edit ? 'Speichern' : 'Erstellen'; ?></button>
<?php if ($edit): ?><a href="?page=wbf-prefixes" class="button">Abbrechen</a><?php endif; ?>
</div>
</form>
<!-- Vorschau -->
<div style="margin-top:16px;padding-top:14px;border-top:1px solid #e5e7eb">
<p style="font-size:.78rem;color:#6b7280;margin:0 0 6px;font-weight:600;text-transform:uppercase;letter-spacing:.04em">Vorschau</p>
<span id="wbfPrefixPreview" style="display:inline-flex;align-items:center;padding:2px 10px;border-radius:4px;font-size:.82rem;font-weight:700">
Vorschau
</span>
</div>
<script>
(function(){
var lbl = document.querySelector('[name=label]');
var col = document.querySelector('[name=color]');
var bg = document.querySelector('[name=bg_color]');
var pre = document.getElementById('wbfPrefixPreview');
function update(){
pre.textContent = lbl.value || 'Vorschau';
pre.style.color = col.value;
pre.style.background = bg.value;
}
[lbl,col,bg].forEach(function(el){ el.addEventListener('input', update); });
update();
})();
</script>
</div>
</div>
</div>
<?php
}
// ── Wortfilter Verwaltung ─────────────────────────────────────────────────────
function wbf_admin_wordfilter() {
if ( ! current_user_can('manage_options') ) return;
if ( isset($_POST['wbf_save_wordfilter']) && check_admin_referer('wbf_wordfilter_nonce') ) {
$raw = sanitize_textarea_field( $_POST['word_list'] ?? '' );
update_option( 'wbf_word_filter', $raw );
echo '<div class="notice notice-success is-dismissible"><p>✅ Wortfilter gespeichert!</p></div>';
}
$current_list = get_option('wbf_word_filter', '');
$word_count = $current_list ? count(array_filter(array_map('trim', explode("\n", $current_list)))) : 0;
?>
<div class="wrap" style="max-width:720px">
<h1>🚫 Wortfilter / Zensurliste</h1>
<p style="color:#666">Gesperrte Wörter werden in Threads und Antworten automatisch mit <code>***</code> ersetzt. Eines pro Zeile, Groß-/Kleinschreibung wird ignoriert.</p>
<div style="background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;padding:12px 16px;margin-bottom:1.5rem;font-size:.875rem;color:#92400e">
<strong> Hinweis:</strong> Der Filter wird beim <em>Speichern</em> neuer Beiträge angewendet — bestehende Inhalte bleiben unverändert.
Aktuell <strong><?php echo $word_count; ?></strong> gesperrte Wörter/Phrasen.
</div>
<form method="post">
<?php wp_nonce_field('wbf_wordfilter_nonce'); ?>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:18px">
<label style="display:block;font-size:.82rem;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px">
Gesperrte Wörter (eines pro Zeile)
</label>
<textarea name="word_list" rows="16"
style="width:100%;font-family:monospace;font-size:.875rem;border:1.5px solid #d1d5db;border-radius:6px;padding:10px;resize:vertical;box-sizing:border-box"
placeholder="schimpfwort&#10;beleidigung&#10;spam phrase"><?php echo esc_textarea($current_list); ?></textarea>
<p style="font-size:.78rem;color:#9ca3af;margin:.4rem 0 1rem">
Auch Phrasen mit Leerzeichen sind möglich (z.B. "böse phrase"). Regex wird nicht unterstützt.
</p>
<button type="submit" name="wbf_save_wordfilter" class="button button-primary">
💾 Wortfilter speichern
</button>
</div>
</form>
</div>
<?php
}