diff --git a/wp-ingame-shop/wp-ingame-shop-pro.php b/wp-ingame-shop/wp-ingame-shop-pro.php index 71c22cd..4484928 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.3 +Version: 2.1.4 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.3'); +define('WIS_VERSION', '2.1.4'); define('WIS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WIS_PLUGIN_URL', plugin_dir_url(__FILE__)); @@ -271,6 +271,23 @@ class WIS_Activator { 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(); @@ -1233,9 +1250,10 @@ class WIS_Admin { add_submenu_page('wis_shop', 'Server', 'Server', 'manage_options', 'wis_servers', [self::class, 'page_servers']); add_submenu_page('wis_shop', 'Kategorien', 'Kategorien', 'manage_options', 'wis_categories', [self::class, 'page_categories']); add_submenu_page('wis_shop', 'Gutscheine', 'Gutscheine', 'manage_options', 'wis_coupons', [self::class, 'page_coupons']); - add_submenu_page('wis_shop', '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']); + 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', '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() { @@ -1249,6 +1267,9 @@ class WIS_Admin { update_option('wis_offline_queue_enabled', isset($_POST['wis_offline_queue_enabled']) ? '1' : '0'); update_option('wis_tax_enabled', isset($_POST['wis_tax_enabled']) ? '1' : '0'); update_option('wis_tax_rate', max(0, min(100, floatval(str_replace(',', '.', $_POST['wis_tax_rate'] ?? '0'))))); + $allowed_pp = ['24', '25', '50', '100', '-1']; + $pp_val = sanitize_text_field($_POST['wis_default_per_page'] ?? '24'); + update_option('wis_default_per_page', in_array($pp_val, $allowed_pp) ? $pp_val : '25'); echo '

✅ Einstellungen gespeichert!

'; } @@ -1394,6 +1415,19 @@ class WIS_Admin {

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

+ + + + +

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

+ +

