Files
WP-Ingame-Shop-Pro/wp-ingame-shop/wp-ingame-shop-pro.php
2026-05-05 21:05:34 +02:00

6945 lines
373 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.');
});
// 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));
$wpdb->update($table, ['is_daily_deal' => 0], ['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;
}
}
// ===========================================================
// 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 === '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') ? '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', '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', '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;
$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' : '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>
</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>
<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('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() {
if (isset($_POST['wis_confirm_reset'])) {
check_admin_referer('wis_reset');
WIS_Activator::reset_shop();
echo '<div class="updated"><p>✅ Shop wurde komplett zurückgesetzt!</p></div>';
}
?>
<div class="wrap">
<h1>🔄 Shop Reset</h1>
<div class="card" style="max-width:800px; padding:20px; background:#fff3cd;">
<h2 style="color:#856404;">⚠️ 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'); ?>
<p class="submit">
<input type="submit" name="wis_confirm_reset" class="button button-secondary button-large" value="🗑️ Shop jetzt zurücksetzen">
</p>
</form>
</div>
</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
}
}
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', '/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);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'completed'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
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;
$valid_cart[] = [
'id' => $item->item_id,
'title' => $item->name,
'price' => $price,
'qty' => $qty,
'is_offer' => $item->is_offer
];
$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'];
} 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 . ')';
return new WP_REST_Response(['success' => true, 'message' => $msg]);
}
/**
* 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); } }
.wis-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 2147483647; display: none; align-items: center; justify-content: center; transform: translateZ(0); }
.wis-modal { background: white; width: 90%; max-width: 600px; max-height: 80vh; padding: 30px; border-radius: 15px; box-shadow: 0 15px 30px rgba(0,0,0,0.3); overflow-y: auto; position: relative; z-index: 2147483648; color: #222 !important; }
.wis-cart-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; border-bottom: 1px solid #eee; }
.wis-cart-item-title { font-weight: bold; color: #333 !important; }
.wis-cart-item-price { color: #e67e22; font-weight: 600; }
.wis-cart-item-remove { background: #dc3545; color: white; border: none; padding: 8px 15px; border-radius: 6px; cursor: pointer; }
.wis-cart-total { border-top: 2px solid #333; padding: 20px 0; margin-top: 15px; font-size: 1.3rem; font-weight: bold; display: flex; justify-content: space-between; color: #222 !important; }
.wis-modal-input { width: 100%; padding: 15px; margin: 20px 0; border: 2px solid #ddd; border-radius: 8px; font-size: 1.1rem; box-sizing: border-box; }
.wis-modal-actions { display: flex; gap: 10px; }
.wis-modal-btn { flex: 1; padding: 12px; border: none; border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 1rem; }
.wis-btn-confirm { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; }
.wis-btn-cancel { background: #6c757d; color: white; }
.wis-coupon-input-group { display: flex; gap: 10px; margin-bottom: 20px; }
.wis-coupon-input { flex-grow: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; }
.wis-coupon-btn { padding: 10px 15px; background: #6f42c1; color: white; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; }
.wis-coupon-msg { font-size: 0.85rem; color: #e67e22; margin-top: 5px; min-height: 20px; }
.wis-gift-toggle { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; cursor: pointer; font-weight: 600; color: #6f42c1; user-select: none; }
.wis-gift-toggle input[type=checkbox] { width: 18px; height: 18px; cursor: pointer; accent-color: #9b59b6; }
.wis-gift-field { display: none; background: #f3eeff; border: 1px solid #c39bd3; border-radius: 8px; padding: 14px 16px; margin-bottom: 15px; }
.wis-gift-field label { display: block; font-weight: 600; color: #6f42c1; margin-bottom: 6px; font-size: 0.9rem; }
.wis-gift-input { width: 100%; padding: 10px 14px; border: 1px solid #c39bd3; border-radius: 6px; font-size: 1rem; box-sizing: border-box; background: #fff; }
.wis-gift-hint { font-size: 0.8rem; color: #888; margin-top: 6px; }
.wis-alert { margin-top: 15px; padding: 12px; border-radius: 5px; display: none; }
.wis-alert.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.wis-alert.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
</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 Modal -->
<div class="wis-modal-overlay" id="cart-modal">
<div class="wis-modal">
<h2 style="color:#222 !important;">🛒 Dein Warenkorb</h2>
<div id="cart-content">
<div style="text-align:center; padding:40px; color:#999;">Dein Warenkorb ist leer</div>
</div>
<div id="cart-checkout" style="display:none;">
<div style="margin-bottom:20px; background:#f8f9fa; padding:15px; border-radius:8px;">
<label style="display:block; font-weight:bold; margin-bottom:5px; color:#333 !important;">🎫 Gutscheincode</label>
<div class="wis-coupon-input-group">
<input type="text" id="coupon-code" class="wis-coupon-input" placeholder="CODE">
<button class="wis-coupon-btn" id="wis-coupon-btn">Einlösen</button>
</div>
<div id="coupon-msg" class="wis-coupon-msg"></div>
</div>
<div id="wis-tax-breakdown" style="display:none; background:#f8f9fa; border-radius:6px; padding:10px 15px; margin-bottom:8px;">
<div style="display:flex; justify-content:space-between; margin-bottom:4px; color:#333 !important; font-size:0.95rem;">
<span>Zwischensumme:</span>
<span id="cart-subtotal">0 <?php echo esc_html($currency); ?></span>
</div>
<div style="display:flex; justify-content:space-between; color:#e67e22; font-size:0.95rem;">
<span>⚖️ Steuer (<?php echo esc_attr(get_option('wis_tax_rate', '0')); ?>%):</span>
<span id="cart-tax">0 <?php echo esc_html($currency); ?></span>
</div>
</div>
<div class="wis-cart-total">
<span>Gesamt:</span>
<span id="cart-total">0 <?php echo esc_html($currency); ?></span>
</div>
<select id="checkout-server" class="wis-modal-input">
<option value="">-- Server wä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>
<input type="text" id="checkout-player" class="wis-modal-input" placeholder="Dein Spielername">
<!-- Gift-System -->
<label class="wis-gift-toggle">
<input type="checkbox" id="wis-gift-toggle">
🎁 Als Geschenk für einen anderen Spieler kaufen
</label>
<div class="wis-gift-field" id="wis-gift-field">
<label for="wis-gift-recipient">Empfänger-Spielername</label>
<input type="text" id="wis-gift-recipient" class="wis-gift-input" placeholder="z.B. Steve123" autocomplete="off">
<div class="wis-gift-hint">⚠️ Der Empfänger muss das Geschenk ingame annehmen. Bei Ablehnung erhältst du dein Geld zurück.</div>
</div>
<div class="wis-modal-actions">
<button class="wis-modal-btn wis-btn-confirm" id="wis-checkout-btn">💰 Kauf abschließen</button>
<button class="wis-modal-btn wis-btn-cancel" id="wis-close-cart-btn">Abbrechen</button>
</div>
<div id="cart-alert" class="wis-alert"></div>
</div>
</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() {
// Modal an <body> hängen damit z-index korrekt greift
const modal = document.getElementById('cart-modal');
if (modal && document.body) document.body.appendChild(modal);
// 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);
// ---- Warenkorb schließen ----
var closeBtn = document.getElementById('wis-close-cart-btn');
if (closeBtn) closeBtn.addEventListener('click', closeCart);
// ---- Modal-Overlay klick ----
var cartModal = document.getElementById('cart-modal');
if (cartModal) {
cartModal.addEventListener('click', function(e) {
if (e.target === cartModal) closeCart();
});
}
// ---- Gutschein einlösen ----
var couponBtn = document.getElementById('wis-coupon-btn');
if (couponBtn) couponBtn.addEventListener('click', validateCoupon);
// ---- Kauf abschließen ----
var checkoutBtn = document.getElementById('wis-checkout-btn');
if (checkoutBtn) checkoutBtn.addEventListener('click', checkout);
// ---- 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
+ '<div class="wis-card-price-container">'
+ oldPrice
+ '<div class="wis-card-price">' + price + ' ' + shopCurrency + '</div>'
+ '</div>'
+ '<div class="wis-card-servers">📡 ' + escHtml(serverNames) + '</div>'
+ '<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;
var existing = cart.find(function(i){ return i.id === item.id; });
if (existing) {
existing.quantity += qty;
} else {
cart.push({
id: item.id,
name: item.name,
price: price,
quantity: qty,
servers: item.servers,
is_offer: !!item.is_offer
});
}
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;
}
function openCart() {
renderCart();
document.getElementById('cart-modal').style.display = 'flex';
}
function closeCart() {
document.getElementById('cart-modal').style.display = 'none';
}
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 checkout = document.getElementById('cart-checkout');
if (cart.length === 0) {
content.innerHTML = '<div style="text-align:center;padding:40px;color:#999;">Dein Warenkorb ist leer</div>';
checkout.style.display = 'none';
// Gift-Feld zurücksetzen wenn Warenkorb leer
var gt = document.getElementById('wis-gift-toggle');
var gf = document.getElementById('wis-gift-field');
if (gt) gt.checked = false;
if (gf) gf.style.display = 'none';
return;
}
content.innerHTML = cart.map(function(item, idx) {
return '<div class="wis-cart-item">'
+ '<div>'
+ '<div class="wis-cart-item-title">' + escHtml(item.name) + '</div>'
+ '<div class="wis-cart-item-price">' + item.quantity + 'x × ' + item.price + ' = ' + (item.price * item.quantity) + ' ' + shopCurrency + '</div>'
+ '</div>'
+ '<button class="wis-cart-item-remove" data-cart-idx="' + idx + '" type="button">🗑️</button>'
+ '</div>';
}).join('');
// Remove-Buttons
content.querySelectorAll('.wis-cart-item-remove').forEach(function(btn) {
btn.addEventListener('click', function() {
var idx = parseInt(btn.getAttribute('data-cart-idx'), 10);
cart.splice(idx, 1);
updateCartBadge();
renderCart();
});
});
document.getElementById('cart-total').textContent = calculateTotal() + ' ' + shopCurrency;
// Steuer-Aufschlüsselung
var taxBreakdown = document.getElementById('wis-tax-breakdown');
if (shopTaxEnabled && shopTaxRate > 0 && taxBreakdown) {
var sub = calculateSubtotal();
var tax = calculateTax(sub);
document.getElementById('cart-subtotal').textContent = sub + ' ' + shopCurrency;
document.getElementById('cart-tax').textContent = tax + ' ' + shopCurrency;
taxBreakdown.style.display = 'block';
} else if (taxBreakdown) {
taxBreakdown.style.display = 'none';
}
checkout.style.display = 'block';
}
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-checkout-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) {
var payload = {player: player, cart: cart, 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…';
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) {
alertEl.textContent = data.message;
alertEl.className = 'wis-alert success';
alertEl.style.display = 'block';
cart = []; couponData = {};
if (giftToggle) { giftToggle.checked = false; }
var giftField = document.getElementById('wis-gift-field');
if (giftField) giftField.style.display = 'none';
document.getElementById('wis-gift-recipient').value = '';
updateCartBadge();
setTimeout(function(){ closeCart(); location.reload(); }, 2000);
} 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.");
}
});