diff --git a/wp-ingame-shop/wp-ingame-shop-pro.php b/wp-ingame-shop/wp-ingame-shop-pro.php index 4484928..fa7b671 100644 --- a/wp-ingame-shop/wp-ingame-shop-pro.php +++ b/wp-ingame-shop/wp-ingame-shop-pro.php @@ -3,7 +3,7 @@ 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.4 +Version: 2.1.3 Author: M_Viper Author URI: https://m-viper.de Requires at least: 6.9.1 @@ -20,7 +20,7 @@ Support: [Telegram Support](https://t.me/M_Viper04) if (!defined('ABSPATH')) exit; // Plugin Constants -define('WIS_VERSION', '2.1.4'); +define('WIS_VERSION', '2.1.3'); define('WIS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WIS_PLUGIN_URL', plugin_dir_url(__FILE__)); @@ -137,6 +137,10 @@ class WIS_Dashboard { ?> prefix . 'wis_coupons'; + $codes = $wpdb->get_results($wpdb->prepare( + "SELECT code, value, type, usage_limit, expiry, min_order_value FROM $table WHERE bulk_id = %s ORDER BY id ASC", + $bulk_id + )); + if ($codes) { + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename="gutscheine-' . $bulk_id . '.csv"'); + $out = fopen('php://output', 'w'); + fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); // UTF-8 BOM + fputcsv($out, ['Code', 'Rabatt', 'Typ', 'Max. Einlösungen', 'Gültig bis', 'Mindestbestellwert'], ';'); + foreach ($codes as $c) { + fputcsv($out, [ + $c->code, + $c->value, + $c->type === 'percent' ? 'Prozent' : 'Fest', + $c->usage_limit, + $c->expiry ?: '∞', + $c->min_order_value ?: '–' + ], ';'); + } + fclose($out); + exit; + } + } + + // ── Bulk-Generierung speichern ──────────────────────────────────────── + if (isset($_POST['wis_bulk_generate'])) { + check_admin_referer('wis_bulk_generate'); + global $wpdb; + + $count = min(500, max(1, intval($_POST['bulk_count'] ?? 10))); + $mode = $_POST['bulk_mode'] === 'prefix' ? 'prefix' : 'random'; + $prefix = strtoupper(preg_replace('/[^A-Z0-9_-]/i', '', $_POST['bulk_prefix'] ?? '')); + $value = intval($_POST['bulk_value'] ?? 10); + $type = in_array($_POST['bulk_type'], ['fixed', 'percent']) ? $_POST['bulk_type'] : 'fixed'; + $usage_limit = max(1, intval($_POST['bulk_usage_limit'] ?? 1)); + $expiry = !empty($_POST['bulk_expiry']) ? sanitize_text_field($_POST['bulk_expiry']) : null; + $min_order = intval($_POST['bulk_min_order'] ?? 0); + $allowed_cats = !empty($_POST['bulk_allowed_categories']) + ? implode(',', array_map('intval', (array)$_POST['bulk_allowed_categories'])) + : null; + + $bulk_id = strtoupper(bin2hex(random_bytes(4))); // gemeinsame ID für diese Batch + $table = $wpdb->prefix . 'wis_coupons'; + $generated = 0; + $skipped = 0; + $attempts = 0; + + while ($generated < $count && $attempts < $count * 5) { + $attempts++; + if ($mode === 'prefix' && $prefix) { + $rand = strtoupper(bin2hex(random_bytes(4))); + $code = $prefix . '-' . substr($rand, 0, 3) . '-' . substr($rand, 3, 5); + } else { + $r = strtoupper(bin2hex(random_bytes(6))); + $code = substr($r, 0, 4) . '-' . substr($r, 4, 4) . '-' . substr($r, 8, 4); + } + // Duplikat-Check + $exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM $table WHERE code = %s", $code)); + if ($exists) { $skipped++; continue; } + + $wpdb->insert($table, [ + 'code' => $code, + 'value' => $value, + 'type' => $type, + 'usage_limit' => $usage_limit, + 'used_count' => 0, + 'expiry' => $expiry, + 'min_order_value' => $min_order, + 'allowed_categories'=> $allowed_cats, + 'bulk_id' => $bulk_id, + ]); + if ($wpdb->insert_id) $generated++; + } + + $export_url = wp_nonce_url( + admin_url('admin.php?page=wis_coupons&action=export_bulk&bulk_id=' . $bulk_id), + 'wis_coupon_action', '_wpnonce' + ); + echo '

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

