'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'], '<')) {
?>
$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:
prefix . 'wis_categories';
$order = $_POST['order'] ?? [];
if (!is_array($order)) wp_send_json_error('Ungültige Daten.');
foreach ($order as $i => $entry) {
$id = intval($entry['id']);
$parent_id = intval($entry['parent_id']);
if ($id > 0) {
$wpdb->update($table, [
'sort_order' => ($i + 1) * 10,
'parent_id' => $parent_id,
], ['id' => $id]);
}
}
wp_send_json_success('Reihenfolge gespeichert.');
});
// AJAX: Angebot-Flag umschalten (Angebote-Übersicht)
add_action('wp_ajax_wis_angebote_toggle', function() {
check_ajax_referer('wis_angebote_nonce', 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('Keine Berechtigung.');
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$id = intval($_POST['id'] ?? 0);
$field = in_array($_POST['field'] ?? '', ['is_offer', 'is_daily_deal'], true) ? $_POST['field'] : null;
if (!$id || !$field) wp_send_json_error('Ungültige Parameter.');
$current = (int) $wpdb->get_var($wpdb->prepare("SELECT $field FROM $table WHERE id = %d", $id));
$new_val = $current ? 0 : 1;
if ($field === 'is_daily_deal' && $new_val === 1) {
$wpdb->query("UPDATE $table SET is_daily_deal = 0 WHERE is_daily_deal = 1");
}
$wpdb->update($table, [$field => $new_val], ['id' => $id]);
wp_send_json_success(['new_val' => $new_val]);
});
// AJAX: Angebotspreis direkt speichern (Angebote-Übersicht)
add_action('wp_ajax_wis_angebote_save_price', function() {
check_ajax_referer('wis_angebote_nonce', 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('Keine Berechtigung.');
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$id = intval($_POST['id'] ?? 0);
$offer_price = max(0, intval($_POST['offer_price'] ?? 0));
if (!$id) wp_send_json_error('Ungültige ID.');
$wpdb->update($table, ['offer_price' => $offer_price], ['id' => $id]);
wp_send_json_success(['offer_price' => $offer_price]);
});
// jQuery UI Sortable für die Kategorien-Seite laden
add_action('admin_enqueue_scripts', function($hook) {
if (isset($_GET['page']) && $_GET['page'] === 'wis_categories') {
wp_enqueue_script('jquery-ui-sortable');
}
});
// ===========================================================
// ACTIVATION & DATABASE
// ===========================================================
class WIS_Activator {
public static function activate() {
// Spalte custom_image_url nachrüsten (für bestehende Installationen)
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$col = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'custom_image_url'");
if (!$col) {
$wpdb->query("ALTER TABLE $table ADD COLUMN custom_image_url varchar(500) DEFAULT NULL AFTER categories");
}
// sort_order für Kategorien nachrüsten (ab v2.3.0)
$cat_table_so = $wpdb->prefix . 'wis_categories';
$col_so = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$cat_table_so' AND COLUMN_NAME = 'sort_order'");
if (!$col_so) {
$wpdb->query("ALTER TABLE $cat_table_so ADD COLUMN sort_order int(11) NOT NULL DEFAULT 0 AFTER parent_id");
// Bestehende Kategorien mit aufsteigender Reihenfolge initialisieren
$existing_cats = $wpdb->get_results("SELECT id FROM $cat_table_so ORDER BY parent_id ASC, name ASC");
foreach ($existing_cats as $i => $ec) {
$wpdb->update($cat_table_so, ['sort_order' => $i * 10], ['id' => $ec->id]);
}
}
// custom_command-Spalte nachrüsten (ab v2.3.0 – Custom Command Items)
$col_cmd = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'custom_command'");
if (!$col_cmd) {
$wpdb->query("ALTER TABLE $table ADD COLUMN custom_command varchar(500) DEFAULT NULL AFTER custom_image_url");
}
// Ankauf-Spalten nachrüsten (ab v6.5)
$col_sell = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'sell_enabled'"); if (!$col_sell) {
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_enabled tinyint(1) NOT NULL DEFAULT 0 AFTER status");
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_price_mode varchar(20) NOT NULL DEFAULT 'percent' AFTER sell_enabled");
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_price_value int(11) NOT NULL DEFAULT 80 AFTER sell_price_mode");
}
// Tageslimit beim Ankauf nachrüsten
$col_dlimit = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'daily_sell_limit'");
if (!$col_dlimit) {
$wpdb->query("ALTER TABLE $table ADD COLUMN daily_sell_limit int(11) NOT NULL DEFAULT 0 AFTER sell_price_value");
}
// Preishistorie-Tabelle anlegen
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_price_history (
id mediumint(9) NOT NULL AUTO_INCREMENT,
item_id varchar(100) NOT NULL,
item_name varchar(255) NOT NULL,
field varchar(30) NOT NULL DEFAULT 'price',
old_value int(11) NOT NULL DEFAULT 0,
new_value int(11) NOT NULL DEFAULT 0,
changed_by varchar(100) NOT NULL DEFAULT '',
changed_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY item_id (item_id),
KEY changed_at (changed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
// Gift-Spalte nachrüsten (ab v6.5-gift)
$orders_table = $wpdb->prefix . 'wis_orders';
$col_gift = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$orders_table' AND COLUMN_NAME = 'gift_recipient'");
if (!$col_gift) {
$wpdb->query("ALTER TABLE $orders_table ADD COLUMN gift_recipient varchar(100) DEFAULT NULL AFTER player_name");
}
// Auto-Expire Cron registrieren (7-Tage-Ablauf für unbestätigte Orders)
if (!wp_next_scheduled('wis_auto_expire_orders')) {
wp_schedule_event(time(), 'hourly', 'wis_auto_expire_orders');
}
// Item-Abo Tabelle nachrüsten
self::create_item_abo_subs_table();
// Item-Abo Liefer-Cron registrieren (täglich)
if (!wp_next_scheduled('wis_item_abo_delivery_event')) {
$midnight = strtotime('tomorrow midnight');
wp_schedule_event($midnight, 'daily', 'wis_item_abo_delivery_event');
}
// Sell-Log-Tabelle anlegen
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_sell_log (
id mediumint(9) NOT NULL AUTO_INCREMENT,
player_name varchar(100) NOT NULL,
server varchar(100) NOT NULL,
item_id varchar(100) NOT NULL,
item_name varchar(255) NOT NULL,
quantity int(11) NOT NULL DEFAULT 1,
price_per_item decimal(10,2) NOT NULL,
total_paid decimal(10,2) NOT NULL,
sold_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY player_name (player_name),
KEY sold_at (sold_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
// Order-Items-Tabelle anlegen (ab Analyse-Update) – einzelne Items pro Bestellung
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_order_items (
id mediumint(9) NOT NULL AUTO_INCREMENT,
order_id mediumint(9) NOT NULL,
item_id varchar(100) NOT NULL,
item_name varchar(255) NOT NULL,
item_type varchar(20) NOT NULL DEFAULT 'item',
quantity int(11) NOT NULL DEFAULT 1,
price_per_item decimal(10,2) NOT NULL DEFAULT 0,
total decimal(10,2) NOT NULL DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY order_id (order_id),
KEY item_id (item_id),
KEY created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
self::create_tables();
self::set_default_options();
self::create_default_categories();
if (!wp_next_scheduled('wis_daily_deal_event')) {
wp_schedule_event(time(), 'daily', 'wis_daily_deal_event');
}
// Fly-Abo Renewal: täglich prüfen (läuft nur durch am 1. des Monats)
if (!wp_next_scheduled('wis_abo_renewal_event')) {
// Nächsten Mitternacht-Zeitstempel berechnen
$midnight = strtotime('tomorrow midnight');
wp_schedule_event($midnight, 'daily', 'wis_abo_renewal_event');
}
flush_rewrite_rules();
}
public static function deactivate() {
wp_clear_scheduled_hook('wis_daily_deal_event');
wp_clear_scheduled_hook('wis_abo_renewal_event');
wp_clear_scheduled_hook('wis_auto_expire_orders');
wp_clear_scheduled_hook('wis_item_abo_delivery_event');
flush_rewrite_rules();
}
private static function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$tables = [];
// Items
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_items (
id mediumint(9) NOT NULL AUTO_INCREMENT,
item_id varchar(100) NOT NULL,
name varchar(255) NOT NULL,
description text,
price int(11) DEFAULT 0,
offer_price int(11) DEFAULT 0,
is_offer tinyint(1) DEFAULT 0,
is_daily_deal tinyint(1) DEFAULT 0,
servers text,
categories text,
custom_image_url varchar(500) DEFAULT NULL,
custom_command varchar(500) DEFAULT NULL,
status varchar(20) DEFAULT 'draft',
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY item_id (item_id),
KEY status (status)
) $charset_collate;";
// Orders
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_orders (
id mediumint(9) NOT NULL AUTO_INCREMENT,
player_name varchar(100) NOT NULL,
gift_recipient varchar(100) DEFAULT NULL,
server varchar(100) NOT NULL,
item_id varchar(100) NOT NULL,
item_title varchar(255) NOT NULL,
price int(11) NOT NULL,
quantity int(11) DEFAULT 1,
status varchar(20) DEFAULT 'pending',
response text,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY player_name (player_name),
KEY gift_recipient (gift_recipient),
KEY status (status)
) $charset_collate;";
// Coupons
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_coupons (
id mediumint(9) NOT NULL AUTO_INCREMENT,
code varchar(50) NOT NULL,
value int(11) NOT NULL,
type varchar(10) DEFAULT 'fixed',
usage_limit int(11) DEFAULT 1,
used_count int(11) DEFAULT 0,
expiry date DEFAULT NULL,
min_order_value int(11) DEFAULT 0,
allowed_categories text DEFAULT NULL,
bulk_id varchar(20) DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY code (code)
) $charset_collate;";
// Pro-Spieler Coupon-Nutzung (verhindert Mehrfacheinlösung)
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_coupon_uses (
id mediumint(9) NOT NULL AUTO_INCREMENT,
coupon_id mediumint(9) NOT NULL,
player_name varchar(100) NOT NULL,
used_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY coupon_player (coupon_id, player_name),
KEY player_name (player_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
// Servers
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_servers (
id mediumint(9) NOT NULL AUTO_INCREMENT,
slug varchar(100) NOT NULL,
name varchar(255) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY slug (slug)
) $charset_collate;";
// Categories
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_categories (
id mediumint(9) NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
slug varchar(100) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY slug (slug)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
foreach ($tables as $sql) {
dbDelta($sql);
}
}
private static function set_default_options() {
$defaults = [
'wis_currency_name' => 'Coins',
'wis_image_base_url' => 'https://git.viper.ipv64.net/M_Viper/minecraft-items/raw/branch/main/images/',
'wis_header_text' => '✅ Auto-Bilder | 💰 Sicherer Checkout | 🎮 Ingame-Bestätigung',
'wis_coupon_exclude_offers' => '0',
'wis_daily_deal_enabled' => '0',
'wis_daily_deal_discount' => '20',
'wis_api_key' => bin2hex(random_bytes(24)),
'wis_tax_enabled' => '0',
'wis_tax_rate' => '0',
];
foreach ($defaults as $key => $value) {
if (get_option($key) === false) {
add_option($key, $value);
}
}
}
public static function check_api_key($request) {
$key = $request->get_header('X-WIS-Key');
if (empty($key)) {
$key = $request->get_param('api_key');
}
$stored = get_option('wis_api_key', '');
return ($stored !== '' && hash_equals($stored, (string) $key));
}
public static function spigot_permission($request) {
if (!self::check_api_key($request)) {
return new WP_Error('wis_unauthorized', 'Ungültiger oder fehlender API-Key.', ['status' => 401]);
}
return true;
}
private static function create_default_categories() {
$default_categories = [
['name' => 'Baublöcke', 'slug' => 'baublocke'],
['name' => 'Dekorationsblöcke', 'slug' => 'dekorationsblocke'],
['name' => 'Redstone', 'slug' => 'redstone'],
['name' => 'Transport', 'slug' => 'transport'],
['name' => 'Natur', 'slug' => 'natur'],
['name' => 'Werkzeuge & Hilfsmittel', 'slug' => 'werkzeuge-hilfsmittel'],
['name' => 'Kampf', 'slug' => 'kampf'],
['name' => 'Nahrung & Tränke', 'slug' => 'nahrung-tranke'],
['name' => 'Zutaten', 'slug' => 'zutaten'],
['name' => 'Spawn-Eier', 'slug' => 'spawn-eier']
];
global $wpdb;
foreach ($default_categories as $cat) {
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}wis_categories WHERE slug = %s",
$cat['slug']
));
if (!$exists) {
$wpdb->insert($wpdb->prefix . 'wis_categories', [
'name' => $cat['name'],
'slug' => $cat['slug']
]);
}
}
}
public static function run_daily_deal() {
if (get_option('wis_daily_deal_enabled') !== '1') return;
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$discount = intval(get_option('wis_daily_deal_discount', 20));
// Altes Daily-Deal-Item vollständig zurücksetzen:
// is_daily_deal, is_offer und offer_price werden alle auf 0 gesetzt,
// damit das Item nicht als normales Angebot mit gesenktem Preis hängen bleibt.
$wpdb->query(
"UPDATE $table
SET is_daily_deal = 0,
is_offer = 0,
offer_price = 0
WHERE is_daily_deal = 1"
);
$item = $wpdb->get_row("SELECT * FROM $table WHERE status = 'publish' AND price > 0 ORDER BY RAND() LIMIT 1");
if ($item) {
$offer_price = max(0, floor($item->price - ($item->price * ($discount / 100))));
$wpdb->update($table, [
'is_daily_deal' => 1,
'is_offer' => 1,
'offer_price' => $offer_price
], ['id' => $item->id]);
}
}
/**
* Fly-Abo Renewal – läuft täglich, handelt aber nur am 1. des Monats.
* Verlängert alle aktiven (nicht gekündigten) Abos automatisch um 30 Tage
* und legt je einen Order-Eintrag als Buchungsnachweis an.
* Gekündigte Abos laufen bis zum letzten Tag des laufenden Monats und werden dann entfernt.
*/
public static function run_abo_renewal() {
// Nur am 1. des Monats ausführen
if (date('j') !== '1') return;
global $wpdb;
$subs_table = $wpdb->prefix . 'wis_fly_abo_subs';
$orders_table = $wpdb->prefix . 'wis_orders';
// Tabelle anlegen falls noch nicht vorhanden (Migration)
self::create_abo_subs_table();
// Alle aktiven, nicht gekündigten Abos verlängern
$active = $wpdb->get_results(
"SELECT * FROM {$subs_table} WHERE cancelled = 0 AND status = 'active'"
);
foreach ($active as $sub) {
// expires_at um 30 Tage verlängern
$wpdb->query($wpdb->prepare(
"UPDATE {$subs_table}
SET expires_at = DATE_ADD(expires_at, INTERVAL 30 DAY),
renewed_at = NOW(),
renewal_count = renewal_count + 1
WHERE id = %d",
$sub->id
));
// Buchungsnachweis als Order anlegen (status: completed)
$wpdb->insert($orders_table, [
'player_name' => $sub->player_name,
'server' => $sub->server,
'item_id' => 'fly_abo_renewal',
'item_title' => '✈ Fly-Abo Verlängerung: ' . $sub->label,
'price' => $sub->price,
'quantity' => 1,
'status' => 'completed',
'response' => json_encode([
'commands' => [[
'type' => 'fly_abo',
'label' => $sub->label,
'price' => $sub->price,
]],
]),
]);
}
// Gekündigte Abos die abgelaufen sind deaktivieren
$wpdb->query(
"UPDATE {$subs_table}
SET status = 'expired'
WHERE cancelled = 1
AND expires_at < NOW()
AND status = 'active'"
);
// Log
$count = count($active);
error_log("[WIS] Fly-Abo Renewal: {$count} Abo(s) verlängert am " . date('d.m.Y'));
}
public static function create_abo_subs_table() {
global $wpdb;
$table = $wpdb->prefix . 'wis_fly_abo_subs';
$wpdb->query("CREATE TABLE IF NOT EXISTS {$table} (
id INT AUTO_INCREMENT PRIMARY KEY,
player_name VARCHAR(64) NOT NULL,
server VARCHAR(64) NOT NULL DEFAULT '',
label VARCHAR(128) NOT NULL,
price INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
cancelled TINYINT(1) NOT NULL DEFAULT 0,
cancelled_at DATETIME DEFAULT NULL,
expires_at DATETIME NOT NULL,
renewed_at DATETIME DEFAULT NULL,
renewal_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_player_server (player_name, server)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
}
/**
* Tabelle für Item-Abo Abonnements anlegen
*/
public static function create_item_abo_subs_table() {
global $wpdb;
$table = $wpdb->prefix . 'wis_item_abo_subs';
$wpdb->query("CREATE TABLE IF NOT EXISTS {$table} (
id INT AUTO_INCREMENT PRIMARY KEY,
player_name VARCHAR(64) NOT NULL,
server VARCHAR(64) NOT NULL DEFAULT '',
item_id VARCHAR(100) NOT NULL,
daily_qty INT NOT NULL DEFAULT 1,
label VARCHAR(128) NOT NULL,
price INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
cancelled TINYINT(1) NOT NULL DEFAULT 0,
cancelled_at DATETIME DEFAULT NULL,
expires_at DATETIME NOT NULL,
last_delivered DATE DEFAULT NULL,
renewal_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY player_server (player_name, server),
KEY status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
}
/**
* Item-Abo Tageslieferung – läuft täglich per Cron.
* Legt für jeden aktiven Abonnenten einen pending Order an,
* damit das Spigot-Plugin die Items ingame ausliefern kann.
*/
public static function run_item_abo_delivery() {
global $wpdb;
$subs_table = $wpdb->prefix . 'wis_item_abo_subs';
$orders_table = $wpdb->prefix . 'wis_orders';
// Tabelle ggf. anlegen (Migration)
self::create_item_abo_subs_table();
$today = date('Y-m-d');
// Alle aktiven, nicht abgelaufenen Abos die heute noch nicht beliefert wurden
$active = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$subs_table}
WHERE status = 'active'
AND (last_delivered IS NULL OR last_delivered < %s)
AND expires_at > NOW()",
$today
));
$count = 0;
foreach ($active as $sub) {
// Payload für Spigot-Plugin
$payload = json_encode([
'items' => [[
'id' => $sub->item_id,
'amount' => intval($sub->daily_qty),
]],
'commands' => [],
'abo_delivery' => true,
]);
$wpdb->insert($orders_table, [
'player_name' => $sub->player_name,
'server' => $sub->server,
'item_id' => 'item_abo_delivery',
'item_title' => '📦 Abo-Lieferung: ' . $sub->label . ' ×' . $sub->daily_qty,
'price' => 0,
'quantity' => intval($sub->daily_qty),
'status' => 'pending',
'response' => $payload,
]);
// last_delivered aktualisieren
$wpdb->update($subs_table, ['last_delivered' => $today], ['id' => $sub->id]);
$count++;
}
// Abgelaufene + gekündigte Abos deaktivieren
$wpdb->query(
"UPDATE {$subs_table}
SET status = 'expired'
WHERE status = 'active'
AND (expires_at < NOW() OR (cancelled = 1 AND expires_at < NOW()))"
);
if ($count > 0) {
error_log("[WIS] Item-Abo Lieferung: {$count} Abo(s) beliefert am {$today}");
}
}
public static function reset_shop() {
global $wpdb;
$tables = [
'wis_items', 'wis_orders', 'wis_coupons',
'wis_servers', 'wis_categories'
];
foreach ($tables as $table) {
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}$table");
}
self::set_default_options();
self::create_default_categories();
return true;
}
public static function reset_sell_log(): bool {
global $wpdb;
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_sell_log");
return true;
}
public static function reset_top_spenders(): bool {
global $wpdb;
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_orders");
return true;
}
public static function reset_analyse(): bool {
global $wpdb;
// Order-Items-Tabelle (Analyse-Grundlage) leeren
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_order_items");
return true;
}
public static function reset_price_history(): bool {
global $wpdb;
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_price_history");
return true;
}
}
// ===========================================================
// ITEM CATEGORIZER
// ===========================================================
class WIS_Item_Categorizer {
public static function auto_categorize($item_id) {
$item_id = strtolower($item_id);
$item_id = str_replace('minecraft:', '', $item_id);
if (strpos($item_id, 'spawn_egg') !== false) {
return ['spawn-eier'];
}
if (preg_match('/(wooden|stone|iron|golden|diamond|netherite|copper)_(pickaxe|axe|shovel|hoe)/', $item_id)) {
return ['werkzeuge-hilfsmittel'];
}
if (in_array($item_id, [
'shears', 'fishing_rod', 'flint_and_steel', 'bucket', 'water_bucket', 'lava_bucket',
'milk_bucket', 'powder_snow_bucket', 'axolotl_bucket', 'cod_bucket', 'pufferfish_bucket',
'salmon_bucket', 'tadpole_bucket', 'tropical_fish_bucket',
'compass', 'recovery_compass', 'clock', 'spyglass', 'map', 'filled_map',
'brush', 'lead', 'name_tag', 'saddle', 'carrot_on_a_stick', 'warped_fungus_on_a_stick'
])) {
return ['werkzeuge-hilfsmittel'];
}
if (preg_match('/(wooden|stone|iron|golden|diamond|netherite|copper)_(sword|spear)/', $item_id)) {
return ['kampf'];
}
if (preg_match('/(leather|chainmail|iron|golden|diamond|netherite|copper|turtle)_(helmet|chestplate|leggings|boots|cap|tunic|pants|shell)/', $item_id)) {
return ['kampf'];
}
if (in_array($item_id, [
'bow', 'crossbow', 'arrow', 'spectral_arrow', 'tipped_arrow',
'shield', 'trident', 'mace',
'totem_of_undying', 'elytra',
'horse_armor', 'iron_horse_armor', 'golden_horse_armor', 'diamond_horse_armor',
'wolf_armor'
]) || strpos($item_id, 'horse_armor') !== false) {
return ['kampf'];
}
if (preg_match('/(raw_|cooked_)?(beef|porkchop|mutton|chicken|rabbit|cod|salmon)/', $item_id)) {
return ['nahrung-tranke'];
}
if (in_array($item_id, [
'apple', 'golden_apple', 'enchanted_golden_apple',
'melon_slice', 'glow_berries', 'sweet_berries', 'chorus_fruit',
'carrot', 'golden_carrot', 'potato', 'baked_potato', 'poisonous_potato',
'beetroot',
'bread', 'cookie', 'cake', 'pumpkin_pie',
'dried_kelp',
'tropical_fish', 'pufferfish', 'rotten_flesh', 'spider_eye'
])) {
return ['nahrung-tranke'];
}
if (strpos($item_id, '_stew') !== false || strpos($item_id, '_soup') !== false) {
return ['nahrung-tranke'];
}
if (strpos($item_id, 'potion') !== false || in_array($item_id, [
'honey_bottle', 'milk_bucket', 'glass_bottle', 'dragon_breath',
'experience_bottle', 'ominous_bottle'
])) {
return ['nahrung-tranke'];
}
if (strpos($item_id, '_boat') !== false || strpos($item_id, '_raft') !== false) {
return ['transport'];
}
if (strpos($item_id, 'minecart') !== false) {
return ['transport'];
}
if (in_array($item_id, ['elytra', 'saddle', 'lead'])) {
return ['transport'];
}
if (strpos($item_id, 'redstone') !== false && $item_id !== 'redstone_ore') {
return ['redstone'];
}
if (in_array($item_id, [
'repeater', 'comparator', 'observer',
'piston', 'sticky_piston',
'dispenser', 'dropper', 'hopper',
'lever', 'tripwire_hook', 'daylight_detector',
'tnt', 'target', 'lightning_rod'
]) || strpos($item_id, 'button') !== false || strpos($item_id, 'pressure_plate') !== false) {
return ['redstone'];
}
if (strpos($item_id, '_rail') !== false || $item_id === 'rail') {
return ['redstone'];
}
$pure_materials = [
'stick', 'coal', 'charcoal', 'diamond', 'emerald', 'lapis_lazuli',
'iron_ingot', 'gold_ingot', 'copper_ingot', 'netherite_ingot',
'iron_nugget', 'gold_nugget', 'copper_nugget',
'raw_iron', 'raw_gold', 'raw_copper',
'netherite_scrap', 'netherite_upgrade',
'amethyst_shard', 'prismarine_shard', 'prismarine_crystals',
'quartz', 'nether_quartz', 'echo_shard', 'disc_fragment',
'string', 'feather', 'leather', 'rabbit_hide',
'slimeball', 'ender_pearl', 'ender_eye',
'blaze_rod', 'blaze_powder', 'magma_cream', 'ghast_tear',
'nether_star', 'nether_brick',
'nautilus_shell', 'heart_of_the_sea', 'scute', 'turtle_scute', 'armadillo_scute',
'bone', 'bone_meal', 'gunpowder', 'glowstone_dust', 'sugar',
'phantom_membrane', 'ink_sac', 'glow_ink_sac',
'paper', 'book', 'flint',
'fermented_spider_eye', 'glistering_melon_slice', 'rabbit_foot',
'nether_wart', 'breeze_rod',
'clay_ball', 'brick', 'firework_star',
'shulker_shell', 'popped_chorus_fruit'
];
if (in_array($item_id, $pure_materials)) {
return ['zutaten'];
}
if (strpos($item_id, '_pottery_sherd') !== false || strpos($item_id, '_armor_trim') !== false) {
return ['zutaten'];
}
if (in_array($item_id, [
'dirt', 'coarse_dirt', 'rooted_dirt', 'grass_block', 'podzol', 'mycelium',
'farmland', 'dirt_path',
'sand', 'red_sand', 'gravel', 'clay',
'suspicious_sand', 'suspicious_gravel'
])) {
return ['natur'];
}
if (strpos($item_id, '_leaves') !== false || strpos($item_id, '_sapling') !== false ||
strpos($item_id, 'azalea') !== false) {
return ['natur'];
}
$flowers = [
'dandelion', 'poppy', 'blue_orchid', 'allium', 'azure_bluet',
'red_tulip', 'orange_tulip', 'white_tulip', 'pink_tulip',
'oxeye_daisy', 'cornflower', 'lily_of_the_valley', 'wither_rose',
'sunflower', 'lilac', 'rose_bush', 'peony',
'pitcher_plant', 'pitcher_pod', 'torchflower', 'torchflower_seeds',
'pink_petals', 'spore_blossom'
];
if (in_array($item_id, $flowers)) {
return ['natur'];
}
if (strpos($item_id, 'mushroom') !== false || strpos($item_id, 'fungus') !== false ||
in_array($item_id, ['short_grass', 'tall_grass', 'fern', 'large_fern', 'dead_bush'])) {
return ['natur'];
}
if (in_array($item_id, [
'seagrass', 'tall_seagrass', 'kelp', 'dried_kelp', 'sea_pickle',
'vine', 'weeping_vines', 'twisting_vines', 'cave_vines', 'glow_lichen',
'hanging_roots', 'mangrove_roots', 'muddy_mangrove_roots'
])) {
return ['natur'];
}
if (strpos($item_id, '_seeds') !== false || in_array($item_id, [
'wheat', 'beetroot', 'carrot', 'potato',
'melon', 'pumpkin', 'carved_pumpkin',
'sugar_cane', 'bamboo', 'cocoa_beans',
'sweet_berries', 'glow_berries', 'sweet_berry_bush',
'nether_wart', 'cactus',
'mangrove_propagule'
])) {
return ['natur'];
}
if (in_array($item_id, [
'crimson_roots', 'warped_roots', 'nether_sprouts',
'crimson_nylium', 'warped_nylium'
])) {
return ['natur'];
}
if (in_array($item_id, [
'moss_block', 'moss_carpet',
'big_dripleaf', 'small_dripleaf',
'lily_pad', 'bee_nest', 'honeycomb', 'honeycomb_block',
'snow', 'snowball', 'powder_snow'
])) {
return ['natur'];
}
if (strpos($item_id, 'glass') !== false && strpos($item_id, '_pane') === false) {
return ['dekorationsblocke'];
}
if (strpos($item_id, 'glass_pane') !== false || strpos($item_id, 'stained_glass_pane') !== false) {
return ['dekorationsblocke'];
}
if (strpos($item_id, '_door') !== false || strpos($item_id, '_trapdoor') !== false ||
strpos($item_id, '_fence_gate') !== false) {
return ['dekorationsblocke'];
}
if ((strpos($item_id, '_fence') !== false && strpos($item_id, '_fence_gate') === false) ||
(strpos($item_id, '_wall') !== false && !in_array($item_id, ['wall_banner', 'wall_sign', 'wall_torch'])) ||
$item_id === 'iron_bars') {
return ['dekorationsblocke'];
}
if (strpos($item_id, '_stairs') !== false || strpos($item_id, '_slab') !== false) {
return ['dekorationsblocke'];
}
if (strpos($item_id, '_carpet') !== false) {
return ['dekorationsblocke'];
}
if (in_array($item_id, [
'torch', 'soul_torch', 'lantern', 'soul_lantern',
'campfire', 'soul_campfire', 'end_rod',
'shroomlight', 'froglight', 'sea_lantern'
]) || strpos($item_id, '_candle') !== false || $item_id === 'candle') {
return ['dekorationsblocke'];
}
if (in_array($item_id, [
'crafting_table', 'furnace', 'blast_furnace', 'smoker',
'chest', 'trapped_chest', 'ender_chest', 'barrel',
'enchanting_table', 'anvil', 'chipped_anvil', 'damaged_anvil',
'grindstone', 'smithing_table', 'cartography_table', 'fletching_table',
'loom', 'stonecutter', 'brewing_stand', 'cauldron', 'composter',
'lectern', 'bookshelf', 'chiseled_bookshelf',
'bell', 'beacon', 'conduit', 'lodestone', 'respawn_anchor'
]) || strpos($item_id, '_bed') !== false || strpos($item_id, 'shulker_box') !== false) {
return ['dekorationsblocke'];
}
if (strpos($item_id, '_sign') !== false || strpos($item_id, '_hanging_sign') !== false ||
strpos($item_id, '_banner') !== false ||
in_array($item_id, ['item_frame', 'glow_item_frame', 'painting', 'armor_stand'])) {
return ['dekorationsblocke'];
}
if (in_array($item_id, [
'ladder', 'scaffolding', 'chain',
'flower_pot', 'decorated_pot',
'dragon_egg', 'dragon_head',
'note_block', 'jukebox'
]) || strpos($item_id, '_head') !== false || strpos($item_id, '_skull') !== false ||
strpos($item_id, 'coral') !== false) {
return ['dekorationsblocke'];
}
if (in_array($item_id, [
'stone', 'cobblestone', 'mossy_cobblestone',
'granite', 'polished_granite', 'diorite', 'polished_diorite',
'andesite', 'polished_andesite', 'calcite', 'tuff',
'smooth_stone'
]) || strpos($item_id, 'stone_brick') !== false) {
return ['baublocke'];
}
if (strpos($item_id, 'deepslate') !== false) {
return ['baublocke'];
}
if (strpos($item_id, 'brick') !== false && $item_id !== 'brick' && $item_id !== 'nether_brick') {
return ['baublocke'];
}
if (strpos($item_id, 'sandstone') !== false) {
return ['baublocke'];
}
if (strpos($item_id, 'quartz_') !== false && strpos($item_id, 'nether_quartz') === false) {
return ['baublocke'];
}
if (strpos($item_id, 'prismarine') !== false || strpos($item_id, 'purpur') !== false) {
return ['baublocke'];
}
if (strpos($item_id, 'concrete') !== false || strpos($item_id, 'terracotta') !== false) {
return ['baublocke'];
}
if (strpos($item_id, '_wool') !== false) {
return ['baublocke'];
}
if (strpos($item_id, '_planks') !== false ||
(strpos($item_id, '_log') !== false && strpos($item_id, 'stripped') === false) ||
(strpos($item_id, '_wood') !== false && strpos($item_id, 'stripped') === false)) {
return ['baublocke'];
}
if (strpos($item_id, 'stripped_') !== false) {
return ['baublocke'];
}
if (in_array($item_id, ['bamboo_block', 'bamboo_mosaic', 'crimson_stem', 'warped_stem',
'crimson_hyphae', 'warped_hyphae'])) {
return ['baublocke'];
}
if (strpos($item_id, 'copper_') !== false && strpos($item_id, '_ingot') === false &&
strpos($item_id, '_nugget') === false && $item_id !== 'copper_door' && $item_id !== 'copper_trapdoor') {
return ['baublocke'];
}
if (strpos($item_id, '_block') !== false) {
return ['baublocke'];
}
if (in_array($item_id, [
'netherrack', 'soul_sand', 'soul_soil',
'basalt', 'polished_basalt', 'smooth_basalt',
'end_stone', 'obsidian', 'crying_obsidian'
]) || strpos($item_id, 'blackstone') !== false || strpos($item_id, 'end_stone_brick') !== false) {
return ['baublocke'];
}
if (in_array($item_id, [
'glowstone', 'sponge', 'wet_sponge',
'ice', 'packed_ice', 'blue_ice',
'magma_block', 'slime_block', 'honey_block',
'hay_block', 'dried_kelp_block',
'mud', 'packed_mud', 'mud_bricks',
'dripstone_block', 'amethyst_block', 'budding_amethyst'
])) {
return ['baublocke'];
}
return ['baublocke'];
}
}
// ===========================================================
// DATABASE HELPER
// ===========================================================
class WIS_DB {
/**
* Gibt die Bild-URL eines Items zurück.
* Priorität: custom_image_url > automatisch generierte URL aus item_id.
*/
public static function get_item_image($item) {
if (!empty($item->custom_image_url)) {
return esc_url($item->custom_image_url);
}
$img_base = get_option('wis_image_base_url', '');
$img_name = str_replace(':', '_', $item->item_id) . '.png';
return $img_base . $img_name;
}
public static function get_items($args = []) {
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$where_parts = ["1=1"];
if (isset($args['status'])) {
$where_parts[] = $wpdb->prepare("status = %s", $args['status']);
}
if (isset($args['category_slug']) && !empty($args['category_slug'])) {
$search_pattern = '%"' . $args['category_slug'] . '"%';
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
}
if (isset($args['ids']) && is_array($args['ids']) && !empty($args['ids'])) {
$ids = array_map('intval', $args['ids']);
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$where_parts[] = sprintf("id IN ($placeholders)", ...$ids);
}
if (isset($args['search']) && !empty($args['search'])) {
$search_like = '%' . $wpdb->esc_like($args['search']) . '%';
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
}
$where = implode(" AND ", $where_parts);
$limit = isset($args['limit']) ? "LIMIT " . intval($args['limit']) : "";
$orderby = isset($args['orderby']) ? "ORDER BY " . sanitize_text_field($args['orderby']) : "ORDER BY name ASC";
return $wpdb->get_results("SELECT * FROM $table WHERE $where $orderby $limit");
}
public static function count_items($args = []) {
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$where_parts = ["1=1"];
if (isset($args['status'])) {
$where_parts[] = $wpdb->prepare("status = %s", $args['status']);
}
if (isset($args['category_slug']) && !empty($args['category_slug'])) {
$search_pattern = '%"' . $args['category_slug'] . '"%';
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
}
if (isset($args['search']) && !empty($args['search'])) {
$search_like = '%' . $wpdb->esc_like($args['search']) . '%';
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
}
$where = implode(" AND ", $where_parts);
return (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where");
}
public static function get_item($id) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_items WHERE id = %d",
$id
));
}
public static function get_item_by_item_id($item_id) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_items WHERE item_id = %s",
$item_id
));
}
public static function insert_item($data) {
global $wpdb;
if (empty($data['categories']) || $data['categories'] === '[]') {
$auto_cats = WIS_Item_Categorizer::auto_categorize($data['item_id']);
$data['categories'] = json_encode($auto_cats);
}
return $wpdb->insert($wpdb->prefix . 'wis_items', $data);
}
public static function update_item($id, $data) {
global $wpdb;
return $wpdb->update($wpdb->prefix . 'wis_items', $data, ['id' => $id]);
}
public static function delete_item($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_items', ['id' => $id]);
}
public static function get_servers() {
global $wpdb;
return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wis_servers ORDER BY name ASC");
}
public static function insert_server($slug, $name) {
global $wpdb;
return $wpdb->insert($wpdb->prefix . 'wis_servers', [
'slug' => sanitize_title($slug),
'name' => sanitize_text_field($name)
]);
}
public static function delete_server($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_servers', ['id' => $id]);
}
public static function get_categories() {
global $wpdb;
$table = $wpdb->prefix . 'wis_categories';
// Prüfen ob sort_order-Spalte existiert (Migration ggf. noch nicht gelaufen)
$has_sort = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'sort_order'");
if ($has_sort) {
return $wpdb->get_results("SELECT * FROM $table ORDER BY parent_id ASC, sort_order ASC, name ASC");
}
return $wpdb->get_results("SELECT * FROM $table ORDER BY parent_id ASC, name ASC");
}
public static function insert_category($name, $parent_id = 0) {
global $wpdb;
// sort_order = höchster bestehender Wert + 10
$max_order = (int) $wpdb->get_var("SELECT MAX(sort_order) FROM {$wpdb->prefix}wis_categories WHERE parent_id = " . intval($parent_id));
return $wpdb->insert($wpdb->prefix . 'wis_categories', [
'parent_id' => intval($parent_id),
'sort_order' => $max_order + 10,
'name' => sanitize_text_field($name),
'slug' => sanitize_title($name),
]);
}
public static function delete_category($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_categories', ['id' => $id]);
}
public static function get_coupons() {
global $wpdb;
return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wis_coupons ORDER BY created_at DESC");
}
public static function get_coupon_by_code($code) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_coupons WHERE code = %s",
strtoupper($code)
));
}
/**
* Prüft ob ein Spieler einen Gutschein bereits eingelöst hat.
*/
public static function coupon_used_by_player($coupon_id, $player_name) {
global $wpdb;
$table = $wpdb->prefix . 'wis_coupon_uses';
// Tabelle existiert? (Fallback für alte Installationen)
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) return false;
return (bool) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $table WHERE coupon_id = %d AND player_name = %s",
$coupon_id, $player_name
));
}
/**
* Schreibt die Einlösung eines Gutscheins durch einen Spieler.
* Nutzt INSERT IGNORE damit der UNIQUE KEY als Doppelschutz wirkt.
*/
public static function record_coupon_use($coupon_id, $player_name) {
global $wpdb;
$table = $wpdb->prefix . 'wis_coupon_uses';
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) return false;
return $wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO $table (coupon_id, player_name) VALUES (%d, %s)",
$coupon_id, $player_name
));
}
public static function insert_coupon($data) {
global $wpdb;
$data['code'] = strtoupper($data['code']);
return $wpdb->insert($wpdb->prefix . 'wis_coupons', $data);
}
public static function update_coupon($id, $data) {
global $wpdb;
if (isset($data['code'])) {
$data['code'] = strtoupper($data['code']);
}
return $wpdb->update($wpdb->prefix . 'wis_coupons', $data, ['id' => $id]);
}
public static function delete_coupon($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_coupons', ['id' => $id]);
}
public static function get_orders($limit = 100) {
global $wpdb;
return $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_orders ORDER BY created_at DESC LIMIT %d",
$limit
));
}
public static function get_order($id) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wis_orders WHERE id = %d",
$id
));
}
public static function insert_order($data) {
global $wpdb;
return $wpdb->insert($wpdb->prefix . 'wis_orders', $data);
}
public static function update_order_status($id, $status) {
global $wpdb;
return $wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => $status],
['id' => $id]
);
}
public static function delete_order($id) {
global $wpdb;
return $wpdb->delete($wpdb->prefix . 'wis_orders', ['id' => $id]);
}
}
// ===========================================================
// ADMIN PAGES
// ===========================================================
class WIS_Admin {
// Wird auf admin_init gefeuert – vor jeder HTML-Ausgabe, damit wp_redirect() funktioniert
public static function handle_save_item() {
if (!isset($_POST['wis_save_item'])) return;
if (!current_user_can('manage_options')) return;
check_admin_referer('wis_item_form');
global $wpdb;
$item_type = sanitize_text_field($_POST['item_type'] ?? 'minecraft');
if ($item_type === 'gift_card') {
$gc_min = max(1, intval($_POST['gift_card_min'] ?? 100));
$gc_max = max($gc_min, intval($_POST['gift_card_max'] ?? 5000));
// item_id kodiert Min+Max – eindeutig und vom create_order erkennbar
$resolved_item_id = 'gift_card_' . $gc_min . '_' . $gc_max;
$_POST['item_id'] = $resolved_item_id;
// Preis = Mindestwert (wird im Frontend durch Nutzereingabe überschrieben)
$_POST['price'] = $gc_min;
} elseif ($item_type === 'fly') {
$resolved_item_id = sanitize_text_field($_POST['fly_duration'] ?? 'fly_5min');
} elseif ($item_type === 'rank') {
$rank_id = preg_replace('/[^a-z0-9_\-]/', '', strtolower($_POST['rank_id'] ?? 'vip'));
$lp_group = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_POST['lp_group'] ?? $rank_id);
$default_group = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_POST['default_group'] ?? 'default');
$rank_days = max(0, intval($_POST['rank_days'] ?? 30));
if (empty($lp_group)) $lp_group = $rank_id;
if (empty($default_group)) $default_group = 'default';
$resolved_item_id = 'rank_' . $rank_id . '_' . $lp_group . '_' . $default_group . '_' . $rank_days;
} elseif ($item_type === 'fly_abo') {
$resolved_item_id = 'fly_abo';
} elseif ($item_type === 'plot_slots') {
$plot_extra_slots = max(1, intval($_POST['plot_extra_slots'] ?? 1));
$resolved_item_id = 'plot_slots_' . $plot_extra_slots;
} elseif ($item_type === 'plot_abo') {
$plot_abo_slots = max(1, intval($_POST['plot_abo_slots'] ?? 1));
$resolved_item_id = 'plot_abo_' . $plot_abo_slots;
} elseif ($item_type === 'item_abo') {
$abo_item_id = sanitize_text_field($_POST['abo_item_id'] ?? '');
$abo_daily_qty = max(1, intval($_POST['abo_daily_qty'] ?? 1));
$abo_duration = max(1, intval($_POST['abo_duration_days'] ?? 30));
if (empty($abo_item_id)) $abo_item_id = 'minecraft:stone';
$resolved_item_id = 'item_abo_' . sanitize_text_field($abo_item_id) . '_' . $abo_daily_qty . '_' . $abo_duration;
} elseif ($item_type === 'custom_cmd') {
$cmd_slug = preg_replace('/[^a-z0-9_\-]/', '', strtolower($_POST['custom_cmd_id'] ?? 'custom'));
if (empty($cmd_slug)) $cmd_slug = 'custom_' . time();
$resolved_item_id = 'custom_cmd_' . $cmd_slug;
} else {
$resolved_item_id = sanitize_text_field($_POST['item_id'] ?? '');
}
if (empty($resolved_item_id) || empty(trim($_POST['name'] ?? ''))) {
// Fehler als transient speichern, damit page_items() ihn anzeigen kann
set_transient('wis_save_error_' . get_current_user_id(), '❌ Name und Item-ID sind Pflichtfelder.', 30);
wp_redirect(wp_get_referer() ?: admin_url('admin.php?page=wis_items'));
exit;
}
$data = [
'item_id' => $resolved_item_id,
'name' => sanitize_text_field($_POST['name']),
'description' => sanitize_textarea_field($_POST['description'] ?? ''),
'price' => intval($_POST['price'] ?? 0),
'offer_price' => intval($_POST['offer_price'] ?? 0),
'is_offer' => isset($_POST['is_offer']) ? 1 : 0,
'servers' => isset($_POST['servers']) ? json_encode(array_map('sanitize_text_field', $_POST['servers'])) : '[]',
'categories' => isset($_POST['categories']) ? json_encode(array_map('sanitize_text_field', $_POST['categories'])) : '[]',
'custom_image_url' => esc_url_raw($_POST['custom_image_url'] ?? ''),
'custom_command' => $item_type === 'custom_cmd' ? sanitize_text_field($_POST['custom_command'] ?? '') : null,
'sell_enabled' => isset($_POST['sell_enabled']) ? 1 : 0,
'sell_price_mode' => in_array($_POST['sell_price_mode'] ?? '', ['percent','fixed','minus']) ? $_POST['sell_price_mode'] : 'percent',
'sell_price_value' => max(0, intval($_POST['sell_price_value'] ?? 80)),
'daily_sell_limit' => max(0, intval($_POST['daily_sell_limit'] ?? 0)),
'status' => (intval($_POST['price'] ?? 0) > 0 || $item_type === 'fly' || $item_type === 'rank' || $item_type === 'fly_abo' || $item_type === 'plot_slots' || $item_type === 'plot_abo' || $item_type === 'item_abo' || $item_type === 'custom_cmd' || $item_type === 'gift_card') ? 'publish' : 'draft',
];
// Preishistorie loggen wenn ein bestehendes Item bearbeitet wird
$price_history_table = $wpdb->prefix . 'wis_price_history';
$ph_exists = $wpdb->get_var("SHOW TABLES LIKE '$price_history_table'");
$editor = wp_get_current_user()->user_login ?: 'admin';
$edit_id = intval($_POST['edit_id'] ?? $_GET['edit'] ?? 0);
if ($edit_id && $ph_exists) {
$existing_item = WIS_DB::get_item($edit_id);
if ($existing_item) {
foreach (['price' => 'Verkaufspreis', 'offer_price' => 'Angebotspreis', 'sell_price_value' => 'Ankaufswert'] as $field => $label) {
$old_val = intval($existing_item->$field ?? 0);
$new_val = intval($data[$field] ?? 0);
if ($old_val !== $new_val) {
$wpdb->insert($price_history_table, [
'item_id' => $existing_item->item_id,
'item_name' => $existing_item->name,
'field' => $label,
'old_value' => $old_val,
'new_value' => $new_val,
'changed_by' => $editor,
]);
}
}
}
}
if (isset($_GET['edit'])) {
$result = WIS_DB::update_item(intval($_GET['edit']), $data);
if ($result !== false) {
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . intval($_GET['edit']) . '&saved=1'));
exit;
} else {
set_transient('wis_save_error_' . get_current_user_id(), '❌ Fehler beim Speichern: ' . $wpdb->last_error, 30);
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . intval($_GET['edit'])));
exit;
}
} else {
$existing = WIS_DB::get_item_by_item_id($resolved_item_id);
if ($existing) {
WIS_DB::update_item($existing->id, $data);
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . $existing->id . '&saved=1'));
exit;
}
$result = WIS_DB::insert_item($data);
if ($result) {
$new_id = $wpdb->insert_id;
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . $new_id . '&created=1'));
exit;
} else {
$err = $wpdb->last_error ?: 'Unbekannter Fehler.';
set_transient('wis_save_error_' . get_current_user_id(), '❌ Fehler beim Erstellen: ' . $err, 30);
wp_redirect(admin_url('admin.php?page=wis_items&add=1'));
exit;
}
}
}
public static function register_menu() {
add_menu_page(
'Ingame Shop',
'Ingame Shop',
'manage_options',
'wis_shop',
[self::class, 'page_overview'],
'dashicons-cart',
6
);
add_submenu_page('wis_shop', 'Einstellungen', 'Einstellungen', 'manage_options', 'wis_shop');
add_submenu_page('wis_shop', 'Items', 'Items', 'manage_options', 'wis_items', [self::class, 'page_items']);
add_submenu_page('wis_shop', 'Bestellungen', 'Bestellungen', 'manage_options', 'wis_orders', [self::class, 'page_orders']);
add_submenu_page('wis_shop', 'Server', 'Server', 'manage_options', 'wis_servers', [self::class, 'page_servers']);
add_submenu_page('wis_shop', 'Kategorien', 'Kategorien', 'manage_options', 'wis_categories', [self::class, 'page_categories']);
add_submenu_page('wis_shop', 'Angebote', 'Angebote', 'manage_options', 'wis_angebote', [self::class, 'page_angebote']);
add_submenu_page('wis_shop', 'Gutscheine', 'Gutscheine', 'manage_options', 'wis_coupons', [self::class, 'page_coupons']);
add_submenu_page('wis_shop', 'Analyse', 'Analyse', 'manage_options', 'wis_analyse', [self::class, 'page_analyse']);
add_submenu_page('wis_shop', 'Top Spender', 'Top Spender', 'manage_options', 'wis_top_spenders', [self::class, 'page_top_spenders']);
add_submenu_page('wis_shop', 'Ankauf-Log', 'Ankauf-Log', 'manage_options', 'wis_sell_log', [self::class, 'page_sell_log']);
add_submenu_page('wis_shop', 'Preishistorie', 'Preishistorie', 'manage_options', 'wis_price_history', [self::class, 'page_price_history']);
add_submenu_page('wis_shop', 'Abo-Verwaltung', 'Abo-Verwaltung', 'manage_options', 'wis_abo_admin', [self::class, 'page_abo_admin']);
add_submenu_page('wis_shop', 'JSON Export/Import', 'JSON Tools', 'manage_options', 'wis_json', [self::class, 'page_json']);
add_submenu_page('wis_shop', 'Shop Reset', 'Reset', 'manage_options', 'wis_reset', [self::class, 'page_reset']);
}
public static function page_overview() {
if (isset($_POST['wis_save_settings']) && check_admin_referer('wis_settings')) {
update_option('wis_currency_name', sanitize_text_field($_POST['wis_currency_name']));
update_option('wis_image_base_url', esc_url_raw($_POST['wis_image_base_url']));
update_option('wis_header_text', wp_kses_post($_POST['wis_header_text']));
update_option('wis_coupon_exclude_offers', isset($_POST['wis_coupon_exclude_offers']) ? '1' : '0');
update_option('wis_daily_deal_discount', intval($_POST['wis_daily_deal_discount']));
update_option('wis_offline_queue_enabled', isset($_POST['wis_offline_queue_enabled']) ? '1' : '0');
update_option('wis_tax_enabled', isset($_POST['wis_tax_enabled']) ? '1' : '0');
update_option('wis_tax_rate', max(0, min(100, floatval(str_replace(',', '.', $_POST['wis_tax_rate'] ?? '0')))));
$allowed_pp = ['24', '25', '50', '100', '-1'];
$pp_val = sanitize_text_field($_POST['wis_default_per_page'] ?? '24');
update_option('wis_default_per_page', in_array($pp_val, $allowed_pp) ? $pp_val : '25');
echo '✅ 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:
🔑 Spigot API-Key
Dieser Key muss in der config.yml des Spigot-Plugins als api-key eingetragen werden.
📋 Kopieren
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.
✅ 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 '';
}
}
// 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 '';
}
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 = [];
}
?>
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';
?>
'publish'] : [],
!empty($search_query) ? ['search' => $search_query] : []
));
$root_cats_tab = array_values(array_filter($categories, fn($c) => $c->parent_id == 0));
$sub_idx_tab = [];
foreach ($categories as $c) {
if ($c->parent_id != 0) $sub_idx_tab[$c->parent_id][] = $c;
}
// Aktive Hauptkategorie bestimmen (direkt oder via Unterkategorie)
$active_root_slug = '';
foreach ($root_cats_tab as $rc) {
if ($current_category === $rc->slug) { $active_root_slug = $rc->slug; break; }
if (!empty($sub_idx_tab[$rc->id])) {
foreach ($sub_idx_tab[$rc->id] as $sc) {
if ($current_category === $sc->slug) { $active_root_slug = $rc->slug; break 2; }
}
}
}
?>
Alle
slug);
$cat_count = WIS_DB::count_items(array_merge(
$hide_drafts ? ['status' => 'publish'] : [],
['category_slug' => $cat->slug],
!empty($search_query) ? ['search' => $search_query] : []
));
?>
name); ?>
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); ?>
()
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): ?>
✅ 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 '';
}
$servers = WIS_DB::get_servers();
?>
Server
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 '';
}
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 '';
}
$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
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.
💾 Reihenfolge speichern
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 '';
} else {
$data['used_count'] = 0;
WIS_DB::insert_coupon($data);
echo '';
}
}
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
check_admin_referer('wis_coupon_action', '_wpnonce');
WIS_DB::delete_coupon(intval($_GET['id']));
echo '';
}
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');
?>
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;
}
}
?>
bulk_id)) $bulk_groups[$c->bulk_id][] = $c;
}
?>
📦 Generierte Bulk-Gruppen
Bulk-ID
Codes
Rabatt
Gültig bis
Aktionen
$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');
?>
value); ?>type==='percent'?'%':' '.esc_html(get_option('wis_currency_name','Coins')); ?>
expiry ? esc_html(date('d.m.Y', strtotime($sample->expiry))) : '∞'; ?>
📥 CSV Export
Code
Rabatt
Genutzt
Gültig bis
Eingelöst von
Aktionen
Noch keine Gutscheine vorhanden.
id] ?? [];
?>
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 '';
}
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
ID id); ?>
Datum created_at))); ?>
Käufer player_name); ?>
gift_recipient)): ?>
🎁 Geschenk für gift_recipient); ?>
Server server); ?>
Zusammenfassung item_title); ?>
Preis price); ?>
Status status] ?? $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'];
?>
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");
?>
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");
?>
'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 '
'.esc_textarea($json_output).' ';
echo '
💾 Als items.json herunterladen
';
echo '';
echo '
📤 Nächste Schritte: ';
echo '
';
echo 'Lade die JSON-Datei herunter ';
echo 'Gehe zu deinem Gitea Repository ';
echo 'Lade die items.json hoch unter: https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro ';
echo 'Klicke dann auf den Quick-Import Button unten! ';
echo ' ';
echo '
';
return;
}
$default_url = 'https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/raw/branch/main/items.json';
?>
📦 JSON Export/Import
⚡ Quick-Import von Gitea
Importiert direkt von deinem Gitea Repository!
⚡ Quick-Import starten
📥 JSON Import (Manuelle URL)
Importiere Items aus einer beliebigen JSON-URL.
✅ Shop wurde komplett zurückgesetzt!
';
}
if (isset($_POST['wis_reset_sell_log'])) {
check_admin_referer('wis_reset_sell_log');
WIS_Activator::reset_sell_log();
$msg = '✅ Ankauf-Log wurde geleert!
';
}
if (isset($_POST['wis_reset_top_spenders'])) {
check_admin_referer('wis_reset_top_spenders');
WIS_Activator::reset_top_spenders();
$msg = '✅ Top-Spender-Daten wurden zurückgesetzt!
';
}
if (isset($_POST['wis_reset_analyse'])) {
check_admin_referer('wis_reset_analyse');
WIS_Activator::reset_analyse();
$msg = '✅ Analyse-Daten wurden geleert!
';
}
if (isset($_POST['wis_reset_price_history'])) {
check_admin_referer('wis_reset_price_history');
WIS_Activator::reset_price_history();
$msg = '✅ Preishistorie wurde geleert!
';
}
?>
0) {
if ($abo_type === 'fly') {
$table = $wpdb->prefix . 'wis_fly_abo_subs';
} else {
$table = $wpdb->prefix . 'wis_item_abo_subs';
}
if ($action === 'cancel') {
$wpdb->update($table,
['cancelled' => 1, 'cancelled_at' => current_time('mysql')],
['id' => $abo_id]
);
echo '✅ Abo #' . $abo_id . ' gekündigt.
';
} elseif ($action === 'delete') {
$wpdb->delete($table, ['id' => $abo_id]);
echo '🗑️ Abo #' . $abo_id . ' gelöscht.
';
} elseif ($action === 'reactivate') {
$wpdb->update($table,
['cancelled' => 0, 'cancelled_at' => null, 'status' => 'active'],
['id' => $abo_id]
);
echo '✔ Abo #' . $abo_id . ' reaktiviert.
';
}
}
}
// ── Filter ───────────────────────────────────────────────────────
$f_player = sanitize_text_field($_GET['player'] ?? '');
$f_type = sanitize_key($_GET['abo_type'] ?? 'all');
$f_status = sanitize_key($_GET['abo_status'] ?? 'active');
$currency = esc_html(get_option('wis_currency_name', '$'));
// ── Fly-Abos laden ────────────────────────────────────────────────
$fly_table = $wpdb->prefix . 'wis_fly_abo_subs';
$fly_exists = $wpdb->get_var("SHOW TABLES LIKE '{$fly_table}'");
$fly_rows = [];
if ($fly_exists && in_array($f_type, ['all', 'fly'])) {
$where = '1=1';
$args = [];
if ($f_status === 'active') { $where .= " AND status = 'active' AND cancelled = 0 AND expires_at > NOW()"; }
elseif ($f_status === 'cancelled') { $where .= " AND cancelled = 1"; }
elseif ($f_status === 'expired') { $where .= " AND (status = 'expired' OR expires_at <= NOW())"; }
if ($f_player) { $where .= " AND player_name LIKE %s"; $args[] = '%' . $f_player . '%'; }
$sql = "SELECT *, 'fly' AS abo_type FROM {$fly_table} WHERE {$where} ORDER BY created_at DESC LIMIT 200";
$fly_rows = $args ? $wpdb->get_results($wpdb->prepare($sql, ...$args)) : $wpdb->get_results($sql);
}
// ── Item-Abos laden ───────────────────────────────────────────────
$item_table = $wpdb->prefix . 'wis_item_abo_subs';
$item_exists = $wpdb->get_var("SHOW TABLES LIKE '{$item_table}'");
$item_rows = [];
if ($item_exists && in_array($f_type, ['all', 'item'])) {
$where = '1=1';
$args = [];
if ($f_status === 'active') { $where .= " AND status = 'active' AND cancelled = 0 AND expires_at > NOW()"; }
elseif ($f_status === 'cancelled') { $where .= " AND cancelled = 1"; }
elseif ($f_status === 'expired') { $where .= " AND (status = 'expired' OR expires_at <= NOW())"; }
if ($f_player) { $where .= " AND player_name LIKE %s"; $args[] = '%' . $f_player . '%'; }
$sql = "SELECT *, 'item' AS abo_type FROM {$item_table} WHERE {$where} ORDER BY created_at DESC LIMIT 200";
$item_rows = $args ? $wpdb->get_results($wpdb->prepare($sql, ...$args)) : $wpdb->get_results($sql);
}
// Zusammenführen und nach Datum sortieren
$all_rows = array_merge($fly_rows, $item_rows);
usort($all_rows, fn($a, $b) => strcmp($b->created_at, $a->created_at));
$nonce = wp_create_nonce('wis_abo_admin_action');
$base_url = admin_url('admin.php?page=wis_abo_admin');
?>
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)' : ''; ?>
📈 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.
prefix . 'wis_items';
$currency = get_option('wis_currency_name', 'Coins');
$nonce = wp_create_nonce('wis_angebote_nonce');
$filter = sanitize_text_field($_GET['filter'] ?? 'all');
$search = sanitize_text_field($_GET['s'] ?? '');
$where = "1=1";
if ($filter === 'offer') $where .= " AND is_offer = 1 AND is_daily_deal = 0";
elseif ($filter === 'daily') $where .= " AND is_daily_deal = 1";
elseif ($filter === 'none') $where .= " AND is_offer = 0 AND is_daily_deal = 0";
else $where .= " AND (is_offer = 1 OR is_daily_deal = 1)";
if ($search !== '') {
$like = '%' . $wpdb->esc_like($search) . '%';
$where .= $wpdb->prepare(" AND (name LIKE %s OR item_id LIKE %s)", $like, $like);
}
if ($filter !== 'none') {
$where .= " AND status = 'publish'";
}
$items = $wpdb->get_results("SELECT * FROM $table WHERE $where ORDER BY is_daily_deal DESC, name ASC");
$count_offer = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_offer = 1 AND is_daily_deal = 0 AND status='publish'");
$count_daily = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_daily_deal = 1 AND status='publish'");
$count_all = $count_offer + $count_daily;
$img_base = get_option('wis_image_base_url', '');
$base_url = admin_url('admin.php?page=wis_angebote');
?>
🔥 Angebote Übersicht
Alle Items die als Angebot oder Daily Deal markiert sind – mit direkter Bearbeitung.
"🔥 Alle Angebote ($count_all) ",
'offer' => "🏷️ Nur Angebote ($count_offer) ",
'daily' => "🎁 Daily Deal ($count_daily) ",
'none' => "📦 Ohne Kennzeichnung",
];
$i = 0;
foreach ($filters as $key => $label) {
$active = ($filter === $key) ? 'current' : '';
$sep = (++$i < count($filters)) ? ' | ' : '';
echo "$label $sep ";
}
?>
Suchen
✕ Zurücksetzen
📭
Keine Items gefunden.
Item(s) gefunden.
'POST',
'callback' => [self::class, 'import_json'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/order', [
'methods' => 'POST',
'callback' => [self::class, 'create_order'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/validate_coupon', [
'methods' => 'POST',
'callback' => [self::class, 'validate_coupon'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/shop_items', [
'methods' => 'GET',
'callback' => [self::class, 'get_shop_items'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/pending_orders', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_orders'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/execute_order', [
'methods' => 'POST',
'callback' => [self::class, 'execute_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/complete_order', [
'methods' => 'POST',
'callback' => [self::class, 'complete_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/cancel_order', [
'methods' => 'POST',
'callback' => [self::class, 'cancel_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
// Gift-System Endpunkte (ab v6.5-gift)
register_rest_route('wis/v1', '/pending_gifts', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_gifts'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/gift_accept', [
'methods' => 'POST',
'callback' => [self::class, 'gift_accept'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/gift_decline', [
'methods' => 'POST',
'callback' => [self::class, 'gift_decline'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/pending_offline', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_offline'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/my_coupons', [
'methods' => 'GET',
'callback' => [self::class, 'get_my_coupons'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/orders_history', [
'methods' => 'GET',
'callback' => [self::class, 'get_orders_history'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
// Ankauf-Endpunkte (ab v6.5)
register_rest_route('wis/v1', '/sell_items', [
'methods' => 'GET',
'callback' => [self::class, 'get_sell_items'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/sell_item', [
'methods' => 'POST',
'callback' => [self::class, 'process_sell'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
// ── Item-Abo Endpoints ────────────────────────────────────────────
register_rest_route('wis/v1', '/item_abo_status', [
'methods' => 'GET',
'callback' => [self::class, 'item_abo_status'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/item_abo_cancel', [
'methods' => 'POST',
'callback' => [self::class, 'item_abo_cancel'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/trigger_abo_delivery', [
'methods' => 'POST',
'callback' => [self::class, 'trigger_abo_delivery'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
}
// =========================================================
// ANKAUF – Endpunkte (ab v6.5)
// =========================================================
/**
* GET /wis/v1/sell_items?server=
* 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);
// Vor dem Update: Order-Payload lesen, um gift_card_codes zu extrahieren
$order = $wpdb->get_row($wpdb->prepare(
"SELECT response FROM {$wpdb->prefix}wis_orders WHERE id = %d",
$id
));
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'completed'],
['id' => $id]
);
// Gift-Card-Codes aus dem gespeicherten Payload sammeln und zurückgeben
$gift_card_codes = [];
if ($order && !empty($order->response)) {
$payload = json_decode($order->response, true);
if (isset($payload['commands']) && is_array($payload['commands'])) {
$currency = get_option('wis_currency_name', 'Coins');
foreach ($payload['commands'] as $cmd) {
if (($cmd['type'] ?? '') === 'gift_card') {
$gift_card_codes[] = [
'code' => $cmd['code'],
'amount' => $cmd['amount'],
'label' => $cmd['label'] ?? '',
];
}
}
}
}
return new WP_REST_Response(['success' => true, 'gift_card_codes' => $gift_card_codes]);
}
/**
* GET /wis/v1/my_coupons?player=Spielername
* Gibt alle ungenutzten Gift-Card-Gutscheine zurück, die ein Spieler gekauft hat.
* Verknüpfung: wis_orders.response enthält die gift_card codes → wis_coupons.used_count = 0
*/
public static function get_my_coupons($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player') ?? '');
if (!$player) {
return new WP_REST_Response(['success' => false, 'message' => 'Kein Spielername angegeben'], 400);
}
$currency = get_option('wis_currency_name', 'Coins');
// Alle abgeschlossenen Orders des Spielers mit gift_card commands holen
$orders = $wpdb->get_results($wpdb->prepare(
"SELECT response FROM {$wpdb->prefix}wis_orders
WHERE player_name = %s
AND status = 'completed'
AND response LIKE '%gift_card%'
ORDER BY created_at DESC",
$player
));
$result = [];
$seen = [];
foreach ($orders as $order) {
if (empty($order->response)) continue;
$payload = json_decode($order->response, true);
if (!isset($payload['commands'])) continue;
foreach ($payload['commands'] as $cmd) {
if (($cmd['type'] ?? '') !== 'gift_card') continue;
$code = $cmd['code'] ?? '';
if (!$code || isset($seen[$code])) continue;
$seen[$code] = true;
// Prüfen ob der Coupon noch ungenutzt ist
$coupon = $wpdb->get_row($wpdb->prepare(
"SELECT code, value, used_count, usage_limit, expiry
FROM {$wpdb->prefix}wis_coupons
WHERE code = %s",
$code
));
if (!$coupon) continue;
if ($coupon->used_count >= $coupon->usage_limit) continue; // bereits eingelöst
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) continue; // abgelaufen
$result[] = [
'code' => $coupon->code,
'value' => intval($coupon->value),
'currency' => $currency,
'label' => ($cmd['label'] ?? ''),
'expiry' => $coupon->expiry ?: null,
];
}
}
return new WP_REST_Response([
'success' => true,
'player' => $player,
'coupons' => $result,
]);
}
public static function cancel_order($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'cancelled'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
// ── Gift-System Endpoints ─────────────────────────────────────────────
/**
* GET /wis/v1/pending_gifts?recipient={player}
* Gibt ausstehende Geschenk-Orders zurück, die an diesen Empfänger gerichtet sind.
*/
public static function get_pending_gifts($request) {
global $wpdb;
$recipient = sanitize_text_field($request->get_param('recipient'));
if (!$recipient) {
return new WP_REST_Response(['orders' => []]);
}
$table = $wpdb->prefix . 'wis_orders';
// Status NICHT auf 'claimed' setzen – bleibt 'pending' bis der Empfänger
// ingame annimmt (gift_accept) oder ablehnt (gift_decline).
// Das Spigot-Plugin prüft pendingGiftRequests intern gegen Duplikate.
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table
WHERE gift_recipient = %s AND status IN ('pending', 'processing')
ORDER BY created_at ASC LIMIT 1",
$recipient
));
return new WP_REST_Response(['orders' => $results]);
}
/**
* POST /wis/v1/gift_accept { "id": 123 }
* Wird aufgerufen wenn Empfänger das Geschenk annimmt.
* Setzt Status auf 'processing' – der Spigot-Server liefert dann die Ware aus.
*/
public static function gift_accept($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'processing'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
/**
* POST /wis/v1/gift_decline { "id": 123, "sender": "SpielerA", "price": 500 }
* Empfänger lehnt ab → Order stornieren.
* Rückerstattung erfolgt auf Spigot-Seite via Vault direkt.
*/
public static function gift_decline($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'cancelled'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
public static function import_json($request) {
$data = $request->get_json_params();
$url = esc_url_raw($data['url'] ?? '');
if (!$url) {
return new WP_REST_Response(['success' => false, 'message' => 'Keine URL angegeben'], 400);
}
$response = wp_remote_get($url, ['timeout' => 30]);
if (is_wp_error($response)) {
return new WP_REST_Response(['success' => false, 'message' => $response->get_error_message()], 400);
}
$body = wp_remote_retrieve_body($response);
$json = json_decode($body, true);
if (!isset($json['items']) || !is_array($json['items'])) {
return new WP_REST_Response(['success' => false, 'message' => 'Ungültiges JSON Format'], 400);
}
$imported = 0;
$skipped = 0;
foreach ($json['items'] as $item) {
$item_id = sanitize_text_field($item['id'] ?? '');
$name = sanitize_text_field($item['name'] ?? 'Unbekannt');
if (empty($item_id)) continue;
$exists = WIS_DB::get_item_by_item_id($item_id);
if ($exists) {
$skipped++;
continue;
}
WIS_DB::insert_item([
'item_id' => $item_id,
'name' => $name,
'description' => sanitize_textarea_field($item['description'] ?? ''),
'price' => intval($item['price'] ?? 0),
'status' => intval($item['price'] ?? 0) > 0 ? 'publish' : 'draft',
'servers' => '[]',
'categories' => '[]'
]);
$imported++;
}
return new WP_REST_Response(['success' => true, 'imported' => $imported, 'skipped' => $skipped]);
}
public static function create_order($request) {
global $wpdb;
$data = $request->get_json_params();
$player = sanitize_text_field($data['player'] ?? '');
$cart = $data['cart'] ?? [];
$server = sanitize_text_field($data['server'] ?? '');
$coupon_code = isset($data['coupon_code']) ? sanitize_text_field(strtoupper($data['coupon_code'])) : '';
// Gift: optionaler Empfänger-Spielername
$gift_recipient = isset($data['gift_recipient'])
? sanitize_text_field(trim($data['gift_recipient'])) : '';
if (!$player || empty($cart) || !$server) {
return new WP_REST_Response(['success' => false, 'message' => 'Fehlende Daten'], 400);
}
// Gift-Validierung: Spieler kann sich nicht selbst beschenken
if ($gift_recipient !== '' && strcasecmp($gift_recipient, $player) === 0) {
return new WP_REST_Response(['success' => false, 'message' => 'Selbst-Geschenk nicht erlaubt'], 400);
}
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
$currency = get_option('wis_currency_name', 'Coins');
$valid_cart = [];
$total_normal = 0;
$total_offer = 0;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if (!$item || $item->status !== 'publish') continue;
$qty = intval($item_data['quantity'] ?? 1);
if ($qty <= 0) continue;
$servers = json_decode($item->servers, true);
if (!in_array($server, $servers ?: [])) continue;
$price = $item->offer_price > 0 ? $item->offer_price : $item->price;
// Gutschein-Karte: Preis = Wunschbetrag des Käufers (innerhalb Min/Max)
$custom_amount = 0;
if (preg_match('/^gift_card_(\d+)_(\d+)$/', $item->item_id, $gc_m2)) {
$gc_min2 = intval($gc_m2[1]);
$gc_max2 = intval($gc_m2[2]);
$raw_amount = intval($item_data['custom_amount'] ?? $gc_min2);
$custom_amount = max($gc_min2, min($gc_max2, $raw_amount));
$price = $custom_amount; // Preis = Wunschbetrag
}
$valid_cart[] = [
'id' => $item->item_id,
'title' => $item->name,
'price' => $price,
'qty' => $qty,
'is_offer' => $item->is_offer,
'custom_amount' => $custom_amount ?: $price,
];
$item_total = $price * $qty;
if ($item->is_offer && $exclude_offers === '1') {
$total_offer += $item_total;
} else {
$total_normal += $item_total;
}
}
if (empty($valid_cart)) {
return new WP_REST_Response(['success' => false, 'message' => 'Keine gültigen Items'], 400);
}
$coupon_discount = 0;
$coupon_msg = '';
$coupon_applied = false;
$coupon_error = false; // Gutschein-Fehler der den Kauf blockiert
if (!empty($coupon_code)) {
$coupon = WIS_DB::get_coupon_by_code($coupon_code);
if ($coupon) {
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) {
$coupon_error = true;
$coupon_msg = 'Dein Gutschein ist abgelaufen.';
} elseif ($coupon->used_count >= $coupon->usage_limit) {
$coupon_error = true;
$coupon_msg = 'Dieser Gutschein ist bereits vollständig aufgebraucht.';
} elseif (WIS_DB::coupon_used_by_player($coupon->id, $player)) {
$coupon_error = true;
$coupon_msg = 'Du hast diesen Gutschein bereits eingelöst.';
} else {
if ($exclude_offers === '1' && $total_normal <= 0) {
$coupon_error = true;
$coupon_msg = 'Dieser Gutschein gilt nicht für Angebots-Items.';
} else {
$restriction_error = self::check_coupon_restrictions($coupon, $cart, $total_normal);
if ($restriction_error !== null) {
$coupon_error = true;
$coupon_msg = $restriction_error;
} else
if ($coupon->type === 'percent') {
$coupon_discount = floor($total_normal * ($coupon->value / 100));
} else {
$coupon_discount = $coupon->value;
}
WIS_DB::update_coupon($coupon->id, ['used_count' => $coupon->used_count + 1]);
WIS_DB::record_coupon_use($coupon->id, $player);
$coupon_applied = true;
$coupon_msg = "Gutschein eingelöst: -{$coupon_discount} {$currency}";
}
}
} else {
$coupon_error = true;
$coupon_msg = 'Ungültiger Gutschein-Code.';
}
}
// Kauf blockieren wenn Gutschein-Fehler vorliegt und Spieler nicht explizit bestätigt hat
$confirmed_no_coupon = (bool)($data['confirmed_no_coupon'] ?? false);
if ($coupon_error && !$confirmed_no_coupon) {
return new WP_REST_Response([
'success' => false,
'coupon_error' => true,
'message' => $coupon_msg,
], 200);
}
$final_price = max(0, $total_normal - $coupon_discount) + $total_offer;
// ── Steuer ──────────────────────────────────────────────────
$tax_enabled = get_option('wis_tax_enabled', '0');
$tax_rate = floatval(get_option('wis_tax_rate', '0'));
$tax_amount = 0;
if ($tax_enabled === '1' && $tax_rate > 0) {
$tax_amount = floor($final_price * $tax_rate / 100);
$final_price = $final_price + $tax_amount;
}
// ────────────────────────────────────────────────────────────
// Fly-Dauern (item_id → Sekunden)
// Fly-Dauern und lesbares Label (wird dem Spieler auf dem Code angezeigt)
$fly_durations = [
'fly_5min' => 5 * 60,
'fly_15min' => 15 * 60,
'fly_30min' => 30 * 60,
'fly_1h' => 1 * 3600,
'fly_2h' => 2 * 3600,
'fly_3h' => 3 * 3600,
];
$fly_labels = [
'fly_5min' => '5 Minuten Fly',
'fly_15min' => '15 Minuten Fly',
'fly_30min' => '30 Minuten Fly',
'fly_1h' => '1 Stunde Fly',
'fly_2h' => '2 Stunden Fly',
'fly_3h' => '3 Stunden Fly',
];
$items_payload = [];
$commands_payload = [];
$title_parts = [];
foreach ($valid_cart as $item) {
$item_id = $item['id']; // Das ist der item_id-String aus wis_items
if (isset($fly_durations[$item_id])) {
// Fly-Gutschein: pro Stueck einen eigenen Code-Eintrag (qty > 1 = mehrere Codes)
$base_sec = $fly_durations[$item_id];
$base_label = $fly_labels[$item_id];
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'fly',
'duration_sec' => $base_sec,
'label' => $base_label,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^rank_([^_]+)_([a-zA-Z0-9_\-]+)_([a-zA-Z0-9_\-]+)_(\d+)$/', $item_id, $rm)) {
// Neues Format: rank_{rank_id}_{lp_group}_{default_group}_{days}
$rank_id = $rm[1];
$lp_group = $rm[2];
$default_group = $rm[3];
$rank_days = intval($rm[4]);
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'rank',
'rank_id' => $rank_id,
'lp_group' => $lp_group,
'default_group' => $default_group,
'label' => $item['title'],
'days' => $rank_days,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^rank_(.+)_(\d+)$/', $item_id, $rm)) {
// Altes Format (Fallback): rank_{rank_id}_{days}
$rank_id = $rm[1];
$rank_days = intval($rm[2]);
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'rank',
'rank_id' => $rank_id,
'lp_group'=> $rank_id,
'default_group' => 'default',
'label' => $item['title'],
'days' => $rank_days,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^fly_abo$/', $item_id) || preg_match('/^fly_abo_\d*$/', $item_id)) {
// Fly-Abo: monatlich abonniert, Preis = Monatsbeitrag
$commands_payload[] = [
'type' => 'fly_abo',
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} elseif (preg_match('/^plot_slots_(\d+)$/', $item_id, $ps_m)) {
// Plot-Slots: einmaliger permanenter Kauf
$extra_slots = intval($ps_m[1]) * intval($item['qty']);
$commands_payload[] = [
'type' => 'plot_slots',
'slots' => $extra_slots,
'label' => $item['title'],
];
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^plot_abo_(\d+)$/', $item_id, $pa_m)) {
// Plot-Abo: monatliche Abbuchung, Preis = Monatsbeitrag
$abo_slots = intval($pa_m[1]);
$commands_payload[] = [
'type' => 'plot_abo',
'slots' => $abo_slots,
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} elseif (preg_match('/^item_abo_(.+)_(\d+)_(\d+)$/', $item_id, $ia_m)) {
// Item-Abo: tägliche Lieferung eines Minecraft-Items
$abo_mc_item = $ia_m[1];
$abo_daily_qty = intval($ia_m[2]);
$abo_dur_days = intval($ia_m[3]);
$commands_payload[] = [
'type' => 'item_abo',
'item_id' => $abo_mc_item,
'daily_qty' => $abo_daily_qty,
'duration_days' => $abo_dur_days,
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} elseif (preg_match('/^custom_cmd_(.+)$/', $item_id, $cc_m)) {
// Custom Command Item: Command aus DB holen
$db_item = WIS_DB::get_item_by_item_id($item_id);
$raw_cmd = $db_item ? ($db_item->custom_command ?? '') : '';
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'custom_cmd',
'cmd_id' => $cc_m[1],
'command' => $raw_cmd,
'label' => $item['title'],
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^gift_card_(\d+)_(\d+)$/', $item_id, $gc_m)) {
// Gutschein-Karte: für jede Stück einen eigenen Coupon generieren
$gc_amount = intval($item['custom_amount'] ?? $item['price']); // custom_amount vom Frontend
$gc_min = intval($gc_m[1]);
$gc_max = intval($gc_m[2]);
// Betrag auf gültigen Bereich clippen
$gc_amount = max($gc_min, min($gc_max, $gc_amount));
for ($q = 0; $q < intval($item['qty']); $q++) {
// Eindeutigen Code generieren
$gc_code = 'GC-' . strtoupper(bin2hex(random_bytes(4))) . '-' . strtoupper(bin2hex(random_bytes(3)));
// Als Coupon in DB eintragen: Typ 'fixed', usage_limit 1, kein Ablauf
WIS_DB::insert_coupon([
'code' => $gc_code,
'value' => $gc_amount,
'type' => 'fixed',
'usage_limit' => 1,
'used_count' => 0,
'expiry' => null,
'min_order_value' => 0,
'allowed_categories' => null,
'bulk_id' => 'gift_card',
]);
$commands_payload[] = [
'type' => 'gift_card',
'code' => $gc_code,
'amount' => $gc_amount,
'label' => $item['title'] . ' (' . $gc_amount . ' ' . $currency . ')',
'currency'=> $currency,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
// Preis = tatsächlicher Betrag x Menge (nicht der Artikel-Mindestpreis)
$gc_amount_total = $gc_amount * intval($item['qty']);
// Korrektur: total_normal wurde mit item->price gerechnet, jetzt auf echten Betrag korrigieren
$total_normal = $total_normal - ($item['price'] * intval($item['qty'])) + $gc_amount_total;
} else {
// Normales Item
$items_payload[] = [
'id' => $item_id,
'amount' => $item['qty'],
];
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
}
}
$title = "Warenkorb: " . implode(', ', $title_parts);
if (strlen($title) > 240) $title = substr($title, 0, 237) . '...';
$payload = [
'items' => $items_payload,
'commands' => $commands_payload,
'coupon' => $coupon_applied ? ['code' => $coupon_code, 'discount' => $coupon_discount] : [],
];
WIS_DB::insert_order([
'player_name' => $player,
'gift_recipient' => $gift_recipient !== '' ? $gift_recipient : null,
'server' => $server,
'item_id' => 'cart',
'item_title' => $title,
'price' => $final_price,
'quantity' => count($valid_cart),
'status' => 'pending',
'response' => json_encode($payload)
]);
// ---- Einzelne Items für Analyse-Tabelle speichern ----
$new_order_id = $wpdb->insert_id;
if ($new_order_id) {
$oi_table = $wpdb->prefix . 'wis_order_items';
// Tabelle existiert? (für bestehende Installationen ohne Re-Aktivierung)
$oi_exists = $wpdb->get_var("SHOW TABLES LIKE '$oi_table'");
if ($oi_exists) {
foreach ($valid_cart as $ci) {
$ci_id = $ci['id'];
$ci_title = $ci['title'];
$ci_qty = intval($ci['qty'] ?? 1);
$ci_price = floatval($ci['price'] ?? 0);
// Item-Typ bestimmen
if (isset($fly_durations[$ci_id])) {
$ci_type = 'fly';
} elseif (preg_match('/^rank_/', $ci_id)) {
$ci_type = 'rank';
} elseif ($ci_id === 'fly_abo' || preg_match('/^fly_abo/', $ci_id)) {
$ci_type = 'fly_abo';
} elseif (preg_match('/^plot_/', $ci_id)) {
$ci_type = 'plot';
} else {
$ci_type = 'item';
}
$wpdb->insert($oi_table, [
'order_id' => $new_order_id,
'item_id' => $ci_id,
'item_name' => $ci_title,
'item_type' => $ci_type,
'quantity' => $ci_qty,
'price_per_item'=> $ci_price,
'total' => round($ci_price * $ci_qty, 2),
]);
}
}
}
// Fly-Abo Abonnements registrieren / verlängern
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') !== 'fly_abo') continue;
$abo_days = intval($cmd['days'] ?? 30);
$abo_label = sanitize_text_field($cmd['label'] ?? 'Fly-Abo');
$abo_price = 0;
// Preis aus dem Warenkorb ermitteln (fly_abo mit oder ohne Zahl-Suffix)
foreach ($valid_cart as $ci) {
if ($ci['id'] === 'fly_abo' || preg_match('/^fly_abo/', $ci['id'])) {
$abo_price = intval($ci['price'] ?? 0);
break;
}
}
global $wpdb;
WIS_Activator::create_abo_subs_table();
$subs_table = $wpdb->prefix . 'wis_fly_abo_subs';
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$subs_table} WHERE player_name = %s AND server = %s",
$player, $server
));
if ($existing && $existing->status === 'active') {
// Bestehendes aktives Abo verlängern (kumulativ)
$wpdb->query($wpdb->prepare(
"UPDATE {$subs_table}
SET expires_at = DATE_ADD(IF(expires_at > NOW(), expires_at, NOW()), INTERVAL %d DAY),
cancelled = 0,
cancelled_at = NULL,
label = %s,
price = %d
WHERE id = %d",
$abo_days, $abo_label, $abo_price, $existing->id
));
} else {
// Neues Abo anlegen
$wpdb->replace($subs_table, [
'player_name' => $player,
'server' => $server,
'label' => $abo_label,
'price' => $abo_price,
'status' => 'active',
'cancelled' => 0,
'expires_at' => date('Y-m-d H:i:s', strtotime("+{$abo_days} days")),
]);
}
}
// Item-Abo Abonnements registrieren / verlängern
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') !== 'item_abo') continue;
$ia_mc_item = sanitize_text_field($cmd['item_id'] ?? '');
$ia_daily_qty = max(1, intval($cmd['daily_qty'] ?? 1));
$ia_dur_days = max(1, intval($cmd['duration_days'] ?? 30));
$ia_label = sanitize_text_field($cmd['label'] ?? 'Item-Abo');
$ia_price = 0;
foreach ($valid_cart as $ci) {
if (preg_match('/^item_abo_/', $ci['id'])) {
$ia_price = intval($ci['price'] ?? 0);
break;
}
}
WIS_Activator::create_item_abo_subs_table();
$ia_table = $wpdb->prefix . 'wis_item_abo_subs';
// Bestehende aktive Abos für dieselbe item_id + player + server verlängern
$ia_existing = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$ia_table} WHERE player_name = %s AND server = %s AND item_id = %s AND status = 'active'",
$player, $server, $ia_mc_item
));
if ($ia_existing) {
$wpdb->query($wpdb->prepare(
"UPDATE {$ia_table}
SET expires_at = DATE_ADD(IF(expires_at > NOW(), expires_at, NOW()), INTERVAL %d DAY),
cancelled = 0,
cancelled_at = NULL,
daily_qty = %d,
label = %s,
price = %d
WHERE id = %d",
$ia_dur_days, $ia_daily_qty, $ia_label, $ia_price, $ia_existing->id
));
$ia_sub_id = $ia_existing->id;
} else {
$wpdb->insert($ia_table, [
'player_name' => $player,
'server' => $server,
'item_id' => $ia_mc_item,
'daily_qty' => $ia_daily_qty,
'label' => $ia_label,
'price' => $ia_price,
'status' => 'active',
'cancelled' => 0,
'expires_at' => date('Y-m-d H:i:s', strtotime("+{$ia_dur_days} days")),
'last_delivered' => null,
]);
$ia_sub_id = $wpdb->insert_id;
}
// Sofort-Lieferung für heute anlegen (damit der Spieler nicht bis Mitternacht warten muss)
$ia_sub = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$ia_table} WHERE id = %d", $ia_sub_id
));
if ($ia_sub && (empty($ia_sub->last_delivered) || $ia_sub->last_delivered < date('Y-m-d'))) {
$ia_payload = json_encode([
'items' => [['id' => $ia_mc_item, 'amount' => $ia_daily_qty]],
'commands' => [],
'abo_delivery' => true,
]);
$wpdb->insert($wpdb->prefix . 'wis_orders', [
'player_name' => $player,
'server' => $server,
'item_id' => 'item_abo_delivery',
'item_title' => '📦 Abo-Lieferung: ' . $ia_label . ' ×' . $ia_daily_qty,
'price' => 0,
'quantity' => $ia_daily_qty,
'status' => 'pending',
'response' => $ia_payload,
]);
$wpdb->update($ia_table, ['last_delivered' => date('Y-m-d')], ['id' => $ia_sub_id]);
}
}
$msg = $gift_recipient !== ''
? "🎁 Geschenk-Bestellung für {$gift_recipient} erfolgreich!"
: '✅ Bestellung erfolgreich!';
if ($coupon_msg) $msg .= ' (' . $coupon_msg . ')';
// Gift-Card-Codes aus dem Payload sammeln und in der Antwort zurückgeben
$gift_card_codes = [];
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') === 'gift_card') {
$gift_card_codes[] = [
'code' => $cmd['code'],
'amount' => $cmd['amount'],
'label' => $cmd['label'] ?? '',
];
}
}
return new WP_REST_Response([
'success' => true,
'message' => $msg,
'gift_card_codes' => [], // Codes werden erst nach complete_order zurückgegeben
]);
}
/**
* Prueft Mindestbestellwert, erlaubte Kategorien und erlaubte Raenge.
* Gibt null zurueck wenn alles OK, sonst Fehlermeldung als String.
*/
private static function check_coupon_restrictions($coupon, $cart, $subtotal_normal) {
// 1. Mindestbestellwert
$min = intval($coupon->min_order_value ?? 0);
if ($min > 0 && $subtotal_normal < $min) {
$currency = get_option('wis_currency_name', 'Coins');
return "Mindestbestellwert von {$min} {$currency} nicht erreicht.";
}
// 2. Erlaubte Kategorien
if (!empty($coupon->allowed_categories)) {
$allowed_cat_ids = array_map('intval', explode(',', $coupon->allowed_categories));
$has_valid_cat = false;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && in_array(intval($item->category_id), $allowed_cat_ids)) {
$has_valid_cat = true;
break;
}
}
if (!$has_valid_cat) {
return 'Dieser Gutschein gilt nicht fuer die Produkte in deinem Warenkorb.';
}
}
return null;
}
public static function validate_coupon($request) {
$data = $request->get_json_params();
$code = sanitize_text_field(strtoupper($data['code'] ?? ''));
$cart = $data['cart'] ?? [];
$player = sanitize_text_field($data['player'] ?? '');
if (!$code) {
return new WP_REST_Response(['success' => false, 'message' => 'Kein Code']);
}
$coupon = WIS_DB::get_coupon_by_code($code);
if (!$coupon) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein nicht gefunden']);
}
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein abgelaufen']);
}
if ($coupon->used_count >= $coupon->usage_limit) {
return new WP_REST_Response(['success' => false, 'message' => 'Bereits aufgebraucht']);
}
// Spieler-spezifische Prüfung: hat dieser Spieler den Code schon eingelöst?
if ($player && WIS_DB::coupon_used_by_player($coupon->id, $player)) {
return new WP_REST_Response(['success' => false, 'message' => 'Du hast diesen Gutschein bereits eingelöst']);
}
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
if ($exclude_offers === '1' && !empty($cart)) {
$has_normal = false;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && !$item->is_offer) {
$has_normal = true;
break;
}
}
if (!$has_normal) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein gilt nicht für Angebote']);
}
}
// Neue Einschränkungen prüfen (Mindestbestellwert, Kategorien, Rang)
$subtotal_normal = 0;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && !($exclude_offers === '1' && $item->is_offer)) {
$subtotal_normal += $item->price * intval($item_data['quantity'] ?? 1);
}
}
$restriction_error = self::check_coupon_restrictions($coupon, $cart, $subtotal_normal);
if ($restriction_error !== null) {
return new WP_REST_Response(['success' => false, 'message' => $restriction_error]);
}
$currency = get_option('wis_currency_name', 'Coins');
$msg = $coupon->type === 'percent'
? "Gutschein gültig (-{$coupon->value}%)"
: "Gutschein gültig (-{$coupon->value} {$currency})";
return new WP_REST_Response([
'success' => true,
'type' => $coupon->type,
'value' => $coupon->value,
'message' => $msg
]);
}
}
// ===========================================================
// SHORTCODE - FRONTEND SHOP
// ===========================================================
class WIS_Shortcode {
public static function register() {
add_shortcode('ingame_shop_form', [self::class, 'render']);
}
public static function render() {
$servers = WIS_DB::get_servers();
$categories = WIS_DB::get_categories();
$currency = get_option('wis_currency_name', 'Coins');
$header_text = get_option('wis_header_text', '');
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
$first_category = !empty($categories) ? $categories[0]->slug : '';
ob_start();
?>
Alle Server
name); ?>
🛒 Warenkorb
0
$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])
: [];
}
?>
name); ?>
>25 pro Seite
>50 pro Seite
>100 pro Seite
>Alle anzeigen
Zwischensumme
✂️ Rabatt
💰 Steuer (%)
Gesamt
0
🖥️ Server
-- Server wählen --
name); ?>
👤 Dein Spielername
🎉
Bestellung erfolgreich!
Deine Items werden ingame bereitgestellt.
'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
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'] : '';
?>
Titel:
Button Text:
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.");
}
});