wp-ingame-shop/wp-ingame-shop-pro.php aktualisiert
This commit is contained in:
@@ -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 '<div class="updated"><p>✅ Einstellungen gespeichert!</p></div>';
|
||||
}
|
||||
|
||||
@@ -1394,6 +1415,19 @@ class WIS_Admin {
|
||||
<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">
|
||||
@@ -3059,11 +3093,421 @@ class WIS_Admin {
|
||||
</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
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// REST API
|
||||
// ===========================================================
|
||||
|
||||
class WIS_API {
|
||||
public static function register_routes() {
|
||||
register_rest_route('wis/v1', '/import_json', [
|
||||
@@ -3287,8 +3731,17 @@ class WIS_API {
|
||||
}
|
||||
|
||||
public static function get_shop_items($request) {
|
||||
$page = max(1, intval($request->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 {
|
||||
</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 Modal -->
|
||||
@@ -4155,6 +4661,7 @@ class WIS_Shortcode {
|
||||
let searchTimer = null;
|
||||
let allItems = [];
|
||||
let itemMap = {}; // id (int) -> item object
|
||||
let currentPerPage = <?php echo intval(get_option('wis_default_per_page', 25)); ?>;
|
||||
|
||||
// -------------------------------------------------------
|
||||
// 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(); })
|
||||
|
||||
Reference in New Issue
Block a user