'; + } + if (isset($_POST['wis_save_coupon'])) { check_admin_referer('wis_coupon_form'); @@ -2599,7 +3339,9 @@ class WIS_Admin { '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 + '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'])) { @@ -2667,6 +3409,34 @@ class WIS_Admin {

Optional

+ + + + +

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

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

Ohne Auswahl gilt der Gutschein für alle Kategorien.

+ + +

@@ -2679,43 +3449,198 @@ class WIS_Admin { $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; + } + } ?>

-

Gutscheine Neu erstellen

- +

Gutscheine + Neu erstellen + 🎲 Bulk generieren +

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

📦 Generierte Bulk-Gruppen

+ + + + + + + + + + $bcodes): + $sample = $bcodes[0]; + $export_url = wp_nonce_url(admin_url('admin.php?page=wis_coupons&action=export_bulk&bulk_id='.$bid), 'wis_coupon_action', '_wpnonce'); + ?> + + + + + + + + + +
Bulk-IDCodesRabattGültig bisAktionen
value); ?>type==='percent'?'%':' '.esc_html(get_option('wis_currency_name','Coins')); ?>expiry ? esc_html(date('d.m.Y', strtotime($sample->expiry))) : '∞'; ?>📥 CSV Export
+
+ + - - - - - + + + + + + - - - - - - - - - - - - + + id] ?? []; + ?> + + + + + + + + +
CodeRabattGenutztGültig bisAktionenCodeRabattGenutztGültig bisEingelöst vonAktionen
Noch keine Gutscheine vorhanden.
code); ?> - type === 'percent'): ?> - value); ?>% - - value); ?> - - used_count); ?> / usage_limit); ?>expiry ? esc_html(date('d.m.Y', strtotime($coupon->expiry))) : '∞'; ?> - Bearbeiten - Löschen -
Noch keine Gutscheine vorhanden.
+ code); ?> + bulk_id)): ?> + BULK + + + type === 'percent'): ?> + value); ?>% + + value); ?> + + used_count); ?> / usage_limit); ?>expiry ? esc_html(date('d.m.Y', strtotime($coupon->expiry))) : '∞'; ?> + + + +
+ + + player_name); ?> + + +
+ +
+ Bearbeiten + Löschen +
@@ -2723,128 +3648,355 @@ class WIS_Admin { } 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 '

✅ Bestellung gelöscht!

'; } - + if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'complete') { check_admin_referer('wis_order_action', '_wpnonce'); WIS_DB::update_order_status(intval($_GET['id']), 'completed'); echo '

✅ Status geändert!

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

Bestellung nicht gefunden.

'; - return; - } - + if (!$order) { echo '

Bestellung nicht gefunden.

'; return; } $currency = get_option('wis_currency_name', 'Coins'); - $status_colors = [ - 'pending' => '#ffc107', - 'processing' => '#0073aa', - 'completed' => 'green', - 'cancelled' => 'red', - 'failed' => 'red' - ]; - $status_labels = [ - 'pending' => 'Warte auf Ingame', - 'processing' => 'In Bearbeitung', - 'completed' => 'Erledigt', - 'cancelled' => 'Abgebrochen', - 'failed' => 'Fehler' - ]; - + $status_colors = ['pending'=>'#ffc107','processing'=>'#0073aa','completed'=>'green','cancelled'=>'red','failed'=>'red']; + $status_labels = ['pending'=>'Warte auf Ingame','processing'=>'In Bearbeitung','completed'=>'Erledigt','cancelled'=>'Abgebrochen','failed'=>'Fehler']; ?>
-

Bestellung #id; ?> - Details

+

Bestellung #id; ?> – Details

