Files
WP-Business-Forum/admin/forum-admin.php
2026-03-30 20:41:48 +02:00

4325 lines
334 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* forum-admin.php
* Backend-Verwaltung für WP Business Forum.
* Enthält: Dashboard, Rollen, Kategorien, Mitglieder, Meldungen.
* Einstellungen (wbf_admin_settings + wbf_get_settings) → admin/forum-settings.php
*/
if ( ! defined( 'ABSPATH' ) ) exit;
// ── Menü ──────────────────────────────────────────────────────────────────────
add_action( 'admin_menu', function() {
add_menu_page(
'Business Forum', 'Business Forum', 'manage_options',
'wbf-admin', 'wbf_admin_page', 'dashicons-format-chat', 30
);
add_submenu_page( 'wbf-admin', 'Übersicht', 'Übersicht', 'manage_options', 'wbf-admin', 'wbf_admin_page' );
add_submenu_page( 'wbf-admin', 'Kategorien', 'Kategorien', 'manage_options', 'wbf-categories', 'wbf_admin_categories' );
add_submenu_page( 'wbf-admin', 'Rollen', 'Rollen', 'manage_options', 'wbf-roles', 'wbf_admin_roles' );
add_submenu_page( 'wbf-admin', 'Level', 'Level', 'manage_options', 'wbf-levels', 'wbf_admin_levels' );
add_submenu_page( 'wbf-admin', 'Mitglieder', 'Mitglieder', 'manage_options', 'wbf-members', 'wbf_admin_members' );
add_submenu_page( 'wbf-admin', 'Meldungen', 'Meldungen', 'manage_options', 'wbf-reports', 'wbf_admin_reports' );
add_submenu_page( 'wbf-admin', 'Profilfelder', 'Profilfelder', 'manage_options', 'wbf-profile-fields', 'wbf_admin_profile_fields' );
add_submenu_page( 'wbf-admin', 'Einstellungen', 'Einstellungen', 'manage_options', 'wbf-settings', 'wbf_admin_settings' );
add_submenu_page( 'wbf-admin', 'Reaktionen', 'Reaktionen', 'manage_options', 'wbf-reactions', 'wbf_admin_reactions' );
add_submenu_page( 'wbf-admin', 'Einladungen', 'Einladungen', 'manage_options', 'wbf-invites', 'wbf_admin_invites' );
add_submenu_page( 'wbf-admin', 'Statistiken', 'Statistiken', 'manage_options', 'wbf-stats', 'wbf_admin_stats' );
add_submenu_page( 'wbf-admin', 'Papierkorb', 'Papierkorb', 'manage_options', 'wbf-trash', 'wbf_admin_trash' );
add_submenu_page( 'wbf-admin', 'Thread-Präfixe','Thread-Präfixe','manage_options', 'wbf-prefixes', 'wbf_admin_prefixes' );
add_submenu_page( 'wbf-admin', 'Wortfilter', 'Wortfilter', 'manage_options', 'wbf-wordfilter', 'wbf_admin_wordfilter' );
add_submenu_page( 'wbf-admin', 'Export / Import','Export / Import','manage_options', 'wbf-export', 'wbf_admin_export' );
add_submenu_page( 'wbf-admin', '🎮 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,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAOEA4QDASIAAhEBAxEB/8QAHQABAAEFAQEBAAAAAAAAAAAAAAECAwYHCAUECf/EAFgQAAIBAgMFBAUGCAkHDAMBAQABAgMEBQYRBxIhMUETUWGRCCJScYEUFTJCVaEWIyRTYpKxwRczQ3KCk6Ky0SVFVmPC0uE0NTZERlRzdZSzw/AmN3SDhP/EABsBAQABBQEAAAAAAAAAAAAAAAAGAQIDBAUH/8QAQxEAAgECAgYHBgUBCAIDAAMAAAECAwQFEQYSITFBURMUYXGBkdEiMqGxweEVQlJi8FMWIzM0Q5Ki8TXiBxclgrLS/9oADAMBAAIRAxEAPwDjIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAu21vXuq0aFtRqVqsvowpxcpP3JcRvKNpLNloGXYPs1zxisYStct3yhJ6KdaHZJfraGW4XsCzjcpu8ucMsUuSnWdRv8AVTNunY3NT3YPyOZcY1h9vsqVorxzfkjUgOhMN9HS2SpyxHM1WT+vChapeUpS/cZNh2wXIttPeuPnS94fRrXKiv7EU/vNyGB3ct6S736ZnIraaYVT92Tl3J/XI5VB2Th2yfZ7YvWjli1qP/X1J1vunJns2WTsp2U9+0yzg9CXtQs4J/sNqOjtZ+9NfH7HNqaf2i9ylJ9+S+rOHtH/APWfXQwvEq61o2F3UT6woyf7Ed1QsLGCShZ20EuW7Siv3F+MIxWkYqK8FoZ46Oc6nw+5pz/+Qf00P+X/AKnCyy9j0uWC4k/daVP8CfwbzB9h4n/6Sp/gd0/F+ZPn5l/9nIf1Ph9zD/8AYFX+gvP7HCv4N5g+w8T/APSVP8CHl7HlzwXEl/8A8lT/AAO6vi/MaeLH9nIf1PgP/sCr/QXn9jg6thWJ0U3Ww67ppdZUJL9qPjaaejO/JRjJaSimvFalidjZT137O3lr30ov9xZLRzlU+H3M0P8A5Bf5qH/L/wBTgrQg7jvsoZVvpb15lvCK8u+dnBv9h42I7Ktn19/HZYtIf+BKdH+40YJaO1l7s18fublPT+0f+JSku7J+hxqDqvEdg+Q7qWtCGJ2PhRut5f21IxvEfR0spOcsOzNcU1p6kK9speck1+w1p4Hdx3JPufrkdGjpphVT3pOPen9MzngG3sT2AZvt4qVneYXe8foxqypv+0tDEsZ2Y56wpVJXOXLycIc50Iqqn7t1t/caVSwuafvQfkdihjeH3GynWi/HJ+TMOBevLW5s67oXdvVt6secKsHGS+D4lk1GsjppprNAAAqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV0qdStVjSpU5VKk2oxjFauTfJJGwMqbHM7Y7uVamHfNdtLR9revcenhD6XmkZaVCpWeVOLZq3V9b2kdevNRXazXhXRpVK1WNKlTlUqTekYxWrb7kkdL5X2AZdstyrjt9dYpVWjdOH4mlr1XD1mvijZ2X8tYBgFJUsGweysVok5UqSU5ae1L6T+LZ2aGj9ee2o1H4v+eJEb3Tqyo5q3i5vyXx2/A5Py7soz1jcY1KOCVbWjJNqpePsV5P1vuNi4D6O8npPHMwRXFN07Ojrquq3pcvJnQWgOvRwK1p+9nLv+xFLvTXEq+ym1Bdi2+bz+hrzAdjWQcK3ZSwmWIVYvVTvarqecVpF+Rm+GYVhmGUVRw3DrSypR5QoUYwS+CR9gOpSt6VL3IpeBG7m/urp51qjl3tsjRdeJIBmNQAAAAAAAAAAAAAAAAAAAAFQAAARouhIAPlxDDsPxGjKhiFja3dKS0lCvSjOLXuaMKx/Y/kHFt+XzKrCrL+Usqjpae6PGH9kz8GKrb0qqynFPvRtW19c2rzo1HHubRoLH/R2ptyngWYZRTlwp3lHXRfzo/4GusxbIs9YNGVSeDyvaMVq6lnJVf7K9b7jsIHLrYFa1PdTj3fcklpppiVDZNqa7V9Vl9TgS5oVravOhcUalGrB6ThUi4yi+5p8UWzuzHcAwTHaHY4xhNlfQ0aXb0VJx17nzT8UayzPsDyvf79TBbm6wmq+UNe2pcuHCXrLzZyK+j9eG2m1L4MlVlp1Z1clcQcH5r1+BzADZOa9i+dcE36ttZwxa2jq9+ze9LTxg/W8tTXVzQr21edvc0alGtTe7OnUi4yi+5p8Ucatb1aLyqRaJdaX9teR1qE1Jdj/AJkWwAYTbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMtyPs7zVm+anheHShaa6Su7j8XRXufOXuimb3yTsKy1hChcY5UljN0uO7JOFCL/mri/i/gdC1wy4udsVkubODieklhh2cak85fpW1+PBeJztlXKeYs0XPYYHhVxd6PSVSMdKcP5036q8zcuT/R8S3a+acV16u2svjzqNe7kjfNnbW9nbQtrShSt6FNaQp0oKMYruSXBF4kdtgVCltqe0/gefYlpte3OcbddHHzfn6LxPBytk/LWWKajguD2trPTR1VDeqy983rL4a6HvAHahCMFqxWSIhVrVK0tepJtvi3mwAC4xgAAAALjy4gAHnYrjmDYVSnUxPFbKzhB6S7avGOj7tNTEsW2v5Cw+VSHzxK8nBfRtaMqil4KXCL8zDUuKVP35JeJt0LC6uP8Km5dybM+BpbEvSBwam0sOwC/udebrVY0tPgtdTGr7b9mKo5qzwXC7eLWkXNzqNePPRmnPF7SH5s+5M7FHRTFKv+nl3teuZ0bqu8lavlxOUrvbLn+4hKMcVoW+r11o2sIte5vU8W62hZ3uqbhWzTicoyerUaqj98UmasseoLdFvy9TpU9B72XvzivN/Q7H0a5p+RS5wXOcF75I4lq5gx6rJyq45ik5Pm5XlR/wC0fBWr1q0t6tWqVH3zm5ftML0gXCn8fsbkNA5/mr/8fudyVLyzpzcJ3dvGS6SqxT/aU/L7H/vtr/XR/wAThjReyvIaR9leRZ/aB/0/j9jL/YOP9f8A4/8Asdz/AC+w/wC/Wv8AXR/xKoXtnOSjC7t5SfJKrFt/ecLaR9leQ0j7K8iv9oH/AE/j9h/YOP8AX/4/+x3cpwfKcH7pIq0b6M4TpVatJ71KrOm++Emv2H30sfx2k06eN4pBx5ON5UWn9ouWkC40/j9jFPQOX5a//H7nbr1T4pr3jVPqcbWmf87WtPs6GaMUjHn61bf++SbPatNse0C3p7jxilccdd6vbQlLzWhmjj1B74teXqalTQe9j7k4vzX0Orwc32O33M1LcV3hGFXOn0nHfpuXk9EZNhnpBYTOTWI5evbdacJUa0amvwaX7TZhi9pL82Xejm1tFMUpf6efc165m6ga+wnbHkK/dOMsVq2U58N26t5Q3ffJaxXmZfhOYMDxamqmGYxYXkXLdXZV4y492mpu07ijU9ySficivh91b/4tOUe9M9IB8OfAGY1AAAACSABoeNmbK2X8yUeyxvCLW90WkZzhpOK8Jr1l8GeyC2UIzWUlmjJSqzpSU6baa4rYaGzh6PltUU6+VsUlRlzVteetF+CmlqvimaZzdkvM2Va7hjeE17enrpGulvUp+6a4fDn4Hb5buKNG4oToV6UKtKa3ZwnFSjJdzT4M41zgVvV20/Zfw8iW4dppfW2Ua/8AeR7dj8/VM4EB1XnfYhlbG9+4wlPBbyWr1oreoyfHnB8v6LRojPWzLNmUXOte2DubGL4XlrrOml+l1j8V7myOXWF3FttazXNHoGGaS2GIZRhLVlyex+HB+BhYAOcd8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9TLeX8YzFiMcPwXD615cS5xguEV3yb4RXizoLZ3sJwvDY073NdWGJ3fNWtNtUIeDfOf3LwfM3bTD692/YWznwOPiuO2eGR/vpe1witr+3ezSWR8gZnzjXSwiwatk9J3db1KMP6XV+C1Z0HkLYnlrAFTusWSxq/jo9a0NKMH4Q6++Wps+2oUbahChb0qdGlTWkKdOKjGK7klwRcJXZ4NQt/al7Uu30PMMW0uvb7OFN9HDkt7736ZFNOEKcI06cYxhFaRjFaJLuSKgDrkVAABQAHz317Z2NvK4vbqhbUY86lWooRXXmw2ltZVRcnkj6A2atzTtvynhe/SwtV8YrrVJ0fVpa9PXfNe5M1bmfbVnHFnOnYVaOD28tUlbR1qaeM5cdfdoc2vi1tR2Z5vs/mRIrLRbEbrJ6mouctnw3/AAOmMVxTDsKtpXGJ31vZ0opycq1RR4L3mA5h21ZKwyTp2le5xWono1a0/U5c9+Wia92pzHiN/e4jcu5xC7r3dZttzrVHN6vnz5fA+Y5FbHqstlOKXxJXZ6D20NtxNyfJbF9X8jceO7fcduFKGD4RZWEWtFOtJ1pp9+nCP3GCY7tBzljW/G+zBe9nLTWlRn2UOHhHT9pi4OXVvrir782SW1wWwtf8Kkk+eWb83myqrUnWqyq1ZyqVJPWU5vek/e3xKQDVOnuAABUAAAAAAAAAAAAAAAAAAAAAEwlKFSNSEnGcXrGUXo0/BkAAyXAs+ZwwVxWH5hvowjLe7OpU7WD96lqZ1gW3vMVqowxfC7HEYpcZ09aM2+nLWP3GoAbVK9uKXuTZzLnB7G6/xaSb55ZPzWTOocvbb8m4jKNO+ld4TUei/KKe9DV8/Wjrol3tI2Bg+MYVjFvGvhWI2t7TktU6NVS4a6clxOHi/ZXd1ZV1cWdzWtqyaanSm4S4eKOnRx2rHZUin8COXehFrPbQm4vt2r6P4s7o5g5Wy1tmzphDhTurqli1Bc43cfX011+mtH5m08rbcsr4k4UcXoXGEVpaJyn+Mo66+0uKXi0jr0MWtquzPJ9v8yIpe6K4ja5tR11zjt+G/wCBtYHy4biNhidtG5w68t7ujJJqdGopLiteh9R0001miOyi4vKSyYABUoCJJSi00mnwa7yQAazz5sZytmNVLixprBr+XHtbeC7OT/Sp8vLRnPmfNnGaMnVJTxGyday10heW+s6T975xfhJL4nZ5TWpU61KVKrCNSnNOMoyWqknzTT5o5F5g1C42xWrLmvQlGFaV3thlCb14cnv8H/2jgIHT+0XYbguMqre5bnDCL96y7Fpu3qP3c4e9arwOec1ZYxzK+IOxxvD6trU+pJrWFRd8ZLhJe4il5h1e0ftrZzW49NwrHrPE4/3Uspfpe/7+B4wANE7QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPcydlXHM2YosPwWylXnwdSo+FOkvanLkl976F0ISnJRis2zHVqwowc6jyS3tniQjKclGKbk3okurNwbMtiOK432OJZllUwvDpaSjQS/KKy9z+gvF8fDqbX2YbJcCyhCle3UYYnjCWruakfUpPupxfL+c+Pu5Gx+RKLDAksp3Hl6nm2N6bSlnRsNi/U9/guHe/JHl5ay/g+XMNhh2C2FKzt481BcZPvk3xk/FnqAEkjFRSUVkjz2pUnUk5zebfFgAFSwAGK51z/lnKdOUcTv4zutNY2lH16r96+rzXFllSpCnHWm8kZqFvVuJqnSi5N8EZUY9mvOmWssUnLGMVo0ai5UIPfqy90Vx6mgs77asyY06ltgyWDWb4J03vV5Lxn06fR8zWNarVrVZVa1SdSpN6ynOTbfvbOHc47GOyis+1k3w3QmrPKd5LVXJbX57l8Tc2cNvOIXKlb5Zw6NlB8PlNzpOp71FcF8dTU+O45i+O3UrnF8Rub2o3rrVm2l7lyXM84HAr3la4f95LP5E5scJs7FZUKaT5735vaAAax0QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD7sHxfE8GuldYVf3NlWT13qNRx1965M2tlDbxi9nu0MyWMMSpda9DSnVXPmvovp3GmwbNC7rW7zpyy+Rz73C7S+WVeCfbx8952TlHPeV80U4/NWKUnXfO2q+pVT/mvny6GTI4ShOdOcZwlKE4/RlF6Ne5myMk7ZM0YC6dviM1jNkuDjXelWK/Rn8eT16cjvW2Oxeyssu1EIxHQqcM52cs+x7/B7vPI6lBh+SNo+V82RhTsr1W961xtLnSFTXw6S+HcZgd2nVhVjrQeaITcW1W2m6dWLi+TAJBkMIPPx7BsLx3DqmH4vY0by2qc6dWOvHvXVPxXE9AFsoqSyZdCcoSUovJo5q2m7DL/DO1xPKUqmIWa1lKznxr01+i/rrw+l7+ZparTnSqSp1IShOLcZRktGmuaaO/TAdpmy3AM50p3LgrDFtPUvKUfpPoqkfrLx5+JHL/Aoyznb7Hy9Cf4LppOnlSvtq/Vx8Vx79/ecfAyLPOTMeydiLtMYtHGEm1RuIcaVZd8ZfufFdxjpF505U5OMlk0ekUa1OvBVKcs4vc0AAWGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuW9GrcV4UKFKdWrUkowhCLcpN8kkubOh9kGxSjZqhjecaMatzwnRw58YU+51Paf6PJddeRt2llVu56tNd74I5eK4xbYXS6Ss9vBcX/OZgmyjZDiubexxPFHUw7BXxVTTSrXX6CfJfpPh3anTmW8BwnLuF0sMwaypWltTXCMVxk/ak+cn4s9KMVGKjFJJLRJLgiSa2OHUrSPs7XzPHcZx+6xWf948oLdFbvu+3yyAAOgcQAFi/vLXD7Spd31xStremtZ1akt2Mfe2UbyWbKxi5PJF88DN+b8AyrZu4xjEKdGTT7OjH1qlR8eCiuPQ1LtE25NqpYZPp6c1K/qx/uRf7X5GksSvrzEr2pe391Vurmq9Z1asnKTOHeY1Cn7NHa+fD7k0wnQ6tcZVLt6keX5n6fPsNl58204/jTqWmBp4PZPVb8HrXmuP1vq/Djw5mratSdWpKpUnKc5vWUpPVt+LKQRqvcVa8tao8z0Wyw+2soalCCivi+98QADCbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMW4yUotprimno0bMyHtkzHgDp2uKyeMWEeGlWX46C/Rn19z8zWQM1G4qUJa1N5Gpd2NveQ1K8FJfzc+B2XkvO+Xc2WqqYVfwlXS1qW1T1atN+MXzXijJThWzubmzuad1aV6tvXpvWFSnJxlF+DRunZ5txuKDp2Gb6buKfJX1KOk4/z4rn715Eks8bhP2a2x8+H2PPsW0Pq0c6lo9aPLj4c/n3nQBJ8mE4jY4rY077DbuldW1Rawq05ap/8fA+o7qaazRC5RcXlJZMkgkgqWnxY5hOG43htXDsVsqN5aVVpOlUjqveu5+K4o5o2s7GsQy32uLZe7XEMIWsp09Na1uvFL6Uf0lxXVdTqQNamje4fSu45TW3gzs4RjlzhdTOk84vfF7n6PtOAAdL7X9i9ri6rY1lOlTtcRes6tmtI0rh9XHpCf3PwfE5vvrS5sbyrZ3lCpb3FGTjUp1IuMotdGmQm8satpPVmtnB8GevYTjNtilLXovat6e9fbtLAANM6wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPQy9g2JY/i1HC8JtKl1d1npGEF06tvkkurfBH2ZJyri+bsbp4Vg9v2k361SpLhTox9qT6L730OttmmQ8HyPhPyaxgq15VS+VXk46Tqvu/RiukfPV8TqYdhk7yWb2R5+hG8f0jo4VDVXtVHuXLtfZ8/ieNsj2WYZku3jfXfZ32Nzj69w1rGjrzjTT5d29zfguBsYAm1ChToQUKayR45eXle9rOtXlnJ/zZ2AAGU1QC3c16Ntb1Li4qwpUqcXOc5vSMUubb7jRG1HbVOcquFZOqOMfo1MQa4vvVNf7T+BrXV3Sto61R+HE6WG4Vc4jU1KMe98F3mw9o20rAsn0pUJzV5ibXqWlJ8V4zf1Uc2Z5ztj2b7x1sVumqClrStabapU/h1fHm/uMerVatatOtWqTq1ZvenOcnKUn3tvmygiV7iVW6eW6PL1PVcH0etcNSklrT/U/py+YABzjvgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHvZNzdjuU7/AOVYPeSpxb1qUJPWlU/nR/fzOkdm21LA83RhaVpRw/FdONtUlwn/ADH193M5QJhKUJxnCUoyi9VKL0afemdCzxGravJbY8jh4tgFriSzktWf6l9eZ3cDnrZbtouLKVLCs3VJ3Fs3uwvtNalPu3/aXjzN/wBhd219Z0ryzr069vWip06kJaxkn1TJba3lK6jnB+HE8txPCbnDqmpWWzg+D/nIvgA2zmAwLars0wjPFo67UbPF6cNKN5GP0tOUai+tH7107jPQYq1GFaDhNZpmza3Va0qqrRllJHCmacv4tlnGKuFYxaTt7im+vGM49JRfWL7zyjtzaBkzBs6YLLD8Uo6VIpu3uYL8ZQl3p9V3rk/vOR8/5NxnJeMyw/FaWsJau3uIL8XXj3xff3rmiE4lhc7R60dsefLvPXcA0jpYpHUn7NRb1z7V6cDGwAcokwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMk2e5NxfOuOxw3DKe7TjpK5uZr8XQh3vvfdHm/dq1OzvJuLZ1x6GG4dDcpR0lc3Mo6woQ733t9F192rXX+S8r4TlLA6WE4TQ3KUONSpL6dafWcn1b+7kjr4XhcruWvPZBfEimkmkkMMh0VLbVfw7X9F9C3kXKWEZOwOGF4TR3VwlWrS41K0/ak/3cl0PfAJtCEYRUYrJI8erVqlabqVHnJ72wAC4xg8jNmZMIyxhU8Sxi6VGjH6MVxnUfsxXVnjbSs/4RkvDnKvNXGIVF+T2kH60n7Uu6K7zlrOOZ8YzXi8sSxe5lVn9GnTXCFKPsxXT382crEMThbLVjtl8u8k+BaN1cRaq1PZp8+L7vUyDabtKxjOdd26crLCov1LWEvp8ec31fLhy4GCgERq1p1pOc3mz1a1taNpSVKjHKKAAMZsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzLZvtCxrJd2o283c4dN/jbSo/V4/Wj7MvuMNBkpVZ0pKcHkzBcW1K5pulVjnFnaWSs2YPm3Co4hhNxvrRdrSlwqUpdVJfvPeOJMr5gxXLWL08Twi6lb14cGl9GpHrGS6o6h2W7RsKznYqi5RtMWprStayf0v0oPqvvRLcPxSNz7E9kvmeX47o3UsG6tH2qfxXf2dpnQAOuRYHj5uy5hOacFq4TjFsq1CfGMlwnSl0lF9Gv8Ag+B7ALZQjOLjJZpl9KpOlNTg8mtzOLdp+Q8VyNjXyW7TrWVZt2t3GOkase590l1X7jETuzNOA4XmXBa+EYvbKva1l7pQl0lF9JLozkLajkPE8jY27W5Uq9jWbdpdqOkase590l1Xx5ELxXCnavpKe2D+B63o3pJHEYqjW2VV/wAu1dvNeK7MQABxSWgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyDIWU8Vzjj9LCcMhpr61atJepQh1lL9y6s+PK2A4nmXHLfB8JoOtdV5aLpGC6yk+kV1Z2Js3yZhuScvU8MsUqleWk7q5cdJVp9/gl0XReOrOrheGyu55y91b/QjOkekEMKpasNtSW5cu1/Tn5n1ZHyrhWUMBpYThVLSEfWq1ZfTrT6zk+/8AYuCPdAJxCEYRUYrJI8Zq1Z1pupUebe9gAFxjBrna5tOssoW87Cw7O7xmpD1KeusKOv1p/uXXzPh207UaOWKU8EwSpCtjM46VJ842qff3z7l05s5pu7ivd3NS5uas61arJynUm9ZSb6tnCxPFeizpUn7XF8vuTbRzRh3WVzdL2OC5/b59xexbEb7FsQrYhiNzUubqtLenUm9W/wDBeB8gBFm23mz02MVFKMVkkAAULgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX7C8urC8pXllXqW9xSlvU6lOWkossAqm080UaTWTOntj+1S2zRSpYRjLp22Mxjpva6QudPrR7pd68vDaRwlRqVKNWFWlOVOpCSlGUXo4tcmmdG7E9qsMbVLL2Yq0aeJpbttcS4Ruf0X3T/b7yUYZivSZUqz28Hz+55xpDoz0Gdzar2eK5dq7Pl3btwAA75CAeVmvL+F5nwOvhGL26rW1VcHylTl0nF9JLv+HJnqgtlFTTjJZpl9OpKnJTg8mtzOKNpWS8TyTmCeHXqdS3nrK1uVHSNaHf4NdV092hix3DnzKmF5wy/WwjE6fCXrUa0V69Gp0lH966rgcc52yxieUswV8HxSlu1Kb1p1I/QqwfKcX3P7uRCMVwx2k9aHuP4dh7Bo5pBHE6XR1NlSO/t7V9TxAAcgk4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL+H2d1iF7RsrKhOvcV5qnSpwWspSb0SRYOnPR52bPAbGGZ8ao6YpdU/yalJcbam1zfdOS8lw6s3bGynd1dSO7i+RyMaxelhds6s9re5c36czJ9jmz62yPgX45QrYxdRTvK647vdTi/ZX3vj3JZ4AT6jRhRgoQWSR4hd3VW7rSrVnnJgAGU1gaq22bUKeWaFTA8DqxqY1UjpUqLirSL6+M+5dOb6I+nbVtIpZUspYVhc4zxmvDh1+Txf1349xzBcVqtxXqV69SVWrUk5TnJ6uTfNtnBxXE+izpUn7XF8vuTfRnRvrLV1cr2OC59r7Pn3b4r1aletOtWqTqVKknKc5vWUm+bb6soAIqemJZAAAqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACYylGSlGTjJPVNPRp95AAOjth+1NY1GjlzMddLE4rdtrmb0Vyl9WX6f8Ae95uI4RhKUJxnCTjKLTjJPRprk0dK7DtpizFb08Bxuqli1KOlKq/+sxX+0uvmSjCsT18qNV7eD5nnOkujvQ53VsvZ4rl2rs7OHdu2yACQEHBhu1fIlhnjL8rWru0cQoJzs7nT6EvZffF9fPoZkDHVpQqwcJrNMz21xUtqsatJ5SW44LxnDb3B8VucMxGhOhdW1R06tOXNNfu6p9UfGdV7fdnCzXhXzzhNCPz1Zw+jFaO5prjufzl9Xy68OVZRcZOMk009GmuKIDiFjKzq6r3Pcz2rBMXp4pbqpHZJe8uT9HwIABonYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABley3Jt3nbNNHDKO9TtaelS8rpfxVNPj/SfJf8C+lTlVmoQWbZhuLinb0pVaryjFZszn0dNnfz5iEc04vQTwy0qaW1Ka4XFVdfGMX5vh0Z02j5sJsLTC8Nt8OsaEaFtbU1TpU48oxSPpPQLCzjaUlBb+L7Tw7GsWqYpcurLZHclyXrzAAN05AMI2uZ9t8lYKnS3KuKXMWrWi+KXfOS7l957GfM04dlHL1fFb+eskt2hRj9KrUfKK/e+iOQs0Y5iGY8cuMXxOq6lxXl38IR6RXckcjFMR6tHUh7z+BKtGsBeIVOmrL+7j8Xy7ufkfJiV9d4lf17+/uKlxdV5udWrN6uUmfOAQ9tt5s9YjFRWS3AAFCoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMVq0l1Mlw3L9vXs4Va7q06klxj3F0YOW4wV7iFBZzMZBl/4NWP52r9w/Bqx/O1fuL+hkav4nb8/gYgDL/wAGrH87V+4fg1Y/nav3DoZD8Tt+fwMQBl/4NWX52r9w/Bqx/O1fuHQyH4nb8/gYgDL/AMGrH87V+4vWOU7K4r7va1lFc3wKqhNh4rbxWbfwMKBsN5LwzjpWr+SMMx7Dp4ZiE7aUoy04pruKTpSgs2XWuI0LmWrTe088AGI3wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXbS4r2l1SurWtUoV6M1OnUhLSUJLk0+8tAJ5FGk1kzq3YxtCp5ywt2l84Qxm1gu3iuCqx5dol+3uZsM4dwLFb7BMXtsVw2vKhdW896Ek/NPvT5NHXWzXONjnPL1O/tmqdzDSF1QfOnPr70+aZL8KxHrEejqP2l8Ty7SXAupT6eiv7t/B+nLyMoAB2SKA5z9JLZ0rK4qZywahpbVp/wCUKUFwpzf8ql3SfPx49TowtXttQvLSraXVKFahWg6dSnNaqcWtGn8DUvbOF3SdOXg+TOphGJ1cNuVWhu4rmv5uOBAZvtiyLXyRmedvTU54Xc61LKq+OsesG/ajy8Vo+phB59WpTozcJrJo9ttbmndUo1qTzjLagADGZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC9ZWtxe3lG0taM61etNU6dOC1cpN6JI7J2SZKtsk5VpWCUZ39fSre1l9epp9FP2Y8l8X1NX+jFkPVvOuKUOWtPDoSXwlV/bFfF9x0DpoS7A7Do4dPNbXu7vv8jyzTPG+nq9Sov2Y+92vl4fPuABBIiCEnzYpfWmGYfXv76vChbUIOdSpLlFI+k5u9ITP7xjEJZYwm4l832s/yqUXoq1VfV8Yx/b7jTvbuNrSc3v4HVwfC6mJXKpR2Le3yX83GG7Uc6XedMxTvJuVOxo6wtKD4KMfaa9p/8DEgCDVKkqs3OTzbPaLe3p21KNKksorcAAWGYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFy3pTr1o0qcHOUnyQKNpLNnq5Xs/lF52st1wpfSjJczMD4cHtPkdlClLdc+rS5n2m9Thqoi97X6aq2tyJBALzUJBAGQJBAGQyDb1SXFvke3ZUVb26j9aXFnn4TQ7Wq60l6keXvPVb1ZkguJp3M83qIGO51wmd9aqtb04upT4y0XrSRkRDKzipLJmO3ryoVFUjvRpx0aibW5Lh4EdlU9iXkbe+S2v/AHel+qh8ktv+70v1EavVO0kH9oF+j4moeyq/m5eRS002mtGjb87K1lBxdCno1pwijXWbsKWGX6VOW9SqLWPeveYqtBwWZu2OLQup6jWTPEABgOuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADJNnebb7J2Y6WJ2jlOi/UuaHSrDqveuj/AMWY2C+nOVOSlF5NGKtRhXpunUWaexncWA4tY43hNvimG141ra4gpwkv2NdGu4+45h2CZ+llvGFgmJ15fNF7P1W3woVXylx5J8n8H3nTyeq1XUnFheRuqWst/E8fxnCp4bcOm9sXtT5r1XEkgA3TkmM7Sso2ec8rXGEXKhCv/GWtdrXsaqXCXu6PwZxhi+H3mE4pc4bf0JULq2qOnVhLpJP9nid6mjvScyIr2w/DLDKH5TbRUL+MVxnS5Kp748n4e44GOWHSw6aC2rf2r7E00Qxnq1bqlV+xLd2P7/M5wABDj1IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGUbMMpXGc83W2EUt6Nv/G3dVfydJNbz971SXi0Ywlq9DrnYHktZTyfC4u6W7imJKNa41XGEdPUp/BPV+LZ0cLsut10n7q2v+dpwNI8XWGWblF+3LZH18PnkZ9h1nbYfY0LKzoxoW9CnGnSpxWijFLRJF8kgnySSyR4k25PNkgg83NGNWWXsBu8YxCbhb21Nyei1cn0ivFvRFJSUVm9yKwhKpJQis29iMF2857jlnAXhWH1ksWv4uMdOdGn1n4PovM5cPVzXjt7mTH7rGcQm3WuJ6qOuqhFfRivBL955RB7+8d1V1uC3Hs+B4THDbZU/wAz2yfb6IAA0TsgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyDKtlvVHdVIPSP8AFy169Tx8Pt3c3dOipbu89NdNdDN7K3p2tvGjSWkV+0z0YZvM5uI3GpDUW9l8EA2yPkggAEjX/wC6EAAnUmMZVJqlHi5PQpbS4s9HB6G7F3M1xfCISzeRZUnqRzPuo01RoxpR6c/eVlLfHUnUz5HMe3aw2fPWrqGi6t6JFVeajEqynYvFsZdeovyW2erfRsvoUZ3FaNGnvk/+34GxQo6+17iLmNe0rQpXMdyU1rHXqVwlqjIc64bLEMM+U0I6XFv9HTqupiGHXKq00+T5Ndxt4pYOwuei/K9qfz8i+tQSWtE9E8nNFh8vwupBOEZQ9bea14LoeomHo1x6nPlHWWRr0qjpTU470acnFwk4voUmU56wpULhXlvR3aU/ptct4xY5c4OEsmT22uI3FJVI8QACw2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdK+jznxY3hH4OYnWTxKxguwlLnWo8lx9qPL3aHNR92AYreYJjFriuH1XTubaopwevPvT8GuDNyxu5WtVTW7j3HKxjDIYjbOk/eW1Pk/R8TuIHi5IzFZ5py1aY1ZP1a0dKkHzp1F9KL9zPbJ1CanFSjuZ47UpSpTcJrJrYwW69GlXo1KFenCrSqRcJwktYyi1o013FwFxYtm1HGW2LJk8l5vrWVJSeH3Gtaym3rrTb+i/GL4eT6mFnZG2nJkc55NrW1CC+crTWvZS75pcYe6S4e/R9DjmpCdOcoTjKMovRxa0afcQPFrLqtf2fde1eh7Jo3i34jaLXftx2P6Px+eZSADlkhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVShOrVjTpxc5zajGK5tvkgDZXo9ZM/CfOUb68pb2GYW416qkuFSp9SHmtX4LTqdZmJbJcpwyfku0wyUIq8qLtryS041ZLitfBaR+BlpPsLs+q0En7z2v+dh4jpJirxK9lKL9iOyPdz8flkCSCTpHAI5I5p9IvOnz1j34O2FVSsMOm+1lF8Klbr71Hl79e429tozgso5Rq1KE1843mtC0WvGLa4z+C+85JnKU5uc5OUpPVtvi33kdxu8yXQR47/Qnuh2E60ne1FsWyPfxfhu/6IABGT0UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEpavQg+7BbR3d5GLgpQi9ZpvoVSzeRZOahFyfA9nKtruUpXLfGXq7rXLQ93UohGMIqMFpFLRJEm/COqsiL16rqzc2VajUpBdkYsidSdSkDIZE6k6lJDei169AMi9bUnXuI01y5y9x7uijFQj9GK0Pkwuh2FvvyXrz/YfUZYLJHOrz15ZLcidSJPRA+e6qqEG29NC5vIxRjm8j5cQqVKk4W1BOVaq92KRsTAsNhhGE0rOC9drWo+9sxnZ3hjurqpjdzD8XDhQT7+pm71b1ZMtF8P1IO7mtst3d99/kb8mqa1F4luPB6NaprRrwNd5ow94LjjnBaWty96L6J9xsZo8/MeFwxfB6ts1+Ngt+m+5rodTHcO69bNR9+O1eniITW57mYXRmpIuHl4dVqRcqFZONWk92afeelF6o83g80adWm4SyZ8mNWdO+sKlGdPfemsFrpx6Grr23qWtxOjVi4zi+KZt0xHP2H78IX0W3Jeq4qP3mC5p6y1kdfBrzo6nRS3P5mFAA55LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADZewHOn4NZmWGXlVRwzEpxhNyfClV5Rn4a8n8O46lT1Wq0fuOD1wOqtgucvwnypGzvKmuJYco0qzb41IfVn5cH4okmB3n+hLw9CBaXYVuvKa7JfR/TyNjgAkhAgzlr0lsmrA80xx+yo7tjisnKe6tFCuuMl/S+l79TqUx3aNli3zdlC+wStuqpVhvW9Rr+Lqx4wl58H4NnPxKz61QcVvW1d528BxN4deRqP3Xsl3fbecQAvX1rXsrytZ3VKVKvQqSp1IPnGSejXmWTz9rLYz2lNNZoAAFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbX9GrKPz5nJY1d0t6ywjSqtVwnXf0F8OMvgu81TFOUlFJtt6JLqdn7IMrLKWRrHDqlNRvKse3u3px7WS1a/orSPwZ18FtOsXCk90dvoRfS3E+pWLhF+1PYu7i/LZ4mYIAE5PHAU1Jxp05TnJRjFNybeiSKjWHpEZseAZQ+arSq4X2K60k0+MKK+nL48I/Ew3FaNCm6kuBtWNnO8uIUIb5P/t+BpHbDm2ebs5V7qnJ/IbXW3tI8dNxPjL3yfHyMMAIDVqyqzc5b2e321vC2pRo01korJAAGMzgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAExTlJJJtvuMsy/ZO2tt+rBKrLr10PAwRUXf03VlJPe9XRc2ZkbNCH5jk4lWaypokEA2jj5E6ggAZE8AQAMiT6MNoO4uU2vUhxZ8r1bUVzlwR7tnRVvbRp/WfGTLorNmCvU1I7N7L7er4cug1KWRroZTnZCpLRHwKhWxXFKOGW+rdSXrtfVRcxC4jRoynJ8EZbs3weVpYTxe6j+UXPCGvOK6GzYWTvrmNHhvfd9zapJU4uo/DvMitrWlZWdKzoRSp0opcOr6lehc0I0PUYJQiorcjBr5lvQmPqyUu4r0IaLsy5SMB2hYW7G/p4zbR/E1npVS6SZ8FvUUopp6o2Nf2lLELCtY10nCrF6eD6GsYWtfDb2rh9x9Km9I+KPPdIMP6rcdNBezP4P7/Mzyaq0+1H3plm8oK5tqlBy3VOOmvcVp8CTiZZmom4vNGrsdw6pht9KjPRp8YtPmu8882BnTDldWXyinCHaU+Mpt8d0wBrR6M5danqSyJvh911mipPet5AAMRvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAybZlmmtlHN1risZS+Tt9ldQX16TfHy5/AxkF9OpKnJTjvRirUYV6cqc1mmsmd22telc21O4oTjUpVYqcJRfBprVMuGofRpzY8Uy5Uy5d1d66wxJ0dXxlQb4fqvh7mjbxPravG4pRqLieMYhZzsridCXB/DgwOgBnNM5l9KPKfzZmKhme1p6W2JepX0XCNeK/wBqK198WaYO3tpOWqWbMmYhgtRR7WrT3reb+pVjxg/Pg/Bs4luaNW2uKlvXg6dWlNwnF84yT0a8yE43adBX11ult8eJ6zoliXW7PopP2obPDh6eBbABxSVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGxvR7yusx7QbavXp79lhml3W1XByT/Fx+MuPuizrpGsfRwy18x7P6V9Xp7t1isvlMtVxVPlTXl639I2cTvB7XoLZZ75bX9DxjSrEeu4hJRfsw9leG/4/DIAA6pGympOFOnKpUlGEIpuUm9EkubZx5tWzPPNmdbzEoyfyWD7C1j3U48E/i9X8TfnpC5nlgGSJ2VtV3LzFG6EGpaSjT+vLnry4a+JywRjHbrOSoLhtf0PRNC8O1YSvJra9i7uL+ngwACPE8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqc5QmpwbjJcmuhmOFXlO6touMtZpaST5+8ww9DBLp215HjFRn6snLojLRnqs072gqsM1vRl2vuGviUqSaTT1T5Mk3iP5FRGviQNSoyJ1GpBNOEqtWNKPOT+4A+7B6HaVHcTXqx4RPUbberKKcI0qUaUOCiuJJlSyRy6s+klmVFFSWiJb0R52J3EoQ3afGpN7sF3spKWSzFOm5yyR9eX8Olj+Yadt/1ag1Oq+jXcbXlGMVGnTSjCmt2KXgePkjBVgmAwU1+VXC36jfPR9D2tCe4DYdUt9afvS2v08DFc1lKWrHci3p3hlehGh3czApFGhDRc0EYOUlFdSuZcpHzX91Rw/D61/XaUKUW14y6Gq1eVsSvquI3D9apLWPgj2tpuL/LL+ngdrP8VR41musl0PJy1Y1cZzDa4JaJ/jJJ1JL6kOrIBj2IO7uVQh7sfjL7G+kqNBzns2ZvsSPojJaE6l/OeFVcrZmeEVm5UXHepVH9Y+WEtUcZpxk4vejVhKNWnGrB5xks0+wmrCFWnKnUipRktGn1NeZsw1WGIN0oz7KXFNrgvBGxDyc0WKvcNlwnKdP1oxj1Zgr09eJ0sMunQrLN7Ga3BVVg6dRwlzT0ZScsmS2gAAqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAe/s+zHWyrm6xxmk24Uqm7Xgvr0pcJry4+9HZtpcUbq1pXNvUjUo1YKdOcXqpRa1TOEzpn0aszyxbKdTA7mpvXOFNRhrLjKjL6Pk9V5HfwK61Zui9z2rvIXpfh+vSjdRW2Ox9z3eT+ZtgAEpPPAzlP0mMsfMmefnWhT3bTFoustFwVVaKov2S/pHVhgW3nLP4TbO72nSp795Y/lltouLcU96Pxi5L36dxzcWtesW0kt62o72jmIdSvoyb9mWx+Po8jjsBggJ7IAAAAAAAAAAAAAAAAAAAAAAAAAAAD3cg4BUzPnDDMEhru3NdKq1zjTXGb/VT+Oh4Rvr0Tcvb93ieZq1N6Uoq0t5NP6T9abXw3V8TcsLfrFxGnw49xy8avuo2NSst6WzvexHQdvSpUKFOhRhGnSpxUIQitFGKWiS+BWAeiHhL27QAYvtTzAss5GxLFE/x/Z9lQ/wDEnwjy4+JZUmqcHOW5GWhRlXqxpQ3yaXmc6bdsy/hFn66jRq9pZYf+S2+j4PR+vJcdOMuvckYETKUpScpScpN6tvm31ZB5/WqutUc5b2e5WltC1oQow3RWQABiNgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyjAr93NLspx9emua5aHqHh5Zt5RpzuN5aS9XQ9o6NJtwWZHbqMY1WolQKdQZDWyJbSPUwahuU3cTXrS+iefZ0Xc3Maa+iuMj3nokox4RXBF0FxNW6qZLUXEkEakSlojIaWRbrzUY8Xoffs6wh4zjksTuIP5JaPWOvKU0eHdqteXNLD7VOVa4koLTpr1Nw4HhdLBcGoYbRS1jFSqtdZdTp4NZdbudeXuw+fDyL61Tq9L90vkfVN78nLp0Xcilor0GhP8AM5KkW2g0V6EdCuZVSKGjzM04rTwLAq15NrtZrcpLvb6nrwipS4vSKW9J+CNS56xj59zE6NKWtlZvciukn3nIxrEep271feexfzsNyzp9LU27ltZ4LqzpUat7cPer1W5PXrLuNz7EctPCMBqY7fU/y6/401JcYwZrjZ3l+ebc30rdxfyCzaqV5dHJdDoWpuerTpRUaNNbtOK6IimDWmvLppblu7+LONpfimpBWUHtltl2LgvExPaxlv8ACTKs69GOuIWP42m1zkl0NKYVcutQ9fhUg92on0l1Ol6M+zqKTWsfrLvRovazl55YzYsQtoP5uxB68OUZvmZcYtdWSrx8TV0UxDWTsKj7Y/VfVHmpklqjNOPPUuanJTJS1kzCM74d2N0rqChGFTgoxXHXvMZNo4xafLbGpQSipterKS5GtsQtatpdSo1YODT5M5tzT1ZZoleE3XS0tST2r5HzgA1jrgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzDY/mR5Yz5YXlSpuWleXye648Ozm9NXx04PR8eXEw8GSlUlSmpx3ow3FCNxSlSnuksjvFNNJppp8U11BhWxTMTzJs/sbmrJyurZfJrht6tzh11fPVaPzM1PQKVRVYKcdzPFrm3lb1pUp74vIETipRcZRUotaNPk13EkmQwo4n2r5deV8+4nhUYuNBVe1t/GlP1o+WunwMVOivSzy722H4XmehTblQk7S4aX1Jayg34J7y/pI51PPcSt+r3MoLdvXcz2rA77rtjTqvflk+9bPjv8QADROsAAAAAAAAAAAAAAAAAAAAAAAADtLY/gH4N7PMJw+dPcuJUVXuE1o+0qes0/FaqP9E5U2WYF+EefsIwucN+jO4U663dV2cPWlr4NLT4nbK5cOC6En0dt/frPuXzf0PO9O73ZTtU/wBz+S+oABKTzkHPfpTZgVfE8Oy3RmnG2i7quk0/XktIp92kdX/SOgq1SFKlOrVko04RcpyfRJatnFeecbnmLN2J4zOTaua8nT1eulNcIpeGiRxccr9HQVNb5fJEu0OsumvHWa2QXxexfDM8UAERPUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfRYWtS6rdnTXvfcfOuZkuAWnY0O1nDdqS6680ZKUNeWRr3NbooZreejQpqlSjBJcFo9EVkA6ORH3teZJDeiB9WF0O3uN+S/FwCWbLZSUE5M9HDKHye2Tl/GT4s+gN6vUjUzJZHJk3J5sk+e7rRpU5Tk+EVqy7OWiPlsLGtj2PW+E26bi5KVWS6R6lGpSajHa3sRkpQTecty3mW7JcEdSdbMd7Dn6lumunRmwHrJuUub5k0bajZWtGxt4qNKhFQSXXxJaPQMOtI2dCNNePecO5uXXqOfDh3FGhGhc0I0N7Mw6xRoRoXNCJTpUKNS6ryUaNGLlJsOSSzZcpGLbSsc+ZcC+S28vy284RS5xXU1PNToW8LeinUuK8tyKXNuXU9HH8VqY9j1xitZ/iYtqinyUVzMt2IZaWNY/UzHf09bGye7R1XCcujPPb64nid37O7cu7i/H0O/Uq08Ls5Vav5dr7XwRsnZvluGVcp0baUV8uukqlxPrr3GQIqqzlVqSnLmykk9ClGjBQjuR5DXuKlxVlWqvOUnmweXnDAqOZ8sXOE1ku13XK3k+ameoTGUoTU4vSS5F1SnGpBwluZbSqzozjUpvKUXmu85msHXtq9bDruLhcW03TlF89Fw1PSi9UZbt3y58kvKGbsPp6UqmkLtRXLTqYVa1VUpxnF8JLVENqUnQqOlLh8j1m2u4X9tG6hx3rk+KPoMVzth29peUoSb/lJa8EuhlOpZvbeld28qFZNwlzMVWGvHI3LSu6FVTRqsH345ZOyxCpR3t9J89ND4DkSTi8mTWnNTipLcwACheAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbf9GDMCsM03WA1pqNLEqe/T1aX42H7W468PA6SOHsuYpXwTH7HF7dtVLSvGqtHpqk+K18Vqvidt2F1RvrGhe201OjcU41acl1jJar7mSzArjXoum/y/JnnGl9n0VzGulsmtvevtkXgSDuERMf2iYHDMeSsVweUVKVxby7LVa6VI+tB+aRw/VhKnUlTnFxlFtST6Pqjv98jjTbfgSy/tLxW1p09y3r1PlVBKOi3KnraLwT3l8CM6RUM4wrLuf0+pPdCbzKVS2b3+0vk/oYSACKnoYAAAAAAAAAAAAAAAAAAAAAAABvP0S8FVbF8Wx6pBNW1KNtSb1+lPjLT4R+86NNdejrgzwjZdYVKkXGrfzneTT7pPSHw3YxfxNinoGFUehtYLi9vmeJaSXfWsSqy4J5Lw2fPNgAHQOGYNtzx35i2b4jOnU3Li8StKOktJaz+k17o7zOSDd3pVY12uJ4TgFOo92jTlc1oqXDelwjqu9JN/E0iQ3Ga3SXLjwjsPWNE7ToMPU3vm8/ovXxAAOSSYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVOEqlSMIrVt6IFNx9mEWiurjdm2oxWr4c/AyeCUIRjFaKK0R8mF2rtLbck9ZPi/A+vU6NGnqx27zh3VbpZ7NxOo1IGplNUn1pSUIrWUuCPftaKt7eNJc+cjz8Ft96TuZrgvo6nqa6syQjxNG6qZvUXABsalurLRFxqpZny4jcqjRlLXjyiu9mydlOAPCcFlit1DS9vfWimuMYswfI+CzzNmmEJJ/IrR79V9HJdDdVRwbUaa3acVpBLojt4FadJN3Ety2L6s1MUuOigreO97X9EWd0aFzRDQlescLXLe6Roy5oNCusVUy3o29NDAdsOOOjQpZbsqmlWtxuJRfJdDOMYxGhguD3OK3LShSi9xP6zZoS5vat5dXWMXsm6taTfHouhH8fv+jpdBB7Zb+xfzYdnCLfpJ9K1sju7X9ibWxuMSxC0wLD4N1riSXDpH6x0zgeE22X8CtcEtIpQoQSqNfWka+2BZZla2dbN2IUtK9xwtYSX0Y9TZj1b1b1ZoYTa9HDpZb38iL6WYp1m46rTfs09/bLj5bu/MgAHbTImQCQVTBavbG2xXDrnCbyKlQuYOL1+r4nN97h9zl3MF3gV4mnSm5UpP60W+B0o0YBtyy1LFsFp5isaet/YcaqiuMo9Dk4ta9JDpY74/Ik+i+JK2uer1H7FTZ3S4Px3M1pCWqRUfFh1zG4oRqReuvB+D6n2pnAi81mT2cHF5M8TNWFzvrdVKWjnTT9XT6XxMCknGTi+aejNrySlFp8mtGYBmjDnZ30pwpqFKb1gk9TSu6X50d3B7r/Rl4HjAA0SQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6q9HbHfnfZ1b2tSpv3GG1Haz1lq93nBv4PT4HKpuD0W8a+SZtvcFqVGqd/b79OLlou0g+7q3FvyOpg9borlLg9hH9JrXrFhJrfHb6/A6SABNDysGgfS4wRSo4NmKnFaxcrOs9OLT9eHwXr+Zv4wrbdg7xvZljFtCEp1aVL5TSS571N737EzRxKh01rOPZn5bTrYHddVv6VThnk+57DjIEvmQeentIAAAAAAAAAAAAAAAAAAAAAL+HWtW+v7eyoJOrcVY0oJ+1JpL72WDOtg2FrFdqWD05RjKnQqO5mpLpCLf7dDLQp9LVjDm0jWvK6t7edZ/lTfkjrzCbKjhuFWmH0IqNK1oQowiuijFJL7j6guQPSkklkjwGUnJtveBprw7weXm3E44NlfFMVlrpaWlSqkubai9PvEpKKbfAupwc5KEd72HJu1vF1je0XGb6MlKkrh0aTUdPUp+qv2MxUmcpzk51JOU5PWUnzbfNkHndSbqTc3x2nulvRjQpRpR3RSXkAAWGYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHq4Da9pVdacVKEeWvRnnW9N1a0YJPi+OnQym1oU7ekqdNe995sW9PWeZpXlbUjqrey+gQDfOOTqVUacq9aNGPV8S22ktWevg1v2VF15r158vAJZsx1anRxzPuhGNOnGlFcIoENgzHKJb0POxKtPdjQorerVpKEIrnxPruKihBt8ElqzINkGALGMbqY/eQ1s7R7tLXlJ9GVp0pV6ipR3v5F7qQt6cq090fi+CM8yLgMcuZZo2rX5XXSqV5dde49rQuTk6k3OXNkaE6o040aahHciG1K8qs3Oe97SjdI0Lmg0MmsWa5b0JhTlOahHmyrQ8XPmOwy1letea/lVdOFvHrqWVq8aMHOW5GWlGVWapw3vYa92v48sUxmGA2c9bS0etZp8JN9DwsnYBWzbmy1wWin8lpSUrmS5JLijw5VJ0LedxU1qXNeWvjJyZ0Hseyv+C+Uo3NzH/KeILfqSfOK6EIpqV/cuc+O193BEjxe+jguH5U37T9mPfxl4ehl3Z0KFGlZ2sVC3oRUKcV7uJSSCTReWxHkiIBJBemVIBJBcmAVU3TalSrRU6NROM4vqikhl29ZA56z3gFTKGcq1kk/kN3LftpdNXxaPnpy1RuraflqOaso1aVOKV/aJzt5Lm+rNC4VcyqU3CrHdrU24Ti+aa4ETu7fq1Zx4Pd6HqmC4j+JWanJ+3DZL6PxXxPTPPxvD6N9ay39Y1Ixe7Jc14H3p6oGCUVJZM6dOcqclKO9GrK1OVOpKEk00+qKDI842HZXbuIb0lU9aTa4LwMcONUg4SyJpb1lWpqaAALDOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD3MgYv8xZ1wjFt5Rhb3UHUbjr6je7Lh/NbPDHHo9H0fcXQk4SUlvRjq041YOEtzWXmd4pprVPVdGSY7s1xX57yFguJNyc6tpBTcue9Fbr1+K1+JkR6JTmpwUlxPFK1J0qkqct6bXkC3c0adxb1KFaKlSqRcJp9YtaP7mXAXNFi2bUcG5lw6phGYcQwuqkp2lzUovTl6smjzzZPpJ4WsN2q3tWMYxhfUaV1FJd63ZecoSfxNbHm1zS6GtKHJs9ysLjrNtTq/qSfwAAMBtgAAAAAAAAAAAAAAAAAA3h6JOGdrmDGMWlGLjb20KEW+alOWvD4RZo86h9FPDvk2z+6v5U92d5fS0l7UIRUV97mdXBaeveR7M2RvSyv0OFzS3yyXx9Ezb4AJ2eOA1p6SWI/Idmda2W9vX1zSocHpwT33/dS+Jss0B6V+Ib19gWFLf8AUp1bmXH1XvNRXx9VmhidTo7Wb57PPYdrR636fEaS5PPy2mjgAQY9iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXFg9bLdgru8UqsJOlHi3017jJSpSqzUI8THVqKlBzlwL2AWVXjcbk0+S4cGj2Oyrew/I9aKUIqMUkkuCXQOTJFTw6EI5axGqt5KpPWaPJ7Kt7D8h2db2H5HqtsplJ8lzfIv6hHmWKu+R8VjaVbi6jFwe5HjJnuuE+CUOC4I+iwo/J7ZL68uLL2rLlZRXE51e5dSWxbEfBuVPZZEo1EuTPvbPkvriNKjKcnokhK1ilnmWQm5PJI8ypb3WJYjb4TaQbrXE0uHSOvE39g2FW+BYLbYRaxSjRgu0a+szCti+BSp0K2aL2n+NrcLaMlyXU2Dzbb4s6mFWipJ1XvfyOHjd7r1Fbw3R39r+xRoNCvQJHX1jhaxToRoVtBrRajWGsRShGUm6j3acVrOXcjRG0THp5mzXUnTb+QWb3aKXJyXNmx9sOYHgmX1hNrPS/v/AFZac4xfU01Tt69SVthNjFzu7qajHTv6s4OLVul/uU9i3/REr0ftNWLup8di7FxZmGxnLP4UZu+crqGuF4a97V8pvuN/16na1XLRJJbsUuiXI8vKGAW+VcrWuC28Uqm6p3El9Ztcj0i20oqhDLi95AsexX8SvHUj7kdke7n47ykEg3VI45SCQy9MEEEgyJlSCCQXpgmlN0qinH3P3dTRu2nLqy9maGPWcNMOxB61NOUJG8D4cy4NbZky5d4JdRT7SDlSk/qyS4GpfWyuKWXFbjr4Jibw67VV+49kl2c/Dec90Z70U9S7r4nmUKdzh1/cYRfJxubWbjLXqteB98XqjiQt4yW89TqQSex5rh3FF7Rhc2s6M4xlvLgpctTXN/bTtbmdGemsXo2uRsk8HNWHQq2zuKVN9qnxUVz8Wat7Y5w1ovajo4Xc9DPUluZhgJa0ejIOEScAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6Y9FzEvlWRLrD3vb1ley01evqzSlw7uOpto5x9FTEVRzXimGS3/yqzVSOj4J05dV7p/cdHE3wmp0lrHs2eR5RpFQ6HEKnbt8165gAHSOIc/8Apd4ZrHAcYjGPDtbWpLq+Uo/sl5nPp1n6TWHK92YV7hUt+dlc0qyfWKb3W/KRyYQbHaepdt80n9PoesaJ1+lw6Mf0tr6/UAA45JQAAAAAAAAAAAAAAAAAAdm7ELBYdsqy/QX8pbfKH/8A6Sc/9o4zgnKSik23wSXU7ywC1hY4FYWVOO7ChbU6aXclFIkejkM6s58ll5/9EE06rZW9KlzbfkvufaACWnmg6HK3pG3zvNqN5RVTejaUKVBL2Wo7zX3o6qXFpPvOLNol985Z7xy93d3tL6rovCMt1fdE4WPTyoRjzZMNDKOtdzqco/Nr0Z4IAIoelgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVODnNQitW3ojOMDtI2ljFaSUprWSfRni5Sst+u7qe61DhutdTKWzv4Vbasellx3HBxS51pdEuG8MhgpZ2TkpE6n04XQ7at2sl6kP2nyJSnJU48XJ6HuUaaoUI0o9OfvBirz1I5Ley5J6vUpBS3oVNFIipLRHz4JhVbM+Z7fB6KfZKSlcS7kuJYxW6VChKS4t8El1bNs7J8uPActq+uo/wCUL9b0m+cV0McIdNUUOHEpd3PUrd1fzPZHv5+BlKpUbejSs7aKjQoRUYJe7iEitIaHbzy2EHcylIaFehOhTWLdYt6CVWhaW1fEbqSjb2sHUm3yfgXFCUmoxWspPSK72a1285h7Glb5Qw+prOelS7cXyi+hr3FwqMNbjwNqxtpXtxGhHjv7Et7NdZixirmDMN5jl3JqkpOFFPkorkzYno+5XdetXznidL1Y+rZwkuq6o11l3A6+ZMx2WW7NPs218okvqw7zqGha2+HWFthVnCMbe1goJJc5Lmzh0vannL+M7mlmJxs7WNlQ2Oa8oL//AF8hOTnJzk+MnrxIKiNDdUjzVFIZPQhmRSKkEFRBlUipDIJBkTKkEMkGRMqUsmLcZKSfFPUEGRMGqtv+WmlQzjh1LWcPVu4xXPXqa7tK0KtKM4STi1wZ0xWt6F9Z18Ou4KdvcwcJJ974JnNOOYRcZVzTdYDcJ9nGbdtJ/WjzOTdU+iqay3P5no2iuI9atnaVH7dPd2x/9fkXSmrBVKcoS10ktHoIPValRj3okm5mBZhw/wCQ3e7GScJLWPeeYZ7j9i76zcKaj2i4rVcX4GCVYSp1JU5rSUXo0Rq+t+hqZrcyU4fc9NTye9FIANE3wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADOdhF/8g2q4NJ1NyFec7eX6W/BpL9bQ65XI4dy1evDsxYbiEYqTtrulVSfXSSO4k0+K5Plr3EqwCedKcOT+f/R59phSyuKdTmsvJ/cAA75EDG9qNgsS2eY9Zt6b9jUkn4xW8v7pxE+Z37dUYXFtUt6q1hVg6cl3prR/tOCL+i7e+r28ouMqVSUHF9NG1oRTSSGU6c+9HoOhNXOnVp8mn55+hYABGSdAAAAAAAAAAAAAAAAAAHq5Qt3dZqwq2UXJ1b2jDTv1mjutLTgunA4w2K0PlG1PL1Pd3l8sjJrwim/3HZ6eqTJbo5HKlOXb/PmeaadVM7ilDkm/N/YkAEjIKWb6q6FlXrrTWnSlNa8uCbOF69apcV6lxVk5VKs3ObfVt6v72dnbR687XIGP16bSlHDq+jfRuDX7zi5ciM6QS9qEe89B0Jp5U6s+bS8s/UAAjpOQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX7G2qXVzGjTjq2ywZTlWx7ODuqtNqT+g/A2bS3deqo8DWuq6o03Liexh9v8AJbWFHg3FcWlzL7YbKWS6MVFJIizbk82SUt6cSRQpuvXjTXvfuKjYtrPuwihzuZrwife3x1ZCShFU4rhFaEcyuRzZyc5axLZZrT0TLknojy8SqVqtSlZ2icrm4koU0uepZUlqrMvo09eWR72zXAXmjNir1464bYNTnJ8pPuN4VZKpPVLSMUoxS6JcjysmYBRyvli3wuml8oqRVS4l1bfHQ9VI3LWn0cNu97yG4tiCvK7cfcjsj3c/EhInQqSJ0M7kclyKdBoV6FdGi69aFGPOb0b7l3lmuWueW1nw4zittl7L95j920o0ItUYv61ToczXuI17u4u8evW6lzczc4p89H9Uz7brmVY5mKjlrD6n5Bh3GtKL4Tqo8nZTlp5uzlTdSD+asOaq1Hp6spL6pxbmv0ktbgtxOsFoU8MsJXtzsclm+yPBd8jZ+wzKksu5ZljN9T/yliK1TfOEHxRnSX/Eu1XFyUYR3acFuQS5aLkUNFkJaqyPMr29qX1xO4q75PyXBeCKdCCoaGaMzWzKSGirQgyxkVKWQVMhozxkVKSCogzRZUgglkGVMqiASQzKmVIZg227K7zBlmOL2UP8o4fx1S4zh1M5K6MoqTjNKVOotyafLdfMtrUlVg4s2rK8qWVxC4pb4vzXFeKOW8Nulc0I1NNG+ceqPtTPv2n5dllHOdR0otYZiDdWlLThF9x5lOWqONBtNxlvR7BCrTuKUa9L3ZLNenhuK2zEM12HY3HyilTahPjKWvUy4+bEbSne27pVNV1T7mYruh09Nx4m3Z1+gqKXDia8BevKMre5nRlzi9NdOZZIq008mSxNNZoAAoVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG84+tF6OPFe9HcOV7mV5lrC7uo051rOjOTT11bgtTh9czsPYzXlcbLcvVJvWSs1BvXubRINH5ZVZx7P58yH6YU86FOfJtea+xlxIIJSQAM4h2o2rs9o+Yrdx3VHEaziv0XNtfc0dvPkce+kNbuhtexv1dI1HRqR8daMNX56ke0ijnQjLk/oTHQueV3Uhzj8mvU1+ACHnpIAAAAAAAAAAAAAAAAABsj0b6Uau1jDXJa9nSrTXvVNnXK5HKnot2/bbTHWf8hY1peekf3nVZNNH1lat9r+h5TppLWxFLlFfNgAHcIiYRt1uHbbKMdlHVSnShTWn6VSCf3anIp1T6R9w6Gyy8h+fuaFL+05f7JysRHHZZ3CXZ9WemaHQysZPnJ/JAAHFJaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUm2kuoB9mD2nyy9hTbajrq3oZ1FKMVFdFoeZl/D5WVtvVGt+ok2u49MlOHW3Q0s3vZGr+46apktyDKdSWUnQNJCT4Hp4ZQ7Ki6sl68+XuPhsaPyi4Sf0I8WexJ9FyXIqka9xP8iIYIZTOWiKmqkWrmrGEG5PRLmzK9h+X/AJwxKvmy/p/k9vwtYSXByXMwulZXOP45a4DZJyqXE0qjX1Y950TY2NthOFWuD2cVGjbQSenWfUx0odLUze5HOxu86rbdDB+3P4R++4uybqTlOXOT1CRKRUkb0pEHciEipLgVKJKXAxOZjcijTRNs8XaNmKnk7Jte+bXzhep0LWOvHjyZkVtThOo5VZKFGlHtKknyUVzOdtpeZJZxztXrQk3hmHt0bePR/pGrcVXlqLe/kdXA8P8AxC6Sn7kNsvovF/AxilRuZRVPebv7+po5N/yjOmdnWWqOUcp0MOp6O5rpVbife3zOcbuDq0G6E0qseNOa+rLvN8bF82LNGVY211P/AClYfi6ifOUV1ObcrUceR39M1cVLKM4e4pe0v/6vuRmmmgaKuncRoYVUPMsyjQhor0IaM0ZlSghlTRBsRkVTKWQ0VMhmxGRcUMMlkGxFlSCCWDPFlSkglhmWLLiCPAlkGVA8HaLlulm3KNxYOK+WW6dW3l11XJHO2HVasXUtblONzby7Oqnz3jqinN06sai6PXTvNJbeMsvBsdo5osKf5HeercRiuEZvqc6+parVVeJN9D8S1ZuwqPZLbHv4rx+ZiiY1S1lLhFcWWqE1OCcXqnyfefLjFxuU1bwfrS+l7jWT2Zk5jTcpapjmZPx9y7lcm91JLuPGMlrU1Uoyp66arTUx+6oyoVXCXwI/iVBxn0i3Mk1pNaupyLQAOYbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOrPRxruvsqsYttujcV6fHu3+H7TlM6d9F647XZ1XofmMQqR/WUZfvOzgbyusux/QjOlcc7HPlJfU2qCSCYHm4OVPSnoqntPjNL+Nw+jN+c4/7J1Wcy+lrbKGdsMuvzuHKH6tSX+8cbHo52j7GiTaJS1cRS5pmlwAQc9TAAAAAAAAAAAAAAAAAANy+iZFPPWJS9nDZf+5A6cOZvRKX/AOaYt/5b/wDLA6ZJxgX+TXezyPS//wAnLuXyAAOwRg1V6UNRx2cUaen08So8e7SMzmM6Y9KT/wDXtr/5lT/uTOZyHY3/AJrwR6lokv8A85d7+gAByCTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9bLdl8pu1UnCMqcPpJnmUacqtSMIJtt8kZzhdlSs7aMYL1mvWb5s6OG2vTVNZ7kaF/cdFT1VvZ9XBJJclyAZDJSRwgiXRLm+CDZ9WGUe0qutJerHkMhKSis2fbZ0uwt1H60uLLupDer1IbLsjnNtvNktnxYhcxt6E6s/oxWvvPpqz0ROTsEqZtzhb4bFN2du1VuZrlu9UYasmlkt7MkXCnF1ajyjFZs2HsNy5LDsJrZnxCn+WXvqUN5cYx6Mz9Jttvm3q/eV1FTgoW9CKjQoRVOnFctFyYijbpwVKCied315O7ryrz3v4LgiUitIiK0K0iyUjnykEidO7n0Kkiatzb4dYXOK3soxtrSDm9585JapGCU0lmzC5PctrMC275qeXcrwwGwqf5TxP6aXOFPkzS2VsLr4lj2HZfsIupUnOLrSXSnrxbKM0ZgrZizDfZnvG92Un8lg3yj3G7PRuydLDsJqZpxGn+WX38QnzhSfQ5dev0cXUe9npcYQ0ewluf+I9/bN8O6KMM215Pjky7sbzD4N4bWiqVR91TvMUydmCrlLOFrjNGbVpWkqd0ly3O86kzvl+2zPlm8we5hFurB9lJ/Vn0ZyLcWNayu7zL+JQca1tN0mn9aK+sYbWsrmm4SKaPX0MUspULja0spdsXx716HXNKtRuraleW01OhXgpwkvHiS0am9HfNc7mzrZSxOonc2q37eUnzg+SNttaNp9DVbcJOMt6PMsUw+pht3O2nw3PmuDKGiCpohmaEzRRQ0UsrZS0bcJFSllJUyGbcGXFLIKmUs2YsuRBBLINiLKkMgkgzRKkMEkdDMipDPlxrCrbH8Bu8Eu4pwrwapt/Vn0Z9RCbTTi9JLky6UVKOTLoTlCSnB5NbU+05arULjAMSvMHxBONW0m409frRXU8udSVapKrLm3w9xuT0kMqyvsMoZtwyl+OtvUuox6wXVmlLatGrSjUjyktfccCUXSm6bPb8EvoYlZxuo+89klya3+e8vNnn4tQ36faxitV9J+B95TOMZxcZLVPoWV6SqwcWdmnJwkmY2D6b+h2NZpa7vRs+Yi1SDhJxfA60ZKSzQABYXAAAAAAAAAAAAAAAAAAAAAAAAAAAA6P8ARRqN5Qxilp9HEVLX30o/4HOB0Z6J/wD0Xxr/APvj/wC1E6uC/wCbj4/Ij+ky/wDz5d6+ZucAE0PMgc5el5HTGcAn321VeU4/4nRpzt6X6/yhlx/6m4/vQOTjf+Sl4fNEg0X/APJ0/H5M0KACCHrAAAAAAAAAAAAAAAAAABun0Sf+meL/APlv/wAsDpg5l9Ep/wD5riy78N/+WB00TjAv8mu9nkml3/k5dy+QAB2CMGp/Sk//AF7a/wDmVP8AuTOZjpn0pP8A9e2v/mVP+5M5mIdjf+afcj1HRP8A8cu9gAHIJMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV0qU6s1CEW23oj3sJwHfTneb0VyUepnoW1SvLKCMNa4hRWcmUZVs9+v8ompLc4xfRmUMoo04UaSp04qMY9CpkrtLdW9NQI3c13XqawbIZDZDeiNkwhJzmqcebPapwjRpRpx6LifHhVHSLuJri/on2MuSNSvPWequBJDegbLVaaS5h7DClmz4sWuZUqOlPjVm92nHvkbt2V5cjlfKUJVY/5Qv12tVvmovoa22R5feZc1vE7qGuHYc97jylNG8atTtarqaaR+qu5dxZbw15Oo/A4Okd5qpWcO+X0X1IitFp3FyKKYlcTYmyHSZVFFaIXIria02a0mVU4OUlCKbcnoah9IvNDqSt8kYbV019e8nF8muSZs7NWO2uVcrXmPXcknCDhQXtSa4fecqXl5eXle4xO6cquI4jV0067zfq6GlVlrPV8yV6H4W7m4d3NezB7O2X/qtvfkZFs1yvPOGc7bC6cNMOsmqtxLprH6vxOt7ejSt6FO3oQjClTiowiuSRhGxLJ8cpZPpKtDTEbxKrdSfPe7jO0R69r9JPJbjn6T4t1+61Kb9iGxdvN+PyHL3mifSWylKnOhnLDaT3qelO8UV9TvN7Hy4rYW2KYbcYbeQU7e4g4TXgzDbVnSmmc3B8Rlh13GvHdua5rijjW0xGvhWI2WY8Pb7W3kqjS+vHuZ1ZlrGLXMOAWmMWk1KFaC39OktOK8zl/M2CXGVM2XuXruL7NSdS3lLk4PkjMtgOaHgeYJ5Xvar+RXb1tXJ8FN80de8pqcFWj4932J5pXhccRsldUNsoLNdsHtflv8zfrRSy5OO62vIoZp05HkyZQyllbKWb1OReihkFTKXzNyDLkUkMqZSzbgy5EPmUsqZDNmLLilkMqZDM8SpDIZLIZliVIAD5mVFR2VC6oVrG7gp211B06ifLRnKWesu18nZzu8HqxatqknVtpdHF8kdWPjw7zCduOU1mvJ0r23hrieGp1ItLjKK6GjiFBzhrx3olOieMfh170dR/3dTY+x8H9Gc8ReqJPlsqzqUvWTU4vdmnzUlzPp1ObGSks0ewSi4vJny4lS7ShrxbjyS6njNaPQyI8jE6O5V31olLkkcnErf/VXibdtU/Kz4wAcY3QAAAAAAAAAAAAAAAAAAAAAAAAAAAdGeih/0Xxr/wDvj/7cTnM6M9FD/otjX/8AfH/24nVwb/Nx8fkcDSb/AMfLvXzRucAE0PMsgc7+l/8A8vy5/wCDcf3oHRBzr6Xz/wApZdX+or/3oHKxv/JT8PmiQaMf+Tp+PyZoYAEDPVwAAAAAAAAAAAAAAAAADcXonT3c+38Pbw2X3Tgzp85P9GGtKltSowXKtZ14P4R3v3HWBNsBedplybPKNMYZYlnzivqgASdoipqv0n6ans0hPTVwxGg9e7WM0/3HMJ1f6RNGNXZTiUpc6VWjUj7+0S/ezlAiGORyuc+aX1PTdEZZ2DXKT+SAAOMSkAAAAAAAAAAAAAAAAAAAAAAAAAH22GG3V29adN7qejfcXwpym8orNlk5xgs5PI+NJt6JanrYXgte5qQlVi4Umtde9GQYdg9rZqMt3fqaaNy5HoJJLRLRdx3LbCMvaqvwOPcYpnspeZ8tnYWtpDdpU1z11fM+lsN8SDtQhGCyiskcmUpTecnmCHyDKS4IFVCm69eNNcupRJ6I9PD6PY0N6X05lUsy2pPUjmfRooxUY8kuBDYZBeaOREmkjzb517qvRw60TlcXM1TilzSfU+q7qxp05Tk+EVqzM9huXvlV3XzZiFPWnT/F2qa59zMM06klTXErVrxs6ErifDd2vgjYuU8Fo5ayxa4RRSVXdU7iS5ufU9OJDlKc3OXGUnqyqJvaqiskecVpyqTc5vNt5srRciURK4mCZqTLiL1vTdarGmml1bfRLmWYmNbWc1LJ+SqtSj/znfp07eHVLk2adWWqsyyjb1LqtGhSWcpPJfzs3mqtu2aVmTNawO0qa4Vhf8Zo+E5f8C/sByl+E2a5Y/e097DsNe7STXCpPo/ga7s7G5uq9vhdu3UvL2qt6XVqT4/tOwci5etcrZXs8GtYrSlBb8tOMpd7OPeVeip5cWeh43c08EwyNnbva1ku780u9v8Amw9wAk4O88uIDAANV+kXk945lqOOWNNfOGG/jOC41I9xzn2lS4tqV5azcLqhJShJc4yXFo7enGFSEqdSKlCS0lF8mjk3axlaeS88VaVODWGYhJ1LeWnBTfFo7WHXCkujkekaG4r0lN2U3tjtj2rivr5m89leaqWb8oW97vJXdFblenrxTXDUyhnMWyvM0sn52pzqT3cNxBqnXXSD5JnTsnFqM4PWE0pRffqYalHoajjw4d32IhpNhH4bevo1/dz2x+q8H8ChkMqZS+ZsUyPooZSyqRSzeplxDKWVMhm3AuRSQySDZiVRDIZJDNiJcQyGSQzNEqQw+YDMiBBXQqdlVUpR3oPhKPtIoYL8s1kVazWTObtuGU3lLOTvrWGmGYi96DXKM3xaMPjJNHU+fstUM4ZPusGqxi7mEXO0k+anzOUacK9rc1rC7g4XFtN05xa05cCP16XQVsuD3HsuimL/AIlZalR/3lPY+1cH9H2n0lm7pdrScUlr0bLuvAjUtnBTi4skqbTzR4NSDhNxkmmik+/E6PHtIp8fpM+Ai9xRdGo4s6lOevHMAAwF4AAAAAAAAAAAAAAAAAAAAAAAAOkfRTpqOS8Wq6cZ4lpr3pUof8Tm46i9GOjGns07WPOrfVnL4aRX3I6+CRzul2Jkd0ollYNc2vX6G0QATI82Bzf6Xk9cfwKn7NpUfnNf4HSBy96WVeU9oNjQ+rTwyDXvlUnr+xHHx15WbXNr5kj0VjniMXyT+Rp0AEGPUwAAAAAAAAAAAAAAAAADYXo73MbbazhCk9FV7WkvfKmzr9ckcT7JLqFltKy/cVHpGN9TTfven7zthLTh3Ew0dlnQlHk/ojzLTanleQnzj8mwSASAhhh+2i1+WbLcwUkk3G17Xj+hKM/9k49O2s72UsRybjVjFayr2FaEeOnFwen3nEq5Ii2Pxyqwl2Hoehk87epDk8/NfYAA4BMgAAAAAAAAAAAAAAAAAAAAAVUoSqVFCK1behSZDlOz3qzuZP6H1WuZntqDr1FBGG4rKjTc2fRhWARh2da5er5uB78YxgtIRUV4Ikhsl1C2p0FlBEXrV51nnNhkMhkNmcxZBkMMhsFyRDBDIerajHm+QLki/Y0e2r6v6EeZ6jfEtW1JUKCgub4srbL0sjSqz15BtlM5aISeiPixG4jQoSm3xXLxfQpOWqs2UhByeSKrKwuMwY/a4LaJt1ZKVWS6Q6nQ9nZ2+GYfb4VaRUaNtBQ4fWfeYNsVy88KwapmC9p6Xt7xpKS4xizO4mW0pNR6SW9/Ii+PXqrVuhg/Zh8XxfhuLsSuJbRXEzTI3MuIuRLcWVp6eJrSNaSPot1S9etXko29CLnVk3polxOa8/5jq50zrcYlq/kFrJ07SL5cODZs3b/miWDYDRyph1ZLEMR4XEovjCPQ0/Y0IULeNOC00XH39TSa6Wp2L5k00Vw7oqTvprbLZHu4vx3LsPb2WqhHanhHyjTTSW7qdXvmcZVp3NneW+J2TaubaanHTqk9WjqLZxnbCs4YHSubetCneRilcW8petCRxcWoS1lJbjBpjZ1aip3MVnFLJ9m3P45mVhjiOJxcmQEEkcRxKZMAwrbJlCnnDJ1xa04J31vF1LWfVSM14gyU5SpyUkbNpc1LWtGtTeTi8zhxRdza1LS5i6dejJwnF81KPXzOgtgObZY/luWCX9RvE8O9V7z4yj0+4w30gsmywHM8cy4fS0sL71bhRXCnJdfiYLl7GrnKuZ7PMNo3uRko3EV9eMv+BIZx6zRU471/Gj1XEbajpDhidLe/aj2SW+Pju8mdYshlNhdUMVsKGJWM41aFxBSTi/AuujV9kwU0eNP2W4y2NFllLLro1fYIdGr7Bu00V1lzLTKWXXRq+wQ6FXX6JtwRXWXMtPmQXXQq+yR2FX2fvNmKLtZcy0QXewq+yvMh0KvsrzNiKK6y5lpkMu9hV9leZDoVfZXmZUV1lzLQfMuOhV9leYdCr7K8zIiutHmWiSvsKvsrzHY1fZ+8vTRXWXMohOVOanB6SXI0b6SOUlZYhRzlhlF/J7h7t1GK+i11ZvV0avsrzLGJ4PRxzBrvBL+nGVC6puOr+q9OBrXlBVqbXFHVwXFnhV7C5W7dJc4vf5b0cfU5qUVJPVNalRfx/BL3K+YbvAcQg4Tozbpt/Wi+X3Hzo4sG2tu892jKFSKnTecXtT5pkTW9FrvPGr0+zqyjrroe0z5L+i6kVOPNdDSxC3dWGst6NihPVeTPMABHjfAAAAAAAAAAAAAAAAAAAAAAAAC5nWno+WrtdlGFapa15Va/DqpTen7DkvXTj3cTszZTZSw/Zxl+0mtJxsYSlx6y9b953cAjnXk+S+pFdLZ5WsI85fJMycAEtPPx0OTvSguY19qlaknq7ezo034cHL/aOsWcaberiN1tczBUi9VGtCn8YU4Rf904OkMsrZLm/oyV6Hw1r2UuUX80YOACFnpQAAAAAAAAAAAAAAAAAB9uA3MbPG7G7m9I0bmnUb8IyT/cd505KcFNPVSSkvjxOAEd1ZLvlieUMHxFcPlNjRqecESfRue2pHu+pAdOKWyjU718j1wASk8+KK1ONajOjL6NSLi/c1ocM4nbfIsSurPe3vk9edLXv3ZOOv3HdPLj3cTjfa3YrDtpWP2sYOEPlkqkV4TSlr5yZH9IIZwhLta/nkTTQyrlVq0+aT8n9zFQARc9AAAAAAAAAAAAAAAAAAAAAAPow+2ndXUKUI6tvivAz22o07ehGlTgoqKPHyxh8KVvG6qJ9pLkmuR7bJRhdr0VPXlvfyI5iNx0s9RbkGyGGQdU56IbIbBDBcGQwylguSDZ9WGUt+bryXqrkj5YwdSoqcep68Ixp04048l+0qkYq88o5LiVN6vUhkMpb0ReaiRFWWiLmSsDqZqzbRtNH8itpb9eXTVcUjzMTuJQgoU1vVKjUIRXNt8DdOzjL8ctZXpU6i/LrtKdeXVeBjhT6erq8FvMd9c9StnNe9LYvq/Ayabgt2nRio0acd2nFckiYlqJcR1ZIgkkXIlcS3FlaMEka8kXYsm4vqGEYZeY3cxcqVjTc3FLXeKIvgX6DoSU7e7gqlrXi4VovqjWqxbi9Xeas0uKzXHu4/A5busXuMzZgvcxXknKdxUcYRf1Ip8D609EfTn7LlTJWdrjDHFqwuZdpaS6PXjoWcDtqWLY5bYRWuvkauXpGt0izRoSUYbeB630lGdCNWj/AIermuyKX0LFSSfU+Sm7izvFeYXd1LO5T134Sej+BtKpsJxVy9XMT05ptLiUfwC4s3/0jfkiypWUtji/I5sNIsJiv8deT9DFLfabtAtaSpRxdVEuTcEVfwq7QvtOP6iMo/gExV88xvyRP8AeJ/6SPyRpOlR/R8DA8W0bbzbh/sfoYr/CrtC+1I/qIj+FXaH9qR/URlf8AWJf6SPyQ/gBxH/SR+SKdHR/R8B+LaNfs/2P0MU/hW2h/akf6tD+FbaH9px/q0ZX/ADiH+kr8kHsBxD/AEmfkiqpUf6fwH4ro1+z/Y/Qw7FtoGa8dwypheM3MK9pV+lHcSMfcabpdm9HFrTibR/gCv8A/SZ+SIewS+XPM0vJGzTkoLKMGvAz0sdwSitWjVUVyUWvoYPgWd815dw6OGYTf7tpD6EZLXdPre1LP/2iv1EZY9gt7/pNLyRS9g94v+0svJGOVCMnn0b8jFLEdHKknOeo297cHt+Bij2pZ/8AtFfqoPajn/7SX6qMpewm8/0lfkil7C7xf9pJfqoqrblBlVeaNvdGH+x+hiz2n5+f+cv7JD2nZ9f+c/7Jk72HXi/7Rv8AVRblsSvlyzC38EZFbT4QZkVzo690Yf7PsY09pefH/nP+yUvaRnt/50/smSS2K365Y838EWpbGsSjyxtv4IyK1r8IvzMsa2AvdGH+37GPfwi56+1H+qP4RM8/aj/VPfex3El/nl+RRLY9iW61HGWm+uhf1W65PzMqngb/ACw/2/YxW82n57jV3KWK8Fze6Wf4UM//AGq/1TJP4EsR4t429X4EPYpiH22/IdUvHwfn9zbjPAEstWH+37GOfwn5++1X+qP4T8/fa39kyF7Fr/rjb8g9i98v89PyHU77k/NepkTwF/kh/t+xj38J+fvtX+yR/Cfn77V/smQPYze/bT8iHsbvV/np+RXqV/yfmvUuUcD/AEQ/2/Y8D+E/P32t/ZJW0/Puv/Ov9k93+B28X+eX5EfwPXf2y/IqrHEOT816lyp4I/8ATh/t+xh2Ysw4rma8p32OVY1rqmtIzUdNUfCpGfx2QXWqTxmXkYfnDAbvKuMxw66m6tOa1p1ukiyrbXFBa9WOS55m/bztWlSt2sluSWWzsPh1IfFaMpi9STHvRnyPKu6XZ1Xw0T5Fk9W6oxqQbfCSXM8trR6EbvbfoamzczfpT1okAA0zKAAAAAAAAAAAAAAAAAAAAAVU4drONLXTfajr73odz4PaxssJs7OOm7Qt6dJafoxS/ccY5DsFiedsEw+UHONe+pRlFezvLX7jtjm2+/iSbR+Gyc+5EI0vqe1Sp97+QABJCGA4Xz7eQxDO+OX1N6wr4hXqRfenUbR27jN2rDCLy+ktVbW9Ss/6MXL9xwVXm6ladR85ScvMi+kk9lOHe/kTnQyl7VWp3L5lAAIqTwAAAAAAAAAAAAAAAAAAHYfo94g8Q2TYO5yUqluqlvLw3JyUV+runHh0r6JGI9rlXGMLfO2vI1l7qkNP/j+87eAVNS61eafr9CK6YUekw/X/AEyT+n1N2gAmx5WDmD0nLF220aN3vJxvLKnNLucdYs6fNHelfhrlh2CYvGEPxVWpbVJfW0kt6K92qkcrGaevat8smSHRit0WIRT/ADJr6/NHPwAIWepAAAAAAAAAAAAAAAAAAA+7BbT5VeQg9VHXi0uR8UU5SUVzZm2BWfySyipRj2kuLa6m/h9q7irt3I0r246Gns3s9CCUYqK6LQlsggl2WRGQyNeBDZBUuyDI1DIBdkGUt6LUllVvTdasodFxYK7Es2fXhtHdg601xfI+rUPRaRXJcEQX5ZGjKWs82Qy1Xmoxb7i5N6I865jcX15Qw20TlXuJKK06LXiY6s9VGSlDWe3cZRslwFY7mKWM3cdbGxfqa8pM3HOo6lRzfDU83L2FUMAwK2wi3iluRTqtfWbPvR1LW36Gnt3veRLE7rrddzXurYu777y4itMtriVpmSSOTJFyLK4stxZXF+JgkjXki5FlafRltMqizDJGCSPA2rZXWcMlVYUV/lTDk6lvPrLq0c4Kc7qz09alcUZ9OEoyi/8AE61tq0revGtDRtcGnyafM0NtzyvHLWbI45Yx0wrFHq2uVOS5/ec24hqS1uDJboliOpN2U3v2w7+MfHevE2psizT+GGS4OvPXFcNSp14a8ZLo/IyqLWnBvzOZNn+ZauTM622LQbdlctU7uHR73BM6dr9k+zr28lO2uIqdGSfNaaltFt+w+HyOHpHhSsLvOmv7ue2PZzj4b12Marx8xqvHzKNRqXuJHnEr8/MPTx8yjeDZTVCiVP4+ZS9PHzI1KWzIol6iHp4+ZRLTx8yWylsyxiZYoiSXj5luSXj5lUmUNmeKM8UQ0vHzKJJePmVNlDM8UbEUUSS8fMoaXj5lUmUszxRsRRQ/j5lMvf8AeVSZQzNFGxBFMve/Mo+LJZSzNFGxFFMvf95RL4+ZVIokZYmxFFL978yiXxKmUSMqNmKKX8Sl/EllMmZEbEUUMpfxKmUMyo2IIpkY3tIy9HMmWKlOnFfLLVb9GXVpcWZGyIT3JqS9z9xjuLeNxSdOa2M26FSVKanHejmm1qSlFxqJxqQe7OPcfRqZPtfwD5lx+OL2sGrK94ySXCMjFYvgQCVOVCpKjPevjyZLVKNWCqR3Mr1POvqe7V1XXifeU1YqcHFpPuNa7odNTy4l9OWqzyQVTi4ycXzRSRlpp5M3wACgAAAAAAAAAAAAAAAAAANi+jpYu92qWNTeSjaUatw136R3V98kdXHP/onYa5XuOYxKEHGFOnbQl1TbcpL3aKJ0ATPBKepap8239Poeb6TVekvnH9KS+v1JBAOuR4xDbPfvDdl+PXEZqE5WrpRb75tR08mzix8zqX0qsR+S7PKFklrK9voR59IJy/akcskL0hqa1yo8kelaI0dSyc/1N/DJeoABwSVAAAAAAAAAAAAAAAAAAA3F6KWJ/Jc93mHSct29spbq14b0Gpfs1NOmVbJMV+Zto+B30nJQV3CnPR/Vn6j18PWNuwq9Fcwn2nOxe36xZVafNPzW1fE7YAXDgD0Y8TBgW33DPnPZbim7CMqlpuXUXLpuS9Zrx3XIz0+fE7SniGG3VhWjGVO5ozoyTWqakmv3mGvT6WlKHNGxaV3b14VV+Vp+TOFQXr62qWV7Xs638Zb1ZUp++LcX+wsnnrWWw9pTTWaAAKFQAAAAAAAAAAAAAV0KcqtWNOKbbfJFUm3kijeW09XLthOvcKtKCdOL473JmXJJLRcEj5cNt42tpClDe5avXvPpJjY2qt6SXF7yM3dd1qmfAkhghm4auRDAZAKoggMgqXIiT7ufQ9KxpdjQ1f0pcz47Gl21bff0YHpN6suijBXn+VENka+IZbqS0RcYEsy3d1o06cpSeiSMy2L4E5Otmm9p+tLhbRl072YVhmHVsw5ht8Hoa7spJ1pL6qN80aNG0taNjbRUaNCKjFLv6l1lR6arrvdH5mtilfoKPQx96W/sX3+Rd3tW23q2VJltFaZ2WiLSRciytMtRK4sxSRryRdTK0y1FlSZhkjBKJeTKky0mVJmGUTBKJdTPkzJgdrmvKt7l26ScqkHK2k/qSS1PpTKozlCUZxekovXVGCpSU4uLMac6clODyknmnya3HKNW1r0Kt3g2Iwcbq2k4TT/ss3f6POaHi+BV8oYhV1v7Djayk+M4dTx/SKy1uzt88YbR4S9W+hBdeSbNX4XitzgWOWOYsNm1Ut5Lf3eUofWOQ9aD7Uej1adPSHDM47JPav2zW9dz+TOrlLvTT6onXxLFniNpjeD2mYLCSlb3lNSlp9SXcV6nQSUkmjy9wabUlk1sa5Pii5vDeLepDZXUGoVtkNlOpS5FyiXKJU2UtkNlLZlUTIoktlDYbKWzLGJmjEhspbEmUNmWKM8YhsobJbKGzNFGeKIk+JRJkspkzMkbEUUspbJZRJmWKM8UUsoZUylmWKNiKKX3FEiqTKGzKjYgiGW2VsoZkibEEUspZLKWXo2Ioob4FLJZTIyI2Inn5kwmhj+A3OF3CTcouVJvpJLgc+ujXsbuth90t2vby3ZJ9TpDVqSkuaeprLbXgDUqWZrKnx+hcpLq+pHNILJuCuYLbHf3fbedvDK2T6KW57u/7mAJ6oFunNSipResXyK9SNpprM6rWR8l7Seu/FcOuh8h6kkpRafU86rHcm1pouhwsRt9SWutzNmjLNZFAAOYZwAAAAAAAAAAAAAAAATCEqk404fSm1GPvfAA6n9GzDPkGzOjcyhGM7+4qXGq5yjrux1+CNlnm5Ww2ODZbw3CoRivklrTpPdWi3lFb336npHoVrS6KjGHJHkN9X6xczq82/sAAbBqnN/pdYoqmYMFweMpfk9rO4mteGtSWi+OkH5mjDOdvGK/O21XGqsZN07eqrWGvRU4qL08N5SfxMGPOsSq9LdTl2/LYewYNQ6CxpQ7M/Pb9QADSOmAAAAAAAAAAAAAAAAAACYSlCSnGTjJPVNPRpkAA7sybisccynhWLx/63aU6sknrpJxWq+D1XwPWNS+i3jPy/Z5PDJzTqYZdSppd1Ofrx+9zNtHpFnW6ehCpzX/AGeI4lbdVu6lHk35cPgAAbJo5HJe33CHhO03EZKE40r3du6blpo95etp4JpmAnQfpWYL2mGYTj9OC3qFSVrVkovXdl60dX3Jp+Zz4QTEqPRXM1z2+Z63gdz1iwpy4pZPw2AAGidYAAAAAAAAAAAAGR5Us91Su5aPXgl1R4uG2lS7uVTprxb7jNbemqNGNNJLRacEdnCLXXn0slsXzOZiNfVh0a3su6jUgEnOEVakEAoAynUlspBckHzKXrJqK5sls+rDaWrdeS4LgiuWYlLVWZ9VGmqNFQXPqVakN6kamTI0nt2sNnxYjcqhRlN8dOS7z6astEz6Mj4O8yZohGotbG0e/Ul0fgYqmtJqEd7MtNRinOe5Gd7J8AeD4HLE7qP5be9/OMehmESmck2owSUILdil3IJkgoUFRpqEeBF7mrKvUdSW9l1MqTLaZUi5o05IuplSZbTKkzE0YJRLqZUmWkytMxtGCUS6mVJllMqTMTiYnEuplSfAtplSZjcTE4l2dvbYhY3OEX0VO1u4OMk+ktNEcvZgwW4yzmS9y3exe7Tk+xb+vBnTmvLvXFGDbd8sSx/LNPMFjD/KWG8Kmi4zh1Zz7uj/AKi4Hd0cv+qXPRTfsVNndLg/ozHPR4zTHD8TuMmYnU/JLvWpaOT+jJckbkqQnRqyo1PpwekjkhV6kqdDE7OTp3FvNVYNc9Y9Dp3JOYqOb8n2uM02vlVKKpXcOu/3mK0lk9TnuNnSrC+irK8gtk9kuyXPx+Z7GobKNRqjf1SJ6hU34kNlLZGpcolyiVN9xS2RqUt8OZeol6iS2UtkNlLZkUTLGIbIbIbKWzJGJmjENlDZLZQ2ZYozRiGUMlsokzLFGeMRJlDYbKZMypGxFENlDKmUNmRIzxRD4FEiWylsyJGxFFMimRL4lDMiM8UQyiTKmyh8jIjYgiGUMqZRIvRsRRSy3c29G+s69hcxUqNeLjo+jfUrZS/ArKKlHVZsQ2PM57xzC62A49cYVXUt2Em6TfWJZTNrbXcv/O+CRxW1h+WWf0tOcoLmajoVVUpqa5vmu488u7V2Vw6L3b13cvAktKp01NT48e8vnz3dLeTmnxSL2ofHga1alGrBxZfF6rzPMBduKbhLXo+paIrUpunJxkbieazAALCoAAAAAAAAAAAAMw2NYQ8a2lYNbOE5UqVf5TV3ekafrcfDVRXxMPN6+ilgu9c4vmGpBaQjG0oycXrq/Wm0+XcmbmH0emuYR7fltOdi1x1ezqT45ZLvew3+ACfHlIPjxq+p4ZhF5iNVrctaE6z1en0Yt/uPsNa+khjKwnZfeUYzUauI1IWkE+qb3pf2YswXNVUaMqnJG1ZW7uLiFJcWkcl31xUu72tdVW3UrVJVJNvVtt6v9pZD5g80bzPZ0slkgAAVAAAAAAAAAAAAAAAAAAAAANv+ivjfyDPVfCKlTSliVs4xTlonUh6y4d+m8dRnCOU8Yq4BmXDsZouW9Z3EKrUeckn6y+K1XxO6rSvSuralc0JqdKtBVIST4OLWqa+DJjo9X16Dpv8AK/gzzXTK06O6jXW6S+K+2RcABICHGObTMDWYsi4thKinVqW7nR114VIetHl4o4xaaejTi+qfNHePice7ZMB/B7aHidpCm4W9ap8pt+DS3J8dFrz0eqI3j9DZGqu5/T6k30Qu8nO3fevk/oYeACNE5AAAAAAAAAAXF6A9DA7NXd3pN6QitX4mSlSlVmoR3ssqTVOLk+B7eW7PsLftpwcakuvgeuUU4xpwjCK0iloirUm1vRVGmoLgRitUdWbkyQU6k6mbIxEkDUhsDIggkpb0BciYQdWpGnHrzPVSUIKEeSR82H0uzp9rL6UuR9GpkijWrS1nktyBTJ6IlvgWa9RRTbeniG8kWRWbPkxGrU9WjQTlWqvdppdWbhyRglPL2XaNqkvlNZdpWl149DBNleC/OmMTxy6h+TWv8UmuEpo2lKbnNyfV+RuYXb67deXh3fc1sSq5JUI8Nr7+XgVLuK0yhMlM7LRw5RLqZUmW0ypMxNGCUS6ipMtplSZjaMMkXEypMtpkpmNowyiXUypMtJlSZY4mJxLmpUmW0yUzG4mNxLmpdt6kFOVOpFSpVYunUi+TT5nz68Rrw0LHDPYY3DNZHPW0rLkso50r28Iv5uvW6tvLol3H17H80SylnFW1xPTC8SfZ1E+UZvqbZ2n5chmzJ9W3UU7+zXa28uui6HOe7O5tZ0aqcLqk9H0cZnErUnRnku9HomGV4YtYOlX2vLVl9JfzidbXFNUqrjF71N8acvaj3lvUwvYxmr8Jsq/Nt3U/ynhq3Hq+MoLqZhvarU61GSqwUkQG5tKlrWlRqe9HZ38n4leobKNSGzLqmJQKtSGylshsvUS5RJbIbKWylsvUTIolTZQ2GylsyJGaMSWyhsNlLZekZYxDZQ2GylsypGeMQ2UthspbMiRmjEhsobJkylsyJGeKIZRIlvgUtmRI2IohlDZLZS+RekZ4oh8yhktlLL0jPFENlDJbKWXo2IopZTIllLL0bEUIuD3oVEnTmt2afcaMz/gcsu5mqQjHSzun2lJ6cFr0N4SPCz9gccxZbq0FFO7t06lKXV6dDj43h7uqGtD3o7V6eJ0bKr0c8nue80qmTqWKMpx3qVWLjVpvdmnz1LupCaclJZnVlHJ5FNeG/B8NWuR8LWj0PRPkuaai9Y9TmYnb5rpF4mWlLLYWAAcQzgAAAAAAAAAAAA7C2MYF8wbOsKtJw3a9an8pr8169T1uT8NEcv7NcBlmXO+F4TuOVKpXU6/BtKlH1pa6cuC018UdoxSikorRLgkSTAKG2VV93qQ7Su62Qt13v5L6kgEEmIVkSc1elpjnynMeF4BSqawsrd16qT4b9R8E13qMU/6R0nUlGEJSnJRilrKT5JdWcPbRsclmPO+LYy5NwuLiXZavXSnH1YL9VI4OkFfUt1TW+T+C/iJTona9Lduq90F8Xs+WZjwAIWekAAAAAAAAAAAAAAAAAAAAAAAAA679HXMHz3s0s6FSe9cYa3Z1OK13Y8ab/VaX9E5ENweizmL5tzpWwOtPSjilLSCb4drDWUfNbyOtgtx0N0k90tnp8SPaT2fWbCTS2w2+vwOoyACdnkwNKelNl5XGC2GZaNNdpaVPk9xJacac/o+UuH9I3WeZmrCKGPZdv8HuIpwu6EqfHho2uD18Hoa15Q6xQlT5/M38Mu3Z3UK3BPb3cfgcQA+jErO4w7EbiwuoOFxbVZUqkWtNJRejPnIA008mevpprNAAFCoAAAAAAPtwm7la3UXv7sW9Je4+IF9Oo6clKO9Fs4KcXFme05xnBTi9YtaoqPIy9eqtb9jUqfjI8k+49Ym9vWjXpqa4kYrUnSm4skEAz5GMkjUEMZAlsqtqbrVlH6q4styfdzZ6NrS7Kgk/pS5lUs2W1JasS6+5clyKQyGZDUSInLRHwVKVfEr+hhdqm6teSi9Oi7y9eVo0qcpy5RWrMy2TYK6VGrmC8h+Mq+pQTXJdGY403cVVRXj3GdSVGDqvhu7zNMIsKOEYTb4ZbpKNOK32usup9SLak29W+L5lSJVGmoRUVwODPOTbe9lxMrTLaZUmGjBKJcTKkW0VJmNowSiXEypMt6lSZY0YZRLiZUmW0ypMxtGJxLiZKZbTJTLGjG4l1MnUt6k6lriY3AuJk6ltMJluqWOJfo1OzqKfRfSXeu40Ztty18wZjhjtnB/IL961ElwjNm7dT4syYRb5jy5dYLcpNzi5UZPpPoal3b9LDZvW46OFXrsblVH7r2S7vtvOecrY5Wytmm1xmhJ9i5KncRXJw6s6WhXoXltRxC1mp0LmCqRa5LXocr17OtZ3N1gt/Fxq28nTaf1kuptXYVmqnCyr5Zxm4VNUm521Sb4adFqc+yq6k9V7n8yS6R4d09JXNPbKO/tjwfgbT1I1PleJYPFtPFLfVc/xiI+c8G+1Lf8ArEdjOPMhaoz/AEvyPqbIbPl+dMG+1Lf+sRHzpg/2pb/1iLk48y9UZ/pfkfU2UtnzfOmDfalv/WIj5zwf7Ut/6xFylHmXqlP9L8j6GyGz5nieD/alv/WIh4ng+mrxS3/rEXqUeZlVKX6X5H0NlDZ8zxXBftW2/rEQ8Uwb7Vtv6xF6nHmZVSn+l+R9DKWz53imDfatt/WIpeKYN9q239Yi9ThzM0aU/wBL8j6GymTPneKYNr/zpbf1iKXimD/alv8A1iL1UhzM0aUuT8i+2Utlh4ng+v8Azpb/ANYih4phH2nb/wBYjIqkOZmjTlyfkfRJlDZYeJ4R9p2/9Yil4lhH2nb/ANYi9VYczPGnLky+2Utlh4nhH2nb/wBYih4lhP2lb/1iL1VhzM8acuRfZTJlh4lhP2lb/ropliWE/aVv/WIvVWHMzxhLkXmUssPEsJ+0bf8AXRS8Rwr7Rt/10XKrDmZ4wlyLz5FLLMsRwr7RofrooeI4X9o0P10XqtDmZ4wfIvMQm4TU1x06d587xHC/tC3/AF0U/OGGfaFv+ui/pafMzqL5GrdreAfNWMxxi0h+S3fCenKM+piMZarVdTe2NwwXG8FuMLuMQt/Xi+zbmvVl3miqtvOzu61nUkpOlJxUk+DS6kExazVvcOdP3ZfB8fM69vU6Snk96J1Kai3ouOpIOfKKksmZdx8Mlo9CD6bmGq3kvefMRa5oOjUcWbMXmgADXLgAAAAAAAXLejVuK9O3oQc6tWahTiucpN6JL4gpuN7eirl5buJZnr003r8ktm9OHWb710Rvk8PIeAUss5Sw7BqaW9b0UqsvaqPjJ+bZ7pPrG36vQjDjx7zyrFLvrd1Orw4dy3AAG4aBhG3HMH4O7NsTuoVNy4uYfJLfitd6fBte6O8/gcZvmbv9LLMXyrH8Py1Qqa07Gn8orpPnUn9FP3R4/wBNmjyDY5cdLdOK3R2ep6doxZ9XslN757fDh6+IABxiRAAAAAAAAAAAAAAAAAAAAAAAAA+vBsQucJxa0xOzlu3FpWhWpv8ASi9Vr4cD5AVTaeaKSipJp7md55dxW2xzA7LGLN60LyhGtDjrpquKfinqn4o9A0l6KeZleZfvMs3FXWtYT7a3TfF0pv1kvdL+8btPRrK4VzQjU5/PieLYnZuyup0Xwezu4fAEMkG0aJzR6TWWXhmaqOYKFPS2xSOlVpcFWiuPmtH8GajOx9rGWI5ryTe4dGCd3CPbWstOKqR4pfHl8TjmcZQm4Si4yi2mn0a5ohmMW3Q3Gst0tvqemaN33WbRQk/ahs8OHp4EAA5JIQAAAAAAAAC9ZV3b3EKiWuj10M0t6qq0Y1Fp6y14MwU93Ll8ofks0+L9VnZwi7VKp0ctz+Zz7+hrx11vRkGo1IBKDhk6kElOjlJRjzYKl+xpdpV7SX0Yn3t8SmnBUqSpr4hmVLJGnOWvLMMoqS0RL5HxYhcdlSbXGT4RXey2pJQjmy6nByeSL+D4fVx/MFDDqa1pRanWkukeqNzQp07ejTtaEVGlRioRS8DGNnGDfM+CfK68fyy89d681F9DJVy0O1hVo6VPpJr2pfzIwXk1KWotyLiKky2mVJnUaOdKJdTKtS0mVpljRhlEuJlSepaTKkyxoxSiXEypMtplSZY0YXEuJkpltPgVJljRjcS4mVJlpMlMsaMbiXNSpMt6k6luRjcSvUnUo1GpTVLdUuakqUoyUov1ovVFrXxJ1KapTVNYbest6qhm3D6fGOlO6UV06s1lGMLmnGpFtJrVNPQ6brW9C+s6+HXcVKhcwcGn016nN+PYTXy1mW6wa4TUFJzoSfWHQ4l5QVKprcH8yZYDeOrR6CT9qG7tj9j4naa/yk/1mUuz/wBZP9Zn2ppoMx9Xg+B21VnzPh+R/wCsqfrMfI/9ZU/WZ9pA6tDkXdNPmfH8j/1k/wBZj5H/AKyp+sz7AU6tDkOlnzPj+R8f4yp+sz4cTp6TVGFWfDn6zPXuK0aFCVR8+SPE1cm5y5viyyVvDdkbFvKcnrM+fsH+dqfrMdg/zs/1mfRqQY+rw5G50kj5+wf52p5sdg/zs/Nn0DUt6vDkV6SR8/YP87P9ZjsH+cn+sz6NRqU6vDkNeR8/YP8AOT/WY7B/nJ/rMv6gp1eHIa8ix2D/ADk/Nkdg/wA5P9Zn0EMp1eHIa8ix2L/OT/WY7F/nJ/rMv6sasp1eHIrryLHYv85P9ZjsX+cn+sy9qCnV4chryLPYv85P9Zkdj/rJ/rMvgtdvDkNeRY7F/nJ/rMdi/wA5P9Zl/UhlOghyK68iz2L/ADk/Njsn+cn5svAp0EOQ12WVSa/lJ/rMurXhq9X3kkF8aajuKNt7ySdSkict2LfArOShFyfAplmWq83rurkWSXxZBFK9aVabkzYSyQABhKgAAAAAA2d6OWWXjeeI4nXp71phKVaTa4Oq+EFy97+CNYnXWxHKv4L5FtadekoX14vlNzquKcl6sfgtPjqdTCLbp7hN7o7fQ4mP3vVrRpe9LYvr8DOgATY82yB8+J3lvh+HXF/dzULe3pSq1ZN6JRitWfQah9KLM6wnJVPAreppdYtU3ZpPiqENHLze7HxW93Gvd3Ct6Mqj4G3Y2sru4hRjxfw4/A5uzbjNfMGZcQxq417S8ryq6P6qb4L4LRHlAHm8pOTcnvZ7FCChFRjuQABaXAAAAAAAAAAAAAAAAAAAAAAAAAAAGUbLMzTylnjDsYcn8njU7O6ivrUZcJeXNeKR2zSnCrSjUpzjOE0pRlF6pp8mj8/jrH0bc1/P+Ro4Zc1d69wlqhLV8ZUn/Fy8tY/0SS6PXerJ0Jcdq+pCNMMP14Ruordsfdw+PzNpAAlh5+Qcs+kPlT5gzpLEralu2OK61o6LhCr9eP8AtfF9x1OYjtbyrDN2S7rD4wi7yku3tJNcVUiuXxWq+Jz8Ttes0GlvW1HYwO/6ldqUvdex+vgcdAqqQnTqSp1IuE4txlF801zRSQY9UAAAAAAAAABXSqSpVFODaa6ooBVNp5oo1mZlh91C5t4yjLWWnFPmfTqYtgV12FyoNxUZ8G2ZOmmtVyJnh911ikm963kfuqHRTyW4q1Pqw+nzrSXuPlpQdWqoL4nqaKMVFckdCK4mhWlksgyGwymTMhrpFFWeiPpyRhLx3MKq1V+R2j3pdzkjyb6pUnKFtRW9VqyUIpeJtbK+FU8EwOjZRS7aaUq0uu8X2dv1q4Sfux2vv4I2c+ip63F7j15zUpeqtIrhFdyCZQiUyV5ZHNki6mSihMqTLWjDKJWmVJltMqTLWjDKJcTKi2mVJljRicS4mSn3lC4kplrRicS4iUy2mVJljRjcS5qSmW0yUy3IscS4mTqy3qTqW5FjiV6k6ltMnUpkW6pc1Gpb1GpTVLdUuN+JhW2jLnz7l2OL2kF8vsPWenOUF0Mx1K6U4xk1UW9TmtJx70Ya9BVqbgzPbVp21WNWG9fxo5msq6rUYz68pLufVH0NnqbScvyytm2apxfyC9e/Sa5KT6HkReq5nDptrOEt6J6pQqxVWnultRXqQRqDMMiWFxZB8+IV+woaJ+vPkUewrGLk8kfFiNftq+5F+pA+cpitF4kmFnTjFRWSJIYILSpIKQUKlRBAKDIkEAtK5EgpJKMZAEAoAAQUBI1IIKMrkSCAWjIknUpBRjIlkBkalCpJ89WW9LwLlWWi0TLBxcTuc/7qPiZILiAAcYyAAAAAAAALV8lqAZ9sLyp+FGebd16blYYfpc3L04S0fqQ+MvuR1sjBNh+UllXJNCNxTUcQvtLi6ei1Ta9WH9Ffe2Z2TfCrTq9BZ73tZ5pjl91u6eq/ZjsX1fiAAdM45Enom20tOrOMttmaPwqz/fXlKpv2du/k1rx4dnBvj8Xq/idGbfs2fgtkC5VvV3MQxHW1ttHxjqvXn8I9e9o49IrpDd7Y0I97+hONErDJSupLsX1f08wACLk2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmmxnNssn56s76rNqwrv5PeLp2cn9L+i9JfBrqYWDJSqypTU470YbihC4pSpT3SWR+gUZKUVKMlJPimnqmiTVvo4Zw/CLJiwq6q72IYSo0pavjOj/Jy+Gm78EbSPR7avG4pRqR3M8YvLWdpXlRnvT/AI/EAAzGscyekhk9YLmWOP2VLdssTbdRLlCuvpfrLj79TU52xnjL1pmnLN5g14vVrwfZz606i+jJe5nGeNYbeYPi11hd/SdK6tajp1ItdV19z5rwZDsYs+gq9JH3ZfM9I0cxHrVv0U37UPiuHofGADjkjAAAAAAAAAJT0epk+D3ruaG7KPrQWjfeYufbg9wre+pynq4b3FJm/h107ess9z3mrd0VUp9qM6sKXZ0t9/SkX2RGSlFNcmuAbJyt2wiUm282NS1WlpFvoi4W6iTWjEt2wujvPf2c4FO6vp43dUm6VL1aKfXxM+cKspOTg22akWJ4tQoqhbXXZ0o8ootSxbMHTEH5m3a4hTtKepGDfN9psyp9Jt1sjcSpVfYZUqVX2Gaa+d8xfaD8yPnjMf2hLzM/45H+mzG7RP8AMjdCo1fYZUqVX2GaU+ecya/84PzHz1mT7Rfmyn42v0Msdjn+dG7VRq+wypUavsPzNH/PeZftJ+bHz5mb7Sfmyn40v0MseHN/nRvJUavsPzJVGt7D8zRbx3M/2k/Nj5+zP9pvzZT8ZX6GWPC5P86N7KjW9h+ZUqNb82zQ3z9mj7Tfmx+EGaftN+bLfxhfoZa8Ik/zo312Nb82/MlUK35t+ZoN5gzV0xN+bH4Q5q+1H5sp+Lr9DLfwab/Ovib+VGt+bfmT2Nb82/M5/eYs1faj82R+EWa/tR+bKfiy/QyjwSb/ADr4nQXYVvzb8yewr/m35nPn4R5r+1H5sfhJmz7Vfmyn4r+xlv4FU/qL4nQfYV/zb8yewr/m2c9fhJmz7VfmyPwlzbr/AM6vzY/Ff2MfgNT+pH4nQ3YV/wA2/MfJ6/5t+Zzz+EubftV+bKXmbNv2s/Nj8V/Yx+AVP6kfidEdhX/NvzJ+T1/zb80c7fhPm77WfmyPwnzd9rPzY/FP2Mf2fq/1I/E3dn7LE8z5XrWXZfldBOdvLrvdxoClGtRlO2uYOnXoycJxfPh1PVpZpzdCaksVeq8WfBXq1bm4ndXDUq9T6cl1NSrUjWnrpZM6tha1bSm6c5JresuD4gakAuNzIqTSTk3wXFni3VZ17iVR/RXBH14rXcIKhB+tLmecuC0Rjm+BuW9PJazKtQUgxmzkTqCAUBLI1BBaVJBBAYyKiCAWjIlggFCpIIBQAEAtAABQAakMgoVyKgUgoCQ3oQW6r6GvcVlRg5sqlmUzestSkAik5ucnJ8TKAAWlQAAAAAAbK9H7J/4SZvjiF3S3sOwxqrUT5VKn1I+fF+415h9pc399QsrOlKtcV6ip0oRWrlJvRI7J2bZWt8oZTtMIpJOso79zU/OVX9J/uXgkdbCLPrFbWl7sf4jhY9iHVbfUi/alsXdxZkgAJoecgMGvNvmcfwTyPWja1d3EsR1trXR8YJr15/BPh4tGGvWjQpupLcjYtbedzVjShvbNBbfs3fhTnuvC3queH4drbW2j4Safrz+MvuSNdgHnFetKvUdSW9nr9rbwtqMaUNyWQABiM4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlOy3NdbJuc7PGIOUrfXsrumvr0ZfSXvXBrxSO1bO4o3drSurarGrQrQVSnOPKUWtU18DgE6R9F3OyvcOqZOxCr+UWkXVsXJ8Z0tfWh74t6rwfgSLAL3o59BLc93f9yG6WYZ0tNXcFtjv7ufh8u43kACYHngNI+ktkn5VZLN+HUV29ulC+jHT1qfSfi118GbuKK9KnXoVKNaEZ06kXCcZLVSTWjTNa7to3NJ05G7YXs7KvGtDhv7VxRweDM9ruTKuTc1VLanGTw651q2c/0esPfHl7tPEwwgVWlKlNwlvR6vQrwr041YPNMAAxmYAAAAAABcGAAZDhOPfJrZU6+s9OEe9H2fhNbfmpeZiQOnTxa5pxUU9iNGeHUJycmjP7HErS7hDcqpTkvovmfU5U/wA5E13b1JUqqlGTT7z2oTc4KUakmn4ncscVdaOUltRzq+GqEvZewyh9n+ciUvs/zkTGtZe3LzGsvbl5m71v9ph6n+4yT8X+ciQ+z9uJjnre1LzGsval5jrf7SvU/wBxkL7P24h9n7cTHtZe1LzJ1l7UvMdb/aV6p+4959n7aI9T20eFrL2peY1l7UvMdc/aV6r2nuPc9tFPqe2jxNX7T8xq/afmOt/tK9W7T2/U9pEaw9pHi6v2n5jV+0/Mdc/aV6v2nsvc9pFPqe0jydX7T8xq/afmOt/tHV+09V7ntIj1faR5fH2n5jj3sdc/aV6DtPTe77SKXu96PO4978xx735jrn7SvQ9p6Pq+0iGo96PP497Gr72OuftHRdp97Ue9FL3e9Hx8e9ha97Kdc/aV6PtPr9XvIaXefNo+9j4sr1z9pXU7T6OHeRqu8saeLGnvHXP2jUL/AA7ymc40qcqknwRaWreh5uLV9+oqEH6seZbK+1VnqmSnR15ZFE6jqVJVJPiynh3lgGDr/wC06OoX+HeOHeWAU69+0apf4d44d5YBTr37Rql/h3jh3lgFOu/tK6peeneRw7y18R8R139o1S7w7xw7y18R8R1z9o1S7w7xw7y18R8SnXOwapd+JHDvLfxHxKdb7BkXOHeOHeWwOtdhXIucO8cO8tgp1rsGRc4d5HDvKAU6z2DIrBQB1nsGRMnoi03q+JMnqyk4N9dOvPJbkXpZAAGiXAAAAAAAAyjZjlK5zlmqhhlNSjaw0qXdVfydNPj8XyRfTpyqSUI72Y6tWFGDqTeSRs/0Z8kb8pZyxGinFN08PjLTnylU/cvj4G/Sxh9pb2FjQsrWlGlb0KcadOEVwjFLRIvk9s7WNtSVNePeeXYheyva7qy8OxAAG0aRTUnGnTlOclCEU3KTeiSXNs4z2z5xlnLOtxe0Zt4fb/iLKL/Np/S98nq/I3X6TWdvmbLyyzYVkr7E4Pt3F8advya/pPh7te85eIlj99rSVvHhv9CeaK4bqRd3NbXsXdxfj/N4ABGiYgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+/L+LXuB41aYvh1V0rq1qqpTl4ro/BrVPwZ8AKxk4vNby2UVOLjJZpndGRsx2Wa8sWeOWL0hXh+Mp68aVRcJQfuf3aHtnJvo9Z8eVMy/NeIVt3CMSmo1HJ8KNXlGp7uj8NH0OslxR6Bht6ruipfmW/+dp5JjWGSw+5cF7r2ru+wAB0TkGM7SspWuccr18Kr6Qrr8Za1fzdVcn7nyfgceYrYXeF4jcYdf0ZUbm3m4VIPo1+1ddTuk1B6Quz/wCesPeZsJop4jaQ/KKcY8a9JdeHOUfvXA4eMWHTR6WC9pfFEo0cxXq1Tq9V+zLd2P0ZzWACInoIAAAAAAAAAAAAPRw24b0pS4rTg+484lNrk9DNb1nRnrIsqQU1ke9vR715jej3rzPC3pe0xvS9pnS/Ff2mt1XtPd3o968xvR715nhb0vaY3pe0x+K/tHVe093ej3rzG9HvXmeFvS9pjel7TH4r+0dV7T3d6PevMb0e9eZ4W9L2mN6XtMfiv7R1XtPd3o968xvR715nhb0vaY3pe0x+K/tHVe093ej3rzG9HvXmeFvS9pjel7TH4r+0dV7T3d6PevMb0e9eZ4W9L2mN6XtMfiv7R1XtPd3o968yd6PtLzPB3pe0xvS9pj8V/aOq9p728u9eY3l3rzPB3pe0xvS9pj8V/aU6p2nvby715jeXevM8Hel7TG9L2mPxX9o6p2nvb0e9EqS70eBvS9pjel7TH4r+0dU7T31OPevMnej3rzMf3pe0xvS9pj8U/aOqdpkG9HvXmN5d68zH96XtMb0vaY/FP2jqnae3dV40KDnqnJ/RR4ybbcpPVvmUNt822QY54lrP3TPSoqCLoKNXpzKRO+SyyRfql0FoFnX/ANpXVLoLQHX/ANo1S6C0B1/9o1S6C0B1/wDaNUugtAdf/aNUugtAdf8A2jVLoLQHX/2jVLoLQHX/ANo1S6C0B1/9o1S6C0B1/wDaNUukSehbBbO+co5JBRAANAuAAAAAAAAAL1lbXF7d0rS1oyrV601CnTiuMpPkjr3ZLkyhkzK1Ozek7640q3tVcd6enCK8FyRgXo5bPvkdvDN+L0V8orR/IKU48acGuNTj1fTuXvN3Eswaw6KPTTW17uxfcg2kOKdNPq9N+yt/a/t8wASd4i5B5macbscuYBeY1iVTctrWm5yS5yfSK8W9Eveemcueknn1Y9jv4NYZW3sNw6o+2lGXq1q64P3qPFLx1NHEb1WlFz48O86eE4dK/uFT/KtrfYa1zfj9/mfMV5jeIz1r3NRy3deEI8owXglojyADz2UnOTlLez1eEI04qMVkkAAWlwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpPRx2gfhDgn4OYpX1xXD6a7Kc5au4oLgn4yjyfho+85bPvy9i9/gOM2uL4ZWdG7tqinTl09zXVNcGu5m9h97KzrKa3ce45eL4bDELd037y2p8n6Pid6Ax3Z1mywznle3xmyajOXqXFHXV0aqXrRfh1T6poyI9Bp1I1IqcXmmeSVaU6M3Tmsmt4ABeYzmj0gNnbwHEJ5jwil/ky6nrXpxj/AMnqN8/5sn5P3mozuvEbO2xCwr2N7RhXt68HTq05xTUotaPgzkra3kO7yTjrjFSq4XctytK/PT9CXdJfeviRLF8O6GXTU17L39n2J/o9jHWIq3qv2lu7V6owkAHCJSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAES/AgnoVz2ZFCAAUKgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2dsJ2eSzVi6xfE6bWD2c+MXH/lFRfU9y5vyMe2X5Jv865gjZ0VKlZUWp3dx0pw7l3yfRfE67wTDLLBsKtsMw+hCha20FCnCK0SX+PidvCMO6eXS1F7K+P2I5juLdWh0NJ+2/gvX/s+uEVCKjFaJLRLuKgCXkCIJB4mdsyYflPLd1jeIy/FUI+pTT9arN/RhHxb8uLLZzjCLlJ5JF1OnKpJQis2zCvSBz+spZd+bMOrbuM4hBxpOL40KfKVTwfSPjq+hyW22229Wz1c35gxHNGYbvG8Uq79xcT13V9GnH6sI9yS4Hknn+JXzvK2t+Vbj1TCMNjYW6h+Z7W+30QABzzqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGa7IM9XWRszRum51MNuNKd7QT+lHpJL2o818V1Ox8OvLXELChfWVeFxbXFNVKVSD1U4tapo4DNx+jvtK/B+9jljG7jTCbmp+T1Zvha1W+vdCT59z49WSDBcS6GXQ1H7L3dj9CJ6S4L1mHWaK9tb1zXqvl4HUIIRJMjzkHk5sy/huZsEr4TilFVKFVcJL6UJdJRfej1gWyippxktjL4TlTkpReTRxbtAyjiWTsfqYZfrtKbbdvcRWka0O9dz710MdO0s/ZSwzOGBVMNxCG7P6VCul61KfRr/A5GzjlvFMq47WwnFaLhVhxhNL1KsOk4vqn9xC8Sw52staPuv4dh6Rg2MRvoak9k1v7e1fU8YAHLO4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+8gAq3m8wAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPbyXlnE82Y9RwnDKa35tOpVkvUow6yl/h1PmyzgeJZixqhhOFW8q9zWeiXSK6yk+kV1Z1xs1yTh2SsDjZWqVW6qaSublrjUl+5Lojp4bh8rueb91b/Q4+L4rGxp5R2ze5fVn3ZIythmUsBo4VhtP1YrWrVkvWqz6yf+HQ90AmsIRhFRiskjzqpUlUk5zebYIJBcWFq5rUra3qXFerClRpRc6k5vRRilq233aHIG23aDWzvmJwtZzhg1nJxtKb4b75OrJd76dy+JmXpHbS1iFerk/AbnWzpS0xCvTfCtNfyafsp8+9+C46MIfjeJdK+gpvYt/a/sT7RzB+hirmsvae5clz738gACOktAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOjvR12nfL6VHKGYLnW7prdw+4m/wCNiv5KT9pdH1XDmuO9T8/qVSdKpGrSnKE4NSjKL0cWuTT7zqnYPtPhmuxhgeNVoxxy3h6s3w+VwX1l+muq6811JbguKa6VCq9vB8+wgGkeB9G3dUF7PFcu3u58vltkAgkpDSTGNouS8LzrgcrC/j2dxT1la3UY6zoz/fF9V195k4LKlONSLhJZpmWlVnRmpweTRxHm/LmK5WxurhOL0OyrQ4wkuMKsek4vqn93JnjnZ+0LJuFZ0wV2GI09ytDWVtcxXr0ZeHen1XU5OzvlTFso4zPDcUo6c3RrRXqVo96f7uhDMRw2VrLWjti/5tPRsIxiF9DVlsmt659qPBAByztgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9LLeB4nmLGKGE4TbSuLqs+CXBRXWUn0iurLuUsuYrmjGaeF4TburVnxnJ/Rpx9qT6I6w2Z5FwvJOEfJ7VKte1kndXUl61R9y7orojpYfh07uWb2RW9+hyMVxanYwyW2b3L6v+bSnZfkTDckYN8nobtxiFZJ3d246Oo/Zj3QXRfFmYAE0pUo0oKEFkked1q0683UqPNsAAyGMhmmfSE2nrAbSplfArj/K1eGl1Wg/+Swa5J9JteS8Wj29t+0y3yVhjw/Dpwq49cw/FQ5q3i/5SS/Yur8Ecl3dxXu7qrdXNWdavVm51Kk5aylJvVtvqyO4zivRJ0KT9ri+X3JZo9gnTNXNdeyty59vd8y2229XzIAIeT4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF+wu7mwvaN7Z16lC4ozU6dSD0lGS5NMsAqnk80UaTWTOutim021zrhysMQnToY7bw/G0+SrxX8pD966e42UcCYZfXmGYhQxDD7mpbXVCanSq03pKMl1Os9jG06zztYKyvXTtsdoQ1rUVwjXS/lIfvj09xMcJxZV0qVV+1wfP7nneP4A7VuvQXscVy+3yNkAAkBFAeJnLK+EZrwephmL26qQkvxdSPCdKXSUX0a/xR7YLZwjOLjJZpl9OcqclODyaON9o+RMYyVifY3kHXsqktLe7hH1J+D7peHkYmdz43hVhjWGVsNxO2p3NrWjpOnNar3rua7zmPazsqxHKVWpiWGdpfYK/W30tZ2/hNdV+l5kRxHCZUM6lLbH5fYn2EY9C6ypVtk/g/ua0ABxSSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyTIGTMYzliqs8NpbtGDXb3M1+LpJ9/e/A93ZTsxxTOVxC8ud+ywWL1ncNetV0+rTXXjw3uS48zqLLeCYbl7CKWF4Tawt7akvoxXGT6yb6t952cOwmVxlOpsj8yP4tjcLTOnS2z+C+/YefkPJ+D5OwaGH4XR1nprXuJr8ZWl1cn+xdEZEAS6EI04qMVkkQOpUnVk5zebYABeWAwDbFtHsci4R2dLs7nGrmD+S2zfCK5dpPuiu76z4Lq1Vte2kYdkXCtyO5dYxcQfyW115L85PuivN8l1a5Fx3FsQxzFrjFMUuZ3N3cT36lSb4t93glySXBI4WLYsrddFSft/L7kmwLA3dtVqy9hfH7FGL4je4tidxiWI3NS5u7ibnVqzerk3/95dD5ACFttvNnocYqKyW4AAoVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9GG313ht/Rv7C4qW11Qmp0qtOWkoyXVHzgqm080UaTWTOs9i+1azzjb08KxWVO1x2nH6PKF0l9aHdLvj5eG0fE/P8At61a3rwr29WdKrTkpQnCTjKLXJprkzpjYptioY5CjgGaK8KGK8IULqWkYXXcpdIz+5+8l2FYyquVKu/a4Pn9yAY5o66Ode2WceK5d3Z8u7dugAEjIiCmpCFSnKnUjGcJJxlGS1TT5poqABojaxsWjNVsZydSUZpOdXD9dFLvdPuf6PXoaGuKNW3rzoV6c6VWnJxnCa0lF9zXQ7wME2mbMsEznSdw0rHFEkoXdOK9bTpNfWX3kfxDBlUznQ2PlwJVhWkUqWVK52rnxXfz+ZyMD386ZQx3KOIfI8ZtHTUv4qvDjSqrvjL9z4ngEXnCUJOMlkybU6kKsVODzTAALS8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHs5SyxjeacSVhgtlO4qc5z5U6a75S5L9pdGEpvVis2WTnGnFyk8kjyKcJ1KkadOEpzk1GMYrVtvkkjeGybYtUuOxxnOFKVOi1v0cP8ArS7nU7l13fdr3Gf7L9lWD5PjC+uXHEcX63E4+rS4cqafLrx5mxEtFoSfD8FUMp19r5epDsU0hc86VtsXPj4ci3bUKNtb07e3pQpUacVGEILSMUuSS6FwAkKRFW8wAAUDNc7YtqGH5IsZWdr2d5jlaGtK311jST5Tqdy7lzfguJ5O2ja9aZWhWwTAJ0rrG2t2pU4Sp2nv75/o8l17ny7f3d1f3la8vbipcXNabnUq1Jb0pyfNtsj+K4yqOdKi/a4vl9yVYJgDuMq9wsocFz+3zL2N4pf41ilfE8Tuql1d3E9+pVm+Lf7l0SXBI+IAhzbk82T+MVFZJbAAChUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEp6PgQADe2xnbVUsY0cAzjXnVtVpC3xCWrnS6KNTq4/pc111XLouhWpV6MK1CpCrSqRUoThJOMk+TTXNH5/Gx9ku1bFslVY2N1v4hgkpetbOXrUdecqbfL+byfg+JI8Mxt08qVfaufLvIhjOjca2da1WUuK4Pu5P4HXoPKyrmHCMzYRTxTBbyF1bT4PThKEusZR5xfgz1SWxlGaUovNEDnCUJOMlk0AAXFp8OOYRhuNYfUsMUs6N3bVFpKFSKa967mc+7S9iV/hbqYjlR1L+zXGVpJ61qfP6Ptrl4+86QBp3djRullNbefE6FjideylnTezk9xwbUhOnUlTqQlCcXpKMk00+5p8ik692hbMsuZwpyrVqCssR09W8oR0k+7eXKS9/HnxRznn7ZxmTKFWdS7tndWCfq3lCLcNOP0usXw68CJ3mF1rbbvjzX1J1h+NW95lHPVlyf05mHAA5p2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATGMpSUYpylJ6JJatvuMpyNkDMeb60Xhtm6dprpO7rJxpR9z+s/BHRmzzZVlzKSjcyprEsTS43VeP0ef0I8o/t8To2eGVrrallHm/5tOTf4xb2exvOXJfXkak2abFsVxx08QzG6uF4c/WjR00r1VpquH1F4vidEZdwHCcvYdDD8HsqVpbw6QXGT72+bfiz0gSy0sKNqvYW3nxIPf4nXvZe29nJbgADdOeADzsxY1heX8Kq4pjF5TtLSkvWnN830SXNt9EuJSUlFZt7CsYSm1GKzbPvqTjThKc5RjGKblKT0SS5vU0Btm22aKtgWS7nVvWFxiUPvjS/wB/y7zDdr+13E83yq4VhXaYfgeujhrpUuPGbXJfor469NWkTxPG3POlbvZz9CcYPo4qeVa6Wb4R9fQqnKU5uc5OUpPVtvVtlIBGiYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHuZNzXjmUsVjiOCXkqFTlUpvjTqx9mceq+9dNDqXZbtVwPOlOFpUlHDsYUfWtKkuFR9XTf1l4c14nHxVSqTpVI1Kc5QnBqUZRejTXJpnSsMTrWbyW2PL05HIxPBqGIRzlslzX15n6BEnNuy3brdWMaOFZy7S8tlpGF/Fa1YL9NfXXjz950PhOJWGLYfSxDDbujd2tZa06tKSlF//e7mTSzv6N3HOm9vLiedX+F3FjPVqrZwfBn1AA3DngpqQhUhKFSEZwktJRktU14oqABqfP2xLAsalUvcBmsHvZNycFHWhN8ecfq/DyNC5xyXmPKdw6eM4dUpUm9IXEPWoz90lw68nodpFu5t6F1bzt7mjTrUZrSdOpFSjJeKZyLvBqNfbD2X8PI71jpBc22UZ+1Ht3+fqcHg6ZzxsNwDFXUusv1fme6lx7LTet5P+bzj8O7kaPzhkDNWVpTlieF1XbRf/KqK7Sl8WuXx0I3dYbcW22SzXNEwssXtbvZCWT5PY/v4GLAA0DpgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGR5RyRmfNM18z4VWqUW9HcVFuUVz+s+fLpqbvyPsJwbD3Tusy3LxW4XHsIawoRfj1l07lz5m9bYdXufcWzm9xzrzFba02Tlm+S2v7eJonKWUswZpuuwwTDatyk9J1Wt2lD+dN8F+03xkPYZg2FuneZkrLFbpcewitKEX4rnP48OfA21Y2lrY2sLWytqNtb01pClSgoxivBIvkktMGo0fan7T+HkRG+0guLjONP2Y9m/z9CijSpUKUaNGnCnTgtIwhFKMV4JcisA7GRwN4AAABYxC9tMPsqt7fXNK2tqMd6pVqzUYwXe2zn/alt4nUVXC8la04cYzxGcdJP/w4vl/OfHuSNS7vqNpHWqPw4s3rHDa99PVpLvfBGzdp203AckW8qVaavcVcdaVjSkt5a8nN/Uj976I5Xz3nPHs54r8uxm7cox17G3hwpUV3Rj+98WeBc161zXqXFxVqVq1STlOpUk5Sk3zbb4tlshd/ilW8eT2R5ep6HhmC0LBay2z5+nIAA5h2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZJkbOuYcm3/wApwW9lTpyetW3n61Gr/Oj+9aPxMbBfTqSpyUoPJox1aUKsHCazT5nXWzXbDlzNnZWV5OGE4tLRdhWn+Lqy/wBXN8H/ADXo/ebKPz7Nn7Ods+ZMsKlZYjJ4xhkeCpVp/jaa/Qnz+D1RJrHSD8lwvFfVehDcS0V3ztH/APxf0fr5nWwMWyLn3LOcrZTwfEIu4S1qWlb1K0P6PVeK1RlGpJqdSFSOtB5oh1WjOjJwqLJrmSAC8xgplGMouMkpRfNNaplQANeZw2QZOzA51qVo8Ku5L+Os9IpvvcH6r+407m3Ylm3CHOthkaWM2yfDsHu1UuPOD58O5/A6lBzbjCravtayfNHXtMbu7bYpZrk9v3OELy1ubK4lb3ltWtq0HpKnVg4SXvT4lk7hx7L+C49buhjGF2t7Bpr8bTTa156Pmn7maxzRsEy/e9pWwK/ucLqvVqlU/G0tei4+sl8TiV8CrQ203rfBkktdJrepsqpxfmvX4HNgNh5m2O53wXtKlKwhidvHV9pZz3nourg9JfBamBX1pdWNxK3vbata1oPSVOtTcJJ+5nIq29Wi8qkWjvULmjXWdKSfcWQAYTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXbS3uLuvGhaUKtxVm92MKUHKTfdojOctbIs8Y3uVHhiw6hLR9rez7Phro/V4y18GkZaVCpVeUItmGtc0qCzqSS7zAS5bUK91Xhb21GpXrTekadOLlKT7klxOissbAcFtXCrj+KXGIzXF0qC7Klz7+MmtDaGXstYBl+iqWDYTaWS00cqdNbz46rWT4v4s69DAq89tR6q82cK50lt6eyknJ+S9fgc15S2L5wxpRrXtGng9s+O9dP8Y1prwguPnobjyhsayfgThWu7eeMXUf5S7S3E+PKmuHXrryNkA7lthNtQ25ZvtI3d45d3OzW1VyWz47ymlThSpxp04RhCK0UYrRL4FQB0jkbwAAAAY7nXOuXMn2nb45iNOjUa1p28PWrVP5sFx+L0XiW1KkacdabyRkp0p1ZKEFm3yMiNe7SdrGWsnRqWiqrEsVitFaUJL1H/AKyXKPu4vwNK7Rtt+YMwqrZYIp4Nh0tU3TnrXqL9Ka+j7o+ZqeTcpOUm23xbfUjV9pAl7NuvF/REuw7RdvKd28uxfV+nmZTtAz9mPOt32mLXbjaxlrRtKWsaNP4dX4vVmKgEXqVJ1ZOU3myZ0qMKMFCmskgACwyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF21uK9rXhcW1apRrU3rCpTk4yi+9NcUbgyDt6xzClTtMzUHjFouHbxajcQXv5T+Oj8TTQNi3u61tLWpSyNS7saF3HVrRz+fmdxZMztlrN1squB4nSr1EtZ28/UrQ98Hx+K1XiZFqcAWtxXtbiFxbVqlCtB6wqU5OMovvTXFG2Mj7eMzYPuW2O0443aLhvzluV4r+elpL4r4ok1ppDCXs11k+a3fzzIdfaKVIZytpay5Pf57n8DqgGF5K2n5OzXGFOxxONtdyX/JLvSlU17lq9JfBszPUkFKtTqx1qbzRFq1vUoS1KkWn2kgAyGIAAAHx4phWG4pSVLEsPtbyC4qNekppcNOGvI+wFHFNZMrFuLzRrXHtimRsSjKVtaXGF1XHRStar3U+/dlqmYHj3o+YlT354Jj9tcpabtK6punLx1lHVfcdDA0KuF2tXfDLu2HToY1e0d08+/b8zkLHNk+fcJ7SVTAql3Sh/KWk41U/FJet9xiOIYbiOH1pUb+wurWpD6Ua1GUGvfqjust3FGlcU3Sr04Vqb4OFSKlF/B8Dn1dH6b9ybXft9DrUdKaq/xIJ92z1ODlo+TT9wO0MUyFkzE913uWcMqOPJxoqDX6uhjN/sQyFcynKla31o5Lh2N3LSL8E9UaM8Arr3ZJnRp6T20vfi15P6nKoOir30esGlCXyPMeI0pN+r21GE0vfppqeHc+jxikYSdtmayqS+qqltKCfxTf7DWlhF3H8mfijchj1hL8+Xg/Q0iDbFbYHnSE2qd5gtRdGq81r5wPOuNie0GnLSnh9nWXfC9pr+80YJYfdR3035GzHFLOW6qvM1wDPZ7HtokZOPzApeMbuk1/eI/gg2if6Pv/ANVS/wB4s6lcf035Mv8AxC0/qx80YGDPP4INon+j7/8AVUv94mGx7aJKSj8wJa9ZXdJL+8Op3H9N+TH4haf1Y+aMCBsehsT2g1JaTw20orvne03+xs9GlsDzrOUVO7wamnzbuJvTygXxw+5lupvyLJYpZx31V5mpwbttvR5xaVNO5zLYUp9VTt5zXm2v2Ht2Xo84TGC+W5kv6stePY0IQWn9LeM0cIu5fk+KNeeO2Mfz5+D9DncPhz4e86nsdh2RLfcdahiF3KPN1bppS96jojJ8L2fZKwyTlZ5Yw2EmtHKdHff9rU2oYDcP3mkaVTSa1j7sW/JfU45sbG9vqsKVlZ3FzUm9IxpUpTb92iMtwTZXnzFt2VHL9e3pylpv3clRS8dJPXT3I67tbeha0lStaNOhTS0UKUFCPki6btPR+mvfm33bPU59XSiq/wDDppd+30OdsC9HzF6rhPGsdtLWO961O2g6stPCT0SfwZneA7EMk4coyvKN3itRLRu5q6Qfjux0Rs4HRpYVa0t0c+/acqvjV7W3zyXZs+58GEYNhOEUuzwvDbSyi0k1QpKGunBatcX8T7wDfUVFZJHMlJyebeYABUtAAKlQB95iOdNo+UcpxlHFMVpzukuFrbfjaz+C4R5fWaRjqVYUo603kjLSo1K0tWnFt9hl3I8LNubsu5VtHc47ilG0TWsKbe9Vn/NguLOfM8bfMwYp2ltly3jg1s9V2zancSXv5R+Cb8TUV/eXd/dTur25rXNeo9Z1Ks3KUve2R+70hpw9mgs3ze71+RJ7HRarUylcvVXJb/RfE3Nn7b9iuIKpZ5UtXhlu+HyqtpKvJeC+jD734mmL68ur+7qXd7cVbi4qPWdSrNylJ+LZYBGbm8rXMs6ss/kTC0sLezjq0Y5fPzAANY3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACU2nqmZxk3atnPLG5StsTleWkf+rXmtWGng9d6PwaMGBlpVqlGWtTeTMNe3pV46tWKa7Tp/KHpAZcxHcoZgsq+EV3wdSH46i/ilvLyenebXwXGcJxq2VzhOI2t9Sa13qFVT096XFfE4JPqw7EL7DbmNzh95cWlaL1VSjUcJL4o7dtpDWhsqrW+D9CN3eitvU20JOL5b16/E77BydlbbrnXCNyliE7fGbeOiauYbtTTwnHR6+MlI2nljb7lHEVCni1veYPWeiblHtaWr/Sjx08Wkd23xq0rfm1X2/wAyI3daPX1vt1dZdm34b/gbeB5eB5iwLHKSqYPi9lfJrlRrKUv1ef3HqHTjNSWcXmjjThKDyksmAAXFgAAAAAAAAKgAAAAAAAAAAAAAAAAAqAAVAAAAA4HnY1juDYLRdXFsUs7GCWv4+qovTwXNlspRis5PIujGUnlFZs9EGp8zbecm4Yp08Njd4xWWqXYw7Onr/Ol09yZqvNO3rOOKb9LC4WuC0HwXYx7Srp/Pl+1JHMuMZtKP5s32bfsdi1wC+uNurqrt2fDf8DqDFsVw3CbV3WKX9tZUEtXOvUUF9/P4GrM37fMrYXv0cDt7jGrhapSX4qin/Oa1fwWnicy4tiuJYtdSusTv7m8ryerqV6jm/vPjOHc6Q1p7KS1fi/QkdporQp7a8nJ8ty9fkZ/nLa7nTMqnRniHzdaS4fJ7LWmmvGWu8/izApylOTlJttvVtvmykHCq16laWtUk2yS0Lalbx1aUUl2AAGIzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFyjWq0aiqUak6c1ylCTT80Zll/atn3BN2NtmC4uKS0/FXaVeOnd62rS9zRhIMlOtUpPOEmu4w1relWWVSKfeszfGAekZiMNyGO5eta619apaVXTaX82WqfmjO8E27ZFxDcjdVr3DJyemlxQ3or+lDVHJgOnSxy7p73n3o49fRuwq7VFx7n65ndGEZxyti8YvDsw4ZcOT0UVcRUm/wCa9H9x7ieq3ly7z8/E2nqnxPTwvMWP4W28NxvEbPXn2FzOH7GdGnpI/wA9PyZyauiK/wBOr5r+fI7x4A44wzbFtFsd1RzDUuIR+rcUadTX3tx3vvMmw30hs3UZJX2G4Rdw/Rpzpy81Jr7jep6QWsvezXh6HOqaLXsfdafj6o6hBz9h/pHxctL/ACrux76F3q/KUUezZ+kRlepNRucHxagvaW5NfczajjFnL8/zNOeA4hDfT+Kf1N0A1bS28ZBnpv18Sp++zb08mehbbZtndaKfz92fhUtqi/cZo4hay3VF5mtLC7yO+lLyZsIGER2s7PJf9qLRe+FT/dKltW2ef6VWP6tT/dMnXLf+ovNFnULr+lLyfoZqDCntW2eL/tVY/q1P90pltZ2eL/tRaP3Qqf7o65b/ANReaHULr+nLyZm4Nf3G2XZ3RTfz+qmnSnb1H+486rt3yBDXcuMSqfzbNrXzZY8QtY76i80ZI4XeS3UpeTNog0ze+kPlWlPdtsIxa4Xe1CC+9njYh6R9NPSwytKS7693p90YswSxizj+f5mxDAb+e6m/gvqb/BzDiXpD5rqvSxwrCbWP6cJ1X57y/YYzie2baLfOS+f3bQl9S3t6cNPc93e+81Z6QWsd2b8PU3Kei97P3sl4+iZ2J0146Hi4vmzLOERk8Sx/DbZx+lGdzHeX9FNv7jirFMzZixXhiWO4neLnpWupzXk2eU5NvVvVvqzSqaSP8lPzZ0aWiP8AUqeS/nyOtsb25ZDw7fjb3V3iVSPS2oNRf9KWiMEx/wBI27lvwwLLtCl7NW8qub+MI6f3jQYOdVxy7qbnl3L1OtQ0bsaW1xcu9+mRneYNrmf8Z3o1cerWlJv+Ls4qil4ax9Zr3tmE3FxXuKjq3FapWqPnKpJyb+LLQOZVr1KrznJvvOxRtqNBZU4pdyAAMRmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOrIABOr72NX3sgAE6vvY1feyAATq+8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9k=" 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 ? '&nbsp;&nbsp;↳ ' : '';
$parent_opts .= "<option value='{$c->id}'$sel>$indent" . esc_html( $c->name ) . "</option>";
}
$role_opts = '';
foreach ( $roles as $k => $r ) {
if ( $k === 'banned' || $k === WBF_Roles::SUPERADMIN ) continue;
$sel = ( $edit && ( $edit->min_role ?? 'member' ) === $k ) ? ' selected' : '';
$role_opts .= "<option value='$k'$sel>" . esc_html( $r['label'] ) . " (Lvl {$r['level']})</option>";
}
echo '<h2>' . ( $edit ? 'Bearbeiten: ' . esc_html( $edit->name ) : 'Neue Kategorie' ) . '</h2>';
echo '<form method="post"><table class="form-table">';
wp_nonce_field( 'wbf_cat_nonce' );
if ( $edit ) echo '<input type="hidden" name="cat_id" value="' . esc_attr( $edit->id ) . '">';
echo '
<tr><th>Elternkategorie</th><td><select name="parent_id">' . $parent_opts . '</select></td></tr>
<tr><th>Name *</th><td><input name="name" value="' . esc_attr( $edit ? $edit->name : '' ) . '" class="regular-text" required></td></tr>
<tr><th>Beschreibung</th><td><textarea name="description" rows="2" class="large-text">' . esc_textarea( $edit ? $edit->description : '' ) . '</textarea></td></tr>
<tr><th>Icon</th><td><input name="icon" value="' . esc_attr( $edit ? $edit->icon : 'fas fa-comments' ) . '" class="regular-text">
<p class="description">z.B. <code>fas fa-home</code>, <code>fas fa-bug</code></p></td></tr>
<tr><th>Min. Rolle</th><td><select name="min_role">' . $role_opts . '</select>
<p class="description">Mindest-Rolle zum <em>Posten</em> in dieser Kategorie.</p></td></tr>
<tr><th>Für Gäste sichtbar</th><td>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" name="guest_visible" value="1"' . ( !$edit || (int)($edit->guest_visible ?? 1) ? " checked" : "" ) . ' style="width:16px;height:16px;accent-color:#0ea5e9">
<span>Nicht eingeloggte Besucher können diese Kategorie sehen</span>
</label>
<p class="description">Deaktivieren = Kategorie und ihre Threads sind nur für eingeloggte Nutzer sichtbar.</p>
</td></tr>
<tr><th>Reihenfolge</th><td><input name="sort_order" type="number" value="' . esc_attr( $edit ? $edit->sort_order : 0 ) . '" class="small-text"></td></tr>';
echo '</table>';
submit_button( $edit ? 'Speichern' : 'Erstellen', 'primary', 'wbf_save_cat' );
echo '</form></div>';
}
// ── Mitglieder ────────────────────────────────────────────────────────────────
function wbf_admin_members() {
$roles = WBF_Roles::get_sorted();
// ── Neuen Nutzer anlegen ──────────────────────────────────────────────────
if ( isset( $_POST['wbf_create_user'] ) && check_admin_referer( 'wbf_create_user_nonce' ) ) {
$username = sanitize_user( $_POST['username'] ?? '' );
$email = sanitize_email( $_POST['email'] ?? '' );
$display_name = sanitize_text_field( $_POST['display_name'] ?? '' );
$password = $_POST['password'] ?? '';
$role = sanitize_key( $_POST['role'] ?? 'member' );
$errors = [];
if ( strlen($username) < 3 ) $errors[] = 'Benutzername mindestens 3 Zeichen.';
if ( ! is_email($email) ) $errors[] = 'Ungültige E-Mail-Adresse.';
if ( strlen($password) < 6 ) $errors[] = 'Passwort mindestens 6 Zeichen.';
if ( empty($display_name) ) $errors[] = 'Anzeigename darf nicht leer sein.';
if ( WBF_DB::get_user_by('username', $username) ) $errors[] = 'Benutzername bereits vergeben.';
if ( WBF_DB::get_user_by('email', $email) ) $errors[] = 'E-Mail bereits registriert.';
if ( $role === WBF_Roles::SUPERADMIN ) $errors[] = 'Superadmin kann nicht manuell vergeben werden.';
if ( empty($errors) ) {
$avatar = 'https://www.gravatar.com/avatar/' . md5(strtolower($email)) . '?d=identicon&s=80';
$new_id = WBF_DB::create_user([
'username' => $username,
'email' => $email,
'password' => password_hash($password, PASSWORD_DEFAULT),
'display_name' => $display_name,
'avatar_url' => $avatar,
'role' => $role,
]);
echo '<div class="notice notice-success is-dismissible"><p>✅ Nutzer <strong>' . esc_html($display_name) . '</strong> wurde angelegt (ID: ' . (int)$new_id . ').</p></div>';
} else {
echo '<div class="notice notice-error is-dismissible"><p>❌ ' . implode('<br>', array_map('esc_html', $errors)) . '</p></div>';
}
}
// ── Rolle ändern ──────────────────────────────────────────────────────────
if ( isset( $_POST['wbf_change_role'] ) && check_admin_referer( 'wbf_member_role_nonce' ) ) {
$uid = (int) $_POST['user_id'];
$role = sanitize_key( $_POST['new_role'] );
if ( $uid && $role !== WBF_Roles::SUPERADMIN && array_key_exists( $role, WBF_Roles::get_all() ) ) {
$update = ['role' => $role];
if ( $role === 'banned' ) {
$update['ban_reason'] = sanitize_textarea_field( $_POST['ban_reason'] ?? '' );
// Zeitlich begrenzte Sperre
$ban_until_raw = sanitize_text_field( $_POST['ban_until'] ?? '' );
if ( $ban_until_raw ) {
$ts = strtotime( $ban_until_raw );
if ( $ts && $ts > time() ) {
// temp_ban speichert pre_ban_role automatisch
WBF_DB::temp_ban( $uid, date('Y-m-d H:i:s', $ts), $update['ban_reason'] );
echo '<div class="notice notice-success is-dismissible"><p>✅ Nutzer temporär gesperrt bis ' . esc_html(date_i18n('d.m.Y H:i', $ts)) . '.</p></div>';
goto after_role_save;
}
}
// Permanente Sperre — pre_ban_role sichern
$existing = WBF_DB::get_user( $uid );
if ( $existing && $existing->role !== 'banned' ) {
$update['pre_ban_role'] = $existing->role;
}
$update['ban_until'] = null;
} else {
$update['ban_reason'] = '';
$update['ban_until'] = null;
$update['pre_ban_role'] = '';
}
WBF_DB::update_user( $uid, $update );
echo '<div class="notice notice-success is-dismissible"><p>Rolle aktualisiert!</p></div>';
after_role_save:;
}
}
// ── Nutzer löschen (DSGVO Art. 17 / Hard Delete) ─────────────────────────
if ( isset( $_POST['wbf_delete_member'] ) && check_admin_referer( 'wbf_delete_member_nonce' ) ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
$mode = sanitize_key( $_POST['delete_mode'] ?? 'anonymize' );
if ( $uid ) {
$target = WBF_DB::get_user( $uid );
if ( ! $target ) {
echo '<div class="notice notice-error is-dismissible"><p>&#10060; Nutzer nicht gefunden.</p></div>';
} elseif ( $target->role === WBF_Roles::SUPERADMIN ) {
echo '<div class="notice notice-error is-dismissible"><p>&#10060; Der Superadmin kann nicht gelöscht werden.</p></div>';
} else {
global $wpdb;
$name = esc_html( $target->display_name );
if ( $mode === 'hard' ) {
// Alle nutzerbezogenen Daten entfernen
$dep_tables = [
'forum_messages' => [ 'from_id', 'to_id' ],
'forum_notifications' => [ 'user_id', 'actor_id' ],
'forum_subscriptions' => [ 'user_id' ],
'forum_bookmarks' => [ 'user_id' ],
'forum_likes' => [ 'user_id' ],
'forum_reactions' => [ 'user_id' ],
'forum_reports' => [ 'reporter_id' ],
'forum_poll_votes' => [ 'user_id' ],
'forum_remember_tokens' => [ 'user_id' ],
'forum_user_meta' => [ 'user_id' ],
'forum_ignore_list' => [ 'user_id', 'ignored_id' ],
];
foreach ( $dep_tables as $tbl => $cols ) {
$full = $wpdb->prefix . $tbl;
if ( $wpdb->get_var( "SHOW TABLES LIKE '$full'" ) !== $full ) continue;
foreach ( $cols as $col ) {
$wpdb->delete( $full, [ $col => $uid ], [ '%d' ] );
}
}
// Threads/Posts auf user_id=0 setzen (Inhalt bleibt)
$wpdb->update( "{$wpdb->prefix}forum_threads", [ 'user_id' => 0 ], [ 'user_id' => $uid ], [ '%d' ], [ '%d' ] );
$wpdb->update( "{$wpdb->prefix}forum_posts", [ 'user_id' => 0 ], [ 'user_id' => $uid ], [ '%d' ], [ '%d' ] );
// Nutzer-Datensatz hart loeschen
$wpdb->delete( "{$wpdb->prefix}forum_users", [ 'id' => $uid ], [ '%d' ] );
echo "<div class='notice notice-success is-dismissible'><p>&#128465; Nutzer <strong>{$name}</strong> dauerhaft geloescht. Threads/Posts anonym gesetzt.</p></div>";
} else {
// DSGVO Art. 17 Anonymisierung (Standard)
$ok = WBF_DB::delete_user_gdpr( $uid );
if ( $ok ) {
echo "<div class='notice notice-success is-dismissible'><p>&#9989; Nutzer <strong>{$name}</strong> anonymisiert (DSGVO Art. 17). Threads und Beitraege bleiben erhalten.</p></div>";
} else {
echo "<div class='notice notice-error is-dismissible'><p>&#10060; Anonymisierung fehlgeschlagen.</p></div>";
}
}
delete_transient( 'wbf_flood_' . $uid );
delete_transient( 'wbf_flood_ts_' . $uid );
}
}
}
// ── Profil bearbeiten ─────────────────────────────────────────────────────
if ( isset( $_POST['wbf_edit_user'] ) && check_admin_referer( 'wbf_edit_user_nonce' ) ) {
$uid = (int) $_POST['user_id'];
$display_name = sanitize_text_field( $_POST['display_name'] ?? '' );
$email = sanitize_email( $_POST['email'] ?? '' );
$new_password = $_POST['new_password'] ?? '';
if ( $uid && $uid !== 0 ) {
$user = WBF_DB::get_user( $uid );
if ( $user && $user->role !== WBF_Roles::SUPERADMIN ) {
$update = [];
if ( ! empty($display_name) ) $update['display_name'] = $display_name;
if ( is_email($email) ) $update['email'] = $email;
if ( strlen($new_password) >= 6 ) $update['password'] = password_hash( $new_password, PASSWORD_DEFAULT );
// Bio + Signatur
if ( isset($_POST['bio']) ) $update['bio'] = sanitize_textarea_field( $_POST['bio'] );
if ( isset($_POST['signature']) ) $update['signature'] = mb_substr( sanitize_textarea_field( $_POST['signature'] ), 0, 300 );
if ( ! empty($update) ) WBF_DB::update_user( $uid, $update );
// Benutzerdefinierte Profilfelder speichern
$cf_defs = WBF_DB::get_profile_field_defs();
foreach ( $cf_defs as $def ) {
$key = sanitize_key( $def['key'] );
if ( ! isset($_POST[ 'cf_' . $key ]) ) continue;
$raw = $_POST[ 'cf_' . $key ];
$value = $def['type'] === 'url' ? esc_url_raw(trim($raw))
: ($def['type'] === 'textarea' ? sanitize_textarea_field($raw)
: ($def['type'] === 'number' ? (is_numeric($raw) ? (string)(float)$raw : '')
: sanitize_text_field($raw)));
WBF_DB::set_user_meta( $uid, $key, $value );
}
echo '<div class="notice notice-success is-dismissible"><p>Profil von <strong>' . esc_html($user->display_name) . '</strong> aktualisiert!</p></div>';
}
}
}
// ── Admin: einzelnen Ignore-Eintrag entfernen ─────────────────────────────
if ( isset( $_POST['wbf_admin_remove_ignore'] ) && check_admin_referer( 'wbf_admin_ignore_nonce' ) ) {
if ( current_user_can('manage_options') ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
$ignored_id = (int) ( $_POST['ignored_id'] ?? 0 );
if ( $uid && $ignored_id ) {
global $wpdb;
$wpdb->delete(
"{$wpdb->prefix}forum_ignore_list",
[ 'user_id' => $uid, 'ignored_id' => $ignored_id ],
[ '%d', '%d' ]
);
echo '<div class="notice notice-success is-dismissible"><p>Ignore-Eintrag entfernt.</p></div>';
}
}
}
// ── Admin: gesamte Ignore-Liste eines Users leeren ────────────────────────
if ( isset( $_POST['wbf_admin_clear_ignores'] ) && check_admin_referer( 'wbf_admin_ignore_nonce' ) ) {
if ( current_user_can('manage_options') ) {
$uid = (int) ( $_POST['user_id'] ?? 0 );
if ( $uid ) {
global $wpdb;
$wpdb->delete( "{$wpdb->prefix}forum_ignore_list", [ 'user_id' => $uid ], [ '%d' ] );
echo '<div class="notice notice-success is-dismissible"><p>Ignore-Liste vollständig geleert.</p></div>';
}
}
}
// ── 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 &amp; Beiträge bleiben unter „Gelöschter Nutzer" erhalten.
</span>
</span>
</label>
</div>
<button type="submit" name="wbf_delete_member" class="button"
style="font-size:.8rem;height:28px;line-height:28px;background:#fff8f0;border-color:#f97316;color:#c2410c"
onclick="return confirm('Nutzer <?php echo esc_js($m->display_name); ?> anonymisieren?\n\nDer Account wird nach DSGVO Art. 17 anonymisiert. Alle Inhalte bleiben anonym erhalten.')">
📋 Anonymisieren
</button>
</form>
<!-- Option B: Hard Delete -->
<form method="post">
<?php wp_nonce_field( 'wbf_delete_member_nonce' ); ?>
<input type="hidden" name="user_id" value="<?php echo (int)$m->id; ?>">
<input type="hidden" name="delete_mode" value="hard">
<div style="background:#fff;border:1px solid #fca5a5;border-radius:6px;padding:10px 12px;margin-bottom:6px">
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
<span>
<strong style="font-size:.83rem;color:#dc2626">⚠️ Dauerhaft löschen</strong>
<span style="display:block;font-size:.77rem;color:#9ca3af;margin-top:2px">
Account + alle nutzerbezogenen Daten werden unwiderruflich gelöscht. Threads/Posts bleiben anonym erhalten.
</span>
</span>
</label>
</div>
<button type="submit" name="wbf_delete_member" class="button"
style="font-size:.8rem;height:28px;line-height:28px;background:#fef2f2;border-color:#dc2626;color:#dc2626"
onclick="return confirm('⚠️ ACHTUNG: Nutzer <?php echo esc_js($m->display_name); ?> DAUERHAFT löschen?\n\nDieser Vorgang kann NICHT rückgängig gemacht werden!\n\nAlle persönlichen Daten, Nachrichten und Einstellungen werden unwiderruflich entfernt.')">
🗑️ Dauerhaft löschen
</button>
</form>
<button type="button" class="button" style="margin-top:8px;font-size:.78rem"
onclick="document.getElementById('wbf-delete-user-<?php echo (int)$m->id; ?>').style.display='none'">
Abbrechen
</button>
</div>
<?php endif; ?>
</td>
<?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&#10;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 &amp; Optionen</li>
<li>🖼️ Avatar-Upload-Verzeichnis (<code>wp-content/uploads/wbf-avatars/</code>)</li>
<li>📄 Die Forum-Seite (erstellt vom Setup-Wizard)</li>
<li>⏰ Alle geplanten Cron-Jobs</li>
</ul>
<p style="margin:.75rem 0 0;color:#b91c1c;font-weight:600">Das Plugin selbst bleibt installiert — du kannst es danach neu einrichten oder manuell löschen.</p>
</div>
<?php if ( $error ): ?>
<div class="notice notice-error" style="margin-bottom:1rem"><p><?php echo $error; ?></p></div>
<?php endif; ?>
<!-- Bestätigungsformular -->
<form method="post" onsubmit="return wbfConfirmUninstall(event)">
<?php wp_nonce_field( 'wbf_uninstall_nonce' ); ?>
<table class="form-table" style="background:#fff;border:1px solid #ddd;border-radius:8px;padding:.5rem">
<tr>
<th style="width:220px;padding:1rem 1rem 1rem 1.25rem">
<label for="wbf_confirm_text">Bestätigung</label>
</th>
<td style="padding:1rem">
<p style="margin:0 0 .5rem;color:#555">Gib <strong style="color:#ef4444;font-family:monospace;font-size:1rem">ALLES LÖSCHEN</strong> ein um fortzufahren:</p>
<input type="text" id="wbf_confirm_text" name="wbf_confirm_text"
placeholder="ALLES LÖSCHEN"
autocomplete="off"
style="width:280px;border:2px solid #ef4444;border-radius:5px;padding:.4rem .6rem;font-size:1rem;font-weight:700;letter-spacing:.05em">
</td>
</tr>
</table>
<p style="margin-top:1.25rem">
<button type="submit" name="wbf_do_uninstall" value="1"
style="background:#ef4444;color:#fff;border:none;border-radius:6px;padding:.65rem 1.4rem;font-size:.95rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:.4rem">
<span class="dashicons dashicons-trash" style="margin-top:2px"></span>
Alle Forumsdaten unwiderruflich löschen
</button>
&nbsp;
<a href="<?php echo admin_url('admin.php?page=wbf-admin'); ?>" class="button">Abbrechen</a>
</p>
</form>
</div>
<script>
function wbfConfirmUninstall(e) {
var val = document.getElementById('wbf_confirm_text').value;
if (val !== 'ALLES LÖSCHEN') {
alert('Bitte gib genau "ALLES LÖSCHEN" ein.');
e.preventDefault();
return false;
}
return confirm('⚠️ Letzte Warnung:\n\nAlle Forumsdaten werden dauerhaft gelöscht.\nDieser Vorgang kann NICHT rückgängig gemacht werden!\n\nWirklich fortfahren?');
}
</script>
</div>
<?php
}
// ── Thread-Präfixe Verwaltung ─────────────────────────────────────────────────
function wbf_admin_prefixes() {
if ( ! current_user_can('manage_options') ) return;
// Löschen
if ( isset($_GET['delete_prefix']) && check_admin_referer('delete_prefix_'.(int)$_GET['delete_prefix']) ) {
WBF_DB::delete_prefix( (int)$_GET['delete_prefix'] );
echo '<div class="notice notice-success is-dismissible"><p>Präfix gelöscht.</p></div>';
}
// Speichern (neu oder bearbeiten)
if ( isset($_POST['wbf_save_prefix']) && check_admin_referer('wbf_prefix_nonce') ) {
$data = [
'label' => sanitize_text_field($_POST['label'] ?? ''),
'color' => sanitize_hex_color($_POST['color'] ?? '') ?: '#ffffff',
'bg_color' => sanitize_hex_color($_POST['bg_color'] ?? '') ?: '#475569',
'sort_order' => (int)($_POST['sort_order'] ?? 0),
];
if ( !empty($data['label']) ) {
if ( !empty($_POST['prefix_id']) ) {
WBF_DB::update_prefix( (int)$_POST['prefix_id'], $data );
echo '<div class="notice notice-success is-dismissible"><p>Präfix aktualisiert!</p></div>';
} else {
WBF_DB::create_prefix( $data );
echo '<div class="notice notice-success is-dismissible"><p>Präfix erstellt!</p></div>';
}
}
}
$prefixes = WBF_DB::get_prefixes();
$edit_id = isset($_GET['edit_prefix']) ? (int)$_GET['edit_prefix'] : 0;
$edit = $edit_id ? WBF_DB::get_prefix($edit_id) : null;
?>
<div class="wrap">
<h1>🏷️ Thread-Präfixe</h1>
<p style="color:#666">Präfixe erscheinen farbig vor dem Thread-Titel. Nutzer können beim Erstellen eines Threads einen Präfix wählen.</p>
<div style="display:grid;grid-template-columns:1fr 360px;gap:20px;margin-top:1.5rem;align-items:start">
<!-- Tabelle -->
<div>
<?php if (empty($prefixes)): ?>
<div style="padding:2rem;text-align:center;color:#94a3b8;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px">
Noch keine Präfixe erstellt.
</div>
<?php else: ?>
<table class="widefat striped">
<thead><tr><th>Vorschau</th><th>Reihenfolge</th><th>Aktionen</th></tr></thead>
<tbody>
<?php foreach ($prefixes as $px):
$del_url = wp_nonce_url("?page=wbf-prefixes&delete_prefix={$px->id}", "delete_prefix_{$px->id}"); ?>
<tr>
<td>
<span style="display:inline-flex;align-items:center;padding:2px 10px;border-radius:4px;font-size:.82rem;font-weight:700;color:<?php echo esc_attr($px->color); ?>;background:<?php echo esc_attr($px->bg_color); ?>">
<?php echo esc_html($px->label); ?>
</span>
</td>
<td><?php echo (int)$px->sort_order; ?></td>
<td>
<a href="?page=wbf-prefixes&edit_prefix=<?php echo (int)$px->id; ?>">Bearbeiten</a> |
<a href="<?php echo esc_url($del_url); ?>" style="color:#dc2626" onclick="return confirm('Präfix löschen?')">Löschen</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Formular -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:18px">
<?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&#10;beleidigung&#10;spam phrase"><?php echo esc_textarea($current_list); ?></textarea>
<p style="font-size:.78rem;color:#9ca3af;margin:.4rem 0 1rem">
Auch Phrasen mit Leerzeichen sind möglich (z.B. "böse phrase"). Regex wird nicht unterstützt.
</p>
<button type="submit" name="wbf_save_wordfilter" class="button button-primary">
💾 Wortfilter speichern
</button>
</div>
</form>
</div>
<?php
}
// ── 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
}
}