Files
WP-Ingame-Shop-Pro/wp-ingame-shop/wp-ingame-shop-pro.php
2026-05-12 23:24:09 +02:00

7950 lines
432 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
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
/*
Plugin Name: WP Ingame Shop Pro
Plugin URI:https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro
Description: Vollautomatischer Shop mit Warenkorb (kein echtgeld Handel)
Version: 2.1.3
Author: M_Viper
Author URI: https://m-viper.de
Requires at least: 6.9.1
Tested up to: 6.9.1
PHP Version: 7.4
License: GPL2
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: wp-ingame-shop-pro
Tags: shop, items, minecraft, coupons, deals, categories
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
Support: [Telegram Support](https://t.me/M_Viper04)
*/
if (!defined('ABSPATH')) exit;
// Plugin Constants
define('WIS_VERSION', '2.1.3');
define('WIS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WIS_PLUGIN_URL', plugin_dir_url(__FILE__));
// ===========================================================
// DASHBOARD WIDGET & UPDATES
// ===========================================================
class WIS_Dashboard {
private static $update_url = 'https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/releases';
private static $api_url = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/WP-Ingame-Shop-Pro/releases/latest';
private static $transient_name = 'wis_update_check';
public static function init() {
add_action('wp_dashboard_setup', [__CLASS__, 'add_dashboard_widget']);
add_action('admin_init', [__CLASS__, 'check_for_update']);
// AJAX Handler für Cache leeren
add_action('wp_ajax_wis_clear_cache', [__CLASS__, 'ajax_clear_cache']);
}
// Button im Dashboard: Cache leeren
public static function ajax_clear_cache() {
check_ajax_referer('wis_clear_cache_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Keine Berechtigung.');
}
// Leere den Object Cache
if (function_exists('wp_cache_flush')) {
wp_cache_flush();
}
// Optional: Leere auch unseren Transienten für den Update-Check, damit er neu lädt
delete_transient(self::$transient_name);
wp_send_json_success(['message' => 'Cache erfolgreich geleert!']);
}
// Prüfe auf Updates (läuft im Admin-Hintergrund)
public static function check_for_update() {
$update_data = get_transient(self::$transient_name);
if (false === $update_data) {
$response = wp_remote_get(self::$api_url, ['timeout' => 10]);
if (is_wp_error($response)) {
return; // Fehler beim Abrufen, nichts tun
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body);
if (isset($data->tag_name) && isset($data->html_url)) {
// Entferne 'v' vom Tag (z.B. v2.1.3 -> 2.1.3)
$remote_version = ltrim($data->tag_name, 'v');
$update_data = [
'new_version' => $remote_version,
'url' => self::$update_url,
'package_url' => isset($data->assets[0]->browser_download_url) ? $data->assets[0]->browser_download_url : '',
'changelog' => isset($data->body) ? substr($data->body, 0, 200) . '...' : ''
];
// Speichere für 12 Stunden
set_transient(self::$transient_name, $update_data, 12 * HOUR_IN_SECONDS);
}
}
}
// Zeige Admin Notice wenn Update verfügbar
public static function show_update_notice() {
$update_data = get_transient(self::$transient_name);
if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')) {
?>
<div class="notice notice-info is-dismissible">
<p>
<strong>🚀 WP Ingame Shop Pro Update verfügbar!</strong><br>
Neue Version: <?php echo esc_html($update_data['new_version']); ?> (Du hast <?php echo WIS_VERSION; ?>)<br>
<a href="<?php echo esc_url($update_data['url']); ?>" target="_blank">Hier im Git Repository ansehen</a> oder unten im Widget Details prüfen.
</p>
</div>
<?php
}
}
// Widget registrieren
public static function add_dashboard_widget() {
wp_add_dashboard_widget(
'wis_dashboard_widget',
'🛒 WP Ingame Shop Pro Dashboard',
[__CLASS__, 'render_widget']
);
}
// Widget Inhalt rendern
public static function render_widget() {
global $wpdb;
$update_data = get_transient(self::$transient_name);
// Statistiken holen
$stats = [
'items' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_items WHERE status = 'publish'"),
'orders' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_orders WHERE status = 'completed'"),
'pending' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_orders WHERE status IN ('pending', 'claimed')"),
'revenue' => $wpdb->get_var("SELECT SUM(price) FROM {$wpdb->prefix}wis_orders WHERE status = 'completed'"),
];
$currency = get_option('wis_currency_name', 'Coins');
// Update Status bestimmen
$update_html = '<span style="color:green;">✅ Aktuell (v' . WIS_VERSION . ')</span>';
if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')) {
$update_html = '<span style="color:#d63638; font-weight:bold;">⚠️ Update verfügbar!</span>';
}
?>
<style>
/* Spinner bei number-Inputs entfernen */
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none !important; margin: 0 !important; }
input[type=number] { -moz-appearance: textfield !important; appearance: textfield !important; }
.wis-dash-stats { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
.wis-stat-box { flex: 1; min-width: 100px; background: #f9f9f9; padding: 10px; border-radius: 5px; text-align: center; border: 1px solid #eee; }
.wis-stat-num { display: block; font-size: 1.5em; font-weight: bold; color: #333; }
.wis-stat-label { font-size: 0.85em; color: #666; }
.wis-dash-actions { margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
</style>
<div class="wis-dash-stats">
<div class="wis-stat-box">
<span class="wis-stat-num"><?php echo intval($stats['items']); ?></span>
<span class="wis-stat-label">Items</span>
</div>
<div class="wis-stat-box">
<span class="wis-stat-num"><?php echo intval($stats['orders']); ?></span>
<span class="wis-stat-label">Bestellungen</span>
</div>
<div class="wis-stat-box">
<span class="wis-stat-num"><?php echo intval($stats['pending']); ?></span>
<span class="wis-stat-label">Offen</span>
</div>
<div class="wis-stat-box">
<span class="wis-stat-num"><?php echo number_format($stats['revenue']); ?></span>
<span class="wis-stat-label">Umsatz (<?php echo esc_html($currency); ?>)</span>
</div>
</div>
<p style="margin-bottom: 10px;">
<strong>Status:</strong> <?php echo $update_html; ?>
</p>
<?php if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')): ?>
<div style="background:#fff3cd; color:#856404; padding:10px; border-radius:4px; margin-bottom:10px; font-size:13px;">
<strong>Neue Version <?php echo esc_html($update_data['new_version']); ?></strong><br>
<a href="<?php echo esc_url($update_data['url']); ?>" target="_blank">Updates ansehen</a>
</div>
<?php endif; ?>
<div class="wis-dash-actions">
<a href="<?php echo admin_url('admin.php?page=wis_orders'); ?>" class="button">Bestellungen ansehen</a>
<button type="button" id="wis-clear-cache-btn" class="button button-secondary button-small">
🔄 Update-Cache jetzt leeren
</button>
</div>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#wis-clear-cache-btn').on('click', function() {
var $btn = $(this);
$btn.prop('disabled', true).text('Leere...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wis_clear_cache',
nonce: '<?php echo wp_create_nonce('wis_clear_cache_nonce'); ?>'
},
success: function(response) {
if (response.success) {
$btn.removeClass('button-secondary').addClass('button-primary').text('✅ ' + response.data.message);
setTimeout(function() {
$btn.prop('disabled', false).removeClass('button-primary').addClass('button-secondary').text('🔄 Update-Cache jetzt leeren');
}, 3000);
} else {
alert('Fehler: ' + (response.data || 'Unbekannt'));
$btn.prop('disabled', false).text('🔄 Update-Cache jetzt leeren');
}
},
error: function() {
alert('Verbindungsfehler.');
$btn.prop('disabled', false).text('🔄 Update-Cache jetzt leeren');
}
});
});
});
</script>
<?php
}
}
WIS_Dashboard::init();
// Zeige Notice auch im Admin-Bereich (außer auf dem Dashboard selbst, da dort das Widget ist)
add_action('admin_notices', [WIS_Dashboard::class, 'show_update_notice']);
// AJAX: Kategorien-Reihenfolge speichern
add_action('wp_ajax_wis_save_cat_order', function() {
check_ajax_referer('wis_cat_order_nonce', 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('Keine Berechtigung.');
global $wpdb;
$table = $wpdb->prefix . 'wis_categories';
$order = $_POST['order'] ?? [];
if (!is_array($order)) wp_send_json_error('Ungültige Daten.');
foreach ($order as $i => $entry) {
$id = intval($entry['id']);
$parent_id = intval($entry['parent_id']);
if ($id > 0) {
$wpdb->update($table, [
'sort_order' => ($i + 1) * 10,
'parent_id' => $parent_id,
], ['id' => $id]);
}
}
wp_send_json_success('Reihenfolge gespeichert.');
});
// AJAX: Angebot-Flag umschalten (Angebote-Übersicht)
add_action('wp_ajax_wis_angebote_toggle', function() {
check_ajax_referer('wis_angebote_nonce', 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('Keine Berechtigung.');
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$id = intval($_POST['id'] ?? 0);
$field = in_array($_POST['field'] ?? '', ['is_offer', 'is_daily_deal'], true) ? $_POST['field'] : null;
if (!$id || !$field) wp_send_json_error('Ungültige Parameter.');
$current = (int) $wpdb->get_var($wpdb->prepare("SELECT $field FROM $table WHERE id = %d", $id));
$new_val = $current ? 0 : 1;
if ($field === 'is_daily_deal' && $new_val === 1) {
$wpdb->query("UPDATE $table SET is_daily_deal = 0 WHERE is_daily_deal = 1");
}
$wpdb->update($table, [$field => $new_val], ['id' => $id]);
wp_send_json_success(['new_val' => $new_val]);
});
// AJAX: Angebotspreis direkt speichern (Angebote-Übersicht)
add_action('wp_ajax_wis_angebote_save_price', function() {
check_ajax_referer('wis_angebote_nonce', 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('Keine Berechtigung.');
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$id = intval($_POST['id'] ?? 0);
$offer_price = max(0, intval($_POST['offer_price'] ?? 0));
if (!$id) wp_send_json_error('Ungültige ID.');
$wpdb->update($table, ['offer_price' => $offer_price], ['id' => $id]);
wp_send_json_success(['offer_price' => $offer_price]);
});
// jQuery UI Sortable für die Kategorien-Seite laden
add_action('admin_enqueue_scripts', function($hook) {
if (isset($_GET['page']) && $_GET['page'] === 'wis_categories') {
wp_enqueue_script('jquery-ui-sortable');
}
});
// ===========================================================
// ACTIVATION & DATABASE
// ===========================================================
class WIS_Activator {
public static function activate() {
// Spalte custom_image_url nachrüsten (für bestehende Installationen)
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$col = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'custom_image_url'");
if (!$col) {
$wpdb->query("ALTER TABLE $table ADD COLUMN custom_image_url varchar(500) DEFAULT NULL AFTER categories");
}
// sort_order für Kategorien nachrüsten (ab v2.3.0)
$cat_table_so = $wpdb->prefix . 'wis_categories';
$col_so = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$cat_table_so' AND COLUMN_NAME = 'sort_order'");
if (!$col_so) {
$wpdb->query("ALTER TABLE $cat_table_so ADD COLUMN sort_order int(11) NOT NULL DEFAULT 0 AFTER parent_id");
// Bestehende Kategorien mit aufsteigender Reihenfolge initialisieren
$existing_cats = $wpdb->get_results("SELECT id FROM $cat_table_so ORDER BY parent_id ASC, name ASC");
foreach ($existing_cats as $i => $ec) {
$wpdb->update($cat_table_so, ['sort_order' => $i * 10], ['id' => $ec->id]);
}
}
// custom_command-Spalte nachrüsten (ab v2.3.0 Custom Command Items)
$col_cmd = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'custom_command'");
if (!$col_cmd) {
$wpdb->query("ALTER TABLE $table ADD COLUMN custom_command varchar(500) DEFAULT NULL AFTER custom_image_url");
}
// Ankauf-Spalten nachrüsten (ab v6.5)
$col_sell = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'sell_enabled'"); if (!$col_sell) {
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_enabled tinyint(1) NOT NULL DEFAULT 0 AFTER status");
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_price_mode varchar(20) NOT NULL DEFAULT 'percent' AFTER sell_enabled");
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_price_value int(11) NOT NULL DEFAULT 80 AFTER sell_price_mode");
}
// Tageslimit beim Ankauf nachrüsten
$col_dlimit = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'daily_sell_limit'");
if (!$col_dlimit) {
$wpdb->query("ALTER TABLE $table ADD COLUMN daily_sell_limit int(11) NOT NULL DEFAULT 0 AFTER sell_price_value");
}
// Preishistorie-Tabelle anlegen
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_price_history (
id mediumint(9) NOT NULL AUTO_INCREMENT,
item_id varchar(100) NOT NULL,
item_name varchar(255) NOT NULL,
field varchar(30) NOT NULL DEFAULT 'price',
old_value int(11) NOT NULL DEFAULT 0,
new_value int(11) NOT NULL DEFAULT 0,
changed_by varchar(100) NOT NULL DEFAULT '',
changed_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY item_id (item_id),
KEY changed_at (changed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
// Gift-Spalte nachrüsten (ab v6.5-gift)
$orders_table = $wpdb->prefix . 'wis_orders';
$col_gift = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$orders_table' AND COLUMN_NAME = 'gift_recipient'");
if (!$col_gift) {
$wpdb->query("ALTER TABLE $orders_table ADD COLUMN gift_recipient varchar(100) DEFAULT NULL AFTER player_name");
}
// Auto-Expire Cron registrieren (7-Tage-Ablauf für unbestätigte Orders)
if (!wp_next_scheduled('wis_auto_expire_orders')) {
wp_schedule_event(time(), 'hourly', 'wis_auto_expire_orders');
}
// Item-Abo Tabelle nachrüsten
self::create_item_abo_subs_table();
// Item-Abo Liefer-Cron registrieren (täglich)
if (!wp_next_scheduled('wis_item_abo_delivery_event')) {
$midnight = strtotime('tomorrow midnight');
wp_schedule_event($midnight, 'daily', 'wis_item_abo_delivery_event');
}
// Sell-Log-Tabelle anlegen
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_sell_log (
id mediumint(9) NOT NULL AUTO_INCREMENT,
player_name varchar(100) NOT NULL,
server varchar(100) NOT NULL,
item_id varchar(100) NOT NULL,
item_name varchar(255) NOT NULL,
quantity int(11) NOT NULL DEFAULT 1,
price_per_item decimal(10,2) NOT NULL,
total_paid decimal(10,2) NOT NULL,
sold_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY player_name (player_name),
KEY sold_at (sold_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
// Order-Items-Tabelle anlegen (ab Analyse-Update) einzelne Items pro Bestellung
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_order_items (
id mediumint(9) NOT NULL AUTO_INCREMENT,
order_id mediumint(9) NOT NULL,
item_id varchar(100) NOT NULL,
item_name varchar(255) NOT NULL,
item_type varchar(20) NOT NULL DEFAULT 'item',
quantity int(11) NOT NULL DEFAULT 1,
price_per_item decimal(10,2) NOT NULL DEFAULT 0,
total decimal(10,2) NOT NULL DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY order_id (order_id),
KEY item_id (item_id),
KEY created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
self::create_tables();
self::set_default_options();
self::create_default_categories();
if (!wp_next_scheduled('wis_daily_deal_event')) {
wp_schedule_event(time(), 'daily', 'wis_daily_deal_event');
}
// Fly-Abo Renewal: täglich prüfen (läuft nur durch am 1. des Monats)
if (!wp_next_scheduled('wis_abo_renewal_event')) {
// Nächsten Mitternacht-Zeitstempel berechnen
$midnight = strtotime('tomorrow midnight');
wp_schedule_event($midnight, 'daily', 'wis_abo_renewal_event');
}
flush_rewrite_rules();
}
public static function deactivate() {
wp_clear_scheduled_hook('wis_daily_deal_event');
wp_clear_scheduled_hook('wis_abo_renewal_event');
wp_clear_scheduled_hook('wis_auto_expire_orders');
wp_clear_scheduled_hook('wis_item_abo_delivery_event');
flush_rewrite_rules();
}
private static function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$tables = [];
// Items
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_items (
id mediumint(9) NOT NULL AUTO_INCREMENT,
item_id varchar(100) NOT NULL,
name varchar(255) NOT NULL,
description text,
price int(11) DEFAULT 0,
offer_price int(11) DEFAULT 0,
is_offer tinyint(1) DEFAULT 0,
is_daily_deal tinyint(1) DEFAULT 0,
servers text,
categories text,
custom_image_url varchar(500) DEFAULT NULL,
custom_command varchar(500) DEFAULT NULL,
status varchar(20) DEFAULT 'draft',
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY item_id (item_id),
KEY status (status)
) $charset_collate;";
// Orders
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_orders (
id mediumint(9) NOT NULL AUTO_INCREMENT,
player_name varchar(100) NOT NULL,
gift_recipient varchar(100) DEFAULT NULL,
server varchar(100) NOT NULL,
item_id varchar(100) NOT NULL,
item_title varchar(255) NOT NULL,
price int(11) NOT NULL,
quantity int(11) DEFAULT 1,
status varchar(20) DEFAULT 'pending',
response text,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY player_name (player_name),
KEY gift_recipient (gift_recipient),
KEY status (status)
) $charset_collate;";
// Coupons
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_coupons (
id mediumint(9) NOT NULL AUTO_INCREMENT,
code varchar(50) NOT NULL,
value int(11) NOT NULL,
type varchar(10) DEFAULT 'fixed',
usage_limit int(11) DEFAULT 1,
used_count int(11) DEFAULT 0,
expiry date DEFAULT NULL,
min_order_value int(11) DEFAULT 0,
allowed_categories text DEFAULT NULL,
bulk_id varchar(20) DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY code (code)
) $charset_collate;";
// Pro-Spieler Coupon-Nutzung (verhindert Mehrfacheinlösung)
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_coupon_uses (
id mediumint(9) NOT NULL AUTO_INCREMENT,
coupon_id mediumint(9) NOT NULL,
player_name varchar(100) NOT NULL,
used_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY coupon_player (coupon_id, player_name),
KEY player_name (player_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
// Servers
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_servers (
id mediumint(9) NOT NULL AUTO_INCREMENT,
slug varchar(100) NOT NULL,
name varchar(255) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY slug (slug)
) $charset_collate;";
// Categories
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_categories (
id mediumint(9) NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
slug varchar(100) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY slug (slug)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
foreach ($tables as $sql) {
dbDelta($sql);
}
}
private static function set_default_options() {
$defaults = [
'wis_currency_name' => 'Coins',
'wis_image_base_url' => 'https://git.viper.ipv64.net/M_Viper/minecraft-items/raw/branch/main/images/',
'wis_header_text' => '✅ Auto-Bilder | 💰 Sicherer Checkout | 🎮 Ingame-Bestätigung',
'wis_coupon_exclude_offers' => '0',
'wis_daily_deal_enabled' => '0',
'wis_daily_deal_discount' => '20',
'wis_api_key' => bin2hex(random_bytes(24)),
'wis_tax_enabled' => '0',
'wis_tax_rate' => '0',
];
foreach ($defaults as $key => $value) {
if (get_option($key) === false) {
add_option($key, $value);
}
}
}
public static function check_api_key($request) {
$key = $request->get_header('X-WIS-Key');
if (empty($key)) {
$key = $request->get_param('api_key');
}
$stored = get_option('wis_api_key', '');
return ($stored !== '' && hash_equals($stored, (string) $key));
}
public static function spigot_permission($request) {
if (!self::check_api_key($request)) {
return new WP_Error('wis_unauthorized', 'Ungültiger oder fehlender API-Key.', ['status' => 401]);
}
return true;
}
private static function create_default_categories() {
$default_categories = [
['name' => 'Baublöcke', 'slug' => 'baublocke'],
['name' => 'Dekorationsblöcke', 'slug' => 'dekorationsblocke'],
['name' => 'Redstone', 'slug' => 'redstone'],
['name' => 'Transport', 'slug' => 'transport'],
['name' => 'Natur', 'slug' => 'natur'],
['name' => 'Werkzeuge & Hilfsmittel', 'slug' => 'werkzeuge-hilfsmittel'],
['name' => 'Kampf', 'slug' => 'kampf'],
['name' => 'Nahrung & Tränke', 'slug' => 'nahrung-tranke'],
['name' => 'Zutaten', 'slug' => 'zutaten'],
['name' => 'Spawn-Eier', 'slug' => 'spawn-eier']
];
global $wpdb;
foreach ($default_categories as $cat) {
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}wis_categories WHERE slug = %s",
$cat['slug']
));
if (!$exists) {
$wpdb->insert($wpdb->prefix . 'wis_categories', [
'name' => $cat['name'],
'slug' => $cat['slug']
]);
}
}
}
public static function run_daily_deal() {
if (get_option('wis_daily_deal_enabled') !== '1') return;
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$discount = intval(get_option('wis_daily_deal_discount', 20));
// Altes Daily-Deal-Item vollständig zurücksetzen:
// is_daily_deal, is_offer und offer_price werden alle auf 0 gesetzt,
// damit das Item nicht als normales Angebot mit gesenktem Preis hängen bleibt.
$wpdb->query(
"UPDATE $table
SET is_daily_deal = 0,
is_offer = 0,
offer_price = 0
WHERE is_daily_deal = 1"
);
$item = $wpdb->get_row("SELECT * FROM $table WHERE status = 'publish' AND price > 0 ORDER BY RAND() LIMIT 1");
if ($item) {
$offer_price = max(0, floor($item->price - ($item->price * ($discount / 100))));
$wpdb->update($table, [
'is_daily_deal' => 1,
'is_offer' => 1,
'offer_price' => $offer_price
], ['id' => $item->id]);
}
}
/**
* Fly-Abo Renewal läuft täglich, handelt aber nur am 1. des Monats.
* Verlängert alle aktiven (nicht gekündigten) Abos automatisch um 30 Tage
* und legt je einen Order-Eintrag als Buchungsnachweis an.
* Gekündigte Abos laufen bis zum letzten Tag des laufenden Monats und werden dann entfernt.
*/
public static function run_abo_renewal() {
// Nur am 1. des Monats ausführen
if (date('j') !== '1') return;
global $wpdb;
$subs_table = $wpdb->prefix . 'wis_fly_abo_subs';
$orders_table = $wpdb->prefix . 'wis_orders';
// Tabelle anlegen falls noch nicht vorhanden (Migration)
self::create_abo_subs_table();
// Alle aktiven, nicht gekündigten Abos verlängern
$active = $wpdb->get_results(
"SELECT * FROM {$subs_table} WHERE cancelled = 0 AND status = 'active'"
);
foreach ($active as $sub) {
// expires_at um 30 Tage verlängern
$wpdb->query($wpdb->prepare(
"UPDATE {$subs_table}
SET expires_at = DATE_ADD(expires_at, INTERVAL 30 DAY),
renewed_at = NOW(),
renewal_count = renewal_count + 1
WHERE id = %d",
$sub->id
));
// Buchungsnachweis als Order anlegen (status: completed)
$wpdb->insert($orders_table, [
'player_name' => $sub->player_name,
'server' => $sub->server,
'item_id' => 'fly_abo_renewal',
'item_title' => '✈ Fly-Abo Verlängerung: ' . $sub->label,
'price' => $sub->price,
'quantity' => 1,
'status' => 'completed',
'response' => json_encode([
'commands' => [[
'type' => 'fly_abo',
'label' => $sub->label,
'price' => $sub->price,
]],
]),
]);
}
// Gekündigte Abos die abgelaufen sind deaktivieren
$wpdb->query(
"UPDATE {$subs_table}
SET status = 'expired'
WHERE cancelled = 1
AND expires_at < NOW()
AND status = 'active'"
);
// Log
$count = count($active);
error_log("[WIS] Fly-Abo Renewal: {$count} Abo(s) verlängert am " . date('d.m.Y'));
}
public static function create_abo_subs_table() {
global $wpdb;
$table = $wpdb->prefix . 'wis_fly_abo_subs';
$wpdb->query("CREATE TABLE IF NOT EXISTS {$table} (
id INT AUTO_INCREMENT PRIMARY KEY,
player_name VARCHAR(64) NOT NULL,
server VARCHAR(64) NOT NULL DEFAULT '',
label VARCHAR(128) NOT NULL,
price INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
cancelled TINYINT(1) NOT NULL DEFAULT 0,
cancelled_at DATETIME DEFAULT NULL,
expires_at DATETIME NOT NULL,
renewed_at DATETIME DEFAULT NULL,
renewal_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_player_server (player_name, server)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
}
/**
* Tabelle für Item-Abo Abonnements anlegen
*/
public static function create_item_abo_subs_table() {
global $wpdb;
$table = $wpdb->prefix . 'wis_item_abo_subs';
$wpdb->query("CREATE TABLE IF NOT EXISTS {$table} (
id INT AUTO_INCREMENT PRIMARY KEY,
player_name VARCHAR(64) NOT NULL,
server VARCHAR(64) NOT NULL DEFAULT '',
item_id VARCHAR(100) NOT NULL,
daily_qty INT NOT NULL DEFAULT 1,
label VARCHAR(128) NOT NULL,
price INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
cancelled TINYINT(1) NOT NULL DEFAULT 0,
cancelled_at DATETIME DEFAULT NULL,
expires_at DATETIME NOT NULL,
last_delivered DATE DEFAULT NULL,
renewal_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY player_server (player_name, server),
KEY status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
}
/**
* Item-Abo Tageslieferung läuft täglich per Cron.
* Legt für jeden aktiven Abonnenten einen pending Order an,
* damit das Spigot-Plugin die Items ingame ausliefern kann.
*/
public static function run_item_abo_delivery() {
global $wpdb;
$subs_table = $wpdb->prefix . 'wis_item_abo_subs';
$orders_table = $wpdb->prefix . 'wis_orders';
// Tabelle ggf. anlegen (Migration)
self::create_item_abo_subs_table();
$today = date('Y-m-d');
// Alle aktiven, nicht abgelaufenen Abos die heute noch nicht beliefert wurden
$active = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$subs_table}
WHERE status = 'active'
AND (last_delivered IS NULL OR last_delivered < %s)
AND expires_at > NOW()",
$today
));
$count = 0;
foreach ($active as $sub) {
// Payload für Spigot-Plugin
$payload = json_encode([
'items' => [[
'id' => $sub->item_id,
'amount' => intval($sub->daily_qty),
]],
'commands' => [],
'abo_delivery' => true,
]);
$wpdb->insert($orders_table, [
'player_name' => $sub->player_name,
'server' => $sub->server,
'item_id' => 'item_abo_delivery',
'item_title' => '📦 Abo-Lieferung: ' . $sub->label . ' ×' . $sub->daily_qty,
'price' => 0,
'quantity' => intval($sub->daily_qty),
'status' => 'pending',
'response' => $payload,
]);
// last_delivered aktualisieren
$wpdb->update($subs_table, ['last_delivered' => $today], ['id' => $sub->id]);
$count++;
}
// Abgelaufene + gekündigte Abos deaktivieren
$wpdb->query(
"UPDATE {$subs_table}
SET status = 'expired'
WHERE status = 'active'
AND (expires_at < NOW() OR (cancelled = 1 AND expires_at < NOW()))"
);
if ($count > 0) {
error_log("[WIS] Item-Abo Lieferung: {$count} Abo(s) beliefert am {$today}");
}
}
public static function reset_shop() {
global $wpdb;
$tables = [
'wis_items', 'wis_orders', 'wis_coupons',
'wis_servers', 'wis_categories'
];
foreach ($tables as $table) {
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}$table");
}
self::set_default_options();
self::create_default_categories();
return true;
}
public static function reset_sell_log(): bool {
global $wpdb;
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_sell_log");
return true;
}
public static function reset_top_spenders(): bool {
global $wpdb;
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_orders");
return true;
}
public static function reset_analyse(): bool {
global $wpdb;
// Order-Items-Tabelle (Analyse-Grundlage) leeren
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_order_items");
return true;
}
public static function reset_price_history(): bool {
global $wpdb;
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_price_history");
return true;
}
}
// ===========================================================
// ITEM CATEGORIZER
// ===========================================================
class WIS_Item_Categorizer {
public static function auto_categorize($item_id) {
$item_id = strtolower($item_id);
$item_id = str_replace('minecraft:', '', $item_id);
if (strpos($item_id, 'spawn_egg') !== false) {
return ['spawn-eier'];
}
if (preg_match('/(wooden|stone|iron|golden|diamond|netherite|copper)_(pickaxe|axe|shovel|hoe)/', $item_id)) {
return ['werkzeuge-hilfsmittel'];
}
if (in_array($item_id, [
'shears', 'fishing_rod', 'flint_and_steel', 'bucket', 'water_bucket', 'lava_bucket',
'milk_bucket', 'powder_snow_bucket', 'axolotl_bucket', 'cod_bucket', 'pufferfish_bucket',
'salmon_bucket', 'tadpole_bucket', 'tropical_fish_bucket',
'compass', 'recovery_compass', 'clock', 'spyglass', 'map', 'filled_map',
'brush', 'lead', 'name_tag', 'saddle', 'carrot_on_a_stick', 'warped_fungus_on_a_stick'
])) {
return ['werkzeuge-hilfsmittel'];
}
if (preg_match('/(wooden|stone|iron|golden|diamond|netherite|copper)_(sword|spear)/', $item_id)) {
return ['kampf'];
}
if (preg_match('/(leather|chainmail|iron|golden|diamond|netherite|copper|turtle)_(helmet|chestplate|leggings|boots|cap|tunic|pants|shell)/', $item_id)) {
return ['kampf'];
}
if (in_array($item_id, [
'bow', 'crossbow', 'arrow', 'spectral_arrow', 'tipped_arrow',
'shield', 'trident', 'mace',
'totem_of_undying', 'elytra',
'horse_armor', 'iron_horse_armor', 'golden_horse_armor', 'diamond_horse_armor',
'wolf_armor'
]) || strpos($item_id, 'horse_armor') !== false) {
return ['kampf'];
}
if (preg_match('/(raw_|cooked_)?(beef|porkchop|mutton|chicken|rabbit|cod|salmon)/', $item_id)) {
return ['nahrung-tranke'];
}
if (in_array($item_id, [
'apple', 'golden_apple', 'enchanted_golden_apple',
'melon_slice', 'glow_berries', 'sweet_berries', 'chorus_fruit',
'carrot', 'golden_carrot', 'potato', 'baked_potato', 'poisonous_potato',
'beetroot',
'bread', 'cookie', 'cake', 'pumpkin_pie',
'dried_kelp',
'tropical_fish', 'pufferfish', 'rotten_flesh', 'spider_eye'
])) {
return ['nahrung-tranke'];
}
if (strpos($item_id, '_stew') !== false || strpos($item_id, '_soup') !== false) {
return ['nahrung-tranke'];
}
if (strpos($item_id, 'potion') !== false || in_array($item_id, [
'honey_bottle', 'milk_bucket', 'glass_bottle', 'dragon_breath',
'experience_bottle', 'ominous_bottle'
])) {
return ['nahrung-tranke'];
}
if (strpos($item_id, '_boat') !== false || strpos($item_id, '_raft') !== false) {
return ['transport'];
}
if (strpos($item_id, 'minecart') !== false) {
return ['transport'];
}
if (in_array($item_id, ['elytra', 'saddle', 'lead'])) {
return ['transport'];
}
if (strpos($item_id, 'redstone') !== false && $item_id !== 'redstone_ore') {
return ['redstone'];
}
if (in_array($item_id, [
'repeater', 'comparator', 'observer',
'piston', 'sticky_piston',
'dispenser', 'dropper', 'hopper',
'lever', 'tripwire_hook', 'daylight_detector',
'tnt', 'target', 'lightning_rod'
]) || strpos($item_id, 'button') !== false || strpos($item_id, 'pressure_plate') !== false) {
return ['redstone'];
}
if (strpos($item_id, '_rail') !== false || $item_id === 'rail') {
return ['redstone'];
}
$pure_materials = [
'stick', 'coal', 'charcoal', 'diamond', 'emerald', 'lapis_lazuli',
'iron_ingot', 'gold_ingot', 'copper_ingot', 'netherite_ingot',
'iron_nugget', 'gold_nugget', 'copper_nugget',
'raw_iron', 'raw_gold', 'raw_copper',
'netherite_scrap', 'netherite_upgrade',
'amethyst_shard', 'prismarine_shard', 'prismarine_crystals',
'quartz', 'nether_quartz', 'echo_shard', 'disc_fragment',
'string', 'feather', 'leather', 'rabbit_hide',
'slimeball', 'ender_pearl', 'ender_eye',
'blaze_rod', 'blaze_powder', 'magma_cream', 'ghast_tear',
'nether_star', 'nether_brick',
'nautilus_shell', 'heart_of_the_sea', 'scute', 'turtle_scute', 'armadillo_scute',
'bone', 'bone_meal', 'gunpowder', 'glowstone_dust', 'sugar',
'phantom_membrane', 'ink_sac', 'glow_ink_sac',
'paper', 'book', 'flint',
'fermented_spider_eye', 'glistering_melon_slice', 'rabbit_foot',
'nether_wart', 'breeze_rod',
'clay_ball', 'brick', 'firework_star',
'shulker_shell', 'popped_chorus_fruit'
];
if (in_array($item_id, $pure_materials)) {
return ['zutaten'];
}
if (strpos($item_id, '_pottery_sherd') !== false || strpos($item_id, '_armor_trim') !== false) {
return ['zutaten'];
}
if (in_array($item_id, [
'dirt', 'coarse_dirt', 'rooted_dirt', 'grass_block', 'podzol', 'mycelium',
'farmland', 'dirt_path',
'sand', 'red_sand', 'gravel', 'clay',
'suspicious_sand', 'suspicious_gravel'
])) {
return ['natur'];
}
if (strpos($item_id, '_leaves') !== false || strpos($item_id, '_sapling') !== false ||
strpos($item_id, 'azalea') !== false) {
return ['natur'];
}
$flowers = [
'dandelion', 'poppy', 'blue_orchid', 'allium', 'azure_bluet',
'red_tulip', 'orange_tulip', 'white_tulip', 'pink_tulip',
'oxeye_daisy', 'cornflower', 'lily_of_the_valley', 'wither_rose',
'sunflower', 'lilac', 'rose_bush', 'peony',
'pitcher_plant', 'pitcher_pod', 'torchflower', 'torchflower_seeds',
'pink_petals', 'spore_blossom'
];
if (in_array($item_id, $flowers)) {
return ['natur'];
}
if (strpos($item_id, 'mushroom') !== false || strpos($item_id, 'fungus') !== false ||
in_array($item_id, ['short_grass', 'tall_grass', 'fern', 'large_fern', 'dead_bush'])) {
return ['natur'];
}
if (in_array($item_id, [
'seagrass', 'tall_seagrass', 'kelp', 'dried_kelp', 'sea_pickle',
'vine', 'weeping_vines', 'twisting_vines', 'cave_vines', 'glow_lichen',
'hanging_roots', 'mangrove_roots', 'muddy_mangrove_roots'
])) {
return ['natur'];
}
if (strpos($item_id, '_seeds') !== false || in_array($item_id, [
'wheat', 'beetroot', 'carrot', 'potato',
'melon', 'pumpkin', 'carved_pumpkin',
'sugar_cane', 'bamboo', 'cocoa_beans',
'sweet_berries', 'glow_berries', 'sweet_berry_bush',
'nether_wart', 'cactus',
'mangrove_propagule'
])) {
return ['natur'];
}
if (in_array($item_id, [
'crimson_roots', 'warped_roots', 'nether_sprouts',
'crimson_nylium', 'warped_nylium'
])) {
return ['natur'];
}
if (in_array($item_id, [
'moss_block', 'moss_carpet',
'big_dripleaf', 'small_dripleaf',
'lily_pad', 'bee_nest', 'honeycomb', 'honeycomb_block',
'snow', 'snowball', 'powder_snow'
])) {
return ['natur'];
}
if (strpos($item_id, 'glass') !== false && strpos($item_id, '_pane') === false) {
return ['dekorationsblocke'];
}
if (strpos($item_id, 'glass_pane') !== false || strpos($item_id, 'stained_glass_pane') !== false) {
return ['dekorationsblocke'];
}
if (strpos($item_id, '_door') !== false || strpos($item_id, '_trapdoor') !== false ||
strpos($item_id, '_fence_gate') !== false) {
return ['dekorationsblocke'];
}
if ((strpos($item_id, '_fence') !== false && strpos($item_id, '_fence_gate') === false) ||
(strpos($item_id, '_wall') !== false && !in_array($item_id, ['wall_banner', 'wall_sign', 'wall_torch'])) ||
$item_id === 'iron_bars') {
return ['dekorationsblocke'];
}
if (strpos($item_id, '_stairs') !== false || strpos($item_id, '_slab') !== false) {
return ['dekorationsblocke'];
}
if (strpos($item_id, '_carpet') !== false) {
return ['dekorationsblocke'];
}
if (in_array($item_id, [
'torch', 'soul_torch', 'lantern', 'soul_lantern',
'campfire', 'soul_campfire', 'end_rod',
'shroomlight', 'froglight', 'sea_lantern'
]) || strpos($item_id, '_candle') !== false || $item_id === 'candle') {
return ['dekorationsblocke'];
}
if (in_array($item_id, [
'crafting_table', 'furnace', 'blast_furnace', 'smoker',
'chest', 'trapped_chest', 'ender_chest', 'barrel',
'enchanting_table', 'anvil', 'chipped_anvil', 'damaged_anvil',
'grindstone', 'smithing_table', 'cartography_table', 'fletching_table',
'loom', 'stonecutter', 'brewing_stand', 'cauldron', 'composter',
'lectern', 'bookshelf', 'chiseled_bookshelf',
'bell', 'beacon', 'conduit', 'lodestone', 'respawn_anchor'
]) || strpos($item_id, '_bed') !== false || strpos($item_id, 'shulker_box') !== false) {
return ['dekorationsblocke'];
}
if (strpos($item_id, '_sign') !== false || strpos($item_id, '_hanging_sign') !== false ||
strpos($item_id, '_banner') !== false ||
in_array($item_id, ['item_frame', 'glow_item_frame', 'painting', 'armor_stand'])) {
return ['dekorationsblocke'];
}
if (in_array($item_id, [
'ladder', 'scaffolding', 'chain',
'flower_pot', 'decorated_pot',
'dragon_egg', 'dragon_head',
'note_block', 'jukebox'
]) || strpos($item_id, '_head') !== false || strpos($item_id, '_skull') !== false ||
strpos($item_id, 'coral') !== false) {
return ['dekorationsblocke'];
}
if (in_array($item_id, [
'stone', 'cobblestone', 'mossy_cobblestone',
'granite', 'polished_granite', 'diorite', 'polished_diorite',
'andesite', 'polished_andesite', 'calcite', 'tuff',
'smooth_stone'
]) || strpos($item_id, 'stone_brick') !== false) {
return ['baublocke'];
}
if (strpos($item_id, 'deepslate') !== false) {
return ['baublocke'];
}
if (strpos($item_id, 'brick') !== false && $item_id !== 'brick' && $item_id !== 'nether_brick') {
return ['baublocke'];
}
if (strpos($item_id, 'sandstone') !== false) {
return ['baublocke'];
}
if (strpos($item_id, 'quartz_') !== false && strpos($item_id, 'nether_quartz') === false) {
return ['baublocke'];
}
if (strpos($item_id, 'prismarine') !== false || strpos($item_id, 'purpur') !== false) {
return ['baublocke'];
}
if (strpos($item_id, 'concrete') !== false || strpos($item_id, 'terracotta') !== false) {
return ['baublocke'];
}
if (strpos($item_id, '_wool') !== false) {
return ['baublocke'];
}
if (strpos($item_id, '_planks') !== false ||
(strpos($item_id, '_log') !== false && strpos($item_id, 'stripped') === false) ||
(strpos($item_id, '_wood') !== false && strpos($item_id, 'stripped') === false)) {
return ['baublocke'];
}
if (strpos($item_id, 'stripped_') !== false) {
return ['baublocke'];
}
if (in_array($item_id, ['bamboo_block', 'bamboo_mosaic', 'crimson_stem', 'warped_stem',
'crimson_hyphae', 'warped_hyphae'])) {
return ['baublocke'];
}
if (strpos($item_id, 'copper_') !== false && strpos($item_id, '_ingot') === false &&
strpos($item_id, '_nugget') === false && $item_id !== 'copper_door' && $item_id !== 'copper_trapdoor') {
return ['baublocke'];
}
if (strpos($item_id, '_block') !== false) {
return ['baublocke'];
}
if (in_array($item_id, [
'netherrack', 'soul_sand', 'soul_soil',
'basalt', 'polished_basalt', 'smooth_basalt',
'end_stone', 'obsidian', 'crying_obsidian'
]) || strpos($item_id, 'blackstone') !== false || strpos($item_id, 'end_stone_brick') !== false) {
return ['baublocke'];
}
if (in_array($item_id, [
'glowstone', 'sponge', 'wet_sponge',
'ice', 'packed_ice', 'blue_ice',
'magma_block', 'slime_block', 'honey_block',
'hay_block', 'dried_kelp_block',
'mud', 'packed_mud', 'mud_bricks',
'dripstone_block', 'amethyst_block', 'budding_amethyst'
])) {
return ['baublocke'];
}
return ['baublocke'];
}
}
// ===========================================================
// DATABASE HELPER
// ===========================================================
class WIS_DB {
/**
* Gibt die Bild-URL eines Items zurück.
* Priorität: custom_image_url > automatisch generierte URL aus item_id.
*/
public static function get_item_image($item) {
if (!empty($item->custom_image_url)) {
return esc_url($item->custom_image_url);
}
$img_base = get_option('wis_image_base_url', '');
$img_name = str_replace(':', '_', $item->item_id) . '.png';
return $img_base . $img_name;
}
public static function get_items($args = []) {
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$where_parts = ["1=1"];
if (isset($args['status'])) {
$where_parts[] = $wpdb->prepare("status = %s", $args['status']);
}
if (isset($args['category_slug']) && !empty($args['category_slug'])) {
$search_pattern = '%"' . $args['category_slug'] . '"%';
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
}
if (isset($args['ids']) && is_array($args['ids']) && !empty($args['ids'])) {
$ids = array_map('intval', $args['ids']);
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$where_parts[] = sprintf("id IN ($placeholders)", ...$ids);
}
if (isset($args['search']) && !empty($args['search'])) {
$search_like = '%' . $wpdb->esc_like($args['search']) . '%';
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
}
$where = implode(" AND ", $where_parts);
$limit = isset($args['limit']) ? "LIMIT " . intval($args['limit']) : "";
$orderby = isset($args['orderby']) ? "ORDER BY " . sanitize_text_field($args['orderby']) : "ORDER BY name ASC";
return $wpdb->get_results("SELECT * FROM $table WHERE $where $orderby $limit");
}
public static function count_items($args = []) {
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$where_parts = ["1=1"];
if (isset($args['status'])) {
$where_parts[] = $wpdb->prepare("status = %s", $args['status']);
}
if (isset($args['category_slug']) && !empty($args['category_slug'])) {
$search_pattern = '%"' . $args['category_slug'] . '"%';
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
}
if (isset($args['search']) && !empty($args['search'])) {
$search_like = '%' . $wpdb->esc_like($args['search']) . '%';
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
}
$where = implode(" AND ", $where_parts);
return (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where");
}
public static function get_item($id) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_items WHERE id = %d",
$id
));
}
public static function get_item_by_item_id($item_id) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_items WHERE item_id = %s",
$item_id
));
}
public static function insert_item($data) {
global $wpdb;
if (empty($data['categories']) || $data['categories'] === '[]') {
$auto_cats = WIS_Item_Categorizer::auto_categorize($data['item_id']);
$data['categories'] = json_encode($auto_cats);
}
return $wpdb->insert($wpdb->prefix . 'wis_items', $data);
}
public static function update_item($id, $data) {
global $wpdb;
return $wpdb->update($wpdb->prefix . 'wis_items', $data, ['id' => $id]);
}
public static function delete_item($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_items', ['id' => $id]);
}
public static function get_servers() {
global $wpdb;
return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wis_servers ORDER BY name ASC");
}
public static function insert_server($slug, $name) {
global $wpdb;
return $wpdb->insert($wpdb->prefix . 'wis_servers', [
'slug' => sanitize_title($slug),
'name' => sanitize_text_field($name)
]);
}
public static function delete_server($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_servers', ['id' => $id]);
}
public static function get_categories() {
global $wpdb;
$table = $wpdb->prefix . 'wis_categories';
// Prüfen ob sort_order-Spalte existiert (Migration ggf. noch nicht gelaufen)
$has_sort = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'sort_order'");
if ($has_sort) {
return $wpdb->get_results("SELECT * FROM $table ORDER BY parent_id ASC, sort_order ASC, name ASC");
}
return $wpdb->get_results("SELECT * FROM $table ORDER BY parent_id ASC, name ASC");
}
public static function insert_category($name, $parent_id = 0) {
global $wpdb;
// sort_order = höchster bestehender Wert + 10
$max_order = (int) $wpdb->get_var("SELECT MAX(sort_order) FROM {$wpdb->prefix}wis_categories WHERE parent_id = " . intval($parent_id));
return $wpdb->insert($wpdb->prefix . 'wis_categories', [
'parent_id' => intval($parent_id),
'sort_order' => $max_order + 10,
'name' => sanitize_text_field($name),
'slug' => sanitize_title($name),
]);
}
public static function delete_category($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_categories', ['id' => $id]);
}
public static function get_coupons() {
global $wpdb;
return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wis_coupons ORDER BY created_at DESC");
}
public static function get_coupon_by_code($code) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_coupons WHERE code = %s",
strtoupper($code)
));
}
/**
* Prüft ob ein Spieler einen Gutschein bereits eingelöst hat.
*/
public static function coupon_used_by_player($coupon_id, $player_name) {
global $wpdb;
$table = $wpdb->prefix . 'wis_coupon_uses';
// Tabelle existiert? (Fallback für alte Installationen)
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) return false;
return (bool) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $table WHERE coupon_id = %d AND player_name = %s",
$coupon_id, $player_name
));
}
/**
* Schreibt die Einlösung eines Gutscheins durch einen Spieler.
* Nutzt INSERT IGNORE damit der UNIQUE KEY als Doppelschutz wirkt.
*/
public static function record_coupon_use($coupon_id, $player_name) {
global $wpdb;
$table = $wpdb->prefix . 'wis_coupon_uses';
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) return false;
return $wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO $table (coupon_id, player_name) VALUES (%d, %s)",
$coupon_id, $player_name
));
}
public static function insert_coupon($data) {
global $wpdb;
$data['code'] = strtoupper($data['code']);
return $wpdb->insert($wpdb->prefix . 'wis_coupons', $data);
}
public static function update_coupon($id, $data) {
global $wpdb;
if (isset($data['code'])) {
$data['code'] = strtoupper($data['code']);
}
return $wpdb->update($wpdb->prefix . 'wis_coupons', $data, ['id' => $id]);
}
public static function delete_coupon($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_coupons', ['id' => $id]);
}
public static function get_orders($limit = 100) {
global $wpdb;
return $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_orders ORDER BY created_at DESC LIMIT %d",
$limit
));
}
public static function get_order($id) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_orders WHERE id = %d",
$id
));
}
public static function insert_order($data) {
global $wpdb;
return $wpdb->insert($wpdb->prefix . 'wis_orders', $data);
}
public static function update_order_status($id, $status) {
global $wpdb;
return $wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => $status],
['id' => $id]
);
}
public static function delete_order($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_orders', ['id' => $id]);
}
}
// ===========================================================
// ADMIN PAGES
// ===========================================================
class WIS_Admin {
// Wird auf admin_init gefeuert vor jeder HTML-Ausgabe, damit wp_redirect() funktioniert
public static function handle_save_item() {
if (!isset($_POST['wis_save_item'])) return;
if (!current_user_can('manage_options')) return;
check_admin_referer('wis_item_form');
global $wpdb;
$item_type = sanitize_text_field($_POST['item_type'] ?? 'minecraft');
if ($item_type === 'gift_card') {
$gc_min = max(1, intval($_POST['gift_card_min'] ?? 100));
$gc_max = max($gc_min, intval($_POST['gift_card_max'] ?? 5000));
// item_id kodiert Min+Max eindeutig und vom create_order erkennbar
$resolved_item_id = 'gift_card_' . $gc_min . '_' . $gc_max;
$_POST['item_id'] = $resolved_item_id;
// Preis = Mindestwert (wird im Frontend durch Nutzereingabe überschrieben)
$_POST['price'] = $gc_min;
} elseif ($item_type === 'fly') {
$resolved_item_id = sanitize_text_field($_POST['fly_duration'] ?? 'fly_5min');
} elseif ($item_type === 'rank') {
$rank_id = preg_replace('/[^a-z0-9_\-]/', '', strtolower($_POST['rank_id'] ?? 'vip'));
$lp_group = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_POST['lp_group'] ?? $rank_id);
$default_group = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_POST['default_group'] ?? 'default');
$rank_days = max(0, intval($_POST['rank_days'] ?? 30));
if (empty($lp_group)) $lp_group = $rank_id;
if (empty($default_group)) $default_group = 'default';
$resolved_item_id = 'rank_' . $rank_id . '_' . $lp_group . '_' . $default_group . '_' . $rank_days;
} elseif ($item_type === 'fly_abo') {
$resolved_item_id = 'fly_abo';
} elseif ($item_type === 'plot_slots') {
$plot_extra_slots = max(1, intval($_POST['plot_extra_slots'] ?? 1));
$resolved_item_id = 'plot_slots_' . $plot_extra_slots;
} elseif ($item_type === 'plot_abo') {
$plot_abo_slots = max(1, intval($_POST['plot_abo_slots'] ?? 1));
$resolved_item_id = 'plot_abo_' . $plot_abo_slots;
} elseif ($item_type === 'item_abo') {
$abo_item_id = sanitize_text_field($_POST['abo_item_id'] ?? '');
$abo_daily_qty = max(1, intval($_POST['abo_daily_qty'] ?? 1));
$abo_duration = max(1, intval($_POST['abo_duration_days'] ?? 30));
if (empty($abo_item_id)) $abo_item_id = 'minecraft:stone';
$resolved_item_id = 'item_abo_' . sanitize_text_field($abo_item_id) . '_' . $abo_daily_qty . '_' . $abo_duration;
} elseif ($item_type === 'custom_cmd') {
$cmd_slug = preg_replace('/[^a-z0-9_\-]/', '', strtolower($_POST['custom_cmd_id'] ?? 'custom'));
if (empty($cmd_slug)) $cmd_slug = 'custom_' . time();
$resolved_item_id = 'custom_cmd_' . $cmd_slug;
} else {
$resolved_item_id = sanitize_text_field($_POST['item_id'] ?? '');
}
if (empty($resolved_item_id) || empty(trim($_POST['name'] ?? ''))) {
// Fehler als transient speichern, damit page_items() ihn anzeigen kann
set_transient('wis_save_error_' . get_current_user_id(), '❌ Name und Item-ID sind Pflichtfelder.', 30);
wp_redirect(wp_get_referer() ?: admin_url('admin.php?page=wis_items'));
exit;
}
$data = [
'item_id' => $resolved_item_id,
'name' => sanitize_text_field($_POST['name']),
'description' => sanitize_textarea_field($_POST['description'] ?? ''),
'price' => intval($_POST['price'] ?? 0),
'offer_price' => intval($_POST['offer_price'] ?? 0),
'is_offer' => isset($_POST['is_offer']) ? 1 : 0,
'servers' => isset($_POST['servers']) ? json_encode(array_map('sanitize_text_field', $_POST['servers'])) : '[]',
'categories' => isset($_POST['categories']) ? json_encode(array_map('sanitize_text_field', $_POST['categories'])) : '[]',
'custom_image_url' => esc_url_raw($_POST['custom_image_url'] ?? ''),
'custom_command' => $item_type === 'custom_cmd' ? sanitize_text_field($_POST['custom_command'] ?? '') : null,
'sell_enabled' => isset($_POST['sell_enabled']) ? 1 : 0,
'sell_price_mode' => in_array($_POST['sell_price_mode'] ?? '', ['percent','fixed','minus']) ? $_POST['sell_price_mode'] : 'percent',
'sell_price_value' => max(0, intval($_POST['sell_price_value'] ?? 80)),
'daily_sell_limit' => max(0, intval($_POST['daily_sell_limit'] ?? 0)),
'status' => (intval($_POST['price'] ?? 0) > 0 || $item_type === 'fly' || $item_type === 'rank' || $item_type === 'fly_abo' || $item_type === 'plot_slots' || $item_type === 'plot_abo' || $item_type === 'item_abo' || $item_type === 'custom_cmd' || $item_type === 'gift_card') ? 'publish' : 'draft',
];
// Preishistorie loggen wenn ein bestehendes Item bearbeitet wird
$price_history_table = $wpdb->prefix . 'wis_price_history';
$ph_exists = $wpdb->get_var("SHOW TABLES LIKE '$price_history_table'");
$editor = wp_get_current_user()->user_login ?: 'admin';
$edit_id = intval($_POST['edit_id'] ?? $_GET['edit'] ?? 0);
if ($edit_id && $ph_exists) {
$existing_item = WIS_DB::get_item($edit_id);
if ($existing_item) {
foreach (['price' => 'Verkaufspreis', 'offer_price' => 'Angebotspreis', 'sell_price_value' => 'Ankaufswert'] as $field => $label) {
$old_val = intval($existing_item->$field ?? 0);
$new_val = intval($data[$field] ?? 0);
if ($old_val !== $new_val) {
$wpdb->insert($price_history_table, [
'item_id' => $existing_item->item_id,
'item_name' => $existing_item->name,
'field' => $label,
'old_value' => $old_val,
'new_value' => $new_val,
'changed_by' => $editor,
]);
}
}
}
}
if (isset($_GET['edit'])) {
$result = WIS_DB::update_item(intval($_GET['edit']), $data);
if ($result !== false) {
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . intval($_GET['edit']) . '&saved=1'));
exit;
} else {
set_transient('wis_save_error_' . get_current_user_id(), '❌ Fehler beim Speichern: ' . $wpdb->last_error, 30);
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . intval($_GET['edit'])));
exit;
}
} else {
$existing = WIS_DB::get_item_by_item_id($resolved_item_id);
if ($existing) {
WIS_DB::update_item($existing->id, $data);
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . $existing->id . '&saved=1'));
exit;
}
$result = WIS_DB::insert_item($data);
if ($result) {
$new_id = $wpdb->insert_id;
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . $new_id . '&created=1'));
exit;
} else {
$err = $wpdb->last_error ?: 'Unbekannter Fehler.';
set_transient('wis_save_error_' . get_current_user_id(), '❌ Fehler beim Erstellen: ' . $err, 30);
wp_redirect(admin_url('admin.php?page=wis_items&add=1'));
exit;
}
}
}
public static function register_menu() {
add_menu_page(
'Ingame Shop',
'Ingame Shop',
'manage_options',
'wis_shop',
[self::class, 'page_overview'],
'dashicons-cart',
6
);
add_submenu_page('wis_shop', 'Einstellungen', 'Einstellungen', 'manage_options', 'wis_shop');
add_submenu_page('wis_shop', 'Items', 'Items', 'manage_options', 'wis_items', [self::class, 'page_items']);
add_submenu_page('wis_shop', 'Bestellungen', 'Bestellungen', 'manage_options', 'wis_orders', [self::class, 'page_orders']);
add_submenu_page('wis_shop', 'Server', 'Server', 'manage_options', 'wis_servers', [self::class, 'page_servers']);
add_submenu_page('wis_shop', 'Kategorien', 'Kategorien', 'manage_options', 'wis_categories', [self::class, 'page_categories']);
add_submenu_page('wis_shop', 'Angebote', 'Angebote', 'manage_options', 'wis_angebote', [self::class, 'page_angebote']);
add_submenu_page('wis_shop', 'Gutscheine', 'Gutscheine', 'manage_options', 'wis_coupons', [self::class, 'page_coupons']);
add_submenu_page('wis_shop', 'Analyse', 'Analyse', 'manage_options', 'wis_analyse', [self::class, 'page_analyse']);
add_submenu_page('wis_shop', 'Top Spender', 'Top Spender', 'manage_options', 'wis_top_spenders', [self::class, 'page_top_spenders']);
add_submenu_page('wis_shop', 'Ankauf-Log', 'Ankauf-Log', 'manage_options', 'wis_sell_log', [self::class, 'page_sell_log']);
add_submenu_page('wis_shop', 'Preishistorie', 'Preishistorie', 'manage_options', 'wis_price_history', [self::class, 'page_price_history']);
add_submenu_page('wis_shop', 'Abo-Verwaltung', 'Abo-Verwaltung', 'manage_options', 'wis_abo_admin', [self::class, 'page_abo_admin']);
add_submenu_page('wis_shop', 'JSON Export/Import', 'JSON Tools', 'manage_options', 'wis_json', [self::class, 'page_json']);
add_submenu_page('wis_shop', 'Shop Reset', 'Reset', 'manage_options', 'wis_reset', [self::class, 'page_reset']);
}
public static function page_overview() {
if (isset($_POST['wis_save_settings']) && check_admin_referer('wis_settings')) {
update_option('wis_currency_name', sanitize_text_field($_POST['wis_currency_name']));
update_option('wis_image_base_url', esc_url_raw($_POST['wis_image_base_url']));
update_option('wis_header_text', wp_kses_post($_POST['wis_header_text']));
update_option('wis_coupon_exclude_offers', isset($_POST['wis_coupon_exclude_offers']) ? '1' : '0');
update_option('wis_daily_deal_discount', intval($_POST['wis_daily_deal_discount']));
update_option('wis_offline_queue_enabled', isset($_POST['wis_offline_queue_enabled']) ? '1' : '0');
update_option('wis_tax_enabled', isset($_POST['wis_tax_enabled']) ? '1' : '0');
update_option('wis_tax_rate', max(0, min(100, floatval(str_replace(',', '.', $_POST['wis_tax_rate'] ?? '0')))));
$allowed_pp = ['24', '25', '50', '100', '-1'];
$pp_val = sanitize_text_field($_POST['wis_default_per_page'] ?? '24');
update_option('wis_default_per_page', in_array($pp_val, $allowed_pp) ? $pp_val : '25');
echo '<div class="updated"><p>✅ Einstellungen gespeichert!</p></div>';
}
if (isset($_POST['wis_regen_key']) && check_admin_referer('wis_regen_key')) {
update_option('wis_api_key', bin2hex(random_bytes(24)));
echo '<div class="updated"><p>🔑 Neuer API-Key generiert! Bitte in der config.yml des Spigot-Plugins aktualisieren.</p></div>';
}
global $wpdb;
$stats = [
'items' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_items"),
'orders' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_orders"),
'servers' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_servers"),
'coupons' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_coupons")
];
?>
<div class="wrap">
<h1>🛒 WP Ingame Shop Pro <span style="color:#28a745;">v<?php echo WIS_VERSION; ?></span></h1>
<div class="card" style="max-width:800px; margin-top:20px;">
<h2>📊 Statistiken</h2>
<table class="widefat">
<tr><th>Items im Shop:</th><td><strong><?php echo $stats['items']; ?></strong></td></tr>
<tr><th>Bestellungen:</th><td><strong><?php echo $stats['orders']; ?></strong></td></tr>
<tr><th>Server:</th><td><strong><?php echo $stats['servers']; ?></strong></td></tr>
<tr><th>Gutscheine:</th><td><strong><?php echo $stats['coupons']; ?></strong></td></tr>
</table>
</div>
<div class="card" style="max-width:800px; margin-top:20px; padding:20px;">
<h2>⚙️ Einstellungen</h2>
<form method="post">
<?php wp_nonce_field('wis_settings'); ?>
<table class="form-table">
<tr>
<th><label>Shop Header Text</label></th>
<td>
<?php
wp_editor(
get_option('wis_header_text', ''),
'wis_header_text',
[
'textarea_name' => 'wis_header_text',
'textarea_rows' => 5,
'media_buttons' => false,
'teeny' => true,
'quicktags' => true,
'tinymce' => [
'toolbar1' => 'bold,italic,underline,bullist,numlist,link,unlink,separator,alignleft,aligncenter,alignright,alignjustify,separator,undo,redo',
'toolbar2' => '',
],
]
);
?>
<p class="description" style="margin-top:8px;"><strong>Live-Vorschau:</strong></p>
<div id="wis-header-preview" style="margin-top:6px;padding:12px 18px;background:#d4edda;border:1px solid #b8dbc0;border-radius:6px;color:#155724;font-size:14px;min-height:40px;"></div>
<script>
jQuery(function($) {
function updatePreview() {
var content = '';
if (typeof tinymce !== 'undefined' && tinymce.get('wis_header_text')) {
content = tinymce.get('wis_header_text').getContent();
} else {
content = $('#wis_header_text').val();
}
$('#wis-header-preview').html(content || '<em style="color:#888;">Vorschau erscheint hier\u2026</em>');
}
$(document).on('tinymce-editor-init', function(event, editor) {
if (editor.id === 'wis_header_text') {
editor.on('keyup change NodeChange', updatePreview);
updatePreview();
}
});
$(document).on('input change', '#wis_header_text', updatePreview);
setTimeout(updatePreview, 800);
});
</script>
</td>
</tr>
<tr>
<th><label for="currency">Währungsname</label></th>
<td>
<input type="text" id="currency" name="wis_currency_name" value="<?php echo esc_attr(get_option('wis_currency_name')); ?>" class="regular-text">
</td>
</tr>
<tr>
<th><label for="image_url">Bilder Basis-URL</label></th>
<td>
<input type="text" id="image_url" name="wis_image_base_url" value="<?php echo esc_attr(get_option('wis_image_base_url')); ?>" class="large-text code">
<p class="description">Muss mit / enden! Z.B.: https://git.viper.ipv64.net/.../images/</p>
</td>
</tr>
<tr>
<th>Gutscheine bei Angeboten</th>
<td>
<label>
<input type="checkbox" name="wis_coupon_exclude_offers" value="1" <?php checked(get_option('wis_coupon_exclude_offers'), '1'); ?>>
Gutscheine NICHT auf Angebote anwenden
</label>
</td>
</tr>
<tr>
<tr>
<th>Daily Deal</th>
<td>
<label>
<input type="checkbox" name="wis_daily_deal_enabled" value="1" <?php checked(get_option('wis_daily_deal_enabled'), '1'); ?>>
Automatisches Tagesangebot aktivieren
</label>
</td>
</tr>
<tr>
<th><label for="discount">Daily Deal Rabatt (%)</label></th>
<td>
<input type="number" id="discount" name="wis_daily_deal_discount" value="<?php echo esc_attr(get_option('wis_daily_deal_discount', 20)); ?>" min="1" max="99">
</td>
</tr>
<tr>
<th>Offline-Queue</th>
<td>
<label>
<input type="checkbox" name="wis_offline_queue_enabled" value="1" <?php checked(get_option('wis_offline_queue_enabled', '0'), '1'); ?>>
Items auch an offline Spieler liefern (beim nächsten Login)
</label>
<p class="description">Erfordert Spigot-Plugin v6.3+ mit Offline-Queue-Unterstützung</p>
</td>
</tr>
<tr>
<th>Steuer aktivieren</th>
<td>
<label>
<input type="checkbox" name="wis_tax_enabled" value="1" <?php checked(get_option('wis_tax_enabled', '0'), '1'); ?>>
Steuer auf den Endbetrag aufschlagen (nach Gutschein-Abzug)
</label>
</td>
</tr>
<tr>
<th><label for="wis_tax_rate">Steuersatz (%)</label></th>
<td>
<input type="number" id="wis_tax_rate" name="wis_tax_rate"
value="<?php echo esc_attr(get_option('wis_tax_rate', '0')); ?>"
min="0" max="100" step="0.01" style="width:100px;">
<p class="description">Z.B. <code>19</code> für 19 % MwSt. Nur aktiv wenn „Steuer aktivieren" angehakt ist.</p>
</td>
</tr>
<tr>
<th><label for="wis_default_per_page">Items pro Seite (Standard)</label></th>
<td>
<select id="wis_default_per_page" name="wis_default_per_page">
<option value="24" <?php selected(get_option('wis_default_per_page','25'), '24'); ?>>24 (Standard)</option>
<option value="25" <?php selected(get_option('wis_default_per_page','25'), '25'); ?>>25</option>
<option value="50" <?php selected(get_option('wis_default_per_page','25'), '50'); ?>>50</option>
<option value="100" <?php selected(get_option('wis_default_per_page','25'), '100'); ?>>100</option>
<option value="-1" <?php selected(get_option('wis_default_per_page','25'), '-1'); ?>>Alle</option>
</select>
<p class="description">Spieler können dies im Shop-Frontend per Dropdown selbst anpassen.</p>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="wis_save_settings" class="button button-primary" value="Einstellungen speichern">
</p>
</form>
</div>
<div class="card" style="max-width:800px; margin-top:20px; padding:20px; border-left:4px solid #0073aa;">
<h2>🔑 Spigot API-Key</h2>
<p>Dieser Key muss in der <code>config.yml</code> des Spigot-Plugins als <code>api-key</code> eingetragen werden.</p>
<div style="display:flex; align-items:center; gap:10px; margin:10px 0;">
<input type="text"
id="wis-api-key-field"
value="<?php echo esc_attr(get_option('wis_api_key', '')); ?>"
class="large-text code"
readonly
style="font-family:monospace; background:#f0f4f8;">
<button type="button" class="button" onclick="
const f = document.getElementById('wis-api-key-field');
f.type='text'; f.select();
document.execCommand('copy');
this.textContent='✅ Kopiert!';
setTimeout(()=>this.textContent='📋 Kopieren',2000);
">📋 Kopieren</button>
</div>
<form method="post" onsubmit="return confirm('Neuen Key generieren? Das Spigot-Plugin muss danach neu konfiguriert werden!');">
<?php wp_nonce_field('wis_regen_key'); ?>
<input type="submit" name="wis_regen_key" class="button button-secondary" value="🔄 Neuen Key generieren">
</form>
<p class="description" style="margin-top:10px;">
Geschützte Endpunkte: <code>pending_orders</code>, <code>execute_order</code>, <code>complete_order</code>, <code>cancel_order</code>, <code>pending_offline</code>
</p>
</div>
</div>
<?php
}
public static function page_items() {
if (isset($_POST['wis_bulk_save']) && check_admin_referer('wis_bulk_edit')) {
$ids = isset($_POST['item_ids']) ? array_map('intval', $_POST['item_ids']) : [];
$action = sanitize_text_field($_POST['bulk_action_type']);
if (!empty($ids) && !empty($action)) {
$count = 0;
foreach ($ids as $id) {
$data = [];
switch ($action) {
case 'price':
if (isset($_POST['item_prices'][$id])) {
$new_price = intval($_POST['item_prices'][$id]);
if ($new_price >= 0) {
$data['price'] = $new_price;
}
}
break;
case 'offer':
$is_offer = isset($_POST['item_offers'][$id]) ? 1 : 0;
$data['is_offer'] = $is_offer;
if (isset($_POST['item_offer_prices'][$id])) {
$offer_price = intval($_POST['item_offer_prices'][$id]);
$data['offer_price'] = $is_offer ? $offer_price : 0;
} else if (!$is_offer) {
$data['offer_price'] = 0;
}
break;
case 'status':
$new_status = sanitize_text_field($_POST['bulk_status']);
if (in_array($new_status, ['publish', 'draft'])) {
$data['status'] = $new_status;
}
break;
case 'server':
if (isset($_POST['item_servers'][$id])) {
$data['servers'] = json_encode($_POST['item_servers'][$id]);
} else {
$data['servers'] = '[]';
}
break;
case 'category':
if (isset($_POST['item_cat_assignments'][$id])) {
$data['categories'] = json_encode($_POST['item_cat_assignments'][$id]);
}
break;
case 'sell':
$data['sell_enabled'] = isset($_POST['item_sell_enabled'][$id]) ? 1 : 0;
$mode = sanitize_text_field($_POST['item_sell_mode'][$id] ?? 'percent');
if (!in_array($mode, ['percent', 'fixed', 'minus'])) $mode = 'percent';
$data['sell_price_mode'] = $mode;
$data['sell_price_value'] = max(0, intval($_POST['item_sell_value'][$id] ?? 80));
break;
}
if (!empty($data)) {
WIS_DB::update_item($id, $data);
$count++;
}
}
echo '<div class="updated"><p>✅ ' . $count . ' Items erfolgreich aktualisiert!</p></div>';
}
}
if (isset($_POST['wis_bulk_apply']) && !empty($_POST['wis_bulk_action']) && isset($_POST['item_ids'])) {
$action = sanitize_text_field($_POST['wis_bulk_action']);
$selected_ids = array_map('intval', $_POST['item_ids']);
$all_servers = WIS_DB::get_servers();
$all_categories = WIS_DB::get_categories();
$currency = get_option('wis_currency_name', 'Coins');
$items_to_edit = WIS_DB::get_items(['ids' => $selected_ids]);
?>
<div class="wrap">
<h1>📦 Mehrfachbearbeitung</h1>
<p><strong>Aktion:</strong> <?php echo esc_html(ucfirst($action)); ?> für <?php echo count($selected_ids); ?> Items.</p>
<div class="card" style="max-width:2000px; padding:20px; margin-top:20px;">
<form method="post">
<?php wp_nonce_field('wis_bulk_edit'); ?>
<input type="hidden" name="bulk_action_type" value="<?php echo esc_attr($action); ?>">
<?php foreach ($selected_ids as $id): ?>
<input type="hidden" name="item_ids[]" value="<?php echo $id; ?>">
<?php endforeach; ?>
<table class="form-table">
<?php if ($action === 'price'): ?>
<tr>
<th colspan="2"><h3>Preis pro Item festlegen</h3></th>
</tr>
<tr>
<td colspan="2" style="padding:0;">
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
<table class="widefat striped">
<thead>
<tr>
<th style="width:50%">Item Name</th>
<th style="width:25%">Aktueller Preis</th>
<th style="width:25%">Neuer Preis (<?php echo esc_html($currency); ?>)</th>
</tr>
</thead>
<tbody>
<?php foreach($items_to_edit as $item): ?>
<tr>
<td>
<strong><?php echo esc_html($item->name); ?></strong><br>
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
</td>
<td><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></td>
<td>
<input type="number"
name="item_prices[<?php echo $item->id; ?>]"
value="<?php echo esc_attr($item->price); ?>"
min="0"
style="width:100%; padding:5px;">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</td>
</tr>
<?php elseif ($action === 'offer'): ?>
<tr>
<th colspan="2"><h3>Angebot pro Item festlegen</h3></th>
</tr>
<tr>
<td colspan="2" style="padding:0;">
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
<table class="widefat striped">
<thead>
<tr>
<th style="width:40%">Item Name</th>
<th style="width:20%">Normalpreis</th>
<th style="width:20%">Als Angebot?</th>
<th style="width:20%">Angebotspreis</th>
</tr>
</thead>
<tbody>
<?php foreach($items_to_edit as $item): ?>
<tr>
<td>
<strong><?php echo esc_html($item->name); ?></strong><br>
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
</td>
<td><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></td>
<td>
<input type="checkbox"
name="item_offers[<?php echo $item->id; ?>]"
value="1"
<?php checked($item->is_offer, 1); ?>
onchange="toggleOfferPrice(this, <?php echo $item->id; ?>)">
</td>
<td>
<input type="number"
name="item_offer_prices[<?php echo $item->id; ?>]"
id="offer_price_<?php echo $item->id; ?>"
value="<?php echo esc_attr($item->offer_price); ?>"
min="0"
style="width:100%; padding:5px;"
<?php if(!$item->is_offer): ?>disabled<?php endif; ?>>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
function toggleOfferPrice(checkbox, itemId) {
const input = document.getElementById('offer_price_' + itemId);
input.disabled = !checkbox.checked;
}
</script>
</td>
</tr>
<?php elseif ($action === 'status'): ?>
<tr>
<th><label>Neuer Status für alle ausgewählten Items</label></th>
<td>
<select name="bulk_status">
<option value="publish">Aktivieren (Publish)</option>
<option value="draft">Deaktivieren (Draft)</option>
</select>
</td>
</tr>
<?php elseif ($action === 'server'): ?>
<tr>
<th colspan="2"><h3>Server pro Item zuweisen</h3></th>
</tr>
<tr>
<td colspan="2" style="padding:0;">
<?php if (empty($all_servers)): ?>
<p>Keine Server vorhanden.</p>
<?php else: ?>
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
<table class="widefat striped">
<thead>
<tr>
<th style="width:40%">Item Name</th>
<th style="width:60%">Server (Mehrfachauswahl möglich)</th>
</tr>
</thead>
<tbody>
<?php foreach($items_to_edit as $item):
$current_servers = json_decode($item->servers, true) ?: [];
?>
<tr>
<td>
<strong><?php echo esc_html($item->name); ?></strong><br>
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
</td>
<td>
<?php foreach ($all_servers as $s):
$checked = in_array($s->slug, $current_servers) ? 'checked' : '';
?>
<label style="display:inline-block; margin-right:15px; margin-bottom:5px;">
<input type="checkbox"
name="item_servers[<?php echo $item->id; ?>][]"
value="<?php echo esc_attr($s->slug); ?>"
<?php echo $checked; ?>>
<?php echo esc_html($s->name); ?>
</label>
<?php endforeach; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</td>
</tr>
<?php elseif ($action === 'category'): ?>
<tr>
<th colspan="2"><h3>Kategorien pro Item zuweisen</h3></th>
</tr>
<tr>
<td colspan="2" style="padding:0;">
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
<?php if(empty($items_to_edit)): ?>
<p>Fehler beim Laden der Items.</p>
<?php else: ?>
<table class="widefat striped">
<thead>
<tr>
<th style="width:35%">Item Name</th>
<th style="width:65%">Kategorie (Mehrfachauswahl mit STRG+Klick)</th>
</tr>
</thead>
<tbody>
<?php foreach($items_to_edit as $item):
$current_cats = json_decode($item->categories, true) ?: [];
?>
<tr>
<td>
<strong><?php echo esc_html($item->name); ?></strong><br>
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
</td>
<td>
<select name="item_cat_assignments[<?php echo $item->id; ?>][]" multiple style="height:250px; width:100%; font-size:14px; padding:5px;">
<?php foreach($all_categories as $cat):
$selected = in_array($cat->slug, $current_cats) ? 'selected' : '';
?>
<option value="<?php echo esc_attr($cat->slug); ?>" <?php echo $selected; ?>>
<?php echo esc_html($cat->name); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</td>
</tr>
<?php elseif ($action === 'sell'): ?>
<tr>
<th colspan="2"><h3>Ankauf pro Item konfigurieren</h3></th>
</tr>
<tr>
<td colspan="2" style="padding:0;">
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
<table class="widefat striped">
<thead>
<tr>
<th style="width:30%">Item Name</th>
<th style="width:12%">VK-Preis</th>
<th style="width:13%">Ankauf?</th>
<th style="width:18%">Modus</th>
<th style="width:12%">Wert</th>
<th style="width:15%">Ankaufspreis</th>
</tr>
</thead>
<tbody>
<?php foreach ($items_to_edit as $item):
$s_enabled = !empty($item->sell_enabled);
$s_mode = $item->sell_price_mode ?? 'percent';
$s_value = $item->sell_price_value ?? 80;
?>
<tr id="sell_row_<?php echo $item->id; ?>">
<td>
<strong><?php echo esc_html($item->name); ?></strong><br>
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
</td>
<td><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></td>
<td>
<input type="checkbox"
name="item_sell_enabled[<?php echo $item->id; ?>]"
value="1"
<?php checked($s_enabled); ?>
onchange="wisUpdateSellRow(<?php echo $item->id; ?>, <?php echo intval($item->price); ?>)">
</td>
<td>
<select name="item_sell_mode[<?php echo $item->id; ?>]"
id="sell_mode_<?php echo $item->id; ?>"
onchange="wisUpdateSellRow(<?php echo $item->id; ?>, <?php echo intval($item->price); ?>)"
style="width:100%">
<option value="percent" <?php selected($s_mode, 'percent'); ?>>% vom VK</option>
<option value="minus" <?php selected($s_mode, 'minus'); ?>>VK minus</option>
<option value="fixed" <?php selected($s_mode, 'fixed'); ?>>Fixpreis</option>
</select>
</td>
<td>
<input type="number"
name="item_sell_value[<?php echo $item->id; ?>]"
id="sell_value_<?php echo $item->id; ?>"
value="<?php echo esc_attr($s_value); ?>"
min="0" style="width:70px"
oninput="wisUpdateSellRow(<?php echo $item->id; ?>, <?php echo intval($item->price); ?>)">
</td>
<td id="sell_preview_<?php echo $item->id; ?>" style="color:#0073aa;font-weight:bold;">
<?php
if ($s_enabled) {
if ($s_mode === 'percent') $sp = round($item->price * $s_value / 100, 2);
elseif ($s_mode === 'minus') $sp = max(0, $item->price - $s_value);
else $sp = max(0, $s_value);
echo number_format($sp, 2) . ' ' . esc_html($currency);
} else {
echo '<span style="color:#aaa"></span>';
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
function wisUpdateSellRow(id, vk) {
var enabled = document.querySelector('input[name="item_sell_enabled[' + id + ']"]').checked;
var mode = document.getElementById('sell_mode_' + id).value;
var val = parseFloat(document.getElementById('sell_value_' + id).value) || 0;
var preview = document.getElementById('sell_preview_' + id);
if (!enabled) { preview.innerHTML = '<span style="color:#aaa"></span>'; return; }
var price = 0;
if (mode === 'percent') price = Math.max(0, Math.round(vk * val) / 100);
else if (mode === 'minus') price = Math.max(0, vk - val);
else price = Math.max(0, val);
preview.textContent = price.toFixed(2) + ' <?php echo esc_js($currency); ?>';
}
// Initiale Vorschau für alle Zeilen
document.addEventListener('DOMContentLoaded', function() {
<?php foreach ($items_to_edit as $item): ?>
wisUpdateSellRow(<?php echo $item->id; ?>, <?php echo intval($item->price); ?>);
<?php endforeach; ?>
});
</script>
</td>
</tr>
<?php endif; ?>
</table>
<p class="submit">
<a href="<?php echo admin_url('admin.php?page=wis_items'); ?>" class="button">Abbrechen</a>
<input type="submit" name="wis_bulk_save" class="button button-primary" value="Jetzt ändern">
</p>
</form>
</div>
</div>
<?php
return;
}
if (isset($_GET['action'], $_GET['id'], $_GET['_wpnonce'])) {
if (!wp_verify_nonce($_GET['_wpnonce'], 'wis_item_action')) {
wp_die('Security check failed');
}
$id = intval($_GET['id']);
if ($_GET['action'] === 'delete') {
WIS_DB::delete_item($id);
echo '<div class="updated"><p>✅ Item gelöscht!</p></div>';
} elseif ($_GET['action'] === 'toggle_status') {
$item = WIS_DB::get_item($id);
$new_status = ($item->status === 'publish') ? 'draft' : 'publish';
WIS_DB::update_item($id, ['status' => $new_status]);
echo '<div class="updated"><p>✅ Status geändert!</p></div>';
}
}
// Fehlermeldung aus handle_save_item() anzeigen (falls Validierung fehlschlug)
$save_error = get_transient('wis_save_error_' . get_current_user_id());
if ($save_error) {
delete_transient('wis_save_error_' . get_current_user_id());
echo '<div class="error"><p>' . esc_html($save_error) . '</p></div>';
}
if (isset($_GET['created'])) {
echo '<div class="updated"><p>✅ Item erfolgreich erstellt!</p></div>';
}
if (isset($_GET['saved'])) {
echo '<div class="updated"><p>✅ Item gespeichert!</p></div>';
}
if (isset($_GET['edit']) || isset($_GET['add'])) {
$item = isset($_GET['edit']) ? WIS_DB::get_item(intval($_GET['edit'])) : null;
$servers = WIS_DB::get_servers();
$categories = WIS_DB::get_categories();
$currency = get_option('wis_currency_name', 'Coins');
$item_servers = $item ? json_decode($item->servers, true) : [];
$item_cats = $item ? json_decode($item->categories, true) : [];
if (!$item && isset($_GET['add'])) {
$item_cats = [];
}
?>
<div class="wrap">
<h1><?php echo $item ? 'Item bearbeiten' : 'Neues Item'; ?></h1>
<a href="<?php echo admin_url('admin.php?page=wis_items'); ?>" class="button">← Zurück zur Liste</a>
<form method="post" style="max-width:800px; margin-top:20px;">
<?php wp_nonce_field('wis_item_form'); ?>
<?php if (isset($_GET['edit'])): ?>
<input type="hidden" name="edit_id" value="<?php echo intval($_GET['edit']); ?>">
<?php endif; ?>
<table class="form-table">
<tr>
<th><label for="name">Name *</label></th>
<td><input type="text" id="name" name="name" value="<?php echo $item ? esc_attr($item->name) : ''; ?>" class="regular-text" required></td>
</tr>
<tr>
<th><label for="item_id">Item ID / Typ *</label></th>
<td>
<?php
$fly_ids = ['fly_5min','fly_15min','fly_30min','fly_1h','fly_2h','fly_3h'];
$is_fly = $item && in_array($item->item_id, $fly_ids);
$is_rank = $item && preg_match('/^rank_([^_]+(?:_[^_]+)*)_([a-zA-Z0-9_\-]+)_([a-zA-Z0-9_\-]+)_(\d+)$/', $item->item_id, $rm);
// Format: rank_{rank_id}_{lp_group}_{default_group}_{days}
// Fallback für altes Format rank_{rank_id}_{days}
$is_rank_old = !$is_rank && $item && preg_match('/^rank_(.+)_(\d+)$/', $item->item_id, $rm_old);
$is_fly_abo = $item && ($item->item_id === 'fly_abo' || preg_match('/^fly_abo_\d+$/', $item->item_id));
$cur_abo_days = 0;
$is_plot_slots = $item && preg_match('/^plot_slots_(\d+)$/', $item->item_id, $ps_m);
$cur_plot_extra_slots = $is_plot_slots ? intval($ps_m[1]) : 1;
$is_plot_abo = $item && preg_match('/^plot_abo_(\d+)$/', $item->item_id, $pa_m);
$cur_plot_abo_slots = $is_plot_abo ? intval($pa_m[1]) : 1;
$cur_rank_id = $is_rank ? $rm[1] : ($is_rank_old ? $rm_old[1] : 'vip');
$cur_lp_group = $is_rank ? $rm[2] : $cur_rank_id;
$cur_default_group = $is_rank ? $rm[3] : 'default';
$cur_rank_days = $is_rank ? intval($rm[4]) : ($is_rank_old ? intval($rm_old[2]) : 30);
$cur_label = $item ? esc_attr($item->name) : '';
$is_custom_cmd = $item && preg_match('/^custom_cmd_(.+)$/', $item->item_id, $cc_m);
$cur_custom_cmd_id = $is_custom_cmd ? $cc_m[1] : '';
$cur_custom_command = ($item && $is_custom_cmd) ? ($item->custom_command ?? '') : '';
// Item-Abo: item_abo_{minecraft_item_id}_{daily_qty}_{duration_days}
$is_item_abo = $item && preg_match('/^item_abo_(.+)_(\d+)_(\d+)$/', $item->item_id, $ia_m);
$cur_abo_item_id = $is_item_abo ? $ia_m[1] : 'minecraft:stone';
$cur_abo_daily_qty = $is_item_abo ? intval($ia_m[2]) : 1;
$cur_abo_duration = $is_item_abo ? intval($ia_m[3]) : 30;
$is_gift_card = $item && str_starts_with($item->item_id, 'gift_card');
$detected_type = $is_fly ? 'fly' : (($is_rank || $is_rank_old) ? 'rank' : ($is_fly_abo ? 'fly_abo' : ($is_plot_slots ? 'plot_slots' : ($is_plot_abo ? 'plot_abo' : ($is_item_abo ? 'item_abo' : ($is_custom_cmd ? 'custom_cmd' : ($is_gift_card ? 'gift_card' : 'minecraft')))))));
?>
<select id="item_type" name="item_type" onchange="wisToggleItemType(this.value)" style="margin-bottom:8px;">
<option value="minecraft" <?php echo $detected_type === 'minecraft' ? 'selected' : ''; ?>>Minecraft Item</option>
<option value="fly" <?php echo $detected_type === 'fly' ? 'selected' : ''; ?>>✈ Fly-Gutschein</option>
<option value="rank" <?php echo $detected_type === 'rank' ? 'selected' : ''; ?>>👑 Rang (LuckPerms)</option>
<option value="fly_abo" <?php echo $detected_type === 'fly_abo' ? 'selected' : ''; ?>>✈ Fly-Abo (tägl. Limit)</option>
<option value="plot_slots" <?php echo $detected_type === 'plot_slots' ? 'selected' : ''; ?>>📦 Plot-Slots (einmalig)</option>
<option value="plot_abo" <?php echo $detected_type === 'plot_abo' ? 'selected' : ''; ?>>📦 Plot-Abo (monatlich)</option>
<option value="item_abo" <?php echo $detected_type === 'item_abo' ? 'selected' : ''; ?>>📅 Item-Abo (täglich)</option>
<option value="custom_cmd" <?php echo $detected_type === 'custom_cmd' ? 'selected' : ''; ?>>⚙️ Custom Command</option>
<option value="gift_card" <?php echo $detected_type === 'gift_card' ? 'selected' : ''; ?>>🎁 Gutschein-Karte (freier Betrag)</option>
</select>
<div id="wis_item_minecraft">
<input type="text" id="item_id" name="item_id" value="<?php echo ($item && !$is_fly && !$is_rank && !$is_fly_abo && !$is_plot_slots && !$is_plot_abo) ? esc_attr($item->item_id) : ''; ?>" class="regular-text" placeholder="minecraft:diamond">
<p class="description">Z.B.: minecraft:diamond (Kategorien werden automatisch zugewiesen)</p>
</div>
<div id="wis_item_fly" style="display:none;">
<select name="fly_duration" id="fly_duration">
<option value="fly_5min" <?php echo ($item && $item->item_id === 'fly_5min') ? 'selected' : ''; ?>>✈ 5 Minuten Fly</option>
<option value="fly_15min" <?php echo ($item && $item->item_id === 'fly_15min') ? 'selected' : ''; ?>>✈ 15 Minuten Fly</option>
<option value="fly_30min" <?php echo ($item && $item->item_id === 'fly_30min') ? 'selected' : ''; ?>>✈ 30 Minuten Fly</option>
<option value="fly_1h" <?php echo ($item && $item->item_id === 'fly_1h') ? 'selected' : ''; ?>>✈ 1 Stunde Fly</option>
<option value="fly_2h" <?php echo ($item && $item->item_id === 'fly_2h') ? 'selected' : ''; ?>>✈ 2 Stunden Fly</option>
<option value="fly_3h" <?php echo ($item && $item->item_id === 'fly_3h') ? 'selected' : ''; ?>>✈ 3 Stunden Fly</option>
</select>
<p class="description">Der Spieler bekommt nach dem Kauf Fly für die gewählte Dauer.</p>
</div>
<div id="wis_item_rank" style="display:none;">
<table style="border-collapse:collapse;">
<tr>
<td style="padding:4px 10px 4px 0;"><label for="rank_id"><strong>Rang-ID:</strong></label></td>
<td><input type="text" id="rank_id" name="rank_id" value="<?php echo esc_attr($cur_rank_id); ?>" class="regular-text" placeholder="vip" style="width:160px;"></td>
<td style="padding:4px 0 4px 12px; color:#666; font-size:12px;">Interner Name (frei wählbar, z.&nbsp;B. <code>vip</code>)</td>
</tr>
<tr>
<td style="padding:4px 10px 4px 0;"><label for="lp_group"><strong>LuckPerms-Gruppe:</strong></label></td>
<td><input type="text" id="lp_group" name="lp_group" value="<?php echo esc_attr($cur_lp_group); ?>" class="regular-text" placeholder="vip" style="width:160px;"></td>
<td style="padding:4px 0 4px 12px; color:#666; font-size:12px;">Exakter Gruppenname in LuckPerms</td>
</tr>
<tr>
<td style="padding:4px 10px 4px 0;"><label for="default_group"><strong>Standard-Gruppe (nach Ablauf):</strong></label></td>
<td><input type="text" id="default_group" name="default_group" value="<?php echo esc_attr($cur_default_group); ?>" class="regular-text" placeholder="default" style="width:160px;"></td>
<td style="padding:4px 0 4px 12px; color:#666; font-size:12px;">Gruppe nach Rang-Ablauf (z.&nbsp;B. <code>default</code>)</td>
</tr>
<tr>
<td style="padding:4px 10px 4px 0;"><label for="rank_days"><strong>Laufzeit (Tage):</strong></label></td>
<td>
<input type="number" id="rank_days" name="rank_days" value="<?php echo esc_attr($cur_rank_days); ?>" min="0" style="width:80px;">
<span class="description" style="margin-left:8px;">0 = dauerhaft</span>
</td>
</tr>
</table>
<p class="description" style="margin-top:6px;">
Der Spielername im Shop (Feld <em>Name</em> oben) wird dem Spieler als Rang-Label angezeigt.<br>
Gespeicherte Item-ID: <code>rank_{rang-id}_{lp-gruppe}_{standard-gruppe}_{tage}</code>
</p>
</div>
<div id="wis_item_fly_abo" style="display:none;">
<p class="description" style="margin-top:4px; padding:10px; background:#f0f6fc; border-left:3px solid #2271b1; border-radius:2px;">
<strong>✈ Fly-Abo monatliches Abonnement</strong><br><br>
Spieler zahlen den <strong>Artikelpreis einmalig beim Kauf</strong>.<br>
Danach wird am <strong>1. jedes Monats</strong> automatisch der gleiche Betrag per Vault abgebucht.<br>
Kann der Spieler nicht zahlen, wird das Abo automatisch gekündigt.<br>
Kündigung jederzeit ingame mit <code>/flyabocancel</code> läuft bis Monatsende.<br><br>
<strong>Artikelpreis</strong> = monatlicher Betrag &nbsp;|&nbsp;
Tägl. Fly-Limit: konfigurierbar per <code>fly-abo.max-daily-hours</code> im Plugin.<br>
Gespeicherte Item-ID: <code>fly_abo</code>
</p>
</div>
<div id="wis_item_plot_slots" style="display:none;">
<table style="border-collapse:collapse;">
<tr>
<td style="padding:4px 10px 4px 0;"><label for="plot_extra_slots"><strong>Zusätzliche Plot-Slots:</strong></label></td>
<td>
<input type="number" id="plot_extra_slots" name="plot_extra_slots"
value="<?php echo esc_attr($cur_plot_extra_slots); ?>"
min="1" style="width:80px;">
<span style="margin-left:6px; color:#666;">Slots</span>
</td>
</tr>
</table>
<p class="description" style="margin-top:6px; padding:8px; background:#f6f7f7; border-left:3px solid #50b56a; border-radius:2px;">
<strong>📦 Plot-Slots einmaliger Kauf (permanent)</strong><br><br>
Der Spieler erhält dauerhaft zusätzliche Plot-Slots on top seines Rang-Limits.<br>
LuckPerms Meta <code>plotlimit</code> wird automatisch gesetzt.<br>
Gespeicherte Item-ID: <code>plot_slots_{anzahl}</code>
</p>
</div>
<div id="wis_item_plot_abo" style="display:none;">
<table style="border-collapse:collapse;">
<tr>
<td style="padding:4px 10px 4px 0;"><label for="plot_abo_slots"><strong>Zusätzliche Plot-Slots (Abo):</strong></label></td>
<td>
<input type="number" id="plot_abo_slots" name="plot_abo_slots"
value="<?php echo esc_attr($cur_plot_abo_slots); ?>"
min="1" style="width:80px;">
<span style="margin-left:6px; color:#666;">Slots</span>
</td>
</tr>
</table>
<p class="description" style="margin-top:6px; padding:8px; background:#f0f6fc; border-left:3px solid #2271b1; border-radius:2px;">
<strong>📦 Plot-Abo monatliches Abonnement</strong><br><br>
Spieler zahlen den <strong>Artikelpreis einmalig beim Kauf</strong>.<br>
Am <strong>1. jedes Monats</strong> wird der gleiche Betrag per Vault abgebucht.<br>
Bei Zahlungsausfall: Abo-Slots verfallen, überzählige Plots werden eingefroren.<br>
Kündigung ingame mit <code>/plotabocancel confirm</code> läuft bis Monatsende.<br><br>
<strong>Artikelpreis</strong> = monatlicher Beitrag<br>
Gespeicherte Item-ID: <code>plot_abo_{anzahl}</code>
</p>
</div>
<div id="wis_item_custom_cmd" style="display:none;">
<table style="border-collapse:collapse;width:100%;">
<tr>
<td style="padding:4px 10px 4px 0;width:180px;"><label for="custom_cmd_id"><strong>Interne ID *</strong></label></td>
<td>
<input type="text" id="custom_cmd_id" name="custom_cmd_id"
value="<?php echo esc_attr($cur_custom_cmd_id ?? ''); ?>"
class="regular-text" placeholder="furnace_upgrade">
<p class="description">Nur a-z, 0-9, _ und - erlaubt. Z.B.: <code>furnace_upgrade</code></p>
</td>
</tr>
<tr>
<td style="padding:8px 10px 4px 0;vertical-align:top;"><label for="custom_command"><strong>Command *</strong></label></td>
<td>
<textarea id="custom_command" name="custom_command"
rows="3" style="width:100%;font-family:monospace;"
placeholder="/flgive {player} {amount}"><?php echo esc_textarea($cur_custom_command ?? ''); ?></textarea>
<p class="description">
Verfügbare Platzhalter:<br>
<code>{player}</code> → Spielername<br>
<code>{amount}</code> → gekaufte Menge<br>
<code>{server}</code> → Server-Slug<br>
Mehrere Commands per Zeile möglich. Jede Zeile wird einzeln ausgeführt.
</p>
</td>
</tr>
</table>
<p class="description" style="margin-top:10px;padding:10px;background:#fff8e1;border-left:3px solid #ffc107;border-radius:2px;">
<strong>⚙️ Custom Command beliebiger Server-Command</strong><br><br>
Der eingetragene Command wird nach dem Kauf auf dem Spigot-Server ausgeführt.<br>
Beispiel für FurnaceLevels: <code>/flgive {player} {amount}</code><br>
Gespeicherte Item-ID: <code>custom_cmd_{id}</code>
</p>
</div>
<div id="wis_item_item_abo" style="display:none;">
<table style="border-collapse:collapse;width:100%;">
<tr>
<td style="padding:4px 10px 4px 0;width:200px;"><label for="abo_item_id"><strong>Minecraft Item-ID *</strong></label></td>
<td>
<input type="text" id="abo_item_id" name="abo_item_id"
value="<?php echo esc_attr($cur_abo_item_id ?? 'minecraft:stone'); ?>"
class="regular-text" placeholder="minecraft:dragon_breath">
<p class="description">Z.B.: <code>minecraft:dragon_breath</code> das Item das täglich geliefert wird.</p>
</td>
</tr>
<tr>
<td style="padding:4px 10px 4px 0;"><label for="abo_daily_qty"><strong>Tägliche Menge *</strong></label></td>
<td>
<input type="number" id="abo_daily_qty" name="abo_daily_qty"
value="<?php echo esc_attr($cur_abo_daily_qty ?? 1); ?>"
min="1" max="64" style="width:80px;">
<span style="margin-left:6px; color:#666;">Stück / Tag</span>
<p class="description">Wie viele Items der Spieler täglich erhält (max. 64).</p>
</td>
</tr>
<tr>
<td style="padding:4px 10px 4px 0;"><label for="abo_duration_days"><strong>Laufzeit (Tage) *</strong></label></td>
<td>
<input type="number" id="abo_duration_days" name="abo_duration_days"
value="<?php echo esc_attr($cur_abo_duration ?? 30); ?>"
min="1" style="width:80px;">
<span style="margin-left:6px; color:#666;">Tage</span>
<p class="description">Wie viele Tage das Abo nach dem Kauf aktiv ist (z.B. 30 = 1 Monat).</p>
</td>
</tr>
</table>
<p class="description" style="margin-top:10px;padding:10px;background:#f0fff4;border-left:3px solid #28a745;border-radius:2px;">
<strong>📅 Item-Abo tägliche Artikel-Lieferung</strong><br><br>
Spieler bezahlen den <strong>Artikelpreis einmalig beim Kauf</strong>.<br>
Danach bekommen sie jeden Tag automatisch die eingestellte Anzahl des Items ingame.<br>
Die Lieferung erfolgt über einen Pending-Order das Spigot-Plugin liefert beim nächsten Login aus.<br><br>
<strong>Artikelpreis</strong> = einmaliger Kaufpreis &nbsp;|&nbsp; Laufzeit konfigurierbar<br>
Gespeicherte Item-ID: <code>item_abo_{minecraft_id}_{menge}_{tage}</code>
</p>
</div>
<div id="wis_item_gift_card" style="display:none;">
<table style="border-collapse:collapse;width:100%;">
<tr>
<td style="padding:4px 10px 4px 0;width:200px;"><label><strong>Min. Betrag (<?php echo esc_html(get_option('wis_currency_name','Coins')); ?>)</strong></label></td>
<td>
<input type="number" id="gift_card_min" name="gift_card_min"
value="<?php echo esc_attr($item && preg_match('/^gift_card_(\d+)_(\d+)$/', $item->item_id, $gcm) ? $gcm[1] : 100); ?>"
min="1" style="width:100px;">
<p class="description">Mindestbetrag den der Käufer eingeben kann.</p>
</td>
</tr>
<tr>
<td style="padding:4px 10px 4px 0;"><label><strong>Max. Betrag (<?php echo esc_html(get_option('wis_currency_name','Coins')); ?>)</strong></label></td>
<td>
<input type="number" id="gift_card_max" name="gift_card_max"
value="<?php echo esc_attr($item && preg_match('/^gift_card_(\d+)_(\d+)$/', $item->item_id, $gcm) ? $gcm[2] : 5000); ?>"
min="1" style="width:100px;">
<p class="description">Höchstbetrag den der Käufer eingeben kann.</p>
</td>
</tr>
</table>
<p class="description" style="margin-top:10px;padding:10px;background:#fff3e0;border-left:3px solid #ff9800;border-radius:2px;">
<strong>🎁 Gutschein-Karte freier Betrag</strong><br><br>
Der Käufer wählt beim Kauf selbst einen Betrag (innerhalb Min/Max).<br>
Er erhält sofort einen <strong>einzigartigen Gutschein-Code</strong> den er beim nächsten Kauf einlösen kann.<br>
Eine Auszahlung ist nicht möglich.<br>
Der Artikelpreis wird beim Speichern automatisch auf den Mindestwert gesetzt.<br><br>
Gespeicherte Item-ID: <code>gift_card_{min}_{max}</code>
</p>
</div>
<script>
function wisToggleItemType(val) {
document.getElementById('wis_item_minecraft').style.display = (val === 'minecraft') ? '' : 'none';
document.getElementById('wis_item_fly').style.display = (val === 'fly') ? '' : 'none';
document.getElementById('wis_item_rank').style.display = (val === 'rank') ? '' : 'none';
document.getElementById('wis_item_fly_abo').style.display = (val === 'fly_abo') ? '' : 'none';
document.getElementById('wis_item_plot_slots').style.display = (val === 'plot_slots') ? '' : 'none';
document.getElementById('wis_item_plot_abo').style.display = (val === 'plot_abo') ? '' : 'none';
document.getElementById('wis_item_item_abo').style.display = (val === 'item_abo') ? '' : 'none';
document.getElementById('wis_item_custom_cmd').style.display = (val === 'custom_cmd') ? '' : 'none';
document.getElementById('wis_item_gift_card').style.display = (val === 'gift_card') ? '' : 'none';
document.getElementById('item_id').required = (val === 'minecraft');
document.getElementById('custom_cmd_id').required = (val === 'custom_cmd');
document.getElementById('abo_item_id').required = (val === 'item_abo');
}
wisToggleItemType(document.getElementById('item_type').value);
</script>
</td>
</tr>
<tr>
<th><label for="custom_image_url">Bild-URL (optional)</label></th>
<td>
<input type="url" id="custom_image_url" name="custom_image_url"
value="<?php echo ($item && !empty($item->custom_image_url)) ? esc_attr($item->custom_image_url) : ''; ?>"
class="large-text" placeholder="https://example.com/mein-fly-bild.png"
oninput="wisPreviewImage(this.value)">
<p class="description">
Eigene Bild-URL überschreibt die automatische Minecraft-Item-ID-URL.<br>
Ideal für Fly-Gutscheine, VIP-Pakete etc. Leer lassen = Standard.
</p>
<div id="wis_img_preview" style="margin-top:8px; <?php echo ($item && !empty($item->custom_image_url)) ? '' : 'display:none;'; ?>">
<img id="wis_img_preview_img"
src="<?php echo ($item && !empty($item->custom_image_url)) ? esc_url($item->custom_image_url) : ''; ?>"
style="max-height:80px; border-radius:6px; border:1px solid #ddd; padding:4px; background:#2d2d2d;"
alt="Vorschau"
onerror="document.getElementById('wis_img_preview').style.display='none';">
<span style="margin-left:8px; color:#666; font-size:12px;">Vorschau</span>
</div>
<script>
function wisPreviewImage(url) {
var box = document.getElementById('wis_img_preview');
var img = document.getElementById('wis_img_preview_img');
if (url) {
img.src = url;
box.style.display = '';
} else {
box.style.display = 'none';
}
}
</script>
</td>
</tr>
<tr>
<th><label for="description">Beschreibung</label></th>
<td><textarea id="description" name="description" rows="3" class="large-text"><?php echo $item ? esc_textarea($item->description) : ''; ?></textarea></td>
</tr>
<tr>
<th><label for="price">Preis (<?php echo esc_html($currency); ?>) *</label></th>
<td><input type="number" id="price" name="price" value="<?php echo $item ? esc_attr($item->price) : '0'; ?>" min="0" required></td>
</tr>
<tr>
<th><label for="offer_price">Angebotspreis</label></th>
<td>
<input type="number" id="offer_price" name="offer_price" value="<?php echo $item ? esc_attr($item->offer_price) : '0'; ?>" min="0">
<p class="description">Optional: Wenn gesetzt, wird der normale Preis durchgestrichen</p>
</td>
</tr>
<tr>
<th><label for="sell_enabled">Ankauf aktivieren</label></th>
<td>
<label>
<input type="checkbox" id="sell_enabled" name="sell_enabled" value="1"
<?php echo ($item && !empty($item->sell_enabled)) ? 'checked' : ''; ?>
onchange="wisToggleSell(this.checked)">
Spieler können dieses Item an den Shop verkaufen
</label>
</td>
</tr>
<tr id="wis_sell_row" <?php echo ($item && !empty($item->sell_enabled)) ? '' : 'style="display:none"'; ?>>
<th><label for="sell_price_mode">Ankaufspreis</label></th>
<td>
<select id="sell_price_mode" name="sell_price_mode" onchange="wisUpdateSellPreview()">
<option value="percent" <?php echo ($item && $item->sell_price_mode === 'percent') ? 'selected' : ''; ?>>% vom Verkaufspreis</option>
<option value="minus" <?php echo ($item && $item->sell_price_mode === 'minus') ? 'selected' : ''; ?>>Verkaufspreis minus fixer Betrag</option>
<option value="fixed" <?php echo ($item && $item->sell_price_mode === 'fixed') ? 'selected' : ''; ?>>Fixer Preis</option>
</select>
&nbsp;
<input type="number" id="sell_price_value" name="sell_price_value" min="0"
value="<?php echo $item ? esc_attr($item->sell_price_value ?? 80) : '80'; ?>"
style="width:80px" oninput="wisUpdateSellPreview()">
<span id="wis_sell_preview" style="margin-left:8px;color:#0073aa;"></span>
<p class="description">
Beispiele: 80&nbsp;%&nbsp;→&nbsp;80&nbsp;% des VK-Preises &nbsp;|&nbsp;
Modus «minus 10»&nbsp;→&nbsp;VK-Preis&nbsp;&nbsp;10&nbsp;|&nbsp;
Fixer Preis 15&nbsp;→&nbsp;immer&nbsp;15&nbsp;<?php echo esc_html($currency); ?>
</p>
<script>
function wisToggleSell(on) {
document.getElementById('wis_sell_row').style.display = on ? '' : 'none';
document.getElementById('wis_daily_sell_row').style.display = on ? '' : 'none';
if (on) wisUpdateSellPreview();
}
function wisUpdateSellPreview() {
var vk = parseFloat(document.getElementById('price').value) || 0;
var mode = document.getElementById('sell_price_mode').value;
var val = parseFloat(document.getElementById('sell_price_value').value) || 0;
var price = 0;
if (mode === 'percent') price = Math.max(0, Math.round(vk * val) / 100);
else if (mode === 'minus') price = Math.max(0, vk - val);
else price = Math.max(0, val);
document.getElementById('wis_sell_preview').textContent =
'→ Ankaufspreis: ' + price.toFixed(2) + ' <?php echo esc_js($currency); ?>';
}
document.getElementById('price').addEventListener('input', wisUpdateSellPreview);
wisUpdateSellPreview();
</script>
</td>
</tr>
<tr id="wis_daily_sell_row" <?php echo ($item && !empty($item->sell_enabled)) ? '' : 'style="display:none"'; ?>>
<th><label for="daily_sell_limit">Tageslimit (Ankauf)</label></th>
<td>
<input type="number" id="daily_sell_limit" name="daily_sell_limit" min="0"
value="<?php echo $item ? esc_attr($item->daily_sell_limit ?? 0) : '0'; ?>"
style="width:100px">
<p class="description">Max. Menge die ein Spieler pro Tag verkaufen kann. <strong>0 = kein Limit.</strong></p>
</td>
</tr>
<tr>
<th>Markierungen</th>
<td>
<label>
<input type="checkbox" name="is_offer" value="1" <?php echo ($item && $item->is_offer) ? 'checked' : ''; ?>>
Als Angebot markieren
</label>
</td>
</tr>
<tr>
<th>Server</th>
<td>
<?php if (empty($servers)): ?>
<p>Keine Server vorhanden. <a href="<?php echo admin_url('admin.php?page=wis_servers'); ?>">Server erstellen</a></p>
<?php else: ?>
<?php foreach ($servers as $server): ?>
<label style="display:block; margin:5px 0;">
<input type="checkbox" name="servers[]" value="<?php echo esc_attr($server->slug); ?>" <?php echo in_array($server->slug, $item_servers ?: []) ? 'checked' : ''; ?>>
<?php echo esc_html($server->name); ?>
</label>
<?php endforeach; ?>
<?php endif; ?>
</td>
</tr>
<tr>
<th>Kategorien</th>
<td>
<?php if (empty($categories)): ?>
<p>Keine Kategorien vorhanden.</p>
<?php else: ?>
<?php foreach ($categories as $cat): ?>
<label style="display:block; margin:5px 0;">
<input type="checkbox" name="categories[]" value="<?php echo esc_attr($cat->slug); ?>" <?php echo in_array($cat->slug, $item_cats ?: []) ? 'checked' : ''; ?>>
<?php echo esc_html($cat->name); ?>
</label>
<?php endforeach; ?>
<p class="description">Bei neuem Item werden Kategorien automatisch basierend auf der Item-ID gesetzt</p>
<?php endif; ?>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="wis_save_item" class="button button-primary" value="Speichern">
</p>
</form>
</div>
<?php
return;
}
// Liste mit Suche + Pagination
$categories = WIS_DB::get_categories();
$current_category = isset($_GET['wis_category']) ? sanitize_text_field($_GET['wis_category']) : '';
$search_query = isset($_GET['wis_search']) ? sanitize_text_field($_GET['wis_search']) : '';
$hide_drafts = isset($_GET['hide_drafts']) && $_GET['hide_drafts'] === '1';
$per_page = 24;
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$fetch_args = [];
if ($hide_drafts) $fetch_args['status'] = 'publish';
if (!empty($current_category)) $fetch_args['category_slug'] = $current_category;
if (!empty($search_query)) $fetch_args['search'] = $search_query;
$total_items = WIS_DB::count_items($fetch_args);
$total_pages = max(1, (int) ceil($total_items / $per_page));
$current_page = min($current_page, $total_pages);
$offset = ($current_page - 1) * $per_page;
global $wpdb;
$table_items = $wpdb->prefix . 'wis_items';
// Admin-Liste: alle Items anzeigen (publish + draft), Query ohne doppeltes prepare()
$where_parts = [];
$where_vals = [];
if ($hide_drafts) {
$where_parts[] = "status = 'publish'";
}
if (!empty($current_category)) {
$where_parts[] = "categories LIKE %s";
$where_vals[] = '%"' . $wpdb->esc_like($current_category) . '"%';
}
if (!empty($search_query)) {
$like = '%' . $wpdb->esc_like($search_query) . '%';
$where_parts[] = "(name LIKE %s OR item_id LIKE %s)";
$where_vals[] = $like;
$where_vals[] = $like;
}
$where_sql = !empty($where_parts) ? 'WHERE ' . implode(' AND ', $where_parts) : '';
$where_vals[] = $per_page;
$where_vals[] = $offset;
$items = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $table_items $where_sql ORDER BY name ASC LIMIT %d OFFSET %d",
$where_vals
)
);
$currency = get_option('wis_currency_name', 'Coins');
$base_url = admin_url('admin.php?page=wis_items');
if (!empty($current_category)) $base_url .= '&wis_category=' . urlencode($current_category);
if (!empty($search_query)) $base_url .= '&wis_search=' . urlencode($search_query);
if ($hide_drafts) $base_url .= '&hide_drafts=1';
?>
<div class="wrap">
<h1>Items
<a href="<?php echo admin_url('admin.php?page=wis_items&add=1'); ?>" class="page-title-action">Neu erstellen</a>
</h1>
<div style="margin:10px 0 15px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
<form method="get" style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<input type="hidden" name="page" value="wis_items">
<?php if (!empty($current_category)): ?>
<input type="hidden" name="wis_category" value="<?php echo esc_attr($current_category); ?>">
<?php endif; ?>
<input
type="search"
name="wis_search"
value="<?php echo esc_attr($search_query); ?>"
placeholder="🔍 Item suchen (Name oder ID)…"
class="regular-text"
style="min-width:280px; padding:6px 10px;">
<?php if ($hide_drafts): ?><input type="hidden" name="hide_drafts" value="1"><?php endif; ?>
<input type="submit" class="button" value="Suchen">
<?php if (!empty($search_query)): ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=wis_items' . (!empty($current_category) ? '&wis_category=' . urlencode($current_category) : '') . ($hide_drafts ? '&hide_drafts=1' : ''))); ?>" class="button">✕ Zurücksetzen</a>
<?php endif; ?>
</form>
<a href="<?php echo esc_url(add_query_arg([
'page' => 'wis_items',
'hide_drafts' => $hide_drafts ? '0' : '1',
'wis_category'=> $current_category ?: null,
'wis_search' => $search_query ?: null,
], admin_url('admin.php'))); ?>"
class="button"
style="display:flex;align-items:center;gap:6px;<?php echo $hide_drafts ? 'background:#2271b1;color:#fff;border-color:#2271b1;' : ''; ?>">
<?php echo $hide_drafts ? '👁 Entwürfe einblenden' : '🙈 Entwürfe ausblenden'; ?>
</a>
<span style="color:#666; font-size:13px;">
<?php echo $total_items; ?> Item(s) gefunden
<?php if (!empty($search_query)): ?>
Suche: <strong><?php echo esc_html($search_query); ?></strong>
<?php endif; ?>
</span>
</div>
<nav class="nav-tab-wrapper">
<?php
$hd_param = $hide_drafts ? '&hide_drafts=1' : '';
$all_count = WIS_DB::count_items(array_merge(
$hide_drafts ? ['status' => 'publish'] : [],
!empty($search_query) ? ['search' => $search_query] : []
));
$root_cats_tab = array_values(array_filter($categories, fn($c) => $c->parent_id == 0));
$sub_idx_tab = [];
foreach ($categories as $c) {
if ($c->parent_id != 0) $sub_idx_tab[$c->parent_id][] = $c;
}
// Aktive Hauptkategorie bestimmen (direkt oder via Unterkategorie)
$active_root_slug = '';
foreach ($root_cats_tab as $rc) {
if ($current_category === $rc->slug) { $active_root_slug = $rc->slug; break; }
if (!empty($sub_idx_tab[$rc->id])) {
foreach ($sub_idx_tab[$rc->id] as $sc) {
if ($current_category === $sc->slug) { $active_root_slug = $rc->slug; break 2; }
}
}
}
?>
<a href="<?php echo admin_url('admin.php?page=wis_items' . (!empty($search_query) ? '&wis_search=' . urlencode($search_query) : '') . $hd_param); ?>"
class="nav-tab <?php echo $current_category === '' ? 'nav-tab-active' : ''; ?>">
Alle <span style="background:<?php echo $current_category===''?'#fff':'#e2e8f0';?>;color:#555;border-radius:10px;padding:1px 7px;font-size:11px;margin-left:3px;"><?php echo $all_count; ?></span>
</a>
<?php foreach ($root_cats_tab as $cat):
$is_active_root = ($active_root_slug === $cat->slug);
$cat_count = WIS_DB::count_items(array_merge(
$hide_drafts ? ['status' => 'publish'] : [],
['category_slug' => $cat->slug],
!empty($search_query) ? ['search' => $search_query] : []
));
?>
<a href="<?php echo admin_url('admin.php?page=wis_items&wis_category=' . $cat->slug . (!empty($search_query) ? '&wis_search=' . urlencode($search_query) : '') . $hd_param); ?>"
class="nav-tab <?php echo $current_category === $cat->slug ? 'nav-tab-active' : ($is_active_root ? 'nav-tab-active' : ''); ?>">
<?php echo esc_html($cat->name); ?>
<span style="background:<?php echo $is_active_root?'#fff':'#e2e8f0';?>;color:#555;border-radius:10px;padding:1px 7px;font-size:11px;margin-left:3px;"><?php echo $cat_count; ?></span>
</a>
<?php endforeach; ?>
</nav>
<?php
// Unterkategorie-Leiste nur wenn eine Hauptkat mit Unterkats aktiv ist
$active_root_obj = null;
foreach ($root_cats_tab as $rc) {
if ($active_root_slug === $rc->slug) { $active_root_obj = $rc; break; }
}
if ($active_root_obj && !empty($sub_idx_tab[$active_root_obj->id])): ?>
<div style="background:#f0f4ff;border:1px solid #c5d0f0;border-top:none;border-radius:0 0 4px 4px;padding:6px 10px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;">
<span style="font-size:12px;color:#666;margin-right:2px;">↳</span>
<a href="<?php echo admin_url('admin.php?page=wis_items&wis_category=' . $active_root_obj->slug . $hd_param); ?>"
style="font-size:12px;padding:3px 10px;border-radius:12px;text-decoration:none;
background:<?php echo $current_category===$active_root_obj->slug?'#667eea':'#fff';?>;
color:<?php echo $current_category===$active_root_obj->slug?'#fff':'#444';?>;
border:1px solid <?php echo $current_category===$active_root_obj->slug?'#667eea':'#ccc';?>;">
Alle
</a>
<?php foreach ($sub_idx_tab[$active_root_obj->id] as $sc):
$sc_count = WIS_DB::count_items(array_merge(
$hide_drafts ? ['status' => 'publish'] : [],
['category_slug' => $sc->slug],
!empty($search_query) ? ['search' => $search_query] : []
));
?>
<a href="<?php echo admin_url('admin.php?page=wis_items&wis_category=' . $sc->slug . $hd_param); ?>"
style="font-size:12px;padding:3px 10px;border-radius:12px;text-decoration:none;
background:<?php echo $current_category===$sc->slug?'#667eea':'#fff';?>;
color:<?php echo $current_category===$sc->slug?'#fff':'#444';?>;
border:1px solid <?php echo $current_category===$sc->slug?'#667eea':'#ccc';?>;">
<?php echo esc_html($sc->name); ?>
<span style="font-size:11px;opacity:.8;">(<?php echo $sc_count; ?>)</span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form method="post">
<div class="tablenav top">
<div class="alignleft actions bulkactions">
<label for="bulk-action-selector-top" class="screen-reader-text">Massenaktionen wählen</label>
<select name="wis_bulk_action" id="bulk-action-selector-top">
<option value="">Massenaktionen</option>
<option value="price">Preis ändern</option>
<option value="offer">Angebot ändern</option>
<option value="server">Server zuweisen</option>
<option value="category">Kategorie zuweisen</option>
<option value="status">Aktivieren/Deaktivieren</option>
<option value="sell">Ankauf konfigurieren</option>
</select>
<input type="submit" name="wis_bulk_apply" class="button action" value="Anwenden">
</div>
<div class="tablenav-pages" style="float:right; margin-top:4px;">
<?php if ($total_pages > 1): ?>
<span class="displaying-num"><?php echo $total_items; ?> Einträge</span>
<span class="pagination-links">
<?php if ($current_page > 1): ?>
<a class="button" href="<?php echo esc_url($base_url . '&paged=1'); ?>">«</a>
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . ($current_page - 1)); ?>"></a>
<?php endif; ?>
<span class="paging-input" style="margin:0 6px;">
Seite <strong><?php echo $current_page; ?></strong> von <strong><?php echo $total_pages; ?></strong>
</span>
<?php if ($current_page < $total_pages): ?>
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . ($current_page + 1)); ?>"></a>
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . $total_pages); ?>">»</a>
<?php endif; ?>
</span>
<?php else: ?>
<span class="displaying-num"><?php echo $total_items; ?> Einträge</span>
<?php endif; ?>
</div>
<br class="clear">
</div>
<table class="widefat fixed striped">
<thead>
<tr>
<th style="width:40px;" class="manage-column column-cb check-column">
<input type="checkbox" id="cb-select-all-1">
</th>
<th>ID</th>
<th>Name</th>
<th>Item ID</th>
<th>Preis</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="7" style="text-align:center; padding:40px;">
<?php echo !empty($search_query) ? 'Keine Items für diese Suche gefunden.' : 'Keine Items in dieser Kategorie gefunden.'; ?>
</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<th class="check-column">
<input type="checkbox" name="item_ids[]" id="cb-select-<?php echo $item->id; ?>" value="<?php echo $item->id; ?>">
</th>
<td><?php echo esc_html($item->id); ?></td>
<td><strong><?php echo esc_html($item->name); ?></strong></td>
<td>
<?php
if (preg_match('/^rank_([^_]+)_([a-zA-Z0-9_\-]+)_([a-zA-Z0-9_\-]+)_(\d+)$/', $item->item_id, $rm2)) {
$rd = intval($rm2[4]);
echo '<span title="' . esc_attr($item->item_id) . '">👑 LP: <code>' . esc_html($rm2[2]) . '</code> &mdash; ' . ($rd === 0 ? '<em>dauerhaft</em>' : esc_html($rd) . ' Tage') . '</span>';
} elseif (preg_match('/^rank_(.+)_(\d+)$/', $item->item_id, $rm2)) {
$rd = intval($rm2[2]);
echo '<span title="' . esc_attr($item->item_id) . '">👑 <code>' . esc_html($rm2[1]) . '</code> &mdash; ' . ($rd === 0 ? '<em>dauerhaft</em>' : esc_html($rd) . ' Tage') . '</span>';
} elseif (preg_match('/^custom_cmd_(.+)$/', $item->item_id, $cc2)) {
echo '<span title="' . esc_attr($item->item_id) . '">⚙️ Custom: <code>' . esc_html($cc2[1]) . '</code></span>';
} elseif ($item->item_id === 'fly_abo' || preg_match('/^fly_abo_/', $item->item_id)) {
echo '<span title="' . esc_attr($item->item_id) . '">✈ Fly-Abo &mdash; monatlich</span>';
} elseif (preg_match('/^plot_slots_(\d+)$/', $item->item_id, $plt_m)) {
echo '<span title="' . esc_attr($item->item_id) . '">📦 Plot-Slots &mdash; +' . esc_html($plt_m[1]) . ' permanent</span>';
} elseif (preg_match('/^plot_abo_(\d+)$/', $item->item_id, $plt_m)) {
echo '<span title="' . esc_attr($item->item_id) . '">📦 Plot-Abo &mdash; +' . esc_html($plt_m[1]) . ' monatlich</span>';
} else {
echo '<code>' . esc_html($item->item_id) . '</code>';
}
?>
</td>
<td><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></td>
<td>
<?php if ($item->status === 'publish'): ?>
<span style="color:green;">✅ Aktiv</span>
<?php else: ?>
<span style="color:orange;">📝 Entwurf</span>
<?php endif; ?>
</td>
<td>
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_items&edit=' . $item->id), 'wis_item_action', '_wpnonce'); ?>" class="button button-small">Bearbeiten</a>
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_items&action=delete&id=' . $item->id), 'wis_item_action', '_wpnonce'); ?>" class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php if ($total_pages > 1): ?>
<div class="tablenav bottom">
<div class="tablenav-pages" style="float:right; margin-top:8px;">
<span class="displaying-num"><?php echo $total_items; ?> Einträge</span>
<span class="pagination-links">
<?php if ($current_page > 1): ?>
<a class="button" href="<?php echo esc_url($base_url . '&paged=1'); ?>">«</a>
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . ($current_page - 1)); ?>"></a>
<?php endif; ?>
<span class="paging-input" style="margin:0 6px;">
Seite <strong><?php echo $current_page; ?></strong> von <strong><?php echo $total_pages; ?></strong>
</span>
<?php if ($current_page < $total_pages): ?>
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . ($current_page + 1)); ?>"></a>
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . $total_pages); ?>">»</a>
<?php endif; ?>
</span>
</div>
<br class="clear">
</div>
<?php endif; ?>
</form>
</div>
<script>
(function() {
const mainCb = document.getElementById('cb-select-all-1');
if(mainCb) {
mainCb.addEventListener('change', function(e) {
const cbs = document.querySelectorAll('input[name="item_ids[]"]');
cbs.forEach(cb => cb.checked = e.target.checked);
});
}
})();
</script>
<?php
}
public static function page_servers() {
if (isset($_POST['wis_add_server'])) {
check_admin_referer('wis_server_form');
WIS_DB::insert_server($_POST['slug'], $_POST['name']);
echo '<div class="updated"><p>✅ Server erstellt!</p></div>';
}
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
check_admin_referer('wis_server_action', '_wpnonce');
WIS_DB::delete_server(intval($_GET['id']));
echo '<div class="updated"><p>✅ Server gelöscht!</p></div>';
}
$servers = WIS_DB::get_servers();
?>
<div class="wrap">
<h1>Server</h1>
<div class="card" style="max-width:600px; padding:20px; margin-top:20px;">
<h2>Neuen Server erstellen</h2>
<form method="post">
<?php wp_nonce_field('wis_server_form'); ?>
<table class="form-table">
<tr>
<th><label for="name">Name *</label></th>
<td><input type="text" id="name" name="name" class="regular-text" required></td>
</tr>
<tr>
<th><label for="slug">Slug *</label></th>
<td>
<input type="text" id="slug" name="slug" class="regular-text" placeholder="survival" required>
<p class="description">Kleinbuchstaben ohne Leerzeichen</p>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="wis_add_server" class="button button-primary" value="Server erstellen">
</p>
</form>
</div>
<h2 style="margin-top:40px;">Vorhandene Server</h2>
<table class="widefat fixed striped">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Slug</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php if (empty($servers)): ?>
<tr><td colspan="4" style="text-align:center; padding:40px;">Noch keine Server vorhanden.</td></tr>
<?php else: ?>
<?php foreach ($servers as $server): ?>
<tr>
<td><?php echo esc_html($server->id); ?></td>
<td><strong><?php echo esc_html($server->name); ?></strong></td>
<td><code><?php echo esc_html($server->slug); ?></code></td>
<td>
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_servers&action=delete&id=' . $server->id), 'wis_server_action', '_wpnonce'); ?>" class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
public static function page_categories() {
global $wpdb;
// ── Kategorie bearbeiten speichern ──
if (isset($_POST['wis_edit_category'])) {
check_admin_referer('wis_edit_category_form');
$edit_id = intval($_POST['edit_cat_id']);
$new_name = sanitize_text_field($_POST['name']);
$new_parent = intval($_POST['parent_id'] ?? 0);
if ($edit_id && $new_name) {
$new_slug = sanitize_title($new_name);
// Slug-Kollision vermeiden
$existing_slug = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}wis_categories WHERE slug = %s AND id != %d LIMIT 1",
$new_slug, $edit_id
));
if ($existing_slug) $new_slug .= '-' . $edit_id;
$wpdb->update($wpdb->prefix . 'wis_categories', [
'name' => $new_name,
'slug' => $new_slug,
'parent_id' => $new_parent,
], ['id' => $edit_id]);
echo '<div class="updated"><p>✅ Kategorie aktualisiert!</p></div>';
}
}
if (isset($_POST['wis_add_category'])) {
check_admin_referer('wis_category_form');
$parent_id = intval($_POST['parent_id'] ?? 0);
WIS_DB::insert_category($_POST['name'], $parent_id);
echo '<div class="updated"><p>✅ Kategorie erstellt!</p></div>';
}
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
check_admin_referer('wis_category_action', '_wpnonce');
$del_id = intval($_GET['id']);
$wpdb->update($wpdb->prefix . 'wis_categories', ['parent_id' => 0], ['parent_id' => $del_id]);
WIS_DB::delete_category($del_id);
echo '<div class="updated"><p>✅ Kategorie gelöscht!</p></div>';
}
$categories = WIS_DB::get_categories();
$root_cats = array_values(array_filter($categories, fn($c) => $c->parent_id == 0));
$sub_index = [];
foreach ($categories as $c) {
if ($c->parent_id != 0) $sub_index[$c->parent_id][] = $c;
}
// Edit-Modus: Inline-Formular für eine Kategorie
$edit_id = isset($_GET['edit_cat']) ? intval($_GET['edit_cat']) : 0;
$edit_cat = $edit_id ? $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_categories WHERE id = %d", $edit_id
)) : null;
?>
<div class="wrap">
<h1>Kategorien &amp; Unterkategorien</h1>
<?php if ($edit_cat): ?>
<!-- ── Edit-Formular ── -->
<div class="card" style="max-width:600px;padding:20px;margin-top:20px;border-left:4px solid #667eea;">
<h2>✏️ Kategorie bearbeiten</h2>
<form method="post">
<?php wp_nonce_field('wis_edit_category_form'); ?>
<input type="hidden" name="edit_cat_id" value="<?php echo esc_attr($edit_cat->id); ?>">
<table class="form-table">
<tr>
<th><label for="edit_name">Name *</label></th>
<td><input type="text" id="edit_name" name="name" value="<?php echo esc_attr($edit_cat->name); ?>" class="regular-text" required></td>
</tr>
<tr>
<th><label for="edit_parent">Übergeordnete Kategorie</label></th>
<td>
<select id="edit_parent" name="parent_id" class="regular-text">
<option value="0" <?php selected($edit_cat->parent_id, 0); ?>>— Hauptkategorie —</option>
<?php foreach ($root_cats as $rc):
if ($rc->id === $edit_cat->id) continue; // sich selbst nicht als Parent ?>
<option value="<?php echo esc_attr($rc->id); ?>" <?php selected($edit_cat->parent_id, $rc->id); ?>>
<?php echo esc_html($rc->name); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th>Aktueller Slug</th>
<td><code><?php echo esc_html($edit_cat->slug); ?></code> <span class="description">(wird beim Speichern automatisch aktualisiert)</span></td>
</tr>
</table>
<p class="submit">
<input type="submit" name="wis_edit_category" class="button button-primary" value="Speichern">
<a href="<?php echo admin_url('admin.php?page=wis_categories'); ?>" class="button">Abbrechen</a>
</p>
</form>
</div>
<?php else: ?>
<!-- ── Neue Kategorie ── -->
<div class="card" style="max-width:600px;padding:20px;margin-top:20px;">
<h2>Neue Kategorie erstellen</h2>
<form method="post">
<?php wp_nonce_field('wis_category_form'); ?>
<table class="form-table">
<tr>
<th><label for="cat_name">Name *</label></th>
<td><input type="text" id="cat_name" name="name" class="regular-text" required></td>
</tr>
<tr>
<th><label for="parent_id">Übergeordnete Kategorie</label></th>
<td>
<select id="parent_id" name="parent_id" class="regular-text">
<option value="0">— Hauptkategorie —</option>
<?php foreach ($root_cats as $rc): ?>
<option value="<?php echo esc_attr($rc->id); ?>"><?php echo esc_html($rc->name); ?></option>
<?php endforeach; ?>
</select>
<p class="description">Leer lassen = Hauptkategorie. Max. 2 Ebenen.</p>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="wis_add_category" class="button button-primary" value="Kategorie erstellen">
</p>
</form>
</div>
<?php endif; ?>
<!-- Drag & Drop Sortierung -->
<h2 style="margin-top:40px;">Reihenfolge &amp; Struktur</h2>
<p class="description" style="margin-bottom:15px;">
Ziehe Kategorien per ☰ in die gewünschte Reihenfolge. Klicke ✏️ um eine Kategorie umzubenennen oder die Ebene zu ändern.
</p>
<div id="wis-cat-sortable-wrap" style="max-width:700px;">
<?php foreach ($root_cats as $rc): ?>
<div class="wis-cat-group" data-id="<?php echo $rc->id; ?>" style="margin-bottom:12px;">
<!-- Hauptkategorie-Zeile -->
<div class="wis-cat-row wis-cat-root"
data-id="<?php echo $rc->id; ?>" data-parent="0"
style="display:flex;align-items:center;gap:10px;background:#f0f4ff;border:1px solid #c5d0f0;border-radius:6px;padding:10px 14px;">
<span style="font-size:18px;color:#999;cursor:grab;" class="wis-handle">☰</span>
<strong style="flex:1;">📁 <?php echo esc_html($rc->name); ?></strong>
<code style="color:#888;font-size:11px;"><?php echo esc_html($rc->slug); ?></code>
<?php if (!empty($sub_index[$rc->id])): ?>
<span style="font-size:11px;color:#667eea;">(<?php echo count($sub_index[$rc->id]); ?> Unterkats)</span>
<?php endif; ?>
<a href="<?php echo admin_url('admin.php?page=wis_categories&edit_cat=' . $rc->id); ?>"
style="color:#0073aa;font-size:12px;text-decoration:none;" title="Bearbeiten">✏️</a>
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_categories&action=delete&id=' . $rc->id), 'wis_category_action', '_wpnonce'); ?>"
onclick="return confirm('Wirklich löschen? Unterkategorien werden zu Hauptkategorien.');"
style="color:#dc3545;font-size:12px;text-decoration:none;" title="Löschen">🗑</a>
</div>
<!-- Unterkategorien -->
<?php if (!empty($sub_index[$rc->id])): ?>
<ul class="wis-subcat-sortable" data-parent="<?php echo $rc->id; ?>"
style="list-style:none;margin:4px 0 0 30px;padding:0;">
<?php foreach ($sub_index[$rc->id] as $sc): ?>
<li class="wis-cat-row wis-cat-sub"
data-id="<?php echo $sc->id; ?>" data-parent="<?php echo $rc->id; ?>"
style="display:flex;align-items:center;gap:10px;background:#fff;border:1px solid #e0e0e0;border-radius:5px;padding:8px 12px;margin-bottom:4px;">
<span style="font-size:16px;color:#bbb;cursor:grab;" class="wis-handle">☰</span>
<span style="flex:1;">↳ <?php echo esc_html($sc->name); ?></span>
<code style="color:#888;font-size:11px;"><?php echo esc_html($sc->slug); ?></code>
<a href="<?php echo admin_url('admin.php?page=wis_categories&edit_cat=' . $sc->id); ?>"
style="color:#0073aa;font-size:12px;text-decoration:none;" title="Bearbeiten">✏️</a>
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_categories&action=delete&id=' . $sc->id), 'wis_category_action', '_wpnonce'); ?>"
onclick="return confirm('Wirklich löschen?');"
style="color:#dc3545;font-size:12px;text-decoration:none;" title="Löschen">🗑</a>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (empty($root_cats)): ?>
<p style="color:#999;padding:20px 0;">Noch keine Kategorien vorhanden.</p>
<?php endif; ?>
</div>
<button id="wis-save-order-btn" class="button button-primary" style="margin-top:20px;display:<?php echo empty($root_cats)?'none':'inline-flex'; ?>;align-items:center;gap:8px;">
💾 Reihenfolge speichern
</button>
<span id="wis-save-order-msg" style="margin-left:12px;font-size:13px;color:green;display:none;"></span>
</div>
<!-- Sortable JS -->
<script>
jQuery(function($) {
var $wrap = $('#wis-cat-sortable-wrap');
$wrap.sortable({
items: '> .wis-cat-group',
handle: '.wis-cat-root .wis-handle',
axis: 'y',
placeholder: 'wis-sortable-placeholder',
tolerance: 'pointer',
});
$wrap.find('.wis-subcat-sortable').each(function() {
$(this).sortable({
items: '> li',
handle: '.wis-handle',
axis: 'y',
placeholder: 'wis-sortable-placeholder',
tolerance: 'pointer',
});
});
$('#wis-save-order-btn').on('click', function() {
var $btn = $(this);
var $msg = $('#wis-save-order-msg');
$btn.prop('disabled', true).text('⏳ Speichere…');
$msg.hide();
var order = [];
$wrap.find('> .wis-cat-group').each(function() {
var rootId = parseInt($(this).data('id'));
order.push({ id: rootId, parent_id: 0 });
$(this).find('.wis-subcat-sortable > li').each(function() {
order.push({ id: parseInt($(this).data('id')), parent_id: rootId });
});
});
$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'wis_save_cat_order',
nonce: '<?php echo wp_create_nonce('wis_cat_order_nonce'); ?>',
order: order,
},
success: function(res) {
if (res.success) {
$msg.text('✅ Gespeichert!').css('color','green').show();
setTimeout(function(){ $msg.fadeOut(); }, 3000);
} else {
$msg.text('❌ Fehler: ' + (res.data || '')).css('color','red').show();
}
},
error: function() {
$msg.text('❌ Verbindungsfehler').css('color','red').show();
},
complete: function() {
$btn.prop('disabled', false).text('💾 Reihenfolge speichern');
}
});
});
});
</script>
<style>
.wis-sortable-placeholder { height:44px;background:#e8f0fe;border:2px dashed #667eea;border-radius:6px;margin-bottom:4px;list-style:none; }
.wis-cat-root:hover, .wis-cat-sub:hover { border-color:#667eea !important; }
.wis-cat-row { user-select:none; }
</style>
<?php
}
public static function page_coupons() {
// ── CSV-Export ────────────────────────────────────────────────────────
if (isset($_GET['action']) && $_GET['action'] === 'export_bulk' && isset($_GET['bulk_id'])) {
check_admin_referer('wis_coupon_action', '_wpnonce');
global $wpdb;
$bulk_id = sanitize_text_field($_GET['bulk_id']);
$table = $wpdb->prefix . 'wis_coupons';
$codes = $wpdb->get_results($wpdb->prepare(
"SELECT code, value, type, usage_limit, expiry, min_order_value FROM $table WHERE bulk_id = %s ORDER BY id ASC",
$bulk_id
));
if ($codes) {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="gutscheine-' . $bulk_id . '.csv"');
$out = fopen('php://output', 'w');
fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); // UTF-8 BOM
fputcsv($out, ['Code', 'Rabatt', 'Typ', 'Max. Einlösungen', 'Gültig bis', 'Mindestbestellwert'], ';');
foreach ($codes as $c) {
fputcsv($out, [
$c->code,
$c->value,
$c->type === 'percent' ? 'Prozent' : 'Fest',
$c->usage_limit,
$c->expiry ?: '∞',
$c->min_order_value ?: ''
], ';');
}
fclose($out);
exit;
}
}
// ── Bulk-Generierung speichern ────────────────────────────────────────
if (isset($_POST['wis_bulk_generate'])) {
check_admin_referer('wis_bulk_generate');
global $wpdb;
$count = min(500, max(1, intval($_POST['bulk_count'] ?? 10)));
$mode = $_POST['bulk_mode'] === 'prefix' ? 'prefix' : 'random';
$prefix = strtoupper(preg_replace('/[^A-Z0-9_-]/i', '', $_POST['bulk_prefix'] ?? ''));
$value = intval($_POST['bulk_value'] ?? 10);
$type = in_array($_POST['bulk_type'], ['fixed', 'percent']) ? $_POST['bulk_type'] : 'fixed';
$usage_limit = max(1, intval($_POST['bulk_usage_limit'] ?? 1));
$expiry = !empty($_POST['bulk_expiry']) ? sanitize_text_field($_POST['bulk_expiry']) : null;
$min_order = intval($_POST['bulk_min_order'] ?? 0);
$allowed_cats = !empty($_POST['bulk_allowed_categories'])
? implode(',', array_map('intval', (array)$_POST['bulk_allowed_categories']))
: null;
$bulk_id = strtoupper(bin2hex(random_bytes(4))); // gemeinsame ID für diese Batch
$table = $wpdb->prefix . 'wis_coupons';
$generated = 0;
$skipped = 0;
$attempts = 0;
while ($generated < $count && $attempts < $count * 5) {
$attempts++;
if ($mode === 'prefix' && $prefix) {
$rand = strtoupper(bin2hex(random_bytes(4)));
$code = $prefix . '-' . substr($rand, 0, 3) . '-' . substr($rand, 3, 5);
} else {
$r = strtoupper(bin2hex(random_bytes(6)));
$code = substr($r, 0, 4) . '-' . substr($r, 4, 4) . '-' . substr($r, 8, 4);
}
// Duplikat-Check
$exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM $table WHERE code = %s", $code));
if ($exists) { $skipped++; continue; }
$wpdb->insert($table, [
'code' => $code,
'value' => $value,
'type' => $type,
'usage_limit' => $usage_limit,
'used_count' => 0,
'expiry' => $expiry,
'min_order_value' => $min_order,
'allowed_categories'=> $allowed_cats,
'bulk_id' => $bulk_id,
]);
if ($wpdb->insert_id) $generated++;
}
$export_url = wp_nonce_url(
admin_url('admin.php?page=wis_coupons&action=export_bulk&bulk_id=' . $bulk_id),
'wis_coupon_action', '_wpnonce'
);
echo '<div class="notice notice-success"><p>';
echo "<strong>{$generated} Gutscheine</strong> wurden generiert";
if ($skipped) echo " ({$skipped} Duplikate übersprungen)";
echo '. <a href= . $export_url . >📥 Als CSV exportieren</a></p></div>';
}
if (isset($_POST['wis_save_coupon'])) {
check_admin_referer('wis_coupon_form');
$data = [
'code' => sanitize_text_field($_POST['code']),
'value' => intval($_POST['value']),
'type' => sanitize_text_field($_POST['type']),
'usage_limit' => intval($_POST['usage_limit']),
'expiry' => !empty($_POST['expiry']) ? sanitize_text_field($_POST['expiry']) : null,
'min_order_value' => intval($_POST['min_order_value'] ?? 0),
'allowed_categories' => !empty($_POST['allowed_categories']) ? implode(',', array_map('intval', (array)$_POST['allowed_categories'])) : null
];
if (isset($_GET['edit'])) {
unset($data['used_count']);
WIS_DB::update_coupon(intval($_GET['edit']), $data);
echo '<div class="updated"><p>✅ Gutschein gespeichert!</p></div>';
} else {
$data['used_count'] = 0;
WIS_DB::insert_coupon($data);
echo '<div class="updated"><p>✅ Gutschein erstellt!</p></div>';
}
}
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
check_admin_referer('wis_coupon_action', '_wpnonce');
WIS_DB::delete_coupon(intval($_GET['id']));
echo '<div class="updated"><p>✅ Gutschein gelöscht!</p></div>';
}
if (isset($_GET['add']) || isset($_GET['edit'])) {
global $wpdb;
$coupon = isset($_GET['edit']) ? $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}wis_coupons WHERE id = %d", intval($_GET['edit']))) : null;
$currency = get_option('wis_currency_name', 'Coins');
?>
<div class="wrap">
<h1><?php echo $coupon ? 'Gutschein bearbeiten' : 'Neuer Gutschein'; ?></h1>
<a href="<?php echo admin_url('admin.php?page=wis_coupons'); ?>" class="button">← Zurück zur Liste</a>
<form method="post" style="max-width:600px; margin-top:20px;">
<?php wp_nonce_field('wis_coupon_form'); ?>
<table class="form-table">
<tr>
<th><label for="code">Code *</label></th>
<td>
<input type="text" id="code" name="code" value="<?php echo $coupon ? esc_attr($coupon->code) : ''; ?>" class="regular-text" style="text-transform:uppercase;" required>
<p class="description">Z.B.: SUMMER20</p>
</td>
</tr>
<tr>
<th><label for="type">Typ *</label></th>
<td>
<select id="type" name="type" required>
<option value="fixed" <?php echo ($coupon && $coupon->type === 'fixed') ? 'selected' : ''; ?>>Festbetrag (z.B. 100 <?php echo esc_html($currency); ?> Rabatt)</option>
<option value="percent" <?php echo ($coupon && $coupon->type === 'percent') ? 'selected' : ''; ?>>Prozentual (z.B. 20% Rabatt)</option>
</select>
</td>
</tr>
<tr>
<th><label for="value">Wert *</label></th>
<td>
<input type="number" id="value" name="value" value="<?php echo $coupon ? esc_attr($coupon->value) : ''; ?>" min="1" required>
<p class="description">Bei Festbetrag: Betrag in <?php echo esc_html($currency); ?>. Bei Prozent: Zahl ohne %</p>
</td>
</tr>
<tr>
<th><label for="usage_limit">Nutzungslimit *</label></th>
<td>
<input type="number" id="usage_limit" name="usage_limit" value="<?php echo $coupon ? esc_attr($coupon->usage_limit) : '1'; ?>" min="1" required>
</td>
</tr>
<tr>
<th><label for="expiry">Ablaufdatum</label></th>
<td>
<input type="date" id="expiry" name="expiry" value="<?php echo $coupon && $coupon->expiry ? esc_attr($coupon->expiry) : ''; ?>">
<p class="description">Optional</p>
</td>
</tr>
<tr>
<th><label for="min_order_value">Mindestbestellwert</label></th>
<td>
<input type="number" id="min_order_value" name="min_order_value" value="<?php echo $coupon ? esc_attr($coupon->min_order_value) : '0'; ?>" min="0">
<p class="description">Gutschein gilt nur ab diesem Bestellwert (0 = kein Minimum). Wert in <?php echo esc_html($currency); ?>.</p>
</td>
</tr>
<tr>
<th><label>Erlaubte Kategorien</label></th>
<td>
<?php
$all_cats = WIS_DB::get_categories();
$selected_cats = $coupon && $coupon->allowed_categories ? array_map('intval', explode(',', $coupon->allowed_categories)) : [];
?>
<div style="display:flex;flex-wrap:wrap;gap:8px;max-width:520px;">
<?php foreach ($all_cats as $cat):
$checked = in_array($cat->id, $selected_cats);
?>
<label style="display:flex;align-items:center;gap:7px;background:<?php echo $checked ? '#667eea' : '#f3f4f6'; ?>;color:<?php echo $checked ? '#fff' : '#333'; ?>;border:1px solid <?php echo $checked ? '#667eea' : '#d1d5db'; ?>;border-radius:20px;padding:5px 14px;cursor:pointer;font-size:0.875rem;font-weight:500;transition:all .15s;" class="wis-tag-label">
<input type="checkbox" name="allowed_categories[]" value="<?php echo esc_attr($cat->id); ?>" <?php echo $checked ? 'checked' : ''; ?> style="display:none;" class="wis-tag-cb">
<?php echo esc_html($cat->name); ?>
</label>
<?php endforeach; ?>
</div>
<p class="description" style="margin-top:8px;">Ohne Auswahl gilt der Gutschein für <strong>alle Kategorien</strong>.</p>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="wis_save_coupon" class="button button-primary" value="Speichern">
</p>
</form>
</div>
<?php
return;
}
$coupons = WIS_DB::get_coupons();
$currency = get_option('wis_currency_name', 'Coins');
global $wpdb;
$uses_table = $wpdb->prefix . 'wis_coupon_uses';
$uses_exists = $wpdb->get_var("SHOW TABLES LIKE '$uses_table'");
// Alle Einlösungen laden (gruppiert nach coupon_id)
$uses_by_coupon = [];
if ($uses_exists) {
$all_uses = $wpdb->get_results("SELECT coupon_id, player_name, used_at FROM $uses_table ORDER BY used_at DESC");
foreach ($all_uses as $u) {
$uses_by_coupon[$u->coupon_id][] = $u;
}
}
?>
<div class="wrap">
<h1>Gutscheine
<a href="<?php echo admin_url('admin.php?page=wis_coupons&add=1'); ?>" class="page-title-action">Neu erstellen</a>
<a href="#wis-bulk-section" class="page-title-action" onclick="document.getElementById('wis-bulk-section').style.display=document.getElementById('wis-bulk-section').style.display==='none'?'block':'none';return false;">🎲 Bulk generieren</a>
</h1>
<?php
// Bulk-Gruppen zusammenfassen für Anzeige
$bulk_groups = [];
foreach ($coupons as $c) {
if (!empty($c->bulk_id)) $bulk_groups[$c->bulk_id][] = $c;
}
?>
<!-- ── Bulk-Generator Formular ─────────────────────────────────── -->
<div id="wis-bulk-section" style="display:none;background:#fff;border:1px solid #c3c4c7;border-radius:4px;padding:20px 24px;margin-bottom:20px;">
<h2 style="margin-top:0;">🎲 Bulk-Gutscheine generieren</h2>
<form method="post">
<?php wp_nonce_field('wis_bulk_generate'); ?>
<table class="form-table" style="max-width:700px;">
<tr>
<th>Anzahl</th>
<td><input type="number" name="bulk_count" value="10" min="1" max="500" class="small-text"> <span class="description">Max. 500 pro Durchlauf</span></td>
</tr>
<tr>
<th>Code-Format</th>
<td>
<label style="margin-right:16px;">
<input type="radio" name="bulk_mode" value="random" checked onchange="document.getElementById('wis-prefix-row').style.display='none'">
Zufällig <code style="font-size:11px;">X7K2-MNP9-Q4RT</code>
</label>
<label>
<input type="radio" name="bulk_mode" value="prefix" onchange="document.getElementById('wis-prefix-row').style.display='table-row'">
Mit Prefix <code style="font-size:11px;">SUMMER-XXX-XXXXX</code>
</label>
</td>
</tr>
<tr id="wis-prefix-row" style="display:none;">
<th>Prefix</th>
<td><input type="text" name="bulk_prefix" class="regular-text" placeholder="z.B. SUMMER" maxlength="20" style="text-transform:uppercase;"></td>
</tr>
<tr>
<th>Rabattwert</th>
<td>
<input type="number" name="bulk_value" value="10" min="1" class="small-text">
<select name="bulk_type">
<option value="fixed"><?php echo esc_html(get_option('wis_currency_name','Coins')); ?> (fest)</option>
<option value="percent">% (Prozent)</option>
</select>
</td>
</tr>
<tr>
<th>Max. Einlösungen</th>
<td><input type="number" name="bulk_usage_limit" value="1" min="1" class="small-text"> <span class="description">pro Code</span></td>
</tr>
<tr>
<th>Ablaufdatum</th>
<td><input type="date" name="bulk_expiry"> <span class="description">Optional</span></td>
</tr>
<tr>
<th>Mindestbestellwert</th>
<td><input type="number" name="bulk_min_order" value="0" min="0" class="small-text"> <?php echo esc_html(get_option('wis_currency_name','Coins')); ?> <span class="description">(0 = kein Minimum)</span></td>
</tr>
<tr>
<th>Erlaubte Kategorien</th>
<td>
<?php
$all_cats = WIS_DB::get_categories();
?>
<div style="display:flex;flex-wrap:wrap;gap:8px;max-width:480px;">
<?php foreach ($all_cats as $cat): ?>
<label style="display:flex;align-items:center;gap:6px;background:#f3f4f6;border:1px solid #d1d5db;border-radius:20px;padding:4px 12px;cursor:pointer;font-size:0.875rem;" class="wis-bulk-cat-tag">
<input type="checkbox" name="bulk_allowed_categories[]" value="<?php echo esc_attr($cat->id); ?>" style="display:none;" class="wis-bulk-cat-cb">
<?php echo esc_html($cat->name); ?>
</label>
<?php endforeach; ?>
</div>
<p class="description" style="margin-top:6px;">Ohne Auswahl gilt für alle Kategorien.</p>
</td>
</tr>
</table>
<p><input type="submit" name="wis_bulk_generate" class="button button-primary" value="🎲 Jetzt generieren"></p>
</form>
<script>
document.querySelectorAll('.wis-bulk-cat-tag').forEach(function(label) {
label.addEventListener('click', function() {
var cb = label.querySelector('.wis-bulk-cat-cb');
setTimeout(function() {
label.style.background = cb.checked ? '#667eea' : '#f3f4f6';
label.style.color = cb.checked ? '#fff' : '#333';
label.style.borderColor = cb.checked ? '#667eea' : '#d1d5db';
}, 0);
});
});
</script>
</div>
<!-- ── Bulk-Gruppen Übersicht ──────────────────────────────────── -->
<?php if (!empty($bulk_groups)): ?>
<div style="background:#f8f9fa;border:1px solid #e2e8f0;border-radius:4px;padding:16px 20px;margin-bottom:20px;">
<h3 style="margin:0 0 12px;">📦 Generierte Bulk-Gruppen</h3>
<table class="widefat fixed striped" style="max-width:700px;">
<thead><tr>
<th>Bulk-ID</th>
<th style="width:80px;">Codes</th>
<th style="width:110px;">Rabatt</th>
<th style="width:100px;">Gültig bis</th>
<th style="width:130px;">Aktionen</th>
</tr></thead>
<tbody>
<?php foreach ($bulk_groups as $bid => $bcodes):
$sample = $bcodes[0];
$export_url = wp_nonce_url(admin_url('admin.php?page=wis_coupons&action=export_bulk&bulk_id='.$bid), 'wis_coupon_action', '_wpnonce');
?>
<tr>
<td><code style="font-size:11px;"><?php echo esc_html($bid); ?></code></td>
<td><?php echo count($bcodes); ?></td>
<td><?php echo esc_html($sample->value); ?><?php echo $sample->type==='percent'?'%':' '.esc_html(get_option('wis_currency_name','Coins')); ?></td>
<td><?php echo $sample->expiry ? esc_html(date('d.m.Y', strtotime($sample->expiry))) : '∞'; ?></td>
<td><a href="<?php echo $export_url; ?>" class="button button-small">📥 CSV Export</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<table class="widefat fixed striped">
<thead>
<tr>
<th style="width:130px;">Code</th>
<th style="width:110px;">Rabatt</th>
<th style="width:90px;">Genutzt</th>
<th style="width:90px;">Gültig bis</th>
<th>Eingelöst von</th>
<th style="width:160px;">Aktionen</th>
</tr>
</thead>
<tbody>
<?php if (empty($coupons)): ?>
<tr><td colspan="6" style="text-align:center;padding:40px;">Noch keine Gutscheine vorhanden.</td></tr>
<?php else: foreach ($coupons as $coupon):
$coupon_uses = $uses_by_coupon[$coupon->id] ?? [];
?>
<tr>
<td>
<strong><?php echo esc_html($coupon->code); ?></strong>
<?php if (!empty($coupon->bulk_id)): ?>
<span title="Bulk-Gruppe: <?php echo esc_attr($coupon->bulk_id); ?>" style="display:inline-block;background:#e0e7ff;color:#3730a3;border-radius:10px;padding:1px 7px;font-size:10px;margin-left:4px;cursor:default;">BULK</span>
<?php endif; ?>
</td>
<td>
<?php if ($coupon->type === 'percent'): ?>
<?php echo esc_html($coupon->value); ?>%
<?php else: ?>
<?php echo esc_html($coupon->value); ?> <?php echo esc_html($currency); ?>
<?php endif; ?>
</td>
<td><?php echo esc_html($coupon->used_count); ?> / <?php echo esc_html($coupon->usage_limit); ?></td>
<td><?php echo $coupon->expiry ? esc_html(date('d.m.Y', strtotime($coupon->expiry))) : '∞'; ?></td>
<td>
<?php if (empty($coupon_uses)): ?>
<span style="color:#ccc;"></span>
<?php else: ?>
<div style="display:flex;flex-wrap:wrap;gap:4px;">
<?php foreach ($coupon_uses as $u): ?>
<span title="<?php echo esc_attr(date('d.m.Y H:i', strtotime($u->used_at))); ?>"
style="background:#eef;border:1px solid #c9d;border-radius:10px;padding:2px 8px;font-size:12px;cursor:default;">
<?php echo esc_html($u->player_name); ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</td>
<td>
<a href="<?php echo admin_url('admin.php?page=wis_coupons&edit=' . $coupon->id); ?>" class="button button-small">Bearbeiten</a>
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_coupons&action=delete&id=' . $coupon->id), 'wis_coupon_action', '_wpnonce'); ?>" class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen</a>
</td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
</div>
<?php
}
public static function page_orders() {
global $wpdb;
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
check_admin_referer('wis_order_action', '_wpnonce');
WIS_DB::delete_order(intval($_GET['id']));
echo '<div class="updated"><p>✅ Bestellung gelöscht!</p></div>';
}
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'complete') {
check_admin_referer('wis_order_action', '_wpnonce');
WIS_DB::update_order_status(intval($_GET['id']), 'completed');
echo '<div class="updated"><p>✅ Status geändert!</p></div>';
}
if (isset($_GET['view'])) {
$order = WIS_DB::get_order(intval($_GET['view']));
if (!$order) { echo '<div class="error"><p>Bestellung nicht gefunden.</p></div>'; return; }
$currency = get_option('wis_currency_name', 'Coins');
$status_colors = ['pending'=>'#ffc107','processing'=>'#0073aa','completed'=>'green','cancelled'=>'red','failed'=>'red'];
$status_labels = ['pending'=>'Warte auf Ingame','processing'=>'In Bearbeitung','completed'=>'Erledigt','cancelled'=>'Abgebrochen','failed'=>'Fehler'];
?>
<div class="wrap">
<h1>Bestellung #<?php echo $order->id; ?> Details</h1>
<a href="<?php echo admin_url('admin.php?page=wis_orders'); ?>" class="button">← Zurück</a>
<table class="widefat" style="max-width:800px; margin-top:20px;">
<tr><th>ID</th><td><?php echo esc_html($order->id); ?></td></tr>
<tr><th>Datum</th><td><?php echo esc_html(date('d.m.Y H:i', strtotime($order->created_at))); ?></td></tr>
<tr><th>Käufer</th><td><strong><?php echo esc_html($order->player_name); ?></strong></td></tr>
<?php if (!empty($order->gift_recipient)): ?>
<tr><th>🎁 Geschenk für</th><td style="color:#9b59b6;font-weight:bold;"><?php echo esc_html($order->gift_recipient); ?></td></tr>
<?php endif; ?>
<tr><th>Server</th><td><?php echo esc_html($order->server); ?></td></tr>
<tr><th>Zusammenfassung</th><td><?php echo esc_html($order->item_title); ?></td></tr>
<tr><th>Preis</th><td><?php echo esc_html($order->price); ?> <?php echo esc_html($currency); ?></td></tr>
<tr><th>Status</th><td style="color:<?php echo $status_colors[$order->status] ?? 'black'; ?>;font-weight:bold;"><?php echo $status_labels[$order->status] ?? $order->status; ?></td></tr>
<tr><th>Details (JSON)</th><td><code style="display:block;background:#eee;padding:10px;font-size:11px;overflow-x:auto;"><?php echo esc_html($order->response); ?></code></td></tr>
</table>
</div>
<?php
return;
}
// --- Filter-Parameter ---
$search = sanitize_text_field($_GET['s'] ?? '');
$f_status = sanitize_text_field($_GET['status'] ?? '');
$f_server = sanitize_text_field($_GET['server'] ?? '');
$per_page = 50;
$cur_page = max(1, intval($_GET['paged'] ?? 1));
$offset = ($cur_page - 1) * $per_page;
$where = ['1=1'];
$params = [];
if ($search) {
$where[] = '(player_name LIKE %s OR gift_recipient LIKE %s OR item_title LIKE %s)';
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like; $params[] = $like; $params[] = $like;
}
if ($f_status) { $where[] = 'status = %s'; $params[] = $f_status; }
if ($f_server) { $where[] = 'server = %s'; $params[] = $f_server; }
$where_sql = implode(' AND ', $where);
$base_sql = "FROM {$wpdb->prefix}wis_orders WHERE $where_sql ORDER BY created_at DESC";
$total = $params
? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) $base_sql", ...$params))
: (int)$wpdb->get_var("SELECT COUNT(*) $base_sql");
$orders_sql = "SELECT * $base_sql LIMIT %d OFFSET %d";
$orders = $params
? $wpdb->get_results($wpdb->prepare($orders_sql, ...[...$params, $per_page, $offset]))
: $wpdb->get_results($wpdb->prepare($orders_sql, $per_page, $offset));
$total_pages = max(1, (int)ceil($total / $per_page));
$currency = get_option('wis_currency_name', 'Coins');
$servers = WIS_DB::get_servers();
$status_map = ['pending'=>'Warte','claimed'=>'Abgeholt','processing'=>'Geben...','completed'=>'Fertig','cancelled'=>'Abgebrochen','failed'=>'Fehler'];
$status_colors = ['pending'=>'#ffc107','claimed'=>'#17a2b8','processing'=>'#0073aa','completed'=>'green','cancelled'=>'red','failed'=>'red'];
?>
<div class="wrap">
<h1>Bestellungen <span style="font-size:14px;color:#666;">(<?php echo number_format($total); ?> gesamt)</span></h1>
<!-- Suchleiste -->
<form method="get" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:15px 0;">
<input type="hidden" name="page" value="wis_orders">
<input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Spieler, Item suchen…" style="min-width:220px;">
<select name="status">
<option value="">Alle Status</option>
<?php foreach ($status_map as $k => $l): ?>
<option value="<?php echo $k; ?>" <?php selected($f_status, $k); ?>><?php echo $l; ?></option>
<?php endforeach; ?>
</select>
<select name="server">
<option value="">Alle Server</option>
<?php foreach ($servers as $s): ?>
<option value="<?php echo esc_attr($s->slug); ?>" <?php selected($f_server, $s->slug); ?>><?php echo esc_html($s->name); ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="button">Filtern</button>
<?php if ($search || $f_status || $f_server): ?>
<a href="<?php echo admin_url('admin.php?page=wis_orders'); ?>" class="button">Zurücksetzen</a>
<?php endif; ?>
</form>
<table class="widefat fixed striped">
<thead>
<tr>
<th style="width:50px;">ID</th>
<th style="width:130px;">Datum</th>
<th style="width:120px;">Käufer</th>
<th style="width:120px;">Empfänger</th>
<th style="width:80px;">Server</th>
<th>Inhalt</th>
<th style="width:90px;">Preis</th>
<th style="width:110px;">Status</th>
<th style="width:180px;">Aktionen</th>
</tr>
</thead>
<tbody>
<?php if (empty($orders)): ?>
<tr><td colspan="9" style="text-align:center;padding:40px;color:#999;">Keine Bestellungen gefunden.</td></tr>
<?php else: foreach ($orders as $order): ?>
<tr>
<td><strong>#<?php echo $order->id; ?></strong></td>
<td><?php echo date('d.m.Y H:i', strtotime($order->created_at)); ?></td>
<td><strong><?php echo esc_html($order->player_name); ?></strong></td>
<td><?php if (!empty($order->gift_recipient)): ?><span style="color:#9b59b6;">🎁 <?php echo esc_html($order->gift_recipient); ?></span><?php else: ?><span style="color:#ccc;"></span><?php endif; ?></td>
<td><?php echo esc_html($order->server); ?></td>
<td><?php echo esc_html(mb_substr($order->item_title, 0, 55)) . (mb_strlen($order->item_title) > 55 ? '…' : ''); ?></td>
<td><?php echo esc_html($order->price); ?> <?php echo esc_html($currency); ?></td>
<td style="color:<?php echo $status_colors[$order->status] ?? '#333'; ?>;font-weight:bold;"><?php echo $status_map[$order->status] ?? $order->status; ?></td>
<td>
<a href="<?php echo admin_url('admin.php?page=wis_orders&view=' . $order->id); ?>" class="button button-small">Details</a>
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_orders&action=delete&id=' . $order->id . ($search?"&s=$search":'') . ($f_status?"&status=$f_status":'') . ($f_server?"&server=$f_server":'')), 'wis_order_action', '_wpnonce'); ?>" class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen</a>
</td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
<?php if ($total_pages > 1): ?>
<div style="margin-top:15px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<?php for ($p = 1; $p <= $total_pages; $p++):
$url = admin_url('admin.php?page=wis_orders&paged='.$p.($search?"&s=$search":'').($f_status?"&status=$f_status":'').($f_server?"&server=$f_server":''));
?>
<a href="<?php echo $url; ?>" class="button<?php echo $p===$cur_page?' button-primary':''; ?>"><?php echo $p; ?></a>
<?php endfor; ?>
<span style="color:#666;font-size:13px;">Seite <?php echo $cur_page; ?> / <?php echo $total_pages; ?> &nbsp;(<?php echo $total; ?> Einträge)</span>
</div>
<?php endif; ?>
</div>
<?php
}
// -------------------------------------------------------
// ANKAUF-LOG
// -------------------------------------------------------
public static function page_sell_log() {
global $wpdb;
$table = $wpdb->prefix . 'wis_sell_log';
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) {
echo '<div class="wrap"><h1>Ankauf-Log</h1><p>Tabelle noch nicht vorhanden Plugin einmal deaktivieren/aktivieren.</p></div>';
return;
}
$search = sanitize_text_field($_GET['s'] ?? '');
$f_item = sanitize_text_field($_GET['item'] ?? '');
$f_server = sanitize_text_field($_GET['server'] ?? '');
$per_page = 50;
$cur_page = max(1, intval($_GET['paged'] ?? 1));
$offset = ($cur_page - 1) * $per_page;
$where = ['1=1']; $params = [];
if ($search) {
$like = '%' . $wpdb->esc_like($search) . '%';
$where[] = '(player_name LIKE %s OR item_name LIKE %s)';
$params[] = $like; $params[] = $like;
}
if ($f_item) { $where[] = 'item_id = %s'; $params[] = $f_item; }
if ($f_server) { $where[] = 'server = %s'; $params[] = $f_server; }
$where_sql = implode(' AND ', $where);
$total = $params
? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE $where_sql", ...$params))
: (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql");
$rows = $params
? $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY sold_at DESC LIMIT %d OFFSET %d", ...[...$params, $per_page, $offset]))
: $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY sold_at DESC LIMIT %d OFFSET %d", $per_page, $offset));
$total_pages = max(1, (int)ceil($total / $per_page));
$currency = get_option('wis_currency_name', 'Coins');
$servers = WIS_DB::get_servers();
$items_with_sell = $wpdb->get_results("SELECT DISTINCT item_id, item_name FROM $table ORDER BY item_name ASC");
?>
<div class="wrap">
<h1>📤 Ankauf-Log <span style="font-size:14px;color:#666;">(<?php echo number_format($total); ?> Einträge)</span></h1>
<form method="get" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:15px 0;">
<input type="hidden" name="page" value="wis_sell_log">
<input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Spieler oder Item…" style="min-width:200px;">
<select name="item">
<option value="">Alle Items</option>
<?php foreach ($items_with_sell as $it): ?>
<option value="<?php echo esc_attr($it->item_id); ?>" <?php selected($f_item, $it->item_id); ?>><?php echo esc_html($it->item_name); ?></option>
<?php endforeach; ?>
</select>
<select name="server">
<option value="">Alle Server</option>
<?php foreach ($servers as $s): ?>
<option value="<?php echo esc_attr($s->slug); ?>" <?php selected($f_server, $s->slug); ?>><?php echo esc_html($s->name); ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="button">Filtern</button>
<?php if ($search || $f_item || $f_server): ?><a href="<?php echo admin_url('admin.php?page=wis_sell_log'); ?>" class="button">Zurücksetzen</a><?php endif; ?>
</form>
<table class="widefat fixed striped">
<thead><tr>
<th style="width:130px;">Datum</th>
<th style="width:120px;">Spieler</th>
<th style="width:80px;">Server</th>
<th>Item</th>
<th style="width:70px;">Menge</th>
<th style="width:100px;">Ø Preis</th>
<th style="width:110px;">Ausgezahlt</th>
</tr></thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="7" style="text-align:center;padding:30px;color:#999;">Keine Einträge gefunden.</td></tr>
<?php else: foreach ($rows as $r): ?>
<tr>
<td><?php echo date('d.m.Y H:i', strtotime($r->sold_at)); ?></td>
<td><strong><?php echo esc_html($r->player_name); ?></strong></td>
<td><?php echo esc_html($r->server); ?></td>
<td><?php echo esc_html($r->item_name); ?><small style="display:block;color:#aaa;"><?php echo esc_html($r->item_id); ?></small></td>
<td><?php echo number_format($r->quantity); ?></td>
<td><?php echo number_format($r->price_per_item, 2); ?> <?php echo esc_html($currency); ?></td>
<td><strong><?php echo number_format($r->total_paid, 2); ?></strong> <?php echo esc_html($currency); ?></td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
<?php if ($total_pages > 1): ?>
<div style="margin-top:12px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<?php for ($p=1;$p<=$total_pages;$p++): $url=admin_url('admin.php?page=wis_sell_log&paged='.$p.($search?"&s=$search":'').($f_item?"&item=$f_item":'').($f_server?"&server=$f_server":'')); ?>
<a href="<?php echo $url; ?>" class="button<?php echo $p===$cur_page?' button-primary':''; ?>"><?php echo $p; ?></a>
<?php endfor; ?>
<span style="color:#666;font-size:13px;">Seite <?php echo $cur_page; ?> / <?php echo $total_pages; ?></span>
</div>
<?php endif; ?>
</div>
<?php
}
// -------------------------------------------------------
// PREISHISTORIE
// -------------------------------------------------------
public static function page_price_history() {
global $wpdb;
$table = $wpdb->prefix . 'wis_price_history';
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) {
echo '<div class="wrap"><h1>Preishistorie</h1><p>Tabelle noch nicht vorhanden Plugin einmal deaktivieren/aktivieren.</p></div>';
return;
}
$search = sanitize_text_field($_GET['s'] ?? '');
$f_field = sanitize_text_field($_GET['field'] ?? '');
$per_page = 50;
$cur_page = max(1, intval($_GET['paged'] ?? 1));
$offset = ($cur_page - 1) * $per_page;
$where = ['1=1']; $params = [];
if ($search) {
$like = '%' . $wpdb->esc_like($search) . '%';
$where[] = '(item_name LIKE %s OR item_id LIKE %s OR changed_by LIKE %s)';
$params[] = $like; $params[] = $like; $params[] = $like;
}
if ($f_field) { $where[] = 'field = %s'; $params[] = $f_field; }
$where_sql = implode(' AND ', $where);
$total = $params
? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE $where_sql", ...$params))
: (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql");
$rows = $params
? $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY changed_at DESC LIMIT %d OFFSET %d", ...[...$params, $per_page, $offset]))
: $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY changed_at DESC LIMIT %d OFFSET %d", $per_page, $offset));
$total_pages = max(1, (int)ceil($total / $per_page));
$currency = get_option('wis_currency_name', 'Coins');
$fields_list = $wpdb->get_col("SELECT DISTINCT field FROM $table ORDER BY field");
?>
<div class="wrap">
<h1>📈 Preishistorie <span style="font-size:14px;color:#666;">(<?php echo number_format($total); ?> Änderungen)</span></h1>
<form method="get" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:15px 0;">
<input type="hidden" name="page" value="wis_price_history">
<input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Item oder Admin…" style="min-width:200px;">
<select name="field">
<option value="">Alle Felder</option>
<?php foreach ($fields_list as $f): ?>
<option value="<?php echo esc_attr($f); ?>" <?php selected($f_field, $f); ?>><?php echo esc_html($f); ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="button">Filtern</button>
<?php if ($search||$f_field): ?><a href="<?php echo admin_url('admin.php?page=wis_price_history'); ?>" class="button">Zurücksetzen</a><?php endif; ?>
</form>
<?php if ($total === 0 && !$search && !$f_field): ?>
<p style="color:#999;">Noch keine Preisänderungen protokolliert. Ändere einen Preis bei einem Item und speichere ab dann wird hier alles festgehalten.</p>
<?php endif; ?>
<table class="widefat fixed striped">
<thead><tr>
<th style="width:130px;">Datum</th>
<th>Item</th>
<th style="width:120px;">Feld</th>
<th style="width:100px;">Alt</th>
<th style="width:100px;">Neu</th>
<th style="width:30px;">Diff</th>
<th style="width:100px;">Geändert von</th>
</tr></thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="7" style="text-align:center;padding:30px;color:#999;">Keine Einträge gefunden.</td></tr>
<?php else: foreach ($rows as $r):
$diff = $r->new_value - $r->old_value;
$diff_color = $diff > 0 ? 'green' : ($diff < 0 ? 'red' : '#666');
?>
<tr>
<td><?php echo date('d.m.Y H:i', strtotime($r->changed_at)); ?></td>
<td><strong><?php echo esc_html($r->item_name); ?></strong><small style="display:block;color:#aaa;"><?php echo esc_html($r->item_id); ?></small></td>
<td><?php echo esc_html($r->field); ?></td>
<td><?php echo number_format($r->old_value); ?> <?php echo esc_html($currency); ?></td>
<td><strong><?php echo number_format($r->new_value); ?></strong> <?php echo esc_html($currency); ?></td>
<td style="color:<?php echo $diff_color; ?>;font-weight:bold;"><?php echo ($diff>0?'+':'') . number_format($diff); ?></td>
<td><?php echo esc_html($r->changed_by); ?></td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
<?php if ($total_pages > 1): ?>
<div style="margin-top:12px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<?php for ($p=1;$p<=$total_pages;$p++): $url=admin_url('admin.php?page=wis_price_history&paged='.$p.($search?"&s=$search":'').($f_field?"&field=$f_field":'')); ?>
<a href="<?php echo $url; ?>" class="button<?php echo $p===$cur_page?' button-primary':''; ?>"><?php echo $p; ?></a>
<?php endfor; ?>
<span style="color:#666;font-size:13px;">Seite <?php echo $cur_page; ?> / <?php echo $total_pages; ?></span>
</div>
<?php endif; ?>
</div>
<?php
}
public static function page_json() {
if (isset($_POST['wis_generate_json'])) {
check_admin_referer('wis_json_export');
$items = WIS_DB::get_items(['status' => 'publish']);
$img_base = get_option('wis_image_base_url', '');
$json_data = ['items' => []];
foreach ($items as $item) {
$json_data['items'][] = [
'id' => $item->item_id,
'name' => $item->name,
'description' => $item->description,
'price' => intval($item->price),
'image' => WIS_DB::get_item_image($item)
];
}
$json_output = json_encode($json_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo '<div class="wrap"><h1>📦 JSON Export</h1>';
echo '<div class="updated"><p>✅ JSON erfolgreich generiert!</p></div>';
echo '<textarea style="width:100%; height:400px; font-family:monospace; font-size:12px;">'.esc_textarea($json_output).'</textarea>';
echo '<p><button onclick="downloadJSON()" class="button button-primary">💾 Als items.json herunterladen</button></p>';
echo '<script>
function downloadJSON() {
const text = document.querySelector("textarea").value;
const blob = new Blob([text], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "items.json";
a.click();
}
</script>';
echo '<h3>📤 Nächste Schritte:</h3>';
echo '<ol>';
echo '<li>Lade die JSON-Datei herunter</li>';
echo '<li>Gehe zu deinem Gitea Repository</li>';
echo '<li>Lade die <code>items.json</code> hoch unter: <code>https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro</code></li>';
echo '<li>Klicke dann auf den <strong>Quick-Import</strong> Button unten!</li>';
echo '</ol>';
echo '</div>';
return;
}
$default_url = 'https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/raw/branch/main/items.json';
?>
<div class="wrap">
<h1>📦 JSON Export/Import</h1>
<div class="card" style="max-width:800px; padding:20px; margin-top:20px;">
<h2>📤 JSON Export</h2>
<p>Generiere eine JSON-Datei mit allen deinen Items für Gitea.</p>
<form method="post">
<?php wp_nonce_field('wis_json_export'); ?>
<p class="submit">
<input type="submit" name="wis_generate_json" class="button button-primary button-large" value="📦 JSON Generieren">
</p>
</form>
</div>
<div class="card" style="max-width:800px; padding:20px; margin-top:20px; background:#e8f5e9;">
<h2>⚡ Quick-Import von Gitea</h2>
<p><strong>Importiert direkt von deinem Gitea Repository!</strong></p>
<p style="margin:10px 0; padding:10px; background:#fff; border-left:4px solid #28a745; font-family:monospace; font-size:12px; word-break:break-all;">
<?php echo esc_html($default_url); ?>
</p>
<button type="button" id="btn-quick-import" class="button button-primary button-large" style="background:#28a745; border-color:#28a745;">
⚡ Quick-Import starten
</button>
<div id="quick-import-result" style="margin-top:15px;"></div>
</div>
<div class="card" style="max-width:800px; padding:20px; margin-top:20px;">
<h2>📥 JSON Import (Manuelle URL)</h2>
<p>Importiere Items aus einer beliebigen JSON-URL.</p>
<div id="wis-import-form">
<input type="text" id="import-url" class="large-text code" value="<?php echo esc_attr($default_url); ?>" style="margin-bottom:15px;">
<br>
<button type="button" id="btn-import" class="button button-primary button-large">📥 Importieren</button>
<div id="import-result" style="margin-top:15px;"></div>
</div>
</div>
</div>
<script>
document.getElementById('btn-quick-import').addEventListener('click', function() {
const url = '<?php echo esc_js($default_url); ?>';
const resultDiv = document.getElementById('quick-import-result');
const btn = this;
btn.disabled = true;
btn.textContent = '⏳ Importiere von Gitea...';
resultDiv.innerHTML = '<div style="padding:10px; background:#fff3cd; border-left:4px solid #ffc107; margin-top:10px;">⏳ Lade Items von Gitea...</div>';
fetch('<?php echo rest_url('wis/v1/import_json'); ?>', {
method: 'POST',
headers: {
'Content-Type':'application/json',
'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
},
body: JSON.stringify({url: url})
})
.then(r => r.json())
.then(data => {
if (data.success) {
resultDiv.innerHTML = '<div class="updated" style="margin-top:10px;"><p><strong>✅ Erfolgreich!</strong><br>' + data.imported + ' Items importiert! (' + data.skipped + ' übersprungen)</p></div>';
setTimeout(() => {
window.location.href = '<?php echo admin_url('admin.php?page=wis_items'); ?>';
}, 2000);
} else {
resultDiv.innerHTML = '<div class="error" style="margin-top:10px;"><p><strong>❌ Fehler:</strong><br>' + data.message + '</p></div>';
}
})
.catch(e => {
resultDiv.innerHTML = '<div class="error" style="margin-top:10px;"><p><strong>❌ Netzwerkfehler:</strong><br>' + e.message + '</p></div>';
})
.finally(() => {
btn.disabled = false;
btn.textContent = '⚡ Quick-Import starten';
});
});
document.getElementById('btn-import').addEventListener('click', function() {
const url = document.getElementById('import-url').value.trim();
const resultDiv = document.getElementById('import-result');
if (!url) {
resultDiv.innerHTML = '<div class="error"><p>Bitte URL eingeben!</p></div>';
return;
}
this.disabled = true;
this.textContent = '⏳ Importiere...';
fetch('<?php echo rest_url('wis/v1/import_json'); ?>', {
method: 'POST',
headers: {
'Content-Type':'application/json',
'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
},
body: JSON.stringify({url: url})
})
.then(r => r.json())
.then(data => {
if (data.success) {
resultDiv.innerHTML = '<div class="updated"><p>✅ ' + data.imported + ' Items importiert! (' + data.skipped + ' übersprungen)</p></div>';
setTimeout(() => {
window.location.href = '<?php echo admin_url('admin.php?page=wis_items'); ?>';
}, 2000);
} else {
resultDiv.innerHTML = '<div class="error"><p>❌ Fehler: ' + data.message + '</p></div>';
}
})
.catch(e => {
resultDiv.innerHTML = '<div class="error"><p>❌ Netzwerkfehler: ' + e.message + '</p></div>';
})
.finally(() => {
this.disabled = false;
this.textContent = '📥 Importieren';
});
});
</script>
<?php
}
public static function page_reset() {
$msg = '';
if (isset($_POST['wis_confirm_reset'])) {
check_admin_referer('wis_reset');
WIS_Activator::reset_shop();
$msg = '<div class="updated"><p>✅ Shop wurde komplett zurückgesetzt!</p></div>';
}
if (isset($_POST['wis_reset_sell_log'])) {
check_admin_referer('wis_reset_sell_log');
WIS_Activator::reset_sell_log();
$msg = '<div class="updated"><p>✅ Ankauf-Log wurde geleert!</p></div>';
}
if (isset($_POST['wis_reset_top_spenders'])) {
check_admin_referer('wis_reset_top_spenders');
WIS_Activator::reset_top_spenders();
$msg = '<div class="updated"><p>✅ Top-Spender-Daten wurden zurückgesetzt!</p></div>';
}
if (isset($_POST['wis_reset_analyse'])) {
check_admin_referer('wis_reset_analyse');
WIS_Activator::reset_analyse();
$msg = '<div class="updated"><p>✅ Analyse-Daten wurden geleert!</p></div>';
}
if (isset($_POST['wis_reset_price_history'])) {
check_admin_referer('wis_reset_price_history');
WIS_Activator::reset_price_history();
$msg = '<div class="updated"><p>✅ Preishistorie wurde geleert!</p></div>';
}
?>
<div class="wrap">
<h1>🔄 Shop Reset</h1>
<?php echo $msg; ?>
<?php
$card = 'max-width:800px;padding:20px;margin-bottom:20px;background:#fff;border:1px solid #ddd;border-radius:4px;';
$warn = 'max-width:800px;padding:20px;margin-bottom:20px;background:#fff3cd;border:1px solid #ffc107;border-radius:4px;';
?>
<!-- Ankauf-Log -->
<div class="card" style="<?php echo $card; ?>">
<h2>📤 Ankauf-Log zurücksetzen</h2>
<p>Löscht alle Einträge in der <code>wis_sell_log</code>-Tabelle.</p>
<p style="color:#666;">Items, Bestellungen und Einstellungen bleiben unberührt.</p>
<form method="post" onsubmit="return confirm('Ankauf-Log wirklich komplett leeren?');">
<?php wp_nonce_field('wis_reset_sell_log'); ?>
<input type="submit" name="wis_reset_sell_log" class="button button-secondary" value="🗑️ Ankauf-Log leeren">
</form>
</div>
<!-- Top Spender -->
<div class="card" style="<?php echo $card; ?>">
<h2>🏆 Top-Spender-Daten zurücksetzen</h2>
<p>Löscht <strong>alle</strong> Bestellungen aus der Datenbank (alle Status).</p>
<p style="color:#666;">Neue Bestellungen werden weiterhin normal erfasst.</p>
<form method="post" onsubmit="return confirm('Top-Spender-Daten wirklich löschen?');">
<?php wp_nonce_field('wis_reset_top_spenders'); ?>
<input type="submit" name="wis_reset_top_spenders" class="button button-secondary" value="🗑️ Top-Spender löschen">
</form>
</div>
<!-- Analyse -->
<div class="card" style="<?php echo $card; ?>">
<h2>📊 Analyse-Daten zurücksetzen</h2>
<p>Leert die <code>wis_order_items</code>-Tabelle (Grundlage für Kauf- & Verkaufsanalyse).</p>
<p style="color:#666;">Bestellungen und Items bleiben erhalten nur die Detailauswertung wird geleert.</p>
<form method="post" onsubmit="return confirm('Analyse-Daten wirklich leeren?');">
<?php wp_nonce_field('wis_reset_analyse'); ?>
<input type="submit" name="wis_reset_analyse" class="button button-secondary" value="🗑️ Analyse-Daten leeren">
</form>
</div>
<!-- Preishistorie -->
<div class="card" style="<?php echo $card; ?>">
<h2>📈 Preishistorie zurücksetzen</h2>
<p>Löscht alle Einträge in der <code>wis_price_history</code>-Tabelle.</p>
<p style="color:#666;">Items und Bestellungen bleiben unberührt.</p>
<form method="post" onsubmit="return confirm('Preishistorie wirklich komplett leeren?');">
<?php wp_nonce_field('wis_reset_price_history'); ?>
<input type="submit" name="wis_reset_price_history" class="button button-secondary" value="🗑️ Preishistorie leeren">
</form>
</div>
<!-- Komplett-Reset -->
<div class="card" style="<?php echo $warn; ?>">
<h2 style="color:#856404;">⚠️ Komplett-Reset WARNUNG</h2>
<p><strong>Diese Aktion löscht ALLE Daten:</strong></p>
<ul>
<li>❌ Alle Items</li>
<li>❌ Alle Bestellungen</li>
<li>❌ Alle Gutscheine</li>
<li>❌ Alle Server</li>
<li>❌ Alle Kategorien</li>
</ul>
<p style="color:red;font-weight:bold;">Diese Aktion kann NICHT rückgängig gemacht werden!</p>
<form method="post" onsubmit="return confirm('WIRKLICH ALLE DATEN LÖSCHEN? Dies kann nicht rückgängig gemacht werden!');">
<?php wp_nonce_field('wis_reset'); ?>
<input type="submit" name="wis_confirm_reset" class="button button-secondary button-large" value="🗑️ Shop jetzt zurücksetzen">
</form>
</div>
</div>
<?php
}
// =========================================================
// ABO-VERWALTUNG (Admin)
// =========================================================
public static function page_abo_admin() {
global $wpdb;
// ── POST-Aktionen ─────────────────────────────────────────────────
if (isset($_POST['wis_abo_action'], $_POST['wis_abo_nonce'])
&& wp_verify_nonce($_POST['wis_abo_nonce'], 'wis_abo_admin_action')) {
$action = sanitize_key($_POST['wis_abo_action']);
$abo_type = sanitize_key($_POST['wis_abo_type'] ?? '');
$abo_id = intval($_POST['wis_abo_id'] ?? 0);
if ($abo_id > 0) {
if ($abo_type === 'fly') {
$table = $wpdb->prefix . 'wis_fly_abo_subs';
} else {
$table = $wpdb->prefix . 'wis_item_abo_subs';
}
if ($action === 'cancel') {
$wpdb->update($table,
['cancelled' => 1, 'cancelled_at' => current_time('mysql')],
['id' => $abo_id]
);
echo '<div class="updated"><p>✅ Abo #' . $abo_id . ' gekündigt.</p></div>';
} elseif ($action === 'delete') {
$wpdb->delete($table, ['id' => $abo_id]);
echo '<div class="updated"><p>🗑️ Abo #' . $abo_id . ' gelöscht.</p></div>';
} elseif ($action === 'reactivate') {
$wpdb->update($table,
['cancelled' => 0, 'cancelled_at' => null, 'status' => 'active'],
['id' => $abo_id]
);
echo '<div class="updated"><p>✔ Abo #' . $abo_id . ' reaktiviert.</p></div>';
}
}
}
// ── Filter ───────────────────────────────────────────────────────
$f_player = sanitize_text_field($_GET['player'] ?? '');
$f_type = sanitize_key($_GET['abo_type'] ?? 'all');
$f_status = sanitize_key($_GET['abo_status'] ?? 'active');
$currency = esc_html(get_option('wis_currency_name', '$'));
// ── Fly-Abos laden ────────────────────────────────────────────────
$fly_table = $wpdb->prefix . 'wis_fly_abo_subs';
$fly_exists = $wpdb->get_var("SHOW TABLES LIKE '{$fly_table}'");
$fly_rows = [];
if ($fly_exists && in_array($f_type, ['all', 'fly'])) {
$where = '1=1';
$args = [];
if ($f_status === 'active') { $where .= " AND status = 'active' AND cancelled = 0 AND expires_at > NOW()"; }
elseif ($f_status === 'cancelled') { $where .= " AND cancelled = 1"; }
elseif ($f_status === 'expired') { $where .= " AND (status = 'expired' OR expires_at <= NOW())"; }
if ($f_player) { $where .= " AND player_name LIKE %s"; $args[] = '%' . $f_player . '%'; }
$sql = "SELECT *, 'fly' AS abo_type FROM {$fly_table} WHERE {$where} ORDER BY created_at DESC LIMIT 200";
$fly_rows = $args ? $wpdb->get_results($wpdb->prepare($sql, ...$args)) : $wpdb->get_results($sql);
}
// ── Item-Abos laden ───────────────────────────────────────────────
$item_table = $wpdb->prefix . 'wis_item_abo_subs';
$item_exists = $wpdb->get_var("SHOW TABLES LIKE '{$item_table}'");
$item_rows = [];
if ($item_exists && in_array($f_type, ['all', 'item'])) {
$where = '1=1';
$args = [];
if ($f_status === 'active') { $where .= " AND status = 'active' AND cancelled = 0 AND expires_at > NOW()"; }
elseif ($f_status === 'cancelled') { $where .= " AND cancelled = 1"; }
elseif ($f_status === 'expired') { $where .= " AND (status = 'expired' OR expires_at <= NOW())"; }
if ($f_player) { $where .= " AND player_name LIKE %s"; $args[] = '%' . $f_player . '%'; }
$sql = "SELECT *, 'item' AS abo_type FROM {$item_table} WHERE {$where} ORDER BY created_at DESC LIMIT 200";
$item_rows = $args ? $wpdb->get_results($wpdb->prepare($sql, ...$args)) : $wpdb->get_results($sql);
}
// Zusammenführen und nach Datum sortieren
$all_rows = array_merge($fly_rows, $item_rows);
usort($all_rows, fn($a, $b) => strcmp($b->created_at, $a->created_at));
$nonce = wp_create_nonce('wis_abo_admin_action');
$base_url = admin_url('admin.php?page=wis_abo_admin');
?>
<div class="wrap">
<h1>📋 Abo-Verwaltung <span style="font-size:14px;color:#666;font-weight:400;">(<?php echo count($all_rows); ?> Einträge)</span></h1>
<?php /* Filter-Leiste */ ?>
<form method="get" style="display:flex;gap:8px;align-items:center;margin:16px 0;flex-wrap:wrap;">
<input type="hidden" name="page" value="wis_abo_admin">
<input type="text" name="player" value="<?php echo esc_attr($f_player); ?>"
placeholder="Spieler suchen…" class="regular-text" style="max-width:180px;">
<select name="abo_type">
<option value="all" <?php selected($f_type,'all'); ?>>Alle Typen</option>
<option value="fly" <?php selected($f_type,'fly'); ?>>✈ Fly-Abo</option>
<option value="item" <?php selected($f_type,'item'); ?>>📦 Item-Abo</option>
</select>
<select name="abo_status">
<option value="active" <?php selected($f_status,'active'); ?>>Aktiv</option>
<option value="cancelled" <?php selected($f_status,'cancelled'); ?>>Gekündigt</option>
<option value="expired" <?php selected($f_status,'expired'); ?>>Abgelaufen</option>
<option value="all" <?php selected($f_status,'all'); ?>>Alle Status</option>
</select>
<button type="submit" class="button">Filtern</button>
<a href="<?php echo $base_url; ?>" class="button">Zurücksetzen</a>
</form>
<?php if (empty($all_rows)): ?>
<p style="color:#888;">Keine Abos gefunden.</p>
<?php else: ?>
<table class="wp-list-table widefat fixed striped" style="margin-top:0;">
<thead>
<tr>
<th style="width:45px;">ID</th>
<th style="width:70px;">Typ</th>
<th style="width:130px;">Spieler</th>
<th style="width:80px;">Server</th>
<th>Bezeichnung / Details</th>
<th style="width:90px;">Preis</th>
<th style="width:110px;">Läuft bis</th>
<th style="width:100px;">Status</th>
<th style="width:180px;">Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($all_rows as $row):
$is_fly = $row->abo_type === 'fly';
$is_active = $row->status === 'active' && !$row->cancelled && strtotime($row->expires_at) > time();
$is_cancelled = (bool) $row->cancelled;
$is_expired = !$is_active && !$is_cancelled;
$status_html = $is_cancelled
? '<span style="color:#c0392b;font-weight:600;">⚠ Gekündigt</span>'
: ($is_active
? '<span style="color:#27ae60;font-weight:600;">✔ Aktiv</span>'
: '<span style="color:#888;">⏱ Abgelaufen</span>');
$detail = $is_fly
? 'Fly-Abo · ' . number_format($row->price) . ' ' . $currency . '/Monat'
: '📦 ' . esc_html($row->item_id) . ' · ' . intval($row->daily_qty) . 'x täglich';
?>
<tr>
<td><strong>#<?php echo $row->id; ?></strong></td>
<td><?php echo $is_fly ? '✈ Fly' : '📦 Item'; ?></td>
<td><strong><?php echo esc_html($row->player_name); ?></strong></td>
<td><?php echo esc_html($row->server); ?></td>
<td>
<strong><?php echo esc_html($row->label); ?></strong>
<br><small style="color:#888;"><?php echo $detail; ?></small>
<?php if (!$is_fly && !empty($row->last_delivered)): ?>
<br><small style="color:#aaa;">Zuletzt geliefert: <?php echo date('d.m.Y', strtotime($row->last_delivered)); ?></small>
<?php endif; ?>
</td>
<td><?php echo $is_fly ? number_format($row->price) . ' ' . $currency : ''; ?></td>
<td><?php echo date('d.m.Y', strtotime($row->expires_at)); ?></td>
<td><?php echo $status_html; ?></td>
<td>
<form method="post" style="display:inline;" onsubmit="return confirm('Sicher?');">
<input type="hidden" name="wis_abo_nonce" value="<?php echo $nonce; ?>">
<input type="hidden" name="wis_abo_type" value="<?php echo esc_attr($row->abo_type); ?>">
<input type="hidden" name="wis_abo_id" value="<?php echo $row->id; ?>">
<?php if ($is_active): ?>
<button name="wis_abo_action" value="cancel"
class="button button-small"
style="color:#c0392b;border-color:#c0392b;"
title="Kündigen">⛔ Kündigen</button>
<?php elseif ($is_cancelled): ?>
<button name="wis_abo_action" value="reactivate"
class="button button-small"
style="color:#27ae60;border-color:#27ae60;"
title="Reaktivieren">✔ Reaktivieren</button>
<?php endif; ?>
<button name="wis_abo_action" value="delete"
class="button button-small"
style="color:#888;"
title="Eintrag löschen"
onclick="return confirm('Abo #<?php echo $row->id; ?> wirklich dauerhaft löschen?');">🗑</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}
public static function page_top_spenders() {
global $wpdb;
$currency = get_option('wis_currency_name', 'Coins');
$results = $wpdb->get_results("
SELECT player_name, SUM(price) as total_spent, COUNT(*) as order_count
FROM {$wpdb->prefix}wis_orders
WHERE status = 'completed'
GROUP BY player_name
ORDER BY total_spent DESC
LIMIT 50
");
?>
<div class="wrap">
<h1>🏆 Top Spender</h1>
<p>Spieler mit den höchsten Gesamtausgaben</p>
<table class="widefat fixed striped">
<thead>
<tr>
<th style="width:80px;">Rang</th>
<th>Spieler</th>
<th style="width:150px;">Ausgegeben</th>
<th style="width:150px;">Bestellungen</th>
</tr>
</thead>
<tbody>
<?php if (empty($results)): ?>
<tr><td colspan="4" style="text-align:center; padding:40px;">Noch keine Statistiken vorhanden.</td></tr>
<?php else: ?>
<?php $rank = 1; foreach ($results as $row): ?>
<tr>
<td><strong>#<?php echo $rank++; ?></strong></td>
<td><?php echo esc_html($row->player_name); ?></td>
<td><?php echo esc_html(number_format($row->total_spent)); ?> <?php echo esc_html($currency); ?></td>
<td><?php echo esc_html($row->order_count); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
public static function page_analyse() {
global $wpdb;
$currency = get_option('wis_currency_name', 'Coins');
// Zeitraum-Filter
$range = sanitize_text_field($_GET['range'] ?? '30');
if (!in_array($range, ['7','30','90','365','all'])) $range = '30';
$date_where_oi = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
$date_where_sell = $range === 'all' ? '' : "AND s.sold_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
$date_where_ord = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
$chart_days = in_array($range, ['7','30','90']) ? intval($range) : ($range === '365' ? 365 : 30);
$oi_table = $wpdb->prefix . 'wis_order_items';
$ord_table = $wpdb->prefix . 'wis_orders';
$sell_table = $wpdb->prefix . 'wis_sell_log';
$oi_exists = $wpdb->get_var("SHOW TABLES LIKE '$oi_table'") ? true : false;
$sell_exists = $wpdb->get_var("SHOW TABLES LIKE '$sell_table'") ? true : false;
// ---- TOP-KÄUFE aus wis_order_items (nur status=completed Orders) ----
$top_buys = [];
$using_legacy = false;
if ($oi_exists) {
$top_buys = $wpdb->get_results("
SELECT
oi.item_id,
oi.item_name,
oi.item_type,
SUM(oi.quantity) AS qty,
SUM(oi.total) AS revenue,
AVG(oi.price_per_item) AS avg_price,
COUNT(DISTINCT oi.order_id) AS order_count
FROM {$oi_table} oi
INNER JOIN {$ord_table} o ON o.id = oi.order_id AND o.status = 'completed'
WHERE 1=1 {$date_where_oi}
GROUP BY oi.item_id, oi.item_name, oi.item_type
ORDER BY qty DESC
LIMIT 20
");
}
// ---- FALLBACK: wis_order_items leer → response-JSON aus wis_orders parsen ----
if (empty($top_buys)) {
$using_legacy = true;
$date_cond = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
$old_orders = $wpdb->get_results("
SELECT o.id, o.price, o.response, o.created_at
FROM {$ord_table} o
WHERE o.status = 'completed'
AND o.item_id = 'cart'
AND o.response IS NOT NULL
{$date_cond}
ORDER BY o.created_at DESC
LIMIT 2000
");
// Parsen und aggregieren
$agg = []; // agg_key => data
$item_cache = []; // item_id => db row (cache um DB-Calls zu sparen)
foreach ($old_orders as $ord) {
$payload = json_decode($ord->response, true);
if (!$payload) continue;
$items_list = $payload['items'] ?? [];
$cmds = $payload['commands'] ?? [];
$order_price = floatval($ord->price);
// Gesamtzahl der Positionen für anteilige Preisberechnung
$total_positions = 0;
foreach ($items_list as $pi) { $total_positions += intval($pi['amount'] ?? 1); }
foreach ($cmds as $cmd) { $total_positions += 1; }
if ($total_positions < 1) $total_positions = 1;
// --- Normale Items ---
foreach ($items_list as $pi) {
$pid = $pi['id'] ?? '';
if (!$pid) continue;
$pqty = intval($pi['amount'] ?? 1);
// DB-Lookup mit Cache
if (!array_key_exists($pid, $item_cache)) {
$item_cache[$pid] = $wpdb->get_row($wpdb->prepare(
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id = %s LIMIT 1", $pid
));
}
$db_item = $item_cache[$pid];
$item_name = $db_item ? $db_item->name : $pid;
// Aktueller Katalogpreis als primäre Quelle
$unit_price = $db_item
? ($db_item->offer_price > 0 ? floatval($db_item->offer_price) : floatval($db_item->price))
: round($order_price / $total_positions, 2); // Fallback: anteilig
if (!isset($agg[$pid])) {
$agg[$pid] = ['item_id'=>$pid,'item_name'=>$item_name,'item_type'=>'item',
'qty'=>0,'revenue'=>0,'price_sum'=>0,'cnt'=>0];
}
$agg[$pid]['qty'] += $pqty;
$agg[$pid]['revenue'] += $unit_price * $pqty;
$agg[$pid]['price_sum']+= $unit_price;
$agg[$pid]['cnt']++;
}
// --- Commands (fly, rank, fly_abo, plot) ---
foreach ($cmds as $cmd) {
$ctype = $cmd['type'] ?? 'item';
$clabel = $cmd['label'] ?? $ctype;
$ckey = $ctype . '||' . $clabel;
// Preis aus wis_items anhand des Labels oder type-basierten item_id
$cmd_price = 0;
if ($ctype === 'fly') {
// Fly-Item anhand der Dauer identifizieren
$dur_sec = intval($cmd['duration_sec'] ?? 0);
$fly_map = [300=>'fly_5min',900=>'fly_15min',1800=>'fly_30min',3600=>'fly_1h',7200=>'fly_2h',10800=>'fly_3h'];
$fly_id = $fly_map[$dur_sec] ?? null;
if ($fly_id) {
if (!array_key_exists($fly_id, $item_cache)) {
$item_cache[$fly_id] = $wpdb->get_row($wpdb->prepare(
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id = %s LIMIT 1", $fly_id
));
}
$fi = $item_cache[$fly_id];
$cmd_price = $fi ? ($fi->offer_price > 0 ? floatval($fi->offer_price) : floatval($fi->price)) : 0;
}
} elseif ($ctype === 'rank') {
// Rank-Item: suche nach rank_{rank_id}* in wis_items
$rid = preg_replace('/[^a-zA-Z0-9_\-]/', '', $cmd['rank_id'] ?? '');
if ($rid && !array_key_exists('rank_'.$rid, $item_cache)) {
$item_cache['rank_'.$rid] = $wpdb->get_row($wpdb->prepare(
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id LIKE %s LIMIT 1",
'rank_' . $rid . '%'
));
}
$ri = $rid ? ($item_cache['rank_'.$rid] ?? null) : null;
$cmd_price = $ri ? ($ri->offer_price > 0 ? floatval($ri->offer_price) : floatval($ri->price)) : 0;
} elseif (in_array($ctype, ['fly_abo','plot_abo','plot_slots'])) {
// Abo/Plot: direkt nach type-ähnlicher item_id suchen
if (!array_key_exists($ctype, $item_cache)) {
$item_cache[$ctype] = $wpdb->get_row($wpdb->prepare(
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id LIKE %s LIMIT 1",
$ctype . '%'
));
}
$ai = $item_cache[$ctype] ?? null;
$cmd_price = $ai ? ($ai->offer_price > 0 ? floatval($ai->offer_price) : floatval($ai->price)) : 0;
}
// Letzter Fallback: anteiliger Orderpreis
if ($cmd_price <= 0) {
$cmd_price = round($order_price / $total_positions, 2);
}
if (!isset($agg[$ckey])) {
$agg[$ckey] = ['item_id'=>$ctype,'item_name'=>$clabel,'item_type'=>$ctype,
'qty'=>0,'revenue'=>0,'price_sum'=>0,'cnt'=>0];
}
$agg[$ckey]['qty'] += 1;
$agg[$ckey]['revenue'] += $cmd_price;
$agg[$ckey]['price_sum']+= $cmd_price;
$agg[$ckey]['cnt']++;
}
}
// Sortieren nach qty DESC, in top_buys-kompatibles Format bringen
usort($agg, fn($a,$b) => $b['qty'] - $a['qty']);
$agg = array_slice($agg, 0, 20);
foreach ($agg as $a) {
$obj = new stdClass();
$obj->item_id = $a['item_id'];
$obj->item_name = $a['item_name'];
$obj->item_type = $a['item_type'];
$obj->qty = $a['qty'];
$obj->revenue = $a['revenue'];
$obj->avg_price = $a['cnt'] > 0 ? round($a['price_sum'] / $a['cnt'], 2) : 0;
$obj->order_count = $a['cnt'];
$top_buys[] = $obj;
}
}
// ---- TOP-VERKÄUFE / ANKÄUFE ----
$top_sells = [];
if ($sell_exists) {
$top_sells = $wpdb->get_results("
SELECT
s.item_name,
s.item_id,
SUM(s.quantity) AS qty,
SUM(s.total_paid) AS total_paid,
AVG(s.price_per_item) AS avg_price
FROM {$sell_table} s
WHERE 1=1 {$date_where_sell}
GROUP BY s.item_id, s.item_name
ORDER BY qty DESC
LIMIT 20
");
}
// ---- UMSATZ PRO TAG (nach gewähltem Zeitraum) ----
$chart_interval = $range === 'all' ? 365 : intval($range);
$daily_revenue = $wpdb->get_results($wpdb->prepare("
SELECT DATE(o.created_at) AS day, SUM(o.price) AS revenue, COUNT(*) AS orders
FROM {$ord_table} o
WHERE o.status = 'completed'
AND o.created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
GROUP BY DATE(o.created_at)
ORDER BY day ASC
", $chart_interval));
// ---- KPI-Gesamtzahlen ----
$buy_qty = $oi_exists ? ($wpdb->get_var("SELECT SUM(oi.quantity) FROM {$oi_table} oi INNER JOIN {$ord_table} o ON o.id=oi.order_id AND o.status='completed' WHERE 1=1 {$date_where_oi}") ?: 0) : 0;
$buy_revenue = $wpdb->get_var("SELECT SUM(o.price) FROM {$ord_table} o WHERE o.status='completed' {$date_where_ord}") ?: 0;
$sell_qty = $sell_exists ? ($wpdb->get_var("SELECT SUM(s.quantity) FROM {$sell_table} s WHERE 1=1 {$date_where_sell}") ?: 0) : 0;
$sell_paid = $sell_exists ? ($wpdb->get_var("SELECT SUM(s.total_paid) FROM {$sell_table} s WHERE 1=1 {$date_where_sell}") ?: 0) : 0;
$chart_labels = [];
$chart_revenue = [];
foreach ($daily_revenue as $dr) {
$chart_labels[] = $dr->day;
$chart_revenue[] = floatval($dr->revenue);
}
?>
<div class="wrap">
<h1>📊 Analyse Kauf & Verkauf</h1>
<style>
.wis-ana-tabs { margin:15px 0 20px; display:flex; gap:8px; flex-wrap:wrap; }
.wis-ana-tabs a { padding:8px 18px; border-radius:20px; border:2px solid #ddd; text-decoration:none; color:#333; font-weight:600; background:#fff; }
.wis-ana-tabs a.active { background:#667eea; color:#fff; border-color:#667eea; }
.wis-ana-kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:15px; margin-bottom:25px; }
.wis-ana-card { background:#fff; border-radius:10px; padding:18px 20px; border:1px solid #e0e0e0; }
.wis-ana-card .val { font-size:1.8rem; font-weight:800; color:#667eea; }
.wis-ana-card .lbl { font-size:0.85rem; color:#666; margin-top:4px; }
.wis-ana-section { background:#fff; border-radius:10px; padding:20px; border:1px solid #e0e0e0; margin-bottom:20px; }
.wis-ana-section h2 { margin-top:0; font-size:1.1rem; border-bottom:1px solid #eee; padding-bottom:10px; }
.wis-ana-table { width:100%; border-collapse:collapse; }
.wis-ana-table th { background:#f4f6f8; padding:10px 12px; text-align:left; font-size:0.82rem; color:#555; white-space:nowrap; }
.wis-ana-table td { padding:9px 12px; border-bottom:1px solid #f0f0f0; font-size:0.88rem; vertical-align:middle; }
.wis-ana-table tr:hover td { background:#fafbff; }
.wis-bar-wrap { background:#f0f0f0; border-radius:4px; height:8px; width:100%; min-width:60px; }
.wis-bar-buy { height:8px; border-radius:4px; background:linear-gradient(90deg,#667eea,#764ba2); }
.wis-bar-sell { height:8px; border-radius:4px; background:linear-gradient(90deg,#28a745,#20c997); }
.wis-ana-hint { font-size:0.8rem; color:#856404; margin-top:10px; background:#fffbe6; border:1px solid #ffe58f; border-radius:6px; padding:8px 12px; }
.wis-two-col { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
.wis-notice-box { background:#fff3cd; border:1px solid #ffc107; border-radius:8px; padding:15px 18px; margin-bottom:20px; color:#856404; }
.wis-notice-box strong { display:block; margin-bottom:4px; }
@media(max-width:960px){ .wis-two-col { grid-template-columns:1fr; } }
.wis-tag { display:inline-block; padding:2px 7px; border-radius:10px; font-size:0.75rem; background:#eef; color:#667; margin-left:4px; }
</style>
<!-- Zeitraum-Tabs -->
<div class="wis-ana-tabs">
<?php foreach (['7'=>'7 Tage','30'=>'30 Tage','90'=>'90 Tage','365'=>'1 Jahr','all'=>'Gesamt'] as $v=>$l): ?>
<a href="<?php echo admin_url('admin.php?page=wis_analyse&range='.$v); ?>"
class="<?php echo $range===$v?'active':''; ?>"><?php echo $l; ?></a>
<?php endforeach; ?>
</div>
<?php if (!$oi_exists): ?>
<div class="wis-notice-box">
<strong>⚠️ Einzelitem-Tracking noch nicht aktiv</strong>
Die Tabelle <code>wis_order_items</code> fehlt noch. Bitte das Plugin einmal <strong>deaktivieren und wieder aktivieren</strong>. Ab dann werden alle neuen Käufe item-genau erfasst. Bis dahin wird die Auswertung aus den gespeicherten Bestell-JSONs rekonstruiert (Preise sind Näherungswerte aus dem aktuellen Katalog).
</div>
<?php endif; ?>
<!-- KPI-Karten -->
<div class="wis-ana-kpis">
<div class="wis-ana-card">
<div class="val"><?php echo number_format($buy_qty); ?></div>
<div class="lbl">🛒 Items gekauft<?php echo !$oi_exists ? ' <small>(n/v)</small>' : ''; ?></div>
</div>
<div class="wis-ana-card">
<div class="val"><?php echo number_format($buy_revenue); ?></div>
<div class="lbl">💰 Einnahmen (<?php echo esc_html($currency); ?>)</div>
</div>
<div class="wis-ana-card">
<div class="val"><?php echo number_format($sell_qty); ?></div>
<div class="lbl">📤 Ankäufe (Menge)</div>
</div>
<div class="wis-ana-card">
<div class="val"><?php echo number_format($sell_paid); ?></div>
<div class="lbl">📉 Ausgezahlt (<?php echo esc_html($currency); ?>)</div>
</div>
</div>
<!-- Umsatz-Chart -->
<div class="wis-ana-section">
<h2>📈 Tagesumsatz <?php echo $range === 'all' ? '(Gesamt, max. 365 Tage)' : ('letzte ' . ($range === '365' ? '365 Tage / 1 Jahr' : $range . ' Tage')); ?></h2>
<?php if (empty($daily_revenue)): ?>
<p style="color:#999;text-align:center;padding:20px 0;">Keine abgeschlossenen Bestellungen in den letzten 30 Tagen.</p>
<?php else: ?>
<canvas id="wis-revenue-chart" height="90"></canvas>
<?php endif; ?>
</div>
<div class="wis-two-col">
<!-- TOP KÄUFE (einzeln) -->
<div class="wis-ana-section">
<h2>Top 20 meistgekaufte Items <small style="font-weight:400;color:#888;">(einzeln<?php echo $using_legacy ? ' · Kompatibilitätsmodus' : ''; ?>)</small></h2>
<?php if (!$oi_exists && empty($top_buys)): ?>
<p style="color:#999;">Keine Daten gefunden.</p>
<?php elseif (empty($top_buys)): ?>
<p style="color:#999;">Keine abgeschlossenen Bestellungen für diesen Zeitraum.</p>
<?php else:
$max_buy = max(array_column($top_buys, 'qty') ?: [1]); ?>
<table class="wis-ana-table">
<thead><tr>
<th>#</th>
<th>Item</th>
<th>Ø Preis</th>
<th>Menge</th>
<th>Umsatz</th>
<th style="width:80px;">Trend</th>
</tr></thead>
<tbody>
<?php $r=1; foreach ($top_buys as $row):
?>
<tr>
<td><strong><?php echo $r++; ?></strong></td>
<td>
<strong><?php echo esc_html($row->item_name); ?></strong>
<small style="color:#aaa;display:block;"><?php echo esc_html($row->item_id); ?></small>
</td>
<td style="white-space:nowrap;"><?php echo number_format($row->avg_price, 0); ?> <?php echo esc_html($currency); ?></td>
<td><strong><?php echo number_format($row->qty); ?></strong>
<small style="color:#aaa;">/ <?php echo number_format($row->order_count); ?> Käufe</small>
</td>
<td style="white-space:nowrap;"><?php echo number_format($row->revenue, 0); ?> <?php echo esc_html($currency); ?></td>
<td><div class="wis-bar-wrap"><div class="wis-bar-buy" style="width:<?php echo min(100,round($row->qty/$max_buy*100)); ?>%"></div></div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="wis-ana-hint">💡 Viel gekauft + hoher Umsatz → Preis erhöhen. Viel gekauft + niedriger Ø-Preis → evtl. zu günstig.</p>
<?php endif; ?>
</div>
<!-- TOP ANKÄUFE -->
<div class="wis-ana-section">
<h2>Top 20 Ankauf durch Spieler <small style="font-weight:400;color:#888;">(einzeln)</small></h2>
<?php if (!$sell_exists): ?>
<p style="color:#999;">Ankauf-Tabelle nicht vorhanden. Ankauf-Feature aktivieren.</p>
<?php elseif (empty($top_sells)): ?>
<p style="color:#999;">Keine Ankaufdaten für diesen Zeitraum.</p>
<?php else:
$max_sell = max(array_column($top_sells, 'qty') ?: [1]); ?>
<table class="wis-ana-table">
<thead><tr>
<th>#</th>
<th>Item</th>
<th>Ø Ankaufspreis</th>
<th>Menge</th>
<th>Ausgezahlt</th>
<th style="width:80px;">Trend</th>
</tr></thead>
<tbody>
<?php $r=1; foreach ($top_sells as $row): ?>
<tr>
<td><strong><?php echo $r++; ?></strong></td>
<td>
<strong><?php echo esc_html($row->item_name); ?></strong>
<small style="color:#aaa;display:block;"><?php echo esc_html($row->item_id); ?></small>
</td>
<td style="white-space:nowrap;"><?php echo number_format($row->avg_price, 2); ?> <?php echo esc_html($currency); ?></td>
<td><strong><?php echo number_format($row->qty); ?></strong></td>
<td style="white-space:nowrap;"><?php echo number_format($row->total_paid, 0); ?> <?php echo esc_html($currency); ?></td>
<td><div class="wis-bar-wrap"><div class="wis-bar-sell" style="width:<?php echo min(100,round($row->qty/$max_sell*100)); ?>%"></div></div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="wis-ana-hint">💡 Viel verkauft = Spieler farmen dieses Item massenhaft → Ankaufspreis senken oder Tageslimit einführen.</p>
<?php endif; ?>
</div>
</div>
</div><!-- .wrap -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
(function() {
var labels = <?php echo json_encode($chart_labels); ?>;
var data = <?php echo json_encode($chart_revenue); ?>;
var canvas = document.getElementById('wis-revenue-chart');
if (!canvas || labels.length === 0) return;
new Chart(canvas, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Einnahmen (<?php echo esc_js($currency); ?>)',
data: data,
backgroundColor: 'rgba(102,126,234,0.55)',
borderColor: '#667eea',
borderWidth: 2,
borderRadius: 5,
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, ticks: { callback: function(v){ return v.toLocaleString('de-DE'); } } },
x: { ticks: { maxRotation: 45, font: { size: 11 } } }
}
}
});
})();
</script>
<?php
}
// ===========================================================
// ANGEBOTE ÜBERSICHT
// ===========================================================
public static function page_angebote() {
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$currency = get_option('wis_currency_name', 'Coins');
$nonce = wp_create_nonce('wis_angebote_nonce');
$filter = sanitize_text_field($_GET['filter'] ?? 'all');
$search = sanitize_text_field($_GET['s'] ?? '');
$where = "1=1";
if ($filter === 'offer') $where .= " AND is_offer = 1 AND is_daily_deal = 0";
elseif ($filter === 'daily') $where .= " AND is_daily_deal = 1";
elseif ($filter === 'none') $where .= " AND is_offer = 0 AND is_daily_deal = 0";
else $where .= " AND (is_offer = 1 OR is_daily_deal = 1)";
if ($search !== '') {
$like = '%' . $wpdb->esc_like($search) . '%';
$where .= $wpdb->prepare(" AND (name LIKE %s OR item_id LIKE %s)", $like, $like);
}
if ($filter !== 'none') {
$where .= " AND status = 'publish'";
}
$items = $wpdb->get_results("SELECT * FROM $table WHERE $where ORDER BY is_daily_deal DESC, name ASC");
$count_offer = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_offer = 1 AND is_daily_deal = 0 AND status='publish'");
$count_daily = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_daily_deal = 1 AND status='publish'");
$count_all = $count_offer + $count_daily;
$img_base = get_option('wis_image_base_url', '');
$base_url = admin_url('admin.php?page=wis_angebote');
?>
<div class="wrap">
<h1>🔥 Angebote Übersicht</h1>
<p style="color:#666;">Alle Items die als <strong>Angebot</strong> oder <strong>Daily Deal</strong> markiert sind mit direkter Bearbeitung.</p>
<!-- Filterleiste -->
<ul class="subsubsub" style="margin-bottom:12px;">
<?php
$filters = [
'all' => "🔥 Alle Angebote <span class='count'>($count_all)</span>",
'offer' => "🏷️ Nur Angebote <span class='count'>($count_offer)</span>",
'daily' => "🎁 Daily Deal <span class='count'>($count_daily)</span>",
'none' => "📦 Ohne Kennzeichnung",
];
$i = 0;
foreach ($filters as $key => $label) {
$active = ($filter === $key) ? 'current' : '';
$sep = (++$i < count($filters)) ? ' | ' : '';
echo "<li><a href='" . esc_url(add_query_arg('filter', $key, $base_url)) . "' class='$active'>$label</a>$sep</li>";
}
?>
</ul>
<!-- Suche -->
<form method="get" action="<?php echo admin_url('admin.php'); ?>" style="margin-bottom:16px; display:flex; gap:8px; align-items:center;">
<input type="hidden" name="page" value="wis_angebote">
<input type="hidden" name="filter" value="<?php echo esc_attr($filter); ?>">
<input type="search" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Name oder Item-ID …" class="regular-text">
<button type="submit" class="button">Suchen</button>
<?php if ($search): ?>
<a href="<?php echo esc_url(add_query_arg('filter', $filter, $base_url)); ?>" class="button button-secondary">✕ Zurücksetzen</a>
<?php endif; ?>
</form>
<?php if (empty($items)): ?>
<div style="background:#f8f9fa; border:1px dashed #ccd; border-radius:8px; padding:30px; text-align:center; color:#888; margin-top:10px;">
<span style="font-size:2em;">📭</span><br>
Keine Items gefunden<?php echo $search ? ' für „' . esc_html($search) . '"' : ''; ?>.
</div>
<?php else: ?>
<table class="wp-list-table widefat fixed striped" id="wis-angebote-table">
<thead>
<tr>
<th style="width:55px;">Bild</th>
<th>Name</th>
<th style="width:160px;">Item-ID</th>
<th style="width:110px;">Normalpreis</th>
<th style="width:150px;">Angebotspreis</th>
<th style="width:100px;">Rabatt</th>
<th style="width:110px;">🏷️ Angebot</th>
<th style="width:120px;">🎁 Daily Deal</th>
<th style="width:80px;">Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item):
$img_url = !empty($item->custom_image_url)
? esc_url($item->custom_image_url)
: $img_base . str_replace(':', '_', $item->item_id) . '.png';
$normal_price = intval($item->price);
$offer_price = intval($item->offer_price);
$discount_pct = ($normal_price > 0 && $offer_price > 0 && $offer_price < $normal_price)
? round((1 - $offer_price / $normal_price) * 100) : 0;
$is_daily = intval($item->is_daily_deal);
$is_offer = intval($item->is_offer);
$row_bg = $is_daily ? '#fffbe6' : ($is_offer ? '#f0fff4' : '');
?>
<tr id="wis-ang-row-<?php echo $item->id; ?>" style="<?php echo $row_bg ? "background:$row_bg;" : ''; ?>">
<td style="text-align:center;">
<img src="<?php echo $img_url; ?>" alt="<?php echo esc_attr($item->name); ?>"
style="width:40px;height:40px;object-fit:contain;background:#2d2d2d;border-radius:4px;padding:2px;"
onerror="this.style.opacity='0.2'">
</td>
<td>
<strong><?php echo esc_html($item->name); ?></strong>
<?php if ($is_daily) echo '<br><span style="font-size:11px;color:#6f42c1;font-weight:bold;">🎁 DAILY DEAL</span>'; ?>
<?php if ($is_offer && !$is_daily) echo '<br><span style="font-size:11px;color:#dc3545;font-weight:bold;">🔥 ANGEBOT</span>'; ?>
</td>
<td><code style="font-size:11px;"><?php echo esc_html($item->item_id); ?></code></td>
<td><?php echo number_format($normal_price); ?> <small><?php echo esc_html($currency); ?></small></td>
<td>
<div style="display:flex;align-items:center;gap:4px;">
<input type="number" class="wis-ang-price-input small-text"
data-id="<?php echo $item->id; ?>"
value="<?php echo $offer_price; ?>"
style="width:70px;text-align:right;" min="0">
<small><?php echo esc_html($currency); ?></small>
<button class="button button-small wis-ang-save-price" data-id="<?php echo $item->id; ?>"
title="Angebotspreis speichern" style="padding:0 6px;">💾</button>
</div>
</td>
<td>
<?php if ($discount_pct > 0): ?>
<span style="background:#28a745;color:#fff;border-radius:4px;padding:2px 7px;font-size:12px;font-weight:bold;">
<?php echo $discount_pct; ?>%
</span>
<?php else: ?>
<span style="color:#bbb;">—</span>
<?php endif; ?>
</td>
<td style="text-align:center;">
<label class="wis-ang-toggle" title="Angebot umschalten">
<input type="checkbox" class="wis-ang-flag"
data-id="<?php echo $item->id; ?>" data-field="is_offer"
<?php checked($is_offer, 1); ?>>
<span class="wis-ang-slider"></span>
</label>
</td>
<td style="text-align:center;">
<label class="wis-ang-toggle" title="Als Daily Deal setzen">
<input type="checkbox" class="wis-ang-flag"
data-id="<?php echo $item->id; ?>" data-field="is_daily_deal"
<?php checked($is_daily, 1); ?>>
<span class="wis-ang-slider"></span>
</label>
</td>
<td>
<a href="<?php echo esc_url(admin_url('admin.php?page=wis_items&edit=' . $item->id)); ?>"
class="button button-small" title="Item bearbeiten">✏️</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p style="color:#888;font-size:12px;margin-top:8px;"><?php echo count($items); ?> Item(s) gefunden.</p>
<?php endif; ?>
</div>
<style>
.wis-ang-toggle { position:relative; display:inline-block; width:44px; height:24px; }
.wis-ang-toggle input { opacity:0; width:0; height:0; }
.wis-ang-slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background:#ccc; transition:.3s; border-radius:24px; }
.wis-ang-slider:before { position:absolute; content:""; height:18px; width:18px; left:3px; bottom:3px; background:#fff; transition:.3s; border-radius:50%; }
.wis-ang-toggle input:checked + .wis-ang-slider { background:#28a745; }
.wis-ang-toggle input:checked + .wis-ang-slider:before { transform:translateX(20px); }
.wis-ang-toggle input:disabled + .wis-ang-slider { opacity:.5; cursor:not-allowed; }
#wis-angebote-table td, #wis-angebote-table th { vertical-align:middle; }
</style>
<script>
jQuery(function($) {
var nonce = <?php echo json_encode($nonce); ?>;
// Flag Toggle (is_offer / is_daily_deal)
$('.wis-ang-flag').on('change', function() {
var $cb = $(this);
var id = $cb.data('id');
var field = $cb.data('field');
$cb.prop('disabled', true);
$.post(ajaxurl, { action:'wis_angebote_toggle', nonce:nonce, id:id, field:field }, function(res) {
if (res.success) {
if (field === 'is_daily_deal' && res.data.new_val === 1) {
$('.wis-ang-flag[data-field="is_daily_deal"]').not($cb).prop('checked', false);
}
var $row = $('#wis-ang-row-' + id);
if (res.data.new_val === 1) {
$row.css('background', field === 'is_daily_deal' ? '#fffbe6' : '#f0fff4');
} else {
$row.css('background', '');
}
} else {
alert('Fehler: ' + (res.data || 'Unbekannt'));
$cb.prop('checked', !$cb.prop('checked'));
}
$cb.prop('disabled', false);
}).fail(function() {
alert('Verbindungsfehler.');
$cb.prop('checked', !$cb.prop('checked'));
$cb.prop('disabled', false);
});
});
// Angebotspreis speichern
$('.wis-ang-save-price').on('click', function() {
var $btn = $(this);
var id = $btn.data('id');
var price = $('#wis-ang-row-' + id).find('.wis-ang-price-input').val();
$btn.prop('disabled', true).text('…');
$.post(ajaxurl, { action:'wis_angebote_save_price', nonce:nonce, id:id, offer_price:price }, function(res) {
if (res.success) {
$btn.text('✅');
setTimeout(function() { $btn.prop('disabled', false).text('💾'); }, 1500);
} else {
alert('Fehler: ' + (res.data || 'Unbekannt'));
$btn.prop('disabled', false).text('💾');
}
}).fail(function() {
alert('Verbindungsfehler.');
$btn.prop('disabled', false).text('💾');
});
});
});
</script>
<?php
}
}
class WIS_API {
public static function register_routes() {
register_rest_route('wis/v1', '/import_json', [
'methods' => 'POST',
'callback' => [self::class, 'import_json'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/order', [
'methods' => 'POST',
'callback' => [self::class, 'create_order'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/validate_coupon', [
'methods' => 'POST',
'callback' => [self::class, 'validate_coupon'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/shop_items', [
'methods' => 'GET',
'callback' => [self::class, 'get_shop_items'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/pending_orders', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_orders'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/execute_order', [
'methods' => 'POST',
'callback' => [self::class, 'execute_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/complete_order', [
'methods' => 'POST',
'callback' => [self::class, 'complete_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/cancel_order', [
'methods' => 'POST',
'callback' => [self::class, 'cancel_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
// Gift-System Endpunkte (ab v6.5-gift)
register_rest_route('wis/v1', '/pending_gifts', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_gifts'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/gift_accept', [
'methods' => 'POST',
'callback' => [self::class, 'gift_accept'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/gift_decline', [
'methods' => 'POST',
'callback' => [self::class, 'gift_decline'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/pending_offline', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_offline'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/my_coupons', [
'methods' => 'GET',
'callback' => [self::class, 'get_my_coupons'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/orders_history', [
'methods' => 'GET',
'callback' => [self::class, 'get_orders_history'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
// Ankauf-Endpunkte (ab v6.5)
register_rest_route('wis/v1', '/sell_items', [
'methods' => 'GET',
'callback' => [self::class, 'get_sell_items'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/sell_item', [
'methods' => 'POST',
'callback' => [self::class, 'process_sell'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
// ── Item-Abo Endpoints ────────────────────────────────────────────
register_rest_route('wis/v1', '/item_abo_status', [
'methods' => 'GET',
'callback' => [self::class, 'item_abo_status'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/item_abo_cancel', [
'methods' => 'POST',
'callback' => [self::class, 'item_abo_cancel'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/trigger_abo_delivery', [
'methods' => 'POST',
'callback' => [self::class, 'trigger_abo_delivery'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
}
// =========================================================
// ANKAUF Endpunkte (ab v6.5)
// =========================================================
/**
* GET /wis/v1/sell_items?server=<slug>
* Liefert alle Items die auf dem angegebenen Server ankaufbar sind.
*/
public static function get_sell_items($request) {
global $wpdb;
$server = sanitize_text_field($request->get_param('server') ?? '');
$currency = get_option('wis_currency_name', 'Coins');
$table = $wpdb->prefix . 'wis_items';
$items = $wpdb->get_results(
"SELECT id, item_id, name, price, sell_price_mode, sell_price_value
FROM $table
WHERE status = 'publish' AND sell_enabled = 1"
);
$result = [];
foreach ($items as $item) {
// Ankaufspreis berechnen
$sell_price = self::calc_sell_price(
(int) $item->price,
$item->sell_price_mode,
(int) $item->sell_price_value
);
if ($sell_price <= 0) continue;
// Server-Filter
if (!empty($server)) {
$servers = $wpdb->get_var($wpdb->prepare(
"SELECT servers FROM $table WHERE id = %d", $item->id
));
$srv_list = json_decode($servers, true) ?: [];
if (!empty($srv_list) && !in_array(strtolower($server), array_map('strtolower', $srv_list))) {
continue;
}
}
$result[] = [
'item_id' => $item->item_id,
'name' => $item->name,
'buy_price' => (int) $item->price,
'sell_price' => $sell_price,
];
}
return new WP_REST_Response([
'items' => $result,
'currency' => $currency,
]);
}
/**
* POST /wis/v1/sell_item
* Body: { "player": "Steve", "server": "survival", "item_id": "minecraft:diamond", "quantity": 5 }
* Antwortet mit dem Betrag der gutgeschrieben werden soll.
*/
// =========================================================
// ITEM-ABO REST-Endpunkte
// =========================================================
/**
* GET /wis/v1/item_abo_status?player=<name>
* Gibt alle aktiven Item-Abos eines Spielers zurück.
*/
public static function item_abo_status($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player') ?? '');
if (empty($player)) {
return new WP_REST_Response(['abos' => []], 200);
}
WIS_Activator::create_item_abo_subs_table();
$table = $wpdb->prefix . 'wis_item_abo_subs';
// id muss mit zurückgegeben werden, damit der Spigot-Client
// beim Kündigen gezielt eine einzelne Abo-Zeile ansprechen kann.
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT id, label, item_id, daily_qty, cancelled,
DATE_FORMAT(expires_at, '%%d.%%m.%%Y') AS expires_at
FROM {$table}
WHERE player_name = %s
AND status = 'active'
AND expires_at > NOW()
ORDER BY created_at ASC",
$player
), ARRAY_A);
foreach ($rows as &$row) {
$row['id'] = (int) $row['id'];
$row['daily_qty'] = (int) $row['daily_qty'];
$row['cancelled'] = (bool) $row['cancelled'];
}
unset($row);
return new WP_REST_Response(['abos' => $rows ?: []], 200);
}
/**
* POST /wis/v1/item_abo_cancel
* Body: {"player":"<name>", "abo_id":<int>}
* Kündigt genau das Item-Abo mit der angegebenen ID nur wenn es dem Spieler gehört.
* Gibt 200 bei Erfolg, 403 bei falschem Besitzer, 404 wenn nicht gefunden.
*/
public static function item_abo_cancel($request) {
global $wpdb;
$body = json_decode($request->get_body(), true);
$player = sanitize_text_field($body['player'] ?? '');
$abo_id = intval($body['abo_id'] ?? 0);
if (empty($player) || $abo_id <= 0) {
return new WP_REST_Response(['success' => false, 'message' => 'Ungültige Parameter'], 400);
}
WIS_Activator::create_item_abo_subs_table();
$table = $wpdb->prefix . 'wis_item_abo_subs';
// Sicherheits-Check: Abo muss dem Spieler gehören und aktiv sein
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT id, label FROM {$table}
WHERE id = %d AND player_name = %s AND status = 'active' AND cancelled = 0",
$abo_id, $player
));
if (!$existing) {
// Unterscheide: existiert gar nicht vs. gehört anderem Spieler
$any = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table} WHERE id = %d", $abo_id
));
if ($any) {
return new WP_REST_Response(['success' => false, 'message' => 'Nicht dein Abo'], 403);
}
return new WP_REST_Response(['success' => false, 'message' => 'Abo nicht gefunden oder bereits gekündigt'], 404);
}
$wpdb->update(
$table,
['cancelled' => 1, 'cancelled_at' => current_time('mysql')],
['id' => $abo_id]
);
error_log("[WIS] Item-Abo #{$abo_id} ({$existing->label}) gekündigt von: {$player}");
return new WP_REST_Response([
'success' => true,
'label' => $existing->label,
'message' => "Item-Abo '{$existing->label}' für {$player} gekündigt",
], 200);
}
/**
* POST /wis/v1/trigger_abo_delivery
* Body: {"player":"<name>", "server":"<server>"}
*
* Prüft ob der Spieler aktive Item-Abos hat, die heute noch nicht beliefert wurden,
* und legt für jedes fehlende Abo sofort eine pending-Order an.
* Wird beim Join und beim Login aufgerufen als Fallback falls der WP-Cron nicht
* gelaufen ist (z.B. kein Traffic auf der WordPress-Seite).
*
* Gibt zurück: {"delivered": <anzahl>} 0 wenn alles bereits geliefert wurde.
*/
public static function trigger_abo_delivery($request) {
global $wpdb;
$body = json_decode($request->get_body(), true);
$player = sanitize_text_field($body['player'] ?? '');
$server = sanitize_text_field($body['server'] ?? '');
if (empty($player)) {
return new WP_REST_Response(['success' => false, 'message' => 'Kein Spielername'], 400);
}
WIS_Activator::create_item_abo_subs_table();
$subs_table = $wpdb->prefix . 'wis_item_abo_subs';
$orders_table = $wpdb->prefix . 'wis_orders';
$today = date('Y-m-d');
// Aktive Abos dieses Spielers die heute noch nicht beliefert wurden
$conditions = "player_name = %s AND status = 'active' AND cancelled = 0"
. " AND expires_at > NOW()"
. " AND (last_delivered IS NULL OR last_delivered < %s)";
// Server-Filter nur wenn angegeben
$args = [$player, $today];
if (!empty($server)) {
$conditions .= " AND server = %s";
$args[] = $server;
}
$pending_subs = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM {$subs_table} WHERE {$conditions}", ...$args)
);
$delivered = 0;
foreach ($pending_subs as $sub) {
$payload = json_encode([
'items' => [['id' => $sub->item_id, 'amount' => intval($sub->daily_qty)]],
'commands' => [],
'abo_delivery' => true,
]);
$wpdb->insert($orders_table, [
'player_name' => $sub->player_name,
'server' => $sub->server,
'item_id' => 'item_abo_delivery',
'item_title' => '📦 Abo-Lieferung: ' . $sub->label . ' ×' . intval($sub->daily_qty),
'price' => 0,
'quantity' => intval($sub->daily_qty),
'status' => 'pending',
'response' => $payload,
]);
$wpdb->update($subs_table, ['last_delivered' => $today], ['id' => $sub->id]);
$delivered++;
}
return new WP_REST_Response(['success' => true, 'delivered' => $delivered], 200);
}
public static function process_sell($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player') ?? '');
$server = sanitize_text_field($request->get_param('server') ?? '');
$item_id = sanitize_text_field($request->get_param('item_id') ?? '');
$quantity = max(1, intval($request->get_param('quantity') ?? 1));
if (!$player || !$item_id) {
return new WP_REST_Response(['success' => false, 'message' => 'Fehlende Parameter'], 400);
}
$table = $wpdb->prefix . 'wis_items';
// Item suchen (case-insensitive, mit/ohne minecraft: prefix)
$clean_id = strtolower(str_replace('minecraft:', '', $item_id));
$row = $wpdb->get_row($wpdb->prepare(
"SELECT id, item_id, name, price, sell_enabled, sell_price_mode, sell_price_value, servers
FROM $table
WHERE sell_enabled = 1
AND (LOWER(item_id) = %s OR LOWER(REPLACE(item_id,'minecraft:','')) = %s)
LIMIT 1",
strtolower($item_id), $clean_id
));
if (!$row) {
return new WP_REST_Response(['success' => false, 'message' => 'Item nicht ankaufbar'], 404);
}
$sell_price = self::calc_sell_price(
(int) $row->price,
$row->sell_price_mode,
(int) $row->sell_price_value
);
if ($sell_price <= 0) {
return new WP_REST_Response(['success' => false, 'message' => 'Ankaufspreis ist 0'], 422);
}
// Tageslimit prüfen
$daily_limit = intval($row->daily_sell_limit ?? 0);
if ($daily_limit > 0) {
$sold_today = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COALESCE(SUM(quantity),0) FROM {$wpdb->prefix}wis_sell_log
WHERE player_name = %s AND item_id = %s
AND DATE(sold_at) = CURDATE()",
$player, $row->item_id
));
if ($sold_today + $quantity > $daily_limit) {
$remaining = max(0, $daily_limit - $sold_today);
return new WP_REST_Response([
'success' => false,
'message' => "Tageslimit erreicht. Du kannst heute noch {$remaining}x dieses Item verkaufen.",
], 429);
}
}
$total = round($sell_price * $quantity, 2);
// Sell-Log schreiben
$wpdb->insert($wpdb->prefix . 'wis_sell_log', [
'player_name' => $player,
'server' => $server,
'item_id' => $row->item_id,
'item_name' => $row->name,
'quantity' => $quantity,
'price_per_item' => $sell_price,
'total_paid' => $total,
]);
return new WP_REST_Response([
'success' => true,
'item_id' => $row->item_id,
'item_name' => $row->name,
'quantity' => $quantity,
'price_per_item' => $sell_price,
'total' => $total,
]);
}
/**
* Berechnet den Ankaufspreis aus Verkaufspreis + Modus.
* mode = "percent" → value ist ein Prozentwert des VK-Preises (z.B. 80 = 80 %)
* mode = "fixed" → value ist ein absoluter Festpreis
* mode = "minus" → value ist ein absoluter Abzug vom VK-Preis
*/
private static function calc_sell_price(int $buy_price, string $mode, int $value): float {
switch ($mode) {
case 'fixed':
return max(0, $value);
case 'minus':
return max(0, $buy_price - $value);
case 'percent':
default:
return max(0, round($buy_price * $value / 100, 2));
}
}
public static function get_shop_items($request) {
$page = max(1, intval($request->get_param('page') ?? 1));
$per_page_param = $request->get_param('per_page');
// -1 = "all", allowed values: 25, 50, 100, -1
$allowed_per_page = [24, 25, 50, 100, -1];
if ($per_page_param !== null) {
$per_page_param = intval($per_page_param);
$per_page = in_array($per_page_param, $allowed_per_page, true) ? $per_page_param : 24;
} else {
$per_page = intval(get_option('wis_default_per_page', 25));
if (!in_array($per_page, $allowed_per_page, true)) $per_page = 24;
}
$category = sanitize_text_field($request->get_param('category') ?? '');
$search = sanitize_text_field($request->get_param('search') ?? '');
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$where_parts = ["status = 'publish'"];
if (!empty($category)) {
$search_pattern = '%"' . $category . '"%';
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
}
if (!empty($search)) {
$search_like = '%' . $wpdb->esc_like($search) . '%';
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
}
$where_sql = implode(" AND ", $where_parts);
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql");
if ($per_page === -1) {
// Alle Items auf einmal
$items = $wpdb->get_results("SELECT * FROM $table WHERE $where_sql ORDER BY name ASC");
$effective_per_page = $total > 0 ? $total : 1;
} else {
$offset = ($page - 1) * $per_page;
$items = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table WHERE $where_sql ORDER BY name ASC LIMIT %d OFFSET %d",
$per_page, $offset
));
$effective_per_page = $per_page;
}
$img_base = get_option('wis_image_base_url', '');
$currency = get_option('wis_currency_name', 'Coins');
$result = [];
foreach ($items as $item) {
$result[] = [
'id' => $item->id,
'item_id' => $item->item_id,
'name' => $item->name,
'description' => $item->description,
'price' => intval($item->price),
'offer_price' => intval($item->offer_price),
'is_offer' => (bool) $item->is_offer,
'is_daily_deal' => (bool) $item->is_daily_deal,
'servers' => json_decode($item->servers, true) ?: [],
'categories' => json_decode($item->categories, true) ?: [],
'image' => WIS_DB::get_item_image($item),
'has_custom_image' => !empty($item->custom_image_url),
'custom_command' => $item->custom_command ?? null,
];
}
// Fly-Items nach Dauer sortieren: 5min→15min→30min→1h→2h→3h
$fly_order = ['fly_5min'=>1,'fly_15min'=>2,'fly_30min'=>3,'fly_1h'=>4,'fly_2h'=>5,'fly_3h'=>6];
usort($result, function($a, $b) use ($fly_order) {
$ap = isset($fly_order[$a['item_id']]) ? $fly_order[$a['item_id']] : 999;
$bp = isset($fly_order[$b['item_id']]) ? $fly_order[$b['item_id']] : 999;
if ($ap !== 999 || $bp !== 999) return $ap - $bp;
return strcmp($a['name'], $b['name']);
});
return new WP_REST_Response([
'items' => $result,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => $per_page === -1 ? 1 : max(1, (int) ceil($total / $per_page)),
'currency' => $currency,
]);
}
public static function get_pending_orders($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player'));
if (!$player) {
return new WP_REST_Response(['orders' => []]);
}
$table = $wpdb->prefix . 'wis_orders';
// Gift-Orders werden genauso wie normale Orders für Spieler A gepollt.
// Spieler A muss zuerst ingame bestätigen (Geld abbuchen), erst dann
// bekommt Spieler B die Gift-Anfrage.
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table
WHERE player_name = %s AND status = 'pending'
ORDER BY created_at ASC LIMIT 5",
$player
));
if (!empty($results)) {
$ids = implode(',', array_map(fn($o) => intval($o->id), $results));
$wpdb->query("UPDATE $table SET status = 'claimed' WHERE id IN ($ids) AND status = 'pending'");
}
return new WP_REST_Response(['orders' => $results]);
}
public static function get_pending_offline($request) {
if (get_option('wis_offline_queue_enabled', '0') !== '1') {
return new WP_REST_Response(['orders' => [], 'message' => 'Offline-Queue deaktiviert']);
}
global $wpdb;
$player = sanitize_text_field($request->get_param('player'));
if (!$player) {
return new WP_REST_Response(['orders' => []]);
}
$table = $wpdb->prefix . 'wis_orders';
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table
WHERE player_name = %s AND status = 'pending'
ORDER BY created_at ASC LIMIT 10",
$player
));
if (!empty($results)) {
$ids = implode(',', array_map(fn($o) => intval($o->id), $results));
$wpdb->query("UPDATE $table SET status = 'claimed' WHERE id IN ($ids) AND status = 'pending'");
}
return new WP_REST_Response(['orders' => $results]);
}
public static function get_orders_history($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player'));
if (!$player) {
return new WP_REST_Response(['orders' => []]);
}
$results = $wpdb->get_results($wpdb->prepare(
"SELECT id, item_title, price, status, created_at
FROM {$wpdb->prefix}wis_orders
WHERE player_name = %s
ORDER BY created_at DESC LIMIT 10",
$player
));
return new WP_REST_Response(['orders' => $results]);
}
public static function execute_order($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->query($wpdb->prepare(
"UPDATE {$wpdb->prefix}wis_orders SET status = 'processing'
WHERE id = %d AND status IN ('pending','claimed')",
$id
));
return new WP_REST_Response(['success' => true]);
}
public static function complete_order($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
// Vor dem Update: Order-Payload lesen, um gift_card_codes zu extrahieren
$order = $wpdb->get_row($wpdb->prepare(
"SELECT response FROM {$wpdb->prefix}wis_orders WHERE id = %d",
$id
));
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'completed'],
['id' => $id]
);
// Gift-Card-Codes aus dem gespeicherten Payload sammeln und zurückgeben
$gift_card_codes = [];
if ($order && !empty($order->response)) {
$payload = json_decode($order->response, true);
if (isset($payload['commands']) && is_array($payload['commands'])) {
$currency = get_option('wis_currency_name', 'Coins');
foreach ($payload['commands'] as $cmd) {
if (($cmd['type'] ?? '') === 'gift_card') {
$gift_card_codes[] = [
'code' => $cmd['code'],
'amount' => $cmd['amount'],
'label' => $cmd['label'] ?? '',
];
}
}
}
}
return new WP_REST_Response(['success' => true, 'gift_card_codes' => $gift_card_codes]);
}
/**
* GET /wis/v1/my_coupons?player=Spielername
* Gibt alle ungenutzten Gift-Card-Gutscheine zurück, die ein Spieler gekauft hat.
* Verknüpfung: wis_orders.response enthält die gift_card codes → wis_coupons.used_count = 0
*/
public static function get_my_coupons($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player') ?? '');
if (!$player) {
return new WP_REST_Response(['success' => false, 'message' => 'Kein Spielername angegeben'], 400);
}
$currency = get_option('wis_currency_name', 'Coins');
// Alle abgeschlossenen Orders des Spielers mit gift_card commands holen
$orders = $wpdb->get_results($wpdb->prepare(
"SELECT response FROM {$wpdb->prefix}wis_orders
WHERE player_name = %s
AND status = 'completed'
AND response LIKE '%gift_card%'
ORDER BY created_at DESC",
$player
));
$result = [];
$seen = [];
foreach ($orders as $order) {
if (empty($order->response)) continue;
$payload = json_decode($order->response, true);
if (!isset($payload['commands'])) continue;
foreach ($payload['commands'] as $cmd) {
if (($cmd['type'] ?? '') !== 'gift_card') continue;
$code = $cmd['code'] ?? '';
if (!$code || isset($seen[$code])) continue;
$seen[$code] = true;
// Prüfen ob der Coupon noch ungenutzt ist
$coupon = $wpdb->get_row($wpdb->prepare(
"SELECT code, value, used_count, usage_limit, expiry
FROM {$wpdb->prefix}wis_coupons
WHERE code = %s",
$code
));
if (!$coupon) continue;
if ($coupon->used_count >= $coupon->usage_limit) continue; // bereits eingelöst
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) continue; // abgelaufen
$result[] = [
'code' => $coupon->code,
'value' => intval($coupon->value),
'currency' => $currency,
'label' => ($cmd['label'] ?? ''),
'expiry' => $coupon->expiry ?: null,
];
}
}
return new WP_REST_Response([
'success' => true,
'player' => $player,
'coupons' => $result,
]);
}
public static function cancel_order($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'cancelled'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
// ── Gift-System Endpoints ─────────────────────────────────────────────
/**
* GET /wis/v1/pending_gifts?recipient={player}
* Gibt ausstehende Geschenk-Orders zurück, die an diesen Empfänger gerichtet sind.
*/
public static function get_pending_gifts($request) {
global $wpdb;
$recipient = sanitize_text_field($request->get_param('recipient'));
if (!$recipient) {
return new WP_REST_Response(['orders' => []]);
}
$table = $wpdb->prefix . 'wis_orders';
// Status NICHT auf 'claimed' setzen bleibt 'pending' bis der Empfänger
// ingame annimmt (gift_accept) oder ablehnt (gift_decline).
// Das Spigot-Plugin prüft pendingGiftRequests intern gegen Duplikate.
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table
WHERE gift_recipient = %s AND status IN ('pending', 'processing')
ORDER BY created_at ASC LIMIT 1",
$recipient
));
return new WP_REST_Response(['orders' => $results]);
}
/**
* POST /wis/v1/gift_accept { "id": 123 }
* Wird aufgerufen wenn Empfänger das Geschenk annimmt.
* Setzt Status auf 'processing' der Spigot-Server liefert dann die Ware aus.
*/
public static function gift_accept($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'processing'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
/**
* POST /wis/v1/gift_decline { "id": 123, "sender": "SpielerA", "price": 500 }
* Empfänger lehnt ab → Order stornieren.
* Rückerstattung erfolgt auf Spigot-Seite via Vault direkt.
*/
public static function gift_decline($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'cancelled'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
public static function import_json($request) {
$data = $request->get_json_params();
$url = esc_url_raw($data['url'] ?? '');
if (!$url) {
return new WP_REST_Response(['success' => false, 'message' => 'Keine URL angegeben'], 400);
}
$response = wp_remote_get($url, ['timeout' => 30]);
if (is_wp_error($response)) {
return new WP_REST_Response(['success' => false, 'message' => $response->get_error_message()], 400);
}
$body = wp_remote_retrieve_body($response);
$json = json_decode($body, true);
if (!isset($json['items']) || !is_array($json['items'])) {
return new WP_REST_Response(['success' => false, 'message' => 'Ungültiges JSON Format'], 400);
}
$imported = 0;
$skipped = 0;
foreach ($json['items'] as $item) {
$item_id = sanitize_text_field($item['id'] ?? '');
$name = sanitize_text_field($item['name'] ?? 'Unbekannt');
if (empty($item_id)) continue;
$exists = WIS_DB::get_item_by_item_id($item_id);
if ($exists) {
$skipped++;
continue;
}
WIS_DB::insert_item([
'item_id' => $item_id,
'name' => $name,
'description' => sanitize_textarea_field($item['description'] ?? ''),
'price' => intval($item['price'] ?? 0),
'status' => intval($item['price'] ?? 0) > 0 ? 'publish' : 'draft',
'servers' => '[]',
'categories' => '[]'
]);
$imported++;
}
return new WP_REST_Response(['success' => true, 'imported' => $imported, 'skipped' => $skipped]);
}
public static function create_order($request) {
global $wpdb;
$data = $request->get_json_params();
$player = sanitize_text_field($data['player'] ?? '');
$cart = $data['cart'] ?? [];
$server = sanitize_text_field($data['server'] ?? '');
$coupon_code = isset($data['coupon_code']) ? sanitize_text_field(strtoupper($data['coupon_code'])) : '';
// Gift: optionaler Empfänger-Spielername
$gift_recipient = isset($data['gift_recipient'])
? sanitize_text_field(trim($data['gift_recipient'])) : '';
if (!$player || empty($cart) || !$server) {
return new WP_REST_Response(['success' => false, 'message' => 'Fehlende Daten'], 400);
}
// Gift-Validierung: Spieler kann sich nicht selbst beschenken
if ($gift_recipient !== '' && strcasecmp($gift_recipient, $player) === 0) {
return new WP_REST_Response(['success' => false, 'message' => 'Selbst-Geschenk nicht erlaubt'], 400);
}
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
$currency = get_option('wis_currency_name', 'Coins');
$valid_cart = [];
$total_normal = 0;
$total_offer = 0;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if (!$item || $item->status !== 'publish') continue;
$qty = intval($item_data['quantity'] ?? 1);
if ($qty <= 0) continue;
$servers = json_decode($item->servers, true);
if (!in_array($server, $servers ?: [])) continue;
$price = $item->offer_price > 0 ? $item->offer_price : $item->price;
// Gutschein-Karte: Preis = Wunschbetrag des Käufers (innerhalb Min/Max)
$custom_amount = 0;
if (preg_match('/^gift_card_(\d+)_(\d+)$/', $item->item_id, $gc_m2)) {
$gc_min2 = intval($gc_m2[1]);
$gc_max2 = intval($gc_m2[2]);
$raw_amount = intval($item_data['custom_amount'] ?? $gc_min2);
$custom_amount = max($gc_min2, min($gc_max2, $raw_amount));
$price = $custom_amount; // Preis = Wunschbetrag
}
$valid_cart[] = [
'id' => $item->item_id,
'title' => $item->name,
'price' => $price,
'qty' => $qty,
'is_offer' => $item->is_offer,
'custom_amount' => $custom_amount ?: $price,
];
$item_total = $price * $qty;
if ($item->is_offer && $exclude_offers === '1') {
$total_offer += $item_total;
} else {
$total_normal += $item_total;
}
}
if (empty($valid_cart)) {
return new WP_REST_Response(['success' => false, 'message' => 'Keine gültigen Items'], 400);
}
$coupon_discount = 0;
$coupon_msg = '';
$coupon_applied = false;
$coupon_error = false; // Gutschein-Fehler der den Kauf blockiert
if (!empty($coupon_code)) {
$coupon = WIS_DB::get_coupon_by_code($coupon_code);
if ($coupon) {
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) {
$coupon_error = true;
$coupon_msg = 'Dein Gutschein ist abgelaufen.';
} elseif ($coupon->used_count >= $coupon->usage_limit) {
$coupon_error = true;
$coupon_msg = 'Dieser Gutschein ist bereits vollständig aufgebraucht.';
} elseif (WIS_DB::coupon_used_by_player($coupon->id, $player)) {
$coupon_error = true;
$coupon_msg = 'Du hast diesen Gutschein bereits eingelöst.';
} else {
if ($exclude_offers === '1' && $total_normal <= 0) {
$coupon_error = true;
$coupon_msg = 'Dieser Gutschein gilt nicht für Angebots-Items.';
} else {
$restriction_error = self::check_coupon_restrictions($coupon, $cart, $total_normal);
if ($restriction_error !== null) {
$coupon_error = true;
$coupon_msg = $restriction_error;
} else
if ($coupon->type === 'percent') {
$coupon_discount = floor($total_normal * ($coupon->value / 100));
} else {
$coupon_discount = $coupon->value;
}
WIS_DB::update_coupon($coupon->id, ['used_count' => $coupon->used_count + 1]);
WIS_DB::record_coupon_use($coupon->id, $player);
$coupon_applied = true;
$coupon_msg = "Gutschein eingelöst: -{$coupon_discount} {$currency}";
}
}
} else {
$coupon_error = true;
$coupon_msg = 'Ungültiger Gutschein-Code.';
}
}
// Kauf blockieren wenn Gutschein-Fehler vorliegt und Spieler nicht explizit bestätigt hat
$confirmed_no_coupon = (bool)($data['confirmed_no_coupon'] ?? false);
if ($coupon_error && !$confirmed_no_coupon) {
return new WP_REST_Response([
'success' => false,
'coupon_error' => true,
'message' => $coupon_msg,
], 200);
}
$final_price = max(0, $total_normal - $coupon_discount) + $total_offer;
// ── Steuer ──────────────────────────────────────────────────
$tax_enabled = get_option('wis_tax_enabled', '0');
$tax_rate = floatval(get_option('wis_tax_rate', '0'));
$tax_amount = 0;
if ($tax_enabled === '1' && $tax_rate > 0) {
$tax_amount = floor($final_price * $tax_rate / 100);
$final_price = $final_price + $tax_amount;
}
// ────────────────────────────────────────────────────────────
// Fly-Dauern (item_id → Sekunden)
// Fly-Dauern und lesbares Label (wird dem Spieler auf dem Code angezeigt)
$fly_durations = [
'fly_5min' => 5 * 60,
'fly_15min' => 15 * 60,
'fly_30min' => 30 * 60,
'fly_1h' => 1 * 3600,
'fly_2h' => 2 * 3600,
'fly_3h' => 3 * 3600,
];
$fly_labels = [
'fly_5min' => '5 Minuten Fly',
'fly_15min' => '15 Minuten Fly',
'fly_30min' => '30 Minuten Fly',
'fly_1h' => '1 Stunde Fly',
'fly_2h' => '2 Stunden Fly',
'fly_3h' => '3 Stunden Fly',
];
$items_payload = [];
$commands_payload = [];
$title_parts = [];
foreach ($valid_cart as $item) {
$item_id = $item['id']; // Das ist der item_id-String aus wis_items
if (isset($fly_durations[$item_id])) {
// Fly-Gutschein: pro Stueck einen eigenen Code-Eintrag (qty > 1 = mehrere Codes)
$base_sec = $fly_durations[$item_id];
$base_label = $fly_labels[$item_id];
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'fly',
'duration_sec' => $base_sec,
'label' => $base_label,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^rank_([^_]+)_([a-zA-Z0-9_\-]+)_([a-zA-Z0-9_\-]+)_(\d+)$/', $item_id, $rm)) {
// Neues Format: rank_{rank_id}_{lp_group}_{default_group}_{days}
$rank_id = $rm[1];
$lp_group = $rm[2];
$default_group = $rm[3];
$rank_days = intval($rm[4]);
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'rank',
'rank_id' => $rank_id,
'lp_group' => $lp_group,
'default_group' => $default_group,
'label' => $item['title'],
'days' => $rank_days,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^rank_(.+)_(\d+)$/', $item_id, $rm)) {
// Altes Format (Fallback): rank_{rank_id}_{days}
$rank_id = $rm[1];
$rank_days = intval($rm[2]);
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'rank',
'rank_id' => $rank_id,
'lp_group'=> $rank_id,
'default_group' => 'default',
'label' => $item['title'],
'days' => $rank_days,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^fly_abo$/', $item_id) || preg_match('/^fly_abo_\d*$/', $item_id)) {
// Fly-Abo: monatlich abonniert, Preis = Monatsbeitrag
$commands_payload[] = [
'type' => 'fly_abo',
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} elseif (preg_match('/^plot_slots_(\d+)$/', $item_id, $ps_m)) {
// Plot-Slots: einmaliger permanenter Kauf
$extra_slots = intval($ps_m[1]) * intval($item['qty']);
$commands_payload[] = [
'type' => 'plot_slots',
'slots' => $extra_slots,
'label' => $item['title'],
];
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^plot_abo_(\d+)$/', $item_id, $pa_m)) {
// Plot-Abo: monatliche Abbuchung, Preis = Monatsbeitrag
$abo_slots = intval($pa_m[1]);
$commands_payload[] = [
'type' => 'plot_abo',
'slots' => $abo_slots,
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} elseif (preg_match('/^item_abo_(.+)_(\d+)_(\d+)$/', $item_id, $ia_m)) {
// Item-Abo: tägliche Lieferung eines Minecraft-Items
$abo_mc_item = $ia_m[1];
$abo_daily_qty = intval($ia_m[2]);
$abo_dur_days = intval($ia_m[3]);
$commands_payload[] = [
'type' => 'item_abo',
'item_id' => $abo_mc_item,
'daily_qty' => $abo_daily_qty,
'duration_days' => $abo_dur_days,
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} elseif (preg_match('/^custom_cmd_(.+)$/', $item_id, $cc_m)) {
// Custom Command Item: Command aus DB holen
$db_item = WIS_DB::get_item_by_item_id($item_id);
$raw_cmd = $db_item ? ($db_item->custom_command ?? '') : '';
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'custom_cmd',
'cmd_id' => $cc_m[1],
'command' => $raw_cmd,
'label' => $item['title'],
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^gift_card_(\d+)_(\d+)$/', $item_id, $gc_m)) {
// Gutschein-Karte: für jede Stück einen eigenen Coupon generieren
$gc_amount = intval($item['custom_amount'] ?? $item['price']); // custom_amount vom Frontend
$gc_min = intval($gc_m[1]);
$gc_max = intval($gc_m[2]);
// Betrag auf gültigen Bereich clippen
$gc_amount = max($gc_min, min($gc_max, $gc_amount));
for ($q = 0; $q < intval($item['qty']); $q++) {
// Eindeutigen Code generieren
$gc_code = 'GC-' . strtoupper(bin2hex(random_bytes(4))) . '-' . strtoupper(bin2hex(random_bytes(3)));
// Als Coupon in DB eintragen: Typ 'fixed', usage_limit 1, kein Ablauf
WIS_DB::insert_coupon([
'code' => $gc_code,
'value' => $gc_amount,
'type' => 'fixed',
'usage_limit' => 1,
'used_count' => 0,
'expiry' => null,
'min_order_value' => 0,
'allowed_categories' => null,
'bulk_id' => 'gift_card',
]);
$commands_payload[] = [
'type' => 'gift_card',
'code' => $gc_code,
'amount' => $gc_amount,
'label' => $item['title'] . ' (' . $gc_amount . ' ' . $currency . ')',
'currency'=> $currency,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
// Preis = tatsächlicher Betrag x Menge (nicht der Artikel-Mindestpreis)
$gc_amount_total = $gc_amount * intval($item['qty']);
// Korrektur: total_normal wurde mit item->price gerechnet, jetzt auf echten Betrag korrigieren
$total_normal = $total_normal - ($item['price'] * intval($item['qty'])) + $gc_amount_total;
} else {
// Normales Item
$items_payload[] = [
'id' => $item_id,
'amount' => $item['qty'],
];
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
}
}
$title = "Warenkorb: " . implode(', ', $title_parts);
if (strlen($title) > 240) $title = substr($title, 0, 237) . '...';
$payload = [
'items' => $items_payload,
'commands' => $commands_payload,
'coupon' => $coupon_applied ? ['code' => $coupon_code, 'discount' => $coupon_discount] : [],
];
WIS_DB::insert_order([
'player_name' => $player,
'gift_recipient' => $gift_recipient !== '' ? $gift_recipient : null,
'server' => $server,
'item_id' => 'cart',
'item_title' => $title,
'price' => $final_price,
'quantity' => count($valid_cart),
'status' => 'pending',
'response' => json_encode($payload)
]);
// ---- Einzelne Items für Analyse-Tabelle speichern ----
$new_order_id = $wpdb->insert_id;
if ($new_order_id) {
$oi_table = $wpdb->prefix . 'wis_order_items';
// Tabelle existiert? (für bestehende Installationen ohne Re-Aktivierung)
$oi_exists = $wpdb->get_var("SHOW TABLES LIKE '$oi_table'");
if ($oi_exists) {
foreach ($valid_cart as $ci) {
$ci_id = $ci['id'];
$ci_title = $ci['title'];
$ci_qty = intval($ci['qty'] ?? 1);
$ci_price = floatval($ci['price'] ?? 0);
// Item-Typ bestimmen
if (isset($fly_durations[$ci_id])) {
$ci_type = 'fly';
} elseif (preg_match('/^rank_/', $ci_id)) {
$ci_type = 'rank';
} elseif ($ci_id === 'fly_abo' || preg_match('/^fly_abo/', $ci_id)) {
$ci_type = 'fly_abo';
} elseif (preg_match('/^plot_/', $ci_id)) {
$ci_type = 'plot';
} else {
$ci_type = 'item';
}
$wpdb->insert($oi_table, [
'order_id' => $new_order_id,
'item_id' => $ci_id,
'item_name' => $ci_title,
'item_type' => $ci_type,
'quantity' => $ci_qty,
'price_per_item'=> $ci_price,
'total' => round($ci_price * $ci_qty, 2),
]);
}
}
}
// Fly-Abo Abonnements registrieren / verlängern
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') !== 'fly_abo') continue;
$abo_days = intval($cmd['days'] ?? 30);
$abo_label = sanitize_text_field($cmd['label'] ?? 'Fly-Abo');
$abo_price = 0;
// Preis aus dem Warenkorb ermitteln (fly_abo mit oder ohne Zahl-Suffix)
foreach ($valid_cart as $ci) {
if ($ci['id'] === 'fly_abo' || preg_match('/^fly_abo/', $ci['id'])) {
$abo_price = intval($ci['price'] ?? 0);
break;
}
}
global $wpdb;
WIS_Activator::create_abo_subs_table();
$subs_table = $wpdb->prefix . 'wis_fly_abo_subs';
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$subs_table} WHERE player_name = %s AND server = %s",
$player, $server
));
if ($existing && $existing->status === 'active') {
// Bestehendes aktives Abo verlängern (kumulativ)
$wpdb->query($wpdb->prepare(
"UPDATE {$subs_table}
SET expires_at = DATE_ADD(IF(expires_at > NOW(), expires_at, NOW()), INTERVAL %d DAY),
cancelled = 0,
cancelled_at = NULL,
label = %s,
price = %d
WHERE id = %d",
$abo_days, $abo_label, $abo_price, $existing->id
));
} else {
// Neues Abo anlegen
$wpdb->replace($subs_table, [
'player_name' => $player,
'server' => $server,
'label' => $abo_label,
'price' => $abo_price,
'status' => 'active',
'cancelled' => 0,
'expires_at' => date('Y-m-d H:i:s', strtotime("+{$abo_days} days")),
]);
}
}
// Item-Abo Abonnements registrieren / verlängern
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') !== 'item_abo') continue;
$ia_mc_item = sanitize_text_field($cmd['item_id'] ?? '');
$ia_daily_qty = max(1, intval($cmd['daily_qty'] ?? 1));
$ia_dur_days = max(1, intval($cmd['duration_days'] ?? 30));
$ia_label = sanitize_text_field($cmd['label'] ?? 'Item-Abo');
$ia_price = 0;
foreach ($valid_cart as $ci) {
if (preg_match('/^item_abo_/', $ci['id'])) {
$ia_price = intval($ci['price'] ?? 0);
break;
}
}
WIS_Activator::create_item_abo_subs_table();
$ia_table = $wpdb->prefix . 'wis_item_abo_subs';
// Bestehende aktive Abos für dieselbe item_id + player + server verlängern
$ia_existing = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$ia_table} WHERE player_name = %s AND server = %s AND item_id = %s AND status = 'active'",
$player, $server, $ia_mc_item
));
if ($ia_existing) {
$wpdb->query($wpdb->prepare(
"UPDATE {$ia_table}
SET expires_at = DATE_ADD(IF(expires_at > NOW(), expires_at, NOW()), INTERVAL %d DAY),
cancelled = 0,
cancelled_at = NULL,
daily_qty = %d,
label = %s,
price = %d
WHERE id = %d",
$ia_dur_days, $ia_daily_qty, $ia_label, $ia_price, $ia_existing->id
));
$ia_sub_id = $ia_existing->id;
} else {
$wpdb->insert($ia_table, [
'player_name' => $player,
'server' => $server,
'item_id' => $ia_mc_item,
'daily_qty' => $ia_daily_qty,
'label' => $ia_label,
'price' => $ia_price,
'status' => 'active',
'cancelled' => 0,
'expires_at' => date('Y-m-d H:i:s', strtotime("+{$ia_dur_days} days")),
'last_delivered' => null,
]);
$ia_sub_id = $wpdb->insert_id;
}
// Sofort-Lieferung für heute anlegen (damit der Spieler nicht bis Mitternacht warten muss)
$ia_sub = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$ia_table} WHERE id = %d", $ia_sub_id
));
if ($ia_sub && (empty($ia_sub->last_delivered) || $ia_sub->last_delivered < date('Y-m-d'))) {
$ia_payload = json_encode([
'items' => [['id' => $ia_mc_item, 'amount' => $ia_daily_qty]],
'commands' => [],
'abo_delivery' => true,
]);
$wpdb->insert($wpdb->prefix . 'wis_orders', [
'player_name' => $player,
'server' => $server,
'item_id' => 'item_abo_delivery',
'item_title' => '📦 Abo-Lieferung: ' . $ia_label . ' ×' . $ia_daily_qty,
'price' => 0,
'quantity' => $ia_daily_qty,
'status' => 'pending',
'response' => $ia_payload,
]);
$wpdb->update($ia_table, ['last_delivered' => date('Y-m-d')], ['id' => $ia_sub_id]);
}
}
$msg = $gift_recipient !== ''
? "🎁 Geschenk-Bestellung für {$gift_recipient} erfolgreich!"
: '✅ Bestellung erfolgreich!';
if ($coupon_msg) $msg .= ' (' . $coupon_msg . ')';
// Gift-Card-Codes aus dem Payload sammeln und in der Antwort zurückgeben
$gift_card_codes = [];
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') === 'gift_card') {
$gift_card_codes[] = [
'code' => $cmd['code'],
'amount' => $cmd['amount'],
'label' => $cmd['label'] ?? '',
];
}
}
return new WP_REST_Response([
'success' => true,
'message' => $msg,
'gift_card_codes' => [], // Codes werden erst nach complete_order zurückgegeben
]);
}
/**
* Prueft Mindestbestellwert, erlaubte Kategorien und erlaubte Raenge.
* Gibt null zurueck wenn alles OK, sonst Fehlermeldung als String.
*/
private static function check_coupon_restrictions($coupon, $cart, $subtotal_normal) {
// 1. Mindestbestellwert
$min = intval($coupon->min_order_value ?? 0);
if ($min > 0 && $subtotal_normal < $min) {
$currency = get_option('wis_currency_name', 'Coins');
return "Mindestbestellwert von {$min} {$currency} nicht erreicht.";
}
// 2. Erlaubte Kategorien
if (!empty($coupon->allowed_categories)) {
$allowed_cat_ids = array_map('intval', explode(',', $coupon->allowed_categories));
$has_valid_cat = false;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && in_array(intval($item->category_id), $allowed_cat_ids)) {
$has_valid_cat = true;
break;
}
}
if (!$has_valid_cat) {
return 'Dieser Gutschein gilt nicht fuer die Produkte in deinem Warenkorb.';
}
}
return null;
}
public static function validate_coupon($request) {
$data = $request->get_json_params();
$code = sanitize_text_field(strtoupper($data['code'] ?? ''));
$cart = $data['cart'] ?? [];
$player = sanitize_text_field($data['player'] ?? '');
if (!$code) {
return new WP_REST_Response(['success' => false, 'message' => 'Kein Code']);
}
$coupon = WIS_DB::get_coupon_by_code($code);
if (!$coupon) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein nicht gefunden']);
}
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein abgelaufen']);
}
if ($coupon->used_count >= $coupon->usage_limit) {
return new WP_REST_Response(['success' => false, 'message' => 'Bereits aufgebraucht']);
}
// Spieler-spezifische Prüfung: hat dieser Spieler den Code schon eingelöst?
if ($player && WIS_DB::coupon_used_by_player($coupon->id, $player)) {
return new WP_REST_Response(['success' => false, 'message' => 'Du hast diesen Gutschein bereits eingelöst']);
}
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
if ($exclude_offers === '1' && !empty($cart)) {
$has_normal = false;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && !$item->is_offer) {
$has_normal = true;
break;
}
}
if (!$has_normal) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein gilt nicht für Angebote']);
}
}
// Neue Einschränkungen prüfen (Mindestbestellwert, Kategorien, Rang)
$subtotal_normal = 0;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && !($exclude_offers === '1' && $item->is_offer)) {
$subtotal_normal += $item->price * intval($item_data['quantity'] ?? 1);
}
}
$restriction_error = self::check_coupon_restrictions($coupon, $cart, $subtotal_normal);
if ($restriction_error !== null) {
return new WP_REST_Response(['success' => false, 'message' => $restriction_error]);
}
$currency = get_option('wis_currency_name', 'Coins');
$msg = $coupon->type === 'percent'
? "Gutschein gültig (-{$coupon->value}%)"
: "Gutschein gültig (-{$coupon->value} {$currency})";
return new WP_REST_Response([
'success' => true,
'type' => $coupon->type,
'value' => $coupon->value,
'message' => $msg
]);
}
}
// ===========================================================
// SHORTCODE - FRONTEND SHOP
// ===========================================================
class WIS_Shortcode {
public static function register() {
add_shortcode('ingame_shop_form', [self::class, 'render']);
}
public static function render() {
$servers = WIS_DB::get_servers();
$categories = WIS_DB::get_categories();
$currency = get_option('wis_currency_name', 'Coins');
$header_text = get_option('wis_header_text', '');
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
$first_category = !empty($categories) ? $categories[0]->slug : '';
ob_start();
?>
<style>
.wis-shop { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 1400px; margin: 40px auto; background: #f4f6f8; padding: 20px; border-radius: 10px; position: relative; }
.wis-header { text-align: center; margin-bottom: 30px; }
.wis-header h2 { color: #333; margin-bottom: 10px; }
.wis-status { background: #d4edda; color: #155724; padding: 15px; border-radius: 8px; border-left: 4px solid #c3e6cb; margin-bottom: 20px; font-weight: 500; }
.wis-control-bar { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 30px; align-items: center; justify-content: space-between; background: #fff; padding: 15px; border-radius: 8px; border: 1px solid #ddd; }
.wis-search-input { padding: 10px 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; width: 100%; max-width: 300px; }
.wis-filter-select { padding: 12px 20px; font-size: 16px; border: 2px solid #ddd; border-radius: 8px; background: white; cursor: pointer; min-width: 200px; }
.wis-cart-btn { padding: 12px 30px; background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border: none; border-radius: 8px; font-weight: bold; font-size: 1rem; cursor: pointer; transition: all 0.3s; position: relative; display: flex; align-items: center; gap: 10px; }
.wis-cart-btn:hover { transform: scale(1.05); box-shadow: 0 5px 15px rgba(40, 167, 69, 0.4); }
.wis-cart-badge { position: absolute; top: -8px; right: -8px; background: #dc3545; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; font-weight: bold; }
.wis-cat-tabs { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; justify-content: center; }
.wis-cat-btn { padding: 8px 16px; border: 1px solid #ddd; background: #fff; border-radius: 20px; cursor: pointer; transition: all 0.2s; }
.wis-cat-btn:hover, .wis-cat-btn.active { background: #667eea; color: white; border-color: #667eea; }
.wis-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 320px)); gap: 15px; justify-content: center; margin: 0 auto; min-height: 200px; }
.wis-subgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 320px)); gap: 15px; justify-content: center; margin: 0 auto 20px; }
.wis-subgroup-divider { display: flex; align-items: center; gap: 16px; margin: 28px 0 14px; }
.wis-sub-hr { flex: 1; border: none; border-top: 2px solid #333; margin: 0; }
.wis-sub-label { font-size: 1rem; font-weight: 800; color: #222; white-space: nowrap; }
.wis-card { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s; display: flex; flex-direction: column; border: 1px solid #eee; position: relative; width: 100%; }
.wis-card.offer { border: 2px solid #ffc107; }
.wis-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
.wis-card-img { width: 100%; height: 180px; background: #2d2d2d; display: flex; align-items: center; justify-content: center; position: relative; padding: 20px; }
.wis-card-img img { width: 160px !important; height: 160px !important; object-fit: contain; filter: drop-shadow(0 6px 8px rgba(0,0,0,0.7)); transition: transform 0.3s; image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges; }
.wis-card:hover .wis-card-img img { transform: scale(1.15) rotate(5deg); }
.wis-card-img--custom { padding: 0 !important; overflow: hidden; height: 180px !important; display: flex !important; align-items: center !important; justify-content: center !important; background: #2d2d2d; }
.wis-card-img--custom img { position: static !important; width: 100% !important; height: 100% !important; object-fit: contain !important; image-rendering: auto !important; filter: none !important; border-radius: 0; display: block !important; transition: transform 0.3s; }
.wis-card:hover .wis-card-img--custom img { transform: scale(1.05) !important; }
.wis-offer-badge, .wis-daily-badge { position: absolute; top: 10px; left: 10px; background: linear-gradient(135deg, #ff416c, #ff4b2b); color: white; padding: 6px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); z-index: 2; }
.wis-daily-badge { background: linear-gradient(135deg, #6f42c1, #8e44ad); border: 1px solid #fff; }
.wis-card-body { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; }
.wis-card-title { font-size: 1.1rem; font-weight: 700; margin: 0 0 10px 0; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wis-card-desc { font-size: 0.85rem; color: #666; margin-bottom: 10px; line-height: 1.4; height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.wis-card-price-container { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.wis-card-price { font-size: 1.2rem; color: #e67e22; font-weight: 800; }
.wis-card-price-old { font-size: 0.9rem; color: #999; text-decoration: line-through; }
.wis-card-servers { font-size: 0.85rem; color: #666; margin-bottom: 15px; }
.wis-quantity-control { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; }
.wis-quantity-btn { width: 35px; height: 35px; border: 2px solid #667eea; background: white; color: #667eea; border-radius: 6px; font-weight: bold; cursor: pointer; transition: all 0.2s; }
.wis-quantity-btn:hover { background: #667eea; color: white; }
.wis-quantity-input { width: 60px; text-align: center; border: 2px solid #ddd; border-radius: 6px; padding: 8px; font-weight: bold; }
.wis-btn-add { margin-top: auto; width: 100%; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: all 0.3s; }
.wis-btn-add:hover { transform: scale(1.05); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); }
.wis-pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin: 30px 0 10px; flex-wrap: wrap; }
.wis-per-page-bar { display: flex; justify-content: flex-end; margin: 0 0 20px; }
.wis-page-btn { padding: 8px 14px; border: 2px solid #ddd; background: #fff; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; color: #333; }
.wis-page-btn:hover { border-color: #667eea; color: #667eea; }
.wis-page-btn.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-color: #667eea; color: #fff; }
.wis-page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.wis-page-info { font-size: 14px; color: #666; margin: 0 8px; }
.wis-loading { text-align: center; padding: 60px 20px; color: #888; font-size: 1.1rem; }
.wis-loading-spinner { display: inline-block; width: 36px; height: 36px; border: 4px solid #ddd; border-top-color: #667eea; border-radius: 50%; animation: wis-spin 0.8s linear infinite; margin-bottom: 12px; }
@keyframes wis-spin { to { transform: rotate(360deg); } }
/* ── CART DRAWER ─────────────────────────────────────────── */
.wis-drawer-overlay { position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2147483640;display:none;backdrop-filter:blur(2px); }
.wis-drawer-overlay.open { display:block; }
.wis-drawer { position:fixed;top:0;right:0;width:100%;max-width:480px;height:100%;background:#fff;z-index:2147483641;display:flex;flex-direction:column;box-shadow:-8px 0 40px rgba(0,0,0,.25);transform:translateX(100%);transition:transform .35s cubic-bezier(.4,0,.2,1); }
.wis-drawer.open { transform:translateX(0); }
.wis-drawer-header { display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid #f0f0f0;flex-shrink:0;background:#fff; }
.wis-drawer-header h2 { margin:0;font-size:1.2rem;font-weight:700;color:#1a1a1a;display:flex;align-items:center;gap:8px; }
.wis-drawer-close { width:36px;height:36px;border:none;background:#f5f5f5;border-radius:50%;cursor:pointer;font-size:1rem;display:flex;align-items:center;justify-content:center;color:#555;transition:background .2s; }
.wis-drawer-close:hover { background:#e0e0e0; }
.wis-steps { display:flex;padding:16px 24px 0;flex-shrink:0; }
.wis-step { flex:1;text-align:center;font-size:.75rem;font-weight:600;color:#bbb;position:relative;padding-bottom:12px; }
.wis-step::after { content:'';position:absolute;bottom:0;left:0;width:100%;height:3px;background:#eee;border-radius:2px; }
.wis-step.active { color:#667eea; }
.wis-step.active::after { background:linear-gradient(90deg,#667eea,#764ba2); }
.wis-step.done { color:#28a745; }
.wis-step.done::after { background:#28a745; }
.wis-step-num { display:inline-flex;width:22px;height:22px;align-items:center;justify-content:center;border-radius:50%;background:#eee;color:#aaa;font-size:.7rem;font-weight:700;margin-bottom:4px; }
.wis-step.active .wis-step-num { background:linear-gradient(135deg,#667eea,#764ba2);color:#fff; }
.wis-step.done .wis-step-num { background:#28a745;color:#fff; }
.wis-drawer-body { flex:1;overflow-y:auto;padding:20px 24px; }
.wis-drawer-body::-webkit-scrollbar { width:5px; }
.wis-drawer-body::-webkit-scrollbar-thumb { background:#ddd;border-radius:3px; }
.wis-drawer-footer { padding:16px 24px;border-top:1px solid #f0f0f0;flex-shrink:0;background:#fff; }
.wis-cart-empty { text-align:center;padding:60px 20px;color:#bbb; }
.wis-cart-empty-icon { font-size:3.5rem;margin-bottom:12px; }
.wis-cart-empty p { font-size:.95rem;margin:0; }
.wis-ci { display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid #f5f5f5; }
.wis-ci:last-child { border-bottom:none; }
.wis-ci-img { width:56px;height:56px;border-radius:8px;object-fit:cover;background:#f5f5f5;flex-shrink:0; }
.wis-ci-info { flex:1;min-width:0; }
.wis-ci-name { font-weight:600;font-size:.9rem;color:#1a1a1a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.wis-ci-price { font-size:.82rem;color:#888;margin-top:2px; }
.wis-ci-price strong { color:#e67e22; }
.wis-ci-qty { display:flex;align-items:center;gap:6px;margin-top:6px; }
.wis-ci-qty-btn { width:26px;height:26px;border:1px solid #ddd;background:#fff;border-radius:6px;cursor:pointer;font-size:.9rem;font-weight:700;display:flex;align-items:center;justify-content:center;color:#555;transition:all .15s; }
.wis-ci-qty-btn:hover { background:#667eea;color:#fff;border-color:#667eea; }
.wis-ci-qty-val { min-width:28px;text-align:center;font-weight:700;font-size:.9rem;color:#333; }
.wis-ci-del { width:30px;height:30px;border:none;background:none;cursor:pointer;color:#ccc;font-size:1rem;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:all .15s;flex-shrink:0; }
.wis-ci-del:hover { background:#fff0f0;color:#dc3545; }
.wis-coupon-row { display:flex;gap:8px;margin-bottom:8px; }
.wis-coupon-row input { flex:1;padding:10px 14px;border:1.5px solid #e0e0e0;border-radius:8px;font-size:.9rem;outline:none;transition:border-color .2s;color:#333; }
.wis-coupon-row input:focus { border-color:#667eea; }
.wis-coupon-row button { padding:10px 16px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:8px;font-weight:600;cursor:pointer;font-size:.85rem;white-space:nowrap; }
.wis-coupon-msg { font-size:.82rem;min-height:18px;padding:0 2px; }
.wis-summary { background:#fafafa;border-radius:10px;padding:14px 16px;margin:16px 0 0; }
.wis-summary-row { display:flex;justify-content:space-between;align-items:center;font-size:.88rem;color:#555;padding:4px 0; }
.wis-summary-row.discount { color:#28a745; }
.wis-summary-row.tax { color:#e67e22; }
.wis-summary-divider { border:none;border-top:1px solid #e8e8e8;margin:8px 0; }
.wis-summary-total { display:flex;justify-content:space-between;align-items:center;padding-top:8px; }
.wis-summary-total span:first-child { font-size:.95rem;font-weight:700;color:#1a1a1a; }
.wis-summary-total span:last-child { font-size:1.3rem;font-weight:800;color:#667eea; }
.wis-form-section { margin-bottom:18px; }
.wis-form-label { display:block;font-size:.82rem;font-weight:600;color:#555;margin-bottom:6px;text-transform:uppercase;letter-spacing:.04em; }
.wis-form-input { width:100%;padding:12px 14px;border:1.5px solid #e0e0e0;border-radius:8px;font-size:.95rem;box-sizing:border-box;outline:none;transition:border-color .2s;color:#1a1a1a;background:#fff; }
.wis-form-input:focus { border-color:#667eea; }
.wis-form-select { -webkit-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center; }
.wis-gift-toggle { display:flex;align-items:center;gap:10px;cursor:pointer;font-weight:600;color:#6f42c1;user-select:none;padding:12px 14px;background:#f8f3ff;border-radius:8px;border:1.5px solid #e2d4f7; }
.wis-gift-toggle input[type=checkbox] { width:17px;height:17px;cursor:pointer;accent-color:#9b59b6;flex-shrink:0; }
.wis-gift-field { display:none;background:#f3eeff;border:1.5px solid #c39bd3;border-radius:8px;padding:14px 16px;margin-top:10px; }
.wis-gift-field label { display:block;font-weight:600;color:#6f42c1;margin-bottom:6px;font-size:.85rem; }
.wis-gift-input { width:100%;padding:10px 14px;border:1px solid #c39bd3;border-radius:6px;font-size:.95rem;box-sizing:border-box;background:#fff;outline:none; }
.wis-gift-hint { font-size:.78rem;color:#888;margin-top:6px; }
.wis-btn-primary { width:100%;padding:14px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none;border-radius:10px;font-weight:700;font-size:1rem;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px; }
.wis-btn-primary:hover:not(:disabled) { transform:translateY(-1px);box-shadow:0 6px 20px rgba(102,126,234,.4); }
.wis-btn-primary:disabled { opacity:.6;cursor:not-allowed; }
.wis-btn-secondary { width:100%;padding:12px;background:#f5f5f5;color:#555;border:none;border-radius:10px;font-weight:600;font-size:.9rem;cursor:pointer;transition:background .2s;margin-top:8px; }
.wis-btn-secondary:hover { background:#e8e8e8; }
.wis-alert { padding:12px 14px;border-radius:8px;font-size:.88rem;display:none;margin-top:10px; }
.wis-alert.success { background:#d4edda;color:#155724;border:1px solid #c3e6cb; }
.wis-alert.error { background:#f8d7da;color:#721c24;border:1px solid #f5c6cb; }
.wis-success-screen { text-align:center;padding:40px 20px; }
.wis-success-icon { font-size:4rem;margin-bottom:16px; }
.wis-success-screen h3 { margin:0 0 8px;font-size:1.2rem;color:#1a1a1a; }
.wis-success-screen p { color:#888;font-size:.9rem;margin:0; }
</style>
<div class="wis-shop">
<div class="wis-header">
<h2>🛒 Ingame Shop</h2>
<?php if (!empty(trim($header_text))): ?>
<div class="wis-status"><?php echo wp_kses_post($header_text); ?></div>
<?php endif; ?>
</div>
<div class="wis-control-bar">
<input type="text" id="wis-search" class="wis-search-input" placeholder="🔍 Suche Item...">
<select id="server-filter" class="wis-filter-select">
<option value="">Alle Server</option>
<?php foreach ($servers as $s): ?>
<option value="<?php echo esc_attr($s->slug); ?>"><?php echo esc_html($s->name); ?></option>
<?php endforeach; ?>
</select>
<button class="wis-cart-btn" id="wis-open-cart-btn">
🛒 Warenkorb
<span class="wis-cart-badge" id="cart-count">0</span>
</button>
</div>
<?php
// Kategorien hierarchisch aufbereiten für JS
$root_cats_fe = array_filter($categories, fn($c) => $c->parent_id == 0);
$sub_idx_fe = [];
foreach ($categories as $c) {
if ($c->parent_id != 0) $sub_idx_fe[$c->parent_id][] = $c;
}
$subcat_map_js = [];
foreach ($root_cats_fe as $rc) {
$subcat_map_js[$rc->slug] = !empty($sub_idx_fe[$rc->id])
? array_map(fn($sc) => ['slug' => $sc->slug, 'name' => $sc->name], $sub_idx_fe[$rc->id])
: [];
}
?>
<?php if (!empty($categories)): ?>
<div class="wis-cat-tabs">
<?php $is_first = true; ?>
<?php foreach ($root_cats_fe as $cat): ?>
<button class="wis-cat-btn <?php echo $is_first ? 'active' : ''; ?>"
data-cat="<?php echo esc_attr($cat->slug); ?>">
<?php echo esc_html($cat->name); ?>
</button>
<?php $is_first = false; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="wis-grid" id="wis-grid">
<div class="wis-loading" style="grid-column:1/-1;">
<div class="wis-loading-spinner"></div><br>Lade Items…
</div>
</div>
<div class="wis-pagination" id="wis-pagination"></div>
<div class="wis-per-page-bar">
<select id="wis-per-page" class="wis-filter-select" style="min-width:130px; padding:8px 12px; font-size:14px;">
<option value="25" <?php selected(get_option('wis_default_per_page','25'), '25'); ?>>25 pro Seite</option>
<option value="50" <?php selected(get_option('wis_default_per_page','25'), '50'); ?>>50 pro Seite</option>
<option value="100" <?php selected(get_option('wis_default_per_page','25'), '100'); ?>>100 pro Seite</option>
<option value="-1" <?php selected(get_option('wis_default_per_page','25'), '-1'); ?>>Alle anzeigen</option>
</select>
</div>
</div>
<!-- Warenkorb Drawer -->
<div class="wis-drawer-overlay" id="wis-drawer-overlay"></div>
<div class="wis-drawer" id="wis-drawer" role="dialog" aria-modal="true">
<div class="wis-drawer-header">
<h2>&#x1F6D2; <span id="wis-drawer-title">Warenkorb</span></h2>
<button class="wis-drawer-close" id="wis-drawer-close" aria-label="Schlie&szlig;en">&#x2715;</button>
</div>
<div class="wis-steps" id="wis-steps">
<div class="wis-step active" id="wis-step-1">
<div><div class="wis-step-num">1</div></div>
<div>Warenkorb</div>
</div>
<div class="wis-step" id="wis-step-2">
<div><div class="wis-step-num">2</div></div>
<div>Bestellung</div>
</div>
</div>
<div class="wis-drawer-body" id="wis-drawer-body">
<div id="wis-panel-cart">
<div id="cart-content"></div>
<div id="wis-coupon-section" style="display:none;margin-top:18px;">
<label class="wis-form-label">&#x1F3AB; Gutscheincode</label>
<div class="wis-coupon-row">
<input type="text" id="coupon-code" placeholder="CODE eingeben">
<button id="wis-coupon-btn">Einl&ouml;sen</button>
</div>
<div id="coupon-msg" class="wis-coupon-msg"></div>
</div>
<div id="wis-summary-section" style="display:none;">
<div class="wis-summary">
<div class="wis-summary-row" id="wis-row-subtotal" style="display:none;">
<span>Zwischensumme</span><span id="cart-subtotal"></span>
</div>
<div class="wis-summary-row discount" id="wis-row-discount" style="display:none;">
<span>&#x2702;&#xFE0F; Rabatt</span><span id="cart-discount"></span>
</div>
<div class="wis-summary-row tax" id="wis-row-tax" style="display:none;">
<span>&#x1F4B0; Steuer (<?php echo esc_attr(get_option('wis_tax_rate','0')); ?>%)</span><span id="cart-tax"></span>
</div>
<hr class="wis-summary-divider">
<div class="wis-summary-total">
<span>Gesamt</span>
<span id="cart-total">0 <?php echo esc_html($currency); ?></span>
</div>
</div>
</div>
</div>
<div id="wis-panel-checkout" style="display:none;">
<div class="wis-form-section">
<label class="wis-form-label">&#x1F5A5;&#xFE0F; Server</label>
<select id="checkout-server" class="wis-form-input wis-form-select">
<option value="">-- Server w&auml;hlen --</option>
<?php foreach ($servers as $s): ?>
<option value="<?php echo esc_attr($s->slug); ?>"><?php echo esc_html($s->name); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="wis-form-section">
<label class="wis-form-label">&#x1F464; Dein Spielername</label>
<input type="text" id="checkout-player" class="wis-form-input" placeholder="z.B. Steve123" autocomplete="off">
</div>
<div class="wis-form-section">
<label class="wis-gift-toggle">
<input type="checkbox" id="wis-gift-toggle">
&#x1F381; Als Geschenk f&uuml;r einen anderen Spieler
</label>
<div class="wis-gift-field" id="wis-gift-field">
<label for="wis-gift-recipient">Empf&auml;nger-Spielername</label>
<input type="text" id="wis-gift-recipient" class="wis-gift-input" placeholder="z.B. Alex456" autocomplete="off">
<div class="wis-gift-hint">&#x26A0;&#xFE0F; Der Empf&auml;nger muss das Geschenk ingame annehmen. Bei Ablehnung erh&auml;ltst du dein Geld zur&uuml;ck.</div>
</div>
</div>
<div class="wis-summary" style="margin-bottom:4px;">
<div class="wis-summary-total">
<span>Zu zahlen</span>
<span id="checkout-total-display"></span>
</div>
</div>
<div id="cart-alert" class="wis-alert"></div>
</div>
<div id="wis-panel-success" style="display:none;">
<div class="wis-success-screen">
<div class="wis-success-icon">&#x1F389;</div>
<h3 id="wis-success-msg">Bestellung erfolgreich!</h3>
<p id="wis-success-sub">Deine Items werden ingame bereitgestellt.</p>
</div>
</div>
</div>
<div class="wis-drawer-footer" id="wis-drawer-footer">
<button class="wis-btn-primary" id="wis-footer-btn">Weiter zur Bestellung &#x2192;</button>
<button class="wis-btn-secondary" id="wis-footer-back" style="display:none;">&#x2190; Zur&uuml;ck zum Warenkorb</button>
</div>
</div>
<script>
(function() {
const shopCurrency = <?php echo json_encode($currency); ?>;
const shopExcludeOffers = <?php echo $exclude_offers === '1' ? 'true' : 'false'; ?>;
const shopTaxEnabled = <?php echo get_option('wis_tax_enabled', '0') === '1' ? 'true' : 'false'; ?>;
const shopTaxRate = <?php echo floatval(get_option('wis_tax_rate', '0')); ?>;
const apiBase = <?php echo json_encode(rest_url('wis/v1')); ?>;
const serverList = <?php echo json_encode(array_map(function($s){ return ['slug'=>$s->slug,'name'=>$s->name]; }, $servers)); ?>;
const subcatMap = <?php echo json_encode($subcat_map_js); ?>;
let cart = [];
let couponData = {};
let currentCat = <?php echo json_encode($first_category); ?>;
let currentSearch = '';
let currentPage = 1;
let totalPages = 1;
let searchTimer = null;
let allItems = [];
let itemMap = {}; // id (int) -> item object
let currentPerPage = <?php echo intval(get_option('wis_default_per_page', 25)); ?>;
// -------------------------------------------------------
// INIT
// -------------------------------------------------------
function init() {
// Drawer an <body> hängen damit z-index korrekt greift
var drawer = document.getElementById('wis-drawer');
var overlay = document.getElementById('wis-drawer-overlay');
if (drawer && document.body) document.body.appendChild(drawer);
if (overlay && document.body) document.body.appendChild(overlay);
// Erste Items laden
loadItems(1);
// ---- Event Delegation: Warenkorb-Buttons im Grid ----
document.getElementById('wis-grid').addEventListener('click', function(e) {
// "In den Warenkorb" Button
const addBtn = e.target.closest('.wis-btn-add');
if (addBtn) {
const card = addBtn.closest('.wis-card');
const id = parseInt(addBtn.getAttribute('data-item-id'), 10);
const item = itemMap[id];
if (!item || !card) return;
const qtyInput = card.querySelector('.wis-quantity-input');
const qty = parseInt(qtyInput ? qtyInput.value : '1', 10) || 1;
addToCartItem(item, qty, addBtn, card);
return;
}
// Mengen-Buttons
const qtyBtn = e.target.closest('.wis-quantity-btn');
if (qtyBtn) {
const delta = parseInt(qtyBtn.getAttribute('data-delta'), 10);
const ctrl = qtyBtn.closest('.wis-quantity-control');
if (!ctrl) return;
const input = ctrl.querySelector('.wis-quantity-input');
if (input) input.value = Math.max(1, Math.min(999, (parseInt(input.value, 10) || 1) + delta));
}
});
// ---- Kategorie-Tabs ----
document.querySelectorAll('.wis-cat-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('.wis-cat-btn').forEach(function(b){ b.classList.remove('active'); });
btn.classList.add('active');
currentCat = btn.getAttribute('data-cat');
currentPage = 1;
// Suche beim Kategoriewechsel leeren
currentSearch = '';
var searchInput = document.getElementById('wis-search');
if (searchInput) searchInput.value = '';
loadItems(1);
});
});
// ---- Suche ----
var searchInput = document.getElementById('wis-search');
if (searchInput) {
searchInput.addEventListener('input', function() {
clearTimeout(searchTimer);
searchTimer = setTimeout(function() {
currentSearch = searchInput.value.trim();
currentPage = 1;
// Bei aktiver Suche: Kategorie-Filter deaktivieren (kategorienübergreifend suchen)
if (currentSearch) {
document.querySelectorAll('.wis-cat-btn').forEach(function(b){ b.classList.remove('active'); });
currentCat = '';
} else {
// Suche geleert: erste Kategorie wieder aktivieren
var firstBtn = document.querySelector('.wis-cat-btn');
if (firstBtn) {
firstBtn.classList.add('active');
currentCat = firstBtn.getAttribute('data-cat');
}
}
loadItems(1);
}, 350);
});
}
// ---- Server-Filter ----
var serverSelect = document.getElementById('server-filter');
if (serverSelect) {
serverSelect.addEventListener('change', function() {
renderGrid(allItems);
});
}
// ---- Per-Page-Selector ----
var perPageSelect = document.getElementById('wis-per-page');
if (perPageSelect) {
perPageSelect.value = String(currentPerPage);
perPageSelect.addEventListener('change', function() {
currentPerPage = parseInt(perPageSelect.value, 10);
currentPage = 1;
loadItems(1);
});
}
// ---- Warenkorb öffnen ----
var openBtn = document.getElementById('wis-open-cart-btn');
if (openBtn) openBtn.addEventListener('click', openCart);
// ---- Modal-Overlay klick ----
// ---- Drawer Events ----
var drawerOverlay = document.getElementById('wis-drawer-overlay');
var drawerClose = document.getElementById('wis-drawer-close');
var footerBtn = document.getElementById('wis-footer-btn');
var footerBack = document.getElementById('wis-footer-back');
if (drawerOverlay) drawerOverlay.addEventListener('click', closeCart);
if (drawerClose) drawerClose.addEventListener('click', closeCart);
if (footerBtn) footerBtn.addEventListener('click', onFooterBtn);
if (footerBack) footerBack.addEventListener('click', goBackToCart);
// ---- Gutschein einlösen ----
var couponBtn = document.getElementById('wis-coupon-btn');
if (couponBtn) couponBtn.addEventListener('click', validateCoupon);
// ---- Gift toggle ----
var giftToggleInit = document.getElementById('wis-gift-toggle');
var giftFieldInit = document.getElementById('wis-gift-field');
if (giftToggleInit) giftToggleInit.addEventListener('change', function() {
if (giftFieldInit) giftFieldInit.style.display = this.checked ? 'block' : 'none';
});
// ---- Gift-Toggle ----
var giftToggle = document.getElementById('wis-gift-toggle');
var giftField = document.getElementById('wis-gift-field');
if (giftToggle && giftField) {
giftToggle.addEventListener('change', function() {
giftField.style.display = giftToggle.checked ? 'block' : 'none';
if (!giftToggle.checked) {
document.getElementById('wis-gift-recipient').value = '';
}
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// -------------------------------------------------------
// ITEMS LADEN
// -------------------------------------------------------
function loadItems(page) {
currentPage = page;
var grid = document.getElementById('wis-grid');
grid.innerHTML = '<div class="wis-loading" style="grid-column:1/-1;"><div class="wis-loading-spinner"></div><br>Lade Items…</div>';
document.getElementById('wis-pagination').innerHTML = '';
var url = apiBase + '/shop_items?page=' + page;
if (currentCat) url += '&category=' + encodeURIComponent(currentCat);
if (currentSearch) url += '&search=' + encodeURIComponent(currentSearch);
url += '&per_page=' + currentPerPage;
fetch(url)
.then(function(r){ return r.json(); })
.then(function(data) {
allItems = data.items || [];
totalPages = data.total_pages || 1;
itemMap = {};
allItems.forEach(function(i){ itemMap[i.id] = i; });
renderGrid(allItems);
renderPagination(data.page, data.total_pages, data.total);
})
.catch(function() {
grid.innerHTML = '<div class="wis-loading" style="grid-column:1/-1; color:#dc3545;">Fehler beim Laden der Items.</div>';
});
}
// -------------------------------------------------------
// GRID RENDERN — bei Hauptkat mit Unterkats: gruppiert
// -------------------------------------------------------
function renderGrid(items) {
var grid = document.getElementById('wis-grid');
var serverSlug = document.getElementById('server-filter').value;
var filtered = serverSlug
? items.filter(function(i){ return i.servers.indexOf(serverSlug) !== -1; })
: items;
if (filtered.length === 0) {
grid.innerHTML = '<div style="grid-column:1/-1; text-align:center; padding:40px; background:#fff; border-radius:10px; color:#888;">Keine Items gefunden.</div>';
return;
}
// Prüfen ob aktive Kategorie eine Hauptkat mit Unterkats ist
var subs = subcatMap[currentCat] || [];
if (subs.length > 0) {
// Gruppierte Ansicht → wis-grid als normaler Block-Container nutzen
grid.style.display = 'block';
var subSlugs = subs.map(function(s){ return s.slug; });
var html = '';
// Items direkt in der Hauptkat (nicht in einer Sub)
var directItems = filtered.filter(function(item) {
return (item.categories || []).indexOf(currentCat) !== -1
&& !(item.categories || []).some(function(s){ return subSlugs.indexOf(s) !== -1; });
});
if (directItems.length > 0) {
html += '<div class="wis-subgrid">' + directItems.map(renderCard).join('') + '</div>';
}
// Unterkategorie-Blöcke mit Trennlinie
subs.forEach(function(sub) {
var subItems = filtered.filter(function(item) {
return (item.categories || []).indexOf(sub.slug) !== -1;
});
if (subItems.length === 0) return;
html += '<div class="wis-subgroup-divider">'
+ '<hr class="wis-sub-hr"><span class="wis-sub-label">' + escHtml(sub.name) + '</span><hr class="wis-sub-hr">'
+ '</div>'
+ '<div class="wis-subgrid">' + subItems.map(renderCard).join('') + '</div>';
});
grid.innerHTML = html || '<p style="text-align:center;padding:40px;color:#888;">Keine Items gefunden.</p>';
} else {
// Normale flache Grid-Ansicht
grid.style.display = '';
grid.innerHTML = filtered.map(renderCard).join('');
}
}
// Einzelne Karte rendern
function renderCard(item) {
var price = item.offer_price > 0 ? item.offer_price : item.price;
var showOld = item.offer_price > 0 && item.offer_price !== item.price;
var serverNames = (item.servers || [])
.map(function(slug) {
var s = serverList.find(function(x){ return x.slug === slug; });
return s ? s.name : slug;
})
.join(', ') || 'Kein Server';
var badge = item.is_daily_deal
? '<div class="wis-daily-badge">🎁 Angebot des Tages</div>'
: (item.is_offer ? '<div class="wis-offer-badge">🔥 Angebot</div>' : '');
var oldPrice = showOld
? '<div class="wis-card-price-old">' + item.price + ' ' + shopCurrency + '</div>'
: '';
var desc = item.description
? '<div class="wis-card-desc">' + escHtml(item.description) + '</div>'
: '';
return '<div class="wis-card' + (item.is_offer ? ' offer' : '') + '">'
+ badge
+ '<div class="wis-card-img' + (item.has_custom_image ? ' wis-card-img--custom' : '') + '">'
+ '<img src="' + escHtml(item.image) + '" alt="' + escHtml(item.name) + '" loading="lazy"'
+ ' onerror="this.onerror=null;this.src=\'https://via.placeholder.com/100/333/fff?text=?\';">'
+ '</div>'
+ '<div class="wis-card-body">'
+ '<h3 class="wis-card-title" title="' + escHtml(item.name) + '">' + escHtml(item.name) + '</h3>'
+ desc
+ (function() {
var gcP = (item.item_id || item.id || '').match(/^gift_card_(\d+)_(\d+)$/);
if (gcP) {
return '<div class="wis-card-price-container">'
+ '<div class="wis-card-price">' + parseInt(gcP[1],10) + ' ' + parseInt(gcP[2],10) + ' ' + shopCurrency + '</div>'
+ '</div>';
}
return '<div class="wis-card-price-container">'
+ oldPrice
+ '<div class="wis-card-price">' + price + ' ' + shopCurrency + '</div>'
+ '</div>';
})()
+ '<div class="wis-card-servers">📡 ' + escHtml(serverNames) + '</div>'
+ (function() {
var gcM = (item.item_id || item.id || '').match(/^gift_card_(\d+)_(\d+)$/);
if (gcM) {
var mn = parseInt(gcM[1], 10), mx = parseInt(gcM[2], 10);
return '<div style="margin:10px 0;">'
+ '<label style="font-size:12px;color:#aaa;display:block;margin-bottom:5px;">'
+ '💰 Betrag eingeben (' + mn + '' + mx + ' ' + shopCurrency + ')</label>'
+ '<input type="number" class="wis-gift-amount-input" '
+ 'value="' + mn + '" min="' + mn + '" max="' + mx + '" step="1" '
+ 'style="width:100%;padding:8px 10px;border-radius:6px;border:2px solid #00bcd4;'
+ 'background:#1a1a2e;color:#fff;font-size:15px;font-weight:700;box-sizing:border-box;'
+ '-moz-appearance:textfield;appearance:textfield;">'
+ '</div>';
}
return '<div class="wis-quantity-control">'
+ '<button class="wis-quantity-btn" data-delta="-1" type="button">-</button>'
+ '<input type="number" class="wis-quantity-input" value="1" min="1" max="999">'
+ '<button class="wis-quantity-btn" data-delta="1" type="button">+</button>'
+ '</div>';
})()
+ '<button class="wis-btn-add" data-item-id="' + item.id + '" type="button"> In den Warenkorb</button>'
+ '</div>'
+ '</div>';
}
// -------------------------------------------------------
// PAGINATION
// -------------------------------------------------------
function renderPagination(page, pages, total) {
var el = document.getElementById('wis-pagination');
if (pages <= 1) { el.innerHTML = ''; return; }
var html = '';
html += '<button class="wis-page-btn" ' + (page <= 1 ? 'disabled' : '') + ' data-page="' + (page - 1) + '"> Zurück</button>';
var start = Math.max(1, page - 2);
var end = Math.min(pages, page + 2);
if (start > 1) {
html += '<button class="wis-page-btn" data-page="1">1</button>';
if (start > 2) html += '<span style="color:#999">…</span>';
}
for (var p = start; p <= end; p++) {
html += '<button class="wis-page-btn' + (p === page ? ' active' : '') + '" data-page="' + p + '">' + p + '</button>';
}
if (end < pages) {
if (end < pages - 1) html += '<span style="color:#999">…</span>';
html += '<button class="wis-page-btn" data-page="' + pages + '">' + pages + '</button>';
}
html += '<button class="wis-page-btn" ' + (page >= pages ? 'disabled' : '') + ' data-page="' + (page + 1) + '">Weiter </button>';
html += '<span class="wis-page-info">' + total + ' Items</span>';
el.innerHTML = html;
// Pagination-Buttons via Event Delegation
el.querySelectorAll('.wis-page-btn:not([disabled])').forEach(function(btn) {
btn.addEventListener('click', function() {
var p = parseInt(btn.getAttribute('data-page'), 10);
if (p) {
loadItems(p);
document.querySelector('.wis-shop').scrollIntoView({behavior: 'smooth', block: 'start'});
}
});
});
}
// -------------------------------------------------------
// WARENKORB LOGIK
// -------------------------------------------------------
function addToCartItem(item, qty, btn, card) {
var price = item.offer_price > 0 ? item.offer_price : item.price;
// Gutschein-Karte: Betrag aus Eingabefeld lesen
var customAmount = 0;
var gcMatch = (item.item_id || item.id || '').match(/^gift_card_(\d+)_(\d+)$/);
if (gcMatch) {
var gcInput = card.querySelector('.wis-gift-amount-input');
var gcMin = parseInt(gcMatch[1], 10);
var gcMax = parseInt(gcMatch[2], 10);
customAmount = gcInput ? Math.max(gcMin, Math.min(gcMax, parseInt(gcInput.value, 10) || gcMin)) : gcMin;
price = customAmount; // Preis im Warenkorb = Wunschbetrag
}
// Gutschein-Karten nie zusammenlegen (jede hat eigenen Code)
var isGiftCard = !!gcMatch;
var existing = !isGiftCard && cart.find(function(i){ return i.id === item.id; });
if (existing) {
existing.quantity += qty;
} else {
cart.push({
id: item.id,
image: item.image || '',
name: item.name,
price: price,
quantity: qty,
servers: item.servers,
is_offer: !!item.is_offer,
custom_amount: customAmount || undefined
});
}
updateCartBadge();
btn.textContent = '✅ Hinzugefügt';
btn.style.background = '#28a745';
setTimeout(function() {
btn.textContent = ' In den Warenkorb';
btn.style.background = '';
}, 1500);
var qtyInput = card.querySelector('.wis-quantity-input');
if (qtyInput) qtyInput.value = 1;
}
function updateCartBadge() {
var total = cart.reduce(function(s, i){ return s + i.quantity; }, 0);
document.getElementById('cart-count').textContent = total;
}
var drawerStep = 1;
function openCart() {
goToStep(1);
renderCart();
document.getElementById('wis-drawer').classList.add('open');
document.getElementById('wis-drawer-overlay').classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeCart() {
document.getElementById('wis-drawer').classList.remove('open');
document.getElementById('wis-drawer-overlay').classList.remove('open');
document.body.style.overflow = '';
}
function goToStep(step) {
drawerStep = step;
var panelCart = document.getElementById('wis-panel-cart');
var panelCheckout = document.getElementById('wis-panel-checkout');
var panelSuccess = document.getElementById('wis-panel-success');
var step1El = document.getElementById('wis-step-1');
var step2El = document.getElementById('wis-step-2');
var footerBtn = document.getElementById('wis-footer-btn');
var footerBack = document.getElementById('wis-footer-back');
var drawerTitle = document.getElementById('wis-drawer-title');
var stepsEl = document.getElementById('wis-steps');
panelCart.style.display = 'none';
panelCheckout.style.display = 'none';
panelSuccess.style.display = 'none';
step1El.className = 'wis-step';
step2El.className = 'wis-step';
if (step === 1) {
panelCart.style.display = 'block';
step1El.className = 'wis-step active';
drawerTitle.textContent = 'Warenkorb';
stepsEl.style.display = 'flex';
footerBtn.style.display = 'block';
footerBack.style.display = 'none';
footerBtn.textContent = 'Weiter zur Bestellung →';
footerBtn.disabled = cart.length === 0;
} else if (step === 2) {
panelCheckout.style.display = 'block';
step1El.className = 'wis-step done';
step2El.className = 'wis-step active';
drawerTitle.textContent = 'Bestellung abschließen';
stepsEl.style.display = 'flex';
footerBtn.style.display = 'block';
footerBack.style.display = 'block';
footerBtn.textContent = '💰 Kauf abschließen';
footerBtn.disabled = false;
var ctd = document.getElementById('checkout-total-display');
if (ctd) ctd.textContent = calculateTotal() + ' ' + shopCurrency;
} else if (step === 3) {
panelSuccess.style.display = 'block';
stepsEl.style.display = 'none';
drawerTitle.textContent = 'Bestellung erfolgreich';
footerBtn.style.display = 'none';
footerBack.style.display = 'none';
}
}
function onFooterBtn() {
if (drawerStep === 1) {
if (cart.length === 0) return;
goToStep(2);
} else if (drawerStep === 2) {
checkout();
}
}
function goBackToCart() { goToStep(1); }
function calculateSubtotal() {
var normal = 0, offer = 0;
cart.forEach(function(item) {
var t = item.price * item.quantity;
if (shopExcludeOffers && item.is_offer) offer += t; else normal += t;
});
var discount = 0;
if (couponData && typeof couponData.value !== 'undefined') {
discount = couponData.type === 'percent'
? Math.floor(normal * couponData.value / 100)
: couponData.value;
}
return Math.max(0, normal - discount) + offer;
}
function calculateTax(subtotal) {
if (!shopTaxEnabled || shopTaxRate <= 0) return 0;
return Math.floor(subtotal * shopTaxRate / 100);
}
function calculateTotal() {
var subtotal = calculateSubtotal();
return subtotal + calculateTax(subtotal);
}
function renderCart() {
var content = document.getElementById('cart-content');
var couponSec = document.getElementById('wis-coupon-section');
var summarySec = document.getElementById('wis-summary-section');
var footerBtn = document.getElementById('wis-footer-btn');
if (cart.length === 0) {
content.innerHTML = '<div class="wis-cart-empty"><div class="wis-cart-empty-icon">🛒</div><p>Dein Warenkorb ist leer</p></div>';
if (couponSec) couponSec.style.display = 'none';
if (summarySec) summarySec.style.display = 'none';
if (footerBtn) footerBtn.disabled = true;
return;
}
if (footerBtn) footerBtn.disabled = false;
content.innerHTML = cart.map(function(item, idx) {
var linePrice = item.price * item.quantity;
return '<div class="wis-ci">'
+ '<img class="wis-ci-img" src="' + escHtml(item.image || '') + '" alt="" onerror="this.src=\'https://via.placeholder.com/56/eee/999?text=?\'">'
+ '<div class="wis-ci-info">'
+ '<div class="wis-ci-name" title="' + escHtml(item.name) + '">' + escHtml(item.name) + '</div>'
+ '<div class="wis-ci-price">' + item.price + ' ' + shopCurrency + ' / Stück &nbsp;&nbsp; <strong>' + linePrice + ' ' + shopCurrency + '</strong></div>'
+ '<div class="wis-ci-qty">'
+ '<button class="wis-ci-qty-btn" data-idx="' + idx + '" data-delta="-1" type="button"></button>'
+ '<span class="wis-ci-qty-val">' + item.quantity + '</span>'
+ '<button class="wis-ci-qty-btn" data-idx="' + idx + '" data-delta="1" type="button">+</button>'
+ '</div>'
+ '</div>'
+ '<button class="wis-ci-del" data-cart-idx="' + idx + '" type="button" title="Entfernen">🗑️</button>'
+ '</div>';
}).join('');
// Qty +/- buttons
content.querySelectorAll('.wis-ci-qty-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var idx = parseInt(btn.getAttribute('data-idx'), 10);
var delta = parseInt(btn.getAttribute('data-delta'), 10);
cart[idx].quantity = Math.max(1, Math.min(999, cart[idx].quantity + delta));
updateCartBadge();
renderCart();
});
});
// Remove buttons
content.querySelectorAll('.wis-ci-del').forEach(function(btn) {
btn.addEventListener('click', function() {
var idx = parseInt(btn.getAttribute('data-cart-idx'), 10);
cart.splice(idx, 1);
updateCartBadge();
renderCart();
});
});
// Show coupon + summary
if (couponSec) couponSec.style.display = 'block';
if (summarySec) summarySec.style.display = 'block';
// Update summary rows
var sub = calculateSubtotal();
var tax = calculateTax(sub);
var total = sub + tax;
var hasDiscount = couponData && typeof couponData.value !== 'undefined';
var discountAmt = 0;
if (hasDiscount) {
var raw = cart.reduce(function(s,i){ return s + i.price*i.quantity; }, 0);
discountAmt = couponData.type === 'percent' ? Math.floor(raw * couponData.value / 100) : couponData.value;
}
var rowSubtotal = document.getElementById('wis-row-subtotal');
var rowDiscount = document.getElementById('wis-row-discount');
var rowTax = document.getElementById('wis-row-tax');
var cartSubtotal = document.getElementById('cart-subtotal');
var cartDiscount = document.getElementById('cart-discount');
var cartTaxEl = document.getElementById('cart-tax');
var cartTotalEl = document.getElementById('cart-total');
if (rowSubtotal && (hasDiscount || (shopTaxEnabled && tax > 0))) {
rowSubtotal.style.display = 'flex'; cartSubtotal.textContent = sub + discountAmt + ' ' + shopCurrency;
} else if (rowSubtotal) { rowSubtotal.style.display = 'none'; }
if (rowDiscount && hasDiscount && discountAmt > 0) {
rowDiscount.style.display = 'flex'; cartDiscount.textContent = '' + discountAmt + ' ' + shopCurrency;
} else if (rowDiscount) { rowDiscount.style.display = 'none'; }
if (rowTax && shopTaxEnabled && tax > 0) {
rowTax.style.display = 'flex'; cartTaxEl.textContent = '+' + tax + ' ' + shopCurrency;
} else if (rowTax) { rowTax.style.display = 'none'; }
if (cartTotalEl) cartTotalEl.textContent = total + ' ' + shopCurrency;
}
function validateCoupon() {
var code = document.getElementById('coupon-code').value.trim().toUpperCase();
var msgEl = document.getElementById('coupon-msg');
if (!code) { couponData = {}; msgEl.textContent = ''; renderCart(); return; }
msgEl.textContent = 'Prüfe…';
msgEl.style.color = '#0073aa';
fetch(apiBase + '/validate_coupon', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code: code, cart: cart, player: document.getElementById('checkout-player') ? document.getElementById('checkout-player').value.trim() : ''})
})
.then(function(r){ return r.json(); })
.then(function(data) {
if (data.success) {
couponData = {type: data.type, value: data.value};
msgEl.textContent = '✅ ' + data.message;
msgEl.style.color = 'green';
} else {
couponData = {};
msgEl.textContent = '❌ ' + data.message;
msgEl.style.color = 'red';
}
renderCart();
});
}
function checkout() {
var player = document.getElementById('checkout-player').value.trim();
var server = document.getElementById('checkout-server').value;
var couponCode = document.getElementById('coupon-code').value.trim();
var alertEl = document.getElementById('cart-alert');
var btn = document.getElementById('wis-footer-btn');
// Gift-Feld auslesen
var giftToggle = document.getElementById('wis-gift-toggle');
var giftRecipient = '';
if (giftToggle && giftToggle.checked) {
giftRecipient = (document.getElementById('wis-gift-recipient').value || '').trim();
}
if (!player) { alertEl.textContent = 'Bitte Spielername eingeben!'; alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return; }
if (!server) { alertEl.textContent = 'Bitte Server auswählen!'; alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return; }
// Gift-Validierungen im Frontend
if (giftToggle && giftToggle.checked) {
if (!giftRecipient) {
alertEl.textContent = '🎁 Bitte den Empfänger-Spielernamen eingeben!';
alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return;
}
if (giftRecipient.toLowerCase() === player.toLowerCase()) {
alertEl.textContent = '🎁 Du kannst dir nicht selbst etwas schenken!';
alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return;
}
}
var invalid = cart.filter(function(i){ return i.servers.indexOf(server) === -1; });
if (invalid.length) { alertEl.textContent = 'Einige Items sind nicht für diesen Server!'; alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return; }
btn.disabled = true;
btn.textContent = '⏳ Speichere…';
function doOrder(confirmedNoCoupon) {
// cart mit custom_amount für Gutschein-Karten serialisieren
var cartPayload = cart.map(function(item) {
var c = {id: item.id, quantity: item.quantity};
if (item.custom_amount) c.custom_amount = item.custom_amount;
return c;
});
var payload = {player: player, cart: cartPayload, server: server, coupon_code: couponCode};
if (giftRecipient) payload.gift_recipient = giftRecipient;
if (confirmedNoCoupon) payload.confirmed_no_coupon = true;
fetch(apiBase + '/order', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(function(r){ return r.json(); })
.then(function(data) {
if (data.coupon_error) {
// Gutschein ungültig Bestätigung einholen ob zum vollen Preis kaufen
btn.disabled = false;
btn.textContent = '💰 Kauf abschließen';
// Bestätigungs-Overlay einblenden
var overlay = document.getElementById('wis-coupon-confirm-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'wis-coupon-confirm-overlay';
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:2147483647;display:flex;align-items:center;justify-content:center;';
overlay.innerHTML = '<div style="background:#fff;border-radius:14px;padding:32px 28px;max-width:420px;width:90%;text-align:center;box-shadow:0 8px 40px rgba(0,0,0,.25);">'
+ '<div style="font-size:2rem;margin-bottom:12px;">⚠️</div>'
+ '<h3 style="margin:0 0 10px;font-size:1.1rem;color:#e53935;font-weight:700;" id="wis-coupon-confirm-msg"></h3>'
+ '<p style="color:#666;font-size:.9rem;margin:0 0 8px;">Möchtest du den Kauf trotzdem <strong>zum vollen Preis</strong> abschließen?</p>'
+ '<p style="color:#333;font-size:1rem;font-weight:700;margin:0 0 22px;">Gesamtbetrag: ' + (function(){ var saved = couponData; couponData = {}; var t = calculateTotal(); couponData = saved; return t; })() + ' ' + shopCurrency + '</p>'
+ '<div style="display:flex;gap:12px;justify-content:center;">'
+ '<button id="wis-coupon-confirm-yes" style="background:#667eea;color:#fff;border:none;border-radius:8px;padding:10px 22px;font-size:.95rem;font-weight:600;cursor:pointer;">Ja, zum vollen Preis kaufen</button>'
+ '<button id="wis-coupon-confirm-no" style="background:#eee;color:#333;border:none;border-radius:8px;padding:10px 22px;font-size:.95rem;cursor:pointer;">Abbrechen</button>'
+ '</div></div>';
document.body.appendChild(overlay);
document.getElementById('wis-coupon-confirm-yes').addEventListener('click', function() {
overlay.remove();
btn.disabled = true;
btn.textContent = '⏳ Speichere…'; btn.disabled = true;
doOrder(true);
});
document.getElementById('wis-coupon-confirm-no').addEventListener('click', function() {
overlay.remove();
});
}
document.getElementById('wis-coupon-confirm-msg').textContent = data.message;
overlay.style.display = 'flex';
return;
}
if (data.success) {
cart = []; couponData = {};
if (giftToggle) { giftToggle.checked = false; }
var giftFieldEl = document.getElementById('wis-gift-field');
if (giftFieldEl) giftFieldEl.style.display = 'none';
document.getElementById('wis-gift-recipient').value = '';
updateCartBadge();
var msgEl = document.getElementById('wis-success-msg');
var subEl = document.getElementById('wis-success-sub');
if (msgEl) msgEl.textContent = data.message || 'Bestellung erfolgreich!';
// Gift-Card-Codes werden erst nach Kaufabschluss (complete_order) ingame zugesendet
// Sie werden hier NICHT angezeigt der Spieler erhält sie via Spigot-Nachricht
if (subEl) {
subEl.textContent = 'Deine Items werden ingame bereitgestellt.';
}
goToStep(3);
setTimeout(function(){ closeCart(); location.reload(); }, 3500);
} else {
alertEl.textContent = data.message;
alertEl.className = 'wis-alert error';
alertEl.style.display = 'block';
}
})
.finally(function() {
btn.disabled = false;
btn.textContent = '💰 Kauf abschließen';
});
}
doOrder(false);
}
// -------------------------------------------------------
// HILFSFUNKTIONEN
// -------------------------------------------------------
function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
})();
</script>
<?php
return ob_get_clean();
}
}
// ===========================================================
// SIDEBAR WIDGET
// ===========================================================
class WIS_Sidebar_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'wis_sidebar_offer',
'WIS Shop Angebot',
['description' => 'Zeigt Daily Deal oder Angebot an']
);
}
public function widget($args, $instance) {
echo $args['before_widget'];
if (!empty($instance['title'])) {
echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title'];
}
global $wpdb;
$currency = get_option('wis_currency_name', 'Coins');
$img_base = get_option('wis_image_base_url', '');
$item = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}wis_items WHERE is_daily_deal = 1 AND status = 'publish' LIMIT 1");
if (!$item) {
$item = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}wis_items WHERE is_offer = 1 AND status = 'publish' ORDER BY updated_at DESC LIMIT 1");
}
if ($item) {
$price = $item->offer_price > 0 ? $item->offer_price : $item->price;
$show_old = $item->offer_price > 0 && $item->offer_price != $item->price;
$img_url = WIS_DB::get_item_image($item);
$target_url = !empty($instance['shop_url']) ? $instance['shop_url'] : home_url();
?>
<div style="background:#fff; border-radius:10px; padding:0; overflow:hidden; box-shadow:0 4px 10px rgba(0,0,0,0.05); text-align:center;">
<div style="position:relative; background:#2d2d2d; padding:15px; height:160px; display:flex; align-items:center; justify-content:center;">
<?php if ($item->is_daily_deal): ?>
<div style="position:absolute; top:10px; left:10px; background:linear-gradient(135deg, #6f42c1, #8e44ad); color:#fff; padding:4px 10px; font-size:10px; border-radius:20px; font-weight:bold; z-index:2;">🎁 DAILY DEAL</div>
<?php elseif ($item->is_offer): ?>
<div style="position:absolute; top:10px; left:10px; background:linear-gradient(135deg, #ff416c, #ff4b2b); color:#fff; padding:4px 10px; font-size:10px; border-radius:20px; font-weight:bold; z-index:2;">🔥 ANGEBOT</div>
<?php endif; ?>
<img src="<?php echo esc_url($img_url); ?>" alt="<?php echo esc_attr($item->name); ?>" style="max-width:90%; max-height:90%; object-fit:contain; filter:drop-shadow(0 4px 8px rgba(0,0,0,0.6));">
</div>
<div style="padding:15px;">
<h4 style="margin:0 0 8px 0; font-size:15px; color:#333; font-weight:700;"><?php echo esc_html($item->name); ?></h4>
<div style="margin-bottom:12px;">
<?php if ($show_old): ?>
<span style="text-decoration:line-through; color:#999; font-size:11px;"><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></span>
<?php endif; ?>
<span style="font-size:20px; font-weight:800; color:#28a745;"><?php echo esc_html($price); ?> <span style="font-size:12px; font-weight:400; color:#666;"><?php echo esc_html($currency); ?></span></span>
</div>
<a href="<?php echo esc_url($target_url); ?>" style="display:block; padding:10px 0; background:linear-gradient(135deg, #667eea 0%, #764ba2 100%); color:#fff; text-decoration:none; border-radius:6px; font-weight:bold; font-size:13px;">
<?php echo esc_html($instance['btn_text'] ?? 'Zum Shop'); ?> 🛒
</a>
</div>
</div>
<?php
} else {
echo '<div style="background:#f8f9fa; border:1px dashed #ddd; border-radius:8px; padding:20px; text-align:center;"><p style="margin:0; color:#888; font-size:13px;">Kein Angebot verfügbar.</p></div>';
}
echo $args['after_widget'];
}
public function form($instance) {
$title = !empty($instance['title']) ? $instance['title'] : '🔥 Angebot des Tages';
$btn_text = !empty($instance['btn_text']) ? $instance['btn_text'] : 'Zum Shop';
$shop_url = !empty($instance['shop_url']) ? $instance['shop_url'] : '';
?>
<p>
<label for="<?php echo $this->get_field_id('title'); ?>">Titel:</label>
<input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo esc_attr($title); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id('btn_text'); ?>">Button Text:</label>
<input class="widefat" id="<?php echo $this->get_field_id('btn_text'); ?>" name="<?php echo $this->get_field_name('btn_text'); ?>" type="text" value="<?php echo esc_attr($btn_text); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id('shop_url'); ?>">Shop URL:</label>
<input class="widefat" id="<?php echo $this->get_field_id('shop_url'); ?>" name="<?php echo $this->get_field_name('shop_url'); ?>" type="text" value="<?php echo esc_attr($shop_url); ?>">
</p>
<?php
}
public function update($new_instance, $old_instance) {
$instance = [];
$instance['title'] = !empty($new_instance['title']) ? sanitize_text_field($new_instance['title']) : '';
$instance['btn_text'] = !empty($new_instance['btn_text']) ? sanitize_text_field($new_instance['btn_text']) : 'Zum Shop';
$instance['shop_url'] = !empty($new_instance['shop_url']) ? esc_url_raw($new_instance['shop_url']) : '';
return $instance;
}
}
// ===========================================================
// INITIALIZATION
// ===========================================================
register_activation_hook(__FILE__, [WIS_Activator::class, 'activate']);
register_deactivation_hook(__FILE__, [WIS_Activator::class, 'deactivate']);
add_action('admin_menu', [WIS_Admin::class, 'register_menu']);
add_action('admin_footer', function() {
$screen = get_current_screen();
if (!str_contains($screen->id ?? '', 'wis_')) return;
echo '<script>
document.addEventListener("wheel", function(e) {
if (document.activeElement && document.activeElement.type === "number") {
document.activeElement.blur();
}
}, { passive: false });
</script>';
});
add_action('admin_head', function() {
$screen = get_current_screen();
if (!str_contains($screen->id ?? '', 'wis_')) return;
echo '<style>
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none !important;
appearance: none !important;
margin: 0 !important;
}
input[type="number"] {
-moz-appearance: textfield !important;
appearance: textfield !important;
}
</style>';
});
add_action('admin_init', [WIS_Admin::class, 'handle_save_item']);
add_action('rest_api_init',[WIS_API::class, 'register_routes']);
add_action('init', [WIS_Shortcode::class, 'register']);
add_action('widgets_init', function() {
register_widget('WIS_Sidebar_Widget');
});
add_action('wis_daily_deal_event', [WIS_Activator::class, 'run_daily_deal']);
add_action('wis_abo_renewal_event', [WIS_Activator::class, 'run_abo_renewal']);
add_action('wis_item_abo_delivery_event', [WIS_Activator::class, 'run_item_abo_delivery']);
// ── Auto-Expire: Orders nach 7 Tagen ohne Ingame-Bestätigung stornieren ──
add_action('wis_auto_expire_orders', function () {
global $wpdb;
$table = $wpdb->prefix . 'wis_orders';
// Alle Orders die seit > 7 Tagen pending/claimed sind → cancelled
$affected = $wpdb->query(
"UPDATE {$table}
SET status = 'cancelled'
WHERE status IN ('pending', 'claimed')
AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY)"
);
if ($affected > 0) {
error_log("[WIS] Auto-Expire: {$affected} Order(s) nach 7 Tagen storniert.");
}
});