← Zurück - gift_recipient)): ?> - + - - - - - + +
IDid); ?>
Datumcreated_at))); ?>
Käuferplayer_name); ?>
🎁 Geschenk fürgift_recipient); ?>
🎁 Geschenk fürgift_recipient); ?>
Serverserver); ?>
Zusammenfassungitem_title); ?>
Preisprice); ?>
Statusstatus] ?? $order->status; ?>
Details (JSON)response); ?>
Statusstatus] ?? $order->status; ?>
Details (JSON)response); ?>
esc_like($search) . '%'; + $params[] = $like; $params[] = $like; $params[] = $like; + } + if ($f_status) { $where[] = 'status = %s'; $params[] = $f_status; } + if ($f_server) { $where[] = 'server = %s'; $params[] = $f_server; } + + $where_sql = implode(' AND ', $where); + $base_sql = "FROM {$wpdb->prefix}wis_orders WHERE $where_sql ORDER BY created_at DESC"; + + $total = $params + ? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) $base_sql", ...$params)) + : (int)$wpdb->get_var("SELECT COUNT(*) $base_sql"); + + $orders_sql = "SELECT * $base_sql LIMIT %d OFFSET %d"; + $orders = $params + ? $wpdb->get_results($wpdb->prepare($orders_sql, ...[...$params, $per_page, $offset])) + : $wpdb->get_results($wpdb->prepare($orders_sql, $per_page, $offset)); + + $total_pages = max(1, (int)ceil($total / $per_page)); + $currency = get_option('wis_currency_name', 'Coins'); + $servers = WIS_DB::get_servers(); + + $status_map = ['pending'=>'Warte','claimed'=>'Abgeholt','processing'=>'Geben...','completed'=>'Fertig','cancelled'=>'Abgebrochen','failed'=>'Fehler']; + $status_colors = ['pending'=>'#ffc107','claimed'=>'#17a2b8','processing'=>'#0073aa','completed'=>'green','cancelled'=>'red','failed'=>'red']; ?>
-

Bestellungen

- +

Bestellungen ( gesamt)

