'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'], '<')) { ?>

🚀 WP Ingame Shop Pro Update verfügbar!
Neue Version: (Du hast )
Hier im Git Repository ansehen oder unten im Widget Details prüfen.

$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 = '✅ Aktuell (v' . WIS_VERSION . ')'; if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')) { $update_html = '⚠️ Update verfügbar!'; } ?>
Items
Bestellungen
Offen
Umsatz ()

Status:

Neue Version
Updates ansehen
Bestellungen ansehen
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 '

✅ Einstellungen gespeichert!

'; } if (isset($_POST['wis_regen_key']) && check_admin_referer('wis_regen_key')) { update_option('wis_api_key', bin2hex(random_bytes(24))); echo '

🔑 Neuer API-Key generiert! Bitte in der config.yml des Spigot-Plugins aktualisieren.

'; } 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") ]; ?>

🛒 WP Ingame Shop Pro v

📊 Statistiken

Items im Shop:
Bestellungen:
Server:
Gutscheine:

⚙️ Einstellungen

'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' => '', ], ] ); ?>

Live-Vorschau:

Muss mit / enden! Z.B.: https://git.viper.ipv64.net/.../images/

Gutscheine bei Angeboten
Daily Deal
Offline-Queue

Erfordert Spigot-Plugin v6.3+ mit Offline-Queue-Unterstützung

Steuer aktivieren

Z.B. 19 für 19 % MwSt. Nur aktiv wenn „Steuer aktivieren" angehakt ist.

Spieler können dies im Shop-Frontend per Dropdown selbst anpassen.

🔑 Spigot API-Key

Dieser Key muss in der config.yml des Spigot-Plugins als api-key eingetragen werden.

Geschützte Endpunkte: pending_orders, execute_order, complete_order, cancel_order, pending_offline

= 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 '

✅ ' . $count . ' Items erfolgreich aktualisiert!

'; } } 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]); ?>

📦 Mehrfachbearbeitung

Aktion: für Items.

Preis pro Item festlegen

Item Name Aktueller Preis Neuer Preis ()
name); ?>
item_id); ?>
price); ?>

Angebot pro Item festlegen

Item Name Normalpreis Als Angebot? Angebotspreis
name); ?>
item_id); ?>
price); ?> is_offer, 1); ?> onchange="toggleOfferPrice(this, id; ?>)"> is_offer): ?>disabled>

Server pro Item zuweisen

Keine Server vorhanden.

servers, true) ?: []; ?>
Item Name Server (Mehrfachauswahl möglich)
name); ?>
item_id); ?>
slug, $current_servers) ? 'checked' : ''; ?>

Kategorien pro Item zuweisen

Fehler beim Laden der Items.

categories, true) ?: []; ?>
Item Name Kategorie (Mehrfachauswahl mit STRG+Klick)
name); ?>
item_id); ?>

Ankauf pro Item konfigurieren

sell_enabled); $s_mode = $item->sell_price_mode ?? 'percent'; $s_value = $item->sell_price_value ?? 80; ?>
Item Name VK-Preis Ankauf? Modus Wert Ankaufspreis
name); ?>
item_id); ?>
price); ?> onchange="wisUpdateSellRow(id; ?>, price); ?>)"> 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 ''; } ?>

Abbrechen

✅ Item gelöscht!

'; } 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 '

✅ Status geändert!

'; } } // 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 '

' . esc_html($save_error) . '

'; } if (isset($_GET['created'])) { echo '

✅ Item erfolgreich erstellt!

'; } if (isset($_GET['saved'])) { echo '

✅ Item gespeichert!

'; } 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 = []; } ?>

← Zurück zur Liste
sell_enabled)) ? '' : 'style="display:none"'; ?>> sell_enabled)) ? '' : 'style="display:none"'; ?>>
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')))))); ?>

Z.B.: minecraft:diamond (Kategorien werden automatisch zugewiesen)

Eigene Bild-URL – überschreibt die automatische Minecraft-Item-ID-URL.
Ideal für Fly-Gutscheine, VIP-Pakete etc. Leer lassen = Standard.

Vorschau Vorschau

Optional: Wenn gesetzt, wird der normale Preis durchgestrichen

 

