4325 lines
334 KiB
PHP
4325 lines
334 KiB
PHP
<?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', '🎮 Discord', '🎮 Discord', 'manage_options', 'wbf-discord', 'wbf_admin_discord' );
|
||
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));
|
||
|
||
// ── MC Bridge StatusAPI ───────────────────────────────────────────────────
|
||
$mc_s = wbf_get_settings();
|
||
$mc_enabled = ! empty( $mc_s['mc_bridge_enabled'] );
|
||
$mc_api_url = trim( $mc_s['mc_bridge_api_url'] ?? '' );
|
||
|
||
// ── 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 ════════════════════════════════════════════════ -->
|
||
<?php
|
||
// Plugin-Status prüfen
|
||
$gallery_active = class_exists('MC_Gallery_Core');
|
||
$shop_active = class_exists('WIS_DB');
|
||
?>
|
||
<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>
|
||
<span class="wbf-sbadge <?php echo $gallery_active?'wbf-sbadge--ok':'wbf-sbadge--err'; ?>">
|
||
<i class="fas fa-images"></i> Galerie: <?php echo $gallery_active?'Aktiv':'Nicht aktiv'; ?>
|
||
</span>
|
||
<span class="wbf-sbadge <?php echo $shop_active?'wbf-sbadge--ok':'wbf-sbadge--err'; ?>">
|
||
<i class="fas fa-shopping-cart"></i> Shop: <?php echo $shop_active?'Aktiv':'Nicht aktiv'; ?>
|
||
</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; ?>
|
||
<div class="wbf-sdivider"></div>
|
||
<?php if ( ! $mc_enabled ) : ?>
|
||
<span class="wbf-sbadge" style="color:#94a3b8;border-color:#e2e8f0;background:#f8fafc" title="MC Bridge in den Einstellungen aktivieren">
|
||
<i class="fas fa-cubes"></i> MC Bridge: Aus
|
||
</span>
|
||
<?php elseif ( empty( $mc_api_url ) ) : ?>
|
||
<span class="wbf-sbadge wbf-sbadge--warn" title="Keine API-URL konfiguriert">
|
||
<i class="fas fa-cubes"></i> StatusAPI: Nicht konfiguriert
|
||
</span>
|
||
<?php else : ?>
|
||
<span id="wbf-mc-status-badge" class="wbf-sbadge wbf-sbadge--err" title="Verbindung wird geprüft...">
|
||
<i class="fas fa-spinner fa-spin"></i> StatusAPI: Prüfe...
|
||
</span>
|
||
<script>
|
||
(function() {
|
||
var badge = document.getElementById('wbf-mc-status-badge');
|
||
if (!badge) return;
|
||
var url = <?php echo json_encode( rest_url( 'mc-bridge/v1/status' ) ); ?>;
|
||
fetch(url, { cache: 'no-store' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d && d.success && d.enabled) {
|
||
badge.className = 'wbf-sbadge wbf-sbadge--ok';
|
||
badge.title = 'MC Bridge aktiv — Verbindung hergestellt';
|
||
badge.innerHTML = '<i class="fas fa-cubes"></i> StatusAPI: Verbunden';
|
||
} else {
|
||
badge.className = 'wbf-sbadge wbf-sbadge--err';
|
||
badge.title = 'MC Bridge deaktiviert oder Fehler';
|
||
badge.innerHTML = '<i class="fas fa-cubes"></i> StatusAPI: Nicht verbunden';
|
||
}
|
||
})
|
||
.catch(function() {
|
||
badge.className = 'wbf-sbadge wbf-sbadge--err';
|
||
badge.title = 'WordPress REST-Endpoint nicht erreichbar';
|
||
badge.innerHTML = '<i class="fas fa-cubes"></i> StatusAPI: Nicht verbunden';
|
||
});
|
||
})();
|
||
</script>
|
||
<?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'],
|
||
];
|
||
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' );
|
||
$clbadge = ( $c->min_role ?? 'member' ) !== 'member'
|
||
? " <span style='color:" . esc_attr( $crole['color'] ) . ";font-size:.8em'>🔒 " . esc_html( $crole['label'] ) . "</span>" : '';
|
||
$cguestbadge = ( (int)($c->guest_visible ?? 1) === 0 )
|
||
? " <span style='color:#f59e0b;font-size:.8em'>👁 Nur eingeloggt</span>" : '';
|
||
$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'] ) . "$clbadge$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 ? ' ↳ ' : '';
|
||
$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>❌ Nutzer nicht gefunden.</p></div>';
|
||
} elseif ( $target->role === WBF_Roles::SUPERADMIN ) {
|
||
echo '<div class="notice notice-error is-dismissible"><p>❌ 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>🗑 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>✅ 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>❌ 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>';
|
||
}
|
||
}
|
||
}
|
||
// ── Admin: 2FA eines Users zurücksetzen ──────────────────────────────────
|
||
if ( isset( $_POST['wbf_admin_reset_2fa'] ) && check_admin_referer( 'wbf_admin_2fa_nonce' ) ) {
|
||
if ( current_user_can('manage_options') && class_exists('WBF_TOTP') ) {
|
||
$uid = (int) ( $_POST['user_id'] ?? 0 );
|
||
if ( $uid ) {
|
||
$target = WBF_DB::get_user( $uid );
|
||
if ( $target && $target->role !== WBF_Roles::SUPERADMIN ) {
|
||
WBF_TOTP::disable_for( $uid );
|
||
echo '<div class="notice notice-success is-dismissible"><p>2FA für <strong>'
|
||
. esc_html($target->display_name) . '</strong> zurückgesetzt.</p></div>';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
$members = WBF_DB::get_all_users( 200 );
|
||
$s_discord = wbf_get_settings();
|
||
$dc_sync_on = ( $s_discord['discord_role_sync'] ?? '0' ) === '1' && trim( $s_discord['discord_bot_token'] ?? '' );
|
||
|
||
// Discord-Meta aller User vorladen (1 Query statt N)
|
||
$dc_meta = [];
|
||
if ( $dc_sync_on ) {
|
||
global $wpdb;
|
||
$rows = $wpdb->get_results(
|
||
"SELECT user_id,
|
||
MAX(CASE WHEN meta_key='discord_user_id' THEN meta_value END) AS discord_uid,
|
||
MAX(CASE WHEN meta_key='discord_username' THEN meta_value END) AS discord_name
|
||
FROM {$wpdb->prefix}forum_user_meta
|
||
WHERE meta_key IN ('discord_user_id','discord_username')
|
||
GROUP BY user_id"
|
||
);
|
||
foreach ( $rows as $r ) {
|
||
$dc_meta[ (int)$r->user_id ] = $r;
|
||
}
|
||
}
|
||
?>
|
||
<div class="wrap">
|
||
<h1 style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
|
||
<span>Mitglieder</span>
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<?php if ( $dc_sync_on ): ?>
|
||
<button type="button" id="wbf-discord-sync-all-btn" class="button"
|
||
style="background:#5865f2;color:#fff;border-color:#4752c4;display:inline-flex;align-items:center;gap:5px"
|
||
title="Synchronisiert Discord-Rollen aller verknüpften Nutzer (Discord → Forum)">
|
||
<span class="dashicons dashicons-update" id="wbf-sync-icon" style="margin-top:3px"></span>
|
||
Discord-Rollen synchronisieren
|
||
</button>
|
||
<span id="wbf-discord-sync-result" style="font-weight:600;font-size:.85rem"></span>
|
||
<?php endif; ?>
|
||
<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>
|
||
</div>
|
||
</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>
|
||
<?php if ( $dc_sync_on ): ?><th style="color:#5865f2"><i class="fab fa-discord"></i> Discord</th><?php endif; ?>
|
||
</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' );
|
||
// Nur sperren wenn dieser Forum-User wirklich dem WP-Superadmin (ID 1) entspricht.
|
||
// Reine Rollen-Prüfung reicht nicht — sonst kann man versehentlich
|
||
// zugewiesene superadmin-Rollen nicht mehr korrigieren.
|
||
$wp_sa_data = get_userdata( WBF_Roles::get_wp_superadmin_id() );
|
||
$is_sa = ( $m->role === WBF_Roles::SUPERADMIN )
|
||
&& $wp_sa_data
|
||
&& ( strtolower($m->email) === strtolower($wp_sa_data->user_email) );
|
||
$ban_reason = esc_attr( $m->ban_reason ?? '' );
|
||
$opts = '';
|
||
$dc_user = $dc_meta[ (int)$m->id ] ?? null;
|
||
$has_dc = $dc_sync_on && $dc_user && ! empty( $dc_user->discord_uid );
|
||
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>
|
||
<?php do_action( 'wbf_admin_user_row_extra', $m ); ?>
|
||
</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">(Haupt-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">Gesperrt — Haupt-Superadmin (WP User ID <?php echo (int) WBF_Roles::get_wp_superadmin_id(); ?>)</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>
|
||
|
||
<!-- 2FA Admin-Panel -->
|
||
<?php if ( class_exists('WBF_TOTP') && ! $is_sa ) : ?>
|
||
<div id="wbf-2fa-admin-<?php echo (int)$m->id; ?>"
|
||
style="margin-top:6px;padding:8px 10px;background:#fefce8;border:1px solid #fde68a;border-radius:4px;font-size:12px">
|
||
<strong style="color:#92400e"><i class="fas fa-shield-halved"></i> 2FA-Status:</strong>
|
||
<?php if ( WBF_TOTP::is_enabled_for($m->id) ) : ?>
|
||
<span style="color:#16a34a;font-weight:600"> ✔ Aktiv</span>
|
||
<form method="post" style="display:inline;margin-left:10px">
|
||
<?php wp_nonce_field( 'wbf_admin_2fa_nonce' ); ?>
|
||
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
|
||
<button type="submit" name="wbf_admin_reset_2fa"
|
||
class="button button-small"
|
||
style="color:#dc2626;border-color:rgba(220,38,38,.4);font-size:11px"
|
||
onclick="return confirm('2FA für <?php echo esc_js($m->display_name); ?> zurücksetzen?')">
|
||
<span class="dashicons dashicons-unlock" style="font-size:13px;width:13px;height:13px;vertical-align:-2px"></span>
|
||
2FA zurücksetzen
|
||
</button>
|
||
</form>
|
||
<?php else : ?>
|
||
<span style="color:#9ca3af"> — Nicht aktiv</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- 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 & 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>
|
||
|
||
<?php if ( $dc_sync_on ) : ?>
|
||
<td style="white-space:nowrap;min-width:140px;vertical-align:top;padding-top:8px">
|
||
<?php if ( $has_dc ) : ?>
|
||
<div style="display:flex;flex-direction:column;gap:5px">
|
||
<span style="font-size:.8rem;color:#5865f2;font-weight:600">
|
||
<i class="fab fa-discord"></i>
|
||
<?php echo esc_html( $dc_user->discord_name ?: $dc_user->discord_uid ); ?>
|
||
</span>
|
||
<button type="button"
|
||
class="button button-small wbf-dc-sync-user"
|
||
data-uid="<?php echo (int)$m->id; ?>"
|
||
data-nonce="<?php echo wp_create_nonce('wbf_nonce'); ?>"
|
||
style="color:#5865f2;border-color:#5865f2;font-size:.75rem;height:24px;line-height:22px">
|
||
<span class="dashicons dashicons-update" style="font-size:12px;width:12px;height:12px;margin-top:5px"></span>
|
||
Sync
|
||
</button>
|
||
<span class="wbf-dc-user-result" style="font-size:.75rem;font-weight:600"></span>
|
||
</div>
|
||
<?php else : ?>
|
||
<span style="font-size:.78rem;color:#9ca3af">Nicht verknüpft</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<?php endif; ?>
|
||
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<?php if ( $dc_sync_on ) : ?>
|
||
<style>
|
||
@keyframes wbf-spin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
|
||
.wbf-spinning { animation: wbf-spin .8s linear infinite; display:inline-block; }
|
||
</style>
|
||
<script>
|
||
(function(){
|
||
var nonce = '<?php echo wp_create_nonce("wbf_nonce"); ?>';
|
||
|
||
// ── Bulk-Sync ──────────────────────────────────────────────────────────
|
||
var allBtn = document.getElementById('wbf-discord-sync-all-btn');
|
||
var allRes = document.getElementById('wbf-discord-sync-result');
|
||
var allIcon = document.getElementById('wbf-sync-icon');
|
||
|
||
if (allBtn) {
|
||
allBtn.addEventListener('click', function() {
|
||
allBtn.disabled = true;
|
||
if (allIcon) allIcon.classList.add('wbf-spinning');
|
||
allRes.style.color = '#374151';
|
||
allRes.textContent = '⏳ Sync läuft…';
|
||
|
||
fetch(ajaxurl, {
|
||
method : 'POST',
|
||
headers : {'Content-Type':'application/x-www-form-urlencoded'},
|
||
body : 'action=wbf_manual_discord_sync&nonce=' + nonce
|
||
})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d) {
|
||
allBtn.disabled = false;
|
||
if (allIcon) allIcon.classList.remove('wbf-spinning');
|
||
if (d.success) {
|
||
allRes.style.color = '#16a34a';
|
||
allRes.textContent = '✅ ' + (d.data.message || 'Fertig!');
|
||
// Seite neu laden damit neue Rollen sichtbar werden
|
||
setTimeout(function(){ location.reload(); }, 1800);
|
||
} else {
|
||
allRes.style.color = '#dc2626';
|
||
allRes.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
|
||
}
|
||
})
|
||
.catch(function() {
|
||
allBtn.disabled = false;
|
||
if (allIcon) allIcon.classList.remove('wbf-spinning');
|
||
allRes.style.color = '#dc2626';
|
||
allRes.textContent = '❌ Netzwerkfehler';
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Pro-Nutzer-Sync ───────────────────────────────────────────────────
|
||
document.querySelectorAll('.wbf-dc-sync-user').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var uid = btn.dataset.uid;
|
||
var icon = btn.querySelector('.dashicons');
|
||
var result = btn.closest('div').querySelector('.wbf-dc-user-result');
|
||
|
||
btn.disabled = true;
|
||
if (icon) icon.classList.add('wbf-spinning');
|
||
if (result) { result.style.color='#374151'; result.textContent='⏳'; }
|
||
|
||
fetch(ajaxurl, {
|
||
method : 'POST',
|
||
headers : {'Content-Type':'application/x-www-form-urlencoded'},
|
||
body : 'action=wbf_discord_sync_user&nonce=' + nonce + '&user_id=' + uid
|
||
})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d) {
|
||
btn.disabled = false;
|
||
if (icon) icon.classList.remove('wbf-spinning');
|
||
if (d.success) {
|
||
if (result) { result.style.color='#16a34a'; result.textContent='✅ OK'; }
|
||
// Rollenbadge in dieser Zeile nach 1s aktualisieren (Seitenreload)
|
||
setTimeout(function(){ location.reload(); }, 1200);
|
||
} else {
|
||
if (result) {
|
||
result.style.color = '#dc2626';
|
||
result.textContent = '❌ ' + ((d.data && d.data.message) || 'Fehler');
|
||
}
|
||
}
|
||
})
|
||
.catch(function() {
|
||
btn.disabled = false;
|
||
if (icon) icon.classList.remove('wbf-spinning');
|
||
if (result) { result.style.color='#dc2626'; result.textContent='❌ Netzwerkfehler'; }
|
||
});
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
<?php endif; ?>
|
||
<?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() {
|
||
|
||
// ── Kategorien speichern ─────────────────────────────────────────────────
|
||
if ( isset($_POST['wbf_save_profile_fields']) && check_admin_referer('wbf_profile_fields_nonce') ) {
|
||
|
||
// Kategorien
|
||
$cat_ids = $_POST['cat_id'] ?? [];
|
||
$cat_names = $_POST['cat_name'] ?? [];
|
||
$cat_icons = $_POST['cat_icon'] ?? [];
|
||
$cats = [];
|
||
foreach ( $cat_ids as $ci => $cid ) {
|
||
$cid = sanitize_key( $cid );
|
||
if ( ! $cid ) continue;
|
||
$cats[] = [
|
||
'id' => $cid,
|
||
'name' => sanitize_text_field( $cat_names[$ci] ?? $cid ),
|
||
'icon' => sanitize_text_field( $cat_icons[$ci] ?? '📋' ),
|
||
];
|
||
}
|
||
// Neue Kategorie
|
||
if ( ! empty( trim( $_POST['new_cat_name'] ?? '' ) ) ) {
|
||
$new_name = sanitize_text_field( $_POST['new_cat_name'] );
|
||
$new_icon = sanitize_text_field( $_POST['new_cat_icon'] ?? '📋' );
|
||
$new_id = 'cat_' . sanitize_key( $new_name ) . '_' . time();
|
||
$cats[] = [ 'id' => $new_id, 'name' => $new_name, 'icon' => $new_icon ];
|
||
}
|
||
WBF_DB::save_profile_field_categories( $cats );
|
||
|
||
// Felder
|
||
$keys = $_POST['field_key'] ?? [];
|
||
$labels = $_POST['field_label'] ?? [];
|
||
$types = $_POST['field_type'] ?? [];
|
||
$phs = $_POST['field_placeholder'] ?? [];
|
||
$req = $_POST['field_required'] ?? [];
|
||
$pub = $_POST['field_public'] ?? [];
|
||
$opts = $_POST['field_options'] ?? [];
|
||
$cats_f = $_POST['field_category'] ?? [];
|
||
|
||
$valid_cat_ids = array_column( $cats, 'id' );
|
||
$fields = [];
|
||
foreach ( $keys as $i => $raw_key ) {
|
||
$key = sanitize_key( $raw_key );
|
||
if ( ! $key ) continue;
|
||
if ( in_array($key, ['username','email','password','display_name','bio','signature','avatar_url','role']) ) continue;
|
||
$cat_id = sanitize_key( $cats_f[$i] ?? '' );
|
||
if ( ! in_array( $cat_id, $valid_cat_ids ) ) $cat_id = '';
|
||
$fields[] = [
|
||
'key' => $key,
|
||
'label' => sanitize_text_field( $labels[$i] ?? $key ),
|
||
'type' => in_array($types[$i] ?? '', ['text','url','textarea','select','number','date']) ? $types[$i] : 'text',
|
||
'placeholder' => sanitize_text_field( $phs[$i] ?? '' ),
|
||
'required' => ! empty( $req[$i] ) ? 1 : 0,
|
||
'public' => ! empty( $pub[$i] ) ? 1 : 0,
|
||
'options' => sanitize_textarea_field( $opts[$i] ?? '' ),
|
||
'category_id' => $cat_id,
|
||
];
|
||
}
|
||
WBF_DB::save_profile_field_defs( $fields );
|
||
echo '<div class="notice notice-success is-dismissible"><p>✅ Profilfelder & Kategorien gespeichert!</p></div>';
|
||
}
|
||
|
||
// Kategorie löschen
|
||
if ( isset($_GET['wbf_del_cat']) && check_admin_referer('wbf_del_cat') ) {
|
||
$del_id = sanitize_key( $_GET['wbf_del_cat'] );
|
||
$cats = WBF_DB::get_profile_field_categories();
|
||
$cats = array_values( array_filter( $cats, fn($c) => $c['id'] !== $del_id ) );
|
||
WBF_DB::save_profile_field_categories( $cats );
|
||
// Felder dieser Kategorie auf "keine Kategorie" setzen
|
||
$fields = WBF_DB::get_profile_field_defs();
|
||
foreach ( $fields as &$f ) {
|
||
if ( ($f['category_id'] ?? '') === $del_id ) $f['category_id'] = '';
|
||
}
|
||
WBF_DB::save_profile_field_defs( $fields );
|
||
wp_safe_redirect( remove_query_arg(['wbf_del_cat','_wpnonce']) );
|
||
exit;
|
||
}
|
||
|
||
$fields = WBF_DB::get_profile_field_defs();
|
||
$cats = WBF_DB::get_profile_field_categories();
|
||
$cat_map = array_column( $cats, null, 'id' );
|
||
$type_opts = ['text'=>'Text','url'=>'URL/Link','textarea'=>'Mehrzeiliger Text','select'=>'Auswahlliste','number'=>'Zahl','date'=>'Datum (Alter)'];
|
||
|
||
// Felder nach Kategorie gruppieren
|
||
$by_cat = [];
|
||
foreach ( $fields as $f ) {
|
||
$cid = $f['category_id'] ?? '';
|
||
if ( ! $cid || ! isset($cat_map[$cid]) ) $cid = '__none__';
|
||
$by_cat[$cid][] = $f;
|
||
}
|
||
?>
|
||
<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'); ?>
|
||
|
||
<!-- ── Kategorien-Verwaltung ──────────────────────────────── -->
|
||
<div style="background:#f0f4ff;border:1px solid #c7d3f8;border-radius:10px;padding:18px 20px;margin-bottom:22px">
|
||
<h3 style="margin:0 0 14px;font-size:1rem;display:flex;align-items:center;gap:8px">
|
||
🗂️ Kategorien verwalten
|
||
</h3>
|
||
<table class="widefat" style="margin-bottom:12px;background:#fff;border-radius:7px;overflow:hidden">
|
||
<thead>
|
||
<tr style="background:#eef2ff">
|
||
<th style="width:56px;padding:8px 10px">Icon</th>
|
||
<th style="padding:8px 10px">Kategorie-Name</th>
|
||
<th style="width:50px"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ( $cats as $ci => $cat ): ?>
|
||
<tr>
|
||
<td style="padding:6px 10px">
|
||
<input type="hidden" name="cat_id[]" value="<?php echo esc_attr($cat['id']); ?>">
|
||
<input type="text" name="cat_icon[]" value="<?php echo esc_attr($cat['icon']); ?>"
|
||
style="width:46px;text-align:center;font-size:1.1rem;padding:4px 2px;border:1px solid #d0d9f7;border-radius:5px">
|
||
</td>
|
||
<td style="padding:6px 10px">
|
||
<input type="text" name="cat_name[]" value="<?php echo esc_attr($cat['name']); ?>"
|
||
style="width:100%;max-width:340px;border:1px solid #d0d9f7;border-radius:5px;padding:5px 8px">
|
||
</td>
|
||
<td style="padding:6px 10px;text-align:center">
|
||
<?php
|
||
$del_url = wp_nonce_url(
|
||
add_query_arg(['page'=>'wbf-profile-fields','wbf_del_cat'=>$cat['id']], admin_url('admin.php')),
|
||
'wbf_del_cat'
|
||
); ?>
|
||
<a href="<?php echo esc_url($del_url); ?>"
|
||
onclick="return confirm('Kategorie löschen? Felder dieser Kategorie bleiben erhalten (ohne Kategorie).')"
|
||
style="color:#dc2626;text-decoration:none;font-size:1.1rem;font-weight:700" title="Kategorie löschen">×</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<tr style="background:#f8faff">
|
||
<td style="padding:6px 10px">
|
||
<input type="text" name="new_cat_icon" value="" placeholder="📋"
|
||
style="width:46px;text-align:center;font-size:1.1rem;padding:4px 2px;border:1px dashed #a5b4f8;border-radius:5px">
|
||
</td>
|
||
<td style="padding:6px 10px" colspan="2">
|
||
<input type="text" name="new_cat_name" value="" placeholder="+ Neue Kategorie (Namen eingeben und speichern)"
|
||
style="width:100%;max-width:380px;border:1px dashed #a5b4f8;border-radius:5px;padding:5px 8px;color:#555">
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- ── Felder je Kategorie ───────────────────────────────── -->
|
||
<?php
|
||
// Globaler Feld-Index — synchronisiert Checkboxen mit den sequentiellen []‑Arrays
|
||
$wbf_fidx = 0;
|
||
// Alle Kategorien + "Ohne Kategorie" am Ende ausgeben
|
||
$all_sections = $cats;
|
||
if ( isset($by_cat['__none__']) ) {
|
||
$all_sections[] = ['id'=>'__none__','name'=>'Ohne Kategorie','icon'=>'📋'];
|
||
}
|
||
foreach ( $all_sections as $cat ):
|
||
$cid = $cat['id'];
|
||
$c_fields = $by_cat[$cid] ?? [];
|
||
?>
|
||
<div class="wbf-cat-section" style="border:1px solid #e0e6f8;border-radius:10px;margin-bottom:16px;overflow:hidden">
|
||
<div style="background:#eef2ff;padding:10px 16px;display:flex;align-items:center;gap:8px;cursor:pointer"
|
||
onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'':'none'">
|
||
<span style="font-size:1.1rem"><?php echo esc_html($cat['icon']); ?></span>
|
||
<strong style="font-size:.95rem"><?php echo esc_html($cat['name']); ?></strong>
|
||
<span style="color:#94a3b8;font-size:.8rem;margin-left:4px"><?php echo count($c_fields); ?> Feld<?php echo count($c_fields)!==1?'er':''; ?></span>
|
||
</div>
|
||
<div class="wbf-cat-body">
|
||
<table class="widefat" style="border:none">
|
||
<thead>
|
||
<tr style="background:#f8faff">
|
||
<th style="width:130px">Schlüssel</th>
|
||
<th style="width:150px">Bezeichnung</th>
|
||
<th style="width:105px">Typ</th>
|
||
<th>Platzhalter</th>
|
||
<th style="width:105px">Optionen</th>
|
||
<th style="width:65px;text-align:center">Pflicht</th>
|
||
<th style="width:70px;text-align:center">Öffentlich</th>
|
||
<th style="width:130px">Kategorie</th>
|
||
<th style="width:44px"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="wbf-field-rows">
|
||
<?php
|
||
if ( empty($c_fields) ):
|
||
?>
|
||
<tr class="wbf-no-fields-hint" style="background:#fafafa">
|
||
<td colspan="9" style="color:#aaa;font-style:italic;font-size:.85rem;padding:10px 14px">
|
||
Noch keine Felder in dieser Kategorie.
|
||
</td>
|
||
</tr>
|
||
<?php
|
||
endif;
|
||
foreach ( $c_fields as $i_f => $f ):
|
||
$fi = $wbf_fidx;
|
||
$wbf_fidx++;
|
||
?>
|
||
<tr class="wbf-field-row" style="background:#fff">
|
||
<td style="padding:6px 8px">
|
||
<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:.68rem;color:#999;margin:2px 0 0">Nicht änderbar</p>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td style="padding:6px 8px">
|
||
<input type="text" name="field_label[]" value="<?php echo esc_attr($f['label']); ?>" placeholder="Website" style="width:100%">
|
||
</td>
|
||
<td style="padding:6px 8px">
|
||
<select name="field_type[]" style="width:100%" onchange="wbfToggleOptions(this)">
|
||
<?php foreach ($type_opts as $val=>$lbl): ?>
|
||
<option value="<?php echo $val; ?>" <?php selected($f['type']??'text',$val); ?>><?php echo $lbl; ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</td>
|
||
<td style="padding:6px 8px">
|
||
<input type="text" name="field_placeholder[]" value="<?php echo esc_attr($f['placeholder']??''); ?>" placeholder="https://..." style="width:100%">
|
||
</td>
|
||
<td style="padding:6px 8px">
|
||
<textarea name="field_options[]" rows="2"
|
||
placeholder="Option 1 Option 2"
|
||
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;padding:6px 8px">
|
||
<input type="hidden" name="field_required[<?php echo $fi; ?>]" value="0">
|
||
<input type="checkbox" name="field_required[<?php echo $fi; ?>]" value="1" <?php checked($f['required']??0,1); ?>>
|
||
</td>
|
||
<td style="text-align:center;padding:6px 8px">
|
||
<input type="hidden" name="field_public[<?php echo $fi; ?>]" value="0">
|
||
<input type="checkbox" name="field_public[<?php echo $fi; ?>]" value="1" <?php checked($f['public']??1,1); ?>>
|
||
</td>
|
||
<td style="padding:6px 8px">
|
||
<select name="field_category[]" style="width:100%;font-size:.82rem">
|
||
<option value="">— Ohne —</option>
|
||
<?php foreach ($cats as $co): ?>
|
||
<option value="<?php echo esc_attr($co['id']); ?>" <?php selected($f['category_id']??'',$co['id']); ?>>
|
||
<?php echo esc_html($co['icon'].' '.$co['name']); ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</td>
|
||
<td style="padding:6px 8px">
|
||
<button type="button" class="button" onclick="wbfRemoveRow(this)" style="color:#dc2626;border-color:#fca5a5;padding:2px 7px">✕</button>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<div style="padding:10px 14px">
|
||
<button type="button" class="button" onclick="wbfAddField(this,'<?php echo esc_attr($cid); ?>')" style="font-size:.82rem">
|
||
+ Feld hinzufügen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<div style="display:flex;gap:.75rem;align-items:center;margin:1rem 0 1.5rem">
|
||
<?php submit_button('💾 Felder & Kategorien speichern','primary','wbf_save_profile_fields',false); ?>
|
||
</div>
|
||
</form>
|
||
|
||
<hr>
|
||
<h3>ℹ️ Hinweise</h3>
|
||
<ul style="color:#555;line-height:1.9;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 Datum (Alter)</strong>: Speichert das Geburtsdatum und zeigt nur das Alter an.</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>
|
||
<li><strong>Kategorie</strong>: Felder werden im Profil nach Kategorie gruppiert dargestellt.</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<script>
|
||
var wbfRowCount = <?php echo $wbf_fidx; ?>;
|
||
|
||
function wbfRemoveRow(btn) {
|
||
var tr = btn.closest('tr');
|
||
tr.remove();
|
||
}
|
||
|
||
function wbfAddField(btn, catId) {
|
||
var i = wbfRowCount++;
|
||
var tbody = btn.closest('.wbf-cat-body').querySelector('.wbf-field-rows');
|
||
// Remove "no fields" hint row if present
|
||
var hint = tbody.querySelector('.wbf-no-fields-hint');
|
||
if (hint) hint.remove();
|
||
|
||
// Build category <options>
|
||
var cats = <?php echo json_encode( array_values($cats) ); ?>;
|
||
var catOpts = '<option value="">— Ohne —</option>';
|
||
cats.forEach(function(c) {
|
||
var sel = (c.id === catId) ? ' selected' : '';
|
||
catOpts += '<option value="' + c.id + '"' + sel + '>' + c.icon + ' ' + c.name + '</option>';
|
||
});
|
||
|
||
var tr = document.createElement('tr');
|
||
tr.className = 'wbf-field-row';
|
||
tr.style.background = '#fff';
|
||
tr.innerHTML =
|
||
'<td style="padding:6px 8px"><input type="text" name="field_key[]" placeholder="mein_feld" style="width:100%;font-family:monospace;font-size:.82rem"></td>' +
|
||
'<td style="padding:6px 8px"><input type="text" name="field_label[]" placeholder="Mein Feld" style="width:100%"></td>' +
|
||
'<td style="padding:6px 8px"><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>' +
|
||
'<option value="date">Datum (Alter)</option>' +
|
||
'</select></td>' +
|
||
'<td style="padding:6px 8px"><input type="text" name="field_placeholder[]" placeholder="..." style="width:100%"></td>' +
|
||
'<td style="padding:6px 8px">' +
|
||
'<textarea name="field_options[]" rows="2" placeholder="Option 1\nOption 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;padding:6px 8px"><input type="hidden" name="field_required[' + i + ']" value="0"><input type="checkbox" name="field_required[' + i + ']" value="1"></td>' +
|
||
'<td style="text-align:center;padding:6px 8px"><input type="hidden" name="field_public[' + i + ']" value="0"><input type="checkbox" name="field_public[' + i + ']" value="1" checked></td>' +
|
||
'<td style="padding:6px 8px"><select name="field_category[]" style="width:100%;font-size:.82rem">' + catOpts + '</select></td>' +
|
||
'<td style="padding:6px 8px"><button type="button" class="button" onclick="wbfRemoveRow(this)" style="color:#dc2626;border-color:#fca5a5;padding:2px 7px">✕</button></td>';
|
||
tbody.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 & 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>
|
||
|
||
<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">
|
||
<?php
|
||
// PHP 8 safe: extract values once — avoids null->property warnings
|
||
$px_label = $edit ? $edit->label : '';
|
||
$px_color = $edit ? $edit->color : '#ffffff';
|
||
$px_bg = $edit ? $edit->bg_color : '#475569';
|
||
$px_order = $edit ? (int) $edit->sort_order : 0;
|
||
$px_id = $edit ? (int) $edit->id : 0;
|
||
?>
|
||
<h3 style="margin:0 0 14px;font-size:.95rem"><?php echo $edit ? 'Bearbeiten: '.esc_html($px_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 $px_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($px_label); ?>" style="width:100%;box-sizing:border-box" 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($px_color); ?>"></td>
|
||
</tr>
|
||
<tr>
|
||
<th style="padding:6px 10px 6px 0">Hintergrund</th>
|
||
<td><input type="color" name="bg_color" value="<?php echo esc_attr($px_bg); ?>"></td>
|
||
</tr>
|
||
<tr>
|
||
<th style="padding:6px 10px 6px 0">Reihenfolge</th>
|
||
<td><input type="number" name="sort_order" value="<?php echo $px_order; ?>" 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 beleidigung 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
|
||
}
|
||
|
||
// ── Discord-Bot-Verbindungstest (Admin AJAX) ──────────────────────────────────
|
||
add_action('wp_ajax_wbf_discord_test', function() {
|
||
if ( ! current_user_can('manage_options') ) wp_send_json_error(['message' => 'Keine Berechtigung.']);
|
||
check_ajax_referer('wbf_discord_test', 'nonce');
|
||
|
||
$s = wbf_get_settings();
|
||
$token = trim($s['discord_bot_token'] ?? '');
|
||
$guild = trim($s['discord_guild_id'] ?? '');
|
||
|
||
if ( ! $token ) {
|
||
wp_send_json_error(['message' => 'Kein Bot-Token gespeichert.']);
|
||
}
|
||
|
||
// Bot-Info abrufen (@me)
|
||
$res = wp_remote_get('https://discord.com/api/v10/users/@me', [
|
||
'timeout' => 8,
|
||
'headers' => [
|
||
'Authorization' => 'Bot ' . $token,
|
||
'Content-Type' => 'application/json',
|
||
],
|
||
]);
|
||
|
||
if ( is_wp_error($res) ) {
|
||
wp_send_json_error(['message' => 'HTTP-Fehler: ' . $res->get_error_message()]);
|
||
}
|
||
|
||
$code = wp_remote_retrieve_response_code($res);
|
||
$body = json_decode(wp_remote_retrieve_body($res), true);
|
||
|
||
if ( $code !== 200 || empty($body['id']) ) {
|
||
$err = $body['message'] ?? 'Unbekannter Fehler (HTTP ' . $code . ')';
|
||
wp_send_json_error(['message' => 'Discord API: ' . $err]);
|
||
}
|
||
|
||
$bot_name = ($body['username'] ?? 'Unbekannt') . '#' . ($body['discriminator'] ?? '0');
|
||
|
||
// Guild-Prüfung falls Guild-ID angegeben
|
||
$guild_info = '';
|
||
if ( $guild ) {
|
||
$gr = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}", [
|
||
'timeout' => 6,
|
||
'headers' => ['Authorization' => 'Bot ' . $token],
|
||
]);
|
||
if ( ! is_wp_error($gr) && wp_remote_retrieve_response_code($gr) === 200 ) {
|
||
$gd = json_decode(wp_remote_retrieve_body($gr), true);
|
||
$guild_info = ' | Server: ' . ($gd['name'] ?? $guild);
|
||
} else {
|
||
$guild_info = ' | ⚠️ Server nicht gefunden oder Bot kein Mitglied';
|
||
}
|
||
}
|
||
|
||
wp_send_json_success(['message' => 'Bot: ' . $bot_name . $guild_info]);
|
||
});
|
||
|
||
// ── Discord-Cron: Rollen synchronisieren ──────────────────────────────────────
|
||
add_action('wbf_discord_role_sync', 'wbf_run_discord_role_sync');
|
||
|
||
if ( ! wp_next_scheduled('wbf_discord_role_sync') ) {
|
||
wp_schedule_event(time(), 'hourly', 'wbf_discord_role_sync');
|
||
}
|
||
|
||
function wbf_run_discord_role_sync() {
|
||
$s = wbf_get_settings();
|
||
if ( ($s['discord_role_sync'] ?? '0') !== '1' ) return;
|
||
$token = trim($s['discord_bot_token'] ?? '');
|
||
$guild = trim($s['discord_guild_id'] ?? '');
|
||
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
|
||
if ( ! $token || ! $guild || empty($role_map) ) return;
|
||
|
||
global $wpdb;
|
||
// Alle verifizierten Discord-User holen (discord_user_id in user_meta gesetzt)
|
||
$rows = $wpdb->get_results(
|
||
"SELECT um.user_id, um.meta_value AS discord_user_id
|
||
FROM {$wpdb->prefix}forum_user_meta um
|
||
WHERE um.meta_key = 'discord_user_id' AND um.meta_value != ''"
|
||
);
|
||
|
||
foreach ( $rows as $row ) {
|
||
wbf_sync_discord_role_for_user((int)$row->user_id, $row->discord_user_id, $token, $guild, $role_map);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Synchronisiert die Discord-Serverrolle eines einzelnen Nutzers mit der Forum-Rolle.
|
||
*/
|
||
function wbf_sync_discord_role_for_user($forum_user_id, $discord_user_id, $token, $guild, $role_map) {
|
||
// Guild-Member-Info abrufen
|
||
$res = wp_remote_get("https://discord.com/api/v10/guilds/{$guild}/members/{$discord_user_id}", [
|
||
'timeout' => 6,
|
||
'headers' => ['Authorization' => 'Bot ' . $token],
|
||
]);
|
||
if ( is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200 ) return;
|
||
|
||
$member = json_decode(wp_remote_retrieve_body($res), true);
|
||
$user_roles = $member['roles'] ?? [];
|
||
|
||
// Rollen-Map prüfen — erster Treffer gewinnt (Reihenfolge = Priorität)
|
||
foreach ( $role_map as $dc_role_id => $forum_role ) {
|
||
if ( in_array((string)$dc_role_id, array_map('strval', $user_roles), true) ) {
|
||
$forum_user = WBF_DB::get_user($forum_user_id);
|
||
if ( $forum_user && $forum_user->role !== 'superadmin' && $forum_user->role !== $forum_role ) {
|
||
WBF_DB::update_user($forum_user_id, ['role' => $forum_role]);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// ── Discord-Admin-Seite ───────────────────────────────────────────────────────
|
||
if ( ! function_exists('wbf_admin_discord') ) {
|
||
function wbf_admin_discord() {
|
||
if ( ! current_user_can('manage_options') ) return;
|
||
$s = wbf_get_settings();
|
||
?>
|
||
<div class="wrap">
|
||
<h1>🎮 Discord-Integration</h1>
|
||
<p>Konfiguriere den Discord-Bot und die Rollen-Synchronisation.
|
||
Einstellungen werden in <a href="admin.php?page=wbf-settings">Einstellungen → Discord-Integration</a> gespeichert.</p>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;max-width:900px;margin-top:1.5rem">
|
||
|
||
<!-- Status-Panel -->
|
||
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px">
|
||
<h3 style="margin-top:0">🔌 Bot-Status</h3>
|
||
<?php
|
||
$token = trim($s['discord_bot_token'] ?? '');
|
||
$guild = trim($s['discord_guild_id'] ?? '');
|
||
if (!$token): ?>
|
||
<p style="color:#dc2626"><i class="dashicons dashicons-warning"></i> Kein Bot-Token konfiguriert.</p>
|
||
<a href="admin.php?page=wbf-settings#discord" class="button button-primary">Jetzt einrichten</a>
|
||
<?php else: ?>
|
||
<p style="color:#16a34a;font-weight:600">✅ Bot-Token gespeichert</p>
|
||
<p style="color:<?php echo $guild ? '#16a34a' : '#f59e0b'; ?>">
|
||
<?php echo $guild ? '✅ Guild-ID: <code>' . esc_html($guild) . '</code>' : '⚠️ Keine Guild-ID gesetzt'; ?>
|
||
</p>
|
||
<p style="color:<?php echo ($s['discord_role_sync']??'0')==='1' ? '#16a34a' : '#9ca3af'; ?>">
|
||
Rollen-Sync: <strong><?php echo ($s['discord_role_sync']??'0')==='1' ? 'Aktiv' : 'Deaktiviert'; ?></strong>
|
||
</p>
|
||
<button type="button" class="button button-secondary" id="wbf-discord-test-btn2">
|
||
🔌 Verbindung testen
|
||
</button>
|
||
<span id="wbf-discord-test-result2" style="margin-left:10px;font-weight:600"></span>
|
||
<script>
|
||
document.getElementById('wbf-discord-test-btn2').addEventListener('click', function(){
|
||
var btn = this, res = document.getElementById('wbf-discord-test-result2');
|
||
btn.disabled = true; res.textContent = '⏳ Teste…';
|
||
fetch(ajaxurl,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},
|
||
body:'action=wbf_discord_test&nonce=<?php echo wp_create_nonce("wbf_discord_test"); ?>'
|
||
}).then(r=>r.json()).then(function(d){
|
||
res.style.color = d.success ? '#16a34a' : '#dc2626';
|
||
res.textContent = d.success ? '✅ '+(d.data.message||'OK') : '❌ '+((d.data&&d.data.message)||'Fehler');
|
||
btn.disabled = false;
|
||
}).catch(function(){ res.style.color='#dc2626'; res.textContent='❌ Netzwerkfehler'; btn.disabled=false; });
|
||
});
|
||
</script>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Rollen-Map-Übersicht -->
|
||
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px">
|
||
<h3 style="margin-top:0">🔗 Aktive Rollen-Zuordnungen</h3>
|
||
<?php
|
||
$role_map = json_decode($s['discord_role_map'] ?? '{}', true) ?: [];
|
||
$all_roles = WBF_Roles::get_all();
|
||
if (empty($role_map)): ?>
|
||
<p style="color:#9ca3af">Keine Zuordnungen konfiguriert.</p>
|
||
<a href="admin.php?page=wbf-settings" class="button">Jetzt einrichten</a>
|
||
<?php else: ?>
|
||
<table class="widefat striped" style="font-size:.85rem">
|
||
<thead><tr><th>Discord Rollen-ID</th><th>Forum-Rolle</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($role_map as $dc_id => $fr_key):
|
||
$fr_label = $all_roles[$fr_key]['label'] ?? $fr_key; ?>
|
||
<tr>
|
||
<td><code><?php echo esc_html($dc_id); ?></code></td>
|
||
<td><?php echo WBF_Roles::badge($fr_key); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<a href="admin.php?page=wbf-settings" class="button" style="margin-top:.75rem">Bearbeiten</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Verknüpfte Discord-Nutzer -->
|
||
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:20px;max-width:900px;margin-top:20px">
|
||
<h3 style="margin-top:0">👥 Verknüpfte Forum-Nutzer</h3>
|
||
<?php
|
||
global $wpdb;
|
||
$linked = $wpdb->get_results(
|
||
"SELECT fu.id, fu.username, fu.display_name, fu.role,
|
||
MAX(CASE WHEN um.meta_key='discord_username' THEN um.meta_value END) AS discord_name,
|
||
MAX(CASE WHEN um.meta_key='discord_user_id' THEN um.meta_value END) AS discord_uid
|
||
FROM {$wpdb->prefix}forum_users fu
|
||
JOIN {$wpdb->prefix}forum_user_meta um ON um.user_id = fu.id
|
||
WHERE um.meta_key IN ('discord_username','discord_user_id')
|
||
GROUP BY fu.id
|
||
HAVING discord_name != '' AND discord_name IS NOT NULL
|
||
ORDER BY fu.username"
|
||
);
|
||
if (empty($linked)): ?>
|
||
<p style="color:#9ca3af">Noch keine verknüpften Nutzer.</p>
|
||
<?php else: ?>
|
||
<table class="widefat striped" style="font-size:.85rem">
|
||
<thead><tr><th>Forum-Nutzer</th><th>Rolle</th><th>Discord-Name</th><th>Discord-ID</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($linked as $u): ?>
|
||
<tr>
|
||
<td><strong><?php echo esc_html($u->display_name); ?></strong>
|
||
<span style="color:#9ca3af"> @<?php echo esc_html($u->username); ?></span></td>
|
||
<td><?php echo WBF_Roles::badge($u->role); ?></td>
|
||
<td><i class="fab fa-discord" style="color:#5865f2"></i> <?php echo esc_html($u->discord_name ?: '–'); ?></td>
|
||
<td><code style="font-size:.78rem"><?php echo esc_html($u->discord_uid ?: '–'); ?></code></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
</div>
|
||
<?php
|
||
}
|
||
}
|