@@ -3059,11 +3093,421 @@ class WIS_Admin { = DATE_SUB(NOW(), INTERVAL {$range} DAY)"; + $date_where_sell = $range === 'all' ? '' : "AND s.sold_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)"; + $date_where_ord = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)"; + $chart_days = in_array($range, ['7','30','90']) ? intval($range) : ($range === '365' ? 365 : 30); + + $oi_table = $wpdb->prefix . 'wis_order_items'; + $ord_table = $wpdb->prefix . 'wis_orders'; + $sell_table = $wpdb->prefix . 'wis_sell_log'; + $oi_exists = $wpdb->get_var("SHOW TABLES LIKE '$oi_table'") ? true : false; + $sell_exists = $wpdb->get_var("SHOW TABLES LIKE '$sell_table'") ? true : false; + + // ---- TOP-KÄUFE aus wis_order_items (nur status=completed Orders) ---- + $top_buys = []; + $using_legacy = false; + if ($oi_exists) { + $top_buys = $wpdb->get_results(" + SELECT + oi.item_id, + oi.item_name, + oi.item_type, + SUM(oi.quantity) AS qty, + SUM(oi.total) AS revenue, + AVG(oi.price_per_item) AS avg_price, + COUNT(DISTINCT oi.order_id) AS order_count + FROM {$oi_table} oi + INNER JOIN {$ord_table} o ON o.id = oi.order_id AND o.status = 'completed' + WHERE 1=1 {$date_where_oi} + GROUP BY oi.item_id, oi.item_name, oi.item_type + ORDER BY qty DESC + LIMIT 20 + "); + } + + // ---- FALLBACK: wis_order_items leer → response-JSON aus wis_orders parsen ---- + if (empty($top_buys)) { + $using_legacy = true; + $date_cond = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)"; + $old_orders = $wpdb->get_results(" + SELECT o.id, o.price, o.response, o.created_at + FROM {$ord_table} o + WHERE o.status = 'completed' + AND o.item_id = 'cart' + AND o.response IS NOT NULL + {$date_cond} + ORDER BY o.created_at DESC + LIMIT 2000 + "); + + // Parsen und aggregieren + $agg = []; // agg_key => data + $item_cache = []; // item_id => db row (cache um DB-Calls zu sparen) + foreach ($old_orders as $ord) { + $payload = json_decode($ord->response, true); + if (!$payload) continue; + + $items_list = $payload['items'] ?? []; + $cmds = $payload['commands'] ?? []; + $order_price = floatval($ord->price); + + // Gesamtzahl der Positionen für anteilige Preisberechnung + $total_positions = 0; + foreach ($items_list as $pi) { $total_positions += intval($pi['amount'] ?? 1); } + foreach ($cmds as $cmd) { $total_positions += 1; } + if ($total_positions < 1) $total_positions = 1; + + // --- Normale Items --- + foreach ($items_list as $pi) { + $pid = $pi['id'] ?? ''; + if (!$pid) continue; + $pqty = intval($pi['amount'] ?? 1); + + // DB-Lookup mit Cache + if (!array_key_exists($pid, $item_cache)) { + $item_cache[$pid] = $wpdb->get_row($wpdb->prepare( + "SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id = %s LIMIT 1", $pid + )); + } + $db_item = $item_cache[$pid]; + $item_name = $db_item ? $db_item->name : $pid; + // Aktueller Katalogpreis als primäre Quelle + $unit_price = $db_item + ? ($db_item->offer_price > 0 ? floatval($db_item->offer_price) : floatval($db_item->price)) + : round($order_price / $total_positions, 2); // Fallback: anteilig + + if (!isset($agg[$pid])) { + $agg[$pid] = ['item_id'=>$pid,'item_name'=>$item_name,'item_type'=>'item', + 'qty'=>0,'revenue'=>0,'price_sum'=>0,'cnt'=>0]; + } + $agg[$pid]['qty'] += $pqty; + $agg[$pid]['revenue'] += $unit_price * $pqty; + $agg[$pid]['price_sum']+= $unit_price; + $agg[$pid]['cnt']++; + } + + // --- Commands (fly, rank, fly_abo, plot) --- + foreach ($cmds as $cmd) { + $ctype = $cmd['type'] ?? 'item'; + $clabel = $cmd['label'] ?? $ctype; + $ckey = $ctype . '||' . $clabel; + + // Preis aus wis_items anhand des Labels oder type-basierten item_id + $cmd_price = 0; + if ($ctype === 'fly') { + // Fly-Item anhand der Dauer identifizieren + $dur_sec = intval($cmd['duration_sec'] ?? 0); + $fly_map = [300=>'fly_5min',900=>'fly_15min',1800=>'fly_30min',3600=>'fly_1h',7200=>'fly_2h',10800=>'fly_3h']; + $fly_id = $fly_map[$dur_sec] ?? null; + if ($fly_id) { + if (!array_key_exists($fly_id, $item_cache)) { + $item_cache[$fly_id] = $wpdb->get_row($wpdb->prepare( + "SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id = %s LIMIT 1", $fly_id + )); + } + $fi = $item_cache[$fly_id]; + $cmd_price = $fi ? ($fi->offer_price > 0 ? floatval($fi->offer_price) : floatval($fi->price)) : 0; + } + } elseif ($ctype === 'rank') { + // Rank-Item: suche nach rank_{rank_id}* in wis_items + $rid = preg_replace('/[^a-zA-Z0-9_\-]/', '', $cmd['rank_id'] ?? ''); + if ($rid && !array_key_exists('rank_'.$rid, $item_cache)) { + $item_cache['rank_'.$rid] = $wpdb->get_row($wpdb->prepare( + "SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id LIKE %s LIMIT 1", + 'rank_' . $rid . '%' + )); + } + $ri = $rid ? ($item_cache['rank_'.$rid] ?? null) : null; + $cmd_price = $ri ? ($ri->offer_price > 0 ? floatval($ri->offer_price) : floatval($ri->price)) : 0; + } elseif (in_array($ctype, ['fly_abo','plot_abo','plot_slots'])) { + // Abo/Plot: direkt nach type-ähnlicher item_id suchen + if (!array_key_exists($ctype, $item_cache)) { + $item_cache[$ctype] = $wpdb->get_row($wpdb->prepare( + "SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id LIKE %s LIMIT 1", + $ctype . '%' + )); + } + $ai = $item_cache[$ctype] ?? null; + $cmd_price = $ai ? ($ai->offer_price > 0 ? floatval($ai->offer_price) : floatval($ai->price)) : 0; + } + // Letzter Fallback: anteiliger Orderpreis + if ($cmd_price <= 0) { + $cmd_price = round($order_price / $total_positions, 2); + } + + if (!isset($agg[$ckey])) { + $agg[$ckey] = ['item_id'=>$ctype,'item_name'=>$clabel,'item_type'=>$ctype, + 'qty'=>0,'revenue'=>0,'price_sum'=>0,'cnt'=>0]; + } + $agg[$ckey]['qty'] += 1; + $agg[$ckey]['revenue'] += $cmd_price; + $agg[$ckey]['price_sum']+= $cmd_price; + $agg[$ckey]['cnt']++; + } + } + + // Sortieren nach qty DESC, in top_buys-kompatibles Format bringen + usort($agg, fn($a,$b) => $b['qty'] - $a['qty']); + $agg = array_slice($agg, 0, 20); + foreach ($agg as $a) { + $obj = new stdClass(); + $obj->item_id = $a['item_id']; + $obj->item_name = $a['item_name']; + $obj->item_type = $a['item_type']; + $obj->qty = $a['qty']; + $obj->revenue = $a['revenue']; + $obj->avg_price = $a['cnt'] > 0 ? round($a['price_sum'] / $a['cnt'], 2) : 0; + $obj->order_count = $a['cnt']; + $top_buys[] = $obj; + } + } + + // ---- TOP-VERKÄUFE / ANKÄUFE ---- + $top_sells = []; + if ($sell_exists) { + $top_sells = $wpdb->get_results(" + SELECT + s.item_name, + s.item_id, + SUM(s.quantity) AS qty, + SUM(s.total_paid) AS total_paid, + AVG(s.price_per_item) AS avg_price + FROM {$sell_table} s + WHERE 1=1 {$date_where_sell} + GROUP BY s.item_id, s.item_name + ORDER BY qty DESC + LIMIT 20 + "); + } + + // ---- UMSATZ PRO TAG (nach gewähltem Zeitraum) ---- + $chart_interval = $range === 'all' ? 365 : intval($range); + $daily_revenue = $wpdb->get_results($wpdb->prepare(" + SELECT DATE(o.created_at) AS day, SUM(o.price) AS revenue, COUNT(*) AS orders + FROM {$ord_table} o + WHERE o.status = 'completed' + AND o.created_at >= DATE_SUB(NOW(), INTERVAL %d DAY) + GROUP BY DATE(o.created_at) + ORDER BY day ASC + ", $chart_interval)); + + // ---- KPI-Gesamtzahlen ---- + $buy_qty = $oi_exists ? ($wpdb->get_var("SELECT SUM(oi.quantity) FROM {$oi_table} oi INNER JOIN {$ord_table} o ON o.id=oi.order_id AND o.status='completed' WHERE 1=1 {$date_where_oi}") ?: 0) : 0; + $buy_revenue = $wpdb->get_var("SELECT SUM(o.price) FROM {$ord_table} o WHERE o.status='completed' {$date_where_ord}") ?: 0; + $sell_qty = $sell_exists ? ($wpdb->get_var("SELECT SUM(s.quantity) FROM {$sell_table} s WHERE 1=1 {$date_where_sell}") ?: 0) : 0; + $sell_paid = $sell_exists ? ($wpdb->get_var("SELECT SUM(s.total_paid) FROM {$sell_table} s WHERE 1=1 {$date_where_sell}") ?: 0) : 0; + + $chart_labels = []; + $chart_revenue = []; + foreach ($daily_revenue as $dr) { + $chart_labels[] = $dr->day; + $chart_revenue[] = floatval($dr->revenue); + } + + ?> +