Beispiele: 80 % → 80 % des VK-Preises  |  Modus «minus 10» → VK-Preis − 10 |  Fixer Preis 15 → immer 15 

Max. Menge die ein Spieler pro Tag verkaufen kann. 0 = kein Limit.

Markierungen
Server

Keine Server vorhanden. Server erstellen

Kategorien

Keine Kategorien vorhanden.

Bei neuem Item werden Kategorien automatisch basierend auf der Item-ID gesetzt

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'; ?>

Items Neu erstellen

✕ Zurücksetzen
Item(s) gefunden – Suche:
slug) { $active_root_obj = $rc; break; } } if ($active_root_obj && !empty($sub_idx_tab[$active_root_obj->id])): ?>
Alle 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] : [] )); ?> name); ?> ()
1): ?> Einträge 1): ?> « Seite von » Einträge

ID Name Item ID Preis Status Aktionen
id); ?> name); ?> item_id, $rm2)) { $rd = intval($rm2[4]); echo '👑 LP: ' . esc_html($rm2[2]) . ' — ' . ($rd === 0 ? 'dauerhaft' : esc_html($rd) . ' Tage') . ''; } elseif (preg_match('/^rank_(.+)_(\d+)$/', $item->item_id, $rm2)) { $rd = intval($rm2[2]); echo '👑 ' . esc_html($rm2[1]) . ' — ' . ($rd === 0 ? 'dauerhaft' : esc_html($rd) . ' Tage') . ''; } elseif (preg_match('/^custom_cmd_(.+)$/', $item->item_id, $cc2)) { echo '⚙️ Custom: ' . esc_html($cc2[1]) . ''; } elseif ($item->item_id === 'fly_abo' || preg_match('/^fly_abo_/', $item->item_id)) { echo '✈ Fly-Abo — monatlich'; } elseif (preg_match('/^plot_slots_(\d+)$/', $item->item_id, $plt_m)) { echo '📦 Plot-Slots — +' . esc_html($plt_m[1]) . ' permanent'; } elseif (preg_match('/^plot_abo_(\d+)$/', $item->item_id, $plt_m)) { echo '📦 Plot-Abo — +' . esc_html($plt_m[1]) . ' monatlich'; } else { echo '' . esc_html($item->item_id) . ''; } ?> price); ?> status === 'publish'): ?> ✅ Aktiv 📝 Entwurf Bearbeiten Löschen
1): ?>
Einträge 1): ?> « Seite von »

✅ Server erstellt!

'; } if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') { check_admin_referer('wis_server_action', '_wpnonce'); WIS_DB::delete_server(intval($_GET['id'])); echo '

✅ Server gelöscht!

'; } $servers = WIS_DB::get_servers(); ?>

Server

Neuen Server erstellen

Kleinbuchstaben ohne Leerzeichen

Vorhandene Server

ID Name Slug Aktionen
Noch keine Server vorhanden.
id); ?> name); ?> slug); ?> Löschen
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 '

✅ Kategorie aktualisiert!

'; } } 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 '

✅ Kategorie erstellt!

'; } 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 '

✅ Kategorie gelöscht!

'; } $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; ?>

Kategorien & Unterkategorien

✏️ Kategorie bearbeiten

Aktueller Slug slug); ?> (wird beim Speichern automatisch aktualisiert)

Abbrechen

Neue Kategorie erstellen

Leer lassen = Hauptkategorie. Max. 2 Ebenen.

Reihenfolge & Struktur

Ziehe Kategorien per ☰ in die gewünschte Reihenfolge. Klicke ✏️ um eine Kategorie umzubenennen oder die Ebene zu ändern.

📁 name); ?> slug); ?> id])): ?> (id]); ?> Unterkats) ✏️ 🗑
id])): ?>
    id] as $sc): ?>
  • name); ?> slug); ?> ✏️ 🗑

Noch keine Kategorien vorhanden.

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 '

'; echo "{$generated} Gutscheine wurden generiert"; if ($skipped) echo " ({$skipped} Duplikate übersprungen)"; echo '. 📥 Als CSV exportieren

'; } 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 '

✅ Gutschein gespeichert!

