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!
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); + } + + ?> +
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).
+ Keine abgeschlossenen Bestellungen in den letzten 30 Tagen.
+ + + +Keine Daten gefunden.
+ +Keine abgeschlossenen Bestellungen für diesen Zeitraum.
+ +| # | +Item | +Ø Preis | +Menge | +Umsatz | +Trend | +
|---|---|---|---|---|---|
| + | + item_name); ?> + item_id); ?> + | +avg_price, 0); ?> | +qty); ?> + / order_count); ?> Käufe + | +revenue, 0); ?> | ++ |
💡 Viel gekauft + hoher Umsatz → Preis erhöhen. Viel gekauft + niedriger Ø-Preis → evtl. zu günstig.
+ +Ankauf-Tabelle nicht vorhanden. Ankauf-Feature aktivieren.
+ +Keine Ankaufdaten für diesen Zeitraum.
+ +| # | +Item | +Ø Ankaufspreis | +Menge | +Ausgezahlt | +Trend | +
|---|---|---|---|---|---|
| + | + item_name); ?> + item_id); ?> + | +avg_price, 2); ?> | +qty); ?> | +total_paid, 0); ?> | ++ |
💡 Viel verkauft = Spieler farmen dieses Item massenhaft → Ankaufspreis senken oder Tageslimit einführen.
+ +