+

📊 Analyse – Kauf & Verkauf

+ + + + +
+ '7 Tage','30'=>'30 Tage','90'=>'90 Tage','365'=>'1 Jahr','all'=>'Gesamt'] as $v=>$l): ?> + + +
+ + +
+ ⚠️ Einzelitem-Tracking noch nicht aktiv + Die Tabelle wis_order_items fehlt noch. Bitte das Plugin einmal deaktivieren und wieder aktivieren. Ab dann werden alle neuen Käufe item-genau erfasst. Bis dahin wird die Auswertung aus den gespeicherten Bestell-JSONs rekonstruiert (Preise sind Näherungswerte aus dem aktuellen Katalog). +
+ + + +
+
+
+
🛒 Items gekauft(n/v)' : ''; ?>
+
+
+
+
💰 Einnahmen ()
+
+
+
+
📤 Ankäufe (Menge)
+
+
+
+
📉 Ausgezahlt ()
+
+
+ + +
+

📈 Tagesumsatz

+ +

Keine abgeschlossenen Bestellungen in den letzten 30 Tagen.

+ + + +
+ +
+ + +
+

Top 20 – meistgekaufte Items (einzeln)

+ +

Keine Daten gefunden.

+ +

Keine abgeschlossenen Bestellungen für diesen Zeitraum.