'; } else { $data['used_count'] = 0; WIS_DB::insert_coupon($data); echo '

✅ Gutschein erstellt!

'; } } if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') { check_admin_referer('wis_coupon_action', '_wpnonce'); WIS_DB::delete_coupon(intval($_GET['id'])); echo '

✅ Gutschein gelöscht!

'; } 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'); ?>

← Zurück zur Liste

Z.B.: SUMMER20

Bei Festbetrag: Betrag in . Bei Prozent: Zahl ohne %

Optional

Gutschein gilt nur ab diesem Bestellwert (0 = kein Minimum). Wert in .

allowed_categories ? array_map('intval', explode(',', $coupon->allowed_categories)) : []; ?>
id, $selected_cats); ?>

Ohne Auswahl gilt der Gutschein für alle Kategorien.

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; } } ?>

Gutscheine Neu erstellen 🎲 Bulk generieren

bulk_id)) $bulk_groups[$c->bulk_id][] = $c; } ?>

📦 Generierte Bulk-Gruppen

$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'); ?>
Bulk-ID Codes Rabatt Gültig bis Aktionen
value); ?>type==='percent'?'%':' '.esc_html(get_option('wis_currency_name','Coins')); ?> expiry ? esc_html(date('d.m.Y', strtotime($sample->expiry))) : '∞'; ?> 📥 CSV Export
id] ?? []; ?>
Code Rabatt Genutzt Gültig bis Eingelöst von Aktionen
Noch keine Gutscheine vorhanden.
code); ?> bulk_id)): ?> BULK type === 'percent'): ?> value); ?>% value); ?> used_count); ?> / usage_limit); ?> expiry ? esc_html(date('d.m.Y', strtotime($coupon->expiry))) : '∞'; ?>
player_name); ?>
Bearbeiten Löschen

✅ Bestellung gelöscht!

'; } 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 '

✅ Status geändert!

'; } if (isset($_GET['view'])) { $order = WIS_DB::get_order(intval($_GET['view'])); if (!$order) { echo '

Bestellung nicht gefunden.

'; 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']; ?>

Bestellung #id; ?> – Details

← Zurück gift_recipient)): ?>
IDid); ?>
Datumcreated_at))); ?>
Käuferplayer_name); ?>
🎁 Geschenk fürgift_recipient); ?>
Serverserver); ?>
Zusammenfassungitem_title); ?>
Preisprice); ?>
Statusstatus] ?? $order->status; ?>
Details (JSON)response); ?>
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']; ?>

Bestellungen ( gesamt)

Zurücksetzen
ID Datum Käufer Empfänger Server Inhalt Preis Status Aktionen
Keine Bestellungen gefunden.
#id; ?> created_at)); ?> player_name); ?> gift_recipient)): ?>🎁 gift_recipient); ?> server); ?> item_title, 0, 55)) . (mb_strlen($order->item_title) > 55 ? '…' : ''); ?> price); ?> status] ?? $order->status; ?> Details " class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen
1): ?>
Seite /  ( Einträge)
prefix . 'wis_sell_log'; if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) { echo '

Ankauf-Log

Tabelle noch nicht vorhanden – Plugin einmal deaktivieren/aktivieren.

'; 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"); ?>

📤 Ankauf-Log ( Einträge)

Zurücksetzen
Datum Spieler Server Item Menge Ø Preis Ausgezahlt
Keine Einträge gefunden.
sold_at)); ?> player_name); ?> server); ?> item_name); ?>item_id); ?> quantity); ?> price_per_item, 2); ?> total_paid, 2); ?>
1): ?>
Seite /
prefix . 'wis_price_history'; if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) { echo '

Preishistorie

Tabelle noch nicht vorhanden – Plugin einmal deaktivieren/aktivieren.

'; 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"); ?>

📈 Preishistorie ( Änderungen)

Zurücksetzen

Noch keine Preisänderungen protokolliert. Ändere einen Preis bei einem Item und speichere – ab dann wird hier alles festgehalten.

new_value - $r->old_value; $diff_color = $diff > 0 ? 'green' : ($diff < 0 ? 'red' : '#666'); ?>
Datum Item Feld Alt Neu Diff Geändert von
Keine Einträge gefunden.
changed_at)); ?> item_name); ?>item_id); ?> field); ?> old_value); ?> new_value); ?> 0?'+':'') . number_format($diff); ?> changed_by); ?>
1): ?>
Seite /
'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 '

