7950 lines
432 KiB
PHP
7950 lines
432 KiB
PHP
<?php
|
||
/*
|
||
Plugin Name: WP Ingame Shop Pro
|
||
Plugin URI:https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro
|
||
Description: Vollautomatischer Shop mit Warenkorb (kein echtgeld Handel)
|
||
Version: 2.1.3
|
||
Author: M_Viper
|
||
Author URI: https://m-viper.de
|
||
Requires at least: 6.9.1
|
||
Tested up to: 6.9.1
|
||
PHP Version: 7.4
|
||
License: GPL2
|
||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||
Text Domain: wp-ingame-shop-pro
|
||
Tags: shop, items, minecraft, coupons, deals, categories
|
||
Support: [Discord Support](https://discord.com/invite/FdRs4BRd8D)
|
||
Support: [Telegram Support](https://t.me/M_Viper04)
|
||
*/
|
||
|
||
if (!defined('ABSPATH')) exit;
|
||
|
||
// Plugin Constants
|
||
define('WIS_VERSION', '2.1.3');
|
||
define('WIS_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||
define('WIS_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||
|
||
// ===========================================================
|
||
// DASHBOARD WIDGET & UPDATES
|
||
// ===========================================================
|
||
class WIS_Dashboard {
|
||
private static $update_url = 'https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/releases';
|
||
private static $api_url = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/WP-Ingame-Shop-Pro/releases/latest';
|
||
private static $transient_name = 'wis_update_check';
|
||
|
||
public static function init() {
|
||
add_action('wp_dashboard_setup', [__CLASS__, 'add_dashboard_widget']);
|
||
add_action('admin_init', [__CLASS__, 'check_for_update']);
|
||
|
||
// AJAX Handler für Cache leeren
|
||
add_action('wp_ajax_wis_clear_cache', [__CLASS__, 'ajax_clear_cache']);
|
||
}
|
||
|
||
// Button im Dashboard: Cache leeren
|
||
public static function ajax_clear_cache() {
|
||
check_ajax_referer('wis_clear_cache_nonce', 'nonce');
|
||
|
||
if (!current_user_can('manage_options')) {
|
||
wp_send_json_error('Keine Berechtigung.');
|
||
}
|
||
|
||
// Leere den Object Cache
|
||
if (function_exists('wp_cache_flush')) {
|
||
wp_cache_flush();
|
||
}
|
||
|
||
// Optional: Leere auch unseren Transienten für den Update-Check, damit er neu lädt
|
||
delete_transient(self::$transient_name);
|
||
|
||
wp_send_json_success(['message' => 'Cache erfolgreich geleert!']);
|
||
}
|
||
|
||
// Prüfe auf Updates (läuft im Admin-Hintergrund)
|
||
public static function check_for_update() {
|
||
$update_data = get_transient(self::$transient_name);
|
||
|
||
if (false === $update_data) {
|
||
$response = wp_remote_get(self::$api_url, ['timeout' => 10]);
|
||
|
||
if (is_wp_error($response)) {
|
||
return; // Fehler beim Abrufen, nichts tun
|
||
}
|
||
|
||
$body = wp_remote_retrieve_body($response);
|
||
$data = json_decode($body);
|
||
|
||
if (isset($data->tag_name) && isset($data->html_url)) {
|
||
// Entferne 'v' vom Tag (z.B. v2.1.3 -> 2.1.3)
|
||
$remote_version = ltrim($data->tag_name, 'v');
|
||
|
||
$update_data = [
|
||
'new_version' => $remote_version,
|
||
'url' => self::$update_url,
|
||
'package_url' => isset($data->assets[0]->browser_download_url) ? $data->assets[0]->browser_download_url : '',
|
||
'changelog' => isset($data->body) ? substr($data->body, 0, 200) . '...' : ''
|
||
];
|
||
|
||
// Speichere für 12 Stunden
|
||
set_transient(self::$transient_name, $update_data, 12 * HOUR_IN_SECONDS);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Zeige Admin Notice wenn Update verfügbar
|
||
public static function show_update_notice() {
|
||
$update_data = get_transient(self::$transient_name);
|
||
if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')) {
|
||
?>
|
||
<div class="notice notice-info is-dismissible">
|
||
<p>
|
||
<strong>🚀 WP Ingame Shop Pro Update verfügbar!</strong><br>
|
||
Neue Version: <?php echo esc_html($update_data['new_version']); ?> (Du hast <?php echo WIS_VERSION; ?>)<br>
|
||
<a href="<?php echo esc_url($update_data['url']); ?>" target="_blank">Hier im Git Repository ansehen</a> oder unten im Widget Details prüfen.
|
||
</p>
|
||
</div>
|
||
<?php
|
||
}
|
||
}
|
||
|
||
// Widget registrieren
|
||
public static function add_dashboard_widget() {
|
||
wp_add_dashboard_widget(
|
||
'wis_dashboard_widget',
|
||
'🛒 WP Ingame Shop Pro Dashboard',
|
||
[__CLASS__, 'render_widget']
|
||
);
|
||
}
|
||
|
||
// Widget Inhalt rendern
|
||
public static function render_widget() {
|
||
global $wpdb;
|
||
$update_data = get_transient(self::$transient_name);
|
||
|
||
// Statistiken holen
|
||
$stats = [
|
||
'items' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_items WHERE status = 'publish'"),
|
||
'orders' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_orders WHERE status = 'completed'"),
|
||
'pending' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_orders WHERE status IN ('pending', 'claimed')"),
|
||
'revenue' => $wpdb->get_var("SELECT SUM(price) FROM {$wpdb->prefix}wis_orders WHERE status = 'completed'"),
|
||
];
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
// Update Status bestimmen
|
||
$update_html = '<span style="color:green;">✅ Aktuell (v' . WIS_VERSION . ')</span>';
|
||
if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')) {
|
||
$update_html = '<span style="color:#d63638; font-weight:bold;">⚠️ Update verfügbar!</span>';
|
||
}
|
||
|
||
?>
|
||
<style>
|
||
/* Spinner bei number-Inputs entfernen */
|
||
input[type=number]::-webkit-outer-spin-button,
|
||
input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none !important; margin: 0 !important; }
|
||
input[type=number] { -moz-appearance: textfield !important; appearance: textfield !important; }
|
||
.wis-dash-stats { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
|
||
.wis-stat-box { flex: 1; min-width: 100px; background: #f9f9f9; padding: 10px; border-radius: 5px; text-align: center; border: 1px solid #eee; }
|
||
.wis-stat-num { display: block; font-size: 1.5em; font-weight: bold; color: #333; }
|
||
.wis-stat-label { font-size: 0.85em; color: #666; }
|
||
.wis-dash-actions { margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
||
</style>
|
||
|
||
<div class="wis-dash-stats">
|
||
<div class="wis-stat-box">
|
||
<span class="wis-stat-num"><?php echo intval($stats['items']); ?></span>
|
||
<span class="wis-stat-label">Items</span>
|
||
</div>
|
||
<div class="wis-stat-box">
|
||
<span class="wis-stat-num"><?php echo intval($stats['orders']); ?></span>
|
||
<span class="wis-stat-label">Bestellungen</span>
|
||
</div>
|
||
<div class="wis-stat-box">
|
||
<span class="wis-stat-num"><?php echo intval($stats['pending']); ?></span>
|
||
<span class="wis-stat-label">Offen</span>
|
||
</div>
|
||
<div class="wis-stat-box">
|
||
<span class="wis-stat-num"><?php echo number_format($stats['revenue']); ?></span>
|
||
<span class="wis-stat-label">Umsatz (<?php echo esc_html($currency); ?>)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<p style="margin-bottom: 10px;">
|
||
<strong>Status:</strong> <?php echo $update_html; ?>
|
||
</p>
|
||
|
||
<?php if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')): ?>
|
||
<div style="background:#fff3cd; color:#856404; padding:10px; border-radius:4px; margin-bottom:10px; font-size:13px;">
|
||
<strong>Neue Version <?php echo esc_html($update_data['new_version']); ?></strong><br>
|
||
<a href="<?php echo esc_url($update_data['url']); ?>" target="_blank">Updates ansehen</a>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="wis-dash-actions">
|
||
<a href="<?php echo admin_url('admin.php?page=wis_orders'); ?>" class="button">Bestellungen ansehen</a>
|
||
|
||
<button type="button" id="wis-clear-cache-btn" class="button button-secondary button-small">
|
||
🔄 Update-Cache jetzt leeren
|
||
</button>
|
||
</div>
|
||
|
||
<script type="text/javascript">
|
||
jQuery(document).ready(function($) {
|
||
$('#wis-clear-cache-btn').on('click', function() {
|
||
var $btn = $(this);
|
||
$btn.prop('disabled', true).text('Leere...');
|
||
|
||
$.ajax({
|
||
url: ajaxurl,
|
||
type: 'POST',
|
||
data: {
|
||
action: 'wis_clear_cache',
|
||
nonce: '<?php echo wp_create_nonce('wis_clear_cache_nonce'); ?>'
|
||
},
|
||
success: function(response) {
|
||
if (response.success) {
|
||
$btn.removeClass('button-secondary').addClass('button-primary').text('✅ ' + response.data.message);
|
||
setTimeout(function() {
|
||
$btn.prop('disabled', false).removeClass('button-primary').addClass('button-secondary').text('🔄 Update-Cache jetzt leeren');
|
||
}, 3000);
|
||
} else {
|
||
alert('Fehler: ' + (response.data || 'Unbekannt'));
|
||
$btn.prop('disabled', false).text('🔄 Update-Cache jetzt leeren');
|
||
}
|
||
},
|
||
error: function() {
|
||
alert('Verbindungsfehler.');
|
||
$btn.prop('disabled', false).text('🔄 Update-Cache jetzt leeren');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
<?php
|
||
}
|
||
}
|
||
WIS_Dashboard::init();
|
||
// Zeige Notice auch im Admin-Bereich (außer auf dem Dashboard selbst, da dort das Widget ist)
|
||
add_action('admin_notices', [WIS_Dashboard::class, 'show_update_notice']);
|
||
|
||
// AJAX: Kategorien-Reihenfolge speichern
|
||
add_action('wp_ajax_wis_save_cat_order', function() {
|
||
check_ajax_referer('wis_cat_order_nonce', 'nonce');
|
||
if (!current_user_can('manage_options')) wp_send_json_error('Keine Berechtigung.');
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_categories';
|
||
$order = $_POST['order'] ?? [];
|
||
if (!is_array($order)) wp_send_json_error('Ungültige Daten.');
|
||
foreach ($order as $i => $entry) {
|
||
$id = intval($entry['id']);
|
||
$parent_id = intval($entry['parent_id']);
|
||
if ($id > 0) {
|
||
$wpdb->update($table, [
|
||
'sort_order' => ($i + 1) * 10,
|
||
'parent_id' => $parent_id,
|
||
], ['id' => $id]);
|
||
}
|
||
}
|
||
wp_send_json_success('Reihenfolge gespeichert.');
|
||
});
|
||
|
||
// AJAX: Angebot-Flag umschalten (Angebote-Übersicht)
|
||
add_action('wp_ajax_wis_angebote_toggle', function() {
|
||
check_ajax_referer('wis_angebote_nonce', 'nonce');
|
||
if (!current_user_can('manage_options')) wp_send_json_error('Keine Berechtigung.');
|
||
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
$id = intval($_POST['id'] ?? 0);
|
||
$field = in_array($_POST['field'] ?? '', ['is_offer', 'is_daily_deal'], true) ? $_POST['field'] : null;
|
||
|
||
if (!$id || !$field) wp_send_json_error('Ungültige Parameter.');
|
||
|
||
$current = (int) $wpdb->get_var($wpdb->prepare("SELECT $field FROM $table WHERE id = %d", $id));
|
||
$new_val = $current ? 0 : 1;
|
||
|
||
if ($field === 'is_daily_deal' && $new_val === 1) {
|
||
$wpdb->query("UPDATE $table SET is_daily_deal = 0 WHERE is_daily_deal = 1");
|
||
}
|
||
$wpdb->update($table, [$field => $new_val], ['id' => $id]);
|
||
wp_send_json_success(['new_val' => $new_val]);
|
||
});
|
||
|
||
// AJAX: Angebotspreis direkt speichern (Angebote-Übersicht)
|
||
add_action('wp_ajax_wis_angebote_save_price', function() {
|
||
check_ajax_referer('wis_angebote_nonce', 'nonce');
|
||
if (!current_user_can('manage_options')) wp_send_json_error('Keine Berechtigung.');
|
||
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
$id = intval($_POST['id'] ?? 0);
|
||
$offer_price = max(0, intval($_POST['offer_price'] ?? 0));
|
||
|
||
if (!$id) wp_send_json_error('Ungültige ID.');
|
||
$wpdb->update($table, ['offer_price' => $offer_price], ['id' => $id]);
|
||
wp_send_json_success(['offer_price' => $offer_price]);
|
||
});
|
||
|
||
// jQuery UI Sortable für die Kategorien-Seite laden
|
||
add_action('admin_enqueue_scripts', function($hook) {
|
||
if (isset($_GET['page']) && $_GET['page'] === 'wis_categories') {
|
||
wp_enqueue_script('jquery-ui-sortable');
|
||
}
|
||
});
|
||
|
||
|
||
// ===========================================================
|
||
// ACTIVATION & DATABASE
|
||
// ===========================================================
|
||
class WIS_Activator {
|
||
public static function activate() {
|
||
// Spalte custom_image_url nachrüsten (für bestehende Installationen)
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
$col = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'custom_image_url'");
|
||
if (!$col) {
|
||
$wpdb->query("ALTER TABLE $table ADD COLUMN custom_image_url varchar(500) DEFAULT NULL AFTER categories");
|
||
}
|
||
|
||
// sort_order für Kategorien nachrüsten (ab v2.3.0)
|
||
$cat_table_so = $wpdb->prefix . 'wis_categories';
|
||
$col_so = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$cat_table_so' AND COLUMN_NAME = 'sort_order'");
|
||
if (!$col_so) {
|
||
$wpdb->query("ALTER TABLE $cat_table_so ADD COLUMN sort_order int(11) NOT NULL DEFAULT 0 AFTER parent_id");
|
||
// Bestehende Kategorien mit aufsteigender Reihenfolge initialisieren
|
||
$existing_cats = $wpdb->get_results("SELECT id FROM $cat_table_so ORDER BY parent_id ASC, name ASC");
|
||
foreach ($existing_cats as $i => $ec) {
|
||
$wpdb->update($cat_table_so, ['sort_order' => $i * 10], ['id' => $ec->id]);
|
||
}
|
||
}
|
||
|
||
// custom_command-Spalte nachrüsten (ab v2.3.0 – Custom Command Items)
|
||
$col_cmd = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'custom_command'");
|
||
if (!$col_cmd) {
|
||
$wpdb->query("ALTER TABLE $table ADD COLUMN custom_command varchar(500) DEFAULT NULL AFTER custom_image_url");
|
||
}
|
||
|
||
// Ankauf-Spalten nachrüsten (ab v6.5)
|
||
$col_sell = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'sell_enabled'"); if (!$col_sell) {
|
||
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_enabled tinyint(1) NOT NULL DEFAULT 0 AFTER status");
|
||
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_price_mode varchar(20) NOT NULL DEFAULT 'percent' AFTER sell_enabled");
|
||
$wpdb->query("ALTER TABLE $table ADD COLUMN sell_price_value int(11) NOT NULL DEFAULT 80 AFTER sell_price_mode");
|
||
}
|
||
|
||
// Tageslimit beim Ankauf nachrüsten
|
||
$col_dlimit = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'daily_sell_limit'");
|
||
if (!$col_dlimit) {
|
||
$wpdb->query("ALTER TABLE $table ADD COLUMN daily_sell_limit int(11) NOT NULL DEFAULT 0 AFTER sell_price_value");
|
||
}
|
||
|
||
// Preishistorie-Tabelle anlegen
|
||
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_price_history (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
item_id varchar(100) NOT NULL,
|
||
item_name varchar(255) NOT NULL,
|
||
field varchar(30) NOT NULL DEFAULT 'price',
|
||
old_value int(11) NOT NULL DEFAULT 0,
|
||
new_value int(11) NOT NULL DEFAULT 0,
|
||
changed_by varchar(100) NOT NULL DEFAULT '',
|
||
changed_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
KEY item_id (item_id),
|
||
KEY changed_at (changed_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
|
||
|
||
// Gift-Spalte nachrüsten (ab v6.5-gift)
|
||
$orders_table = $wpdb->prefix . 'wis_orders';
|
||
$col_gift = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$orders_table' AND COLUMN_NAME = 'gift_recipient'");
|
||
if (!$col_gift) {
|
||
$wpdb->query("ALTER TABLE $orders_table ADD COLUMN gift_recipient varchar(100) DEFAULT NULL AFTER player_name");
|
||
}
|
||
|
||
// Auto-Expire Cron registrieren (7-Tage-Ablauf für unbestätigte Orders)
|
||
if (!wp_next_scheduled('wis_auto_expire_orders')) {
|
||
wp_schedule_event(time(), 'hourly', 'wis_auto_expire_orders');
|
||
}
|
||
|
||
// Item-Abo Tabelle nachrüsten
|
||
self::create_item_abo_subs_table();
|
||
|
||
// Item-Abo Liefer-Cron registrieren (täglich)
|
||
if (!wp_next_scheduled('wis_item_abo_delivery_event')) {
|
||
$midnight = strtotime('tomorrow midnight');
|
||
wp_schedule_event($midnight, 'daily', 'wis_item_abo_delivery_event');
|
||
}
|
||
|
||
// Sell-Log-Tabelle anlegen
|
||
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_sell_log (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
player_name varchar(100) NOT NULL,
|
||
server varchar(100) NOT NULL,
|
||
item_id varchar(100) NOT NULL,
|
||
item_name varchar(255) NOT NULL,
|
||
quantity int(11) NOT NULL DEFAULT 1,
|
||
price_per_item decimal(10,2) NOT NULL,
|
||
total_paid decimal(10,2) NOT NULL,
|
||
sold_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
KEY player_name (player_name),
|
||
KEY sold_at (sold_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
|
||
|
||
// Order-Items-Tabelle anlegen (ab Analyse-Update) – einzelne Items pro Bestellung
|
||
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_order_items (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
order_id mediumint(9) NOT NULL,
|
||
item_id varchar(100) NOT NULL,
|
||
item_name varchar(255) NOT NULL,
|
||
item_type varchar(20) NOT NULL DEFAULT 'item',
|
||
quantity int(11) NOT NULL DEFAULT 1,
|
||
price_per_item decimal(10,2) NOT NULL DEFAULT 0,
|
||
total decimal(10,2) NOT NULL DEFAULT 0,
|
||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
KEY order_id (order_id),
|
||
KEY item_id (item_id),
|
||
KEY created_at (created_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
|
||
|
||
self::create_tables();
|
||
self::set_default_options();
|
||
self::create_default_categories();
|
||
|
||
if (!wp_next_scheduled('wis_daily_deal_event')) {
|
||
wp_schedule_event(time(), 'daily', 'wis_daily_deal_event');
|
||
}
|
||
// Fly-Abo Renewal: täglich prüfen (läuft nur durch am 1. des Monats)
|
||
if (!wp_next_scheduled('wis_abo_renewal_event')) {
|
||
// Nächsten Mitternacht-Zeitstempel berechnen
|
||
$midnight = strtotime('tomorrow midnight');
|
||
wp_schedule_event($midnight, 'daily', 'wis_abo_renewal_event');
|
||
}
|
||
flush_rewrite_rules();
|
||
}
|
||
|
||
public static function deactivate() {
|
||
wp_clear_scheduled_hook('wis_daily_deal_event');
|
||
wp_clear_scheduled_hook('wis_abo_renewal_event');
|
||
wp_clear_scheduled_hook('wis_auto_expire_orders');
|
||
wp_clear_scheduled_hook('wis_item_abo_delivery_event');
|
||
flush_rewrite_rules();
|
||
}
|
||
|
||
private static function create_tables() {
|
||
global $wpdb;
|
||
$charset_collate = $wpdb->get_charset_collate();
|
||
|
||
$tables = [];
|
||
|
||
// Items
|
||
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_items (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
item_id varchar(100) NOT NULL,
|
||
name varchar(255) NOT NULL,
|
||
description text,
|
||
price int(11) DEFAULT 0,
|
||
offer_price int(11) DEFAULT 0,
|
||
is_offer tinyint(1) DEFAULT 0,
|
||
is_daily_deal tinyint(1) DEFAULT 0,
|
||
servers text,
|
||
categories text,
|
||
custom_image_url varchar(500) DEFAULT NULL,
|
||
custom_command varchar(500) DEFAULT NULL,
|
||
status varchar(20) DEFAULT 'draft',
|
||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY item_id (item_id),
|
||
KEY status (status)
|
||
) $charset_collate;";
|
||
|
||
// Orders
|
||
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_orders (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
player_name varchar(100) NOT NULL,
|
||
gift_recipient varchar(100) DEFAULT NULL,
|
||
server varchar(100) NOT NULL,
|
||
item_id varchar(100) NOT NULL,
|
||
item_title varchar(255) NOT NULL,
|
||
price int(11) NOT NULL,
|
||
quantity int(11) DEFAULT 1,
|
||
status varchar(20) DEFAULT 'pending',
|
||
response text,
|
||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
KEY player_name (player_name),
|
||
KEY gift_recipient (gift_recipient),
|
||
KEY status (status)
|
||
) $charset_collate;";
|
||
|
||
// Coupons
|
||
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_coupons (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
code varchar(50) NOT NULL,
|
||
value int(11) NOT NULL,
|
||
type varchar(10) DEFAULT 'fixed',
|
||
usage_limit int(11) DEFAULT 1,
|
||
used_count int(11) DEFAULT 0,
|
||
expiry date DEFAULT NULL,
|
||
min_order_value int(11) DEFAULT 0,
|
||
allowed_categories text DEFAULT NULL,
|
||
bulk_id varchar(20) DEFAULT NULL,
|
||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY code (code)
|
||
) $charset_collate;";
|
||
|
||
// Pro-Spieler Coupon-Nutzung (verhindert Mehrfacheinlösung)
|
||
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wis_coupon_uses (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
coupon_id mediumint(9) NOT NULL,
|
||
player_name varchar(100) NOT NULL,
|
||
used_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY coupon_player (coupon_id, player_name),
|
||
KEY player_name (player_name)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
|
||
|
||
// Servers
|
||
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_servers (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
slug varchar(100) NOT NULL,
|
||
name varchar(255) NOT NULL,
|
||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY slug (slug)
|
||
) $charset_collate;";
|
||
|
||
// Categories
|
||
$tables[] = "CREATE TABLE {$wpdb->prefix}wis_categories (
|
||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||
name varchar(255) NOT NULL,
|
||
slug varchar(100) NOT NULL,
|
||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY slug (slug)
|
||
) $charset_collate;";
|
||
|
||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||
foreach ($tables as $sql) {
|
||
dbDelta($sql);
|
||
}
|
||
}
|
||
|
||
private static function set_default_options() {
|
||
$defaults = [
|
||
'wis_currency_name' => 'Coins',
|
||
'wis_image_base_url' => 'https://git.viper.ipv64.net/M_Viper/minecraft-items/raw/branch/main/images/',
|
||
'wis_header_text' => '✅ Auto-Bilder | 💰 Sicherer Checkout | 🎮 Ingame-Bestätigung',
|
||
'wis_coupon_exclude_offers' => '0',
|
||
'wis_daily_deal_enabled' => '0',
|
||
'wis_daily_deal_discount' => '20',
|
||
'wis_api_key' => bin2hex(random_bytes(24)),
|
||
'wis_tax_enabled' => '0',
|
||
'wis_tax_rate' => '0',
|
||
];
|
||
|
||
foreach ($defaults as $key => $value) {
|
||
if (get_option($key) === false) {
|
||
add_option($key, $value);
|
||
}
|
||
}
|
||
}
|
||
|
||
public static function check_api_key($request) {
|
||
$key = $request->get_header('X-WIS-Key');
|
||
if (empty($key)) {
|
||
$key = $request->get_param('api_key');
|
||
}
|
||
$stored = get_option('wis_api_key', '');
|
||
return ($stored !== '' && hash_equals($stored, (string) $key));
|
||
}
|
||
|
||
public static function spigot_permission($request) {
|
||
if (!self::check_api_key($request)) {
|
||
return new WP_Error('wis_unauthorized', 'Ungültiger oder fehlender API-Key.', ['status' => 401]);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private static function create_default_categories() {
|
||
$default_categories = [
|
||
['name' => 'Baublöcke', 'slug' => 'baublocke'],
|
||
['name' => 'Dekorationsblöcke', 'slug' => 'dekorationsblocke'],
|
||
['name' => 'Redstone', 'slug' => 'redstone'],
|
||
['name' => 'Transport', 'slug' => 'transport'],
|
||
['name' => 'Natur', 'slug' => 'natur'],
|
||
['name' => 'Werkzeuge & Hilfsmittel', 'slug' => 'werkzeuge-hilfsmittel'],
|
||
['name' => 'Kampf', 'slug' => 'kampf'],
|
||
['name' => 'Nahrung & Tränke', 'slug' => 'nahrung-tranke'],
|
||
['name' => 'Zutaten', 'slug' => 'zutaten'],
|
||
['name' => 'Spawn-Eier', 'slug' => 'spawn-eier']
|
||
];
|
||
|
||
global $wpdb;
|
||
foreach ($default_categories as $cat) {
|
||
$exists = $wpdb->get_var($wpdb->prepare(
|
||
"SELECT id FROM {$wpdb->prefix}wis_categories WHERE slug = %s",
|
||
$cat['slug']
|
||
));
|
||
|
||
if (!$exists) {
|
||
$wpdb->insert($wpdb->prefix . 'wis_categories', [
|
||
'name' => $cat['name'],
|
||
'slug' => $cat['slug']
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
public static function run_daily_deal() {
|
||
if (get_option('wis_daily_deal_enabled') !== '1') return;
|
||
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
$discount = intval(get_option('wis_daily_deal_discount', 20));
|
||
|
||
// Altes Daily-Deal-Item vollständig zurücksetzen:
|
||
// is_daily_deal, is_offer und offer_price werden alle auf 0 gesetzt,
|
||
// damit das Item nicht als normales Angebot mit gesenktem Preis hängen bleibt.
|
||
$wpdb->query(
|
||
"UPDATE $table
|
||
SET is_daily_deal = 0,
|
||
is_offer = 0,
|
||
offer_price = 0
|
||
WHERE is_daily_deal = 1"
|
||
);
|
||
|
||
$item = $wpdb->get_row("SELECT * FROM $table WHERE status = 'publish' AND price > 0 ORDER BY RAND() LIMIT 1");
|
||
|
||
if ($item) {
|
||
$offer_price = max(0, floor($item->price - ($item->price * ($discount / 100))));
|
||
$wpdb->update($table, [
|
||
'is_daily_deal' => 1,
|
||
'is_offer' => 1,
|
||
'offer_price' => $offer_price
|
||
], ['id' => $item->id]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Fly-Abo Renewal – läuft täglich, handelt aber nur am 1. des Monats.
|
||
* Verlängert alle aktiven (nicht gekündigten) Abos automatisch um 30 Tage
|
||
* und legt je einen Order-Eintrag als Buchungsnachweis an.
|
||
* Gekündigte Abos laufen bis zum letzten Tag des laufenden Monats und werden dann entfernt.
|
||
*/
|
||
public static function run_abo_renewal() {
|
||
// Nur am 1. des Monats ausführen
|
||
if (date('j') !== '1') return;
|
||
|
||
global $wpdb;
|
||
$subs_table = $wpdb->prefix . 'wis_fly_abo_subs';
|
||
$orders_table = $wpdb->prefix . 'wis_orders';
|
||
|
||
// Tabelle anlegen falls noch nicht vorhanden (Migration)
|
||
self::create_abo_subs_table();
|
||
|
||
// Alle aktiven, nicht gekündigten Abos verlängern
|
||
$active = $wpdb->get_results(
|
||
"SELECT * FROM {$subs_table} WHERE cancelled = 0 AND status = 'active'"
|
||
);
|
||
|
||
foreach ($active as $sub) {
|
||
// expires_at um 30 Tage verlängern
|
||
$wpdb->query($wpdb->prepare(
|
||
"UPDATE {$subs_table}
|
||
SET expires_at = DATE_ADD(expires_at, INTERVAL 30 DAY),
|
||
renewed_at = NOW(),
|
||
renewal_count = renewal_count + 1
|
||
WHERE id = %d",
|
||
$sub->id
|
||
));
|
||
|
||
// Buchungsnachweis als Order anlegen (status: completed)
|
||
$wpdb->insert($orders_table, [
|
||
'player_name' => $sub->player_name,
|
||
'server' => $sub->server,
|
||
'item_id' => 'fly_abo_renewal',
|
||
'item_title' => '✈ Fly-Abo Verlängerung: ' . $sub->label,
|
||
'price' => $sub->price,
|
||
'quantity' => 1,
|
||
'status' => 'completed',
|
||
'response' => json_encode([
|
||
'commands' => [[
|
||
'type' => 'fly_abo',
|
||
'label' => $sub->label,
|
||
'price' => $sub->price,
|
||
]],
|
||
]),
|
||
]);
|
||
}
|
||
|
||
// Gekündigte Abos die abgelaufen sind deaktivieren
|
||
$wpdb->query(
|
||
"UPDATE {$subs_table}
|
||
SET status = 'expired'
|
||
WHERE cancelled = 1
|
||
AND expires_at < NOW()
|
||
AND status = 'active'"
|
||
);
|
||
|
||
// Log
|
||
$count = count($active);
|
||
error_log("[WIS] Fly-Abo Renewal: {$count} Abo(s) verlängert am " . date('d.m.Y'));
|
||
}
|
||
|
||
public static function create_abo_subs_table() {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_fly_abo_subs';
|
||
$wpdb->query("CREATE TABLE IF NOT EXISTS {$table} (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
player_name VARCHAR(64) NOT NULL,
|
||
server VARCHAR(64) NOT NULL DEFAULT '',
|
||
label VARCHAR(128) NOT NULL,
|
||
price INT NOT NULL DEFAULT 0,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||
cancelled TINYINT(1) NOT NULL DEFAULT 0,
|
||
cancelled_at DATETIME DEFAULT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
renewed_at DATETIME DEFAULT NULL,
|
||
renewal_count INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uq_player_server (player_name, server)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||
}
|
||
|
||
/**
|
||
* Tabelle für Item-Abo Abonnements anlegen
|
||
*/
|
||
public static function create_item_abo_subs_table() {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_item_abo_subs';
|
||
$wpdb->query("CREATE TABLE IF NOT EXISTS {$table} (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
player_name VARCHAR(64) NOT NULL,
|
||
server VARCHAR(64) NOT NULL DEFAULT '',
|
||
item_id VARCHAR(100) NOT NULL,
|
||
daily_qty INT NOT NULL DEFAULT 1,
|
||
label VARCHAR(128) NOT NULL,
|
||
price INT NOT NULL DEFAULT 0,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||
cancelled TINYINT(1) NOT NULL DEFAULT 0,
|
||
cancelled_at DATETIME DEFAULT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
last_delivered DATE DEFAULT NULL,
|
||
renewal_count INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
KEY player_server (player_name, server),
|
||
KEY status (status)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||
}
|
||
|
||
/**
|
||
* Item-Abo Tageslieferung – läuft täglich per Cron.
|
||
* Legt für jeden aktiven Abonnenten einen pending Order an,
|
||
* damit das Spigot-Plugin die Items ingame ausliefern kann.
|
||
*/
|
||
public static function run_item_abo_delivery() {
|
||
global $wpdb;
|
||
$subs_table = $wpdb->prefix . 'wis_item_abo_subs';
|
||
$orders_table = $wpdb->prefix . 'wis_orders';
|
||
|
||
// Tabelle ggf. anlegen (Migration)
|
||
self::create_item_abo_subs_table();
|
||
|
||
$today = date('Y-m-d');
|
||
|
||
// Alle aktiven, nicht abgelaufenen Abos die heute noch nicht beliefert wurden
|
||
$active = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM {$subs_table}
|
||
WHERE status = 'active'
|
||
AND (last_delivered IS NULL OR last_delivered < %s)
|
||
AND expires_at > NOW()",
|
||
$today
|
||
));
|
||
|
||
$count = 0;
|
||
foreach ($active as $sub) {
|
||
// Payload für Spigot-Plugin
|
||
$payload = json_encode([
|
||
'items' => [[
|
||
'id' => $sub->item_id,
|
||
'amount' => intval($sub->daily_qty),
|
||
]],
|
||
'commands' => [],
|
||
'abo_delivery' => true,
|
||
]);
|
||
|
||
$wpdb->insert($orders_table, [
|
||
'player_name' => $sub->player_name,
|
||
'server' => $sub->server,
|
||
'item_id' => 'item_abo_delivery',
|
||
'item_title' => '📦 Abo-Lieferung: ' . $sub->label . ' ×' . $sub->daily_qty,
|
||
'price' => 0,
|
||
'quantity' => intval($sub->daily_qty),
|
||
'status' => 'pending',
|
||
'response' => $payload,
|
||
]);
|
||
|
||
// last_delivered aktualisieren
|
||
$wpdb->update($subs_table, ['last_delivered' => $today], ['id' => $sub->id]);
|
||
$count++;
|
||
}
|
||
|
||
// Abgelaufene + gekündigte Abos deaktivieren
|
||
$wpdb->query(
|
||
"UPDATE {$subs_table}
|
||
SET status = 'expired'
|
||
WHERE status = 'active'
|
||
AND (expires_at < NOW() OR (cancelled = 1 AND expires_at < NOW()))"
|
||
);
|
||
|
||
if ($count > 0) {
|
||
error_log("[WIS] Item-Abo Lieferung: {$count} Abo(s) beliefert am {$today}");
|
||
}
|
||
}
|
||
|
||
public static function reset_shop() {
|
||
global $wpdb;
|
||
|
||
$tables = [
|
||
'wis_items', 'wis_orders', 'wis_coupons',
|
||
'wis_servers', 'wis_categories'
|
||
];
|
||
|
||
foreach ($tables as $table) {
|
||
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}$table");
|
||
}
|
||
|
||
self::set_default_options();
|
||
self::create_default_categories();
|
||
return true;
|
||
}
|
||
|
||
public static function reset_sell_log(): bool {
|
||
global $wpdb;
|
||
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_sell_log");
|
||
return true;
|
||
}
|
||
|
||
public static function reset_top_spenders(): bool {
|
||
global $wpdb;
|
||
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_orders");
|
||
return true;
|
||
}
|
||
|
||
public static function reset_analyse(): bool {
|
||
global $wpdb;
|
||
// Order-Items-Tabelle (Analyse-Grundlage) leeren
|
||
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_order_items");
|
||
return true;
|
||
}
|
||
|
||
public static function reset_price_history(): bool {
|
||
global $wpdb;
|
||
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wis_price_history");
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// ===========================================================
|
||
// ITEM CATEGORIZER
|
||
// ===========================================================
|
||
class WIS_Item_Categorizer {
|
||
public static function auto_categorize($item_id) {
|
||
$item_id = strtolower($item_id);
|
||
$item_id = str_replace('minecraft:', '', $item_id);
|
||
|
||
if (strpos($item_id, 'spawn_egg') !== false) {
|
||
return ['spawn-eier'];
|
||
}
|
||
|
||
if (preg_match('/(wooden|stone|iron|golden|diamond|netherite|copper)_(pickaxe|axe|shovel|hoe)/', $item_id)) {
|
||
return ['werkzeuge-hilfsmittel'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'shears', 'fishing_rod', 'flint_and_steel', 'bucket', 'water_bucket', 'lava_bucket',
|
||
'milk_bucket', 'powder_snow_bucket', 'axolotl_bucket', 'cod_bucket', 'pufferfish_bucket',
|
||
'salmon_bucket', 'tadpole_bucket', 'tropical_fish_bucket',
|
||
'compass', 'recovery_compass', 'clock', 'spyglass', 'map', 'filled_map',
|
||
'brush', 'lead', 'name_tag', 'saddle', 'carrot_on_a_stick', 'warped_fungus_on_a_stick'
|
||
])) {
|
||
return ['werkzeuge-hilfsmittel'];
|
||
}
|
||
|
||
if (preg_match('/(wooden|stone|iron|golden|diamond|netherite|copper)_(sword|spear)/', $item_id)) {
|
||
return ['kampf'];
|
||
}
|
||
|
||
if (preg_match('/(leather|chainmail|iron|golden|diamond|netherite|copper|turtle)_(helmet|chestplate|leggings|boots|cap|tunic|pants|shell)/', $item_id)) {
|
||
return ['kampf'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'bow', 'crossbow', 'arrow', 'spectral_arrow', 'tipped_arrow',
|
||
'shield', 'trident', 'mace',
|
||
'totem_of_undying', 'elytra',
|
||
'horse_armor', 'iron_horse_armor', 'golden_horse_armor', 'diamond_horse_armor',
|
||
'wolf_armor'
|
||
]) || strpos($item_id, 'horse_armor') !== false) {
|
||
return ['kampf'];
|
||
}
|
||
|
||
if (preg_match('/(raw_|cooked_)?(beef|porkchop|mutton|chicken|rabbit|cod|salmon)/', $item_id)) {
|
||
return ['nahrung-tranke'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'apple', 'golden_apple', 'enchanted_golden_apple',
|
||
'melon_slice', 'glow_berries', 'sweet_berries', 'chorus_fruit',
|
||
'carrot', 'golden_carrot', 'potato', 'baked_potato', 'poisonous_potato',
|
||
'beetroot',
|
||
'bread', 'cookie', 'cake', 'pumpkin_pie',
|
||
'dried_kelp',
|
||
'tropical_fish', 'pufferfish', 'rotten_flesh', 'spider_eye'
|
||
])) {
|
||
return ['nahrung-tranke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_stew') !== false || strpos($item_id, '_soup') !== false) {
|
||
return ['nahrung-tranke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'potion') !== false || in_array($item_id, [
|
||
'honey_bottle', 'milk_bucket', 'glass_bottle', 'dragon_breath',
|
||
'experience_bottle', 'ominous_bottle'
|
||
])) {
|
||
return ['nahrung-tranke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_boat') !== false || strpos($item_id, '_raft') !== false) {
|
||
return ['transport'];
|
||
}
|
||
|
||
if (strpos($item_id, 'minecart') !== false) {
|
||
return ['transport'];
|
||
}
|
||
|
||
if (in_array($item_id, ['elytra', 'saddle', 'lead'])) {
|
||
return ['transport'];
|
||
}
|
||
|
||
if (strpos($item_id, 'redstone') !== false && $item_id !== 'redstone_ore') {
|
||
return ['redstone'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'repeater', 'comparator', 'observer',
|
||
'piston', 'sticky_piston',
|
||
'dispenser', 'dropper', 'hopper',
|
||
'lever', 'tripwire_hook', 'daylight_detector',
|
||
'tnt', 'target', 'lightning_rod'
|
||
]) || strpos($item_id, 'button') !== false || strpos($item_id, 'pressure_plate') !== false) {
|
||
return ['redstone'];
|
||
}
|
||
|
||
if (strpos($item_id, '_rail') !== false || $item_id === 'rail') {
|
||
return ['redstone'];
|
||
}
|
||
|
||
$pure_materials = [
|
||
'stick', 'coal', 'charcoal', 'diamond', 'emerald', 'lapis_lazuli',
|
||
'iron_ingot', 'gold_ingot', 'copper_ingot', 'netherite_ingot',
|
||
'iron_nugget', 'gold_nugget', 'copper_nugget',
|
||
'raw_iron', 'raw_gold', 'raw_copper',
|
||
'netherite_scrap', 'netherite_upgrade',
|
||
'amethyst_shard', 'prismarine_shard', 'prismarine_crystals',
|
||
'quartz', 'nether_quartz', 'echo_shard', 'disc_fragment',
|
||
'string', 'feather', 'leather', 'rabbit_hide',
|
||
'slimeball', 'ender_pearl', 'ender_eye',
|
||
'blaze_rod', 'blaze_powder', 'magma_cream', 'ghast_tear',
|
||
'nether_star', 'nether_brick',
|
||
'nautilus_shell', 'heart_of_the_sea', 'scute', 'turtle_scute', 'armadillo_scute',
|
||
'bone', 'bone_meal', 'gunpowder', 'glowstone_dust', 'sugar',
|
||
'phantom_membrane', 'ink_sac', 'glow_ink_sac',
|
||
'paper', 'book', 'flint',
|
||
'fermented_spider_eye', 'glistering_melon_slice', 'rabbit_foot',
|
||
'nether_wart', 'breeze_rod',
|
||
'clay_ball', 'brick', 'firework_star',
|
||
'shulker_shell', 'popped_chorus_fruit'
|
||
];
|
||
|
||
if (in_array($item_id, $pure_materials)) {
|
||
return ['zutaten'];
|
||
}
|
||
|
||
if (strpos($item_id, '_pottery_sherd') !== false || strpos($item_id, '_armor_trim') !== false) {
|
||
return ['zutaten'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'dirt', 'coarse_dirt', 'rooted_dirt', 'grass_block', 'podzol', 'mycelium',
|
||
'farmland', 'dirt_path',
|
||
'sand', 'red_sand', 'gravel', 'clay',
|
||
'suspicious_sand', 'suspicious_gravel'
|
||
])) {
|
||
return ['natur'];
|
||
}
|
||
|
||
if (strpos($item_id, '_leaves') !== false || strpos($item_id, '_sapling') !== false ||
|
||
strpos($item_id, 'azalea') !== false) {
|
||
return ['natur'];
|
||
}
|
||
|
||
$flowers = [
|
||
'dandelion', 'poppy', 'blue_orchid', 'allium', 'azure_bluet',
|
||
'red_tulip', 'orange_tulip', 'white_tulip', 'pink_tulip',
|
||
'oxeye_daisy', 'cornflower', 'lily_of_the_valley', 'wither_rose',
|
||
'sunflower', 'lilac', 'rose_bush', 'peony',
|
||
'pitcher_plant', 'pitcher_pod', 'torchflower', 'torchflower_seeds',
|
||
'pink_petals', 'spore_blossom'
|
||
];
|
||
if (in_array($item_id, $flowers)) {
|
||
return ['natur'];
|
||
}
|
||
|
||
if (strpos($item_id, 'mushroom') !== false || strpos($item_id, 'fungus') !== false ||
|
||
in_array($item_id, ['short_grass', 'tall_grass', 'fern', 'large_fern', 'dead_bush'])) {
|
||
return ['natur'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'seagrass', 'tall_seagrass', 'kelp', 'dried_kelp', 'sea_pickle',
|
||
'vine', 'weeping_vines', 'twisting_vines', 'cave_vines', 'glow_lichen',
|
||
'hanging_roots', 'mangrove_roots', 'muddy_mangrove_roots'
|
||
])) {
|
||
return ['natur'];
|
||
}
|
||
|
||
if (strpos($item_id, '_seeds') !== false || in_array($item_id, [
|
||
'wheat', 'beetroot', 'carrot', 'potato',
|
||
'melon', 'pumpkin', 'carved_pumpkin',
|
||
'sugar_cane', 'bamboo', 'cocoa_beans',
|
||
'sweet_berries', 'glow_berries', 'sweet_berry_bush',
|
||
'nether_wart', 'cactus',
|
||
'mangrove_propagule'
|
||
])) {
|
||
return ['natur'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'crimson_roots', 'warped_roots', 'nether_sprouts',
|
||
'crimson_nylium', 'warped_nylium'
|
||
])) {
|
||
return ['natur'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'moss_block', 'moss_carpet',
|
||
'big_dripleaf', 'small_dripleaf',
|
||
'lily_pad', 'bee_nest', 'honeycomb', 'honeycomb_block',
|
||
'snow', 'snowball', 'powder_snow'
|
||
])) {
|
||
return ['natur'];
|
||
}
|
||
|
||
if (strpos($item_id, 'glass') !== false && strpos($item_id, '_pane') === false) {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
if (strpos($item_id, 'glass_pane') !== false || strpos($item_id, 'stained_glass_pane') !== false) {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_door') !== false || strpos($item_id, '_trapdoor') !== false ||
|
||
strpos($item_id, '_fence_gate') !== false) {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if ((strpos($item_id, '_fence') !== false && strpos($item_id, '_fence_gate') === false) ||
|
||
(strpos($item_id, '_wall') !== false && !in_array($item_id, ['wall_banner', 'wall_sign', 'wall_torch'])) ||
|
||
$item_id === 'iron_bars') {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_stairs') !== false || strpos($item_id, '_slab') !== false) {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_carpet') !== false) {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'torch', 'soul_torch', 'lantern', 'soul_lantern',
|
||
'campfire', 'soul_campfire', 'end_rod',
|
||
'shroomlight', 'froglight', 'sea_lantern'
|
||
]) || strpos($item_id, '_candle') !== false || $item_id === 'candle') {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'crafting_table', 'furnace', 'blast_furnace', 'smoker',
|
||
'chest', 'trapped_chest', 'ender_chest', 'barrel',
|
||
'enchanting_table', 'anvil', 'chipped_anvil', 'damaged_anvil',
|
||
'grindstone', 'smithing_table', 'cartography_table', 'fletching_table',
|
||
'loom', 'stonecutter', 'brewing_stand', 'cauldron', 'composter',
|
||
'lectern', 'bookshelf', 'chiseled_bookshelf',
|
||
'bell', 'beacon', 'conduit', 'lodestone', 'respawn_anchor'
|
||
]) || strpos($item_id, '_bed') !== false || strpos($item_id, 'shulker_box') !== false) {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_sign') !== false || strpos($item_id, '_hanging_sign') !== false ||
|
||
strpos($item_id, '_banner') !== false ||
|
||
in_array($item_id, ['item_frame', 'glow_item_frame', 'painting', 'armor_stand'])) {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'ladder', 'scaffolding', 'chain',
|
||
'flower_pot', 'decorated_pot',
|
||
'dragon_egg', 'dragon_head',
|
||
'note_block', 'jukebox'
|
||
]) || strpos($item_id, '_head') !== false || strpos($item_id, '_skull') !== false ||
|
||
strpos($item_id, 'coral') !== false) {
|
||
return ['dekorationsblocke'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'stone', 'cobblestone', 'mossy_cobblestone',
|
||
'granite', 'polished_granite', 'diorite', 'polished_diorite',
|
||
'andesite', 'polished_andesite', 'calcite', 'tuff',
|
||
'smooth_stone'
|
||
]) || strpos($item_id, 'stone_brick') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'deepslate') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'brick') !== false && $item_id !== 'brick' && $item_id !== 'nether_brick') {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'sandstone') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'quartz_') !== false && strpos($item_id, 'nether_quartz') === false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'prismarine') !== false || strpos($item_id, 'purpur') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'concrete') !== false || strpos($item_id, 'terracotta') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_wool') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_planks') !== false ||
|
||
(strpos($item_id, '_log') !== false && strpos($item_id, 'stripped') === false) ||
|
||
(strpos($item_id, '_wood') !== false && strpos($item_id, 'stripped') === false)) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'stripped_') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (in_array($item_id, ['bamboo_block', 'bamboo_mosaic', 'crimson_stem', 'warped_stem',
|
||
'crimson_hyphae', 'warped_hyphae'])) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, 'copper_') !== false && strpos($item_id, '_ingot') === false &&
|
||
strpos($item_id, '_nugget') === false && $item_id !== 'copper_door' && $item_id !== 'copper_trapdoor') {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (strpos($item_id, '_block') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'netherrack', 'soul_sand', 'soul_soil',
|
||
'basalt', 'polished_basalt', 'smooth_basalt',
|
||
'end_stone', 'obsidian', 'crying_obsidian'
|
||
]) || strpos($item_id, 'blackstone') !== false || strpos($item_id, 'end_stone_brick') !== false) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
if (in_array($item_id, [
|
||
'glowstone', 'sponge', 'wet_sponge',
|
||
'ice', 'packed_ice', 'blue_ice',
|
||
'magma_block', 'slime_block', 'honey_block',
|
||
'hay_block', 'dried_kelp_block',
|
||
'mud', 'packed_mud', 'mud_bricks',
|
||
'dripstone_block', 'amethyst_block', 'budding_amethyst'
|
||
])) {
|
||
return ['baublocke'];
|
||
}
|
||
|
||
return ['baublocke'];
|
||
}
|
||
}
|
||
|
||
// ===========================================================
|
||
// DATABASE HELPER
|
||
// ===========================================================
|
||
class WIS_DB {
|
||
|
||
/**
|
||
* Gibt die Bild-URL eines Items zurück.
|
||
* Priorität: custom_image_url > automatisch generierte URL aus item_id.
|
||
*/
|
||
public static function get_item_image($item) {
|
||
if (!empty($item->custom_image_url)) {
|
||
return esc_url($item->custom_image_url);
|
||
}
|
||
$img_base = get_option('wis_image_base_url', '');
|
||
$img_name = str_replace(':', '_', $item->item_id) . '.png';
|
||
return $img_base . $img_name;
|
||
}
|
||
|
||
public static function get_items($args = []) {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
|
||
$where_parts = ["1=1"];
|
||
|
||
if (isset($args['status'])) {
|
||
$where_parts[] = $wpdb->prepare("status = %s", $args['status']);
|
||
}
|
||
|
||
if (isset($args['category_slug']) && !empty($args['category_slug'])) {
|
||
$search_pattern = '%"' . $args['category_slug'] . '"%';
|
||
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
|
||
}
|
||
|
||
if (isset($args['ids']) && is_array($args['ids']) && !empty($args['ids'])) {
|
||
$ids = array_map('intval', $args['ids']);
|
||
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
||
$where_parts[] = sprintf("id IN ($placeholders)", ...$ids);
|
||
}
|
||
|
||
if (isset($args['search']) && !empty($args['search'])) {
|
||
$search_like = '%' . $wpdb->esc_like($args['search']) . '%';
|
||
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
|
||
}
|
||
|
||
$where = implode(" AND ", $where_parts);
|
||
$limit = isset($args['limit']) ? "LIMIT " . intval($args['limit']) : "";
|
||
$orderby = isset($args['orderby']) ? "ORDER BY " . sanitize_text_field($args['orderby']) : "ORDER BY name ASC";
|
||
|
||
return $wpdb->get_results("SELECT * FROM $table WHERE $where $orderby $limit");
|
||
}
|
||
|
||
public static function count_items($args = []) {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
$where_parts = ["1=1"];
|
||
|
||
if (isset($args['status'])) {
|
||
$where_parts[] = $wpdb->prepare("status = %s", $args['status']);
|
||
}
|
||
if (isset($args['category_slug']) && !empty($args['category_slug'])) {
|
||
$search_pattern = '%"' . $args['category_slug'] . '"%';
|
||
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
|
||
}
|
||
if (isset($args['search']) && !empty($args['search'])) {
|
||
$search_like = '%' . $wpdb->esc_like($args['search']) . '%';
|
||
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
|
||
}
|
||
|
||
$where = implode(" AND ", $where_parts);
|
||
return (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where");
|
||
}
|
||
|
||
public static function get_item($id) {
|
||
global $wpdb;
|
||
return $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}wis_items WHERE id = %d",
|
||
$id
|
||
));
|
||
}
|
||
|
||
public static function get_item_by_item_id($item_id) {
|
||
global $wpdb;
|
||
return $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}wis_items WHERE item_id = %s",
|
||
$item_id
|
||
));
|
||
}
|
||
|
||
public static function insert_item($data) {
|
||
global $wpdb;
|
||
|
||
if (empty($data['categories']) || $data['categories'] === '[]') {
|
||
$auto_cats = WIS_Item_Categorizer::auto_categorize($data['item_id']);
|
||
$data['categories'] = json_encode($auto_cats);
|
||
}
|
||
|
||
return $wpdb->insert($wpdb->prefix . 'wis_items', $data);
|
||
}
|
||
|
||
public static function update_item($id, $data) {
|
||
global $wpdb;
|
||
return $wpdb->update($wpdb->prefix . 'wis_items', $data, ['id' => $id]);
|
||
}
|
||
|
||
public static function delete_item($id) {
|
||
global $wpdb;
|
||
return $wpdb->delete($wpdb->prefix . 'wis_items', ['id' => $id]);
|
||
}
|
||
|
||
public static function get_servers() {
|
||
global $wpdb;
|
||
return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wis_servers ORDER BY name ASC");
|
||
}
|
||
|
||
public static function insert_server($slug, $name) {
|
||
global $wpdb;
|
||
return $wpdb->insert($wpdb->prefix . 'wis_servers', [
|
||
'slug' => sanitize_title($slug),
|
||
'name' => sanitize_text_field($name)
|
||
]);
|
||
}
|
||
|
||
public static function delete_server($id) {
|
||
global $wpdb;
|
||
return $wpdb->delete($wpdb->prefix . 'wis_servers', ['id' => $id]);
|
||
}
|
||
|
||
public static function get_categories() {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_categories';
|
||
// Prüfen ob sort_order-Spalte existiert (Migration ggf. noch nicht gelaufen)
|
||
$has_sort = $wpdb->get_var("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = 'sort_order'");
|
||
if ($has_sort) {
|
||
return $wpdb->get_results("SELECT * FROM $table ORDER BY parent_id ASC, sort_order ASC, name ASC");
|
||
}
|
||
return $wpdb->get_results("SELECT * FROM $table ORDER BY parent_id ASC, name ASC");
|
||
}
|
||
|
||
public static function insert_category($name, $parent_id = 0) {
|
||
global $wpdb;
|
||
// sort_order = höchster bestehender Wert + 10
|
||
$max_order = (int) $wpdb->get_var("SELECT MAX(sort_order) FROM {$wpdb->prefix}wis_categories WHERE parent_id = " . intval($parent_id));
|
||
return $wpdb->insert($wpdb->prefix . 'wis_categories', [
|
||
'parent_id' => intval($parent_id),
|
||
'sort_order' => $max_order + 10,
|
||
'name' => sanitize_text_field($name),
|
||
'slug' => sanitize_title($name),
|
||
]);
|
||
}
|
||
|
||
public static function delete_category($id) {
|
||
global $wpdb;
|
||
return $wpdb->delete($wpdb->prefix . 'wis_categories', ['id' => $id]);
|
||
}
|
||
|
||
public static function get_coupons() {
|
||
global $wpdb;
|
||
return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wis_coupons ORDER BY created_at DESC");
|
||
}
|
||
|
||
public static function get_coupon_by_code($code) {
|
||
global $wpdb;
|
||
return $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}wis_coupons WHERE code = %s",
|
||
strtoupper($code)
|
||
));
|
||
}
|
||
|
||
/**
|
||
* Prüft ob ein Spieler einen Gutschein bereits eingelöst hat.
|
||
*/
|
||
public static function coupon_used_by_player($coupon_id, $player_name) {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_coupon_uses';
|
||
// Tabelle existiert? (Fallback für alte Installationen)
|
||
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) return false;
|
||
return (bool) $wpdb->get_var($wpdb->prepare(
|
||
"SELECT COUNT(*) FROM $table WHERE coupon_id = %d AND player_name = %s",
|
||
$coupon_id, $player_name
|
||
));
|
||
}
|
||
|
||
/**
|
||
* Schreibt die Einlösung eines Gutscheins durch einen Spieler.
|
||
* Nutzt INSERT IGNORE damit der UNIQUE KEY als Doppelschutz wirkt.
|
||
*/
|
||
public static function record_coupon_use($coupon_id, $player_name) {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_coupon_uses';
|
||
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) return false;
|
||
return $wpdb->query($wpdb->prepare(
|
||
"INSERT IGNORE INTO $table (coupon_id, player_name) VALUES (%d, %s)",
|
||
$coupon_id, $player_name
|
||
));
|
||
}
|
||
|
||
public static function insert_coupon($data) {
|
||
global $wpdb;
|
||
$data['code'] = strtoupper($data['code']);
|
||
return $wpdb->insert($wpdb->prefix . 'wis_coupons', $data);
|
||
}
|
||
|
||
public static function update_coupon($id, $data) {
|
||
global $wpdb;
|
||
if (isset($data['code'])) {
|
||
$data['code'] = strtoupper($data['code']);
|
||
}
|
||
return $wpdb->update($wpdb->prefix . 'wis_coupons', $data, ['id' => $id]);
|
||
}
|
||
|
||
public static function delete_coupon($id) {
|
||
global $wpdb;
|
||
return $wpdb->delete($wpdb->prefix . 'wis_coupons', ['id' => $id]);
|
||
}
|
||
|
||
public static function get_orders($limit = 100) {
|
||
global $wpdb;
|
||
return $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}wis_orders ORDER BY created_at DESC LIMIT %d",
|
||
$limit
|
||
));
|
||
}
|
||
|
||
public static function get_order($id) {
|
||
global $wpdb;
|
||
return $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}wis_orders WHERE id = %d",
|
||
$id
|
||
));
|
||
}
|
||
|
||
public static function insert_order($data) {
|
||
global $wpdb;
|
||
return $wpdb->insert($wpdb->prefix . 'wis_orders', $data);
|
||
}
|
||
|
||
public static function update_order_status($id, $status) {
|
||
global $wpdb;
|
||
return $wpdb->update(
|
||
$wpdb->prefix . 'wis_orders',
|
||
['status' => $status],
|
||
['id' => $id]
|
||
);
|
||
}
|
||
|
||
public static function delete_order($id) {
|
||
global $wpdb;
|
||
return $wpdb->delete($wpdb->prefix . 'wis_orders', ['id' => $id]);
|
||
}
|
||
}
|
||
|
||
// ===========================================================
|
||
// ADMIN PAGES
|
||
// ===========================================================
|
||
class WIS_Admin {
|
||
// Wird auf admin_init gefeuert – vor jeder HTML-Ausgabe, damit wp_redirect() funktioniert
|
||
public static function handle_save_item() {
|
||
if (!isset($_POST['wis_save_item'])) return;
|
||
if (!current_user_can('manage_options')) return;
|
||
check_admin_referer('wis_item_form');
|
||
|
||
global $wpdb;
|
||
|
||
$item_type = sanitize_text_field($_POST['item_type'] ?? 'minecraft');
|
||
if ($item_type === 'gift_card') {
|
||
$gc_min = max(1, intval($_POST['gift_card_min'] ?? 100));
|
||
$gc_max = max($gc_min, intval($_POST['gift_card_max'] ?? 5000));
|
||
// item_id kodiert Min+Max – eindeutig und vom create_order erkennbar
|
||
$resolved_item_id = 'gift_card_' . $gc_min . '_' . $gc_max;
|
||
$_POST['item_id'] = $resolved_item_id;
|
||
// Preis = Mindestwert (wird im Frontend durch Nutzereingabe überschrieben)
|
||
$_POST['price'] = $gc_min;
|
||
} elseif ($item_type === 'fly') {
|
||
$resolved_item_id = sanitize_text_field($_POST['fly_duration'] ?? 'fly_5min');
|
||
} elseif ($item_type === 'rank') {
|
||
$rank_id = preg_replace('/[^a-z0-9_\-]/', '', strtolower($_POST['rank_id'] ?? 'vip'));
|
||
$lp_group = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_POST['lp_group'] ?? $rank_id);
|
||
$default_group = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_POST['default_group'] ?? 'default');
|
||
$rank_days = max(0, intval($_POST['rank_days'] ?? 30));
|
||
if (empty($lp_group)) $lp_group = $rank_id;
|
||
if (empty($default_group)) $default_group = 'default';
|
||
$resolved_item_id = 'rank_' . $rank_id . '_' . $lp_group . '_' . $default_group . '_' . $rank_days;
|
||
} elseif ($item_type === 'fly_abo') {
|
||
$resolved_item_id = 'fly_abo';
|
||
} elseif ($item_type === 'plot_slots') {
|
||
$plot_extra_slots = max(1, intval($_POST['plot_extra_slots'] ?? 1));
|
||
$resolved_item_id = 'plot_slots_' . $plot_extra_slots;
|
||
} elseif ($item_type === 'plot_abo') {
|
||
$plot_abo_slots = max(1, intval($_POST['plot_abo_slots'] ?? 1));
|
||
$resolved_item_id = 'plot_abo_' . $plot_abo_slots;
|
||
} elseif ($item_type === 'item_abo') {
|
||
$abo_item_id = sanitize_text_field($_POST['abo_item_id'] ?? '');
|
||
$abo_daily_qty = max(1, intval($_POST['abo_daily_qty'] ?? 1));
|
||
$abo_duration = max(1, intval($_POST['abo_duration_days'] ?? 30));
|
||
if (empty($abo_item_id)) $abo_item_id = 'minecraft:stone';
|
||
$resolved_item_id = 'item_abo_' . sanitize_text_field($abo_item_id) . '_' . $abo_daily_qty . '_' . $abo_duration;
|
||
} elseif ($item_type === 'custom_cmd') {
|
||
$cmd_slug = preg_replace('/[^a-z0-9_\-]/', '', strtolower($_POST['custom_cmd_id'] ?? 'custom'));
|
||
if (empty($cmd_slug)) $cmd_slug = 'custom_' . time();
|
||
$resolved_item_id = 'custom_cmd_' . $cmd_slug;
|
||
} else {
|
||
$resolved_item_id = sanitize_text_field($_POST['item_id'] ?? '');
|
||
}
|
||
|
||
if (empty($resolved_item_id) || empty(trim($_POST['name'] ?? ''))) {
|
||
// Fehler als transient speichern, damit page_items() ihn anzeigen kann
|
||
set_transient('wis_save_error_' . get_current_user_id(), '❌ Name und Item-ID sind Pflichtfelder.', 30);
|
||
wp_redirect(wp_get_referer() ?: admin_url('admin.php?page=wis_items'));
|
||
exit;
|
||
}
|
||
|
||
$data = [
|
||
'item_id' => $resolved_item_id,
|
||
'name' => sanitize_text_field($_POST['name']),
|
||
'description' => sanitize_textarea_field($_POST['description'] ?? ''),
|
||
'price' => intval($_POST['price'] ?? 0),
|
||
'offer_price' => intval($_POST['offer_price'] ?? 0),
|
||
'is_offer' => isset($_POST['is_offer']) ? 1 : 0,
|
||
'servers' => isset($_POST['servers']) ? json_encode(array_map('sanitize_text_field', $_POST['servers'])) : '[]',
|
||
'categories' => isset($_POST['categories']) ? json_encode(array_map('sanitize_text_field', $_POST['categories'])) : '[]',
|
||
'custom_image_url' => esc_url_raw($_POST['custom_image_url'] ?? ''),
|
||
'custom_command' => $item_type === 'custom_cmd' ? sanitize_text_field($_POST['custom_command'] ?? '') : null,
|
||
'sell_enabled' => isset($_POST['sell_enabled']) ? 1 : 0,
|
||
'sell_price_mode' => in_array($_POST['sell_price_mode'] ?? '', ['percent','fixed','minus']) ? $_POST['sell_price_mode'] : 'percent',
|
||
'sell_price_value' => max(0, intval($_POST['sell_price_value'] ?? 80)),
|
||
'daily_sell_limit' => max(0, intval($_POST['daily_sell_limit'] ?? 0)),
|
||
'status' => (intval($_POST['price'] ?? 0) > 0 || $item_type === 'fly' || $item_type === 'rank' || $item_type === 'fly_abo' || $item_type === 'plot_slots' || $item_type === 'plot_abo' || $item_type === 'item_abo' || $item_type === 'custom_cmd' || $item_type === 'gift_card') ? 'publish' : 'draft',
|
||
];
|
||
|
||
// Preishistorie loggen wenn ein bestehendes Item bearbeitet wird
|
||
$price_history_table = $wpdb->prefix . 'wis_price_history';
|
||
$ph_exists = $wpdb->get_var("SHOW TABLES LIKE '$price_history_table'");
|
||
$editor = wp_get_current_user()->user_login ?: 'admin';
|
||
|
||
$edit_id = intval($_POST['edit_id'] ?? $_GET['edit'] ?? 0);
|
||
if ($edit_id && $ph_exists) {
|
||
$existing_item = WIS_DB::get_item($edit_id);
|
||
if ($existing_item) {
|
||
foreach (['price' => 'Verkaufspreis', 'offer_price' => 'Angebotspreis', 'sell_price_value' => 'Ankaufswert'] as $field => $label) {
|
||
$old_val = intval($existing_item->$field ?? 0);
|
||
$new_val = intval($data[$field] ?? 0);
|
||
if ($old_val !== $new_val) {
|
||
$wpdb->insert($price_history_table, [
|
||
'item_id' => $existing_item->item_id,
|
||
'item_name' => $existing_item->name,
|
||
'field' => $label,
|
||
'old_value' => $old_val,
|
||
'new_value' => $new_val,
|
||
'changed_by' => $editor,
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (isset($_GET['edit'])) {
|
||
$result = WIS_DB::update_item(intval($_GET['edit']), $data);
|
||
if ($result !== false) {
|
||
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . intval($_GET['edit']) . '&saved=1'));
|
||
exit;
|
||
} else {
|
||
set_transient('wis_save_error_' . get_current_user_id(), '❌ Fehler beim Speichern: ' . $wpdb->last_error, 30);
|
||
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . intval($_GET['edit'])));
|
||
exit;
|
||
}
|
||
} else {
|
||
$existing = WIS_DB::get_item_by_item_id($resolved_item_id);
|
||
if ($existing) {
|
||
WIS_DB::update_item($existing->id, $data);
|
||
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . $existing->id . '&saved=1'));
|
||
exit;
|
||
}
|
||
$result = WIS_DB::insert_item($data);
|
||
if ($result) {
|
||
$new_id = $wpdb->insert_id;
|
||
wp_redirect(admin_url('admin.php?page=wis_items&edit=' . $new_id . '&created=1'));
|
||
exit;
|
||
} else {
|
||
$err = $wpdb->last_error ?: 'Unbekannter Fehler.';
|
||
set_transient('wis_save_error_' . get_current_user_id(), '❌ Fehler beim Erstellen: ' . $err, 30);
|
||
wp_redirect(admin_url('admin.php?page=wis_items&add=1'));
|
||
exit;
|
||
}
|
||
}
|
||
}
|
||
|
||
public static function register_menu() {
|
||
add_menu_page(
|
||
'Ingame Shop',
|
||
'Ingame Shop',
|
||
'manage_options',
|
||
'wis_shop',
|
||
[self::class, 'page_overview'],
|
||
'dashicons-cart',
|
||
6
|
||
);
|
||
|
||
add_submenu_page('wis_shop', 'Einstellungen', 'Einstellungen', 'manage_options', 'wis_shop');
|
||
add_submenu_page('wis_shop', 'Items', 'Items', 'manage_options', 'wis_items', [self::class, 'page_items']);
|
||
add_submenu_page('wis_shop', 'Bestellungen', 'Bestellungen', 'manage_options', 'wis_orders', [self::class, 'page_orders']);
|
||
add_submenu_page('wis_shop', 'Server', 'Server', 'manage_options', 'wis_servers', [self::class, 'page_servers']);
|
||
add_submenu_page('wis_shop', 'Kategorien', 'Kategorien', 'manage_options', 'wis_categories', [self::class, 'page_categories']);
|
||
add_submenu_page('wis_shop', 'Angebote', 'Angebote', 'manage_options', 'wis_angebote', [self::class, 'page_angebote']);
|
||
add_submenu_page('wis_shop', 'Gutscheine', 'Gutscheine', 'manage_options', 'wis_coupons', [self::class, 'page_coupons']);
|
||
add_submenu_page('wis_shop', 'Analyse', 'Analyse', 'manage_options', 'wis_analyse', [self::class, 'page_analyse']);
|
||
add_submenu_page('wis_shop', 'Top Spender', 'Top Spender', 'manage_options', 'wis_top_spenders', [self::class, 'page_top_spenders']);
|
||
add_submenu_page('wis_shop', 'Ankauf-Log', 'Ankauf-Log', 'manage_options', 'wis_sell_log', [self::class, 'page_sell_log']);
|
||
add_submenu_page('wis_shop', 'Preishistorie', 'Preishistorie', 'manage_options', 'wis_price_history', [self::class, 'page_price_history']);
|
||
add_submenu_page('wis_shop', 'Abo-Verwaltung', 'Abo-Verwaltung', 'manage_options', 'wis_abo_admin', [self::class, 'page_abo_admin']);
|
||
add_submenu_page('wis_shop', 'JSON Export/Import', 'JSON Tools', 'manage_options', 'wis_json', [self::class, 'page_json']);
|
||
add_submenu_page('wis_shop', 'Shop Reset', 'Reset', 'manage_options', 'wis_reset', [self::class, 'page_reset']);
|
||
}
|
||
|
||
public static function page_overview() {
|
||
if (isset($_POST['wis_save_settings']) && check_admin_referer('wis_settings')) {
|
||
update_option('wis_currency_name', sanitize_text_field($_POST['wis_currency_name']));
|
||
update_option('wis_image_base_url', esc_url_raw($_POST['wis_image_base_url']));
|
||
update_option('wis_header_text', wp_kses_post($_POST['wis_header_text']));
|
||
update_option('wis_coupon_exclude_offers', isset($_POST['wis_coupon_exclude_offers']) ? '1' : '0');
|
||
update_option('wis_daily_deal_discount', intval($_POST['wis_daily_deal_discount']));
|
||
update_option('wis_offline_queue_enabled', isset($_POST['wis_offline_queue_enabled']) ? '1' : '0');
|
||
update_option('wis_tax_enabled', isset($_POST['wis_tax_enabled']) ? '1' : '0');
|
||
update_option('wis_tax_rate', max(0, min(100, floatval(str_replace(',', '.', $_POST['wis_tax_rate'] ?? '0')))));
|
||
$allowed_pp = ['24', '25', '50', '100', '-1'];
|
||
$pp_val = sanitize_text_field($_POST['wis_default_per_page'] ?? '24');
|
||
update_option('wis_default_per_page', in_array($pp_val, $allowed_pp) ? $pp_val : '25');
|
||
echo '<div class="updated"><p>✅ Einstellungen gespeichert!</p></div>';
|
||
}
|
||
|
||
if (isset($_POST['wis_regen_key']) && check_admin_referer('wis_regen_key')) {
|
||
update_option('wis_api_key', bin2hex(random_bytes(24)));
|
||
echo '<div class="updated"><p>🔑 Neuer API-Key generiert! Bitte in der config.yml des Spigot-Plugins aktualisieren.</p></div>';
|
||
}
|
||
|
||
global $wpdb;
|
||
$stats = [
|
||
'items' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_items"),
|
||
'orders' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_orders"),
|
||
'servers' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_servers"),
|
||
'coupons' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_coupons")
|
||
];
|
||
|
||
?>
|
||
<div class="wrap">
|
||
<h1>🛒 WP Ingame Shop Pro <span style="color:#28a745;">v<?php echo WIS_VERSION; ?></span></h1>
|
||
|
||
<div class="card" style="max-width:800px; margin-top:20px;">
|
||
<h2>📊 Statistiken</h2>
|
||
<table class="widefat">
|
||
<tr><th>Items im Shop:</th><td><strong><?php echo $stats['items']; ?></strong></td></tr>
|
||
<tr><th>Bestellungen:</th><td><strong><?php echo $stats['orders']; ?></strong></td></tr>
|
||
<tr><th>Server:</th><td><strong><?php echo $stats['servers']; ?></strong></td></tr>
|
||
<tr><th>Gutscheine:</th><td><strong><?php echo $stats['coupons']; ?></strong></td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="card" style="max-width:800px; margin-top:20px; padding:20px;">
|
||
<h2>⚙️ Einstellungen</h2>
|
||
<form method="post">
|
||
<?php wp_nonce_field('wis_settings'); ?>
|
||
<table class="form-table">
|
||
<tr>
|
||
<th><label>Shop Header Text</label></th>
|
||
<td>
|
||
<?php
|
||
wp_editor(
|
||
get_option('wis_header_text', ''),
|
||
'wis_header_text',
|
||
[
|
||
'textarea_name' => 'wis_header_text',
|
||
'textarea_rows' => 5,
|
||
'media_buttons' => false,
|
||
'teeny' => true,
|
||
'quicktags' => true,
|
||
'tinymce' => [
|
||
'toolbar1' => 'bold,italic,underline,bullist,numlist,link,unlink,separator,alignleft,aligncenter,alignright,alignjustify,separator,undo,redo',
|
||
'toolbar2' => '',
|
||
],
|
||
]
|
||
);
|
||
?>
|
||
<p class="description" style="margin-top:8px;"><strong>Live-Vorschau:</strong></p>
|
||
<div id="wis-header-preview" style="margin-top:6px;padding:12px 18px;background:#d4edda;border:1px solid #b8dbc0;border-radius:6px;color:#155724;font-size:14px;min-height:40px;"></div>
|
||
<script>
|
||
jQuery(function($) {
|
||
function updatePreview() {
|
||
var content = '';
|
||
if (typeof tinymce !== 'undefined' && tinymce.get('wis_header_text')) {
|
||
content = tinymce.get('wis_header_text').getContent();
|
||
} else {
|
||
content = $('#wis_header_text').val();
|
||
}
|
||
$('#wis-header-preview').html(content || '<em style="color:#888;">Vorschau erscheint hier\u2026</em>');
|
||
}
|
||
$(document).on('tinymce-editor-init', function(event, editor) {
|
||
if (editor.id === 'wis_header_text') {
|
||
editor.on('keyup change NodeChange', updatePreview);
|
||
updatePreview();
|
||
}
|
||
});
|
||
$(document).on('input change', '#wis_header_text', updatePreview);
|
||
setTimeout(updatePreview, 800);
|
||
});
|
||
</script>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="currency">Währungsname</label></th>
|
||
<td>
|
||
<input type="text" id="currency" name="wis_currency_name" value="<?php echo esc_attr(get_option('wis_currency_name')); ?>" class="regular-text">
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="image_url">Bilder Basis-URL</label></th>
|
||
<td>
|
||
<input type="text" id="image_url" name="wis_image_base_url" value="<?php echo esc_attr(get_option('wis_image_base_url')); ?>" class="large-text code">
|
||
<p class="description">Muss mit / enden! Z.B.: https://git.viper.ipv64.net/.../images/</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Gutscheine bei Angeboten</th>
|
||
<td>
|
||
<label>
|
||
<input type="checkbox" name="wis_coupon_exclude_offers" value="1" <?php checked(get_option('wis_coupon_exclude_offers'), '1'); ?>>
|
||
Gutscheine NICHT auf Angebote anwenden
|
||
</label>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<tr>
|
||
<th>Daily Deal</th>
|
||
<td>
|
||
<label>
|
||
<input type="checkbox" name="wis_daily_deal_enabled" value="1" <?php checked(get_option('wis_daily_deal_enabled'), '1'); ?>>
|
||
Automatisches Tagesangebot aktivieren
|
||
</label>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="discount">Daily Deal Rabatt (%)</label></th>
|
||
<td>
|
||
<input type="number" id="discount" name="wis_daily_deal_discount" value="<?php echo esc_attr(get_option('wis_daily_deal_discount', 20)); ?>" min="1" max="99">
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Offline-Queue</th>
|
||
<td>
|
||
<label>
|
||
<input type="checkbox" name="wis_offline_queue_enabled" value="1" <?php checked(get_option('wis_offline_queue_enabled', '0'), '1'); ?>>
|
||
Items auch an offline Spieler liefern (beim nächsten Login)
|
||
</label>
|
||
<p class="description">Erfordert Spigot-Plugin v6.3+ mit Offline-Queue-Unterstützung</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Steuer aktivieren</th>
|
||
<td>
|
||
<label>
|
||
<input type="checkbox" name="wis_tax_enabled" value="1" <?php checked(get_option('wis_tax_enabled', '0'), '1'); ?>>
|
||
Steuer auf den Endbetrag aufschlagen (nach Gutschein-Abzug)
|
||
</label>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="wis_tax_rate">Steuersatz (%)</label></th>
|
||
<td>
|
||
<input type="number" id="wis_tax_rate" name="wis_tax_rate"
|
||
value="<?php echo esc_attr(get_option('wis_tax_rate', '0')); ?>"
|
||
min="0" max="100" step="0.01" style="width:100px;">
|
||
<p class="description">Z.B. <code>19</code> für 19 % MwSt. Nur aktiv wenn „Steuer aktivieren" angehakt ist.</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="wis_default_per_page">Items pro Seite (Standard)</label></th>
|
||
<td>
|
||
<select id="wis_default_per_page" name="wis_default_per_page">
|
||
<option value="24" <?php selected(get_option('wis_default_per_page','25'), '24'); ?>>24 (Standard)</option>
|
||
<option value="25" <?php selected(get_option('wis_default_per_page','25'), '25'); ?>>25</option>
|
||
<option value="50" <?php selected(get_option('wis_default_per_page','25'), '50'); ?>>50</option>
|
||
<option value="100" <?php selected(get_option('wis_default_per_page','25'), '100'); ?>>100</option>
|
||
<option value="-1" <?php selected(get_option('wis_default_per_page','25'), '-1'); ?>>Alle</option>
|
||
</select>
|
||
<p class="description">Spieler können dies im Shop-Frontend per Dropdown selbst anpassen.</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="submit">
|
||
<input type="submit" name="wis_save_settings" class="button button-primary" value="Einstellungen speichern">
|
||
</p>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card" style="max-width:800px; margin-top:20px; padding:20px; border-left:4px solid #0073aa;">
|
||
<h2>🔑 Spigot API-Key</h2>
|
||
<p>Dieser Key muss in der <code>config.yml</code> des Spigot-Plugins als <code>api-key</code> eingetragen werden.</p>
|
||
<div style="display:flex; align-items:center; gap:10px; margin:10px 0;">
|
||
<input type="text"
|
||
id="wis-api-key-field"
|
||
value="<?php echo esc_attr(get_option('wis_api_key', '')); ?>"
|
||
class="large-text code"
|
||
readonly
|
||
style="font-family:monospace; background:#f0f4f8;">
|
||
<button type="button" class="button" onclick="
|
||
const f = document.getElementById('wis-api-key-field');
|
||
f.type='text'; f.select();
|
||
document.execCommand('copy');
|
||
this.textContent='✅ Kopiert!';
|
||
setTimeout(()=>this.textContent='📋 Kopieren',2000);
|
||
">📋 Kopieren</button>
|
||
</div>
|
||
<form method="post" onsubmit="return confirm('Neuen Key generieren? Das Spigot-Plugin muss danach neu konfiguriert werden!');">
|
||
<?php wp_nonce_field('wis_regen_key'); ?>
|
||
<input type="submit" name="wis_regen_key" class="button button-secondary" value="🔄 Neuen Key generieren">
|
||
</form>
|
||
<p class="description" style="margin-top:10px;">
|
||
Geschützte Endpunkte: <code>pending_orders</code>, <code>execute_order</code>, <code>complete_order</code>, <code>cancel_order</code>, <code>pending_offline</code>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
public static function page_items() {
|
||
if (isset($_POST['wis_bulk_save']) && check_admin_referer('wis_bulk_edit')) {
|
||
$ids = isset($_POST['item_ids']) ? array_map('intval', $_POST['item_ids']) : [];
|
||
$action = sanitize_text_field($_POST['bulk_action_type']);
|
||
|
||
if (!empty($ids) && !empty($action)) {
|
||
$count = 0;
|
||
foreach ($ids as $id) {
|
||
$data = [];
|
||
|
||
switch ($action) {
|
||
case 'price':
|
||
if (isset($_POST['item_prices'][$id])) {
|
||
$new_price = intval($_POST['item_prices'][$id]);
|
||
if ($new_price >= 0) {
|
||
$data['price'] = $new_price;
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'offer':
|
||
$is_offer = isset($_POST['item_offers'][$id]) ? 1 : 0;
|
||
$data['is_offer'] = $is_offer;
|
||
|
||
if (isset($_POST['item_offer_prices'][$id])) {
|
||
$offer_price = intval($_POST['item_offer_prices'][$id]);
|
||
$data['offer_price'] = $is_offer ? $offer_price : 0;
|
||
} else if (!$is_offer) {
|
||
$data['offer_price'] = 0;
|
||
}
|
||
break;
|
||
|
||
case 'status':
|
||
$new_status = sanitize_text_field($_POST['bulk_status']);
|
||
if (in_array($new_status, ['publish', 'draft'])) {
|
||
$data['status'] = $new_status;
|
||
}
|
||
break;
|
||
|
||
case 'server':
|
||
if (isset($_POST['item_servers'][$id])) {
|
||
$data['servers'] = json_encode($_POST['item_servers'][$id]);
|
||
} else {
|
||
$data['servers'] = '[]';
|
||
}
|
||
break;
|
||
|
||
case 'category':
|
||
if (isset($_POST['item_cat_assignments'][$id])) {
|
||
$data['categories'] = json_encode($_POST['item_cat_assignments'][$id]);
|
||
}
|
||
break;
|
||
|
||
case 'sell':
|
||
$data['sell_enabled'] = isset($_POST['item_sell_enabled'][$id]) ? 1 : 0;
|
||
$mode = sanitize_text_field($_POST['item_sell_mode'][$id] ?? 'percent');
|
||
if (!in_array($mode, ['percent', 'fixed', 'minus'])) $mode = 'percent';
|
||
$data['sell_price_mode'] = $mode;
|
||
$data['sell_price_value'] = max(0, intval($_POST['item_sell_value'][$id] ?? 80));
|
||
break;
|
||
}
|
||
|
||
if (!empty($data)) {
|
||
WIS_DB::update_item($id, $data);
|
||
$count++;
|
||
}
|
||
}
|
||
echo '<div class="updated"><p>✅ ' . $count . ' Items erfolgreich aktualisiert!</p></div>';
|
||
}
|
||
}
|
||
|
||
if (isset($_POST['wis_bulk_apply']) && !empty($_POST['wis_bulk_action']) && isset($_POST['item_ids'])) {
|
||
$action = sanitize_text_field($_POST['wis_bulk_action']);
|
||
$selected_ids = array_map('intval', $_POST['item_ids']);
|
||
|
||
$all_servers = WIS_DB::get_servers();
|
||
$all_categories = WIS_DB::get_categories();
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
$items_to_edit = WIS_DB::get_items(['ids' => $selected_ids]);
|
||
|
||
?>
|
||
<div class="wrap">
|
||
<h1>📦 Mehrfachbearbeitung</h1>
|
||
<p><strong>Aktion:</strong> <?php echo esc_html(ucfirst($action)); ?> für <?php echo count($selected_ids); ?> Items.</p>
|
||
|
||
<div class="card" style="max-width:2000px; padding:20px; margin-top:20px;">
|
||
<form method="post">
|
||
<?php wp_nonce_field('wis_bulk_edit'); ?>
|
||
<input type="hidden" name="bulk_action_type" value="<?php echo esc_attr($action); ?>">
|
||
|
||
<?php foreach ($selected_ids as $id): ?>
|
||
<input type="hidden" name="item_ids[]" value="<?php echo $id; ?>">
|
||
<?php endforeach; ?>
|
||
|
||
<table class="form-table">
|
||
<?php if ($action === 'price'): ?>
|
||
<tr>
|
||
<th colspan="2"><h3>Preis pro Item festlegen</h3></th>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding:0;">
|
||
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:50%">Item Name</th>
|
||
<th style="width:25%">Aktueller Preis</th>
|
||
<th style="width:25%">Neuer Preis (<?php echo esc_html($currency); ?>)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach($items_to_edit as $item): ?>
|
||
<tr>
|
||
<td>
|
||
<strong><?php echo esc_html($item->name); ?></strong><br>
|
||
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
|
||
</td>
|
||
<td><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></td>
|
||
<td>
|
||
<input type="number"
|
||
name="item_prices[<?php echo $item->id; ?>]"
|
||
value="<?php echo esc_attr($item->price); ?>"
|
||
min="0"
|
||
style="width:100%; padding:5px;">
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
|
||
<?php elseif ($action === 'offer'): ?>
|
||
<tr>
|
||
<th colspan="2"><h3>Angebot pro Item festlegen</h3></th>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding:0;">
|
||
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:40%">Item Name</th>
|
||
<th style="width:20%">Normalpreis</th>
|
||
<th style="width:20%">Als Angebot?</th>
|
||
<th style="width:20%">Angebotspreis</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach($items_to_edit as $item): ?>
|
||
<tr>
|
||
<td>
|
||
<strong><?php echo esc_html($item->name); ?></strong><br>
|
||
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
|
||
</td>
|
||
<td><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></td>
|
||
<td>
|
||
<input type="checkbox"
|
||
name="item_offers[<?php echo $item->id; ?>]"
|
||
value="1"
|
||
<?php checked($item->is_offer, 1); ?>
|
||
onchange="toggleOfferPrice(this, <?php echo $item->id; ?>)">
|
||
</td>
|
||
<td>
|
||
<input type="number"
|
||
name="item_offer_prices[<?php echo $item->id; ?>]"
|
||
id="offer_price_<?php echo $item->id; ?>"
|
||
value="<?php echo esc_attr($item->offer_price); ?>"
|
||
min="0"
|
||
style="width:100%; padding:5px;"
|
||
<?php if(!$item->is_offer): ?>disabled<?php endif; ?>>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<script>
|
||
function toggleOfferPrice(checkbox, itemId) {
|
||
const input = document.getElementById('offer_price_' + itemId);
|
||
input.disabled = !checkbox.checked;
|
||
}
|
||
</script>
|
||
</td>
|
||
</tr>
|
||
|
||
<?php elseif ($action === 'status'): ?>
|
||
<tr>
|
||
<th><label>Neuer Status für alle ausgewählten Items</label></th>
|
||
<td>
|
||
<select name="bulk_status">
|
||
<option value="publish">Aktivieren (Publish)</option>
|
||
<option value="draft">Deaktivieren (Draft)</option>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
|
||
<?php elseif ($action === 'server'): ?>
|
||
<tr>
|
||
<th colspan="2"><h3>Server pro Item zuweisen</h3></th>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding:0;">
|
||
<?php if (empty($all_servers)): ?>
|
||
<p>Keine Server vorhanden.</p>
|
||
<?php else: ?>
|
||
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:40%">Item Name</th>
|
||
<th style="width:60%">Server (Mehrfachauswahl möglich)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach($items_to_edit as $item):
|
||
$current_servers = json_decode($item->servers, true) ?: [];
|
||
?>
|
||
<tr>
|
||
<td>
|
||
<strong><?php echo esc_html($item->name); ?></strong><br>
|
||
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
|
||
</td>
|
||
<td>
|
||
<?php foreach ($all_servers as $s):
|
||
$checked = in_array($s->slug, $current_servers) ? 'checked' : '';
|
||
?>
|
||
<label style="display:inline-block; margin-right:15px; margin-bottom:5px;">
|
||
<input type="checkbox"
|
||
name="item_servers[<?php echo $item->id; ?>][]"
|
||
value="<?php echo esc_attr($s->slug); ?>"
|
||
<?php echo $checked; ?>>
|
||
<?php echo esc_html($s->name); ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
|
||
<?php elseif ($action === 'category'): ?>
|
||
<tr>
|
||
<th colspan="2"><h3>Kategorien pro Item zuweisen</h3></th>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding:0;">
|
||
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
|
||
<?php if(empty($items_to_edit)): ?>
|
||
<p>Fehler beim Laden der Items.</p>
|
||
<?php else: ?>
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:35%">Item Name</th>
|
||
<th style="width:65%">Kategorie (Mehrfachauswahl mit STRG+Klick)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach($items_to_edit as $item):
|
||
$current_cats = json_decode($item->categories, true) ?: [];
|
||
?>
|
||
<tr>
|
||
<td>
|
||
<strong><?php echo esc_html($item->name); ?></strong><br>
|
||
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
|
||
</td>
|
||
<td>
|
||
<select name="item_cat_assignments[<?php echo $item->id; ?>][]" multiple style="height:250px; width:100%; font-size:14px; padding:5px;">
|
||
<?php foreach($all_categories as $cat):
|
||
$selected = in_array($cat->slug, $current_cats) ? 'selected' : '';
|
||
?>
|
||
<option value="<?php echo esc_attr($cat->slug); ?>" <?php echo $selected; ?>>
|
||
<?php echo esc_html($cat->name); ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php elseif ($action === 'sell'): ?>
|
||
<tr>
|
||
<th colspan="2"><h3>Ankauf pro Item konfigurieren</h3></th>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2" style="padding:0;">
|
||
<div style="max-height:800px; overflow-y:auto; border:1px solid #ddd; padding:10px;">
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:30%">Item Name</th>
|
||
<th style="width:12%">VK-Preis</th>
|
||
<th style="width:13%">Ankauf?</th>
|
||
<th style="width:18%">Modus</th>
|
||
<th style="width:12%">Wert</th>
|
||
<th style="width:15%">Ankaufspreis</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($items_to_edit as $item):
|
||
$s_enabled = !empty($item->sell_enabled);
|
||
$s_mode = $item->sell_price_mode ?? 'percent';
|
||
$s_value = $item->sell_price_value ?? 80;
|
||
?>
|
||
<tr id="sell_row_<?php echo $item->id; ?>">
|
||
<td>
|
||
<strong><?php echo esc_html($item->name); ?></strong><br>
|
||
<small style="color:#666"><?php echo esc_html($item->item_id); ?></small>
|
||
</td>
|
||
<td><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></td>
|
||
<td>
|
||
<input type="checkbox"
|
||
name="item_sell_enabled[<?php echo $item->id; ?>]"
|
||
value="1"
|
||
<?php checked($s_enabled); ?>
|
||
onchange="wisUpdateSellRow(<?php echo $item->id; ?>, <?php echo intval($item->price); ?>)">
|
||
</td>
|
||
<td>
|
||
<select name="item_sell_mode[<?php echo $item->id; ?>]"
|
||
id="sell_mode_<?php echo $item->id; ?>"
|
||
onchange="wisUpdateSellRow(<?php echo $item->id; ?>, <?php echo intval($item->price); ?>)"
|
||
style="width:100%">
|
||
<option value="percent" <?php selected($s_mode, 'percent'); ?>>% vom VK</option>
|
||
<option value="minus" <?php selected($s_mode, 'minus'); ?>>VK minus</option>
|
||
<option value="fixed" <?php selected($s_mode, 'fixed'); ?>>Fixpreis</option>
|
||
</select>
|
||
</td>
|
||
<td>
|
||
<input type="number"
|
||
name="item_sell_value[<?php echo $item->id; ?>]"
|
||
id="sell_value_<?php echo $item->id; ?>"
|
||
value="<?php echo esc_attr($s_value); ?>"
|
||
min="0" style="width:70px"
|
||
oninput="wisUpdateSellRow(<?php echo $item->id; ?>, <?php echo intval($item->price); ?>)">
|
||
</td>
|
||
<td id="sell_preview_<?php echo $item->id; ?>" style="color:#0073aa;font-weight:bold;">
|
||
<?php
|
||
if ($s_enabled) {
|
||
if ($s_mode === 'percent') $sp = round($item->price * $s_value / 100, 2);
|
||
elseif ($s_mode === 'minus') $sp = max(0, $item->price - $s_value);
|
||
else $sp = max(0, $s_value);
|
||
echo number_format($sp, 2) . ' ' . esc_html($currency);
|
||
} else {
|
||
echo '<span style="color:#aaa">–</span>';
|
||
}
|
||
?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<script>
|
||
function wisUpdateSellRow(id, vk) {
|
||
var enabled = document.querySelector('input[name="item_sell_enabled[' + id + ']"]').checked;
|
||
var mode = document.getElementById('sell_mode_' + id).value;
|
||
var val = parseFloat(document.getElementById('sell_value_' + id).value) || 0;
|
||
var preview = document.getElementById('sell_preview_' + id);
|
||
if (!enabled) { preview.innerHTML = '<span style="color:#aaa">–</span>'; return; }
|
||
var price = 0;
|
||
if (mode === 'percent') price = Math.max(0, Math.round(vk * val) / 100);
|
||
else if (mode === 'minus') price = Math.max(0, vk - val);
|
||
else price = Math.max(0, val);
|
||
preview.textContent = price.toFixed(2) + ' <?php echo esc_js($currency); ?>';
|
||
}
|
||
// Initiale Vorschau für alle Zeilen
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
<?php foreach ($items_to_edit as $item): ?>
|
||
wisUpdateSellRow(<?php echo $item->id; ?>, <?php echo intval($item->price); ?>);
|
||
<?php endforeach; ?>
|
||
});
|
||
</script>
|
||
</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
</table>
|
||
|
||
<p class="submit">
|
||
<a href="<?php echo admin_url('admin.php?page=wis_items'); ?>" class="button">Abbrechen</a>
|
||
<input type="submit" name="wis_bulk_save" class="button button-primary" value="Jetzt ändern">
|
||
</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
return;
|
||
}
|
||
|
||
if (isset($_GET['action'], $_GET['id'], $_GET['_wpnonce'])) {
|
||
if (!wp_verify_nonce($_GET['_wpnonce'], 'wis_item_action')) {
|
||
wp_die('Security check failed');
|
||
}
|
||
|
||
$id = intval($_GET['id']);
|
||
|
||
if ($_GET['action'] === 'delete') {
|
||
WIS_DB::delete_item($id);
|
||
echo '<div class="updated"><p>✅ Item gelöscht!</p></div>';
|
||
} elseif ($_GET['action'] === 'toggle_status') {
|
||
$item = WIS_DB::get_item($id);
|
||
$new_status = ($item->status === 'publish') ? 'draft' : 'publish';
|
||
WIS_DB::update_item($id, ['status' => $new_status]);
|
||
echo '<div class="updated"><p>✅ Status geändert!</p></div>';
|
||
}
|
||
}
|
||
|
||
// Fehlermeldung aus handle_save_item() anzeigen (falls Validierung fehlschlug)
|
||
$save_error = get_transient('wis_save_error_' . get_current_user_id());
|
||
if ($save_error) {
|
||
delete_transient('wis_save_error_' . get_current_user_id());
|
||
echo '<div class="error"><p>' . esc_html($save_error) . '</p></div>';
|
||
}
|
||
|
||
if (isset($_GET['created'])) {
|
||
echo '<div class="updated"><p>✅ Item erfolgreich erstellt!</p></div>';
|
||
}
|
||
if (isset($_GET['saved'])) {
|
||
echo '<div class="updated"><p>✅ Item gespeichert!</p></div>';
|
||
}
|
||
|
||
if (isset($_GET['edit']) || isset($_GET['add'])) {
|
||
$item = isset($_GET['edit']) ? WIS_DB::get_item(intval($_GET['edit'])) : null;
|
||
$servers = WIS_DB::get_servers();
|
||
$categories = WIS_DB::get_categories();
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
$item_servers = $item ? json_decode($item->servers, true) : [];
|
||
$item_cats = $item ? json_decode($item->categories, true) : [];
|
||
|
||
if (!$item && isset($_GET['add'])) {
|
||
$item_cats = [];
|
||
}
|
||
|
||
?>
|
||
<div class="wrap">
|
||
<h1><?php echo $item ? 'Item bearbeiten' : 'Neues Item'; ?></h1>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_items'); ?>" class="button">← Zurück zur Liste</a>
|
||
|
||
<form method="post" style="max-width:800px; margin-top:20px;">
|
||
<?php wp_nonce_field('wis_item_form'); ?>
|
||
<?php if (isset($_GET['edit'])): ?>
|
||
<input type="hidden" name="edit_id" value="<?php echo intval($_GET['edit']); ?>">
|
||
<?php endif; ?>
|
||
|
||
<table class="form-table">
|
||
<tr>
|
||
<th><label for="name">Name *</label></th>
|
||
<td><input type="text" id="name" name="name" value="<?php echo $item ? esc_attr($item->name) : ''; ?>" class="regular-text" required></td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="item_id">Item ID / Typ *</label></th>
|
||
<td>
|
||
<?php
|
||
$fly_ids = ['fly_5min','fly_15min','fly_30min','fly_1h','fly_2h','fly_3h'];
|
||
$is_fly = $item && in_array($item->item_id, $fly_ids);
|
||
$is_rank = $item && preg_match('/^rank_([^_]+(?:_[^_]+)*)_([a-zA-Z0-9_\-]+)_([a-zA-Z0-9_\-]+)_(\d+)$/', $item->item_id, $rm);
|
||
// Format: rank_{rank_id}_{lp_group}_{default_group}_{days}
|
||
// Fallback für altes Format rank_{rank_id}_{days}
|
||
$is_rank_old = !$is_rank && $item && preg_match('/^rank_(.+)_(\d+)$/', $item->item_id, $rm_old);
|
||
$is_fly_abo = $item && ($item->item_id === 'fly_abo' || preg_match('/^fly_abo_\d+$/', $item->item_id));
|
||
$cur_abo_days = 0;
|
||
$is_plot_slots = $item && preg_match('/^plot_slots_(\d+)$/', $item->item_id, $ps_m);
|
||
$cur_plot_extra_slots = $is_plot_slots ? intval($ps_m[1]) : 1;
|
||
$is_plot_abo = $item && preg_match('/^plot_abo_(\d+)$/', $item->item_id, $pa_m);
|
||
$cur_plot_abo_slots = $is_plot_abo ? intval($pa_m[1]) : 1;
|
||
$cur_rank_id = $is_rank ? $rm[1] : ($is_rank_old ? $rm_old[1] : 'vip');
|
||
$cur_lp_group = $is_rank ? $rm[2] : $cur_rank_id;
|
||
$cur_default_group = $is_rank ? $rm[3] : 'default';
|
||
$cur_rank_days = $is_rank ? intval($rm[4]) : ($is_rank_old ? intval($rm_old[2]) : 30);
|
||
$cur_label = $item ? esc_attr($item->name) : '';
|
||
$is_custom_cmd = $item && preg_match('/^custom_cmd_(.+)$/', $item->item_id, $cc_m);
|
||
$cur_custom_cmd_id = $is_custom_cmd ? $cc_m[1] : '';
|
||
$cur_custom_command = ($item && $is_custom_cmd) ? ($item->custom_command ?? '') : '';
|
||
// Item-Abo: item_abo_{minecraft_item_id}_{daily_qty}_{duration_days}
|
||
$is_item_abo = $item && preg_match('/^item_abo_(.+)_(\d+)_(\d+)$/', $item->item_id, $ia_m);
|
||
$cur_abo_item_id = $is_item_abo ? $ia_m[1] : 'minecraft:stone';
|
||
$cur_abo_daily_qty = $is_item_abo ? intval($ia_m[2]) : 1;
|
||
$cur_abo_duration = $is_item_abo ? intval($ia_m[3]) : 30;
|
||
$is_gift_card = $item && str_starts_with($item->item_id, 'gift_card');
|
||
$detected_type = $is_fly ? 'fly' : (($is_rank || $is_rank_old) ? 'rank' : ($is_fly_abo ? 'fly_abo' : ($is_plot_slots ? 'plot_slots' : ($is_plot_abo ? 'plot_abo' : ($is_item_abo ? 'item_abo' : ($is_custom_cmd ? 'custom_cmd' : ($is_gift_card ? 'gift_card' : 'minecraft')))))));
|
||
?>
|
||
<select id="item_type" name="item_type" onchange="wisToggleItemType(this.value)" style="margin-bottom:8px;">
|
||
<option value="minecraft" <?php echo $detected_type === 'minecraft' ? 'selected' : ''; ?>>Minecraft Item</option>
|
||
<option value="fly" <?php echo $detected_type === 'fly' ? 'selected' : ''; ?>>✈ Fly-Gutschein</option>
|
||
<option value="rank" <?php echo $detected_type === 'rank' ? 'selected' : ''; ?>>👑 Rang (LuckPerms)</option>
|
||
<option value="fly_abo" <?php echo $detected_type === 'fly_abo' ? 'selected' : ''; ?>>✈ Fly-Abo (tägl. Limit)</option>
|
||
<option value="plot_slots" <?php echo $detected_type === 'plot_slots' ? 'selected' : ''; ?>>📦 Plot-Slots (einmalig)</option>
|
||
<option value="plot_abo" <?php echo $detected_type === 'plot_abo' ? 'selected' : ''; ?>>📦 Plot-Abo (monatlich)</option>
|
||
<option value="item_abo" <?php echo $detected_type === 'item_abo' ? 'selected' : ''; ?>>📅 Item-Abo (täglich)</option>
|
||
<option value="custom_cmd" <?php echo $detected_type === 'custom_cmd' ? 'selected' : ''; ?>>⚙️ Custom Command</option>
|
||
<option value="gift_card" <?php echo $detected_type === 'gift_card' ? 'selected' : ''; ?>>🎁 Gutschein-Karte (freier Betrag)</option>
|
||
</select>
|
||
|
||
<div id="wis_item_minecraft">
|
||
<input type="text" id="item_id" name="item_id" value="<?php echo ($item && !$is_fly && !$is_rank && !$is_fly_abo && !$is_plot_slots && !$is_plot_abo) ? esc_attr($item->item_id) : ''; ?>" class="regular-text" placeholder="minecraft:diamond">
|
||
<p class="description">Z.B.: minecraft:diamond (Kategorien werden automatisch zugewiesen)</p>
|
||
</div>
|
||
|
||
<div id="wis_item_fly" style="display:none;">
|
||
<select name="fly_duration" id="fly_duration">
|
||
<option value="fly_5min" <?php echo ($item && $item->item_id === 'fly_5min') ? 'selected' : ''; ?>>✈ 5 Minuten Fly</option>
|
||
<option value="fly_15min" <?php echo ($item && $item->item_id === 'fly_15min') ? 'selected' : ''; ?>>✈ 15 Minuten Fly</option>
|
||
<option value="fly_30min" <?php echo ($item && $item->item_id === 'fly_30min') ? 'selected' : ''; ?>>✈ 30 Minuten Fly</option>
|
||
<option value="fly_1h" <?php echo ($item && $item->item_id === 'fly_1h') ? 'selected' : ''; ?>>✈ 1 Stunde Fly</option>
|
||
<option value="fly_2h" <?php echo ($item && $item->item_id === 'fly_2h') ? 'selected' : ''; ?>>✈ 2 Stunden Fly</option>
|
||
<option value="fly_3h" <?php echo ($item && $item->item_id === 'fly_3h') ? 'selected' : ''; ?>>✈ 3 Stunden Fly</option>
|
||
</select>
|
||
<p class="description">Der Spieler bekommt nach dem Kauf Fly für die gewählte Dauer.</p>
|
||
</div>
|
||
|
||
<div id="wis_item_rank" style="display:none;">
|
||
<table style="border-collapse:collapse;">
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label for="rank_id"><strong>Rang-ID:</strong></label></td>
|
||
<td><input type="text" id="rank_id" name="rank_id" value="<?php echo esc_attr($cur_rank_id); ?>" class="regular-text" placeholder="vip" style="width:160px;"></td>
|
||
<td style="padding:4px 0 4px 12px; color:#666; font-size:12px;">Interner Name (frei wählbar, z. B. <code>vip</code>)</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label for="lp_group"><strong>LuckPerms-Gruppe:</strong></label></td>
|
||
<td><input type="text" id="lp_group" name="lp_group" value="<?php echo esc_attr($cur_lp_group); ?>" class="regular-text" placeholder="vip" style="width:160px;"></td>
|
||
<td style="padding:4px 0 4px 12px; color:#666; font-size:12px;">Exakter Gruppenname in LuckPerms</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label for="default_group"><strong>Standard-Gruppe (nach Ablauf):</strong></label></td>
|
||
<td><input type="text" id="default_group" name="default_group" value="<?php echo esc_attr($cur_default_group); ?>" class="regular-text" placeholder="default" style="width:160px;"></td>
|
||
<td style="padding:4px 0 4px 12px; color:#666; font-size:12px;">Gruppe nach Rang-Ablauf (z. B. <code>default</code>)</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label for="rank_days"><strong>Laufzeit (Tage):</strong></label></td>
|
||
<td>
|
||
<input type="number" id="rank_days" name="rank_days" value="<?php echo esc_attr($cur_rank_days); ?>" min="0" style="width:80px;">
|
||
<span class="description" style="margin-left:8px;">0 = dauerhaft</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="description" style="margin-top:6px;">
|
||
Der Spielername im Shop (Feld <em>Name</em> oben) wird dem Spieler als Rang-Label angezeigt.<br>
|
||
Gespeicherte Item-ID: <code>rank_{rang-id}_{lp-gruppe}_{standard-gruppe}_{tage}</code>
|
||
</p>
|
||
</div>
|
||
|
||
<div id="wis_item_fly_abo" style="display:none;">
|
||
<p class="description" style="margin-top:4px; padding:10px; background:#f0f6fc; border-left:3px solid #2271b1; border-radius:2px;">
|
||
<strong>✈ Fly-Abo – monatliches Abonnement</strong><br><br>
|
||
Spieler zahlen den <strong>Artikelpreis einmalig beim Kauf</strong>.<br>
|
||
Danach wird am <strong>1. jedes Monats</strong> automatisch der gleiche Betrag per Vault abgebucht.<br>
|
||
Kann der Spieler nicht zahlen, wird das Abo automatisch gekündigt.<br>
|
||
Kündigung jederzeit ingame mit <code>/flyabocancel</code> – läuft bis Monatsende.<br><br>
|
||
<strong>Artikelpreis</strong> = monatlicher Betrag |
|
||
Tägl. Fly-Limit: konfigurierbar per <code>fly-abo.max-daily-hours</code> im Plugin.<br>
|
||
Gespeicherte Item-ID: <code>fly_abo</code>
|
||
</p>
|
||
</div>
|
||
|
||
<div id="wis_item_plot_slots" style="display:none;">
|
||
<table style="border-collapse:collapse;">
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label for="plot_extra_slots"><strong>Zusätzliche Plot-Slots:</strong></label></td>
|
||
<td>
|
||
<input type="number" id="plot_extra_slots" name="plot_extra_slots"
|
||
value="<?php echo esc_attr($cur_plot_extra_slots); ?>"
|
||
min="1" style="width:80px;">
|
||
<span style="margin-left:6px; color:#666;">Slots</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="description" style="margin-top:6px; padding:8px; background:#f6f7f7; border-left:3px solid #50b56a; border-radius:2px;">
|
||
<strong>📦 Plot-Slots – einmaliger Kauf (permanent)</strong><br><br>
|
||
Der Spieler erhält dauerhaft zusätzliche Plot-Slots on top seines Rang-Limits.<br>
|
||
LuckPerms Meta <code>plotlimit</code> wird automatisch gesetzt.<br>
|
||
Gespeicherte Item-ID: <code>plot_slots_{anzahl}</code>
|
||
</p>
|
||
</div>
|
||
|
||
<div id="wis_item_plot_abo" style="display:none;">
|
||
<table style="border-collapse:collapse;">
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label for="plot_abo_slots"><strong>Zusätzliche Plot-Slots (Abo):</strong></label></td>
|
||
<td>
|
||
<input type="number" id="plot_abo_slots" name="plot_abo_slots"
|
||
value="<?php echo esc_attr($cur_plot_abo_slots); ?>"
|
||
min="1" style="width:80px;">
|
||
<span style="margin-left:6px; color:#666;">Slots</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="description" style="margin-top:6px; padding:8px; background:#f0f6fc; border-left:3px solid #2271b1; border-radius:2px;">
|
||
<strong>📦 Plot-Abo – monatliches Abonnement</strong><br><br>
|
||
Spieler zahlen den <strong>Artikelpreis einmalig beim Kauf</strong>.<br>
|
||
Am <strong>1. jedes Monats</strong> wird der gleiche Betrag per Vault abgebucht.<br>
|
||
Bei Zahlungsausfall: Abo-Slots verfallen, überzählige Plots werden eingefroren.<br>
|
||
Kündigung ingame mit <code>/plotabocancel confirm</code> – läuft bis Monatsende.<br><br>
|
||
<strong>Artikelpreis</strong> = monatlicher Beitrag<br>
|
||
Gespeicherte Item-ID: <code>plot_abo_{anzahl}</code>
|
||
</p>
|
||
</div>
|
||
|
||
<div id="wis_item_custom_cmd" style="display:none;">
|
||
<table style="border-collapse:collapse;width:100%;">
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;width:180px;"><label for="custom_cmd_id"><strong>Interne ID *</strong></label></td>
|
||
<td>
|
||
<input type="text" id="custom_cmd_id" name="custom_cmd_id"
|
||
value="<?php echo esc_attr($cur_custom_cmd_id ?? ''); ?>"
|
||
class="regular-text" placeholder="furnace_upgrade">
|
||
<p class="description">Nur a-z, 0-9, _ und - erlaubt. Z.B.: <code>furnace_upgrade</code></p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px 10px 4px 0;vertical-align:top;"><label for="custom_command"><strong>Command *</strong></label></td>
|
||
<td>
|
||
<textarea id="custom_command" name="custom_command"
|
||
rows="3" style="width:100%;font-family:monospace;"
|
||
placeholder="/flgive {player} {amount}"><?php echo esc_textarea($cur_custom_command ?? ''); ?></textarea>
|
||
<p class="description">
|
||
Verfügbare Platzhalter:<br>
|
||
<code>{player}</code> → Spielername<br>
|
||
<code>{amount}</code> → gekaufte Menge<br>
|
||
<code>{server}</code> → Server-Slug<br>
|
||
Mehrere Commands per Zeile möglich. Jede Zeile wird einzeln ausgeführt.
|
||
</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="description" style="margin-top:10px;padding:10px;background:#fff8e1;border-left:3px solid #ffc107;border-radius:2px;">
|
||
<strong>⚙️ Custom Command – beliebiger Server-Command</strong><br><br>
|
||
Der eingetragene Command wird nach dem Kauf auf dem Spigot-Server ausgeführt.<br>
|
||
Beispiel für FurnaceLevels: <code>/flgive {player} {amount}</code><br>
|
||
Gespeicherte Item-ID: <code>custom_cmd_{id}</code>
|
||
</p>
|
||
</div>
|
||
|
||
<div id="wis_item_item_abo" style="display:none;">
|
||
<table style="border-collapse:collapse;width:100%;">
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;width:200px;"><label for="abo_item_id"><strong>Minecraft Item-ID *</strong></label></td>
|
||
<td>
|
||
<input type="text" id="abo_item_id" name="abo_item_id"
|
||
value="<?php echo esc_attr($cur_abo_item_id ?? 'minecraft:stone'); ?>"
|
||
class="regular-text" placeholder="minecraft:dragon_breath">
|
||
<p class="description">Z.B.: <code>minecraft:dragon_breath</code> – das Item das täglich geliefert wird.</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label for="abo_daily_qty"><strong>Tägliche Menge *</strong></label></td>
|
||
<td>
|
||
<input type="number" id="abo_daily_qty" name="abo_daily_qty"
|
||
value="<?php echo esc_attr($cur_abo_daily_qty ?? 1); ?>"
|
||
min="1" max="64" style="width:80px;">
|
||
<span style="margin-left:6px; color:#666;">Stück / Tag</span>
|
||
<p class="description">Wie viele Items der Spieler täglich erhält (max. 64).</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label for="abo_duration_days"><strong>Laufzeit (Tage) *</strong></label></td>
|
||
<td>
|
||
<input type="number" id="abo_duration_days" name="abo_duration_days"
|
||
value="<?php echo esc_attr($cur_abo_duration ?? 30); ?>"
|
||
min="1" style="width:80px;">
|
||
<span style="margin-left:6px; color:#666;">Tage</span>
|
||
<p class="description">Wie viele Tage das Abo nach dem Kauf aktiv ist (z.B. 30 = 1 Monat).</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="description" style="margin-top:10px;padding:10px;background:#f0fff4;border-left:3px solid #28a745;border-radius:2px;">
|
||
<strong>📅 Item-Abo – tägliche Artikel-Lieferung</strong><br><br>
|
||
Spieler bezahlen den <strong>Artikelpreis einmalig beim Kauf</strong>.<br>
|
||
Danach bekommen sie jeden Tag automatisch die eingestellte Anzahl des Items ingame.<br>
|
||
Die Lieferung erfolgt über einen Pending-Order – das Spigot-Plugin liefert beim nächsten Login aus.<br><br>
|
||
<strong>Artikelpreis</strong> = einmaliger Kaufpreis | Laufzeit konfigurierbar<br>
|
||
Gespeicherte Item-ID: <code>item_abo_{minecraft_id}_{menge}_{tage}</code>
|
||
</p>
|
||
</div>
|
||
|
||
|
||
<div id="wis_item_gift_card" style="display:none;">
|
||
<table style="border-collapse:collapse;width:100%;">
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;width:200px;"><label><strong>Min. Betrag (<?php echo esc_html(get_option('wis_currency_name','Coins')); ?>)</strong></label></td>
|
||
<td>
|
||
<input type="number" id="gift_card_min" name="gift_card_min"
|
||
value="<?php echo esc_attr($item && preg_match('/^gift_card_(\d+)_(\d+)$/', $item->item_id, $gcm) ? $gcm[1] : 100); ?>"
|
||
min="1" style="width:100px;">
|
||
<p class="description">Mindestbetrag den der Käufer eingeben kann.</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:4px 10px 4px 0;"><label><strong>Max. Betrag (<?php echo esc_html(get_option('wis_currency_name','Coins')); ?>)</strong></label></td>
|
||
<td>
|
||
<input type="number" id="gift_card_max" name="gift_card_max"
|
||
value="<?php echo esc_attr($item && preg_match('/^gift_card_(\d+)_(\d+)$/', $item->item_id, $gcm) ? $gcm[2] : 5000); ?>"
|
||
min="1" style="width:100px;">
|
||
<p class="description">Höchstbetrag den der Käufer eingeben kann.</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="description" style="margin-top:10px;padding:10px;background:#fff3e0;border-left:3px solid #ff9800;border-radius:2px;">
|
||
<strong>🎁 Gutschein-Karte – freier Betrag</strong><br><br>
|
||
Der Käufer wählt beim Kauf selbst einen Betrag (innerhalb Min/Max).<br>
|
||
Er erhält sofort einen <strong>einzigartigen Gutschein-Code</strong> den er beim nächsten Kauf einlösen kann.<br>
|
||
Eine Auszahlung ist nicht möglich.<br>
|
||
Der Artikelpreis wird beim Speichern automatisch auf den Mindestwert gesetzt.<br><br>
|
||
Gespeicherte Item-ID: <code>gift_card_{min}_{max}</code>
|
||
</p>
|
||
</div>
|
||
|
||
<script>
|
||
function wisToggleItemType(val) {
|
||
document.getElementById('wis_item_minecraft').style.display = (val === 'minecraft') ? '' : 'none';
|
||
document.getElementById('wis_item_fly').style.display = (val === 'fly') ? '' : 'none';
|
||
document.getElementById('wis_item_rank').style.display = (val === 'rank') ? '' : 'none';
|
||
document.getElementById('wis_item_fly_abo').style.display = (val === 'fly_abo') ? '' : 'none';
|
||
document.getElementById('wis_item_plot_slots').style.display = (val === 'plot_slots') ? '' : 'none';
|
||
document.getElementById('wis_item_plot_abo').style.display = (val === 'plot_abo') ? '' : 'none';
|
||
document.getElementById('wis_item_item_abo').style.display = (val === 'item_abo') ? '' : 'none';
|
||
document.getElementById('wis_item_custom_cmd').style.display = (val === 'custom_cmd') ? '' : 'none';
|
||
document.getElementById('wis_item_gift_card').style.display = (val === 'gift_card') ? '' : 'none';
|
||
document.getElementById('item_id').required = (val === 'minecraft');
|
||
document.getElementById('custom_cmd_id').required = (val === 'custom_cmd');
|
||
document.getElementById('abo_item_id').required = (val === 'item_abo');
|
||
}
|
||
wisToggleItemType(document.getElementById('item_type').value);
|
||
</script>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="custom_image_url">Bild-URL (optional)</label></th>
|
||
<td>
|
||
<input type="url" id="custom_image_url" name="custom_image_url"
|
||
value="<?php echo ($item && !empty($item->custom_image_url)) ? esc_attr($item->custom_image_url) : ''; ?>"
|
||
class="large-text" placeholder="https://example.com/mein-fly-bild.png"
|
||
oninput="wisPreviewImage(this.value)">
|
||
<p class="description">
|
||
Eigene Bild-URL – überschreibt die automatische Minecraft-Item-ID-URL.<br>
|
||
Ideal für Fly-Gutscheine, VIP-Pakete etc. Leer lassen = Standard.
|
||
</p>
|
||
<div id="wis_img_preview" style="margin-top:8px; <?php echo ($item && !empty($item->custom_image_url)) ? '' : 'display:none;'; ?>">
|
||
<img id="wis_img_preview_img"
|
||
src="<?php echo ($item && !empty($item->custom_image_url)) ? esc_url($item->custom_image_url) : ''; ?>"
|
||
style="max-height:80px; border-radius:6px; border:1px solid #ddd; padding:4px; background:#2d2d2d;"
|
||
alt="Vorschau"
|
||
onerror="document.getElementById('wis_img_preview').style.display='none';">
|
||
<span style="margin-left:8px; color:#666; font-size:12px;">Vorschau</span>
|
||
</div>
|
||
<script>
|
||
function wisPreviewImage(url) {
|
||
var box = document.getElementById('wis_img_preview');
|
||
var img = document.getElementById('wis_img_preview_img');
|
||
if (url) {
|
||
img.src = url;
|
||
box.style.display = '';
|
||
} else {
|
||
box.style.display = 'none';
|
||
}
|
||
}
|
||
</script>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="description">Beschreibung</label></th>
|
||
<td><textarea id="description" name="description" rows="3" class="large-text"><?php echo $item ? esc_textarea($item->description) : ''; ?></textarea></td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="price">Preis (<?php echo esc_html($currency); ?>) *</label></th>
|
||
<td><input type="number" id="price" name="price" value="<?php echo $item ? esc_attr($item->price) : '0'; ?>" min="0" required></td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="offer_price">Angebotspreis</label></th>
|
||
<td>
|
||
<input type="number" id="offer_price" name="offer_price" value="<?php echo $item ? esc_attr($item->offer_price) : '0'; ?>" min="0">
|
||
<p class="description">Optional: Wenn gesetzt, wird der normale Preis durchgestrichen</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="sell_enabled">Ankauf aktivieren</label></th>
|
||
<td>
|
||
<label>
|
||
<input type="checkbox" id="sell_enabled" name="sell_enabled" value="1"
|
||
<?php echo ($item && !empty($item->sell_enabled)) ? 'checked' : ''; ?>
|
||
onchange="wisToggleSell(this.checked)">
|
||
Spieler können dieses Item an den Shop verkaufen
|
||
</label>
|
||
</td>
|
||
</tr>
|
||
<tr id="wis_sell_row" <?php echo ($item && !empty($item->sell_enabled)) ? '' : 'style="display:none"'; ?>>
|
||
<th><label for="sell_price_mode">Ankaufspreis</label></th>
|
||
<td>
|
||
<select id="sell_price_mode" name="sell_price_mode" onchange="wisUpdateSellPreview()">
|
||
<option value="percent" <?php echo ($item && $item->sell_price_mode === 'percent') ? 'selected' : ''; ?>>% vom Verkaufspreis</option>
|
||
<option value="minus" <?php echo ($item && $item->sell_price_mode === 'minus') ? 'selected' : ''; ?>>Verkaufspreis minus fixer Betrag</option>
|
||
<option value="fixed" <?php echo ($item && $item->sell_price_mode === 'fixed') ? 'selected' : ''; ?>>Fixer Preis</option>
|
||
</select>
|
||
|
||
<input type="number" id="sell_price_value" name="sell_price_value" min="0"
|
||
value="<?php echo $item ? esc_attr($item->sell_price_value ?? 80) : '80'; ?>"
|
||
style="width:80px" oninput="wisUpdateSellPreview()">
|
||
<span id="wis_sell_preview" style="margin-left:8px;color:#0073aa;"></span>
|
||
<p class="description">
|
||
Beispiele: 80 % → 80 % des VK-Preises |
|
||
Modus «minus 10» → VK-Preis − 10 |
|
||
Fixer Preis 15 → immer 15 <?php echo esc_html($currency); ?>
|
||
</p>
|
||
<script>
|
||
function wisToggleSell(on) {
|
||
document.getElementById('wis_sell_row').style.display = on ? '' : 'none';
|
||
document.getElementById('wis_daily_sell_row').style.display = on ? '' : 'none';
|
||
if (on) wisUpdateSellPreview();
|
||
}
|
||
function wisUpdateSellPreview() {
|
||
var vk = parseFloat(document.getElementById('price').value) || 0;
|
||
var mode = document.getElementById('sell_price_mode').value;
|
||
var val = parseFloat(document.getElementById('sell_price_value').value) || 0;
|
||
var price = 0;
|
||
if (mode === 'percent') price = Math.max(0, Math.round(vk * val) / 100);
|
||
else if (mode === 'minus') price = Math.max(0, vk - val);
|
||
else price = Math.max(0, val);
|
||
document.getElementById('wis_sell_preview').textContent =
|
||
'→ Ankaufspreis: ' + price.toFixed(2) + ' <?php echo esc_js($currency); ?>';
|
||
}
|
||
document.getElementById('price').addEventListener('input', wisUpdateSellPreview);
|
||
wisUpdateSellPreview();
|
||
</script>
|
||
</td>
|
||
</tr>
|
||
<tr id="wis_daily_sell_row" <?php echo ($item && !empty($item->sell_enabled)) ? '' : 'style="display:none"'; ?>>
|
||
<th><label for="daily_sell_limit">Tageslimit (Ankauf)</label></th>
|
||
<td>
|
||
<input type="number" id="daily_sell_limit" name="daily_sell_limit" min="0"
|
||
value="<?php echo $item ? esc_attr($item->daily_sell_limit ?? 0) : '0'; ?>"
|
||
style="width:100px">
|
||
<p class="description">Max. Menge die ein Spieler pro Tag verkaufen kann. <strong>0 = kein Limit.</strong></p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Markierungen</th>
|
||
<td>
|
||
<label>
|
||
<input type="checkbox" name="is_offer" value="1" <?php echo ($item && $item->is_offer) ? 'checked' : ''; ?>>
|
||
Als Angebot markieren
|
||
</label>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Server</th>
|
||
<td>
|
||
<?php if (empty($servers)): ?>
|
||
<p>Keine Server vorhanden. <a href="<?php echo admin_url('admin.php?page=wis_servers'); ?>">Server erstellen</a></p>
|
||
<?php else: ?>
|
||
<?php foreach ($servers as $server): ?>
|
||
<label style="display:block; margin:5px 0;">
|
||
<input type="checkbox" name="servers[]" value="<?php echo esc_attr($server->slug); ?>" <?php echo in_array($server->slug, $item_servers ?: []) ? 'checked' : ''; ?>>
|
||
<?php echo esc_html($server->name); ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Kategorien</th>
|
||
<td>
|
||
<?php if (empty($categories)): ?>
|
||
<p>Keine Kategorien vorhanden.</p>
|
||
<?php else: ?>
|
||
<?php foreach ($categories as $cat): ?>
|
||
<label style="display:block; margin:5px 0;">
|
||
<input type="checkbox" name="categories[]" value="<?php echo esc_attr($cat->slug); ?>" <?php echo in_array($cat->slug, $item_cats ?: []) ? 'checked' : ''; ?>>
|
||
<?php echo esc_html($cat->name); ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
<p class="description">Bei neuem Item werden Kategorien automatisch basierend auf der Item-ID gesetzt</p>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p class="submit">
|
||
<input type="submit" name="wis_save_item" class="button button-primary" value="Speichern">
|
||
</p>
|
||
</form>
|
||
</div>
|
||
<?php
|
||
return;
|
||
}
|
||
|
||
// Liste mit Suche + Pagination
|
||
$categories = WIS_DB::get_categories();
|
||
$current_category = isset($_GET['wis_category']) ? sanitize_text_field($_GET['wis_category']) : '';
|
||
$search_query = isset($_GET['wis_search']) ? sanitize_text_field($_GET['wis_search']) : '';
|
||
$hide_drafts = isset($_GET['hide_drafts']) && $_GET['hide_drafts'] === '1';
|
||
$per_page = 24;
|
||
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
|
||
|
||
$fetch_args = [];
|
||
if ($hide_drafts) $fetch_args['status'] = 'publish';
|
||
if (!empty($current_category)) $fetch_args['category_slug'] = $current_category;
|
||
if (!empty($search_query)) $fetch_args['search'] = $search_query;
|
||
|
||
$total_items = WIS_DB::count_items($fetch_args);
|
||
$total_pages = max(1, (int) ceil($total_items / $per_page));
|
||
$current_page = min($current_page, $total_pages);
|
||
$offset = ($current_page - 1) * $per_page;
|
||
|
||
global $wpdb;
|
||
$table_items = $wpdb->prefix . 'wis_items';
|
||
// Admin-Liste: alle Items anzeigen (publish + draft), Query ohne doppeltes prepare()
|
||
$where_parts = [];
|
||
$where_vals = [];
|
||
if ($hide_drafts) {
|
||
$where_parts[] = "status = 'publish'";
|
||
}
|
||
if (!empty($current_category)) {
|
||
$where_parts[] = "categories LIKE %s";
|
||
$where_vals[] = '%"' . $wpdb->esc_like($current_category) . '"%';
|
||
}
|
||
if (!empty($search_query)) {
|
||
$like = '%' . $wpdb->esc_like($search_query) . '%';
|
||
$where_parts[] = "(name LIKE %s OR item_id LIKE %s)";
|
||
$where_vals[] = $like;
|
||
$where_vals[] = $like;
|
||
}
|
||
$where_sql = !empty($where_parts) ? 'WHERE ' . implode(' AND ', $where_parts) : '';
|
||
$where_vals[] = $per_page;
|
||
$where_vals[] = $offset;
|
||
$items = $wpdb->get_results(
|
||
$wpdb->prepare(
|
||
"SELECT * FROM $table_items $where_sql ORDER BY name ASC LIMIT %d OFFSET %d",
|
||
$where_vals
|
||
)
|
||
);
|
||
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
$base_url = admin_url('admin.php?page=wis_items');
|
||
if (!empty($current_category)) $base_url .= '&wis_category=' . urlencode($current_category);
|
||
if (!empty($search_query)) $base_url .= '&wis_search=' . urlencode($search_query);
|
||
if ($hide_drafts) $base_url .= '&hide_drafts=1';
|
||
?>
|
||
<div class="wrap">
|
||
<h1>Items
|
||
<a href="<?php echo admin_url('admin.php?page=wis_items&add=1'); ?>" class="page-title-action">Neu erstellen</a>
|
||
</h1>
|
||
|
||
<div style="margin:10px 0 15px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
|
||
<form method="get" style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||
<input type="hidden" name="page" value="wis_items">
|
||
<?php if (!empty($current_category)): ?>
|
||
<input type="hidden" name="wis_category" value="<?php echo esc_attr($current_category); ?>">
|
||
<?php endif; ?>
|
||
<input
|
||
type="search"
|
||
name="wis_search"
|
||
value="<?php echo esc_attr($search_query); ?>"
|
||
placeholder="🔍 Item suchen (Name oder ID)…"
|
||
class="regular-text"
|
||
style="min-width:280px; padding:6px 10px;">
|
||
<?php if ($hide_drafts): ?><input type="hidden" name="hide_drafts" value="1"><?php endif; ?>
|
||
<input type="submit" class="button" value="Suchen">
|
||
<?php if (!empty($search_query)): ?>
|
||
<a href="<?php echo esc_url(admin_url('admin.php?page=wis_items' . (!empty($current_category) ? '&wis_category=' . urlencode($current_category) : '') . ($hide_drafts ? '&hide_drafts=1' : ''))); ?>" class="button">✕ Zurücksetzen</a>
|
||
<?php endif; ?>
|
||
</form>
|
||
<a href="<?php echo esc_url(add_query_arg([
|
||
'page' => 'wis_items',
|
||
'hide_drafts' => $hide_drafts ? '0' : '1',
|
||
'wis_category'=> $current_category ?: null,
|
||
'wis_search' => $search_query ?: null,
|
||
], admin_url('admin.php'))); ?>"
|
||
class="button"
|
||
style="display:flex;align-items:center;gap:6px;<?php echo $hide_drafts ? 'background:#2271b1;color:#fff;border-color:#2271b1;' : ''; ?>">
|
||
<?php echo $hide_drafts ? '👁 Entwürfe einblenden' : '🙈 Entwürfe ausblenden'; ?>
|
||
</a>
|
||
<span style="color:#666; font-size:13px;">
|
||
<?php echo $total_items; ?> Item(s) gefunden
|
||
<?php if (!empty($search_query)): ?>
|
||
– Suche: <strong><?php echo esc_html($search_query); ?></strong>
|
||
<?php endif; ?>
|
||
</span>
|
||
</div>
|
||
|
||
<nav class="nav-tab-wrapper">
|
||
<?php
|
||
$hd_param = $hide_drafts ? '&hide_drafts=1' : '';
|
||
$all_count = WIS_DB::count_items(array_merge(
|
||
$hide_drafts ? ['status' => 'publish'] : [],
|
||
!empty($search_query) ? ['search' => $search_query] : []
|
||
));
|
||
$root_cats_tab = array_values(array_filter($categories, fn($c) => $c->parent_id == 0));
|
||
$sub_idx_tab = [];
|
||
foreach ($categories as $c) {
|
||
if ($c->parent_id != 0) $sub_idx_tab[$c->parent_id][] = $c;
|
||
}
|
||
// Aktive Hauptkategorie bestimmen (direkt oder via Unterkategorie)
|
||
$active_root_slug = '';
|
||
foreach ($root_cats_tab as $rc) {
|
||
if ($current_category === $rc->slug) { $active_root_slug = $rc->slug; break; }
|
||
if (!empty($sub_idx_tab[$rc->id])) {
|
||
foreach ($sub_idx_tab[$rc->id] as $sc) {
|
||
if ($current_category === $sc->slug) { $active_root_slug = $rc->slug; break 2; }
|
||
}
|
||
}
|
||
}
|
||
?>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_items' . (!empty($search_query) ? '&wis_search=' . urlencode($search_query) : '') . $hd_param); ?>"
|
||
class="nav-tab <?php echo $current_category === '' ? 'nav-tab-active' : ''; ?>">
|
||
Alle <span style="background:<?php echo $current_category===''?'#fff':'#e2e8f0';?>;color:#555;border-radius:10px;padding:1px 7px;font-size:11px;margin-left:3px;"><?php echo $all_count; ?></span>
|
||
</a>
|
||
<?php foreach ($root_cats_tab as $cat):
|
||
$is_active_root = ($active_root_slug === $cat->slug);
|
||
$cat_count = WIS_DB::count_items(array_merge(
|
||
$hide_drafts ? ['status' => 'publish'] : [],
|
||
['category_slug' => $cat->slug],
|
||
!empty($search_query) ? ['search' => $search_query] : []
|
||
));
|
||
?>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_items&wis_category=' . $cat->slug . (!empty($search_query) ? '&wis_search=' . urlencode($search_query) : '') . $hd_param); ?>"
|
||
class="nav-tab <?php echo $current_category === $cat->slug ? 'nav-tab-active' : ($is_active_root ? 'nav-tab-active' : ''); ?>">
|
||
<?php echo esc_html($cat->name); ?>
|
||
<span style="background:<?php echo $is_active_root?'#fff':'#e2e8f0';?>;color:#555;border-radius:10px;padding:1px 7px;font-size:11px;margin-left:3px;"><?php echo $cat_count; ?></span>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</nav>
|
||
<?php
|
||
// Unterkategorie-Leiste – nur wenn eine Hauptkat mit Unterkats aktiv ist
|
||
$active_root_obj = null;
|
||
foreach ($root_cats_tab as $rc) {
|
||
if ($active_root_slug === $rc->slug) { $active_root_obj = $rc; break; }
|
||
}
|
||
if ($active_root_obj && !empty($sub_idx_tab[$active_root_obj->id])): ?>
|
||
<div style="background:#f0f4ff;border:1px solid #c5d0f0;border-top:none;border-radius:0 0 4px 4px;padding:6px 10px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;">
|
||
<span style="font-size:12px;color:#666;margin-right:2px;">↳</span>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_items&wis_category=' . $active_root_obj->slug . $hd_param); ?>"
|
||
style="font-size:12px;padding:3px 10px;border-radius:12px;text-decoration:none;
|
||
background:<?php echo $current_category===$active_root_obj->slug?'#667eea':'#fff';?>;
|
||
color:<?php echo $current_category===$active_root_obj->slug?'#fff':'#444';?>;
|
||
border:1px solid <?php echo $current_category===$active_root_obj->slug?'#667eea':'#ccc';?>;">
|
||
Alle
|
||
</a>
|
||
<?php foreach ($sub_idx_tab[$active_root_obj->id] as $sc):
|
||
$sc_count = WIS_DB::count_items(array_merge(
|
||
$hide_drafts ? ['status' => 'publish'] : [],
|
||
['category_slug' => $sc->slug],
|
||
!empty($search_query) ? ['search' => $search_query] : []
|
||
));
|
||
?>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_items&wis_category=' . $sc->slug . $hd_param); ?>"
|
||
style="font-size:12px;padding:3px 10px;border-radius:12px;text-decoration:none;
|
||
background:<?php echo $current_category===$sc->slug?'#667eea':'#fff';?>;
|
||
color:<?php echo $current_category===$sc->slug?'#fff':'#444';?>;
|
||
border:1px solid <?php echo $current_category===$sc->slug?'#667eea':'#ccc';?>;">
|
||
<?php echo esc_html($sc->name); ?>
|
||
<span style="font-size:11px;opacity:.8;">(<?php echo $sc_count; ?>)</span>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<form method="post">
|
||
<div class="tablenav top">
|
||
<div class="alignleft actions bulkactions">
|
||
<label for="bulk-action-selector-top" class="screen-reader-text">Massenaktionen wählen</label>
|
||
<select name="wis_bulk_action" id="bulk-action-selector-top">
|
||
<option value="">Massenaktionen</option>
|
||
<option value="price">Preis ändern</option>
|
||
<option value="offer">Angebot ändern</option>
|
||
<option value="server">Server zuweisen</option>
|
||
<option value="category">Kategorie zuweisen</option>
|
||
<option value="status">Aktivieren/Deaktivieren</option>
|
||
<option value="sell">Ankauf konfigurieren</option>
|
||
</select>
|
||
<input type="submit" name="wis_bulk_apply" class="button action" value="Anwenden">
|
||
</div>
|
||
|
||
<div class="tablenav-pages" style="float:right; margin-top:4px;">
|
||
<?php if ($total_pages > 1): ?>
|
||
<span class="displaying-num"><?php echo $total_items; ?> Einträge</span>
|
||
<span class="pagination-links">
|
||
<?php if ($current_page > 1): ?>
|
||
<a class="button" href="<?php echo esc_url($base_url . '&paged=1'); ?>">«</a>
|
||
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . ($current_page - 1)); ?>">‹</a>
|
||
<?php endif; ?>
|
||
<span class="paging-input" style="margin:0 6px;">
|
||
Seite <strong><?php echo $current_page; ?></strong> von <strong><?php echo $total_pages; ?></strong>
|
||
</span>
|
||
<?php if ($current_page < $total_pages): ?>
|
||
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . ($current_page + 1)); ?>">›</a>
|
||
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . $total_pages); ?>">»</a>
|
||
<?php endif; ?>
|
||
</span>
|
||
<?php else: ?>
|
||
<span class="displaying-num"><?php echo $total_items; ?> Einträge</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<br class="clear">
|
||
</div>
|
||
|
||
<table class="widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:40px;" class="manage-column column-cb check-column">
|
||
<input type="checkbox" id="cb-select-all-1">
|
||
</th>
|
||
<th>ID</th>
|
||
<th>Name</th>
|
||
<th>Item ID</th>
|
||
<th>Preis</th>
|
||
<th>Status</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (empty($items)): ?>
|
||
<tr><td colspan="7" style="text-align:center; padding:40px;">
|
||
<?php echo !empty($search_query) ? 'Keine Items für diese Suche gefunden.' : 'Keine Items in dieser Kategorie gefunden.'; ?>
|
||
</td></tr>
|
||
<?php else: ?>
|
||
<?php foreach ($items as $item): ?>
|
||
<tr>
|
||
<th class="check-column">
|
||
<input type="checkbox" name="item_ids[]" id="cb-select-<?php echo $item->id; ?>" value="<?php echo $item->id; ?>">
|
||
</th>
|
||
<td><?php echo esc_html($item->id); ?></td>
|
||
<td><strong><?php echo esc_html($item->name); ?></strong></td>
|
||
<td>
|
||
<?php
|
||
if (preg_match('/^rank_([^_]+)_([a-zA-Z0-9_\-]+)_([a-zA-Z0-9_\-]+)_(\d+)$/', $item->item_id, $rm2)) {
|
||
$rd = intval($rm2[4]);
|
||
echo '<span title="' . esc_attr($item->item_id) . '">👑 LP: <code>' . esc_html($rm2[2]) . '</code> — ' . ($rd === 0 ? '<em>dauerhaft</em>' : esc_html($rd) . ' Tage') . '</span>';
|
||
} elseif (preg_match('/^rank_(.+)_(\d+)$/', $item->item_id, $rm2)) {
|
||
$rd = intval($rm2[2]);
|
||
echo '<span title="' . esc_attr($item->item_id) . '">👑 <code>' . esc_html($rm2[1]) . '</code> — ' . ($rd === 0 ? '<em>dauerhaft</em>' : esc_html($rd) . ' Tage') . '</span>';
|
||
} elseif (preg_match('/^custom_cmd_(.+)$/', $item->item_id, $cc2)) {
|
||
echo '<span title="' . esc_attr($item->item_id) . '">⚙️ Custom: <code>' . esc_html($cc2[1]) . '</code></span>';
|
||
} elseif ($item->item_id === 'fly_abo' || preg_match('/^fly_abo_/', $item->item_id)) {
|
||
echo '<span title="' . esc_attr($item->item_id) . '">✈ Fly-Abo — monatlich</span>';
|
||
} elseif (preg_match('/^plot_slots_(\d+)$/', $item->item_id, $plt_m)) {
|
||
echo '<span title="' . esc_attr($item->item_id) . '">📦 Plot-Slots — +' . esc_html($plt_m[1]) . ' permanent</span>';
|
||
} elseif (preg_match('/^plot_abo_(\d+)$/', $item->item_id, $plt_m)) {
|
||
echo '<span title="' . esc_attr($item->item_id) . '">📦 Plot-Abo — +' . esc_html($plt_m[1]) . ' monatlich</span>';
|
||
} else {
|
||
echo '<code>' . esc_html($item->item_id) . '</code>';
|
||
}
|
||
?>
|
||
</td>
|
||
<td><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></td>
|
||
<td>
|
||
<?php if ($item->status === 'publish'): ?>
|
||
<span style="color:green;">✅ Aktiv</span>
|
||
<?php else: ?>
|
||
<span style="color:orange;">📝 Entwurf</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td>
|
||
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_items&edit=' . $item->id), 'wis_item_action', '_wpnonce'); ?>" class="button button-small">Bearbeiten</a>
|
||
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_items&action=delete&id=' . $item->id), 'wis_item_action', '_wpnonce'); ?>" class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
|
||
<?php if ($total_pages > 1): ?>
|
||
<div class="tablenav bottom">
|
||
<div class="tablenav-pages" style="float:right; margin-top:8px;">
|
||
<span class="displaying-num"><?php echo $total_items; ?> Einträge</span>
|
||
<span class="pagination-links">
|
||
<?php if ($current_page > 1): ?>
|
||
<a class="button" href="<?php echo esc_url($base_url . '&paged=1'); ?>">«</a>
|
||
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . ($current_page - 1)); ?>">‹</a>
|
||
<?php endif; ?>
|
||
<span class="paging-input" style="margin:0 6px;">
|
||
Seite <strong><?php echo $current_page; ?></strong> von <strong><?php echo $total_pages; ?></strong>
|
||
</span>
|
||
<?php if ($current_page < $total_pages): ?>
|
||
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . ($current_page + 1)); ?>">›</a>
|
||
<a class="button" href="<?php echo esc_url($base_url . '&paged=' . $total_pages); ?>">»</a>
|
||
<?php endif; ?>
|
||
</span>
|
||
</div>
|
||
<br class="clear">
|
||
</div>
|
||
<?php endif; ?>
|
||
</form>
|
||
</div>
|
||
|
||
<script>
|
||
(function() {
|
||
const mainCb = document.getElementById('cb-select-all-1');
|
||
if(mainCb) {
|
||
mainCb.addEventListener('change', function(e) {
|
||
const cbs = document.querySelectorAll('input[name="item_ids[]"]');
|
||
cbs.forEach(cb => cb.checked = e.target.checked);
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
<?php
|
||
}
|
||
|
||
public static function page_servers() {
|
||
if (isset($_POST['wis_add_server'])) {
|
||
check_admin_referer('wis_server_form');
|
||
WIS_DB::insert_server($_POST['slug'], $_POST['name']);
|
||
echo '<div class="updated"><p>✅ Server erstellt!</p></div>';
|
||
}
|
||
|
||
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
|
||
check_admin_referer('wis_server_action', '_wpnonce');
|
||
WIS_DB::delete_server(intval($_GET['id']));
|
||
echo '<div class="updated"><p>✅ Server gelöscht!</p></div>';
|
||
}
|
||
|
||
$servers = WIS_DB::get_servers();
|
||
?>
|
||
<div class="wrap">
|
||
<h1>Server</h1>
|
||
|
||
<div class="card" style="max-width:600px; padding:20px; margin-top:20px;">
|
||
<h2>Neuen Server erstellen</h2>
|
||
<form method="post">
|
||
<?php wp_nonce_field('wis_server_form'); ?>
|
||
<table class="form-table">
|
||
<tr>
|
||
<th><label for="name">Name *</label></th>
|
||
<td><input type="text" id="name" name="name" class="regular-text" required></td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="slug">Slug *</label></th>
|
||
<td>
|
||
<input type="text" id="slug" name="slug" class="regular-text" placeholder="survival" required>
|
||
<p class="description">Kleinbuchstaben ohne Leerzeichen</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="submit">
|
||
<input type="submit" name="wis_add_server" class="button button-primary" value="Server erstellen">
|
||
</p>
|
||
</form>
|
||
</div>
|
||
|
||
<h2 style="margin-top:40px;">Vorhandene Server</h2>
|
||
<table class="widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Name</th>
|
||
<th>Slug</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (empty($servers)): ?>
|
||
<tr><td colspan="4" style="text-align:center; padding:40px;">Noch keine Server vorhanden.</td></tr>
|
||
<?php else: ?>
|
||
<?php foreach ($servers as $server): ?>
|
||
<tr>
|
||
<td><?php echo esc_html($server->id); ?></td>
|
||
<td><strong><?php echo esc_html($server->name); ?></strong></td>
|
||
<td><code><?php echo esc_html($server->slug); ?></code></td>
|
||
<td>
|
||
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_servers&action=delete&id=' . $server->id), 'wis_server_action', '_wpnonce'); ?>" class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
public static function page_categories() {
|
||
global $wpdb;
|
||
|
||
// ── Kategorie bearbeiten speichern ──
|
||
if (isset($_POST['wis_edit_category'])) {
|
||
check_admin_referer('wis_edit_category_form');
|
||
$edit_id = intval($_POST['edit_cat_id']);
|
||
$new_name = sanitize_text_field($_POST['name']);
|
||
$new_parent = intval($_POST['parent_id'] ?? 0);
|
||
if ($edit_id && $new_name) {
|
||
$new_slug = sanitize_title($new_name);
|
||
// Slug-Kollision vermeiden
|
||
$existing_slug = $wpdb->get_var($wpdb->prepare(
|
||
"SELECT id FROM {$wpdb->prefix}wis_categories WHERE slug = %s AND id != %d LIMIT 1",
|
||
$new_slug, $edit_id
|
||
));
|
||
if ($existing_slug) $new_slug .= '-' . $edit_id;
|
||
$wpdb->update($wpdb->prefix . 'wis_categories', [
|
||
'name' => $new_name,
|
||
'slug' => $new_slug,
|
||
'parent_id' => $new_parent,
|
||
], ['id' => $edit_id]);
|
||
echo '<div class="updated"><p>✅ Kategorie aktualisiert!</p></div>';
|
||
}
|
||
}
|
||
|
||
if (isset($_POST['wis_add_category'])) {
|
||
check_admin_referer('wis_category_form');
|
||
$parent_id = intval($_POST['parent_id'] ?? 0);
|
||
WIS_DB::insert_category($_POST['name'], $parent_id);
|
||
echo '<div class="updated"><p>✅ Kategorie erstellt!</p></div>';
|
||
}
|
||
|
||
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
|
||
check_admin_referer('wis_category_action', '_wpnonce');
|
||
$del_id = intval($_GET['id']);
|
||
$wpdb->update($wpdb->prefix . 'wis_categories', ['parent_id' => 0], ['parent_id' => $del_id]);
|
||
WIS_DB::delete_category($del_id);
|
||
echo '<div class="updated"><p>✅ Kategorie gelöscht!</p></div>';
|
||
}
|
||
|
||
$categories = WIS_DB::get_categories();
|
||
$root_cats = array_values(array_filter($categories, fn($c) => $c->parent_id == 0));
|
||
$sub_index = [];
|
||
foreach ($categories as $c) {
|
||
if ($c->parent_id != 0) $sub_index[$c->parent_id][] = $c;
|
||
}
|
||
|
||
// Edit-Modus: Inline-Formular für eine Kategorie
|
||
$edit_id = isset($_GET['edit_cat']) ? intval($_GET['edit_cat']) : 0;
|
||
$edit_cat = $edit_id ? $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}wis_categories WHERE id = %d", $edit_id
|
||
)) : null;
|
||
?>
|
||
<div class="wrap">
|
||
<h1>Kategorien & Unterkategorien</h1>
|
||
|
||
<?php if ($edit_cat): ?>
|
||
<!-- ── Edit-Formular ── -->
|
||
<div class="card" style="max-width:600px;padding:20px;margin-top:20px;border-left:4px solid #667eea;">
|
||
<h2>✏️ Kategorie bearbeiten</h2>
|
||
<form method="post">
|
||
<?php wp_nonce_field('wis_edit_category_form'); ?>
|
||
<input type="hidden" name="edit_cat_id" value="<?php echo esc_attr($edit_cat->id); ?>">
|
||
<table class="form-table">
|
||
<tr>
|
||
<th><label for="edit_name">Name *</label></th>
|
||
<td><input type="text" id="edit_name" name="name" value="<?php echo esc_attr($edit_cat->name); ?>" class="regular-text" required></td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="edit_parent">Übergeordnete Kategorie</label></th>
|
||
<td>
|
||
<select id="edit_parent" name="parent_id" class="regular-text">
|
||
<option value="0" <?php selected($edit_cat->parent_id, 0); ?>>— Hauptkategorie —</option>
|
||
<?php foreach ($root_cats as $rc):
|
||
if ($rc->id === $edit_cat->id) continue; // sich selbst nicht als Parent ?>
|
||
<option value="<?php echo esc_attr($rc->id); ?>" <?php selected($edit_cat->parent_id, $rc->id); ?>>
|
||
<?php echo esc_html($rc->name); ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Aktueller Slug</th>
|
||
<td><code><?php echo esc_html($edit_cat->slug); ?></code> <span class="description">(wird beim Speichern automatisch aktualisiert)</span></td>
|
||
</tr>
|
||
</table>
|
||
<p class="submit">
|
||
<input type="submit" name="wis_edit_category" class="button button-primary" value="Speichern">
|
||
<a href="<?php echo admin_url('admin.php?page=wis_categories'); ?>" class="button">Abbrechen</a>
|
||
</p>
|
||
</form>
|
||
</div>
|
||
<?php else: ?>
|
||
|
||
<!-- ── Neue Kategorie ── -->
|
||
<div class="card" style="max-width:600px;padding:20px;margin-top:20px;">
|
||
<h2>Neue Kategorie erstellen</h2>
|
||
<form method="post">
|
||
<?php wp_nonce_field('wis_category_form'); ?>
|
||
<table class="form-table">
|
||
<tr>
|
||
<th><label for="cat_name">Name *</label></th>
|
||
<td><input type="text" id="cat_name" name="name" class="regular-text" required></td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="parent_id">Übergeordnete Kategorie</label></th>
|
||
<td>
|
||
<select id="parent_id" name="parent_id" class="regular-text">
|
||
<option value="0">— Hauptkategorie —</option>
|
||
<?php foreach ($root_cats as $rc): ?>
|
||
<option value="<?php echo esc_attr($rc->id); ?>"><?php echo esc_html($rc->name); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<p class="description">Leer lassen = Hauptkategorie. Max. 2 Ebenen.</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p class="submit">
|
||
<input type="submit" name="wis_add_category" class="button button-primary" value="Kategorie erstellen">
|
||
</p>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Drag & Drop Sortierung -->
|
||
<h2 style="margin-top:40px;">Reihenfolge & Struktur</h2>
|
||
<p class="description" style="margin-bottom:15px;">
|
||
Ziehe Kategorien per ☰ in die gewünschte Reihenfolge. Klicke ✏️ um eine Kategorie umzubenennen oder die Ebene zu ändern.
|
||
</p>
|
||
|
||
<div id="wis-cat-sortable-wrap" style="max-width:700px;">
|
||
|
||
<?php foreach ($root_cats as $rc): ?>
|
||
<div class="wis-cat-group" data-id="<?php echo $rc->id; ?>" style="margin-bottom:12px;">
|
||
|
||
<!-- Hauptkategorie-Zeile -->
|
||
<div class="wis-cat-row wis-cat-root"
|
||
data-id="<?php echo $rc->id; ?>" data-parent="0"
|
||
style="display:flex;align-items:center;gap:10px;background:#f0f4ff;border:1px solid #c5d0f0;border-radius:6px;padding:10px 14px;">
|
||
<span style="font-size:18px;color:#999;cursor:grab;" class="wis-handle">☰</span>
|
||
<strong style="flex:1;">📁 <?php echo esc_html($rc->name); ?></strong>
|
||
<code style="color:#888;font-size:11px;"><?php echo esc_html($rc->slug); ?></code>
|
||
<?php if (!empty($sub_index[$rc->id])): ?>
|
||
<span style="font-size:11px;color:#667eea;">(<?php echo count($sub_index[$rc->id]); ?> Unterkats)</span>
|
||
<?php endif; ?>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_categories&edit_cat=' . $rc->id); ?>"
|
||
style="color:#0073aa;font-size:12px;text-decoration:none;" title="Bearbeiten">✏️</a>
|
||
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_categories&action=delete&id=' . $rc->id), 'wis_category_action', '_wpnonce'); ?>"
|
||
onclick="return confirm('Wirklich löschen? Unterkategorien werden zu Hauptkategorien.');"
|
||
style="color:#dc3545;font-size:12px;text-decoration:none;" title="Löschen">🗑</a>
|
||
</div>
|
||
|
||
<!-- Unterkategorien -->
|
||
<?php if (!empty($sub_index[$rc->id])): ?>
|
||
<ul class="wis-subcat-sortable" data-parent="<?php echo $rc->id; ?>"
|
||
style="list-style:none;margin:4px 0 0 30px;padding:0;">
|
||
<?php foreach ($sub_index[$rc->id] as $sc): ?>
|
||
<li class="wis-cat-row wis-cat-sub"
|
||
data-id="<?php echo $sc->id; ?>" data-parent="<?php echo $rc->id; ?>"
|
||
style="display:flex;align-items:center;gap:10px;background:#fff;border:1px solid #e0e0e0;border-radius:5px;padding:8px 12px;margin-bottom:4px;">
|
||
<span style="font-size:16px;color:#bbb;cursor:grab;" class="wis-handle">☰</span>
|
||
<span style="flex:1;">↳ <?php echo esc_html($sc->name); ?></span>
|
||
<code style="color:#888;font-size:11px;"><?php echo esc_html($sc->slug); ?></code>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_categories&edit_cat=' . $sc->id); ?>"
|
||
style="color:#0073aa;font-size:12px;text-decoration:none;" title="Bearbeiten">✏️</a>
|
||
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_categories&action=delete&id=' . $sc->id), 'wis_category_action', '_wpnonce'); ?>"
|
||
onclick="return confirm('Wirklich löschen?');"
|
||
style="color:#dc3545;font-size:12px;text-decoration:none;" title="Löschen">🗑</a>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<?php if (empty($root_cats)): ?>
|
||
<p style="color:#999;padding:20px 0;">Noch keine Kategorien vorhanden.</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<button id="wis-save-order-btn" class="button button-primary" style="margin-top:20px;display:<?php echo empty($root_cats)?'none':'inline-flex'; ?>;align-items:center;gap:8px;">
|
||
💾 Reihenfolge speichern
|
||
</button>
|
||
<span id="wis-save-order-msg" style="margin-left:12px;font-size:13px;color:green;display:none;"></span>
|
||
</div>
|
||
|
||
<!-- Sortable JS -->
|
||
<script>
|
||
jQuery(function($) {
|
||
var $wrap = $('#wis-cat-sortable-wrap');
|
||
|
||
$wrap.sortable({
|
||
items: '> .wis-cat-group',
|
||
handle: '.wis-cat-root .wis-handle',
|
||
axis: 'y',
|
||
placeholder: 'wis-sortable-placeholder',
|
||
tolerance: 'pointer',
|
||
});
|
||
|
||
$wrap.find('.wis-subcat-sortable').each(function() {
|
||
$(this).sortable({
|
||
items: '> li',
|
||
handle: '.wis-handle',
|
||
axis: 'y',
|
||
placeholder: 'wis-sortable-placeholder',
|
||
tolerance: 'pointer',
|
||
});
|
||
});
|
||
|
||
$('#wis-save-order-btn').on('click', function() {
|
||
var $btn = $(this);
|
||
var $msg = $('#wis-save-order-msg');
|
||
$btn.prop('disabled', true).text('⏳ Speichere…');
|
||
$msg.hide();
|
||
|
||
var order = [];
|
||
$wrap.find('> .wis-cat-group').each(function() {
|
||
var rootId = parseInt($(this).data('id'));
|
||
order.push({ id: rootId, parent_id: 0 });
|
||
$(this).find('.wis-subcat-sortable > li').each(function() {
|
||
order.push({ id: parseInt($(this).data('id')), parent_id: rootId });
|
||
});
|
||
});
|
||
|
||
$.ajax({
|
||
url: ajaxurl,
|
||
method: 'POST',
|
||
data: {
|
||
action: 'wis_save_cat_order',
|
||
nonce: '<?php echo wp_create_nonce('wis_cat_order_nonce'); ?>',
|
||
order: order,
|
||
},
|
||
success: function(res) {
|
||
if (res.success) {
|
||
$msg.text('✅ Gespeichert!').css('color','green').show();
|
||
setTimeout(function(){ $msg.fadeOut(); }, 3000);
|
||
} else {
|
||
$msg.text('❌ Fehler: ' + (res.data || '')).css('color','red').show();
|
||
}
|
||
},
|
||
error: function() {
|
||
$msg.text('❌ Verbindungsfehler').css('color','red').show();
|
||
},
|
||
complete: function() {
|
||
$btn.prop('disabled', false).text('💾 Reihenfolge speichern');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
<style>
|
||
.wis-sortable-placeholder { height:44px;background:#e8f0fe;border:2px dashed #667eea;border-radius:6px;margin-bottom:4px;list-style:none; }
|
||
.wis-cat-root:hover, .wis-cat-sub:hover { border-color:#667eea !important; }
|
||
.wis-cat-row { user-select:none; }
|
||
</style>
|
||
<?php
|
||
}
|
||
|
||
public static function page_coupons() {
|
||
|
||
// ── CSV-Export ────────────────────────────────────────────────────────
|
||
if (isset($_GET['action']) && $_GET['action'] === 'export_bulk' && isset($_GET['bulk_id'])) {
|
||
check_admin_referer('wis_coupon_action', '_wpnonce');
|
||
global $wpdb;
|
||
$bulk_id = sanitize_text_field($_GET['bulk_id']);
|
||
$table = $wpdb->prefix . 'wis_coupons';
|
||
$codes = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT code, value, type, usage_limit, expiry, min_order_value FROM $table WHERE bulk_id = %s ORDER BY id ASC",
|
||
$bulk_id
|
||
));
|
||
if ($codes) {
|
||
header('Content-Type: text/csv; charset=utf-8');
|
||
header('Content-Disposition: attachment; filename="gutscheine-' . $bulk_id . '.csv"');
|
||
$out = fopen('php://output', 'w');
|
||
fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); // UTF-8 BOM
|
||
fputcsv($out, ['Code', 'Rabatt', 'Typ', 'Max. Einlösungen', 'Gültig bis', 'Mindestbestellwert'], ';');
|
||
foreach ($codes as $c) {
|
||
fputcsv($out, [
|
||
$c->code,
|
||
$c->value,
|
||
$c->type === 'percent' ? 'Prozent' : 'Fest',
|
||
$c->usage_limit,
|
||
$c->expiry ?: '∞',
|
||
$c->min_order_value ?: '–'
|
||
], ';');
|
||
}
|
||
fclose($out);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
// ── Bulk-Generierung speichern ────────────────────────────────────────
|
||
if (isset($_POST['wis_bulk_generate'])) {
|
||
check_admin_referer('wis_bulk_generate');
|
||
global $wpdb;
|
||
|
||
$count = min(500, max(1, intval($_POST['bulk_count'] ?? 10)));
|
||
$mode = $_POST['bulk_mode'] === 'prefix' ? 'prefix' : 'random';
|
||
$prefix = strtoupper(preg_replace('/[^A-Z0-9_-]/i', '', $_POST['bulk_prefix'] ?? ''));
|
||
$value = intval($_POST['bulk_value'] ?? 10);
|
||
$type = in_array($_POST['bulk_type'], ['fixed', 'percent']) ? $_POST['bulk_type'] : 'fixed';
|
||
$usage_limit = max(1, intval($_POST['bulk_usage_limit'] ?? 1));
|
||
$expiry = !empty($_POST['bulk_expiry']) ? sanitize_text_field($_POST['bulk_expiry']) : null;
|
||
$min_order = intval($_POST['bulk_min_order'] ?? 0);
|
||
$allowed_cats = !empty($_POST['bulk_allowed_categories'])
|
||
? implode(',', array_map('intval', (array)$_POST['bulk_allowed_categories']))
|
||
: null;
|
||
|
||
$bulk_id = strtoupper(bin2hex(random_bytes(4))); // gemeinsame ID für diese Batch
|
||
$table = $wpdb->prefix . 'wis_coupons';
|
||
$generated = 0;
|
||
$skipped = 0;
|
||
$attempts = 0;
|
||
|
||
while ($generated < $count && $attempts < $count * 5) {
|
||
$attempts++;
|
||
if ($mode === 'prefix' && $prefix) {
|
||
$rand = strtoupper(bin2hex(random_bytes(4)));
|
||
$code = $prefix . '-' . substr($rand, 0, 3) . '-' . substr($rand, 3, 5);
|
||
} else {
|
||
$r = strtoupper(bin2hex(random_bytes(6)));
|
||
$code = substr($r, 0, 4) . '-' . substr($r, 4, 4) . '-' . substr($r, 8, 4);
|
||
}
|
||
// Duplikat-Check
|
||
$exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM $table WHERE code = %s", $code));
|
||
if ($exists) { $skipped++; continue; }
|
||
|
||
$wpdb->insert($table, [
|
||
'code' => $code,
|
||
'value' => $value,
|
||
'type' => $type,
|
||
'usage_limit' => $usage_limit,
|
||
'used_count' => 0,
|
||
'expiry' => $expiry,
|
||
'min_order_value' => $min_order,
|
||
'allowed_categories'=> $allowed_cats,
|
||
'bulk_id' => $bulk_id,
|
||
]);
|
||
if ($wpdb->insert_id) $generated++;
|
||
}
|
||
|
||
$export_url = wp_nonce_url(
|
||
admin_url('admin.php?page=wis_coupons&action=export_bulk&bulk_id=' . $bulk_id),
|
||
'wis_coupon_action', '_wpnonce'
|
||
);
|
||
echo '<div class="notice notice-success"><p>';
|
||
echo "<strong>{$generated} Gutscheine</strong> wurden generiert";
|
||
if ($skipped) echo " ({$skipped} Duplikate übersprungen)";
|
||
echo '. <a href= . $export_url . >📥 Als CSV exportieren</a></p></div>';
|
||
}
|
||
|
||
if (isset($_POST['wis_save_coupon'])) {
|
||
check_admin_referer('wis_coupon_form');
|
||
|
||
$data = [
|
||
'code' => sanitize_text_field($_POST['code']),
|
||
'value' => intval($_POST['value']),
|
||
'type' => sanitize_text_field($_POST['type']),
|
||
'usage_limit' => intval($_POST['usage_limit']),
|
||
'expiry' => !empty($_POST['expiry']) ? sanitize_text_field($_POST['expiry']) : null,
|
||
'min_order_value' => intval($_POST['min_order_value'] ?? 0),
|
||
'allowed_categories' => !empty($_POST['allowed_categories']) ? implode(',', array_map('intval', (array)$_POST['allowed_categories'])) : null
|
||
];
|
||
|
||
if (isset($_GET['edit'])) {
|
||
unset($data['used_count']);
|
||
WIS_DB::update_coupon(intval($_GET['edit']), $data);
|
||
echo '<div class="updated"><p>✅ Gutschein gespeichert!</p></div>';
|
||
} else {
|
||
$data['used_count'] = 0;
|
||
WIS_DB::insert_coupon($data);
|
||
echo '<div class="updated"><p>✅ Gutschein erstellt!</p></div>';
|
||
}
|
||
}
|
||
|
||
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
|
||
check_admin_referer('wis_coupon_action', '_wpnonce');
|
||
WIS_DB::delete_coupon(intval($_GET['id']));
|
||
echo '<div class="updated"><p>✅ Gutschein gelöscht!</p></div>';
|
||
}
|
||
|
||
if (isset($_GET['add']) || isset($_GET['edit'])) {
|
||
global $wpdb;
|
||
$coupon = isset($_GET['edit']) ? $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}wis_coupons WHERE id = %d", intval($_GET['edit']))) : null;
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
?>
|
||
<div class="wrap">
|
||
<h1><?php echo $coupon ? 'Gutschein bearbeiten' : 'Neuer Gutschein'; ?></h1>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_coupons'); ?>" class="button">← Zurück zur Liste</a>
|
||
|
||
<form method="post" style="max-width:600px; margin-top:20px;">
|
||
<?php wp_nonce_field('wis_coupon_form'); ?>
|
||
<table class="form-table">
|
||
<tr>
|
||
<th><label for="code">Code *</label></th>
|
||
<td>
|
||
<input type="text" id="code" name="code" value="<?php echo $coupon ? esc_attr($coupon->code) : ''; ?>" class="regular-text" style="text-transform:uppercase;" required>
|
||
<p class="description">Z.B.: SUMMER20</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="type">Typ *</label></th>
|
||
<td>
|
||
<select id="type" name="type" required>
|
||
<option value="fixed" <?php echo ($coupon && $coupon->type === 'fixed') ? 'selected' : ''; ?>>Festbetrag (z.B. 100 <?php echo esc_html($currency); ?> Rabatt)</option>
|
||
<option value="percent" <?php echo ($coupon && $coupon->type === 'percent') ? 'selected' : ''; ?>>Prozentual (z.B. 20% Rabatt)</option>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="value">Wert *</label></th>
|
||
<td>
|
||
<input type="number" id="value" name="value" value="<?php echo $coupon ? esc_attr($coupon->value) : ''; ?>" min="1" required>
|
||
<p class="description">Bei Festbetrag: Betrag in <?php echo esc_html($currency); ?>. Bei Prozent: Zahl ohne %</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="usage_limit">Nutzungslimit *</label></th>
|
||
<td>
|
||
<input type="number" id="usage_limit" name="usage_limit" value="<?php echo $coupon ? esc_attr($coupon->usage_limit) : '1'; ?>" min="1" required>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="expiry">Ablaufdatum</label></th>
|
||
<td>
|
||
<input type="date" id="expiry" name="expiry" value="<?php echo $coupon && $coupon->expiry ? esc_attr($coupon->expiry) : ''; ?>">
|
||
<p class="description">Optional</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="min_order_value">Mindestbestellwert</label></th>
|
||
<td>
|
||
<input type="number" id="min_order_value" name="min_order_value" value="<?php echo $coupon ? esc_attr($coupon->min_order_value) : '0'; ?>" min="0">
|
||
<p class="description">Gutschein gilt nur ab diesem Bestellwert (0 = kein Minimum). Wert in <?php echo esc_html($currency); ?>.</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th><label>Erlaubte Kategorien</label></th>
|
||
<td>
|
||
<?php
|
||
$all_cats = WIS_DB::get_categories();
|
||
$selected_cats = $coupon && $coupon->allowed_categories ? array_map('intval', explode(',', $coupon->allowed_categories)) : [];
|
||
?>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px;max-width:520px;">
|
||
<?php foreach ($all_cats as $cat):
|
||
$checked = in_array($cat->id, $selected_cats);
|
||
?>
|
||
<label style="display:flex;align-items:center;gap:7px;background:<?php echo $checked ? '#667eea' : '#f3f4f6'; ?>;color:<?php echo $checked ? '#fff' : '#333'; ?>;border:1px solid <?php echo $checked ? '#667eea' : '#d1d5db'; ?>;border-radius:20px;padding:5px 14px;cursor:pointer;font-size:0.875rem;font-weight:500;transition:all .15s;" class="wis-tag-label">
|
||
<input type="checkbox" name="allowed_categories[]" value="<?php echo esc_attr($cat->id); ?>" <?php echo $checked ? 'checked' : ''; ?> style="display:none;" class="wis-tag-cb">
|
||
<?php echo esc_html($cat->name); ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<p class="description" style="margin-top:8px;">Ohne Auswahl gilt der Gutschein für <strong>alle Kategorien</strong>.</p>
|
||
</td>
|
||
</tr>
|
||
|
||
</table>
|
||
<p class="submit">
|
||
<input type="submit" name="wis_save_coupon" class="button button-primary" value="Speichern">
|
||
</p>
|
||
</form>
|
||
</div>
|
||
<?php
|
||
return;
|
||
}
|
||
|
||
$coupons = WIS_DB::get_coupons();
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
global $wpdb;
|
||
$uses_table = $wpdb->prefix . 'wis_coupon_uses';
|
||
$uses_exists = $wpdb->get_var("SHOW TABLES LIKE '$uses_table'");
|
||
// Alle Einlösungen laden (gruppiert nach coupon_id)
|
||
$uses_by_coupon = [];
|
||
if ($uses_exists) {
|
||
$all_uses = $wpdb->get_results("SELECT coupon_id, player_name, used_at FROM $uses_table ORDER BY used_at DESC");
|
||
foreach ($all_uses as $u) {
|
||
$uses_by_coupon[$u->coupon_id][] = $u;
|
||
}
|
||
}
|
||
?>
|
||
<div class="wrap">
|
||
<h1>Gutscheine
|
||
<a href="<?php echo admin_url('admin.php?page=wis_coupons&add=1'); ?>" class="page-title-action">Neu erstellen</a>
|
||
<a href="#wis-bulk-section" class="page-title-action" onclick="document.getElementById('wis-bulk-section').style.display=document.getElementById('wis-bulk-section').style.display==='none'?'block':'none';return false;">🎲 Bulk generieren</a>
|
||
</h1>
|
||
|
||
<?php
|
||
// Bulk-Gruppen zusammenfassen für Anzeige
|
||
$bulk_groups = [];
|
||
foreach ($coupons as $c) {
|
||
if (!empty($c->bulk_id)) $bulk_groups[$c->bulk_id][] = $c;
|
||
}
|
||
?>
|
||
|
||
<!-- ── Bulk-Generator Formular ─────────────────────────────────── -->
|
||
<div id="wis-bulk-section" style="display:none;background:#fff;border:1px solid #c3c4c7;border-radius:4px;padding:20px 24px;margin-bottom:20px;">
|
||
<h2 style="margin-top:0;">🎲 Bulk-Gutscheine generieren</h2>
|
||
<form method="post">
|
||
<?php wp_nonce_field('wis_bulk_generate'); ?>
|
||
<table class="form-table" style="max-width:700px;">
|
||
<tr>
|
||
<th>Anzahl</th>
|
||
<td><input type="number" name="bulk_count" value="10" min="1" max="500" class="small-text"> <span class="description">Max. 500 pro Durchlauf</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th>Code-Format</th>
|
||
<td>
|
||
<label style="margin-right:16px;">
|
||
<input type="radio" name="bulk_mode" value="random" checked onchange="document.getElementById('wis-prefix-row').style.display='none'">
|
||
Zufällig <code style="font-size:11px;">X7K2-MNP9-Q4RT</code>
|
||
</label>
|
||
<label>
|
||
<input type="radio" name="bulk_mode" value="prefix" onchange="document.getElementById('wis-prefix-row').style.display='table-row'">
|
||
Mit Prefix <code style="font-size:11px;">SUMMER-XXX-XXXXX</code>
|
||
</label>
|
||
</td>
|
||
</tr>
|
||
<tr id="wis-prefix-row" style="display:none;">
|
||
<th>Prefix</th>
|
||
<td><input type="text" name="bulk_prefix" class="regular-text" placeholder="z.B. SUMMER" maxlength="20" style="text-transform:uppercase;"></td>
|
||
</tr>
|
||
<tr>
|
||
<th>Rabattwert</th>
|
||
<td>
|
||
<input type="number" name="bulk_value" value="10" min="1" class="small-text">
|
||
<select name="bulk_type">
|
||
<option value="fixed"><?php echo esc_html(get_option('wis_currency_name','Coins')); ?> (fest)</option>
|
||
<option value="percent">% (Prozent)</option>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Max. Einlösungen</th>
|
||
<td><input type="number" name="bulk_usage_limit" value="1" min="1" class="small-text"> <span class="description">pro Code</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th>Ablaufdatum</th>
|
||
<td><input type="date" name="bulk_expiry"> <span class="description">Optional</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th>Mindestbestellwert</th>
|
||
<td><input type="number" name="bulk_min_order" value="0" min="0" class="small-text"> <?php echo esc_html(get_option('wis_currency_name','Coins')); ?> <span class="description">(0 = kein Minimum)</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th>Erlaubte Kategorien</th>
|
||
<td>
|
||
<?php
|
||
$all_cats = WIS_DB::get_categories();
|
||
?>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px;max-width:480px;">
|
||
<?php foreach ($all_cats as $cat): ?>
|
||
<label style="display:flex;align-items:center;gap:6px;background:#f3f4f6;border:1px solid #d1d5db;border-radius:20px;padding:4px 12px;cursor:pointer;font-size:0.875rem;" class="wis-bulk-cat-tag">
|
||
<input type="checkbox" name="bulk_allowed_categories[]" value="<?php echo esc_attr($cat->id); ?>" style="display:none;" class="wis-bulk-cat-cb">
|
||
<?php echo esc_html($cat->name); ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<p class="description" style="margin-top:6px;">Ohne Auswahl gilt für alle Kategorien.</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<p><input type="submit" name="wis_bulk_generate" class="button button-primary" value="🎲 Jetzt generieren"></p>
|
||
</form>
|
||
<script>
|
||
document.querySelectorAll('.wis-bulk-cat-tag').forEach(function(label) {
|
||
label.addEventListener('click', function() {
|
||
var cb = label.querySelector('.wis-bulk-cat-cb');
|
||
setTimeout(function() {
|
||
label.style.background = cb.checked ? '#667eea' : '#f3f4f6';
|
||
label.style.color = cb.checked ? '#fff' : '#333';
|
||
label.style.borderColor = cb.checked ? '#667eea' : '#d1d5db';
|
||
}, 0);
|
||
});
|
||
});
|
||
</script>
|
||
</div>
|
||
|
||
<!-- ── Bulk-Gruppen Übersicht ──────────────────────────────────── -->
|
||
<?php if (!empty($bulk_groups)): ?>
|
||
<div style="background:#f8f9fa;border:1px solid #e2e8f0;border-radius:4px;padding:16px 20px;margin-bottom:20px;">
|
||
<h3 style="margin:0 0 12px;">📦 Generierte Bulk-Gruppen</h3>
|
||
<table class="widefat fixed striped" style="max-width:700px;">
|
||
<thead><tr>
|
||
<th>Bulk-ID</th>
|
||
<th style="width:80px;">Codes</th>
|
||
<th style="width:110px;">Rabatt</th>
|
||
<th style="width:100px;">Gültig bis</th>
|
||
<th style="width:130px;">Aktionen</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php foreach ($bulk_groups as $bid => $bcodes):
|
||
$sample = $bcodes[0];
|
||
$export_url = wp_nonce_url(admin_url('admin.php?page=wis_coupons&action=export_bulk&bulk_id='.$bid), 'wis_coupon_action', '_wpnonce');
|
||
?>
|
||
<tr>
|
||
<td><code style="font-size:11px;"><?php echo esc_html($bid); ?></code></td>
|
||
<td><?php echo count($bcodes); ?></td>
|
||
<td><?php echo esc_html($sample->value); ?><?php echo $sample->type==='percent'?'%':' '.esc_html(get_option('wis_currency_name','Coins')); ?></td>
|
||
<td><?php echo $sample->expiry ? esc_html(date('d.m.Y', strtotime($sample->expiry))) : '∞'; ?></td>
|
||
<td><a href="<?php echo $export_url; ?>" class="button button-small">📥 CSV Export</a></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<table class="widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:130px;">Code</th>
|
||
<th style="width:110px;">Rabatt</th>
|
||
<th style="width:90px;">Genutzt</th>
|
||
<th style="width:90px;">Gültig bis</th>
|
||
<th>Eingelöst von</th>
|
||
<th style="width:160px;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (empty($coupons)): ?>
|
||
<tr><td colspan="6" style="text-align:center;padding:40px;">Noch keine Gutscheine vorhanden.</td></tr>
|
||
<?php else: foreach ($coupons as $coupon):
|
||
$coupon_uses = $uses_by_coupon[$coupon->id] ?? [];
|
||
?>
|
||
<tr>
|
||
<td>
|
||
<strong><?php echo esc_html($coupon->code); ?></strong>
|
||
<?php if (!empty($coupon->bulk_id)): ?>
|
||
<span title="Bulk-Gruppe: <?php echo esc_attr($coupon->bulk_id); ?>" style="display:inline-block;background:#e0e7ff;color:#3730a3;border-radius:10px;padding:1px 7px;font-size:10px;margin-left:4px;cursor:default;">BULK</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td>
|
||
<?php if ($coupon->type === 'percent'): ?>
|
||
<?php echo esc_html($coupon->value); ?>%
|
||
<?php else: ?>
|
||
<?php echo esc_html($coupon->value); ?> <?php echo esc_html($currency); ?>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?php echo esc_html($coupon->used_count); ?> / <?php echo esc_html($coupon->usage_limit); ?></td>
|
||
<td><?php echo $coupon->expiry ? esc_html(date('d.m.Y', strtotime($coupon->expiry))) : '∞'; ?></td>
|
||
<td>
|
||
<?php if (empty($coupon_uses)): ?>
|
||
<span style="color:#ccc;">–</span>
|
||
<?php else: ?>
|
||
<div style="display:flex;flex-wrap:wrap;gap:4px;">
|
||
<?php foreach ($coupon_uses as $u): ?>
|
||
<span title="<?php echo esc_attr(date('d.m.Y H:i', strtotime($u->used_at))); ?>"
|
||
style="background:#eef;border:1px solid #c9d;border-radius:10px;padding:2px 8px;font-size:12px;cursor:default;">
|
||
<?php echo esc_html($u->player_name); ?>
|
||
</span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_coupons&edit=' . $coupon->id); ?>" class="button button-small">Bearbeiten</a>
|
||
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_coupons&action=delete&id=' . $coupon->id), 'wis_coupon_action', '_wpnonce'); ?>" class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
public static function page_orders() {
|
||
global $wpdb;
|
||
|
||
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'delete') {
|
||
check_admin_referer('wis_order_action', '_wpnonce');
|
||
WIS_DB::delete_order(intval($_GET['id']));
|
||
echo '<div class="updated"><p>✅ Bestellung gelöscht!</p></div>';
|
||
}
|
||
|
||
if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'complete') {
|
||
check_admin_referer('wis_order_action', '_wpnonce');
|
||
WIS_DB::update_order_status(intval($_GET['id']), 'completed');
|
||
echo '<div class="updated"><p>✅ Status geändert!</p></div>';
|
||
}
|
||
|
||
if (isset($_GET['view'])) {
|
||
$order = WIS_DB::get_order(intval($_GET['view']));
|
||
if (!$order) { echo '<div class="error"><p>Bestellung nicht gefunden.</p></div>'; return; }
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$status_colors = ['pending'=>'#ffc107','processing'=>'#0073aa','completed'=>'green','cancelled'=>'red','failed'=>'red'];
|
||
$status_labels = ['pending'=>'Warte auf Ingame','processing'=>'In Bearbeitung','completed'=>'Erledigt','cancelled'=>'Abgebrochen','failed'=>'Fehler'];
|
||
?>
|
||
<div class="wrap">
|
||
<h1>Bestellung #<?php echo $order->id; ?> – Details</h1>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_orders'); ?>" class="button">← Zurück</a>
|
||
<table class="widefat" style="max-width:800px; margin-top:20px;">
|
||
<tr><th>ID</th><td><?php echo esc_html($order->id); ?></td></tr>
|
||
<tr><th>Datum</th><td><?php echo esc_html(date('d.m.Y H:i', strtotime($order->created_at))); ?></td></tr>
|
||
<tr><th>Käufer</th><td><strong><?php echo esc_html($order->player_name); ?></strong></td></tr>
|
||
<?php if (!empty($order->gift_recipient)): ?>
|
||
<tr><th>🎁 Geschenk für</th><td style="color:#9b59b6;font-weight:bold;"><?php echo esc_html($order->gift_recipient); ?></td></tr>
|
||
<?php endif; ?>
|
||
<tr><th>Server</th><td><?php echo esc_html($order->server); ?></td></tr>
|
||
<tr><th>Zusammenfassung</th><td><?php echo esc_html($order->item_title); ?></td></tr>
|
||
<tr><th>Preis</th><td><?php echo esc_html($order->price); ?> <?php echo esc_html($currency); ?></td></tr>
|
||
<tr><th>Status</th><td style="color:<?php echo $status_colors[$order->status] ?? 'black'; ?>;font-weight:bold;"><?php echo $status_labels[$order->status] ?? $order->status; ?></td></tr>
|
||
<tr><th>Details (JSON)</th><td><code style="display:block;background:#eee;padding:10px;font-size:11px;overflow-x:auto;"><?php echo esc_html($order->response); ?></code></td></tr>
|
||
</table>
|
||
</div>
|
||
<?php
|
||
return;
|
||
}
|
||
|
||
// --- Filter-Parameter ---
|
||
$search = sanitize_text_field($_GET['s'] ?? '');
|
||
$f_status = sanitize_text_field($_GET['status'] ?? '');
|
||
$f_server = sanitize_text_field($_GET['server'] ?? '');
|
||
$per_page = 50;
|
||
$cur_page = max(1, intval($_GET['paged'] ?? 1));
|
||
$offset = ($cur_page - 1) * $per_page;
|
||
|
||
$where = ['1=1'];
|
||
$params = [];
|
||
if ($search) {
|
||
$where[] = '(player_name LIKE %s OR gift_recipient LIKE %s OR item_title LIKE %s)';
|
||
$like = '%' . $wpdb->esc_like($search) . '%';
|
||
$params[] = $like; $params[] = $like; $params[] = $like;
|
||
}
|
||
if ($f_status) { $where[] = 'status = %s'; $params[] = $f_status; }
|
||
if ($f_server) { $where[] = 'server = %s'; $params[] = $f_server; }
|
||
|
||
$where_sql = implode(' AND ', $where);
|
||
$base_sql = "FROM {$wpdb->prefix}wis_orders WHERE $where_sql ORDER BY created_at DESC";
|
||
|
||
$total = $params
|
||
? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) $base_sql", ...$params))
|
||
: (int)$wpdb->get_var("SELECT COUNT(*) $base_sql");
|
||
|
||
$orders_sql = "SELECT * $base_sql LIMIT %d OFFSET %d";
|
||
$orders = $params
|
||
? $wpdb->get_results($wpdb->prepare($orders_sql, ...[...$params, $per_page, $offset]))
|
||
: $wpdb->get_results($wpdb->prepare($orders_sql, $per_page, $offset));
|
||
|
||
$total_pages = max(1, (int)ceil($total / $per_page));
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$servers = WIS_DB::get_servers();
|
||
|
||
$status_map = ['pending'=>'Warte','claimed'=>'Abgeholt','processing'=>'Geben...','completed'=>'Fertig','cancelled'=>'Abgebrochen','failed'=>'Fehler'];
|
||
$status_colors = ['pending'=>'#ffc107','claimed'=>'#17a2b8','processing'=>'#0073aa','completed'=>'green','cancelled'=>'red','failed'=>'red'];
|
||
?>
|
||
<div class="wrap">
|
||
<h1>Bestellungen <span style="font-size:14px;color:#666;">(<?php echo number_format($total); ?> gesamt)</span></h1>
|
||
|
||
<!-- Suchleiste -->
|
||
<form method="get" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:15px 0;">
|
||
<input type="hidden" name="page" value="wis_orders">
|
||
<input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Spieler, Item suchen…" style="min-width:220px;">
|
||
<select name="status">
|
||
<option value="">Alle Status</option>
|
||
<?php foreach ($status_map as $k => $l): ?>
|
||
<option value="<?php echo $k; ?>" <?php selected($f_status, $k); ?>><?php echo $l; ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<select name="server">
|
||
<option value="">Alle Server</option>
|
||
<?php foreach ($servers as $s): ?>
|
||
<option value="<?php echo esc_attr($s->slug); ?>" <?php selected($f_server, $s->slug); ?>><?php echo esc_html($s->name); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit" class="button">Filtern</button>
|
||
<?php if ($search || $f_status || $f_server): ?>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_orders'); ?>" class="button">Zurücksetzen</a>
|
||
<?php endif; ?>
|
||
</form>
|
||
|
||
<table class="widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:50px;">ID</th>
|
||
<th style="width:130px;">Datum</th>
|
||
<th style="width:120px;">Käufer</th>
|
||
<th style="width:120px;">Empfänger</th>
|
||
<th style="width:80px;">Server</th>
|
||
<th>Inhalt</th>
|
||
<th style="width:90px;">Preis</th>
|
||
<th style="width:110px;">Status</th>
|
||
<th style="width:180px;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (empty($orders)): ?>
|
||
<tr><td colspan="9" style="text-align:center;padding:40px;color:#999;">Keine Bestellungen gefunden.</td></tr>
|
||
<?php else: foreach ($orders as $order): ?>
|
||
<tr>
|
||
<td><strong>#<?php echo $order->id; ?></strong></td>
|
||
<td><?php echo date('d.m.Y H:i', strtotime($order->created_at)); ?></td>
|
||
<td><strong><?php echo esc_html($order->player_name); ?></strong></td>
|
||
<td><?php if (!empty($order->gift_recipient)): ?><span style="color:#9b59b6;">🎁 <?php echo esc_html($order->gift_recipient); ?></span><?php else: ?><span style="color:#ccc;">–</span><?php endif; ?></td>
|
||
<td><?php echo esc_html($order->server); ?></td>
|
||
<td><?php echo esc_html(mb_substr($order->item_title, 0, 55)) . (mb_strlen($order->item_title) > 55 ? '…' : ''); ?></td>
|
||
<td><?php echo esc_html($order->price); ?> <?php echo esc_html($currency); ?></td>
|
||
<td style="color:<?php echo $status_colors[$order->status] ?? '#333'; ?>;font-weight:bold;"><?php echo $status_map[$order->status] ?? $order->status; ?></td>
|
||
<td>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_orders&view=' . $order->id); ?>" class="button button-small">Details</a>
|
||
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=wis_orders&action=delete&id=' . $order->id . ($search?"&s=$search":'') . ($f_status?"&status=$f_status":'') . ($f_server?"&server=$f_server":'')), 'wis_order_action', '_wpnonce'); ?>" class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; endif; ?>
|
||
</tbody>
|
||
</table>
|
||
|
||
<?php if ($total_pages > 1): ?>
|
||
<div style="margin-top:15px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
|
||
<?php for ($p = 1; $p <= $total_pages; $p++):
|
||
$url = admin_url('admin.php?page=wis_orders&paged='.$p.($search?"&s=$search":'').($f_status?"&status=$f_status":'').($f_server?"&server=$f_server":''));
|
||
?>
|
||
<a href="<?php echo $url; ?>" class="button<?php echo $p===$cur_page?' button-primary':''; ?>"><?php echo $p; ?></a>
|
||
<?php endfor; ?>
|
||
<span style="color:#666;font-size:13px;">Seite <?php echo $cur_page; ?> / <?php echo $total_pages; ?> (<?php echo $total; ?> Einträge)</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
// -------------------------------------------------------
|
||
// ANKAUF-LOG
|
||
// -------------------------------------------------------
|
||
public static function page_sell_log() {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_sell_log';
|
||
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) {
|
||
echo '<div class="wrap"><h1>Ankauf-Log</h1><p>Tabelle noch nicht vorhanden – Plugin einmal deaktivieren/aktivieren.</p></div>';
|
||
return;
|
||
}
|
||
|
||
$search = sanitize_text_field($_GET['s'] ?? '');
|
||
$f_item = sanitize_text_field($_GET['item'] ?? '');
|
||
$f_server = sanitize_text_field($_GET['server'] ?? '');
|
||
$per_page = 50;
|
||
$cur_page = max(1, intval($_GET['paged'] ?? 1));
|
||
$offset = ($cur_page - 1) * $per_page;
|
||
|
||
$where = ['1=1']; $params = [];
|
||
if ($search) {
|
||
$like = '%' . $wpdb->esc_like($search) . '%';
|
||
$where[] = '(player_name LIKE %s OR item_name LIKE %s)';
|
||
$params[] = $like; $params[] = $like;
|
||
}
|
||
if ($f_item) { $where[] = 'item_id = %s'; $params[] = $f_item; }
|
||
if ($f_server) { $where[] = 'server = %s'; $params[] = $f_server; }
|
||
|
||
$where_sql = implode(' AND ', $where);
|
||
$total = $params
|
||
? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE $where_sql", ...$params))
|
||
: (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql");
|
||
|
||
$rows = $params
|
||
? $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY sold_at DESC LIMIT %d OFFSET %d", ...[...$params, $per_page, $offset]))
|
||
: $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY sold_at DESC LIMIT %d OFFSET %d", $per_page, $offset));
|
||
|
||
$total_pages = max(1, (int)ceil($total / $per_page));
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$servers = WIS_DB::get_servers();
|
||
$items_with_sell = $wpdb->get_results("SELECT DISTINCT item_id, item_name FROM $table ORDER BY item_name ASC");
|
||
?>
|
||
<div class="wrap">
|
||
<h1>📤 Ankauf-Log <span style="font-size:14px;color:#666;">(<?php echo number_format($total); ?> Einträge)</span></h1>
|
||
<form method="get" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:15px 0;">
|
||
<input type="hidden" name="page" value="wis_sell_log">
|
||
<input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Spieler oder Item…" style="min-width:200px;">
|
||
<select name="item">
|
||
<option value="">Alle Items</option>
|
||
<?php foreach ($items_with_sell as $it): ?>
|
||
<option value="<?php echo esc_attr($it->item_id); ?>" <?php selected($f_item, $it->item_id); ?>><?php echo esc_html($it->item_name); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<select name="server">
|
||
<option value="">Alle Server</option>
|
||
<?php foreach ($servers as $s): ?>
|
||
<option value="<?php echo esc_attr($s->slug); ?>" <?php selected($f_server, $s->slug); ?>><?php echo esc_html($s->name); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit" class="button">Filtern</button>
|
||
<?php if ($search || $f_item || $f_server): ?><a href="<?php echo admin_url('admin.php?page=wis_sell_log'); ?>" class="button">Zurücksetzen</a><?php endif; ?>
|
||
</form>
|
||
<table class="widefat fixed striped">
|
||
<thead><tr>
|
||
<th style="width:130px;">Datum</th>
|
||
<th style="width:120px;">Spieler</th>
|
||
<th style="width:80px;">Server</th>
|
||
<th>Item</th>
|
||
<th style="width:70px;">Menge</th>
|
||
<th style="width:100px;">Ø Preis</th>
|
||
<th style="width:110px;">Ausgezahlt</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php if (empty($rows)): ?>
|
||
<tr><td colspan="7" style="text-align:center;padding:30px;color:#999;">Keine Einträge gefunden.</td></tr>
|
||
<?php else: foreach ($rows as $r): ?>
|
||
<tr>
|
||
<td><?php echo date('d.m.Y H:i', strtotime($r->sold_at)); ?></td>
|
||
<td><strong><?php echo esc_html($r->player_name); ?></strong></td>
|
||
<td><?php echo esc_html($r->server); ?></td>
|
||
<td><?php echo esc_html($r->item_name); ?><small style="display:block;color:#aaa;"><?php echo esc_html($r->item_id); ?></small></td>
|
||
<td><?php echo number_format($r->quantity); ?></td>
|
||
<td><?php echo number_format($r->price_per_item, 2); ?> <?php echo esc_html($currency); ?></td>
|
||
<td><strong><?php echo number_format($r->total_paid, 2); ?></strong> <?php echo esc_html($currency); ?></td>
|
||
</tr>
|
||
<?php endforeach; endif; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php if ($total_pages > 1): ?>
|
||
<div style="margin-top:12px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
|
||
<?php for ($p=1;$p<=$total_pages;$p++): $url=admin_url('admin.php?page=wis_sell_log&paged='.$p.($search?"&s=$search":'').($f_item?"&item=$f_item":'').($f_server?"&server=$f_server":'')); ?>
|
||
<a href="<?php echo $url; ?>" class="button<?php echo $p===$cur_page?' button-primary':''; ?>"><?php echo $p; ?></a>
|
||
<?php endfor; ?>
|
||
<span style="color:#666;font-size:13px;">Seite <?php echo $cur_page; ?> / <?php echo $total_pages; ?></span>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
// -------------------------------------------------------
|
||
// PREISHISTORIE
|
||
// -------------------------------------------------------
|
||
public static function page_price_history() {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_price_history';
|
||
if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) {
|
||
echo '<div class="wrap"><h1>Preishistorie</h1><p>Tabelle noch nicht vorhanden – Plugin einmal deaktivieren/aktivieren.</p></div>';
|
||
return;
|
||
}
|
||
|
||
$search = sanitize_text_field($_GET['s'] ?? '');
|
||
$f_field = sanitize_text_field($_GET['field'] ?? '');
|
||
$per_page = 50;
|
||
$cur_page = max(1, intval($_GET['paged'] ?? 1));
|
||
$offset = ($cur_page - 1) * $per_page;
|
||
|
||
$where = ['1=1']; $params = [];
|
||
if ($search) {
|
||
$like = '%' . $wpdb->esc_like($search) . '%';
|
||
$where[] = '(item_name LIKE %s OR item_id LIKE %s OR changed_by LIKE %s)';
|
||
$params[] = $like; $params[] = $like; $params[] = $like;
|
||
}
|
||
if ($f_field) { $where[] = 'field = %s'; $params[] = $f_field; }
|
||
|
||
$where_sql = implode(' AND ', $where);
|
||
$total = $params
|
||
? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE $where_sql", ...$params))
|
||
: (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql");
|
||
|
||
$rows = $params
|
||
? $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY changed_at DESC LIMIT %d OFFSET %d", ...[...$params, $per_page, $offset]))
|
||
: $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY changed_at DESC LIMIT %d OFFSET %d", $per_page, $offset));
|
||
|
||
$total_pages = max(1, (int)ceil($total / $per_page));
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$fields_list = $wpdb->get_col("SELECT DISTINCT field FROM $table ORDER BY field");
|
||
?>
|
||
<div class="wrap">
|
||
<h1>📈 Preishistorie <span style="font-size:14px;color:#666;">(<?php echo number_format($total); ?> Änderungen)</span></h1>
|
||
<form method="get" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:15px 0;">
|
||
<input type="hidden" name="page" value="wis_price_history">
|
||
<input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Item oder Admin…" style="min-width:200px;">
|
||
<select name="field">
|
||
<option value="">Alle Felder</option>
|
||
<?php foreach ($fields_list as $f): ?>
|
||
<option value="<?php echo esc_attr($f); ?>" <?php selected($f_field, $f); ?>><?php echo esc_html($f); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit" class="button">Filtern</button>
|
||
<?php if ($search||$f_field): ?><a href="<?php echo admin_url('admin.php?page=wis_price_history'); ?>" class="button">Zurücksetzen</a><?php endif; ?>
|
||
</form>
|
||
<?php if ($total === 0 && !$search && !$f_field): ?>
|
||
<p style="color:#999;">Noch keine Preisänderungen protokolliert. Ändere einen Preis bei einem Item und speichere – ab dann wird hier alles festgehalten.</p>
|
||
<?php endif; ?>
|
||
<table class="widefat fixed striped">
|
||
<thead><tr>
|
||
<th style="width:130px;">Datum</th>
|
||
<th>Item</th>
|
||
<th style="width:120px;">Feld</th>
|
||
<th style="width:100px;">Alt</th>
|
||
<th style="width:100px;">Neu</th>
|
||
<th style="width:30px;">Diff</th>
|
||
<th style="width:100px;">Geändert von</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php if (empty($rows)): ?>
|
||
<tr><td colspan="7" style="text-align:center;padding:30px;color:#999;">Keine Einträge gefunden.</td></tr>
|
||
<?php else: foreach ($rows as $r):
|
||
$diff = $r->new_value - $r->old_value;
|
||
$diff_color = $diff > 0 ? 'green' : ($diff < 0 ? 'red' : '#666');
|
||
?>
|
||
<tr>
|
||
<td><?php echo date('d.m.Y H:i', strtotime($r->changed_at)); ?></td>
|
||
<td><strong><?php echo esc_html($r->item_name); ?></strong><small style="display:block;color:#aaa;"><?php echo esc_html($r->item_id); ?></small></td>
|
||
<td><?php echo esc_html($r->field); ?></td>
|
||
<td><?php echo number_format($r->old_value); ?> <?php echo esc_html($currency); ?></td>
|
||
<td><strong><?php echo number_format($r->new_value); ?></strong> <?php echo esc_html($currency); ?></td>
|
||
<td style="color:<?php echo $diff_color; ?>;font-weight:bold;"><?php echo ($diff>0?'+':'') . number_format($diff); ?></td>
|
||
<td><?php echo esc_html($r->changed_by); ?></td>
|
||
</tr>
|
||
<?php endforeach; endif; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php if ($total_pages > 1): ?>
|
||
<div style="margin-top:12px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
|
||
<?php for ($p=1;$p<=$total_pages;$p++): $url=admin_url('admin.php?page=wis_price_history&paged='.$p.($search?"&s=$search":'').($f_field?"&field=$f_field":'')); ?>
|
||
<a href="<?php echo $url; ?>" class="button<?php echo $p===$cur_page?' button-primary':''; ?>"><?php echo $p; ?></a>
|
||
<?php endfor; ?>
|
||
<span style="color:#666;font-size:13px;">Seite <?php echo $cur_page; ?> / <?php echo $total_pages; ?></span>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
public static function page_json() {
|
||
if (isset($_POST['wis_generate_json'])) {
|
||
check_admin_referer('wis_json_export');
|
||
|
||
$items = WIS_DB::get_items(['status' => 'publish']);
|
||
$img_base = get_option('wis_image_base_url', '');
|
||
|
||
$json_data = ['items' => []];
|
||
|
||
foreach ($items as $item) {
|
||
$json_data['items'][] = [
|
||
'id' => $item->item_id,
|
||
'name' => $item->name,
|
||
'description' => $item->description,
|
||
'price' => intval($item->price),
|
||
'image' => WIS_DB::get_item_image($item)
|
||
];
|
||
}
|
||
|
||
$json_output = json_encode($json_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
|
||
echo '<div class="wrap"><h1>📦 JSON Export</h1>';
|
||
echo '<div class="updated"><p>✅ JSON erfolgreich generiert!</p></div>';
|
||
echo '<textarea style="width:100%; height:400px; font-family:monospace; font-size:12px;">'.esc_textarea($json_output).'</textarea>';
|
||
echo '<p><button onclick="downloadJSON()" class="button button-primary">💾 Als items.json herunterladen</button></p>';
|
||
echo '<script>
|
||
function downloadJSON() {
|
||
const text = document.querySelector("textarea").value;
|
||
const blob = new Blob([text], {type: "application/json"});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = "items.json";
|
||
a.click();
|
||
}
|
||
</script>';
|
||
echo '<h3>📤 Nächste Schritte:</h3>';
|
||
echo '<ol>';
|
||
echo '<li>Lade die JSON-Datei herunter</li>';
|
||
echo '<li>Gehe zu deinem Gitea Repository</li>';
|
||
echo '<li>Lade die <code>items.json</code> hoch unter: <code>https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro</code></li>';
|
||
echo '<li>Klicke dann auf den <strong>Quick-Import</strong> Button unten!</li>';
|
||
echo '</ol>';
|
||
echo '</div>';
|
||
return;
|
||
}
|
||
|
||
$default_url = 'https://git.viper.ipv64.net/M_Viper/WP-Ingame-Shop-Pro/raw/branch/main/items.json';
|
||
?>
|
||
<div class="wrap">
|
||
<h1>📦 JSON Export/Import</h1>
|
||
|
||
<div class="card" style="max-width:800px; padding:20px; margin-top:20px;">
|
||
<h2>📤 JSON Export</h2>
|
||
<p>Generiere eine JSON-Datei mit allen deinen Items für Gitea.</p>
|
||
<form method="post">
|
||
<?php wp_nonce_field('wis_json_export'); ?>
|
||
<p class="submit">
|
||
<input type="submit" name="wis_generate_json" class="button button-primary button-large" value="📦 JSON Generieren">
|
||
</p>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card" style="max-width:800px; padding:20px; margin-top:20px; background:#e8f5e9;">
|
||
<h2>⚡ Quick-Import von Gitea</h2>
|
||
<p><strong>Importiert direkt von deinem Gitea Repository!</strong></p>
|
||
<p style="margin:10px 0; padding:10px; background:#fff; border-left:4px solid #28a745; font-family:monospace; font-size:12px; word-break:break-all;">
|
||
<?php echo esc_html($default_url); ?>
|
||
</p>
|
||
<button type="button" id="btn-quick-import" class="button button-primary button-large" style="background:#28a745; border-color:#28a745;">
|
||
⚡ Quick-Import starten
|
||
</button>
|
||
<div id="quick-import-result" style="margin-top:15px;"></div>
|
||
</div>
|
||
|
||
<div class="card" style="max-width:800px; padding:20px; margin-top:20px;">
|
||
<h2>📥 JSON Import (Manuelle URL)</h2>
|
||
<p>Importiere Items aus einer beliebigen JSON-URL.</p>
|
||
|
||
<div id="wis-import-form">
|
||
<input type="text" id="import-url" class="large-text code" value="<?php echo esc_attr($default_url); ?>" style="margin-bottom:15px;">
|
||
<br>
|
||
<button type="button" id="btn-import" class="button button-primary button-large">📥 Importieren</button>
|
||
<div id="import-result" style="margin-top:15px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.getElementById('btn-quick-import').addEventListener('click', function() {
|
||
const url = '<?php echo esc_js($default_url); ?>';
|
||
const resultDiv = document.getElementById('quick-import-result');
|
||
const btn = this;
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Importiere von Gitea...';
|
||
resultDiv.innerHTML = '<div style="padding:10px; background:#fff3cd; border-left:4px solid #ffc107; margin-top:10px;">⏳ Lade Items von Gitea...</div>';
|
||
|
||
fetch('<?php echo rest_url('wis/v1/import_json'); ?>', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type':'application/json',
|
||
'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
|
||
},
|
||
body: JSON.stringify({url: url})
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
resultDiv.innerHTML = '<div class="updated" style="margin-top:10px;"><p><strong>✅ Erfolgreich!</strong><br>' + data.imported + ' Items importiert! (' + data.skipped + ' übersprungen)</p></div>';
|
||
setTimeout(() => {
|
||
window.location.href = '<?php echo admin_url('admin.php?page=wis_items'); ?>';
|
||
}, 2000);
|
||
} else {
|
||
resultDiv.innerHTML = '<div class="error" style="margin-top:10px;"><p><strong>❌ Fehler:</strong><br>' + data.message + '</p></div>';
|
||
}
|
||
})
|
||
.catch(e => {
|
||
resultDiv.innerHTML = '<div class="error" style="margin-top:10px;"><p><strong>❌ Netzwerkfehler:</strong><br>' + e.message + '</p></div>';
|
||
})
|
||
.finally(() => {
|
||
btn.disabled = false;
|
||
btn.textContent = '⚡ Quick-Import starten';
|
||
});
|
||
});
|
||
|
||
document.getElementById('btn-import').addEventListener('click', function() {
|
||
const url = document.getElementById('import-url').value.trim();
|
||
const resultDiv = document.getElementById('import-result');
|
||
|
||
if (!url) {
|
||
resultDiv.innerHTML = '<div class="error"><p>Bitte URL eingeben!</p></div>';
|
||
return;
|
||
}
|
||
|
||
this.disabled = true;
|
||
this.textContent = '⏳ Importiere...';
|
||
|
||
fetch('<?php echo rest_url('wis/v1/import_json'); ?>', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type':'application/json',
|
||
'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
|
||
},
|
||
body: JSON.stringify({url: url})
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
resultDiv.innerHTML = '<div class="updated"><p>✅ ' + data.imported + ' Items importiert! (' + data.skipped + ' übersprungen)</p></div>';
|
||
setTimeout(() => {
|
||
window.location.href = '<?php echo admin_url('admin.php?page=wis_items'); ?>';
|
||
}, 2000);
|
||
} else {
|
||
resultDiv.innerHTML = '<div class="error"><p>❌ Fehler: ' + data.message + '</p></div>';
|
||
}
|
||
})
|
||
.catch(e => {
|
||
resultDiv.innerHTML = '<div class="error"><p>❌ Netzwerkfehler: ' + e.message + '</p></div>';
|
||
})
|
||
.finally(() => {
|
||
this.disabled = false;
|
||
this.textContent = '📥 Importieren';
|
||
});
|
||
});
|
||
</script>
|
||
<?php
|
||
}
|
||
|
||
public static function page_reset() {
|
||
$msg = '';
|
||
|
||
if (isset($_POST['wis_confirm_reset'])) {
|
||
check_admin_referer('wis_reset');
|
||
WIS_Activator::reset_shop();
|
||
$msg = '<div class="updated"><p>✅ Shop wurde komplett zurückgesetzt!</p></div>';
|
||
}
|
||
if (isset($_POST['wis_reset_sell_log'])) {
|
||
check_admin_referer('wis_reset_sell_log');
|
||
WIS_Activator::reset_sell_log();
|
||
$msg = '<div class="updated"><p>✅ Ankauf-Log wurde geleert!</p></div>';
|
||
}
|
||
if (isset($_POST['wis_reset_top_spenders'])) {
|
||
check_admin_referer('wis_reset_top_spenders');
|
||
WIS_Activator::reset_top_spenders();
|
||
$msg = '<div class="updated"><p>✅ Top-Spender-Daten wurden zurückgesetzt!</p></div>';
|
||
}
|
||
if (isset($_POST['wis_reset_analyse'])) {
|
||
check_admin_referer('wis_reset_analyse');
|
||
WIS_Activator::reset_analyse();
|
||
$msg = '<div class="updated"><p>✅ Analyse-Daten wurden geleert!</p></div>';
|
||
}
|
||
if (isset($_POST['wis_reset_price_history'])) {
|
||
check_admin_referer('wis_reset_price_history');
|
||
WIS_Activator::reset_price_history();
|
||
$msg = '<div class="updated"><p>✅ Preishistorie wurde geleert!</p></div>';
|
||
}
|
||
|
||
?>
|
||
<div class="wrap">
|
||
<h1>🔄 Shop Reset</h1>
|
||
<?php echo $msg; ?>
|
||
|
||
<?php
|
||
$card = 'max-width:800px;padding:20px;margin-bottom:20px;background:#fff;border:1px solid #ddd;border-radius:4px;';
|
||
$warn = 'max-width:800px;padding:20px;margin-bottom:20px;background:#fff3cd;border:1px solid #ffc107;border-radius:4px;';
|
||
?>
|
||
|
||
<!-- Ankauf-Log -->
|
||
<div class="card" style="<?php echo $card; ?>">
|
||
<h2>📤 Ankauf-Log zurücksetzen</h2>
|
||
<p>Löscht alle Einträge in der <code>wis_sell_log</code>-Tabelle.</p>
|
||
<p style="color:#666;">Items, Bestellungen und Einstellungen bleiben unberührt.</p>
|
||
<form method="post" onsubmit="return confirm('Ankauf-Log wirklich komplett leeren?');">
|
||
<?php wp_nonce_field('wis_reset_sell_log'); ?>
|
||
<input type="submit" name="wis_reset_sell_log" class="button button-secondary" value="🗑️ Ankauf-Log leeren">
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Top Spender -->
|
||
<div class="card" style="<?php echo $card; ?>">
|
||
<h2>🏆 Top-Spender-Daten zurücksetzen</h2>
|
||
<p>Löscht <strong>alle</strong> Bestellungen aus der Datenbank (alle Status).</p>
|
||
<p style="color:#666;">Neue Bestellungen werden weiterhin normal erfasst.</p>
|
||
<form method="post" onsubmit="return confirm('Top-Spender-Daten wirklich löschen?');">
|
||
<?php wp_nonce_field('wis_reset_top_spenders'); ?>
|
||
<input type="submit" name="wis_reset_top_spenders" class="button button-secondary" value="🗑️ Top-Spender löschen">
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Analyse -->
|
||
<div class="card" style="<?php echo $card; ?>">
|
||
<h2>📊 Analyse-Daten zurücksetzen</h2>
|
||
<p>Leert die <code>wis_order_items</code>-Tabelle (Grundlage für Kauf- & Verkaufsanalyse).</p>
|
||
<p style="color:#666;">Bestellungen und Items bleiben erhalten – nur die Detailauswertung wird geleert.</p>
|
||
<form method="post" onsubmit="return confirm('Analyse-Daten wirklich leeren?');">
|
||
<?php wp_nonce_field('wis_reset_analyse'); ?>
|
||
<input type="submit" name="wis_reset_analyse" class="button button-secondary" value="🗑️ Analyse-Daten leeren">
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Preishistorie -->
|
||
<div class="card" style="<?php echo $card; ?>">
|
||
<h2>📈 Preishistorie zurücksetzen</h2>
|
||
<p>Löscht alle Einträge in der <code>wis_price_history</code>-Tabelle.</p>
|
||
<p style="color:#666;">Items und Bestellungen bleiben unberührt.</p>
|
||
<form method="post" onsubmit="return confirm('Preishistorie wirklich komplett leeren?');">
|
||
<?php wp_nonce_field('wis_reset_price_history'); ?>
|
||
<input type="submit" name="wis_reset_price_history" class="button button-secondary" value="🗑️ Preishistorie leeren">
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Komplett-Reset -->
|
||
<div class="card" style="<?php echo $warn; ?>">
|
||
<h2 style="color:#856404;">⚠️ Komplett-Reset – WARNUNG</h2>
|
||
<p><strong>Diese Aktion löscht ALLE Daten:</strong></p>
|
||
<ul>
|
||
<li>❌ Alle Items</li>
|
||
<li>❌ Alle Bestellungen</li>
|
||
<li>❌ Alle Gutscheine</li>
|
||
<li>❌ Alle Server</li>
|
||
<li>❌ Alle Kategorien</li>
|
||
</ul>
|
||
<p style="color:red;font-weight:bold;">Diese Aktion kann NICHT rückgängig gemacht werden!</p>
|
||
<form method="post" onsubmit="return confirm('WIRKLICH ALLE DATEN LÖSCHEN? Dies kann nicht rückgängig gemacht werden!');">
|
||
<?php wp_nonce_field('wis_reset'); ?>
|
||
<input type="submit" name="wis_confirm_reset" class="button button-secondary button-large" value="🗑️ Shop jetzt zurücksetzen">
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
// =========================================================
|
||
// ABO-VERWALTUNG (Admin)
|
||
// =========================================================
|
||
|
||
public static function page_abo_admin() {
|
||
global $wpdb;
|
||
|
||
// ── POST-Aktionen ─────────────────────────────────────────────────
|
||
if (isset($_POST['wis_abo_action'], $_POST['wis_abo_nonce'])
|
||
&& wp_verify_nonce($_POST['wis_abo_nonce'], 'wis_abo_admin_action')) {
|
||
|
||
$action = sanitize_key($_POST['wis_abo_action']);
|
||
$abo_type = sanitize_key($_POST['wis_abo_type'] ?? '');
|
||
$abo_id = intval($_POST['wis_abo_id'] ?? 0);
|
||
|
||
if ($abo_id > 0) {
|
||
if ($abo_type === 'fly') {
|
||
$table = $wpdb->prefix . 'wis_fly_abo_subs';
|
||
} else {
|
||
$table = $wpdb->prefix . 'wis_item_abo_subs';
|
||
}
|
||
|
||
if ($action === 'cancel') {
|
||
$wpdb->update($table,
|
||
['cancelled' => 1, 'cancelled_at' => current_time('mysql')],
|
||
['id' => $abo_id]
|
||
);
|
||
echo '<div class="updated"><p>✅ Abo #' . $abo_id . ' gekündigt.</p></div>';
|
||
} elseif ($action === 'delete') {
|
||
$wpdb->delete($table, ['id' => $abo_id]);
|
||
echo '<div class="updated"><p>🗑️ Abo #' . $abo_id . ' gelöscht.</p></div>';
|
||
} elseif ($action === 'reactivate') {
|
||
$wpdb->update($table,
|
||
['cancelled' => 0, 'cancelled_at' => null, 'status' => 'active'],
|
||
['id' => $abo_id]
|
||
);
|
||
echo '<div class="updated"><p>✔ Abo #' . $abo_id . ' reaktiviert.</p></div>';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Filter ───────────────────────────────────────────────────────
|
||
$f_player = sanitize_text_field($_GET['player'] ?? '');
|
||
$f_type = sanitize_key($_GET['abo_type'] ?? 'all');
|
||
$f_status = sanitize_key($_GET['abo_status'] ?? 'active');
|
||
|
||
$currency = esc_html(get_option('wis_currency_name', '$'));
|
||
|
||
// ── Fly-Abos laden ────────────────────────────────────────────────
|
||
$fly_table = $wpdb->prefix . 'wis_fly_abo_subs';
|
||
$fly_exists = $wpdb->get_var("SHOW TABLES LIKE '{$fly_table}'");
|
||
$fly_rows = [];
|
||
if ($fly_exists && in_array($f_type, ['all', 'fly'])) {
|
||
$where = '1=1';
|
||
$args = [];
|
||
if ($f_status === 'active') { $where .= " AND status = 'active' AND cancelled = 0 AND expires_at > NOW()"; }
|
||
elseif ($f_status === 'cancelled') { $where .= " AND cancelled = 1"; }
|
||
elseif ($f_status === 'expired') { $where .= " AND (status = 'expired' OR expires_at <= NOW())"; }
|
||
if ($f_player) { $where .= " AND player_name LIKE %s"; $args[] = '%' . $f_player . '%'; }
|
||
$sql = "SELECT *, 'fly' AS abo_type FROM {$fly_table} WHERE {$where} ORDER BY created_at DESC LIMIT 200";
|
||
$fly_rows = $args ? $wpdb->get_results($wpdb->prepare($sql, ...$args)) : $wpdb->get_results($sql);
|
||
}
|
||
|
||
// ── Item-Abos laden ───────────────────────────────────────────────
|
||
$item_table = $wpdb->prefix . 'wis_item_abo_subs';
|
||
$item_exists = $wpdb->get_var("SHOW TABLES LIKE '{$item_table}'");
|
||
$item_rows = [];
|
||
if ($item_exists && in_array($f_type, ['all', 'item'])) {
|
||
$where = '1=1';
|
||
$args = [];
|
||
if ($f_status === 'active') { $where .= " AND status = 'active' AND cancelled = 0 AND expires_at > NOW()"; }
|
||
elseif ($f_status === 'cancelled') { $where .= " AND cancelled = 1"; }
|
||
elseif ($f_status === 'expired') { $where .= " AND (status = 'expired' OR expires_at <= NOW())"; }
|
||
if ($f_player) { $where .= " AND player_name LIKE %s"; $args[] = '%' . $f_player . '%'; }
|
||
$sql = "SELECT *, 'item' AS abo_type FROM {$item_table} WHERE {$where} ORDER BY created_at DESC LIMIT 200";
|
||
$item_rows = $args ? $wpdb->get_results($wpdb->prepare($sql, ...$args)) : $wpdb->get_results($sql);
|
||
}
|
||
|
||
// Zusammenführen und nach Datum sortieren
|
||
$all_rows = array_merge($fly_rows, $item_rows);
|
||
usort($all_rows, fn($a, $b) => strcmp($b->created_at, $a->created_at));
|
||
|
||
$nonce = wp_create_nonce('wis_abo_admin_action');
|
||
$base_url = admin_url('admin.php?page=wis_abo_admin');
|
||
|
||
?>
|
||
<div class="wrap">
|
||
<h1>📋 Abo-Verwaltung <span style="font-size:14px;color:#666;font-weight:400;">(<?php echo count($all_rows); ?> Einträge)</span></h1>
|
||
|
||
<?php /* Filter-Leiste */ ?>
|
||
<form method="get" style="display:flex;gap:8px;align-items:center;margin:16px 0;flex-wrap:wrap;">
|
||
<input type="hidden" name="page" value="wis_abo_admin">
|
||
<input type="text" name="player" value="<?php echo esc_attr($f_player); ?>"
|
||
placeholder="Spieler suchen…" class="regular-text" style="max-width:180px;">
|
||
<select name="abo_type">
|
||
<option value="all" <?php selected($f_type,'all'); ?>>Alle Typen</option>
|
||
<option value="fly" <?php selected($f_type,'fly'); ?>>✈ Fly-Abo</option>
|
||
<option value="item" <?php selected($f_type,'item'); ?>>📦 Item-Abo</option>
|
||
</select>
|
||
<select name="abo_status">
|
||
<option value="active" <?php selected($f_status,'active'); ?>>Aktiv</option>
|
||
<option value="cancelled" <?php selected($f_status,'cancelled'); ?>>Gekündigt</option>
|
||
<option value="expired" <?php selected($f_status,'expired'); ?>>Abgelaufen</option>
|
||
<option value="all" <?php selected($f_status,'all'); ?>>Alle Status</option>
|
||
</select>
|
||
<button type="submit" class="button">Filtern</button>
|
||
<a href="<?php echo $base_url; ?>" class="button">Zurücksetzen</a>
|
||
</form>
|
||
|
||
<?php if (empty($all_rows)): ?>
|
||
<p style="color:#888;">Keine Abos gefunden.</p>
|
||
<?php else: ?>
|
||
|
||
<table class="wp-list-table widefat fixed striped" style="margin-top:0;">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:45px;">ID</th>
|
||
<th style="width:70px;">Typ</th>
|
||
<th style="width:130px;">Spieler</th>
|
||
<th style="width:80px;">Server</th>
|
||
<th>Bezeichnung / Details</th>
|
||
<th style="width:90px;">Preis</th>
|
||
<th style="width:110px;">Läuft bis</th>
|
||
<th style="width:100px;">Status</th>
|
||
<th style="width:180px;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($all_rows as $row):
|
||
$is_fly = $row->abo_type === 'fly';
|
||
$is_active = $row->status === 'active' && !$row->cancelled && strtotime($row->expires_at) > time();
|
||
$is_cancelled = (bool) $row->cancelled;
|
||
$is_expired = !$is_active && !$is_cancelled;
|
||
|
||
$status_html = $is_cancelled
|
||
? '<span style="color:#c0392b;font-weight:600;">⚠ Gekündigt</span>'
|
||
: ($is_active
|
||
? '<span style="color:#27ae60;font-weight:600;">✔ Aktiv</span>'
|
||
: '<span style="color:#888;">⏱ Abgelaufen</span>');
|
||
|
||
$detail = $is_fly
|
||
? 'Fly-Abo · ' . number_format($row->price) . ' ' . $currency . '/Monat'
|
||
: '📦 ' . esc_html($row->item_id) . ' · ' . intval($row->daily_qty) . 'x täglich';
|
||
?>
|
||
<tr>
|
||
<td><strong>#<?php echo $row->id; ?></strong></td>
|
||
<td><?php echo $is_fly ? '✈ Fly' : '📦 Item'; ?></td>
|
||
<td><strong><?php echo esc_html($row->player_name); ?></strong></td>
|
||
<td><?php echo esc_html($row->server); ?></td>
|
||
<td>
|
||
<strong><?php echo esc_html($row->label); ?></strong>
|
||
<br><small style="color:#888;"><?php echo $detail; ?></small>
|
||
<?php if (!$is_fly && !empty($row->last_delivered)): ?>
|
||
<br><small style="color:#aaa;">Zuletzt geliefert: <?php echo date('d.m.Y', strtotime($row->last_delivered)); ?></small>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?php echo $is_fly ? number_format($row->price) . ' ' . $currency : '–'; ?></td>
|
||
<td><?php echo date('d.m.Y', strtotime($row->expires_at)); ?></td>
|
||
<td><?php echo $status_html; ?></td>
|
||
<td>
|
||
<form method="post" style="display:inline;" onsubmit="return confirm('Sicher?');">
|
||
<input type="hidden" name="wis_abo_nonce" value="<?php echo $nonce; ?>">
|
||
<input type="hidden" name="wis_abo_type" value="<?php echo esc_attr($row->abo_type); ?>">
|
||
<input type="hidden" name="wis_abo_id" value="<?php echo $row->id; ?>">
|
||
<?php if ($is_active): ?>
|
||
<button name="wis_abo_action" value="cancel"
|
||
class="button button-small"
|
||
style="color:#c0392b;border-color:#c0392b;"
|
||
title="Kündigen">⛔ Kündigen</button>
|
||
<?php elseif ($is_cancelled): ?>
|
||
<button name="wis_abo_action" value="reactivate"
|
||
class="button button-small"
|
||
style="color:#27ae60;border-color:#27ae60;"
|
||
title="Reaktivieren">✔ Reaktivieren</button>
|
||
<?php endif; ?>
|
||
<button name="wis_abo_action" value="delete"
|
||
class="button button-small"
|
||
style="color:#888;"
|
||
title="Eintrag löschen"
|
||
onclick="return confirm('Abo #<?php echo $row->id; ?> wirklich dauerhaft löschen?');">🗑</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
public static function page_top_spenders() {
|
||
global $wpdb;
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
$results = $wpdb->get_results("
|
||
SELECT player_name, SUM(price) as total_spent, COUNT(*) as order_count
|
||
FROM {$wpdb->prefix}wis_orders
|
||
WHERE status = 'completed'
|
||
GROUP BY player_name
|
||
ORDER BY total_spent DESC
|
||
LIMIT 50
|
||
");
|
||
|
||
?>
|
||
<div class="wrap">
|
||
<h1>🏆 Top Spender</h1>
|
||
<p>Spieler mit den höchsten Gesamtausgaben</p>
|
||
|
||
<table class="widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:80px;">Rang</th>
|
||
<th>Spieler</th>
|
||
<th style="width:150px;">Ausgegeben</th>
|
||
<th style="width:150px;">Bestellungen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (empty($results)): ?>
|
||
<tr><td colspan="4" style="text-align:center; padding:40px;">Noch keine Statistiken vorhanden.</td></tr>
|
||
<?php else: ?>
|
||
<?php $rank = 1; foreach ($results as $row): ?>
|
||
<tr>
|
||
<td><strong>#<?php echo $rank++; ?></strong></td>
|
||
<td><?php echo esc_html($row->player_name); ?></td>
|
||
<td><?php echo esc_html(number_format($row->total_spent)); ?> <?php echo esc_html($currency); ?></td>
|
||
<td><?php echo esc_html($row->order_count); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
public static function page_analyse() {
|
||
global $wpdb;
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
// Zeitraum-Filter
|
||
$range = sanitize_text_field($_GET['range'] ?? '30');
|
||
if (!in_array($range, ['7','30','90','365','all'])) $range = '30';
|
||
|
||
$date_where_oi = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
|
||
$date_where_sell = $range === 'all' ? '' : "AND s.sold_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
|
||
$date_where_ord = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
|
||
$chart_days = in_array($range, ['7','30','90']) ? intval($range) : ($range === '365' ? 365 : 30);
|
||
|
||
$oi_table = $wpdb->prefix . 'wis_order_items';
|
||
$ord_table = $wpdb->prefix . 'wis_orders';
|
||
$sell_table = $wpdb->prefix . 'wis_sell_log';
|
||
$oi_exists = $wpdb->get_var("SHOW TABLES LIKE '$oi_table'") ? true : false;
|
||
$sell_exists = $wpdb->get_var("SHOW TABLES LIKE '$sell_table'") ? true : false;
|
||
|
||
// ---- TOP-KÄUFE aus wis_order_items (nur status=completed Orders) ----
|
||
$top_buys = [];
|
||
$using_legacy = false;
|
||
if ($oi_exists) {
|
||
$top_buys = $wpdb->get_results("
|
||
SELECT
|
||
oi.item_id,
|
||
oi.item_name,
|
||
oi.item_type,
|
||
SUM(oi.quantity) AS qty,
|
||
SUM(oi.total) AS revenue,
|
||
AVG(oi.price_per_item) AS avg_price,
|
||
COUNT(DISTINCT oi.order_id) AS order_count
|
||
FROM {$oi_table} oi
|
||
INNER JOIN {$ord_table} o ON o.id = oi.order_id AND o.status = 'completed'
|
||
WHERE 1=1 {$date_where_oi}
|
||
GROUP BY oi.item_id, oi.item_name, oi.item_type
|
||
ORDER BY qty DESC
|
||
LIMIT 20
|
||
");
|
||
}
|
||
|
||
// ---- FALLBACK: wis_order_items leer → response-JSON aus wis_orders parsen ----
|
||
if (empty($top_buys)) {
|
||
$using_legacy = true;
|
||
$date_cond = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
|
||
$old_orders = $wpdb->get_results("
|
||
SELECT o.id, o.price, o.response, o.created_at
|
||
FROM {$ord_table} o
|
||
WHERE o.status = 'completed'
|
||
AND o.item_id = 'cart'
|
||
AND o.response IS NOT NULL
|
||
{$date_cond}
|
||
ORDER BY o.created_at DESC
|
||
LIMIT 2000
|
||
");
|
||
|
||
// Parsen und aggregieren
|
||
$agg = []; // agg_key => data
|
||
$item_cache = []; // item_id => db row (cache um DB-Calls zu sparen)
|
||
foreach ($old_orders as $ord) {
|
||
$payload = json_decode($ord->response, true);
|
||
if (!$payload) continue;
|
||
|
||
$items_list = $payload['items'] ?? [];
|
||
$cmds = $payload['commands'] ?? [];
|
||
$order_price = floatval($ord->price);
|
||
|
||
// Gesamtzahl der Positionen für anteilige Preisberechnung
|
||
$total_positions = 0;
|
||
foreach ($items_list as $pi) { $total_positions += intval($pi['amount'] ?? 1); }
|
||
foreach ($cmds as $cmd) { $total_positions += 1; }
|
||
if ($total_positions < 1) $total_positions = 1;
|
||
|
||
// --- Normale Items ---
|
||
foreach ($items_list as $pi) {
|
||
$pid = $pi['id'] ?? '';
|
||
if (!$pid) continue;
|
||
$pqty = intval($pi['amount'] ?? 1);
|
||
|
||
// DB-Lookup mit Cache
|
||
if (!array_key_exists($pid, $item_cache)) {
|
||
$item_cache[$pid] = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id = %s LIMIT 1", $pid
|
||
));
|
||
}
|
||
$db_item = $item_cache[$pid];
|
||
$item_name = $db_item ? $db_item->name : $pid;
|
||
// Aktueller Katalogpreis als primäre Quelle
|
||
$unit_price = $db_item
|
||
? ($db_item->offer_price > 0 ? floatval($db_item->offer_price) : floatval($db_item->price))
|
||
: round($order_price / $total_positions, 2); // Fallback: anteilig
|
||
|
||
if (!isset($agg[$pid])) {
|
||
$agg[$pid] = ['item_id'=>$pid,'item_name'=>$item_name,'item_type'=>'item',
|
||
'qty'=>0,'revenue'=>0,'price_sum'=>0,'cnt'=>0];
|
||
}
|
||
$agg[$pid]['qty'] += $pqty;
|
||
$agg[$pid]['revenue'] += $unit_price * $pqty;
|
||
$agg[$pid]['price_sum']+= $unit_price;
|
||
$agg[$pid]['cnt']++;
|
||
}
|
||
|
||
// --- Commands (fly, rank, fly_abo, plot) ---
|
||
foreach ($cmds as $cmd) {
|
||
$ctype = $cmd['type'] ?? 'item';
|
||
$clabel = $cmd['label'] ?? $ctype;
|
||
$ckey = $ctype . '||' . $clabel;
|
||
|
||
// Preis aus wis_items anhand des Labels oder type-basierten item_id
|
||
$cmd_price = 0;
|
||
if ($ctype === 'fly') {
|
||
// Fly-Item anhand der Dauer identifizieren
|
||
$dur_sec = intval($cmd['duration_sec'] ?? 0);
|
||
$fly_map = [300=>'fly_5min',900=>'fly_15min',1800=>'fly_30min',3600=>'fly_1h',7200=>'fly_2h',10800=>'fly_3h'];
|
||
$fly_id = $fly_map[$dur_sec] ?? null;
|
||
if ($fly_id) {
|
||
if (!array_key_exists($fly_id, $item_cache)) {
|
||
$item_cache[$fly_id] = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id = %s LIMIT 1", $fly_id
|
||
));
|
||
}
|
||
$fi = $item_cache[$fly_id];
|
||
$cmd_price = $fi ? ($fi->offer_price > 0 ? floatval($fi->offer_price) : floatval($fi->price)) : 0;
|
||
}
|
||
} elseif ($ctype === 'rank') {
|
||
// Rank-Item: suche nach rank_{rank_id}* in wis_items
|
||
$rid = preg_replace('/[^a-zA-Z0-9_\-]/', '', $cmd['rank_id'] ?? '');
|
||
if ($rid && !array_key_exists('rank_'.$rid, $item_cache)) {
|
||
$item_cache['rank_'.$rid] = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id LIKE %s LIMIT 1",
|
||
'rank_' . $rid . '%'
|
||
));
|
||
}
|
||
$ri = $rid ? ($item_cache['rank_'.$rid] ?? null) : null;
|
||
$cmd_price = $ri ? ($ri->offer_price > 0 ? floatval($ri->offer_price) : floatval($ri->price)) : 0;
|
||
} elseif (in_array($ctype, ['fly_abo','plot_abo','plot_slots'])) {
|
||
// Abo/Plot: direkt nach type-ähnlicher item_id suchen
|
||
if (!array_key_exists($ctype, $item_cache)) {
|
||
$item_cache[$ctype] = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id LIKE %s LIMIT 1",
|
||
$ctype . '%'
|
||
));
|
||
}
|
||
$ai = $item_cache[$ctype] ?? null;
|
||
$cmd_price = $ai ? ($ai->offer_price > 0 ? floatval($ai->offer_price) : floatval($ai->price)) : 0;
|
||
}
|
||
// Letzter Fallback: anteiliger Orderpreis
|
||
if ($cmd_price <= 0) {
|
||
$cmd_price = round($order_price / $total_positions, 2);
|
||
}
|
||
|
||
if (!isset($agg[$ckey])) {
|
||
$agg[$ckey] = ['item_id'=>$ctype,'item_name'=>$clabel,'item_type'=>$ctype,
|
||
'qty'=>0,'revenue'=>0,'price_sum'=>0,'cnt'=>0];
|
||
}
|
||
$agg[$ckey]['qty'] += 1;
|
||
$agg[$ckey]['revenue'] += $cmd_price;
|
||
$agg[$ckey]['price_sum']+= $cmd_price;
|
||
$agg[$ckey]['cnt']++;
|
||
}
|
||
}
|
||
|
||
// Sortieren nach qty DESC, in top_buys-kompatibles Format bringen
|
||
usort($agg, fn($a,$b) => $b['qty'] - $a['qty']);
|
||
$agg = array_slice($agg, 0, 20);
|
||
foreach ($agg as $a) {
|
||
$obj = new stdClass();
|
||
$obj->item_id = $a['item_id'];
|
||
$obj->item_name = $a['item_name'];
|
||
$obj->item_type = $a['item_type'];
|
||
$obj->qty = $a['qty'];
|
||
$obj->revenue = $a['revenue'];
|
||
$obj->avg_price = $a['cnt'] > 0 ? round($a['price_sum'] / $a['cnt'], 2) : 0;
|
||
$obj->order_count = $a['cnt'];
|
||
$top_buys[] = $obj;
|
||
}
|
||
}
|
||
|
||
// ---- TOP-VERKÄUFE / ANKÄUFE ----
|
||
$top_sells = [];
|
||
if ($sell_exists) {
|
||
$top_sells = $wpdb->get_results("
|
||
SELECT
|
||
s.item_name,
|
||
s.item_id,
|
||
SUM(s.quantity) AS qty,
|
||
SUM(s.total_paid) AS total_paid,
|
||
AVG(s.price_per_item) AS avg_price
|
||
FROM {$sell_table} s
|
||
WHERE 1=1 {$date_where_sell}
|
||
GROUP BY s.item_id, s.item_name
|
||
ORDER BY qty DESC
|
||
LIMIT 20
|
||
");
|
||
}
|
||
|
||
// ---- UMSATZ PRO TAG (nach gewähltem Zeitraum) ----
|
||
$chart_interval = $range === 'all' ? 365 : intval($range);
|
||
$daily_revenue = $wpdb->get_results($wpdb->prepare("
|
||
SELECT DATE(o.created_at) AS day, SUM(o.price) AS revenue, COUNT(*) AS orders
|
||
FROM {$ord_table} o
|
||
WHERE o.status = 'completed'
|
||
AND o.created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
||
GROUP BY DATE(o.created_at)
|
||
ORDER BY day ASC
|
||
", $chart_interval));
|
||
|
||
// ---- KPI-Gesamtzahlen ----
|
||
$buy_qty = $oi_exists ? ($wpdb->get_var("SELECT SUM(oi.quantity) FROM {$oi_table} oi INNER JOIN {$ord_table} o ON o.id=oi.order_id AND o.status='completed' WHERE 1=1 {$date_where_oi}") ?: 0) : 0;
|
||
$buy_revenue = $wpdb->get_var("SELECT SUM(o.price) FROM {$ord_table} o WHERE o.status='completed' {$date_where_ord}") ?: 0;
|
||
$sell_qty = $sell_exists ? ($wpdb->get_var("SELECT SUM(s.quantity) FROM {$sell_table} s WHERE 1=1 {$date_where_sell}") ?: 0) : 0;
|
||
$sell_paid = $sell_exists ? ($wpdb->get_var("SELECT SUM(s.total_paid) FROM {$sell_table} s WHERE 1=1 {$date_where_sell}") ?: 0) : 0;
|
||
|
||
$chart_labels = [];
|
||
$chart_revenue = [];
|
||
foreach ($daily_revenue as $dr) {
|
||
$chart_labels[] = $dr->day;
|
||
$chart_revenue[] = floatval($dr->revenue);
|
||
}
|
||
|
||
?>
|
||
<div class="wrap">
|
||
<h1>📊 Analyse – Kauf & Verkauf</h1>
|
||
|
||
<style>
|
||
.wis-ana-tabs { margin:15px 0 20px; display:flex; gap:8px; flex-wrap:wrap; }
|
||
.wis-ana-tabs a { padding:8px 18px; border-radius:20px; border:2px solid #ddd; text-decoration:none; color:#333; font-weight:600; background:#fff; }
|
||
.wis-ana-tabs a.active { background:#667eea; color:#fff; border-color:#667eea; }
|
||
.wis-ana-kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:15px; margin-bottom:25px; }
|
||
.wis-ana-card { background:#fff; border-radius:10px; padding:18px 20px; border:1px solid #e0e0e0; }
|
||
.wis-ana-card .val { font-size:1.8rem; font-weight:800; color:#667eea; }
|
||
.wis-ana-card .lbl { font-size:0.85rem; color:#666; margin-top:4px; }
|
||
.wis-ana-section { background:#fff; border-radius:10px; padding:20px; border:1px solid #e0e0e0; margin-bottom:20px; }
|
||
.wis-ana-section h2 { margin-top:0; font-size:1.1rem; border-bottom:1px solid #eee; padding-bottom:10px; }
|
||
.wis-ana-table { width:100%; border-collapse:collapse; }
|
||
.wis-ana-table th { background:#f4f6f8; padding:10px 12px; text-align:left; font-size:0.82rem; color:#555; white-space:nowrap; }
|
||
.wis-ana-table td { padding:9px 12px; border-bottom:1px solid #f0f0f0; font-size:0.88rem; vertical-align:middle; }
|
||
.wis-ana-table tr:hover td { background:#fafbff; }
|
||
.wis-bar-wrap { background:#f0f0f0; border-radius:4px; height:8px; width:100%; min-width:60px; }
|
||
.wis-bar-buy { height:8px; border-radius:4px; background:linear-gradient(90deg,#667eea,#764ba2); }
|
||
.wis-bar-sell { height:8px; border-radius:4px; background:linear-gradient(90deg,#28a745,#20c997); }
|
||
.wis-ana-hint { font-size:0.8rem; color:#856404; margin-top:10px; background:#fffbe6; border:1px solid #ffe58f; border-radius:6px; padding:8px 12px; }
|
||
.wis-two-col { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
|
||
.wis-notice-box { background:#fff3cd; border:1px solid #ffc107; border-radius:8px; padding:15px 18px; margin-bottom:20px; color:#856404; }
|
||
.wis-notice-box strong { display:block; margin-bottom:4px; }
|
||
@media(max-width:960px){ .wis-two-col { grid-template-columns:1fr; } }
|
||
.wis-tag { display:inline-block; padding:2px 7px; border-radius:10px; font-size:0.75rem; background:#eef; color:#667; margin-left:4px; }
|
||
</style>
|
||
|
||
<!-- Zeitraum-Tabs -->
|
||
<div class="wis-ana-tabs">
|
||
<?php foreach (['7'=>'7 Tage','30'=>'30 Tage','90'=>'90 Tage','365'=>'1 Jahr','all'=>'Gesamt'] as $v=>$l): ?>
|
||
<a href="<?php echo admin_url('admin.php?page=wis_analyse&range='.$v); ?>"
|
||
class="<?php echo $range===$v?'active':''; ?>"><?php echo $l; ?></a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<?php if (!$oi_exists): ?>
|
||
<div class="wis-notice-box">
|
||
<strong>⚠️ Einzelitem-Tracking noch nicht aktiv</strong>
|
||
Die Tabelle <code>wis_order_items</code> fehlt noch. Bitte das Plugin einmal <strong>deaktivieren und wieder aktivieren</strong>. Ab dann werden alle neuen Käufe item-genau erfasst. Bis dahin wird die Auswertung aus den gespeicherten Bestell-JSONs rekonstruiert (Preise sind Näherungswerte aus dem aktuellen Katalog).
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- KPI-Karten -->
|
||
<div class="wis-ana-kpis">
|
||
<div class="wis-ana-card">
|
||
<div class="val"><?php echo number_format($buy_qty); ?></div>
|
||
<div class="lbl">🛒 Items gekauft<?php echo !$oi_exists ? ' <small>(n/v)</small>' : ''; ?></div>
|
||
</div>
|
||
<div class="wis-ana-card">
|
||
<div class="val"><?php echo number_format($buy_revenue); ?></div>
|
||
<div class="lbl">💰 Einnahmen (<?php echo esc_html($currency); ?>)</div>
|
||
</div>
|
||
<div class="wis-ana-card">
|
||
<div class="val"><?php echo number_format($sell_qty); ?></div>
|
||
<div class="lbl">📤 Ankäufe (Menge)</div>
|
||
</div>
|
||
<div class="wis-ana-card">
|
||
<div class="val"><?php echo number_format($sell_paid); ?></div>
|
||
<div class="lbl">📉 Ausgezahlt (<?php echo esc_html($currency); ?>)</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Umsatz-Chart -->
|
||
<div class="wis-ana-section">
|
||
<h2>📈 Tagesumsatz <?php echo $range === 'all' ? '(Gesamt, max. 365 Tage)' : ('letzte ' . ($range === '365' ? '365 Tage / 1 Jahr' : $range . ' Tage')); ?></h2>
|
||
<?php if (empty($daily_revenue)): ?>
|
||
<p style="color:#999;text-align:center;padding:20px 0;">Keine abgeschlossenen Bestellungen in den letzten 30 Tagen.</p>
|
||
<?php else: ?>
|
||
<canvas id="wis-revenue-chart" height="90"></canvas>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="wis-two-col">
|
||
|
||
<!-- TOP KÄUFE (einzeln) -->
|
||
<div class="wis-ana-section">
|
||
<h2>Top 20 – meistgekaufte Items <small style="font-weight:400;color:#888;">(einzeln<?php echo $using_legacy ? ' · Kompatibilitätsmodus' : ''; ?>)</small></h2>
|
||
<?php if (!$oi_exists && empty($top_buys)): ?>
|
||
<p style="color:#999;">Keine Daten gefunden.</p>
|
||
<?php elseif (empty($top_buys)): ?>
|
||
<p style="color:#999;">Keine abgeschlossenen Bestellungen für diesen Zeitraum.</p>
|
||
<?php else:
|
||
$max_buy = max(array_column($top_buys, 'qty') ?: [1]); ?>
|
||
<table class="wis-ana-table">
|
||
<thead><tr>
|
||
<th>#</th>
|
||
<th>Item</th>
|
||
<th>Ø Preis</th>
|
||
<th>Menge</th>
|
||
<th>Umsatz</th>
|
||
<th style="width:80px;">Trend</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php $r=1; foreach ($top_buys as $row):
|
||
?>
|
||
<tr>
|
||
<td><strong><?php echo $r++; ?></strong></td>
|
||
<td>
|
||
<strong><?php echo esc_html($row->item_name); ?></strong>
|
||
<small style="color:#aaa;display:block;"><?php echo esc_html($row->item_id); ?></small>
|
||
</td>
|
||
<td style="white-space:nowrap;"><?php echo number_format($row->avg_price, 0); ?> <?php echo esc_html($currency); ?></td>
|
||
<td><strong><?php echo number_format($row->qty); ?></strong>
|
||
<small style="color:#aaa;">/ <?php echo number_format($row->order_count); ?> Käufe</small>
|
||
</td>
|
||
<td style="white-space:nowrap;"><?php echo number_format($row->revenue, 0); ?> <?php echo esc_html($currency); ?></td>
|
||
<td><div class="wis-bar-wrap"><div class="wis-bar-buy" style="width:<?php echo min(100,round($row->qty/$max_buy*100)); ?>%"></div></div></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<p class="wis-ana-hint">💡 Viel gekauft + hoher Umsatz → Preis erhöhen. Viel gekauft + niedriger Ø-Preis → evtl. zu günstig.</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- TOP ANKÄUFE -->
|
||
<div class="wis-ana-section">
|
||
<h2>Top 20 – Ankauf durch Spieler <small style="font-weight:400;color:#888;">(einzeln)</small></h2>
|
||
<?php if (!$sell_exists): ?>
|
||
<p style="color:#999;">Ankauf-Tabelle nicht vorhanden. Ankauf-Feature aktivieren.</p>
|
||
<?php elseif (empty($top_sells)): ?>
|
||
<p style="color:#999;">Keine Ankaufdaten für diesen Zeitraum.</p>
|
||
<?php else:
|
||
$max_sell = max(array_column($top_sells, 'qty') ?: [1]); ?>
|
||
<table class="wis-ana-table">
|
||
<thead><tr>
|
||
<th>#</th>
|
||
<th>Item</th>
|
||
<th>Ø Ankaufspreis</th>
|
||
<th>Menge</th>
|
||
<th>Ausgezahlt</th>
|
||
<th style="width:80px;">Trend</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php $r=1; foreach ($top_sells as $row): ?>
|
||
<tr>
|
||
<td><strong><?php echo $r++; ?></strong></td>
|
||
<td>
|
||
<strong><?php echo esc_html($row->item_name); ?></strong>
|
||
<small style="color:#aaa;display:block;"><?php echo esc_html($row->item_id); ?></small>
|
||
</td>
|
||
<td style="white-space:nowrap;"><?php echo number_format($row->avg_price, 2); ?> <?php echo esc_html($currency); ?></td>
|
||
<td><strong><?php echo number_format($row->qty); ?></strong></td>
|
||
<td style="white-space:nowrap;"><?php echo number_format($row->total_paid, 0); ?> <?php echo esc_html($currency); ?></td>
|
||
<td><div class="wis-bar-wrap"><div class="wis-bar-sell" style="width:<?php echo min(100,round($row->qty/$max_sell*100)); ?>%"></div></div></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<p class="wis-ana-hint">💡 Viel verkauft = Spieler farmen dieses Item massenhaft → Ankaufspreis senken oder Tageslimit einführen.</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- .wrap -->
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||
<script>
|
||
(function() {
|
||
var labels = <?php echo json_encode($chart_labels); ?>;
|
||
var data = <?php echo json_encode($chart_revenue); ?>;
|
||
var canvas = document.getElementById('wis-revenue-chart');
|
||
if (!canvas || labels.length === 0) return;
|
||
new Chart(canvas, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [{
|
||
label: 'Einnahmen (<?php echo esc_js($currency); ?>)',
|
||
data: data,
|
||
backgroundColor: 'rgba(102,126,234,0.55)',
|
||
borderColor: '#667eea',
|
||
borderWidth: 2,
|
||
borderRadius: 5,
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
plugins: { legend: { display: false } },
|
||
scales: {
|
||
y: { beginAtZero: true, ticks: { callback: function(v){ return v.toLocaleString('de-DE'); } } },
|
||
x: { ticks: { maxRotation: 45, font: { size: 11 } } }
|
||
}
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
<?php
|
||
}
|
||
|
||
// ===========================================================
|
||
// ANGEBOTE ÜBERSICHT
|
||
// ===========================================================
|
||
|
||
public static function page_angebote() {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$nonce = wp_create_nonce('wis_angebote_nonce');
|
||
|
||
$filter = sanitize_text_field($_GET['filter'] ?? 'all');
|
||
$search = sanitize_text_field($_GET['s'] ?? '');
|
||
|
||
$where = "1=1";
|
||
if ($filter === 'offer') $where .= " AND is_offer = 1 AND is_daily_deal = 0";
|
||
elseif ($filter === 'daily') $where .= " AND is_daily_deal = 1";
|
||
elseif ($filter === 'none') $where .= " AND is_offer = 0 AND is_daily_deal = 0";
|
||
else $where .= " AND (is_offer = 1 OR is_daily_deal = 1)";
|
||
|
||
if ($search !== '') {
|
||
$like = '%' . $wpdb->esc_like($search) . '%';
|
||
$where .= $wpdb->prepare(" AND (name LIKE %s OR item_id LIKE %s)", $like, $like);
|
||
}
|
||
|
||
if ($filter !== 'none') {
|
||
$where .= " AND status = 'publish'";
|
||
}
|
||
|
||
$items = $wpdb->get_results("SELECT * FROM $table WHERE $where ORDER BY is_daily_deal DESC, name ASC");
|
||
$count_offer = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_offer = 1 AND is_daily_deal = 0 AND status='publish'");
|
||
$count_daily = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE is_daily_deal = 1 AND status='publish'");
|
||
$count_all = $count_offer + $count_daily;
|
||
|
||
$img_base = get_option('wis_image_base_url', '');
|
||
$base_url = admin_url('admin.php?page=wis_angebote');
|
||
?>
|
||
<div class="wrap">
|
||
<h1>🔥 Angebote Übersicht</h1>
|
||
<p style="color:#666;">Alle Items die als <strong>Angebot</strong> oder <strong>Daily Deal</strong> markiert sind – mit direkter Bearbeitung.</p>
|
||
|
||
<!-- Filterleiste -->
|
||
<ul class="subsubsub" style="margin-bottom:12px;">
|
||
<?php
|
||
$filters = [
|
||
'all' => "🔥 Alle Angebote <span class='count'>($count_all)</span>",
|
||
'offer' => "🏷️ Nur Angebote <span class='count'>($count_offer)</span>",
|
||
'daily' => "🎁 Daily Deal <span class='count'>($count_daily)</span>",
|
||
'none' => "📦 Ohne Kennzeichnung",
|
||
];
|
||
$i = 0;
|
||
foreach ($filters as $key => $label) {
|
||
$active = ($filter === $key) ? 'current' : '';
|
||
$sep = (++$i < count($filters)) ? ' | ' : '';
|
||
echo "<li><a href='" . esc_url(add_query_arg('filter', $key, $base_url)) . "' class='$active'>$label</a>$sep</li>";
|
||
}
|
||
?>
|
||
</ul>
|
||
|
||
<!-- Suche -->
|
||
<form method="get" action="<?php echo admin_url('admin.php'); ?>" style="margin-bottom:16px; display:flex; gap:8px; align-items:center;">
|
||
<input type="hidden" name="page" value="wis_angebote">
|
||
<input type="hidden" name="filter" value="<?php echo esc_attr($filter); ?>">
|
||
<input type="search" name="s" value="<?php echo esc_attr($search); ?>" placeholder="Name oder Item-ID …" class="regular-text">
|
||
<button type="submit" class="button">Suchen</button>
|
||
<?php if ($search): ?>
|
||
<a href="<?php echo esc_url(add_query_arg('filter', $filter, $base_url)); ?>" class="button button-secondary">✕ Zurücksetzen</a>
|
||
<?php endif; ?>
|
||
</form>
|
||
|
||
<?php if (empty($items)): ?>
|
||
<div style="background:#f8f9fa; border:1px dashed #ccd; border-radius:8px; padding:30px; text-align:center; color:#888; margin-top:10px;">
|
||
<span style="font-size:2em;">📭</span><br>
|
||
Keine Items gefunden<?php echo $search ? ' für „' . esc_html($search) . '"' : ''; ?>.
|
||
</div>
|
||
<?php else: ?>
|
||
|
||
<table class="wp-list-table widefat fixed striped" id="wis-angebote-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:55px;">Bild</th>
|
||
<th>Name</th>
|
||
<th style="width:160px;">Item-ID</th>
|
||
<th style="width:110px;">Normalpreis</th>
|
||
<th style="width:150px;">Angebotspreis</th>
|
||
<th style="width:100px;">Rabatt</th>
|
||
<th style="width:110px;">🏷️ Angebot</th>
|
||
<th style="width:120px;">🎁 Daily Deal</th>
|
||
<th style="width:80px;">Aktion</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($items as $item):
|
||
$img_url = !empty($item->custom_image_url)
|
||
? esc_url($item->custom_image_url)
|
||
: $img_base . str_replace(':', '_', $item->item_id) . '.png';
|
||
|
||
$normal_price = intval($item->price);
|
||
$offer_price = intval($item->offer_price);
|
||
$discount_pct = ($normal_price > 0 && $offer_price > 0 && $offer_price < $normal_price)
|
||
? round((1 - $offer_price / $normal_price) * 100) : 0;
|
||
|
||
$is_daily = intval($item->is_daily_deal);
|
||
$is_offer = intval($item->is_offer);
|
||
$row_bg = $is_daily ? '#fffbe6' : ($is_offer ? '#f0fff4' : '');
|
||
?>
|
||
<tr id="wis-ang-row-<?php echo $item->id; ?>" style="<?php echo $row_bg ? "background:$row_bg;" : ''; ?>">
|
||
<td style="text-align:center;">
|
||
<img src="<?php echo $img_url; ?>" alt="<?php echo esc_attr($item->name); ?>"
|
||
style="width:40px;height:40px;object-fit:contain;background:#2d2d2d;border-radius:4px;padding:2px;"
|
||
onerror="this.style.opacity='0.2'">
|
||
</td>
|
||
<td>
|
||
<strong><?php echo esc_html($item->name); ?></strong>
|
||
<?php if ($is_daily) echo '<br><span style="font-size:11px;color:#6f42c1;font-weight:bold;">🎁 DAILY DEAL</span>'; ?>
|
||
<?php if ($is_offer && !$is_daily) echo '<br><span style="font-size:11px;color:#dc3545;font-weight:bold;">🔥 ANGEBOT</span>'; ?>
|
||
</td>
|
||
<td><code style="font-size:11px;"><?php echo esc_html($item->item_id); ?></code></td>
|
||
<td><?php echo number_format($normal_price); ?> <small><?php echo esc_html($currency); ?></small></td>
|
||
<td>
|
||
<div style="display:flex;align-items:center;gap:4px;">
|
||
<input type="number" class="wis-ang-price-input small-text"
|
||
data-id="<?php echo $item->id; ?>"
|
||
value="<?php echo $offer_price; ?>"
|
||
style="width:70px;text-align:right;" min="0">
|
||
<small><?php echo esc_html($currency); ?></small>
|
||
<button class="button button-small wis-ang-save-price" data-id="<?php echo $item->id; ?>"
|
||
title="Angebotspreis speichern" style="padding:0 6px;">💾</button>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<?php if ($discount_pct > 0): ?>
|
||
<span style="background:#28a745;color:#fff;border-radius:4px;padding:2px 7px;font-size:12px;font-weight:bold;">
|
||
−<?php echo $discount_pct; ?>%
|
||
</span>
|
||
<?php else: ?>
|
||
<span style="color:#bbb;">—</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td style="text-align:center;">
|
||
<label class="wis-ang-toggle" title="Angebot umschalten">
|
||
<input type="checkbox" class="wis-ang-flag"
|
||
data-id="<?php echo $item->id; ?>" data-field="is_offer"
|
||
<?php checked($is_offer, 1); ?>>
|
||
<span class="wis-ang-slider"></span>
|
||
</label>
|
||
</td>
|
||
<td style="text-align:center;">
|
||
<label class="wis-ang-toggle" title="Als Daily Deal setzen">
|
||
<input type="checkbox" class="wis-ang-flag"
|
||
data-id="<?php echo $item->id; ?>" data-field="is_daily_deal"
|
||
<?php checked($is_daily, 1); ?>>
|
||
<span class="wis-ang-slider"></span>
|
||
</label>
|
||
</td>
|
||
<td>
|
||
<a href="<?php echo esc_url(admin_url('admin.php?page=wis_items&edit=' . $item->id)); ?>"
|
||
class="button button-small" title="Item bearbeiten">✏️</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<p style="color:#888;font-size:12px;margin-top:8px;"><?php echo count($items); ?> Item(s) gefunden.</p>
|
||
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<style>
|
||
.wis-ang-toggle { position:relative; display:inline-block; width:44px; height:24px; }
|
||
.wis-ang-toggle input { opacity:0; width:0; height:0; }
|
||
.wis-ang-slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background:#ccc; transition:.3s; border-radius:24px; }
|
||
.wis-ang-slider:before { position:absolute; content:""; height:18px; width:18px; left:3px; bottom:3px; background:#fff; transition:.3s; border-radius:50%; }
|
||
.wis-ang-toggle input:checked + .wis-ang-slider { background:#28a745; }
|
||
.wis-ang-toggle input:checked + .wis-ang-slider:before { transform:translateX(20px); }
|
||
.wis-ang-toggle input:disabled + .wis-ang-slider { opacity:.5; cursor:not-allowed; }
|
||
#wis-angebote-table td, #wis-angebote-table th { vertical-align:middle; }
|
||
</style>
|
||
|
||
<script>
|
||
jQuery(function($) {
|
||
var nonce = <?php echo json_encode($nonce); ?>;
|
||
|
||
// Flag Toggle (is_offer / is_daily_deal)
|
||
$('.wis-ang-flag').on('change', function() {
|
||
var $cb = $(this);
|
||
var id = $cb.data('id');
|
||
var field = $cb.data('field');
|
||
$cb.prop('disabled', true);
|
||
|
||
$.post(ajaxurl, { action:'wis_angebote_toggle', nonce:nonce, id:id, field:field }, function(res) {
|
||
if (res.success) {
|
||
if (field === 'is_daily_deal' && res.data.new_val === 1) {
|
||
$('.wis-ang-flag[data-field="is_daily_deal"]').not($cb).prop('checked', false);
|
||
}
|
||
var $row = $('#wis-ang-row-' + id);
|
||
if (res.data.new_val === 1) {
|
||
$row.css('background', field === 'is_daily_deal' ? '#fffbe6' : '#f0fff4');
|
||
} else {
|
||
$row.css('background', '');
|
||
}
|
||
} else {
|
||
alert('Fehler: ' + (res.data || 'Unbekannt'));
|
||
$cb.prop('checked', !$cb.prop('checked'));
|
||
}
|
||
$cb.prop('disabled', false);
|
||
}).fail(function() {
|
||
alert('Verbindungsfehler.');
|
||
$cb.prop('checked', !$cb.prop('checked'));
|
||
$cb.prop('disabled', false);
|
||
});
|
||
});
|
||
|
||
// Angebotspreis speichern
|
||
$('.wis-ang-save-price').on('click', function() {
|
||
var $btn = $(this);
|
||
var id = $btn.data('id');
|
||
var price = $('#wis-ang-row-' + id).find('.wis-ang-price-input').val();
|
||
$btn.prop('disabled', true).text('…');
|
||
|
||
$.post(ajaxurl, { action:'wis_angebote_save_price', nonce:nonce, id:id, offer_price:price }, function(res) {
|
||
if (res.success) {
|
||
$btn.text('✅');
|
||
setTimeout(function() { $btn.prop('disabled', false).text('💾'); }, 1500);
|
||
} else {
|
||
alert('Fehler: ' + (res.data || 'Unbekannt'));
|
||
$btn.prop('disabled', false).text('💾');
|
||
}
|
||
}).fail(function() {
|
||
alert('Verbindungsfehler.');
|
||
$btn.prop('disabled', false).text('💾');
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
<?php
|
||
}
|
||
}
|
||
|
||
|
||
class WIS_API {
|
||
public static function register_routes() {
|
||
register_rest_route('wis/v1', '/import_json', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'import_json'],
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('wis/v1', '/order', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'create_order'],
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('wis/v1', '/validate_coupon', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'validate_coupon'],
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('wis/v1', '/shop_items', [
|
||
'methods' => 'GET',
|
||
'callback' => [self::class, 'get_shop_items'],
|
||
'permission_callback' => '__return_true',
|
||
]);
|
||
register_rest_route('wis/v1', '/pending_orders', [
|
||
'methods' => 'GET',
|
||
'callback' => [self::class, 'get_pending_orders'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/execute_order', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'execute_order'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/complete_order', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'complete_order'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/cancel_order', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'cancel_order'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
// Gift-System Endpunkte (ab v6.5-gift)
|
||
register_rest_route('wis/v1', '/pending_gifts', [
|
||
'methods' => 'GET',
|
||
'callback' => [self::class, 'get_pending_gifts'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/gift_accept', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'gift_accept'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/gift_decline', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'gift_decline'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/pending_offline', [
|
||
'methods' => 'GET',
|
||
'callback' => [self::class, 'get_pending_offline'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/my_coupons', [
|
||
'methods' => 'GET',
|
||
'callback' => [self::class, 'get_my_coupons'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/orders_history', [
|
||
'methods' => 'GET',
|
||
'callback' => [self::class, 'get_orders_history'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
|
||
// Ankauf-Endpunkte (ab v6.5)
|
||
register_rest_route('wis/v1', '/sell_items', [
|
||
'methods' => 'GET',
|
||
'callback' => [self::class, 'get_sell_items'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
register_rest_route('wis/v1', '/sell_item', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'process_sell'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
|
||
// ── Item-Abo Endpoints ────────────────────────────────────────────
|
||
|
||
register_rest_route('wis/v1', '/item_abo_status', [
|
||
'methods' => 'GET',
|
||
'callback' => [self::class, 'item_abo_status'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
|
||
register_rest_route('wis/v1', '/item_abo_cancel', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'item_abo_cancel'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
|
||
register_rest_route('wis/v1', '/trigger_abo_delivery', [
|
||
'methods' => 'POST',
|
||
'callback' => [self::class, 'trigger_abo_delivery'],
|
||
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
|
||
]);
|
||
}
|
||
|
||
|
||
// =========================================================
|
||
// ANKAUF – Endpunkte (ab v6.5)
|
||
// =========================================================
|
||
|
||
/**
|
||
* GET /wis/v1/sell_items?server=<slug>
|
||
* Liefert alle Items die auf dem angegebenen Server ankaufbar sind.
|
||
*/
|
||
public static function get_sell_items($request) {
|
||
global $wpdb;
|
||
$server = sanitize_text_field($request->get_param('server') ?? '');
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
|
||
$items = $wpdb->get_results(
|
||
"SELECT id, item_id, name, price, sell_price_mode, sell_price_value
|
||
FROM $table
|
||
WHERE status = 'publish' AND sell_enabled = 1"
|
||
);
|
||
|
||
$result = [];
|
||
foreach ($items as $item) {
|
||
// Ankaufspreis berechnen
|
||
$sell_price = self::calc_sell_price(
|
||
(int) $item->price,
|
||
$item->sell_price_mode,
|
||
(int) $item->sell_price_value
|
||
);
|
||
if ($sell_price <= 0) continue;
|
||
|
||
// Server-Filter
|
||
if (!empty($server)) {
|
||
$servers = $wpdb->get_var($wpdb->prepare(
|
||
"SELECT servers FROM $table WHERE id = %d", $item->id
|
||
));
|
||
$srv_list = json_decode($servers, true) ?: [];
|
||
if (!empty($srv_list) && !in_array(strtolower($server), array_map('strtolower', $srv_list))) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
$result[] = [
|
||
'item_id' => $item->item_id,
|
||
'name' => $item->name,
|
||
'buy_price' => (int) $item->price,
|
||
'sell_price' => $sell_price,
|
||
];
|
||
}
|
||
|
||
return new WP_REST_Response([
|
||
'items' => $result,
|
||
'currency' => $currency,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST /wis/v1/sell_item
|
||
* Body: { "player": "Steve", "server": "survival", "item_id": "minecraft:diamond", "quantity": 5 }
|
||
* Antwortet mit dem Betrag der gutgeschrieben werden soll.
|
||
*/
|
||
// =========================================================
|
||
// ITEM-ABO – REST-Endpunkte
|
||
// =========================================================
|
||
|
||
/**
|
||
* GET /wis/v1/item_abo_status?player=<name>
|
||
* Gibt alle aktiven Item-Abos eines Spielers zurück.
|
||
*/
|
||
public static function item_abo_status($request) {
|
||
global $wpdb;
|
||
$player = sanitize_text_field($request->get_param('player') ?? '');
|
||
if (empty($player)) {
|
||
return new WP_REST_Response(['abos' => []], 200);
|
||
}
|
||
|
||
WIS_Activator::create_item_abo_subs_table();
|
||
$table = $wpdb->prefix . 'wis_item_abo_subs';
|
||
|
||
// id muss mit zurückgegeben werden, damit der Spigot-Client
|
||
// beim Kündigen gezielt eine einzelne Abo-Zeile ansprechen kann.
|
||
$rows = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT id, label, item_id, daily_qty, cancelled,
|
||
DATE_FORMAT(expires_at, '%%d.%%m.%%Y') AS expires_at
|
||
FROM {$table}
|
||
WHERE player_name = %s
|
||
AND status = 'active'
|
||
AND expires_at > NOW()
|
||
ORDER BY created_at ASC",
|
||
$player
|
||
), ARRAY_A);
|
||
|
||
foreach ($rows as &$row) {
|
||
$row['id'] = (int) $row['id'];
|
||
$row['daily_qty'] = (int) $row['daily_qty'];
|
||
$row['cancelled'] = (bool) $row['cancelled'];
|
||
}
|
||
unset($row);
|
||
|
||
return new WP_REST_Response(['abos' => $rows ?: []], 200);
|
||
}
|
||
|
||
/**
|
||
* POST /wis/v1/item_abo_cancel
|
||
* Body: {"player":"<name>", "abo_id":<int>}
|
||
* Kündigt genau das Item-Abo mit der angegebenen ID – nur wenn es dem Spieler gehört.
|
||
* Gibt 200 bei Erfolg, 403 bei falschem Besitzer, 404 wenn nicht gefunden.
|
||
*/
|
||
public static function item_abo_cancel($request) {
|
||
global $wpdb;
|
||
$body = json_decode($request->get_body(), true);
|
||
$player = sanitize_text_field($body['player'] ?? '');
|
||
$abo_id = intval($body['abo_id'] ?? 0);
|
||
|
||
if (empty($player) || $abo_id <= 0) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Ungültige Parameter'], 400);
|
||
}
|
||
|
||
WIS_Activator::create_item_abo_subs_table();
|
||
$table = $wpdb->prefix . 'wis_item_abo_subs';
|
||
|
||
// Sicherheits-Check: Abo muss dem Spieler gehören und aktiv sein
|
||
$existing = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT id, label FROM {$table}
|
||
WHERE id = %d AND player_name = %s AND status = 'active' AND cancelled = 0",
|
||
$abo_id, $player
|
||
));
|
||
|
||
if (!$existing) {
|
||
// Unterscheide: existiert gar nicht vs. gehört anderem Spieler
|
||
$any = $wpdb->get_var($wpdb->prepare(
|
||
"SELECT id FROM {$table} WHERE id = %d", $abo_id
|
||
));
|
||
if ($any) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Nicht dein Abo'], 403);
|
||
}
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Abo nicht gefunden oder bereits gekündigt'], 404);
|
||
}
|
||
|
||
$wpdb->update(
|
||
$table,
|
||
['cancelled' => 1, 'cancelled_at' => current_time('mysql')],
|
||
['id' => $abo_id]
|
||
);
|
||
|
||
error_log("[WIS] Item-Abo #{$abo_id} ({$existing->label}) gekündigt von: {$player}");
|
||
return new WP_REST_Response([
|
||
'success' => true,
|
||
'label' => $existing->label,
|
||
'message' => "Item-Abo '{$existing->label}' für {$player} gekündigt",
|
||
], 200);
|
||
}
|
||
|
||
/**
|
||
* POST /wis/v1/trigger_abo_delivery
|
||
* Body: {"player":"<name>", "server":"<server>"}
|
||
*
|
||
* Prüft ob der Spieler aktive Item-Abos hat, die heute noch nicht beliefert wurden,
|
||
* und legt für jedes fehlende Abo sofort eine pending-Order an.
|
||
* Wird beim Join und beim Login aufgerufen – als Fallback falls der WP-Cron nicht
|
||
* gelaufen ist (z.B. kein Traffic auf der WordPress-Seite).
|
||
*
|
||
* Gibt zurück: {"delivered": <anzahl>} – 0 wenn alles bereits geliefert wurde.
|
||
*/
|
||
public static function trigger_abo_delivery($request) {
|
||
global $wpdb;
|
||
$body = json_decode($request->get_body(), true);
|
||
$player = sanitize_text_field($body['player'] ?? '');
|
||
$server = sanitize_text_field($body['server'] ?? '');
|
||
|
||
if (empty($player)) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Kein Spielername'], 400);
|
||
}
|
||
|
||
WIS_Activator::create_item_abo_subs_table();
|
||
$subs_table = $wpdb->prefix . 'wis_item_abo_subs';
|
||
$orders_table = $wpdb->prefix . 'wis_orders';
|
||
$today = date('Y-m-d');
|
||
|
||
// Aktive Abos dieses Spielers die heute noch nicht beliefert wurden
|
||
$conditions = "player_name = %s AND status = 'active' AND cancelled = 0"
|
||
. " AND expires_at > NOW()"
|
||
. " AND (last_delivered IS NULL OR last_delivered < %s)";
|
||
|
||
// Server-Filter nur wenn angegeben
|
||
$args = [$player, $today];
|
||
if (!empty($server)) {
|
||
$conditions .= " AND server = %s";
|
||
$args[] = $server;
|
||
}
|
||
|
||
$pending_subs = $wpdb->get_results(
|
||
$wpdb->prepare("SELECT * FROM {$subs_table} WHERE {$conditions}", ...$args)
|
||
);
|
||
|
||
$delivered = 0;
|
||
foreach ($pending_subs as $sub) {
|
||
$payload = json_encode([
|
||
'items' => [['id' => $sub->item_id, 'amount' => intval($sub->daily_qty)]],
|
||
'commands' => [],
|
||
'abo_delivery' => true,
|
||
]);
|
||
|
||
$wpdb->insert($orders_table, [
|
||
'player_name' => $sub->player_name,
|
||
'server' => $sub->server,
|
||
'item_id' => 'item_abo_delivery',
|
||
'item_title' => '📦 Abo-Lieferung: ' . $sub->label . ' ×' . intval($sub->daily_qty),
|
||
'price' => 0,
|
||
'quantity' => intval($sub->daily_qty),
|
||
'status' => 'pending',
|
||
'response' => $payload,
|
||
]);
|
||
|
||
$wpdb->update($subs_table, ['last_delivered' => $today], ['id' => $sub->id]);
|
||
$delivered++;
|
||
}
|
||
|
||
return new WP_REST_Response(['success' => true, 'delivered' => $delivered], 200);
|
||
}
|
||
|
||
public static function process_sell($request) {
|
||
global $wpdb;
|
||
$player = sanitize_text_field($request->get_param('player') ?? '');
|
||
$server = sanitize_text_field($request->get_param('server') ?? '');
|
||
$item_id = sanitize_text_field($request->get_param('item_id') ?? '');
|
||
$quantity = max(1, intval($request->get_param('quantity') ?? 1));
|
||
|
||
if (!$player || !$item_id) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Fehlende Parameter'], 400);
|
||
}
|
||
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
|
||
// Item suchen (case-insensitive, mit/ohne minecraft: prefix)
|
||
$clean_id = strtolower(str_replace('minecraft:', '', $item_id));
|
||
$row = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT id, item_id, name, price, sell_enabled, sell_price_mode, sell_price_value, servers
|
||
FROM $table
|
||
WHERE sell_enabled = 1
|
||
AND (LOWER(item_id) = %s OR LOWER(REPLACE(item_id,'minecraft:','')) = %s)
|
||
LIMIT 1",
|
||
strtolower($item_id), $clean_id
|
||
));
|
||
|
||
if (!$row) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Item nicht ankaufbar'], 404);
|
||
}
|
||
|
||
$sell_price = self::calc_sell_price(
|
||
(int) $row->price,
|
||
$row->sell_price_mode,
|
||
(int) $row->sell_price_value
|
||
);
|
||
|
||
if ($sell_price <= 0) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Ankaufspreis ist 0'], 422);
|
||
}
|
||
|
||
// Tageslimit prüfen
|
||
$daily_limit = intval($row->daily_sell_limit ?? 0);
|
||
if ($daily_limit > 0) {
|
||
$sold_today = (int) $wpdb->get_var($wpdb->prepare(
|
||
"SELECT COALESCE(SUM(quantity),0) FROM {$wpdb->prefix}wis_sell_log
|
||
WHERE player_name = %s AND item_id = %s
|
||
AND DATE(sold_at) = CURDATE()",
|
||
$player, $row->item_id
|
||
));
|
||
if ($sold_today + $quantity > $daily_limit) {
|
||
$remaining = max(0, $daily_limit - $sold_today);
|
||
return new WP_REST_Response([
|
||
'success' => false,
|
||
'message' => "Tageslimit erreicht. Du kannst heute noch {$remaining}x dieses Item verkaufen.",
|
||
], 429);
|
||
}
|
||
}
|
||
|
||
$total = round($sell_price * $quantity, 2);
|
||
|
||
// Sell-Log schreiben
|
||
$wpdb->insert($wpdb->prefix . 'wis_sell_log', [
|
||
'player_name' => $player,
|
||
'server' => $server,
|
||
'item_id' => $row->item_id,
|
||
'item_name' => $row->name,
|
||
'quantity' => $quantity,
|
||
'price_per_item' => $sell_price,
|
||
'total_paid' => $total,
|
||
]);
|
||
|
||
return new WP_REST_Response([
|
||
'success' => true,
|
||
'item_id' => $row->item_id,
|
||
'item_name' => $row->name,
|
||
'quantity' => $quantity,
|
||
'price_per_item' => $sell_price,
|
||
'total' => $total,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Berechnet den Ankaufspreis aus Verkaufspreis + Modus.
|
||
* mode = "percent" → value ist ein Prozentwert des VK-Preises (z.B. 80 = 80 %)
|
||
* mode = "fixed" → value ist ein absoluter Festpreis
|
||
* mode = "minus" → value ist ein absoluter Abzug vom VK-Preis
|
||
*/
|
||
private static function calc_sell_price(int $buy_price, string $mode, int $value): float {
|
||
switch ($mode) {
|
||
case 'fixed':
|
||
return max(0, $value);
|
||
case 'minus':
|
||
return max(0, $buy_price - $value);
|
||
case 'percent':
|
||
default:
|
||
return max(0, round($buy_price * $value / 100, 2));
|
||
}
|
||
}
|
||
|
||
public static function get_shop_items($request) {
|
||
$page = max(1, intval($request->get_param('page') ?? 1));
|
||
$per_page_param = $request->get_param('per_page');
|
||
// -1 = "all", allowed values: 25, 50, 100, -1
|
||
$allowed_per_page = [24, 25, 50, 100, -1];
|
||
if ($per_page_param !== null) {
|
||
$per_page_param = intval($per_page_param);
|
||
$per_page = in_array($per_page_param, $allowed_per_page, true) ? $per_page_param : 24;
|
||
} else {
|
||
$per_page = intval(get_option('wis_default_per_page', 25));
|
||
if (!in_array($per_page, $allowed_per_page, true)) $per_page = 24;
|
||
}
|
||
$category = sanitize_text_field($request->get_param('category') ?? '');
|
||
$search = sanitize_text_field($request->get_param('search') ?? '');
|
||
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_items';
|
||
$where_parts = ["status = 'publish'"];
|
||
|
||
if (!empty($category)) {
|
||
$search_pattern = '%"' . $category . '"%';
|
||
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
|
||
}
|
||
if (!empty($search)) {
|
||
$search_like = '%' . $wpdb->esc_like($search) . '%';
|
||
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
|
||
}
|
||
|
||
$where_sql = implode(" AND ", $where_parts);
|
||
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql");
|
||
if ($per_page === -1) {
|
||
// Alle Items auf einmal
|
||
$items = $wpdb->get_results("SELECT * FROM $table WHERE $where_sql ORDER BY name ASC");
|
||
$effective_per_page = $total > 0 ? $total : 1;
|
||
} else {
|
||
$offset = ($page - 1) * $per_page;
|
||
$items = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM $table WHERE $where_sql ORDER BY name ASC LIMIT %d OFFSET %d",
|
||
$per_page, $offset
|
||
));
|
||
$effective_per_page = $per_page;
|
||
}
|
||
|
||
$img_base = get_option('wis_image_base_url', '');
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
$result = [];
|
||
foreach ($items as $item) {
|
||
$result[] = [
|
||
'id' => $item->id,
|
||
'item_id' => $item->item_id,
|
||
'name' => $item->name,
|
||
'description' => $item->description,
|
||
'price' => intval($item->price),
|
||
'offer_price' => intval($item->offer_price),
|
||
'is_offer' => (bool) $item->is_offer,
|
||
'is_daily_deal' => (bool) $item->is_daily_deal,
|
||
'servers' => json_decode($item->servers, true) ?: [],
|
||
'categories' => json_decode($item->categories, true) ?: [],
|
||
'image' => WIS_DB::get_item_image($item),
|
||
'has_custom_image' => !empty($item->custom_image_url),
|
||
'custom_command' => $item->custom_command ?? null,
|
||
];
|
||
}
|
||
|
||
// Fly-Items nach Dauer sortieren: 5min→15min→30min→1h→2h→3h
|
||
$fly_order = ['fly_5min'=>1,'fly_15min'=>2,'fly_30min'=>3,'fly_1h'=>4,'fly_2h'=>5,'fly_3h'=>6];
|
||
usort($result, function($a, $b) use ($fly_order) {
|
||
$ap = isset($fly_order[$a['item_id']]) ? $fly_order[$a['item_id']] : 999;
|
||
$bp = isset($fly_order[$b['item_id']]) ? $fly_order[$b['item_id']] : 999;
|
||
if ($ap !== 999 || $bp !== 999) return $ap - $bp;
|
||
return strcmp($a['name'], $b['name']);
|
||
});
|
||
|
||
return new WP_REST_Response([
|
||
'items' => $result,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'per_page' => $per_page,
|
||
'total_pages' => $per_page === -1 ? 1 : max(1, (int) ceil($total / $per_page)),
|
||
'currency' => $currency,
|
||
]);
|
||
}
|
||
|
||
public static function get_pending_orders($request) {
|
||
global $wpdb;
|
||
$player = sanitize_text_field($request->get_param('player'));
|
||
|
||
if (!$player) {
|
||
return new WP_REST_Response(['orders' => []]);
|
||
}
|
||
|
||
$table = $wpdb->prefix . 'wis_orders';
|
||
|
||
// Gift-Orders werden genauso wie normale Orders für Spieler A gepollt.
|
||
// Spieler A muss zuerst ingame bestätigen (Geld abbuchen), erst dann
|
||
// bekommt Spieler B die Gift-Anfrage.
|
||
$results = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM $table
|
||
WHERE player_name = %s AND status = 'pending'
|
||
ORDER BY created_at ASC LIMIT 5",
|
||
$player
|
||
));
|
||
|
||
if (!empty($results)) {
|
||
$ids = implode(',', array_map(fn($o) => intval($o->id), $results));
|
||
$wpdb->query("UPDATE $table SET status = 'claimed' WHERE id IN ($ids) AND status = 'pending'");
|
||
}
|
||
|
||
return new WP_REST_Response(['orders' => $results]);
|
||
}
|
||
|
||
public static function get_pending_offline($request) {
|
||
if (get_option('wis_offline_queue_enabled', '0') !== '1') {
|
||
return new WP_REST_Response(['orders' => [], 'message' => 'Offline-Queue deaktiviert']);
|
||
}
|
||
|
||
global $wpdb;
|
||
$player = sanitize_text_field($request->get_param('player'));
|
||
|
||
if (!$player) {
|
||
return new WP_REST_Response(['orders' => []]);
|
||
}
|
||
|
||
$table = $wpdb->prefix . 'wis_orders';
|
||
$results = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM $table
|
||
WHERE player_name = %s AND status = 'pending'
|
||
ORDER BY created_at ASC LIMIT 10",
|
||
$player
|
||
));
|
||
|
||
if (!empty($results)) {
|
||
$ids = implode(',', array_map(fn($o) => intval($o->id), $results));
|
||
$wpdb->query("UPDATE $table SET status = 'claimed' WHERE id IN ($ids) AND status = 'pending'");
|
||
}
|
||
|
||
return new WP_REST_Response(['orders' => $results]);
|
||
}
|
||
|
||
public static function get_orders_history($request) {
|
||
global $wpdb;
|
||
$player = sanitize_text_field($request->get_param('player'));
|
||
|
||
if (!$player) {
|
||
return new WP_REST_Response(['orders' => []]);
|
||
}
|
||
|
||
$results = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT id, item_title, price, status, created_at
|
||
FROM {$wpdb->prefix}wis_orders
|
||
WHERE player_name = %s
|
||
ORDER BY created_at DESC LIMIT 10",
|
||
$player
|
||
));
|
||
|
||
return new WP_REST_Response(['orders' => $results]);
|
||
}
|
||
|
||
public static function execute_order($request) {
|
||
global $wpdb;
|
||
$data = $request->get_json_params();
|
||
$id = intval($data['id'] ?? 0);
|
||
|
||
if (!$id) return new WP_REST_Response(['success' => false], 400);
|
||
|
||
$wpdb->query($wpdb->prepare(
|
||
"UPDATE {$wpdb->prefix}wis_orders SET status = 'processing'
|
||
WHERE id = %d AND status IN ('pending','claimed')",
|
||
$id
|
||
));
|
||
|
||
return new WP_REST_Response(['success' => true]);
|
||
}
|
||
|
||
public static function complete_order($request) {
|
||
global $wpdb;
|
||
$data = $request->get_json_params();
|
||
$id = intval($data['id'] ?? 0);
|
||
|
||
if (!$id) return new WP_REST_Response(['success' => false], 400);
|
||
|
||
// Vor dem Update: Order-Payload lesen, um gift_card_codes zu extrahieren
|
||
$order = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT response FROM {$wpdb->prefix}wis_orders WHERE id = %d",
|
||
$id
|
||
));
|
||
|
||
$wpdb->update(
|
||
$wpdb->prefix . 'wis_orders',
|
||
['status' => 'completed'],
|
||
['id' => $id]
|
||
);
|
||
|
||
// Gift-Card-Codes aus dem gespeicherten Payload sammeln und zurückgeben
|
||
$gift_card_codes = [];
|
||
if ($order && !empty($order->response)) {
|
||
$payload = json_decode($order->response, true);
|
||
if (isset($payload['commands']) && is_array($payload['commands'])) {
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
foreach ($payload['commands'] as $cmd) {
|
||
if (($cmd['type'] ?? '') === 'gift_card') {
|
||
$gift_card_codes[] = [
|
||
'code' => $cmd['code'],
|
||
'amount' => $cmd['amount'],
|
||
'label' => $cmd['label'] ?? '',
|
||
];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return new WP_REST_Response(['success' => true, 'gift_card_codes' => $gift_card_codes]);
|
||
}
|
||
|
||
/**
|
||
* GET /wis/v1/my_coupons?player=Spielername
|
||
* Gibt alle ungenutzten Gift-Card-Gutscheine zurück, die ein Spieler gekauft hat.
|
||
* Verknüpfung: wis_orders.response enthält die gift_card codes → wis_coupons.used_count = 0
|
||
*/
|
||
public static function get_my_coupons($request) {
|
||
global $wpdb;
|
||
$player = sanitize_text_field($request->get_param('player') ?? '');
|
||
if (!$player) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Kein Spielername angegeben'], 400);
|
||
}
|
||
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
// Alle abgeschlossenen Orders des Spielers mit gift_card commands holen
|
||
$orders = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT response FROM {$wpdb->prefix}wis_orders
|
||
WHERE player_name = %s
|
||
AND status = 'completed'
|
||
AND response LIKE '%gift_card%'
|
||
ORDER BY created_at DESC",
|
||
$player
|
||
));
|
||
|
||
$result = [];
|
||
$seen = [];
|
||
|
||
foreach ($orders as $order) {
|
||
if (empty($order->response)) continue;
|
||
$payload = json_decode($order->response, true);
|
||
if (!isset($payload['commands'])) continue;
|
||
foreach ($payload['commands'] as $cmd) {
|
||
if (($cmd['type'] ?? '') !== 'gift_card') continue;
|
||
$code = $cmd['code'] ?? '';
|
||
if (!$code || isset($seen[$code])) continue;
|
||
$seen[$code] = true;
|
||
|
||
// Prüfen ob der Coupon noch ungenutzt ist
|
||
$coupon = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT code, value, used_count, usage_limit, expiry
|
||
FROM {$wpdb->prefix}wis_coupons
|
||
WHERE code = %s",
|
||
$code
|
||
));
|
||
if (!$coupon) continue;
|
||
if ($coupon->used_count >= $coupon->usage_limit) continue; // bereits eingelöst
|
||
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) continue; // abgelaufen
|
||
|
||
$result[] = [
|
||
'code' => $coupon->code,
|
||
'value' => intval($coupon->value),
|
||
'currency' => $currency,
|
||
'label' => ($cmd['label'] ?? ''),
|
||
'expiry' => $coupon->expiry ?: null,
|
||
];
|
||
}
|
||
}
|
||
|
||
return new WP_REST_Response([
|
||
'success' => true,
|
||
'player' => $player,
|
||
'coupons' => $result,
|
||
]);
|
||
}
|
||
|
||
public static function cancel_order($request) {
|
||
global $wpdb;
|
||
$data = $request->get_json_params();
|
||
$id = intval($data['id'] ?? 0);
|
||
|
||
if (!$id) return new WP_REST_Response(['success' => false], 400);
|
||
|
||
$wpdb->update(
|
||
$wpdb->prefix . 'wis_orders',
|
||
['status' => 'cancelled'],
|
||
['id' => $id]
|
||
);
|
||
|
||
return new WP_REST_Response(['success' => true]);
|
||
}
|
||
|
||
// ── Gift-System Endpoints ─────────────────────────────────────────────
|
||
|
||
/**
|
||
* GET /wis/v1/pending_gifts?recipient={player}
|
||
* Gibt ausstehende Geschenk-Orders zurück, die an diesen Empfänger gerichtet sind.
|
||
*/
|
||
public static function get_pending_gifts($request) {
|
||
global $wpdb;
|
||
$recipient = sanitize_text_field($request->get_param('recipient'));
|
||
|
||
if (!$recipient) {
|
||
return new WP_REST_Response(['orders' => []]);
|
||
}
|
||
|
||
$table = $wpdb->prefix . 'wis_orders';
|
||
// Status NICHT auf 'claimed' setzen – bleibt 'pending' bis der Empfänger
|
||
// ingame annimmt (gift_accept) oder ablehnt (gift_decline).
|
||
// Das Spigot-Plugin prüft pendingGiftRequests intern gegen Duplikate.
|
||
$results = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM $table
|
||
WHERE gift_recipient = %s AND status IN ('pending', 'processing')
|
||
ORDER BY created_at ASC LIMIT 1",
|
||
$recipient
|
||
));
|
||
|
||
return new WP_REST_Response(['orders' => $results]);
|
||
}
|
||
|
||
/**
|
||
* POST /wis/v1/gift_accept { "id": 123 }
|
||
* Wird aufgerufen wenn Empfänger das Geschenk annimmt.
|
||
* Setzt Status auf 'processing' – der Spigot-Server liefert dann die Ware aus.
|
||
*/
|
||
public static function gift_accept($request) {
|
||
global $wpdb;
|
||
$data = $request->get_json_params();
|
||
$id = intval($data['id'] ?? 0);
|
||
if (!$id) return new WP_REST_Response(['success' => false], 400);
|
||
|
||
$wpdb->update(
|
||
$wpdb->prefix . 'wis_orders',
|
||
['status' => 'processing'],
|
||
['id' => $id]
|
||
);
|
||
|
||
return new WP_REST_Response(['success' => true]);
|
||
}
|
||
|
||
/**
|
||
* POST /wis/v1/gift_decline { "id": 123, "sender": "SpielerA", "price": 500 }
|
||
* Empfänger lehnt ab → Order stornieren.
|
||
* Rückerstattung erfolgt auf Spigot-Seite via Vault direkt.
|
||
*/
|
||
public static function gift_decline($request) {
|
||
global $wpdb;
|
||
$data = $request->get_json_params();
|
||
$id = intval($data['id'] ?? 0);
|
||
if (!$id) return new WP_REST_Response(['success' => false], 400);
|
||
|
||
$wpdb->update(
|
||
$wpdb->prefix . 'wis_orders',
|
||
['status' => 'cancelled'],
|
||
['id' => $id]
|
||
);
|
||
|
||
return new WP_REST_Response(['success' => true]);
|
||
}
|
||
|
||
public static function import_json($request) {
|
||
$data = $request->get_json_params();
|
||
$url = esc_url_raw($data['url'] ?? '');
|
||
|
||
if (!$url) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Keine URL angegeben'], 400);
|
||
}
|
||
|
||
$response = wp_remote_get($url, ['timeout' => 30]);
|
||
|
||
if (is_wp_error($response)) {
|
||
return new WP_REST_Response(['success' => false, 'message' => $response->get_error_message()], 400);
|
||
}
|
||
|
||
$body = wp_remote_retrieve_body($response);
|
||
$json = json_decode($body, true);
|
||
|
||
if (!isset($json['items']) || !is_array($json['items'])) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Ungültiges JSON Format'], 400);
|
||
}
|
||
|
||
$imported = 0;
|
||
$skipped = 0;
|
||
|
||
foreach ($json['items'] as $item) {
|
||
$item_id = sanitize_text_field($item['id'] ?? '');
|
||
$name = sanitize_text_field($item['name'] ?? 'Unbekannt');
|
||
|
||
if (empty($item_id)) continue;
|
||
|
||
$exists = WIS_DB::get_item_by_item_id($item_id);
|
||
|
||
if ($exists) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
WIS_DB::insert_item([
|
||
'item_id' => $item_id,
|
||
'name' => $name,
|
||
'description' => sanitize_textarea_field($item['description'] ?? ''),
|
||
'price' => intval($item['price'] ?? 0),
|
||
'status' => intval($item['price'] ?? 0) > 0 ? 'publish' : 'draft',
|
||
'servers' => '[]',
|
||
'categories' => '[]'
|
||
]);
|
||
|
||
$imported++;
|
||
}
|
||
|
||
return new WP_REST_Response(['success' => true, 'imported' => $imported, 'skipped' => $skipped]);
|
||
}
|
||
|
||
public static function create_order($request) {
|
||
global $wpdb;
|
||
$data = $request->get_json_params();
|
||
|
||
$player = sanitize_text_field($data['player'] ?? '');
|
||
$cart = $data['cart'] ?? [];
|
||
$server = sanitize_text_field($data['server'] ?? '');
|
||
$coupon_code = isset($data['coupon_code']) ? sanitize_text_field(strtoupper($data['coupon_code'])) : '';
|
||
// Gift: optionaler Empfänger-Spielername
|
||
$gift_recipient = isset($data['gift_recipient'])
|
||
? sanitize_text_field(trim($data['gift_recipient'])) : '';
|
||
|
||
if (!$player || empty($cart) || !$server) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Fehlende Daten'], 400);
|
||
}
|
||
// Gift-Validierung: Spieler kann sich nicht selbst beschenken
|
||
if ($gift_recipient !== '' && strcasecmp($gift_recipient, $player) === 0) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Selbst-Geschenk nicht erlaubt'], 400);
|
||
}
|
||
|
||
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
|
||
$valid_cart = [];
|
||
$total_normal = 0;
|
||
$total_offer = 0;
|
||
|
||
foreach ($cart as $item_data) {
|
||
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
|
||
if (!$item || $item->status !== 'publish') continue;
|
||
|
||
$qty = intval($item_data['quantity'] ?? 1);
|
||
if ($qty <= 0) continue;
|
||
|
||
$servers = json_decode($item->servers, true);
|
||
if (!in_array($server, $servers ?: [])) continue;
|
||
|
||
$price = $item->offer_price > 0 ? $item->offer_price : $item->price;
|
||
|
||
// Gutschein-Karte: Preis = Wunschbetrag des Käufers (innerhalb Min/Max)
|
||
$custom_amount = 0;
|
||
if (preg_match('/^gift_card_(\d+)_(\d+)$/', $item->item_id, $gc_m2)) {
|
||
$gc_min2 = intval($gc_m2[1]);
|
||
$gc_max2 = intval($gc_m2[2]);
|
||
$raw_amount = intval($item_data['custom_amount'] ?? $gc_min2);
|
||
$custom_amount = max($gc_min2, min($gc_max2, $raw_amount));
|
||
$price = $custom_amount; // Preis = Wunschbetrag
|
||
}
|
||
$valid_cart[] = [
|
||
'id' => $item->item_id,
|
||
'title' => $item->name,
|
||
'price' => $price,
|
||
'qty' => $qty,
|
||
'is_offer' => $item->is_offer,
|
||
'custom_amount' => $custom_amount ?: $price,
|
||
];
|
||
|
||
$item_total = $price * $qty;
|
||
|
||
if ($item->is_offer && $exclude_offers === '1') {
|
||
$total_offer += $item_total;
|
||
} else {
|
||
$total_normal += $item_total;
|
||
}
|
||
}
|
||
|
||
if (empty($valid_cart)) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Keine gültigen Items'], 400);
|
||
}
|
||
|
||
$coupon_discount = 0;
|
||
$coupon_msg = '';
|
||
$coupon_applied = false;
|
||
$coupon_error = false; // Gutschein-Fehler der den Kauf blockiert
|
||
|
||
if (!empty($coupon_code)) {
|
||
$coupon = WIS_DB::get_coupon_by_code($coupon_code);
|
||
|
||
if ($coupon) {
|
||
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) {
|
||
$coupon_error = true;
|
||
$coupon_msg = 'Dein Gutschein ist abgelaufen.';
|
||
} elseif ($coupon->used_count >= $coupon->usage_limit) {
|
||
$coupon_error = true;
|
||
$coupon_msg = 'Dieser Gutschein ist bereits vollständig aufgebraucht.';
|
||
} elseif (WIS_DB::coupon_used_by_player($coupon->id, $player)) {
|
||
$coupon_error = true;
|
||
$coupon_msg = 'Du hast diesen Gutschein bereits eingelöst.';
|
||
} else {
|
||
if ($exclude_offers === '1' && $total_normal <= 0) {
|
||
$coupon_error = true;
|
||
$coupon_msg = 'Dieser Gutschein gilt nicht für Angebots-Items.';
|
||
} else {
|
||
$restriction_error = self::check_coupon_restrictions($coupon, $cart, $total_normal);
|
||
if ($restriction_error !== null) {
|
||
$coupon_error = true;
|
||
$coupon_msg = $restriction_error;
|
||
} else
|
||
if ($coupon->type === 'percent') {
|
||
$coupon_discount = floor($total_normal * ($coupon->value / 100));
|
||
} else {
|
||
$coupon_discount = $coupon->value;
|
||
}
|
||
WIS_DB::update_coupon($coupon->id, ['used_count' => $coupon->used_count + 1]);
|
||
WIS_DB::record_coupon_use($coupon->id, $player);
|
||
$coupon_applied = true;
|
||
$coupon_msg = "Gutschein eingelöst: -{$coupon_discount} {$currency}";
|
||
}
|
||
}
|
||
} else {
|
||
$coupon_error = true;
|
||
$coupon_msg = 'Ungültiger Gutschein-Code.';
|
||
}
|
||
}
|
||
|
||
// Kauf blockieren wenn Gutschein-Fehler vorliegt und Spieler nicht explizit bestätigt hat
|
||
$confirmed_no_coupon = (bool)($data['confirmed_no_coupon'] ?? false);
|
||
if ($coupon_error && !$confirmed_no_coupon) {
|
||
return new WP_REST_Response([
|
||
'success' => false,
|
||
'coupon_error' => true,
|
||
'message' => $coupon_msg,
|
||
], 200);
|
||
}
|
||
|
||
$final_price = max(0, $total_normal - $coupon_discount) + $total_offer;
|
||
|
||
// ── Steuer ──────────────────────────────────────────────────
|
||
$tax_enabled = get_option('wis_tax_enabled', '0');
|
||
$tax_rate = floatval(get_option('wis_tax_rate', '0'));
|
||
$tax_amount = 0;
|
||
if ($tax_enabled === '1' && $tax_rate > 0) {
|
||
$tax_amount = floor($final_price * $tax_rate / 100);
|
||
$final_price = $final_price + $tax_amount;
|
||
}
|
||
// ────────────────────────────────────────────────────────────
|
||
|
||
// Fly-Dauern (item_id → Sekunden)
|
||
// Fly-Dauern und lesbares Label (wird dem Spieler auf dem Code angezeigt)
|
||
$fly_durations = [
|
||
'fly_5min' => 5 * 60,
|
||
'fly_15min' => 15 * 60,
|
||
'fly_30min' => 30 * 60,
|
||
'fly_1h' => 1 * 3600,
|
||
'fly_2h' => 2 * 3600,
|
||
'fly_3h' => 3 * 3600,
|
||
];
|
||
$fly_labels = [
|
||
'fly_5min' => '5 Minuten Fly',
|
||
'fly_15min' => '15 Minuten Fly',
|
||
'fly_30min' => '30 Minuten Fly',
|
||
'fly_1h' => '1 Stunde Fly',
|
||
'fly_2h' => '2 Stunden Fly',
|
||
'fly_3h' => '3 Stunden Fly',
|
||
];
|
||
|
||
$items_payload = [];
|
||
$commands_payload = [];
|
||
$title_parts = [];
|
||
|
||
foreach ($valid_cart as $item) {
|
||
$item_id = $item['id']; // Das ist der item_id-String aus wis_items
|
||
|
||
if (isset($fly_durations[$item_id])) {
|
||
// Fly-Gutschein: pro Stueck einen eigenen Code-Eintrag (qty > 1 = mehrere Codes)
|
||
$base_sec = $fly_durations[$item_id];
|
||
$base_label = $fly_labels[$item_id];
|
||
for ($q = 0; $q < intval($item['qty']); $q++) {
|
||
$commands_payload[] = [
|
||
'type' => 'fly',
|
||
'duration_sec' => $base_sec,
|
||
'label' => $base_label,
|
||
];
|
||
}
|
||
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
|
||
} elseif (preg_match('/^rank_([^_]+)_([a-zA-Z0-9_\-]+)_([a-zA-Z0-9_\-]+)_(\d+)$/', $item_id, $rm)) {
|
||
// Neues Format: rank_{rank_id}_{lp_group}_{default_group}_{days}
|
||
$rank_id = $rm[1];
|
||
$lp_group = $rm[2];
|
||
$default_group = $rm[3];
|
||
$rank_days = intval($rm[4]);
|
||
for ($q = 0; $q < intval($item['qty']); $q++) {
|
||
$commands_payload[] = [
|
||
'type' => 'rank',
|
||
'rank_id' => $rank_id,
|
||
'lp_group' => $lp_group,
|
||
'default_group' => $default_group,
|
||
'label' => $item['title'],
|
||
'days' => $rank_days,
|
||
];
|
||
}
|
||
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
|
||
} elseif (preg_match('/^rank_(.+)_(\d+)$/', $item_id, $rm)) {
|
||
// Altes Format (Fallback): rank_{rank_id}_{days}
|
||
$rank_id = $rm[1];
|
||
$rank_days = intval($rm[2]);
|
||
for ($q = 0; $q < intval($item['qty']); $q++) {
|
||
$commands_payload[] = [
|
||
'type' => 'rank',
|
||
'rank_id' => $rank_id,
|
||
'lp_group'=> $rank_id,
|
||
'default_group' => 'default',
|
||
'label' => $item['title'],
|
||
'days' => $rank_days,
|
||
];
|
||
}
|
||
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
|
||
} elseif (preg_match('/^fly_abo$/', $item_id) || preg_match('/^fly_abo_\d*$/', $item_id)) {
|
||
// Fly-Abo: monatlich abonniert, Preis = Monatsbeitrag
|
||
$commands_payload[] = [
|
||
'type' => 'fly_abo',
|
||
'label' => $item['title'],
|
||
];
|
||
$title_parts[] = $item['title'];
|
||
} elseif (preg_match('/^plot_slots_(\d+)$/', $item_id, $ps_m)) {
|
||
// Plot-Slots: einmaliger permanenter Kauf
|
||
$extra_slots = intval($ps_m[1]) * intval($item['qty']);
|
||
$commands_payload[] = [
|
||
'type' => 'plot_slots',
|
||
'slots' => $extra_slots,
|
||
'label' => $item['title'],
|
||
];
|
||
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
|
||
} elseif (preg_match('/^plot_abo_(\d+)$/', $item_id, $pa_m)) {
|
||
// Plot-Abo: monatliche Abbuchung, Preis = Monatsbeitrag
|
||
$abo_slots = intval($pa_m[1]);
|
||
$commands_payload[] = [
|
||
'type' => 'plot_abo',
|
||
'slots' => $abo_slots,
|
||
'label' => $item['title'],
|
||
];
|
||
$title_parts[] = $item['title'];
|
||
} elseif (preg_match('/^item_abo_(.+)_(\d+)_(\d+)$/', $item_id, $ia_m)) {
|
||
// Item-Abo: tägliche Lieferung eines Minecraft-Items
|
||
$abo_mc_item = $ia_m[1];
|
||
$abo_daily_qty = intval($ia_m[2]);
|
||
$abo_dur_days = intval($ia_m[3]);
|
||
$commands_payload[] = [
|
||
'type' => 'item_abo',
|
||
'item_id' => $abo_mc_item,
|
||
'daily_qty' => $abo_daily_qty,
|
||
'duration_days' => $abo_dur_days,
|
||
'label' => $item['title'],
|
||
];
|
||
$title_parts[] = $item['title'];
|
||
} elseif (preg_match('/^custom_cmd_(.+)$/', $item_id, $cc_m)) {
|
||
// Custom Command Item: Command aus DB holen
|
||
$db_item = WIS_DB::get_item_by_item_id($item_id);
|
||
$raw_cmd = $db_item ? ($db_item->custom_command ?? '') : '';
|
||
for ($q = 0; $q < intval($item['qty']); $q++) {
|
||
$commands_payload[] = [
|
||
'type' => 'custom_cmd',
|
||
'cmd_id' => $cc_m[1],
|
||
'command' => $raw_cmd,
|
||
'label' => $item['title'],
|
||
];
|
||
}
|
||
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
|
||
} elseif (preg_match('/^gift_card_(\d+)_(\d+)$/', $item_id, $gc_m)) {
|
||
// Gutschein-Karte: für jede Stück einen eigenen Coupon generieren
|
||
$gc_amount = intval($item['custom_amount'] ?? $item['price']); // custom_amount vom Frontend
|
||
$gc_min = intval($gc_m[1]);
|
||
$gc_max = intval($gc_m[2]);
|
||
// Betrag auf gültigen Bereich clippen
|
||
$gc_amount = max($gc_min, min($gc_max, $gc_amount));
|
||
for ($q = 0; $q < intval($item['qty']); $q++) {
|
||
// Eindeutigen Code generieren
|
||
$gc_code = 'GC-' . strtoupper(bin2hex(random_bytes(4))) . '-' . strtoupper(bin2hex(random_bytes(3)));
|
||
// Als Coupon in DB eintragen: Typ 'fixed', usage_limit 1, kein Ablauf
|
||
WIS_DB::insert_coupon([
|
||
'code' => $gc_code,
|
||
'value' => $gc_amount,
|
||
'type' => 'fixed',
|
||
'usage_limit' => 1,
|
||
'used_count' => 0,
|
||
'expiry' => null,
|
||
'min_order_value' => 0,
|
||
'allowed_categories' => null,
|
||
'bulk_id' => 'gift_card',
|
||
]);
|
||
$commands_payload[] = [
|
||
'type' => 'gift_card',
|
||
'code' => $gc_code,
|
||
'amount' => $gc_amount,
|
||
'label' => $item['title'] . ' (' . $gc_amount . ' ' . $currency . ')',
|
||
'currency'=> $currency,
|
||
];
|
||
}
|
||
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
|
||
// Preis = tatsächlicher Betrag x Menge (nicht der Artikel-Mindestpreis)
|
||
$gc_amount_total = $gc_amount * intval($item['qty']);
|
||
// Korrektur: total_normal wurde mit item->price gerechnet, jetzt auf echten Betrag korrigieren
|
||
$total_normal = $total_normal - ($item['price'] * intval($item['qty'])) + $gc_amount_total;
|
||
} else {
|
||
// Normales Item
|
||
$items_payload[] = [
|
||
'id' => $item_id,
|
||
'amount' => $item['qty'],
|
||
];
|
||
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
|
||
}
|
||
}
|
||
|
||
$title = "Warenkorb: " . implode(', ', $title_parts);
|
||
if (strlen($title) > 240) $title = substr($title, 0, 237) . '...';
|
||
|
||
$payload = [
|
||
'items' => $items_payload,
|
||
'commands' => $commands_payload,
|
||
'coupon' => $coupon_applied ? ['code' => $coupon_code, 'discount' => $coupon_discount] : [],
|
||
];
|
||
|
||
WIS_DB::insert_order([
|
||
'player_name' => $player,
|
||
'gift_recipient' => $gift_recipient !== '' ? $gift_recipient : null,
|
||
'server' => $server,
|
||
'item_id' => 'cart',
|
||
'item_title' => $title,
|
||
'price' => $final_price,
|
||
'quantity' => count($valid_cart),
|
||
'status' => 'pending',
|
||
'response' => json_encode($payload)
|
||
]);
|
||
|
||
// ---- Einzelne Items für Analyse-Tabelle speichern ----
|
||
$new_order_id = $wpdb->insert_id;
|
||
if ($new_order_id) {
|
||
$oi_table = $wpdb->prefix . 'wis_order_items';
|
||
// Tabelle existiert? (für bestehende Installationen ohne Re-Aktivierung)
|
||
$oi_exists = $wpdb->get_var("SHOW TABLES LIKE '$oi_table'");
|
||
if ($oi_exists) {
|
||
foreach ($valid_cart as $ci) {
|
||
$ci_id = $ci['id'];
|
||
$ci_title = $ci['title'];
|
||
$ci_qty = intval($ci['qty'] ?? 1);
|
||
$ci_price = floatval($ci['price'] ?? 0);
|
||
// Item-Typ bestimmen
|
||
if (isset($fly_durations[$ci_id])) {
|
||
$ci_type = 'fly';
|
||
} elseif (preg_match('/^rank_/', $ci_id)) {
|
||
$ci_type = 'rank';
|
||
} elseif ($ci_id === 'fly_abo' || preg_match('/^fly_abo/', $ci_id)) {
|
||
$ci_type = 'fly_abo';
|
||
} elseif (preg_match('/^plot_/', $ci_id)) {
|
||
$ci_type = 'plot';
|
||
} else {
|
||
$ci_type = 'item';
|
||
}
|
||
$wpdb->insert($oi_table, [
|
||
'order_id' => $new_order_id,
|
||
'item_id' => $ci_id,
|
||
'item_name' => $ci_title,
|
||
'item_type' => $ci_type,
|
||
'quantity' => $ci_qty,
|
||
'price_per_item'=> $ci_price,
|
||
'total' => round($ci_price * $ci_qty, 2),
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fly-Abo Abonnements registrieren / verlängern
|
||
foreach ($commands_payload as $cmd) {
|
||
if (($cmd['type'] ?? '') !== 'fly_abo') continue;
|
||
$abo_days = intval($cmd['days'] ?? 30);
|
||
$abo_label = sanitize_text_field($cmd['label'] ?? 'Fly-Abo');
|
||
$abo_price = 0;
|
||
// Preis aus dem Warenkorb ermitteln (fly_abo mit oder ohne Zahl-Suffix)
|
||
foreach ($valid_cart as $ci) {
|
||
if ($ci['id'] === 'fly_abo' || preg_match('/^fly_abo/', $ci['id'])) {
|
||
$abo_price = intval($ci['price'] ?? 0);
|
||
break;
|
||
}
|
||
}
|
||
global $wpdb;
|
||
WIS_Activator::create_abo_subs_table();
|
||
$subs_table = $wpdb->prefix . 'wis_fly_abo_subs';
|
||
$existing = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$subs_table} WHERE player_name = %s AND server = %s",
|
||
$player, $server
|
||
));
|
||
if ($existing && $existing->status === 'active') {
|
||
// Bestehendes aktives Abo verlängern (kumulativ)
|
||
$wpdb->query($wpdb->prepare(
|
||
"UPDATE {$subs_table}
|
||
SET expires_at = DATE_ADD(IF(expires_at > NOW(), expires_at, NOW()), INTERVAL %d DAY),
|
||
cancelled = 0,
|
||
cancelled_at = NULL,
|
||
label = %s,
|
||
price = %d
|
||
WHERE id = %d",
|
||
$abo_days, $abo_label, $abo_price, $existing->id
|
||
));
|
||
} else {
|
||
// Neues Abo anlegen
|
||
$wpdb->replace($subs_table, [
|
||
'player_name' => $player,
|
||
'server' => $server,
|
||
'label' => $abo_label,
|
||
'price' => $abo_price,
|
||
'status' => 'active',
|
||
'cancelled' => 0,
|
||
'expires_at' => date('Y-m-d H:i:s', strtotime("+{$abo_days} days")),
|
||
]);
|
||
}
|
||
}
|
||
|
||
// Item-Abo Abonnements registrieren / verlängern
|
||
foreach ($commands_payload as $cmd) {
|
||
if (($cmd['type'] ?? '') !== 'item_abo') continue;
|
||
$ia_mc_item = sanitize_text_field($cmd['item_id'] ?? '');
|
||
$ia_daily_qty = max(1, intval($cmd['daily_qty'] ?? 1));
|
||
$ia_dur_days = max(1, intval($cmd['duration_days'] ?? 30));
|
||
$ia_label = sanitize_text_field($cmd['label'] ?? 'Item-Abo');
|
||
$ia_price = 0;
|
||
foreach ($valid_cart as $ci) {
|
||
if (preg_match('/^item_abo_/', $ci['id'])) {
|
||
$ia_price = intval($ci['price'] ?? 0);
|
||
break;
|
||
}
|
||
}
|
||
WIS_Activator::create_item_abo_subs_table();
|
||
$ia_table = $wpdb->prefix . 'wis_item_abo_subs';
|
||
// Bestehende aktive Abos für dieselbe item_id + player + server verlängern
|
||
$ia_existing = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$ia_table} WHERE player_name = %s AND server = %s AND item_id = %s AND status = 'active'",
|
||
$player, $server, $ia_mc_item
|
||
));
|
||
if ($ia_existing) {
|
||
$wpdb->query($wpdb->prepare(
|
||
"UPDATE {$ia_table}
|
||
SET expires_at = DATE_ADD(IF(expires_at > NOW(), expires_at, NOW()), INTERVAL %d DAY),
|
||
cancelled = 0,
|
||
cancelled_at = NULL,
|
||
daily_qty = %d,
|
||
label = %s,
|
||
price = %d
|
||
WHERE id = %d",
|
||
$ia_dur_days, $ia_daily_qty, $ia_label, $ia_price, $ia_existing->id
|
||
));
|
||
$ia_sub_id = $ia_existing->id;
|
||
} else {
|
||
$wpdb->insert($ia_table, [
|
||
'player_name' => $player,
|
||
'server' => $server,
|
||
'item_id' => $ia_mc_item,
|
||
'daily_qty' => $ia_daily_qty,
|
||
'label' => $ia_label,
|
||
'price' => $ia_price,
|
||
'status' => 'active',
|
||
'cancelled' => 0,
|
||
'expires_at' => date('Y-m-d H:i:s', strtotime("+{$ia_dur_days} days")),
|
||
'last_delivered' => null,
|
||
]);
|
||
$ia_sub_id = $wpdb->insert_id;
|
||
}
|
||
|
||
// Sofort-Lieferung für heute anlegen (damit der Spieler nicht bis Mitternacht warten muss)
|
||
$ia_sub = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT * FROM {$ia_table} WHERE id = %d", $ia_sub_id
|
||
));
|
||
if ($ia_sub && (empty($ia_sub->last_delivered) || $ia_sub->last_delivered < date('Y-m-d'))) {
|
||
$ia_payload = json_encode([
|
||
'items' => [['id' => $ia_mc_item, 'amount' => $ia_daily_qty]],
|
||
'commands' => [],
|
||
'abo_delivery' => true,
|
||
]);
|
||
$wpdb->insert($wpdb->prefix . 'wis_orders', [
|
||
'player_name' => $player,
|
||
'server' => $server,
|
||
'item_id' => 'item_abo_delivery',
|
||
'item_title' => '📦 Abo-Lieferung: ' . $ia_label . ' ×' . $ia_daily_qty,
|
||
'price' => 0,
|
||
'quantity' => $ia_daily_qty,
|
||
'status' => 'pending',
|
||
'response' => $ia_payload,
|
||
]);
|
||
$wpdb->update($ia_table, ['last_delivered' => date('Y-m-d')], ['id' => $ia_sub_id]);
|
||
}
|
||
}
|
||
|
||
$msg = $gift_recipient !== ''
|
||
? "🎁 Geschenk-Bestellung für {$gift_recipient} erfolgreich!"
|
||
: '✅ Bestellung erfolgreich!';
|
||
if ($coupon_msg) $msg .= ' (' . $coupon_msg . ')';
|
||
|
||
// Gift-Card-Codes aus dem Payload sammeln und in der Antwort zurückgeben
|
||
$gift_card_codes = [];
|
||
foreach ($commands_payload as $cmd) {
|
||
if (($cmd['type'] ?? '') === 'gift_card') {
|
||
$gift_card_codes[] = [
|
||
'code' => $cmd['code'],
|
||
'amount' => $cmd['amount'],
|
||
'label' => $cmd['label'] ?? '',
|
||
];
|
||
}
|
||
}
|
||
|
||
return new WP_REST_Response([
|
||
'success' => true,
|
||
'message' => $msg,
|
||
'gift_card_codes' => [], // Codes werden erst nach complete_order zurückgegeben
|
||
]);
|
||
}
|
||
/**
|
||
* Prueft Mindestbestellwert, erlaubte Kategorien und erlaubte Raenge.
|
||
* Gibt null zurueck wenn alles OK, sonst Fehlermeldung als String.
|
||
*/
|
||
private static function check_coupon_restrictions($coupon, $cart, $subtotal_normal) {
|
||
// 1. Mindestbestellwert
|
||
$min = intval($coupon->min_order_value ?? 0);
|
||
if ($min > 0 && $subtotal_normal < $min) {
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
return "Mindestbestellwert von {$min} {$currency} nicht erreicht.";
|
||
}
|
||
|
||
// 2. Erlaubte Kategorien
|
||
if (!empty($coupon->allowed_categories)) {
|
||
$allowed_cat_ids = array_map('intval', explode(',', $coupon->allowed_categories));
|
||
$has_valid_cat = false;
|
||
foreach ($cart as $item_data) {
|
||
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
|
||
if ($item && in_array(intval($item->category_id), $allowed_cat_ids)) {
|
||
$has_valid_cat = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!$has_valid_cat) {
|
||
return 'Dieser Gutschein gilt nicht fuer die Produkte in deinem Warenkorb.';
|
||
}
|
||
}
|
||
|
||
|
||
|
||
return null;
|
||
}
|
||
|
||
public static function validate_coupon($request) {
|
||
$data = $request->get_json_params();
|
||
$code = sanitize_text_field(strtoupper($data['code'] ?? ''));
|
||
$cart = $data['cart'] ?? [];
|
||
$player = sanitize_text_field($data['player'] ?? '');
|
||
|
||
if (!$code) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Kein Code']);
|
||
}
|
||
|
||
$coupon = WIS_DB::get_coupon_by_code($code);
|
||
|
||
if (!$coupon) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein nicht gefunden']);
|
||
}
|
||
|
||
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein abgelaufen']);
|
||
}
|
||
|
||
if ($coupon->used_count >= $coupon->usage_limit) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Bereits aufgebraucht']);
|
||
}
|
||
|
||
// Spieler-spezifische Prüfung: hat dieser Spieler den Code schon eingelöst?
|
||
if ($player && WIS_DB::coupon_used_by_player($coupon->id, $player)) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Du hast diesen Gutschein bereits eingelöst']);
|
||
}
|
||
|
||
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
|
||
|
||
if ($exclude_offers === '1' && !empty($cart)) {
|
||
$has_normal = false;
|
||
foreach ($cart as $item_data) {
|
||
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
|
||
if ($item && !$item->is_offer) {
|
||
$has_normal = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$has_normal) {
|
||
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein gilt nicht für Angebote']);
|
||
}
|
||
}
|
||
// Neue Einschränkungen prüfen (Mindestbestellwert, Kategorien, Rang)
|
||
$subtotal_normal = 0;
|
||
foreach ($cart as $item_data) {
|
||
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
|
||
if ($item && !($exclude_offers === '1' && $item->is_offer)) {
|
||
$subtotal_normal += $item->price * intval($item_data['quantity'] ?? 1);
|
||
}
|
||
}
|
||
$restriction_error = self::check_coupon_restrictions($coupon, $cart, $subtotal_normal);
|
||
if ($restriction_error !== null) {
|
||
return new WP_REST_Response(['success' => false, 'message' => $restriction_error]);
|
||
}
|
||
|
||
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$msg = $coupon->type === 'percent'
|
||
? "Gutschein gültig (-{$coupon->value}%)"
|
||
: "Gutschein gültig (-{$coupon->value} {$currency})";
|
||
|
||
return new WP_REST_Response([
|
||
'success' => true,
|
||
'type' => $coupon->type,
|
||
'value' => $coupon->value,
|
||
'message' => $msg
|
||
]);
|
||
}
|
||
}
|
||
|
||
// ===========================================================
|
||
// SHORTCODE - FRONTEND SHOP
|
||
// ===========================================================
|
||
class WIS_Shortcode {
|
||
public static function register() {
|
||
add_shortcode('ingame_shop_form', [self::class, 'render']);
|
||
}
|
||
|
||
public static function render() {
|
||
$servers = WIS_DB::get_servers();
|
||
$categories = WIS_DB::get_categories();
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$header_text = get_option('wis_header_text', '');
|
||
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
|
||
$first_category = !empty($categories) ? $categories[0]->slug : '';
|
||
|
||
ob_start();
|
||
?>
|
||
<style>
|
||
.wis-shop { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 1400px; margin: 40px auto; background: #f4f6f8; padding: 20px; border-radius: 10px; position: relative; }
|
||
.wis-header { text-align: center; margin-bottom: 30px; }
|
||
.wis-header h2 { color: #333; margin-bottom: 10px; }
|
||
.wis-status { background: #d4edda; color: #155724; padding: 15px; border-radius: 8px; border-left: 4px solid #c3e6cb; margin-bottom: 20px; font-weight: 500; }
|
||
.wis-control-bar { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 30px; align-items: center; justify-content: space-between; background: #fff; padding: 15px; border-radius: 8px; border: 1px solid #ddd; }
|
||
.wis-search-input { padding: 10px 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; width: 100%; max-width: 300px; }
|
||
.wis-filter-select { padding: 12px 20px; font-size: 16px; border: 2px solid #ddd; border-radius: 8px; background: white; cursor: pointer; min-width: 200px; }
|
||
.wis-cart-btn { padding: 12px 30px; background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; border: none; border-radius: 8px; font-weight: bold; font-size: 1rem; cursor: pointer; transition: all 0.3s; position: relative; display: flex; align-items: center; gap: 10px; }
|
||
.wis-cart-btn:hover { transform: scale(1.05); box-shadow: 0 5px 15px rgba(40, 167, 69, 0.4); }
|
||
.wis-cart-badge { position: absolute; top: -8px; right: -8px; background: #dc3545; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; font-weight: bold; }
|
||
.wis-cat-tabs { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; justify-content: center; }
|
||
.wis-cat-btn { padding: 8px 16px; border: 1px solid #ddd; background: #fff; border-radius: 20px; cursor: pointer; transition: all 0.2s; }
|
||
.wis-cat-btn:hover, .wis-cat-btn.active { background: #667eea; color: white; border-color: #667eea; }
|
||
.wis-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 320px)); gap: 15px; justify-content: center; margin: 0 auto; min-height: 200px; }
|
||
.wis-subgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 320px)); gap: 15px; justify-content: center; margin: 0 auto 20px; }
|
||
.wis-subgroup-divider { display: flex; align-items: center; gap: 16px; margin: 28px 0 14px; }
|
||
.wis-sub-hr { flex: 1; border: none; border-top: 2px solid #333; margin: 0; }
|
||
.wis-sub-label { font-size: 1rem; font-weight: 800; color: #222; white-space: nowrap; }
|
||
.wis-card { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s; display: flex; flex-direction: column; border: 1px solid #eee; position: relative; width: 100%; }
|
||
.wis-card.offer { border: 2px solid #ffc107; }
|
||
.wis-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
|
||
.wis-card-img { width: 100%; height: 180px; background: #2d2d2d; display: flex; align-items: center; justify-content: center; position: relative; padding: 20px; }
|
||
.wis-card-img img { width: 160px !important; height: 160px !important; object-fit: contain; filter: drop-shadow(0 6px 8px rgba(0,0,0,0.7)); transition: transform 0.3s; image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges; }
|
||
.wis-card:hover .wis-card-img img { transform: scale(1.15) rotate(5deg); }
|
||
.wis-card-img--custom { padding: 0 !important; overflow: hidden; height: 180px !important; display: flex !important; align-items: center !important; justify-content: center !important; background: #2d2d2d; }
|
||
.wis-card-img--custom img { position: static !important; width: 100% !important; height: 100% !important; object-fit: contain !important; image-rendering: auto !important; filter: none !important; border-radius: 0; display: block !important; transition: transform 0.3s; }
|
||
.wis-card:hover .wis-card-img--custom img { transform: scale(1.05) !important; }
|
||
.wis-offer-badge, .wis-daily-badge { position: absolute; top: 10px; left: 10px; background: linear-gradient(135deg, #ff416c, #ff4b2b); color: white; padding: 6px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); z-index: 2; }
|
||
.wis-daily-badge { background: linear-gradient(135deg, #6f42c1, #8e44ad); border: 1px solid #fff; }
|
||
.wis-card-body { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; }
|
||
.wis-card-title { font-size: 1.1rem; font-weight: 700; margin: 0 0 10px 0; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.wis-card-desc { font-size: 0.85rem; color: #666; margin-bottom: 10px; line-height: 1.4; height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||
.wis-card-price-container { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
|
||
.wis-card-price { font-size: 1.2rem; color: #e67e22; font-weight: 800; }
|
||
.wis-card-price-old { font-size: 0.9rem; color: #999; text-decoration: line-through; }
|
||
.wis-card-servers { font-size: 0.85rem; color: #666; margin-bottom: 15px; }
|
||
.wis-quantity-control { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; }
|
||
.wis-quantity-btn { width: 35px; height: 35px; border: 2px solid #667eea; background: white; color: #667eea; border-radius: 6px; font-weight: bold; cursor: pointer; transition: all 0.2s; }
|
||
.wis-quantity-btn:hover { background: #667eea; color: white; }
|
||
.wis-quantity-input { width: 60px; text-align: center; border: 2px solid #ddd; border-radius: 6px; padding: 8px; font-weight: bold; }
|
||
.wis-btn-add { margin-top: auto; width: 100%; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: all 0.3s; }
|
||
.wis-btn-add:hover { transform: scale(1.05); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); }
|
||
.wis-pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin: 30px 0 10px; flex-wrap: wrap; }
|
||
.wis-per-page-bar { display: flex; justify-content: flex-end; margin: 0 0 20px; }
|
||
.wis-page-btn { padding: 8px 14px; border: 2px solid #ddd; background: #fff; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; color: #333; }
|
||
.wis-page-btn:hover { border-color: #667eea; color: #667eea; }
|
||
.wis-page-btn.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-color: #667eea; color: #fff; }
|
||
.wis-page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
.wis-page-info { font-size: 14px; color: #666; margin: 0 8px; }
|
||
.wis-loading { text-align: center; padding: 60px 20px; color: #888; font-size: 1.1rem; }
|
||
.wis-loading-spinner { display: inline-block; width: 36px; height: 36px; border: 4px solid #ddd; border-top-color: #667eea; border-radius: 50%; animation: wis-spin 0.8s linear infinite; margin-bottom: 12px; }
|
||
@keyframes wis-spin { to { transform: rotate(360deg); } }
|
||
/* ── CART DRAWER ─────────────────────────────────────────── */
|
||
.wis-drawer-overlay { position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2147483640;display:none;backdrop-filter:blur(2px); }
|
||
.wis-drawer-overlay.open { display:block; }
|
||
.wis-drawer { position:fixed;top:0;right:0;width:100%;max-width:480px;height:100%;background:#fff;z-index:2147483641;display:flex;flex-direction:column;box-shadow:-8px 0 40px rgba(0,0,0,.25);transform:translateX(100%);transition:transform .35s cubic-bezier(.4,0,.2,1); }
|
||
.wis-drawer.open { transform:translateX(0); }
|
||
.wis-drawer-header { display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid #f0f0f0;flex-shrink:0;background:#fff; }
|
||
.wis-drawer-header h2 { margin:0;font-size:1.2rem;font-weight:700;color:#1a1a1a;display:flex;align-items:center;gap:8px; }
|
||
.wis-drawer-close { width:36px;height:36px;border:none;background:#f5f5f5;border-radius:50%;cursor:pointer;font-size:1rem;display:flex;align-items:center;justify-content:center;color:#555;transition:background .2s; }
|
||
.wis-drawer-close:hover { background:#e0e0e0; }
|
||
.wis-steps { display:flex;padding:16px 24px 0;flex-shrink:0; }
|
||
.wis-step { flex:1;text-align:center;font-size:.75rem;font-weight:600;color:#bbb;position:relative;padding-bottom:12px; }
|
||
.wis-step::after { content:'';position:absolute;bottom:0;left:0;width:100%;height:3px;background:#eee;border-radius:2px; }
|
||
.wis-step.active { color:#667eea; }
|
||
.wis-step.active::after { background:linear-gradient(90deg,#667eea,#764ba2); }
|
||
.wis-step.done { color:#28a745; }
|
||
.wis-step.done::after { background:#28a745; }
|
||
.wis-step-num { display:inline-flex;width:22px;height:22px;align-items:center;justify-content:center;border-radius:50%;background:#eee;color:#aaa;font-size:.7rem;font-weight:700;margin-bottom:4px; }
|
||
.wis-step.active .wis-step-num { background:linear-gradient(135deg,#667eea,#764ba2);color:#fff; }
|
||
.wis-step.done .wis-step-num { background:#28a745;color:#fff; }
|
||
.wis-drawer-body { flex:1;overflow-y:auto;padding:20px 24px; }
|
||
.wis-drawer-body::-webkit-scrollbar { width:5px; }
|
||
.wis-drawer-body::-webkit-scrollbar-thumb { background:#ddd;border-radius:3px; }
|
||
.wis-drawer-footer { padding:16px 24px;border-top:1px solid #f0f0f0;flex-shrink:0;background:#fff; }
|
||
.wis-cart-empty { text-align:center;padding:60px 20px;color:#bbb; }
|
||
.wis-cart-empty-icon { font-size:3.5rem;margin-bottom:12px; }
|
||
.wis-cart-empty p { font-size:.95rem;margin:0; }
|
||
.wis-ci { display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid #f5f5f5; }
|
||
.wis-ci:last-child { border-bottom:none; }
|
||
.wis-ci-img { width:56px;height:56px;border-radius:8px;object-fit:cover;background:#f5f5f5;flex-shrink:0; }
|
||
.wis-ci-info { flex:1;min-width:0; }
|
||
.wis-ci-name { font-weight:600;font-size:.9rem;color:#1a1a1a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
|
||
.wis-ci-price { font-size:.82rem;color:#888;margin-top:2px; }
|
||
.wis-ci-price strong { color:#e67e22; }
|
||
.wis-ci-qty { display:flex;align-items:center;gap:6px;margin-top:6px; }
|
||
.wis-ci-qty-btn { width:26px;height:26px;border:1px solid #ddd;background:#fff;border-radius:6px;cursor:pointer;font-size:.9rem;font-weight:700;display:flex;align-items:center;justify-content:center;color:#555;transition:all .15s; }
|
||
.wis-ci-qty-btn:hover { background:#667eea;color:#fff;border-color:#667eea; }
|
||
.wis-ci-qty-val { min-width:28px;text-align:center;font-weight:700;font-size:.9rem;color:#333; }
|
||
.wis-ci-del { width:30px;height:30px;border:none;background:none;cursor:pointer;color:#ccc;font-size:1rem;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:all .15s;flex-shrink:0; }
|
||
.wis-ci-del:hover { background:#fff0f0;color:#dc3545; }
|
||
.wis-coupon-row { display:flex;gap:8px;margin-bottom:8px; }
|
||
.wis-coupon-row input { flex:1;padding:10px 14px;border:1.5px solid #e0e0e0;border-radius:8px;font-size:.9rem;outline:none;transition:border-color .2s;color:#333; }
|
||
.wis-coupon-row input:focus { border-color:#667eea; }
|
||
.wis-coupon-row button { padding:10px 16px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:8px;font-weight:600;cursor:pointer;font-size:.85rem;white-space:nowrap; }
|
||
.wis-coupon-msg { font-size:.82rem;min-height:18px;padding:0 2px; }
|
||
.wis-summary { background:#fafafa;border-radius:10px;padding:14px 16px;margin:16px 0 0; }
|
||
.wis-summary-row { display:flex;justify-content:space-between;align-items:center;font-size:.88rem;color:#555;padding:4px 0; }
|
||
.wis-summary-row.discount { color:#28a745; }
|
||
.wis-summary-row.tax { color:#e67e22; }
|
||
.wis-summary-divider { border:none;border-top:1px solid #e8e8e8;margin:8px 0; }
|
||
.wis-summary-total { display:flex;justify-content:space-between;align-items:center;padding-top:8px; }
|
||
.wis-summary-total span:first-child { font-size:.95rem;font-weight:700;color:#1a1a1a; }
|
||
.wis-summary-total span:last-child { font-size:1.3rem;font-weight:800;color:#667eea; }
|
||
.wis-form-section { margin-bottom:18px; }
|
||
.wis-form-label { display:block;font-size:.82rem;font-weight:600;color:#555;margin-bottom:6px;text-transform:uppercase;letter-spacing:.04em; }
|
||
.wis-form-input { width:100%;padding:12px 14px;border:1.5px solid #e0e0e0;border-radius:8px;font-size:.95rem;box-sizing:border-box;outline:none;transition:border-color .2s;color:#1a1a1a;background:#fff; }
|
||
.wis-form-input:focus { border-color:#667eea; }
|
||
.wis-form-select { -webkit-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center; }
|
||
.wis-gift-toggle { display:flex;align-items:center;gap:10px;cursor:pointer;font-weight:600;color:#6f42c1;user-select:none;padding:12px 14px;background:#f8f3ff;border-radius:8px;border:1.5px solid #e2d4f7; }
|
||
.wis-gift-toggle input[type=checkbox] { width:17px;height:17px;cursor:pointer;accent-color:#9b59b6;flex-shrink:0; }
|
||
.wis-gift-field { display:none;background:#f3eeff;border:1.5px solid #c39bd3;border-radius:8px;padding:14px 16px;margin-top:10px; }
|
||
.wis-gift-field label { display:block;font-weight:600;color:#6f42c1;margin-bottom:6px;font-size:.85rem; }
|
||
.wis-gift-input { width:100%;padding:10px 14px;border:1px solid #c39bd3;border-radius:6px;font-size:.95rem;box-sizing:border-box;background:#fff;outline:none; }
|
||
.wis-gift-hint { font-size:.78rem;color:#888;margin-top:6px; }
|
||
.wis-btn-primary { width:100%;padding:14px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none;border-radius:10px;font-weight:700;font-size:1rem;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px; }
|
||
.wis-btn-primary:hover:not(:disabled) { transform:translateY(-1px);box-shadow:0 6px 20px rgba(102,126,234,.4); }
|
||
.wis-btn-primary:disabled { opacity:.6;cursor:not-allowed; }
|
||
.wis-btn-secondary { width:100%;padding:12px;background:#f5f5f5;color:#555;border:none;border-radius:10px;font-weight:600;font-size:.9rem;cursor:pointer;transition:background .2s;margin-top:8px; }
|
||
.wis-btn-secondary:hover { background:#e8e8e8; }
|
||
.wis-alert { padding:12px 14px;border-radius:8px;font-size:.88rem;display:none;margin-top:10px; }
|
||
.wis-alert.success { background:#d4edda;color:#155724;border:1px solid #c3e6cb; }
|
||
.wis-alert.error { background:#f8d7da;color:#721c24;border:1px solid #f5c6cb; }
|
||
.wis-success-screen { text-align:center;padding:40px 20px; }
|
||
.wis-success-icon { font-size:4rem;margin-bottom:16px; }
|
||
.wis-success-screen h3 { margin:0 0 8px;font-size:1.2rem;color:#1a1a1a; }
|
||
.wis-success-screen p { color:#888;font-size:.9rem;margin:0; }
|
||
</style>
|
||
|
||
<div class="wis-shop">
|
||
<div class="wis-header">
|
||
<h2>🛒 Ingame Shop</h2>
|
||
<?php if (!empty(trim($header_text))): ?>
|
||
<div class="wis-status"><?php echo wp_kses_post($header_text); ?></div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="wis-control-bar">
|
||
<input type="text" id="wis-search" class="wis-search-input" placeholder="🔍 Suche Item...">
|
||
|
||
<select id="server-filter" class="wis-filter-select">
|
||
<option value="">Alle Server</option>
|
||
<?php foreach ($servers as $s): ?>
|
||
<option value="<?php echo esc_attr($s->slug); ?>"><?php echo esc_html($s->name); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
|
||
<button class="wis-cart-btn" id="wis-open-cart-btn">
|
||
🛒 Warenkorb
|
||
<span class="wis-cart-badge" id="cart-count">0</span>
|
||
</button>
|
||
</div>
|
||
|
||
<?php
|
||
// Kategorien hierarchisch aufbereiten für JS
|
||
$root_cats_fe = array_filter($categories, fn($c) => $c->parent_id == 0);
|
||
$sub_idx_fe = [];
|
||
foreach ($categories as $c) {
|
||
if ($c->parent_id != 0) $sub_idx_fe[$c->parent_id][] = $c;
|
||
}
|
||
$subcat_map_js = [];
|
||
foreach ($root_cats_fe as $rc) {
|
||
$subcat_map_js[$rc->slug] = !empty($sub_idx_fe[$rc->id])
|
||
? array_map(fn($sc) => ['slug' => $sc->slug, 'name' => $sc->name], $sub_idx_fe[$rc->id])
|
||
: [];
|
||
}
|
||
?>
|
||
<?php if (!empty($categories)): ?>
|
||
<div class="wis-cat-tabs">
|
||
<?php $is_first = true; ?>
|
||
<?php foreach ($root_cats_fe as $cat): ?>
|
||
<button class="wis-cat-btn <?php echo $is_first ? 'active' : ''; ?>"
|
||
data-cat="<?php echo esc_attr($cat->slug); ?>">
|
||
<?php echo esc_html($cat->name); ?>
|
||
</button>
|
||
<?php $is_first = false; ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="wis-grid" id="wis-grid">
|
||
<div class="wis-loading" style="grid-column:1/-1;">
|
||
<div class="wis-loading-spinner"></div><br>Lade Items…
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wis-pagination" id="wis-pagination"></div>
|
||
<div class="wis-per-page-bar">
|
||
<select id="wis-per-page" class="wis-filter-select" style="min-width:130px; padding:8px 12px; font-size:14px;">
|
||
<option value="25" <?php selected(get_option('wis_default_per_page','25'), '25'); ?>>25 pro Seite</option>
|
||
<option value="50" <?php selected(get_option('wis_default_per_page','25'), '50'); ?>>50 pro Seite</option>
|
||
<option value="100" <?php selected(get_option('wis_default_per_page','25'), '100'); ?>>100 pro Seite</option>
|
||
<option value="-1" <?php selected(get_option('wis_default_per_page','25'), '-1'); ?>>Alle anzeigen</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Warenkorb Drawer -->
|
||
<div class="wis-drawer-overlay" id="wis-drawer-overlay"></div>
|
||
<div class="wis-drawer" id="wis-drawer" role="dialog" aria-modal="true">
|
||
<div class="wis-drawer-header">
|
||
<h2>🛒 <span id="wis-drawer-title">Warenkorb</span></h2>
|
||
<button class="wis-drawer-close" id="wis-drawer-close" aria-label="Schließen">✕</button>
|
||
</div>
|
||
<div class="wis-steps" id="wis-steps">
|
||
<div class="wis-step active" id="wis-step-1">
|
||
<div><div class="wis-step-num">1</div></div>
|
||
<div>Warenkorb</div>
|
||
</div>
|
||
<div class="wis-step" id="wis-step-2">
|
||
<div><div class="wis-step-num">2</div></div>
|
||
<div>Bestellung</div>
|
||
</div>
|
||
</div>
|
||
<div class="wis-drawer-body" id="wis-drawer-body">
|
||
<div id="wis-panel-cart">
|
||
<div id="cart-content"></div>
|
||
<div id="wis-coupon-section" style="display:none;margin-top:18px;">
|
||
<label class="wis-form-label">🎫 Gutscheincode</label>
|
||
<div class="wis-coupon-row">
|
||
<input type="text" id="coupon-code" placeholder="CODE eingeben">
|
||
<button id="wis-coupon-btn">Einlösen</button>
|
||
</div>
|
||
<div id="coupon-msg" class="wis-coupon-msg"></div>
|
||
</div>
|
||
<div id="wis-summary-section" style="display:none;">
|
||
<div class="wis-summary">
|
||
<div class="wis-summary-row" id="wis-row-subtotal" style="display:none;">
|
||
<span>Zwischensumme</span><span id="cart-subtotal"></span>
|
||
</div>
|
||
<div class="wis-summary-row discount" id="wis-row-discount" style="display:none;">
|
||
<span>✂️ Rabatt</span><span id="cart-discount"></span>
|
||
</div>
|
||
<div class="wis-summary-row tax" id="wis-row-tax" style="display:none;">
|
||
<span>💰 Steuer (<?php echo esc_attr(get_option('wis_tax_rate','0')); ?>%)</span><span id="cart-tax"></span>
|
||
</div>
|
||
<hr class="wis-summary-divider">
|
||
<div class="wis-summary-total">
|
||
<span>Gesamt</span>
|
||
<span id="cart-total">0 <?php echo esc_html($currency); ?></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="wis-panel-checkout" style="display:none;">
|
||
<div class="wis-form-section">
|
||
<label class="wis-form-label">🖥️ Server</label>
|
||
<select id="checkout-server" class="wis-form-input wis-form-select">
|
||
<option value="">-- Server wählen --</option>
|
||
<?php foreach ($servers as $s): ?>
|
||
<option value="<?php echo esc_attr($s->slug); ?>"><?php echo esc_html($s->name); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="wis-form-section">
|
||
<label class="wis-form-label">👤 Dein Spielername</label>
|
||
<input type="text" id="checkout-player" class="wis-form-input" placeholder="z.B. Steve123" autocomplete="off">
|
||
</div>
|
||
<div class="wis-form-section">
|
||
<label class="wis-gift-toggle">
|
||
<input type="checkbox" id="wis-gift-toggle">
|
||
🎁 Als Geschenk für einen anderen Spieler
|
||
</label>
|
||
<div class="wis-gift-field" id="wis-gift-field">
|
||
<label for="wis-gift-recipient">Empfänger-Spielername</label>
|
||
<input type="text" id="wis-gift-recipient" class="wis-gift-input" placeholder="z.B. Alex456" autocomplete="off">
|
||
<div class="wis-gift-hint">⚠️ Der Empfänger muss das Geschenk ingame annehmen. Bei Ablehnung erhältst du dein Geld zurück.</div>
|
||
</div>
|
||
</div>
|
||
<div class="wis-summary" style="margin-bottom:4px;">
|
||
<div class="wis-summary-total">
|
||
<span>Zu zahlen</span>
|
||
<span id="checkout-total-display"></span>
|
||
</div>
|
||
</div>
|
||
<div id="cart-alert" class="wis-alert"></div>
|
||
</div>
|
||
<div id="wis-panel-success" style="display:none;">
|
||
<div class="wis-success-screen">
|
||
<div class="wis-success-icon">🎉</div>
|
||
<h3 id="wis-success-msg">Bestellung erfolgreich!</h3>
|
||
<p id="wis-success-sub">Deine Items werden ingame bereitgestellt.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="wis-drawer-footer" id="wis-drawer-footer">
|
||
<button class="wis-btn-primary" id="wis-footer-btn">Weiter zur Bestellung →</button>
|
||
<button class="wis-btn-secondary" id="wis-footer-back" style="display:none;">← Zurück zum Warenkorb</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function() {
|
||
const shopCurrency = <?php echo json_encode($currency); ?>;
|
||
const shopExcludeOffers = <?php echo $exclude_offers === '1' ? 'true' : 'false'; ?>;
|
||
const shopTaxEnabled = <?php echo get_option('wis_tax_enabled', '0') === '1' ? 'true' : 'false'; ?>;
|
||
const shopTaxRate = <?php echo floatval(get_option('wis_tax_rate', '0')); ?>;
|
||
const apiBase = <?php echo json_encode(rest_url('wis/v1')); ?>;
|
||
const serverList = <?php echo json_encode(array_map(function($s){ return ['slug'=>$s->slug,'name'=>$s->name]; }, $servers)); ?>;
|
||
const subcatMap = <?php echo json_encode($subcat_map_js); ?>;
|
||
|
||
let cart = [];
|
||
let couponData = {};
|
||
let currentCat = <?php echo json_encode($first_category); ?>;
|
||
let currentSearch = '';
|
||
let currentPage = 1;
|
||
let totalPages = 1;
|
||
let searchTimer = null;
|
||
let allItems = [];
|
||
let itemMap = {}; // id (int) -> item object
|
||
let currentPerPage = <?php echo intval(get_option('wis_default_per_page', 25)); ?>;
|
||
|
||
// -------------------------------------------------------
|
||
// INIT
|
||
// -------------------------------------------------------
|
||
function init() {
|
||
// Drawer an <body> hängen damit z-index korrekt greift
|
||
var drawer = document.getElementById('wis-drawer');
|
||
var overlay = document.getElementById('wis-drawer-overlay');
|
||
if (drawer && document.body) document.body.appendChild(drawer);
|
||
if (overlay && document.body) document.body.appendChild(overlay);
|
||
|
||
// Erste Items laden
|
||
loadItems(1);
|
||
|
||
// ---- Event Delegation: Warenkorb-Buttons im Grid ----
|
||
document.getElementById('wis-grid').addEventListener('click', function(e) {
|
||
|
||
// "In den Warenkorb" Button
|
||
const addBtn = e.target.closest('.wis-btn-add');
|
||
if (addBtn) {
|
||
const card = addBtn.closest('.wis-card');
|
||
const id = parseInt(addBtn.getAttribute('data-item-id'), 10);
|
||
const item = itemMap[id];
|
||
if (!item || !card) return;
|
||
const qtyInput = card.querySelector('.wis-quantity-input');
|
||
const qty = parseInt(qtyInput ? qtyInput.value : '1', 10) || 1;
|
||
addToCartItem(item, qty, addBtn, card);
|
||
return;
|
||
}
|
||
|
||
// Mengen-Buttons
|
||
const qtyBtn = e.target.closest('.wis-quantity-btn');
|
||
if (qtyBtn) {
|
||
const delta = parseInt(qtyBtn.getAttribute('data-delta'), 10);
|
||
const ctrl = qtyBtn.closest('.wis-quantity-control');
|
||
if (!ctrl) return;
|
||
const input = ctrl.querySelector('.wis-quantity-input');
|
||
if (input) input.value = Math.max(1, Math.min(999, (parseInt(input.value, 10) || 1) + delta));
|
||
}
|
||
});
|
||
|
||
// ---- Kategorie-Tabs ----
|
||
document.querySelectorAll('.wis-cat-btn').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
document.querySelectorAll('.wis-cat-btn').forEach(function(b){ b.classList.remove('active'); });
|
||
btn.classList.add('active');
|
||
currentCat = btn.getAttribute('data-cat');
|
||
currentPage = 1;
|
||
// Suche beim Kategoriewechsel leeren
|
||
currentSearch = '';
|
||
var searchInput = document.getElementById('wis-search');
|
||
if (searchInput) searchInput.value = '';
|
||
loadItems(1);
|
||
});
|
||
});
|
||
|
||
// ---- Suche ----
|
||
var searchInput = document.getElementById('wis-search');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', function() {
|
||
clearTimeout(searchTimer);
|
||
searchTimer = setTimeout(function() {
|
||
currentSearch = searchInput.value.trim();
|
||
currentPage = 1;
|
||
// Bei aktiver Suche: Kategorie-Filter deaktivieren (kategorienübergreifend suchen)
|
||
if (currentSearch) {
|
||
document.querySelectorAll('.wis-cat-btn').forEach(function(b){ b.classList.remove('active'); });
|
||
currentCat = '';
|
||
} else {
|
||
// Suche geleert: erste Kategorie wieder aktivieren
|
||
var firstBtn = document.querySelector('.wis-cat-btn');
|
||
if (firstBtn) {
|
||
firstBtn.classList.add('active');
|
||
currentCat = firstBtn.getAttribute('data-cat');
|
||
}
|
||
}
|
||
loadItems(1);
|
||
}, 350);
|
||
});
|
||
}
|
||
|
||
// ---- Server-Filter ----
|
||
var serverSelect = document.getElementById('server-filter');
|
||
if (serverSelect) {
|
||
serverSelect.addEventListener('change', function() {
|
||
renderGrid(allItems);
|
||
});
|
||
}
|
||
|
||
// ---- Per-Page-Selector ----
|
||
var perPageSelect = document.getElementById('wis-per-page');
|
||
if (perPageSelect) {
|
||
perPageSelect.value = String(currentPerPage);
|
||
perPageSelect.addEventListener('change', function() {
|
||
currentPerPage = parseInt(perPageSelect.value, 10);
|
||
currentPage = 1;
|
||
loadItems(1);
|
||
});
|
||
}
|
||
|
||
// ---- Warenkorb öffnen ----
|
||
var openBtn = document.getElementById('wis-open-cart-btn');
|
||
if (openBtn) openBtn.addEventListener('click', openCart);
|
||
|
||
// ---- Modal-Overlay klick ----
|
||
// ---- Drawer Events ----
|
||
var drawerOverlay = document.getElementById('wis-drawer-overlay');
|
||
var drawerClose = document.getElementById('wis-drawer-close');
|
||
var footerBtn = document.getElementById('wis-footer-btn');
|
||
var footerBack = document.getElementById('wis-footer-back');
|
||
if (drawerOverlay) drawerOverlay.addEventListener('click', closeCart);
|
||
if (drawerClose) drawerClose.addEventListener('click', closeCart);
|
||
if (footerBtn) footerBtn.addEventListener('click', onFooterBtn);
|
||
if (footerBack) footerBack.addEventListener('click', goBackToCart);
|
||
|
||
// ---- Gutschein einlösen ----
|
||
var couponBtn = document.getElementById('wis-coupon-btn');
|
||
if (couponBtn) couponBtn.addEventListener('click', validateCoupon);
|
||
|
||
// ---- Gift toggle ----
|
||
var giftToggleInit = document.getElementById('wis-gift-toggle');
|
||
var giftFieldInit = document.getElementById('wis-gift-field');
|
||
if (giftToggleInit) giftToggleInit.addEventListener('change', function() {
|
||
if (giftFieldInit) giftFieldInit.style.display = this.checked ? 'block' : 'none';
|
||
});
|
||
|
||
// ---- Gift-Toggle ----
|
||
var giftToggle = document.getElementById('wis-gift-toggle');
|
||
var giftField = document.getElementById('wis-gift-field');
|
||
if (giftToggle && giftField) {
|
||
giftToggle.addEventListener('change', function() {
|
||
giftField.style.display = giftToggle.checked ? 'block' : 'none';
|
||
if (!giftToggle.checked) {
|
||
document.getElementById('wis-gift-recipient').value = '';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
|
||
// -------------------------------------------------------
|
||
// ITEMS LADEN
|
||
// -------------------------------------------------------
|
||
function loadItems(page) {
|
||
currentPage = page;
|
||
var grid = document.getElementById('wis-grid');
|
||
grid.innerHTML = '<div class="wis-loading" style="grid-column:1/-1;"><div class="wis-loading-spinner"></div><br>Lade Items…</div>';
|
||
document.getElementById('wis-pagination').innerHTML = '';
|
||
|
||
var url = apiBase + '/shop_items?page=' + page;
|
||
if (currentCat) url += '&category=' + encodeURIComponent(currentCat);
|
||
if (currentSearch) url += '&search=' + encodeURIComponent(currentSearch);
|
||
url += '&per_page=' + currentPerPage;
|
||
|
||
fetch(url)
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(data) {
|
||
allItems = data.items || [];
|
||
totalPages = data.total_pages || 1;
|
||
itemMap = {};
|
||
allItems.forEach(function(i){ itemMap[i.id] = i; });
|
||
renderGrid(allItems);
|
||
renderPagination(data.page, data.total_pages, data.total);
|
||
})
|
||
.catch(function() {
|
||
grid.innerHTML = '<div class="wis-loading" style="grid-column:1/-1; color:#dc3545;">Fehler beim Laden der Items.</div>';
|
||
});
|
||
}
|
||
|
||
// -------------------------------------------------------
|
||
// GRID RENDERN — bei Hauptkat mit Unterkats: gruppiert
|
||
// -------------------------------------------------------
|
||
function renderGrid(items) {
|
||
var grid = document.getElementById('wis-grid');
|
||
var serverSlug = document.getElementById('server-filter').value;
|
||
|
||
var filtered = serverSlug
|
||
? items.filter(function(i){ return i.servers.indexOf(serverSlug) !== -1; })
|
||
: items;
|
||
|
||
if (filtered.length === 0) {
|
||
grid.innerHTML = '<div style="grid-column:1/-1; text-align:center; padding:40px; background:#fff; border-radius:10px; color:#888;">Keine Items gefunden.</div>';
|
||
return;
|
||
}
|
||
|
||
// Prüfen ob aktive Kategorie eine Hauptkat mit Unterkats ist
|
||
var subs = subcatMap[currentCat] || [];
|
||
if (subs.length > 0) {
|
||
// Gruppierte Ansicht → wis-grid als normaler Block-Container nutzen
|
||
grid.style.display = 'block';
|
||
var subSlugs = subs.map(function(s){ return s.slug; });
|
||
var html = '';
|
||
|
||
// Items direkt in der Hauptkat (nicht in einer Sub)
|
||
var directItems = filtered.filter(function(item) {
|
||
return (item.categories || []).indexOf(currentCat) !== -1
|
||
&& !(item.categories || []).some(function(s){ return subSlugs.indexOf(s) !== -1; });
|
||
});
|
||
if (directItems.length > 0) {
|
||
html += '<div class="wis-subgrid">' + directItems.map(renderCard).join('') + '</div>';
|
||
}
|
||
|
||
// Unterkategorie-Blöcke mit Trennlinie
|
||
subs.forEach(function(sub) {
|
||
var subItems = filtered.filter(function(item) {
|
||
return (item.categories || []).indexOf(sub.slug) !== -1;
|
||
});
|
||
if (subItems.length === 0) return;
|
||
html += '<div class="wis-subgroup-divider">'
|
||
+ '<hr class="wis-sub-hr"><span class="wis-sub-label">' + escHtml(sub.name) + '</span><hr class="wis-sub-hr">'
|
||
+ '</div>'
|
||
+ '<div class="wis-subgrid">' + subItems.map(renderCard).join('') + '</div>';
|
||
});
|
||
|
||
grid.innerHTML = html || '<p style="text-align:center;padding:40px;color:#888;">Keine Items gefunden.</p>';
|
||
} else {
|
||
// Normale flache Grid-Ansicht
|
||
grid.style.display = '';
|
||
grid.innerHTML = filtered.map(renderCard).join('');
|
||
}
|
||
}
|
||
|
||
// Einzelne Karte rendern
|
||
function renderCard(item) {
|
||
var price = item.offer_price > 0 ? item.offer_price : item.price;
|
||
var showOld = item.offer_price > 0 && item.offer_price !== item.price;
|
||
var serverNames = (item.servers || [])
|
||
.map(function(slug) {
|
||
var s = serverList.find(function(x){ return x.slug === slug; });
|
||
return s ? s.name : slug;
|
||
})
|
||
.join(', ') || 'Kein Server';
|
||
|
||
var badge = item.is_daily_deal
|
||
? '<div class="wis-daily-badge">🎁 Angebot des Tages</div>'
|
||
: (item.is_offer ? '<div class="wis-offer-badge">🔥 Angebot</div>' : '');
|
||
|
||
var oldPrice = showOld
|
||
? '<div class="wis-card-price-old">' + item.price + ' ' + shopCurrency + '</div>'
|
||
: '';
|
||
|
||
var desc = item.description
|
||
? '<div class="wis-card-desc">' + escHtml(item.description) + '</div>'
|
||
: '';
|
||
|
||
return '<div class="wis-card' + (item.is_offer ? ' offer' : '') + '">'
|
||
+ badge
|
||
+ '<div class="wis-card-img' + (item.has_custom_image ? ' wis-card-img--custom' : '') + '">'
|
||
+ '<img src="' + escHtml(item.image) + '" alt="' + escHtml(item.name) + '" loading="lazy"'
|
||
+ ' onerror="this.onerror=null;this.src=\'https://via.placeholder.com/100/333/fff?text=?\';">'
|
||
+ '</div>'
|
||
+ '<div class="wis-card-body">'
|
||
+ '<h3 class="wis-card-title" title="' + escHtml(item.name) + '">' + escHtml(item.name) + '</h3>'
|
||
+ desc
|
||
+ (function() {
|
||
var gcP = (item.item_id || item.id || '').match(/^gift_card_(\d+)_(\d+)$/);
|
||
if (gcP) {
|
||
return '<div class="wis-card-price-container">'
|
||
+ '<div class="wis-card-price">' + parseInt(gcP[1],10) + ' – ' + parseInt(gcP[2],10) + ' ' + shopCurrency + '</div>'
|
||
+ '</div>';
|
||
}
|
||
return '<div class="wis-card-price-container">'
|
||
+ oldPrice
|
||
+ '<div class="wis-card-price">' + price + ' ' + shopCurrency + '</div>'
|
||
+ '</div>';
|
||
})()
|
||
+ '<div class="wis-card-servers">📡 ' + escHtml(serverNames) + '</div>'
|
||
+ (function() {
|
||
var gcM = (item.item_id || item.id || '').match(/^gift_card_(\d+)_(\d+)$/);
|
||
if (gcM) {
|
||
var mn = parseInt(gcM[1], 10), mx = parseInt(gcM[2], 10);
|
||
return '<div style="margin:10px 0;">'
|
||
+ '<label style="font-size:12px;color:#aaa;display:block;margin-bottom:5px;">'
|
||
+ '💰 Betrag eingeben (' + mn + '–' + mx + ' ' + shopCurrency + ')</label>'
|
||
+ '<input type="number" class="wis-gift-amount-input" '
|
||
+ 'value="' + mn + '" min="' + mn + '" max="' + mx + '" step="1" '
|
||
+ 'style="width:100%;padding:8px 10px;border-radius:6px;border:2px solid #00bcd4;'
|
||
+ 'background:#1a1a2e;color:#fff;font-size:15px;font-weight:700;box-sizing:border-box;'
|
||
+ '-moz-appearance:textfield;appearance:textfield;">'
|
||
+ '</div>';
|
||
}
|
||
return '<div class="wis-quantity-control">'
|
||
+ '<button class="wis-quantity-btn" data-delta="-1" type="button">-</button>'
|
||
+ '<input type="number" class="wis-quantity-input" value="1" min="1" max="999">'
|
||
+ '<button class="wis-quantity-btn" data-delta="1" type="button">+</button>'
|
||
+ '</div>';
|
||
})()
|
||
+ '<button class="wis-btn-add" data-item-id="' + item.id + '" type="button">➕ In den Warenkorb</button>'
|
||
+ '</div>'
|
||
+ '</div>';
|
||
}
|
||
|
||
// -------------------------------------------------------
|
||
// PAGINATION
|
||
// -------------------------------------------------------
|
||
function renderPagination(page, pages, total) {
|
||
var el = document.getElementById('wis-pagination');
|
||
if (pages <= 1) { el.innerHTML = ''; return; }
|
||
|
||
var html = '';
|
||
html += '<button class="wis-page-btn" ' + (page <= 1 ? 'disabled' : '') + ' data-page="' + (page - 1) + '">‹ Zurück</button>';
|
||
|
||
var start = Math.max(1, page - 2);
|
||
var end = Math.min(pages, page + 2);
|
||
if (start > 1) {
|
||
html += '<button class="wis-page-btn" data-page="1">1</button>';
|
||
if (start > 2) html += '<span style="color:#999">…</span>';
|
||
}
|
||
for (var p = start; p <= end; p++) {
|
||
html += '<button class="wis-page-btn' + (p === page ? ' active' : '') + '" data-page="' + p + '">' + p + '</button>';
|
||
}
|
||
if (end < pages) {
|
||
if (end < pages - 1) html += '<span style="color:#999">…</span>';
|
||
html += '<button class="wis-page-btn" data-page="' + pages + '">' + pages + '</button>';
|
||
}
|
||
html += '<button class="wis-page-btn" ' + (page >= pages ? 'disabled' : '') + ' data-page="' + (page + 1) + '">Weiter ›</button>';
|
||
html += '<span class="wis-page-info">' + total + ' Items</span>';
|
||
el.innerHTML = html;
|
||
|
||
// Pagination-Buttons via Event Delegation
|
||
el.querySelectorAll('.wis-page-btn:not([disabled])').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var p = parseInt(btn.getAttribute('data-page'), 10);
|
||
if (p) {
|
||
loadItems(p);
|
||
document.querySelector('.wis-shop').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// -------------------------------------------------------
|
||
// WARENKORB LOGIK
|
||
// -------------------------------------------------------
|
||
function addToCartItem(item, qty, btn, card) {
|
||
var price = item.offer_price > 0 ? item.offer_price : item.price;
|
||
|
||
// Gutschein-Karte: Betrag aus Eingabefeld lesen
|
||
var customAmount = 0;
|
||
var gcMatch = (item.item_id || item.id || '').match(/^gift_card_(\d+)_(\d+)$/);
|
||
if (gcMatch) {
|
||
var gcInput = card.querySelector('.wis-gift-amount-input');
|
||
var gcMin = parseInt(gcMatch[1], 10);
|
||
var gcMax = parseInt(gcMatch[2], 10);
|
||
customAmount = gcInput ? Math.max(gcMin, Math.min(gcMax, parseInt(gcInput.value, 10) || gcMin)) : gcMin;
|
||
price = customAmount; // Preis im Warenkorb = Wunschbetrag
|
||
}
|
||
|
||
// Gutschein-Karten nie zusammenlegen (jede hat eigenen Code)
|
||
var isGiftCard = !!gcMatch;
|
||
var existing = !isGiftCard && cart.find(function(i){ return i.id === item.id; });
|
||
if (existing) {
|
||
existing.quantity += qty;
|
||
} else {
|
||
cart.push({
|
||
id: item.id,
|
||
image: item.image || '',
|
||
name: item.name,
|
||
price: price,
|
||
quantity: qty,
|
||
servers: item.servers,
|
||
is_offer: !!item.is_offer,
|
||
custom_amount: customAmount || undefined
|
||
});
|
||
}
|
||
updateCartBadge();
|
||
btn.textContent = '✅ Hinzugefügt';
|
||
btn.style.background = '#28a745';
|
||
setTimeout(function() {
|
||
btn.textContent = '➕ In den Warenkorb';
|
||
btn.style.background = '';
|
||
}, 1500);
|
||
var qtyInput = card.querySelector('.wis-quantity-input');
|
||
if (qtyInput) qtyInput.value = 1;
|
||
}
|
||
|
||
function updateCartBadge() {
|
||
var total = cart.reduce(function(s, i){ return s + i.quantity; }, 0);
|
||
document.getElementById('cart-count').textContent = total;
|
||
}
|
||
|
||
var drawerStep = 1;
|
||
|
||
function openCart() {
|
||
goToStep(1);
|
||
renderCart();
|
||
document.getElementById('wis-drawer').classList.add('open');
|
||
document.getElementById('wis-drawer-overlay').classList.add('open');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
function closeCart() {
|
||
document.getElementById('wis-drawer').classList.remove('open');
|
||
document.getElementById('wis-drawer-overlay').classList.remove('open');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
function goToStep(step) {
|
||
drawerStep = step;
|
||
var panelCart = document.getElementById('wis-panel-cart');
|
||
var panelCheckout = document.getElementById('wis-panel-checkout');
|
||
var panelSuccess = document.getElementById('wis-panel-success');
|
||
var step1El = document.getElementById('wis-step-1');
|
||
var step2El = document.getElementById('wis-step-2');
|
||
var footerBtn = document.getElementById('wis-footer-btn');
|
||
var footerBack = document.getElementById('wis-footer-back');
|
||
var drawerTitle = document.getElementById('wis-drawer-title');
|
||
var stepsEl = document.getElementById('wis-steps');
|
||
panelCart.style.display = 'none';
|
||
panelCheckout.style.display = 'none';
|
||
panelSuccess.style.display = 'none';
|
||
step1El.className = 'wis-step';
|
||
step2El.className = 'wis-step';
|
||
if (step === 1) {
|
||
panelCart.style.display = 'block';
|
||
step1El.className = 'wis-step active';
|
||
drawerTitle.textContent = 'Warenkorb';
|
||
stepsEl.style.display = 'flex';
|
||
footerBtn.style.display = 'block';
|
||
footerBack.style.display = 'none';
|
||
footerBtn.textContent = 'Weiter zur Bestellung →';
|
||
footerBtn.disabled = cart.length === 0;
|
||
} else if (step === 2) {
|
||
panelCheckout.style.display = 'block';
|
||
step1El.className = 'wis-step done';
|
||
step2El.className = 'wis-step active';
|
||
drawerTitle.textContent = 'Bestellung abschließen';
|
||
stepsEl.style.display = 'flex';
|
||
footerBtn.style.display = 'block';
|
||
footerBack.style.display = 'block';
|
||
footerBtn.textContent = '💰 Kauf abschließen';
|
||
footerBtn.disabled = false;
|
||
var ctd = document.getElementById('checkout-total-display');
|
||
if (ctd) ctd.textContent = calculateTotal() + ' ' + shopCurrency;
|
||
} else if (step === 3) {
|
||
panelSuccess.style.display = 'block';
|
||
stepsEl.style.display = 'none';
|
||
drawerTitle.textContent = 'Bestellung erfolgreich';
|
||
footerBtn.style.display = 'none';
|
||
footerBack.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function onFooterBtn() {
|
||
if (drawerStep === 1) {
|
||
if (cart.length === 0) return;
|
||
goToStep(2);
|
||
} else if (drawerStep === 2) {
|
||
checkout();
|
||
}
|
||
}
|
||
|
||
function goBackToCart() { goToStep(1); }
|
||
|
||
function calculateSubtotal() {
|
||
var normal = 0, offer = 0;
|
||
cart.forEach(function(item) {
|
||
var t = item.price * item.quantity;
|
||
if (shopExcludeOffers && item.is_offer) offer += t; else normal += t;
|
||
});
|
||
var discount = 0;
|
||
if (couponData && typeof couponData.value !== 'undefined') {
|
||
discount = couponData.type === 'percent'
|
||
? Math.floor(normal * couponData.value / 100)
|
||
: couponData.value;
|
||
}
|
||
return Math.max(0, normal - discount) + offer;
|
||
}
|
||
|
||
function calculateTax(subtotal) {
|
||
if (!shopTaxEnabled || shopTaxRate <= 0) return 0;
|
||
return Math.floor(subtotal * shopTaxRate / 100);
|
||
}
|
||
|
||
function calculateTotal() {
|
||
var subtotal = calculateSubtotal();
|
||
return subtotal + calculateTax(subtotal);
|
||
}
|
||
|
||
function renderCart() {
|
||
var content = document.getElementById('cart-content');
|
||
var couponSec = document.getElementById('wis-coupon-section');
|
||
var summarySec = document.getElementById('wis-summary-section');
|
||
var footerBtn = document.getElementById('wis-footer-btn');
|
||
if (cart.length === 0) {
|
||
content.innerHTML = '<div class="wis-cart-empty"><div class="wis-cart-empty-icon">🛒</div><p>Dein Warenkorb ist leer</p></div>';
|
||
if (couponSec) couponSec.style.display = 'none';
|
||
if (summarySec) summarySec.style.display = 'none';
|
||
if (footerBtn) footerBtn.disabled = true;
|
||
return;
|
||
}
|
||
if (footerBtn) footerBtn.disabled = false;
|
||
content.innerHTML = cart.map(function(item, idx) {
|
||
var linePrice = item.price * item.quantity;
|
||
return '<div class="wis-ci">'
|
||
+ '<img class="wis-ci-img" src="' + escHtml(item.image || '') + '" alt="" onerror="this.src=\'https://via.placeholder.com/56/eee/999?text=?\'">'
|
||
+ '<div class="wis-ci-info">'
|
||
+ '<div class="wis-ci-name" title="' + escHtml(item.name) + '">' + escHtml(item.name) + '</div>'
|
||
+ '<div class="wis-ci-price">' + item.price + ' ' + shopCurrency + ' / Stück – <strong>' + linePrice + ' ' + shopCurrency + '</strong></div>'
|
||
+ '<div class="wis-ci-qty">'
|
||
+ '<button class="wis-ci-qty-btn" data-idx="' + idx + '" data-delta="-1" type="button">−</button>'
|
||
+ '<span class="wis-ci-qty-val">' + item.quantity + '</span>'
|
||
+ '<button class="wis-ci-qty-btn" data-idx="' + idx + '" data-delta="1" type="button">+</button>'
|
||
+ '</div>'
|
||
+ '</div>'
|
||
+ '<button class="wis-ci-del" data-cart-idx="' + idx + '" type="button" title="Entfernen">🗑️</button>'
|
||
+ '</div>';
|
||
}).join('');
|
||
|
||
// Qty +/- buttons
|
||
content.querySelectorAll('.wis-ci-qty-btn').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var idx = parseInt(btn.getAttribute('data-idx'), 10);
|
||
var delta = parseInt(btn.getAttribute('data-delta'), 10);
|
||
cart[idx].quantity = Math.max(1, Math.min(999, cart[idx].quantity + delta));
|
||
updateCartBadge();
|
||
renderCart();
|
||
});
|
||
});
|
||
|
||
// Remove buttons
|
||
content.querySelectorAll('.wis-ci-del').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var idx = parseInt(btn.getAttribute('data-cart-idx'), 10);
|
||
cart.splice(idx, 1);
|
||
updateCartBadge();
|
||
renderCart();
|
||
});
|
||
});
|
||
|
||
// Show coupon + summary
|
||
if (couponSec) couponSec.style.display = 'block';
|
||
if (summarySec) summarySec.style.display = 'block';
|
||
|
||
// Update summary rows
|
||
var sub = calculateSubtotal();
|
||
var tax = calculateTax(sub);
|
||
var total = sub + tax;
|
||
var hasDiscount = couponData && typeof couponData.value !== 'undefined';
|
||
var discountAmt = 0;
|
||
if (hasDiscount) {
|
||
var raw = cart.reduce(function(s,i){ return s + i.price*i.quantity; }, 0);
|
||
discountAmt = couponData.type === 'percent' ? Math.floor(raw * couponData.value / 100) : couponData.value;
|
||
}
|
||
var rowSubtotal = document.getElementById('wis-row-subtotal');
|
||
var rowDiscount = document.getElementById('wis-row-discount');
|
||
var rowTax = document.getElementById('wis-row-tax');
|
||
var cartSubtotal = document.getElementById('cart-subtotal');
|
||
var cartDiscount = document.getElementById('cart-discount');
|
||
var cartTaxEl = document.getElementById('cart-tax');
|
||
var cartTotalEl = document.getElementById('cart-total');
|
||
if (rowSubtotal && (hasDiscount || (shopTaxEnabled && tax > 0))) {
|
||
rowSubtotal.style.display = 'flex'; cartSubtotal.textContent = sub + discountAmt + ' ' + shopCurrency;
|
||
} else if (rowSubtotal) { rowSubtotal.style.display = 'none'; }
|
||
if (rowDiscount && hasDiscount && discountAmt > 0) {
|
||
rowDiscount.style.display = 'flex'; cartDiscount.textContent = '−' + discountAmt + ' ' + shopCurrency;
|
||
} else if (rowDiscount) { rowDiscount.style.display = 'none'; }
|
||
if (rowTax && shopTaxEnabled && tax > 0) {
|
||
rowTax.style.display = 'flex'; cartTaxEl.textContent = '+' + tax + ' ' + shopCurrency;
|
||
} else if (rowTax) { rowTax.style.display = 'none'; }
|
||
if (cartTotalEl) cartTotalEl.textContent = total + ' ' + shopCurrency;
|
||
}
|
||
|
||
function validateCoupon() {
|
||
var code = document.getElementById('coupon-code').value.trim().toUpperCase();
|
||
var msgEl = document.getElementById('coupon-msg');
|
||
if (!code) { couponData = {}; msgEl.textContent = ''; renderCart(); return; }
|
||
msgEl.textContent = 'Prüfe…';
|
||
msgEl.style.color = '#0073aa';
|
||
fetch(apiBase + '/validate_coupon', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({code: code, cart: cart, player: document.getElementById('checkout-player') ? document.getElementById('checkout-player').value.trim() : ''})
|
||
})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(data) {
|
||
if (data.success) {
|
||
couponData = {type: data.type, value: data.value};
|
||
msgEl.textContent = '✅ ' + data.message;
|
||
msgEl.style.color = 'green';
|
||
} else {
|
||
couponData = {};
|
||
msgEl.textContent = '❌ ' + data.message;
|
||
msgEl.style.color = 'red';
|
||
}
|
||
renderCart();
|
||
});
|
||
}
|
||
|
||
function checkout() {
|
||
var player = document.getElementById('checkout-player').value.trim();
|
||
var server = document.getElementById('checkout-server').value;
|
||
var couponCode = document.getElementById('coupon-code').value.trim();
|
||
var alertEl = document.getElementById('cart-alert');
|
||
var btn = document.getElementById('wis-footer-btn');
|
||
|
||
// Gift-Feld auslesen
|
||
var giftToggle = document.getElementById('wis-gift-toggle');
|
||
var giftRecipient = '';
|
||
if (giftToggle && giftToggle.checked) {
|
||
giftRecipient = (document.getElementById('wis-gift-recipient').value || '').trim();
|
||
}
|
||
|
||
if (!player) { alertEl.textContent = 'Bitte Spielername eingeben!'; alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return; }
|
||
if (!server) { alertEl.textContent = 'Bitte Server auswählen!'; alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return; }
|
||
|
||
// Gift-Validierungen im Frontend
|
||
if (giftToggle && giftToggle.checked) {
|
||
if (!giftRecipient) {
|
||
alertEl.textContent = '🎁 Bitte den Empfänger-Spielernamen eingeben!';
|
||
alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return;
|
||
}
|
||
if (giftRecipient.toLowerCase() === player.toLowerCase()) {
|
||
alertEl.textContent = '🎁 Du kannst dir nicht selbst etwas schenken!';
|
||
alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return;
|
||
}
|
||
}
|
||
|
||
var invalid = cart.filter(function(i){ return i.servers.indexOf(server) === -1; });
|
||
if (invalid.length) { alertEl.textContent = 'Einige Items sind nicht für diesen Server!'; alertEl.className = 'wis-alert error'; alertEl.style.display = 'block'; return; }
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Speichere…';
|
||
|
||
function doOrder(confirmedNoCoupon) {
|
||
// cart mit custom_amount für Gutschein-Karten serialisieren
|
||
var cartPayload = cart.map(function(item) {
|
||
var c = {id: item.id, quantity: item.quantity};
|
||
if (item.custom_amount) c.custom_amount = item.custom_amount;
|
||
return c;
|
||
});
|
||
var payload = {player: player, cart: cartPayload, server: server, coupon_code: couponCode};
|
||
if (giftRecipient) payload.gift_recipient = giftRecipient;
|
||
if (confirmedNoCoupon) payload.confirmed_no_coupon = true;
|
||
|
||
fetch(apiBase + '/order', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload)
|
||
})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(data) {
|
||
if (data.coupon_error) {
|
||
// Gutschein ungültig – Bestätigung einholen ob zum vollen Preis kaufen
|
||
btn.disabled = false;
|
||
btn.textContent = '💰 Kauf abschließen';
|
||
|
||
// Bestätigungs-Overlay einblenden
|
||
var overlay = document.getElementById('wis-coupon-confirm-overlay');
|
||
if (!overlay) {
|
||
overlay = document.createElement('div');
|
||
overlay.id = 'wis-coupon-confirm-overlay';
|
||
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:2147483647;display:flex;align-items:center;justify-content:center;';
|
||
overlay.innerHTML = '<div style="background:#fff;border-radius:14px;padding:32px 28px;max-width:420px;width:90%;text-align:center;box-shadow:0 8px 40px rgba(0,0,0,.25);">'
|
||
+ '<div style="font-size:2rem;margin-bottom:12px;">⚠️</div>'
|
||
+ '<h3 style="margin:0 0 10px;font-size:1.1rem;color:#e53935;font-weight:700;" id="wis-coupon-confirm-msg"></h3>'
|
||
+ '<p style="color:#666;font-size:.9rem;margin:0 0 8px;">Möchtest du den Kauf trotzdem <strong>zum vollen Preis</strong> abschließen?</p>'
|
||
+ '<p style="color:#333;font-size:1rem;font-weight:700;margin:0 0 22px;">Gesamtbetrag: ' + (function(){ var saved = couponData; couponData = {}; var t = calculateTotal(); couponData = saved; return t; })() + ' ' + shopCurrency + '</p>'
|
||
+ '<div style="display:flex;gap:12px;justify-content:center;">'
|
||
+ '<button id="wis-coupon-confirm-yes" style="background:#667eea;color:#fff;border:none;border-radius:8px;padding:10px 22px;font-size:.95rem;font-weight:600;cursor:pointer;">Ja, zum vollen Preis kaufen</button>'
|
||
+ '<button id="wis-coupon-confirm-no" style="background:#eee;color:#333;border:none;border-radius:8px;padding:10px 22px;font-size:.95rem;cursor:pointer;">Abbrechen</button>'
|
||
+ '</div></div>';
|
||
document.body.appendChild(overlay);
|
||
document.getElementById('wis-coupon-confirm-yes').addEventListener('click', function() {
|
||
overlay.remove();
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Speichere…'; btn.disabled = true;
|
||
doOrder(true);
|
||
});
|
||
document.getElementById('wis-coupon-confirm-no').addEventListener('click', function() {
|
||
overlay.remove();
|
||
});
|
||
}
|
||
document.getElementById('wis-coupon-confirm-msg').textContent = data.message;
|
||
overlay.style.display = 'flex';
|
||
return;
|
||
}
|
||
|
||
if (data.success) {
|
||
cart = []; couponData = {};
|
||
if (giftToggle) { giftToggle.checked = false; }
|
||
var giftFieldEl = document.getElementById('wis-gift-field');
|
||
if (giftFieldEl) giftFieldEl.style.display = 'none';
|
||
document.getElementById('wis-gift-recipient').value = '';
|
||
updateCartBadge();
|
||
var msgEl = document.getElementById('wis-success-msg');
|
||
var subEl = document.getElementById('wis-success-sub');
|
||
if (msgEl) msgEl.textContent = data.message || 'Bestellung erfolgreich!';
|
||
|
||
// Gift-Card-Codes werden erst nach Kaufabschluss (complete_order) ingame zugesendet
|
||
// Sie werden hier NICHT angezeigt – der Spieler erhält sie via Spigot-Nachricht
|
||
if (subEl) {
|
||
subEl.textContent = 'Deine Items werden ingame bereitgestellt.';
|
||
}
|
||
|
||
goToStep(3);
|
||
setTimeout(function(){ closeCart(); location.reload(); }, 3500);
|
||
} else {
|
||
alertEl.textContent = data.message;
|
||
alertEl.className = 'wis-alert error';
|
||
alertEl.style.display = 'block';
|
||
}
|
||
})
|
||
.finally(function() {
|
||
btn.disabled = false;
|
||
btn.textContent = '💰 Kauf abschließen';
|
||
});
|
||
}
|
||
|
||
doOrder(false);
|
||
}
|
||
|
||
// -------------------------------------------------------
|
||
// HILFSFUNKTIONEN
|
||
// -------------------------------------------------------
|
||
function escHtml(str) {
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
})();
|
||
</script>
|
||
<?php
|
||
return ob_get_clean();
|
||
}
|
||
}
|
||
|
||
// ===========================================================
|
||
// SIDEBAR WIDGET
|
||
// ===========================================================
|
||
class WIS_Sidebar_Widget extends WP_Widget {
|
||
public function __construct() {
|
||
parent::__construct(
|
||
'wis_sidebar_offer',
|
||
'WIS Shop Angebot',
|
||
['description' => 'Zeigt Daily Deal oder Angebot an']
|
||
);
|
||
}
|
||
|
||
public function widget($args, $instance) {
|
||
echo $args['before_widget'];
|
||
|
||
if (!empty($instance['title'])) {
|
||
echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title'];
|
||
}
|
||
|
||
global $wpdb;
|
||
$currency = get_option('wis_currency_name', 'Coins');
|
||
$img_base = get_option('wis_image_base_url', '');
|
||
|
||
$item = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}wis_items WHERE is_daily_deal = 1 AND status = 'publish' LIMIT 1");
|
||
|
||
if (!$item) {
|
||
$item = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}wis_items WHERE is_offer = 1 AND status = 'publish' ORDER BY updated_at DESC LIMIT 1");
|
||
}
|
||
|
||
if ($item) {
|
||
$price = $item->offer_price > 0 ? $item->offer_price : $item->price;
|
||
$show_old = $item->offer_price > 0 && $item->offer_price != $item->price;
|
||
$img_url = WIS_DB::get_item_image($item);
|
||
$target_url = !empty($instance['shop_url']) ? $instance['shop_url'] : home_url();
|
||
|
||
?>
|
||
<div style="background:#fff; border-radius:10px; padding:0; overflow:hidden; box-shadow:0 4px 10px rgba(0,0,0,0.05); text-align:center;">
|
||
<div style="position:relative; background:#2d2d2d; padding:15px; height:160px; display:flex; align-items:center; justify-content:center;">
|
||
<?php if ($item->is_daily_deal): ?>
|
||
<div style="position:absolute; top:10px; left:10px; background:linear-gradient(135deg, #6f42c1, #8e44ad); color:#fff; padding:4px 10px; font-size:10px; border-radius:20px; font-weight:bold; z-index:2;">🎁 DAILY DEAL</div>
|
||
<?php elseif ($item->is_offer): ?>
|
||
<div style="position:absolute; top:10px; left:10px; background:linear-gradient(135deg, #ff416c, #ff4b2b); color:#fff; padding:4px 10px; font-size:10px; border-radius:20px; font-weight:bold; z-index:2;">🔥 ANGEBOT</div>
|
||
<?php endif; ?>
|
||
<img src="<?php echo esc_url($img_url); ?>" alt="<?php echo esc_attr($item->name); ?>" style="max-width:90%; max-height:90%; object-fit:contain; filter:drop-shadow(0 4px 8px rgba(0,0,0,0.6));">
|
||
</div>
|
||
<div style="padding:15px;">
|
||
<h4 style="margin:0 0 8px 0; font-size:15px; color:#333; font-weight:700;"><?php echo esc_html($item->name); ?></h4>
|
||
<div style="margin-bottom:12px;">
|
||
<?php if ($show_old): ?>
|
||
<span style="text-decoration:line-through; color:#999; font-size:11px;"><?php echo esc_html($item->price); ?> <?php echo esc_html($currency); ?></span>
|
||
<?php endif; ?>
|
||
<span style="font-size:20px; font-weight:800; color:#28a745;"><?php echo esc_html($price); ?> <span style="font-size:12px; font-weight:400; color:#666;"><?php echo esc_html($currency); ?></span></span>
|
||
</div>
|
||
<a href="<?php echo esc_url($target_url); ?>" style="display:block; padding:10px 0; background:linear-gradient(135deg, #667eea 0%, #764ba2 100%); color:#fff; text-decoration:none; border-radius:6px; font-weight:bold; font-size:13px;">
|
||
<?php echo esc_html($instance['btn_text'] ?? 'Zum Shop'); ?> 🛒
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
} else {
|
||
echo '<div style="background:#f8f9fa; border:1px dashed #ddd; border-radius:8px; padding:20px; text-align:center;"><p style="margin:0; color:#888; font-size:13px;">Kein Angebot verfügbar.</p></div>';
|
||
}
|
||
|
||
echo $args['after_widget'];
|
||
}
|
||
|
||
public function form($instance) {
|
||
$title = !empty($instance['title']) ? $instance['title'] : '🔥 Angebot des Tages';
|
||
$btn_text = !empty($instance['btn_text']) ? $instance['btn_text'] : 'Zum Shop';
|
||
$shop_url = !empty($instance['shop_url']) ? $instance['shop_url'] : '';
|
||
?>
|
||
<p>
|
||
<label for="<?php echo $this->get_field_id('title'); ?>">Titel:</label>
|
||
<input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||
</p>
|
||
<p>
|
||
<label for="<?php echo $this->get_field_id('btn_text'); ?>">Button Text:</label>
|
||
<input class="widefat" id="<?php echo $this->get_field_id('btn_text'); ?>" name="<?php echo $this->get_field_name('btn_text'); ?>" type="text" value="<?php echo esc_attr($btn_text); ?>">
|
||
</p>
|
||
<p>
|
||
<label for="<?php echo $this->get_field_id('shop_url'); ?>">Shop URL:</label>
|
||
<input class="widefat" id="<?php echo $this->get_field_id('shop_url'); ?>" name="<?php echo $this->get_field_name('shop_url'); ?>" type="text" value="<?php echo esc_attr($shop_url); ?>">
|
||
</p>
|
||
<?php
|
||
}
|
||
|
||
public function update($new_instance, $old_instance) {
|
||
$instance = [];
|
||
$instance['title'] = !empty($new_instance['title']) ? sanitize_text_field($new_instance['title']) : '';
|
||
$instance['btn_text'] = !empty($new_instance['btn_text']) ? sanitize_text_field($new_instance['btn_text']) : 'Zum Shop';
|
||
$instance['shop_url'] = !empty($new_instance['shop_url']) ? esc_url_raw($new_instance['shop_url']) : '';
|
||
return $instance;
|
||
}
|
||
}
|
||
|
||
// ===========================================================
|
||
// INITIALIZATION
|
||
// ===========================================================
|
||
register_activation_hook(__FILE__, [WIS_Activator::class, 'activate']);
|
||
register_deactivation_hook(__FILE__, [WIS_Activator::class, 'deactivate']);
|
||
|
||
add_action('admin_menu', [WIS_Admin::class, 'register_menu']);
|
||
add_action('admin_footer', function() {
|
||
$screen = get_current_screen();
|
||
if (!str_contains($screen->id ?? '', 'wis_')) return;
|
||
echo '<script>
|
||
document.addEventListener("wheel", function(e) {
|
||
if (document.activeElement && document.activeElement.type === "number") {
|
||
document.activeElement.blur();
|
||
}
|
||
}, { passive: false });
|
||
</script>';
|
||
});
|
||
add_action('admin_head', function() {
|
||
$screen = get_current_screen();
|
||
if (!str_contains($screen->id ?? '', 'wis_')) return;
|
||
echo '<style>
|
||
input[type="number"]::-webkit-inner-spin-button,
|
||
input[type="number"]::-webkit-outer-spin-button {
|
||
-webkit-appearance: none !important;
|
||
appearance: none !important;
|
||
margin: 0 !important;
|
||
}
|
||
input[type="number"] {
|
||
-moz-appearance: textfield !important;
|
||
appearance: textfield !important;
|
||
}
|
||
</style>';
|
||
});
|
||
add_action('admin_init', [WIS_Admin::class, 'handle_save_item']);
|
||
add_action('rest_api_init',[WIS_API::class, 'register_routes']);
|
||
add_action('init', [WIS_Shortcode::class, 'register']);
|
||
add_action('widgets_init', function() {
|
||
register_widget('WIS_Sidebar_Widget');
|
||
});
|
||
add_action('wis_daily_deal_event', [WIS_Activator::class, 'run_daily_deal']);
|
||
add_action('wis_abo_renewal_event', [WIS_Activator::class, 'run_abo_renewal']);
|
||
add_action('wis_item_abo_delivery_event', [WIS_Activator::class, 'run_item_abo_delivery']);
|
||
|
||
// ── Auto-Expire: Orders nach 7 Tagen ohne Ingame-Bestätigung stornieren ──
|
||
add_action('wis_auto_expire_orders', function () {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'wis_orders';
|
||
|
||
// Alle Orders die seit > 7 Tagen pending/claimed sind → cancelled
|
||
$affected = $wpdb->query(
|
||
"UPDATE {$table}
|
||
SET status = 'cancelled'
|
||
WHERE status IN ('pending', 'claimed')
|
||
AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY)"
|
||
);
|
||
|
||
if ($affected > 0) {
|
||
error_log("[WIS] Auto-Expire: {$affected} Order(s) nach 7 Tagen storniert.");
|
||
}
|
||
});
|