+ + + + + + + + + + + + + + + + + + + + + + +
#ItemØ PreisMengeUmsatzTrend
+ item_name); ?> + item_id); ?> + avg_price, 0); ?> qty); ?> + / order_count); ?> Käufe + revenue, 0); ?>
+

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

+ +
+ + +
+

Top 20 – Ankauf durch Spieler (einzeln)

+ +

Ankauf-Tabelle nicht vorhanden. Ankauf-Feature aktivieren.

+ +

Keine Ankaufdaten für diesen Zeitraum.

+ + + + + + + + + + + + + + + + + + + + + + +
#ItemØ AnkaufspreisMengeAusgezahltTrend
+ item_name); ?> + item_id); ?> + avg_price, 2); ?> qty); ?>total_paid, 0); ?>
+

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

+ +
+
+ +
+ + + + get_param('page') ?? 1)); - $per_page = 24; + $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') ?? ''); @@ -3307,11 +3760,18 @@ class WIS_API { $where_sql = implode(" AND ", $where_parts); $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql"); - $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 - )); + 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'); @@ -3348,7 +3808,7 @@ class WIS_API { 'total' => $total, 'page' => $page, 'per_page' => $per_page, - 'total_pages' => max(1, (int) ceil($total / $per_page)), + 'total_pages' => $per_page === -1 ? 1 : max(1, (int) ceil($total / $per_page)), 'currency' => $currency, ]); } @@ -3827,6 +4287,43 @@ class WIS_API { '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; @@ -3993,6 +4490,7 @@ class WIS_Shortcode { .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; } @@ -4072,6 +4570,14 @@ class WIS_Shortcode {
+
+ +
@@ -4155,6 +4661,7 @@ class WIS_Shortcode { let searchTimer = null; let allItems = []; let itemMap = {}; // id (int) -> item object + let currentPerPage = ; // ------------------------------------------------------- // INIT @@ -4242,6 +4749,17 @@ class WIS_Shortcode { }); } + // ---- 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); @@ -4297,6 +4815,7 @@ class WIS_Shortcode { 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(); })