📦 JSON Export

'; echo '

✅ JSON erfolgreich generiert!

'; echo ''; echo '

'; echo ''; echo '

📤 Nächste Schritte:

'; echo '
    '; echo '
  1. Lade die JSON-Datei herunter
  2. '; echo '
  3. Gehe zu deinem Gitea Repository
  4. '; echo '
  5. Lade die items.json hoch unter: https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro
  6. '; echo '
  7. Klicke dann auf den Quick-Import Button unten!
  8. '; echo '
'; echo '
'; return; } $default_url = 'https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/raw/branch/main/items.json'; ?>

📦 JSON Export/Import

📤 JSON Export

Generiere eine JSON-Datei mit allen deinen Items für Gitea.

⚡ Quick-Import von Gitea

Importiert direkt von deinem Gitea Repository!

📥 JSON Import (Manuelle URL)

Importiere Items aus einer beliebigen JSON-URL.


✅ Shop wurde komplett zurückgesetzt!

'; } ?>

🔄 Shop Reset

⚠️ WARNUNG

Diese Aktion löscht ALLE Daten:

Diese Aktion kann NICHT rückgängig gemacht werden!

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 "); ?>

🏆 Top Spender

Spieler mit den höchsten Gesamtausgaben

Rang Spieler Ausgegeben Bestellungen
Noch keine Statistiken vorhanden.
# player_name); ?> total_spent)); ?> order_count); ?>
= 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); } ?>

📊 Analyse – Kauf & Verkauf

'7 Tage','30'=>'30 Tage','90'=>'90 Tage','365'=>'1 Jahr','all'=>'Gesamt'] as $v=>$l): ?>
⚠️ Einzelitem-Tracking noch nicht aktiv Die Tabelle wis_order_items fehlt noch. Bitte das Plugin einmal deaktivieren und wieder aktivieren. 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).
🛒 Items gekauft(n/v)' : ''; ?>
💰 Einnahmen ()
📤 Ankäufe (Menge)
📉 Ausgezahlt ()

📈 Tagesumsatz

Keine abgeschlossenen Bestellungen in den letzten 30 Tagen.

Top 20 – meistgekaufte Items (einzeln)

Keine Daten gefunden.

Keine abgeschlossenen Bestellungen für diesen Zeitraum.

# Item Ø Preis Menge Umsatz Trend
item_name); ?> item_id); ?> avg_price, 0); ?> qty); ?> / order_count); ?> Käufe revenue, 0); ?>

💡 Viel gekauft + hoher Umsatz → Preis erhöhen. Viel gekauft + niedriger Ø-Preis → evtl. zu günstig.

Top 20 – Ankauf durch Spieler (einzeln)

Ankauf-Tabelle nicht vorhanden. Ankauf-Feature aktivieren.

Keine Ankaufdaten für diesen Zeitraum.

# Item Ø Ankaufspreis Menge Ausgezahlt Trend
item_name); ?> item_id); ?> avg_price, 2); ?> qty); ?> total_paid, 0); ?>

💡 Viel verkauft = Spieler farmen dieses Item massenhaft → Ankaufspreis senken oder Tageslimit einführen.

'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= * 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= * 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":"", "abo_id":} * 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":"", "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": } – 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(); ?>

🛒 Ingame Shop

$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]) : []; } ?>

Lade Items…

🛒 Dein Warenkorb

Dein Warenkorb ist leer
'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(); ?>
is_daily_deal): ?>
🎁 DAILY DEAL
is_offer): ?>
🔥 ANGEBOT
<?php echo esc_attr($item->name); ?>

name); ?>

price); ?>
🛒

Kein Angebot verfügbar.

'; } 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'] : ''; ?>

id ?? '', 'wis_')) return; echo ''; }); add_action('admin_head', function() { $screen = get_current_screen(); if (!str_contains($screen->id ?? '', 'wis_')) return; echo ''; }); 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."); } });