+ + +
+ + + + + + + Zurücksetzen + +
+ - - - + + + + + - - - + + + - - - - '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' - ]; - ?> - - - - - - - - - - - + + + + + + + + + + + + + +
DatumKäuferEmpfängerIDDatumKäuferEmpfängerServer InhaltPreisStatusAktionenPreisStatusAktionen
Noch keine Bestellungen vorhanden.
created_at))); ?>player_name); ?>gift_recipient)): ?>🎁 gift_recipient); ?>item_title, 0, 50)) . (strlen($order->item_title) > 50 ? '...' : ''); ?>price); ?> status] ?? $order->status; ?> - 👁️ Details - 🗑️ Löschen -
Keine Bestellungen gefunden.
#id; ?>created_at)); ?>player_name); ?>gift_recipient)): ?>🎁 gift_recipient); ?>server); ?>item_title, 0, 55)) . (mb_strlen($order->item_title) > 55 ? '…' : ''); ?>price); ?> status] ?? $order->status; ?> + Details + " class="button button-small" onclick="return confirm('Wirklich löschen?');" style="color:red;">Löschen +
+ + 1): ?> +
+ + + + Seite /  ( Einträge) +
+
prefix . 'wis_sell_log'; + if (!$wpdb->get_var("SHOW TABLES LIKE '$table'")) { + echo '

Ankauf-Log

Tabelle noch nicht vorhanden – Plugin einmal deaktivieren/aktivieren.

'; + return; + } + + $search = sanitize_text_field($_GET['s'] ?? ''); + $f_item = sanitize_text_field($_GET['item'] ?? ''); + $f_server = sanitize_text_field($_GET['server'] ?? ''); + $per_page = 50; + $cur_page = max(1, intval($_GET['paged'] ?? 1)); + $offset = ($cur_page - 1) * $per_page; + + $where = ['1=1']; $params = []; + if ($search) { + $like = '%' . $wpdb->esc_like($search) . '%'; + $where[] = '(player_name LIKE %s OR item_name LIKE %s)'; + $params[] = $like; $params[] = $like; + } + if ($f_item) { $where[] = 'item_id = %s'; $params[] = $f_item; } + if ($f_server) { $where[] = 'server = %s'; $params[] = $f_server; } + + $where_sql = implode(' AND ', $where); + $total = $params + ? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE $where_sql", ...$params)) + : (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql"); + + $rows = $params + ? $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY sold_at DESC LIMIT %d OFFSET %d", ...[...$params, $per_page, $offset])) + : $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY sold_at DESC LIMIT %d OFFSET %d", $per_page, $offset)); + + $total_pages = max(1, (int)ceil($total / $per_page)); + $currency = get_option('wis_currency_name', 'Coins'); + $servers = WIS_DB::get_servers(); + $items_with_sell = $wpdb->get_results("SELECT DISTINCT item_id, item_name FROM $table ORDER BY item_name ASC"); + ?> +
+

📤 Ankauf-Log ( Einträge)

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

Preishistorie

Tabelle noch nicht vorhanden – Plugin einmal deaktivieren/aktivieren.

'; + return; + } + + $search = sanitize_text_field($_GET['s'] ?? ''); + $f_field = sanitize_text_field($_GET['field'] ?? ''); + $per_page = 50; + $cur_page = max(1, intval($_GET['paged'] ?? 1)); + $offset = ($cur_page - 1) * $per_page; + + $where = ['1=1']; $params = []; + if ($search) { + $like = '%' . $wpdb->esc_like($search) . '%'; + $where[] = '(item_name LIKE %s OR item_id LIKE %s OR changed_by LIKE %s)'; + $params[] = $like; $params[] = $like; $params[] = $like; + } + if ($f_field) { $where[] = 'field = %s'; $params[] = $f_field; } + + $where_sql = implode(' AND ', $where); + $total = $params + ? (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table WHERE $where_sql", ...$params)) + : (int)$wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql"); + + $rows = $params + ? $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY changed_at DESC LIMIT %d OFFSET %d", ...[...$params, $per_page, $offset])) + : $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE $where_sql ORDER BY changed_at DESC LIMIT %d OFFSET %d", $per_page, $offset)); + + $total_pages = max(1, (int)ceil($total / $per_page)); + $currency = get_option('wis_currency_name', 'Coins'); + $fields_list = $wpdb->get_col("SELECT DISTINCT field FROM $table ORDER BY field"); + ?> +
+

📈 Preishistorie ( Änderungen)

+
+ + + + + Zurücksetzen +
+ +

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

+ + + + + + + + + + + + + + + new_value - $r->old_value; + $diff_color = $diff > 0 ? 'green' : ($diff < 0 ? 'red' : '#666'); + ?> + + + + + + + + + + + +
DatumItemFeldAltNeuDiffGeändert von
Keine Einträge gefunden.
changed_at)); ?>item_name); ?>item_id); ?>field); ?>old_value); ?> new_value); ?> 0?'+':'') . number_format($diff); ?>changed_by); ?>
+ 1): ?> +
+ + + + Seite / +
+ +
+ [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'], + ]); } @@ -3651,6 +4823,166 @@ class WIS_API { * Body: { "player": "Steve", "server": "survival", "item_id": "minecraft:diamond", "quantity": 5 } * Antwortet mit dem Betrag der gutgeschrieben werden soll. */ + // ========================================================= + // ITEM-ABO – REST-Endpunkte + // ========================================================= + + /** + * GET /wis/v1/item_abo_status?player= + * Gibt alle aktiven Item-Abos eines Spielers zurück. + */ + public static function item_abo_status($request) { + global $wpdb; + $player = sanitize_text_field($request->get_param('player') ?? ''); + if (empty($player)) { + return new WP_REST_Response(['abos' => []], 200); + } + + WIS_Activator::create_item_abo_subs_table(); + $table = $wpdb->prefix . 'wis_item_abo_subs'; + + // id muss mit zurückgegeben werden, damit der Spigot-Client + // beim Kündigen gezielt eine einzelne Abo-Zeile ansprechen kann. + $rows = $wpdb->get_results($wpdb->prepare( + "SELECT id, label, item_id, daily_qty, cancelled, + DATE_FORMAT(expires_at, '%%d.%%m.%%Y') AS expires_at + FROM {$table} + WHERE player_name = %s + AND status = 'active' + AND expires_at > NOW() + ORDER BY created_at ASC", + $player + ), ARRAY_A); + + foreach ($rows as &$row) { + $row['id'] = (int) $row['id']; + $row['daily_qty'] = (int) $row['daily_qty']; + $row['cancelled'] = (bool) $row['cancelled']; + } + unset($row); + + return new WP_REST_Response(['abos' => $rows ?: []], 200); + } + + /** + * POST /wis/v1/item_abo_cancel + * Body: {"player":"", "abo_id":} + * Kündigt genau das Item-Abo mit der angegebenen ID – nur wenn es dem Spieler gehört. + * Gibt 200 bei Erfolg, 403 bei falschem Besitzer, 404 wenn nicht gefunden. + */ + public static function item_abo_cancel($request) { + global $wpdb; + $body = json_decode($request->get_body(), true); + $player = sanitize_text_field($body['player'] ?? ''); + $abo_id = intval($body['abo_id'] ?? 0); + + if (empty($player) || $abo_id <= 0) { + return new WP_REST_Response(['success' => false, 'message' => 'Ungültige Parameter'], 400); + } + + WIS_Activator::create_item_abo_subs_table(); + $table = $wpdb->prefix . 'wis_item_abo_subs'; + + // Sicherheits-Check: Abo muss dem Spieler gehören und aktiv sein + $existing = $wpdb->get_row($wpdb->prepare( + "SELECT id, label FROM {$table} + WHERE id = %d AND player_name = %s AND status = 'active' AND cancelled = 0", + $abo_id, $player + )); + + if (!$existing) { + // Unterscheide: existiert gar nicht vs. gehört anderem Spieler + $any = $wpdb->get_var($wpdb->prepare( + "SELECT id FROM {$table} WHERE id = %d", $abo_id + )); + if ($any) { + return new WP_REST_Response(['success' => false, 'message' => 'Nicht dein Abo'], 403); + } + return new WP_REST_Response(['success' => false, 'message' => 'Abo nicht gefunden oder bereits gekündigt'], 404); + } + + $wpdb->update( + $table, + ['cancelled' => 1, 'cancelled_at' => current_time('mysql')], + ['id' => $abo_id] + ); + + error_log("[WIS] Item-Abo #{$abo_id} ({$existing->label}) gekündigt von: {$player}"); + return new WP_REST_Response([ + 'success' => true, + 'label' => $existing->label, + 'message' => "Item-Abo '{$existing->label}' für {$player} gekündigt", + ], 200); + } + + /** + * POST /wis/v1/trigger_abo_delivery + * Body: {"player":"", "server":""} + * + * Prüft ob der Spieler aktive Item-Abos hat, die heute noch nicht beliefert wurden, + * und legt für jedes fehlende Abo sofort eine pending-Order an. + * Wird beim Join und beim Login aufgerufen – als Fallback falls der WP-Cron nicht + * gelaufen ist (z.B. kein Traffic auf der WordPress-Seite). + * + * Gibt zurück: {"delivered": } – 0 wenn alles bereits geliefert wurde. + */ + public static function trigger_abo_delivery($request) { + global $wpdb; + $body = json_decode($request->get_body(), true); + $player = sanitize_text_field($body['player'] ?? ''); + $server = sanitize_text_field($body['server'] ?? ''); + + if (empty($player)) { + return new WP_REST_Response(['success' => false, 'message' => 'Kein Spielername'], 400); + } + + WIS_Activator::create_item_abo_subs_table(); + $subs_table = $wpdb->prefix . 'wis_item_abo_subs'; + $orders_table = $wpdb->prefix . 'wis_orders'; + $today = date('Y-m-d'); + + // Aktive Abos dieses Spielers die heute noch nicht beliefert wurden + $conditions = "player_name = %s AND status = 'active' AND cancelled = 0" + . " AND expires_at > NOW()" + . " AND (last_delivered IS NULL OR last_delivered < %s)"; + + // Server-Filter nur wenn angegeben + $args = [$player, $today]; + if (!empty($server)) { + $conditions .= " AND server = %s"; + $args[] = $server; + } + + $pending_subs = $wpdb->get_results( + $wpdb->prepare("SELECT * FROM {$subs_table} WHERE {$conditions}", ...$args) + ); + + $delivered = 0; + foreach ($pending_subs as $sub) { + $payload = json_encode([ + 'items' => [['id' => $sub->item_id, 'amount' => intval($sub->daily_qty)]], + 'commands' => [], + 'abo_delivery' => true, + ]); + + $wpdb->insert($orders_table, [ + 'player_name' => $sub->player_name, + 'server' => $sub->server, + 'item_id' => 'item_abo_delivery', + 'item_title' => '📦 Abo-Lieferung: ' . $sub->label . ' ×' . intval($sub->daily_qty), + 'price' => 0, + 'quantity' => intval($sub->daily_qty), + 'status' => 'pending', + 'response' => $payload, + ]); + + $wpdb->update($subs_table, ['last_delivered' => $today], ['id' => $sub->id]); + $delivered++; + } + + return new WP_REST_Response(['success' => true, 'delivered' => $delivered], 200); + } + public static function process_sell($request) { global $wpdb; $player = sanitize_text_field($request->get_param('player') ?? ''); @@ -3689,6 +5021,24 @@ class WIS_API { 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 @@ -3791,6 +5141,7 @@ class WIS_API { '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, ]; } @@ -4120,34 +5471,57 @@ class WIS_API { $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_msg = 'Gutschein abgelaufen'; + $coupon_error = true; + $coupon_msg = 'Dein Gutschein ist abgelaufen.'; } elseif ($coupon->used_count >= $coupon->usage_limit) { - $coupon_msg = 'Gutschein bereits aufgebraucht'; + $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_msg = 'Gutschein gilt nicht für Angebote'; + $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}"; + $coupon_msg = "Gutschein eingelöst: -{$coupon_discount} {$currency}"; } } } else { - $coupon_msg = 'Ungültiger Code'; + $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; @@ -4256,6 +5630,32 @@ class WIS_API { 'label' => $item['title'], ]; $title_parts[] = $item['title']; + } elseif (preg_match('/^item_abo_(.+)_(\d+)_(\d+)$/', $item_id, $ia_m)) { + // Item-Abo: tägliche Lieferung eines Minecraft-Items + $abo_mc_item = $ia_m[1]; + $abo_daily_qty = intval($ia_m[2]); + $abo_dur_days = intval($ia_m[3]); + $commands_payload[] = [ + 'type' => 'item_abo', + 'item_id' => $abo_mc_item, + 'daily_qty' => $abo_daily_qty, + 'duration_days' => $abo_dur_days, + 'label' => $item['title'], + ]; + $title_parts[] = $item['title']; + } elseif (preg_match('/^custom_cmd_(.+)$/', $item_id, $cc_m)) { + // Custom Command Item: Command aus DB holen + $db_item = WIS_DB::get_item_by_item_id($item_id); + $raw_cmd = $db_item ? ($db_item->custom_command ?? '') : ''; + for ($q = 0; $q < intval($item['qty']); $q++) { + $commands_payload[] = [ + 'type' => 'custom_cmd', + 'cmd_id' => $cc_m[1], + 'command' => $raw_cmd, + 'label' => $item['title'], + ]; + } + $title_parts[] = $item['qty'] . 'x ' . $item['title']; } else { // Normales Item $items_payload[] = [ @@ -4370,6 +5770,80 @@ class WIS_API { } } + // 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!'; @@ -4377,11 +5851,44 @@ class WIS_API { return new WP_REST_Response(['success' => true, 'message' => $msg]); } - + /** + * Prueft Mindestbestellwert, erlaubte Kategorien und erlaubte Raenge. + * Gibt null zurueck wenn alles OK, sonst Fehlermeldung als String. + */ + private static function check_coupon_restrictions($coupon, $cart, $subtotal_normal) { + // 1. Mindestbestellwert + $min = intval($coupon->min_order_value ?? 0); + if ($min > 0 && $subtotal_normal < $min) { + $currency = get_option('wis_currency_name', 'Coins'); + return "Mindestbestellwert von {$min} {$currency} nicht erreicht."; + } + + // 2. Erlaubte Kategorien + if (!empty($coupon->allowed_categories)) { + $allowed_cat_ids = array_map('intval', explode(',', $coupon->allowed_categories)); + $has_valid_cat = false; + foreach ($cart as $item_data) { + $item = WIS_DB::get_item(intval($item_data['id'] ?? 0)); + if ($item && in_array(intval($item->category_id), $allowed_cat_ids)) { + $has_valid_cat = true; + break; + } + } + if (!$has_valid_cat) { + return 'Dieser Gutschein gilt nicht fuer die Produkte in deinem Warenkorb.'; + } + } + + + + return null; + } + public static function validate_coupon($request) { - $data = $request->get_json_params(); - $code = sanitize_text_field(strtoupper($data['code'] ?? '')); - $cart = $data['cart'] ?? []; + $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']); @@ -4400,6 +5907,11 @@ class WIS_API { 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'); @@ -4417,6 +5929,19 @@ class WIS_API { 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' @@ -4465,14 +5990,18 @@ class WIS_Shortcode { .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; } - .wis-card-img--custom img { width: 100% !important; height: 100% !important; object-fit: cover !important; image-rendering: auto !important; filter: none !important; border-radius: 0; } + .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; } @@ -4500,12 +6029,12 @@ class WIS_Shortcode { .wis-loading-spinner { display: inline-block; width: 36px; height: 36px; border: 4px solid #ddd; border-top-color: #667eea; border-radius: 50%; animation: wis-spin 0.8s linear infinite; margin-bottom: 12px; } @keyframes wis-spin { to { transform: rotate(360deg); } } .wis-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 2147483647; display: none; align-items: center; justify-content: center; transform: translateZ(0); } - .wis-modal { background: white; width: 90%; max-width: 600px; max-height: 80vh; padding: 30px; border-radius: 15px; box-shadow: 0 15px 30px rgba(0,0,0,0.3); overflow-y: auto; position: relative; z-index: 2147483648; } + .wis-modal { background: white; width: 90%; max-width: 600px; max-height: 80vh; padding: 30px; border-radius: 15px; box-shadow: 0 15px 30px rgba(0,0,0,0.3); overflow-y: auto; position: relative; z-index: 2147483648; color: #222 !important; } .wis-cart-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; border-bottom: 1px solid #eee; } - .wis-cart-item-title { font-weight: bold; color: #333; } + .wis-cart-item-title { font-weight: bold; color: #333 !important; } .wis-cart-item-price { color: #e67e22; font-weight: 600; } .wis-cart-item-remove { background: #dc3545; color: white; border: none; padding: 8px 15px; border-radius: 6px; cursor: pointer; } - .wis-cart-total { border-top: 2px solid #333; padding: 20px 0; margin-top: 15px; font-size: 1.3rem; font-weight: bold; display: flex; justify-content: space-between; } + .wis-cart-total { border-top: 2px solid #333; padding: 20px 0; margin-top: 15px; font-size: 1.3rem; font-weight: bold; display: flex; justify-content: space-between; color: #222 !important; } .wis-modal-input { width: 100%; padding: 15px; margin: 20px 0; border: 2px solid #ddd; border-radius: 8px; font-size: 1.1rem; box-sizing: border-box; } .wis-modal-actions { display: flex; gap: 10px; } .wis-modal-btn { flex: 1; padding: 12px; border: none; border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 1rem; } @@ -4550,10 +6079,24 @@ class WIS_Shortcode { + $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]) + : []; + } + ?>
- + @@ -4598,7 +6141,7 @@ class WIS_Shortcode {