1208 lines
34 KiB
PHP
1208 lines
34 KiB
PHP
<?php
|
|
/**
|
|
* Analytics API Controller
|
|
*
|
|
* Handles all analytics endpoints for the dashboard
|
|
*
|
|
* @package WooNooW
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
namespace WooNooW\Api;
|
|
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WP_Error;
|
|
|
|
class AnalyticsController {
|
|
|
|
/**
|
|
* Register REST API routes
|
|
*/
|
|
public static function register_routes() {
|
|
// Overview/Dashboard analytics
|
|
register_rest_route('woonoow/v1', '/analytics/overview', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_overview'],
|
|
'permission_callback' => [Permissions::class, 'check_admin_permission'],
|
|
]);
|
|
|
|
// Revenue analytics
|
|
register_rest_route('woonoow/v1', '/analytics/revenue', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_revenue'],
|
|
'permission_callback' => [Permissions::class, 'check_admin_permission'],
|
|
'args' => [
|
|
'granularity' => [
|
|
'required' => false,
|
|
'default' => 'day',
|
|
'validate_callback' => function($param) {
|
|
return in_array($param, ['day', 'week', 'month']);
|
|
},
|
|
],
|
|
],
|
|
]);
|
|
|
|
// Orders analytics
|
|
register_rest_route('woonoow/v1', '/analytics/orders', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_orders'],
|
|
'permission_callback' => [Permissions::class, 'check_admin_permission'],
|
|
]);
|
|
|
|
// Products analytics
|
|
register_rest_route('woonoow/v1', '/analytics/products', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_products'],
|
|
'permission_callback' => [Permissions::class, 'check_admin_permission'],
|
|
]);
|
|
|
|
// Customers analytics
|
|
register_rest_route('woonoow/v1', '/analytics/customers', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_customers'],
|
|
'permission_callback' => [Permissions::class, 'check_admin_permission'],
|
|
]);
|
|
|
|
// Coupons analytics
|
|
register_rest_route('woonoow/v1', '/analytics/coupons', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_coupons'],
|
|
'permission_callback' => [Permissions::class, 'check_admin_permission'],
|
|
]);
|
|
|
|
// Taxes analytics
|
|
register_rest_route('woonoow/v1', '/analytics/taxes', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_taxes'],
|
|
'permission_callback' => [Permissions::class, 'check_admin_permission'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get overview/dashboard analytics
|
|
*
|
|
* @param WP_REST_Request $request
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public static function get_overview(WP_REST_Request $request) {
|
|
try {
|
|
$period = $request->get_param('period') ?: '30';
|
|
|
|
$cache_key = 'woonoow_overview_' . md5($period);
|
|
$cached_data = get_transient($cache_key);
|
|
|
|
if (false !== $cached_data) {
|
|
return new WP_REST_Response($cached_data, 200);
|
|
}
|
|
|
|
$data = self::calculate_overview_metrics($period);
|
|
set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
} catch (\Exception $e) {
|
|
return new WP_Error(
|
|
'analytics_error',
|
|
$e->getMessage(),
|
|
['status' => 500]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get revenue analytics
|
|
*
|
|
* @param WP_REST_Request $request
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public static function get_revenue(WP_REST_Request $request) {
|
|
try {
|
|
$granularity = $request->get_param('granularity') ?: 'day';
|
|
$period = $request->get_param('period') ?: '30';
|
|
|
|
// Cache key based on parameters
|
|
$cache_key = 'woonoow_revenue_' . md5($granularity . '_' . $period);
|
|
$cached_data = get_transient($cache_key);
|
|
|
|
if (false !== $cached_data) {
|
|
return new WP_REST_Response($cached_data, 200);
|
|
}
|
|
|
|
$data = self::calculate_revenue_metrics($granularity, $period);
|
|
|
|
// Cache for 5 minutes
|
|
set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
} catch (\Exception $e) {
|
|
return new WP_Error(
|
|
'analytics_error',
|
|
$e->getMessage(),
|
|
['status' => 500]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get orders analytics
|
|
*
|
|
* @param WP_REST_Request $request
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public static function get_orders(WP_REST_Request $request) {
|
|
try {
|
|
$period = $request->get_param('period') ?: '30';
|
|
|
|
// Cache key
|
|
$cache_key = 'woonoow_orders_' . md5($period);
|
|
$cached_data = get_transient($cache_key);
|
|
|
|
if (false !== $cached_data) {
|
|
return new WP_REST_Response($cached_data, 200);
|
|
}
|
|
|
|
$data = self::calculate_orders_metrics($period);
|
|
|
|
// Cache for 5 minutes
|
|
set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
} catch (\Exception $e) {
|
|
return new WP_Error(
|
|
'analytics_error',
|
|
$e->getMessage(),
|
|
['status' => 500]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get products analytics
|
|
*
|
|
* @param WP_REST_Request $request
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public static function get_products(WP_REST_Request $request) {
|
|
try {
|
|
$period = $request->get_param('period') ?: '30';
|
|
|
|
$cache_key = 'woonoow_products_' . md5($period);
|
|
$cached_data = get_transient($cache_key);
|
|
|
|
if (false !== $cached_data) {
|
|
return new WP_REST_Response($cached_data, 200);
|
|
}
|
|
|
|
$data = self::calculate_products_metrics($period);
|
|
set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
} catch (\Exception $e) {
|
|
return new WP_Error(
|
|
'analytics_error',
|
|
$e->getMessage(),
|
|
['status' => 500]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get customers analytics
|
|
*
|
|
* @param WP_REST_Request $request
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public static function get_customers(WP_REST_Request $request) {
|
|
try {
|
|
$period = $request->get_param('period') ?: '30';
|
|
|
|
$cache_key = 'woonoow_customers_' . md5($period);
|
|
$cached_data = get_transient($cache_key);
|
|
|
|
if (false !== $cached_data) {
|
|
return new WP_REST_Response($cached_data, 200);
|
|
}
|
|
|
|
$data = self::calculate_customers_metrics($period);
|
|
set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
} catch (\Exception $e) {
|
|
return new WP_Error(
|
|
'analytics_error',
|
|
$e->getMessage(),
|
|
['status' => 500]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get coupons analytics
|
|
*
|
|
* @param WP_REST_Request $request
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public static function get_coupons(WP_REST_Request $request) {
|
|
try {
|
|
$period = $request->get_param('period') ?: '30';
|
|
|
|
$cache_key = 'woonoow_coupons_' . md5($period);
|
|
$cached_data = get_transient($cache_key);
|
|
|
|
if (false !== $cached_data) {
|
|
return new WP_REST_Response($cached_data, 200);
|
|
}
|
|
|
|
$data = self::calculate_coupons_metrics($period);
|
|
set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
} catch (\Exception $e) {
|
|
return new WP_Error(
|
|
'analytics_error',
|
|
$e->getMessage(),
|
|
['status' => 500]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get taxes analytics
|
|
*
|
|
* @param WP_REST_Request $request
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public static function get_taxes(WP_REST_Request $request) {
|
|
try {
|
|
$period = $request->get_param('period') ?: '30';
|
|
|
|
$cache_key = 'woonoow_taxes_' . md5($period);
|
|
$cached_data = get_transient($cache_key);
|
|
|
|
if (false !== $cached_data) {
|
|
return new WP_REST_Response($cached_data, 200);
|
|
}
|
|
|
|
$data = self::calculate_taxes_metrics($period);
|
|
set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
|
|
|
|
return new WP_REST_Response($data, 200);
|
|
} catch (\Exception $e) {
|
|
return new WP_Error(
|
|
'analytics_error',
|
|
$e->getMessage(),
|
|
['status' => 500]
|
|
);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// PRIVATE HELPER METHODS (Future Implementation)
|
|
// ========================================
|
|
|
|
/**
|
|
* Calculate overview metrics
|
|
*/
|
|
private static function calculate_overview_metrics($period = '30') {
|
|
global $wpdb;
|
|
$orders_table = $wpdb->prefix . 'wc_orders';
|
|
$days = $period === 'all' ? 365 : intval($period);
|
|
|
|
// Get sales chart data
|
|
$salesChart = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
DATE(date_created_gmt) as date,
|
|
SUM(total_amount) as revenue,
|
|
COUNT(*) as orders
|
|
FROM {$orders_table}
|
|
WHERE status IN ('wc-completed', 'wc-processing')
|
|
AND date_created_gmt >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
GROUP BY DATE(date_created_gmt)
|
|
ORDER BY date ASC
|
|
", $days));
|
|
|
|
// Create a map of existing data
|
|
$data_map = [];
|
|
foreach ($salesChart as $row) {
|
|
$data_map[$row->date] = [
|
|
'revenue' => round(floatval($row->revenue), 2),
|
|
'orders' => intval($row->orders),
|
|
];
|
|
}
|
|
|
|
// Fill in ALL dates in the range (including dates with no data)
|
|
$formatted_sales = [];
|
|
$total_revenue = 0;
|
|
$total_orders = 0;
|
|
|
|
for ($i = $days - 1; $i >= 0; $i--) {
|
|
$date = date('Y-m-d', strtotime("-{$i} days"));
|
|
|
|
if (isset($data_map[$date])) {
|
|
$revenue = $data_map[$date]['revenue'];
|
|
$orders = $data_map[$date]['orders'];
|
|
} else {
|
|
// No data for this date, fill with zeros
|
|
$revenue = 0.00;
|
|
$orders = 0;
|
|
}
|
|
|
|
$total_revenue += $revenue;
|
|
$total_orders += $orders;
|
|
|
|
$formatted_sales[] = [
|
|
'date' => $date,
|
|
'revenue' => $revenue,
|
|
'orders' => $orders,
|
|
];
|
|
}
|
|
|
|
// Get order status distribution
|
|
$orderStatusDistribution = $wpdb->get_results("
|
|
SELECT
|
|
status,
|
|
COUNT(*) as count
|
|
FROM {$orders_table}
|
|
WHERE date_created_gmt >= DATE_SUB(NOW(), INTERVAL {$days} DAY)
|
|
GROUP BY status
|
|
ORDER BY count DESC
|
|
");
|
|
|
|
// Format status distribution
|
|
$formatted_status = [];
|
|
$status_colors = [
|
|
'wc-completed' => '#10b981',
|
|
'wc-processing' => '#3b82f6',
|
|
'wc-pending' => '#f59e0b',
|
|
'wc-on-hold' => '#6b7280',
|
|
'wc-cancelled' => '#ef4444',
|
|
'wc-refunded' => '#8b5cf6',
|
|
'wc-failed' => '#dc2626',
|
|
];
|
|
|
|
foreach ($orderStatusDistribution as $status) {
|
|
$status_name = str_replace('wc-', '', $status->status);
|
|
$formatted_status[] = [
|
|
'status' => ucfirst($status_name),
|
|
'count' => intval($status->count),
|
|
'color' => $status_colors[$status->status] ?? '#6b7280',
|
|
];
|
|
}
|
|
|
|
// Calculate metrics
|
|
$avg_order_value = $total_orders > 0 ? round($total_revenue / $total_orders, 2) : 0;
|
|
|
|
// Get top products
|
|
$products_table = $wpdb->prefix . 'wc_order_product_lookup';
|
|
$top_products = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
product_id,
|
|
SUM(product_qty) as items_sold,
|
|
SUM(product_gross_revenue) as revenue
|
|
FROM {$products_table}
|
|
WHERE date_created >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
GROUP BY product_id
|
|
ORDER BY revenue DESC
|
|
LIMIT 5
|
|
", $days));
|
|
|
|
$formatted_products = [];
|
|
foreach ($top_products as $product) {
|
|
$wc_product = wc_get_product($product->product_id);
|
|
if ($wc_product) {
|
|
$formatted_products[] = [
|
|
'product_id' => intval($product->product_id),
|
|
'name' => $wc_product->get_name(),
|
|
'items_sold' => intval($product->items_sold),
|
|
'revenue' => round(floatval($product->revenue), 2),
|
|
];
|
|
}
|
|
}
|
|
|
|
// Get top customers
|
|
$top_customers = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
customer_id,
|
|
billing_email,
|
|
COUNT(*) as order_count,
|
|
SUM(total_amount) as total_spent
|
|
FROM {$orders_table}
|
|
WHERE status IN ('wc-completed', 'wc-processing')
|
|
AND date_created_gmt >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
AND customer_id > 0
|
|
GROUP BY customer_id
|
|
ORDER BY total_spent DESC
|
|
LIMIT 5
|
|
", $days));
|
|
|
|
$formatted_customers = [];
|
|
foreach ($top_customers as $customer) {
|
|
$user = get_user_by('id', $customer->customer_id);
|
|
$formatted_customers[] = [
|
|
'customer_id' => intval($customer->customer_id),
|
|
'name' => $user ? $user->display_name : 'Guest',
|
|
'email' => $customer->billing_email,
|
|
'order_count' => intval($customer->order_count),
|
|
'total_spent' => round(floatval($customer->total_spent), 2),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'metrics' => [
|
|
'revenue' => [
|
|
'today' => $total_revenue,
|
|
'yesterday' => 0, // TODO: Calculate
|
|
'change' => 0,
|
|
],
|
|
'orders' => [
|
|
'today' => $total_orders,
|
|
'yesterday' => 0, // TODO: Calculate
|
|
'change' => 0,
|
|
],
|
|
'avgOrderValue' => [
|
|
'today' => $avg_order_value,
|
|
'yesterday' => 0, // TODO: Calculate
|
|
'change' => 0,
|
|
],
|
|
'conversionRate' => [
|
|
'today' => 0, // TODO: Calculate
|
|
'yesterday' => 0,
|
|
'change' => 0,
|
|
],
|
|
],
|
|
'salesChart' => $formatted_sales,
|
|
'orderStatus' => $formatted_status,
|
|
'orderStatusDistribution' => $formatted_status, // Keep for backward compatibility
|
|
'topProducts' => $formatted_products,
|
|
'topCustomers' => $formatted_customers,
|
|
'lowStock' => [], // TODO: Implement
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate revenue metrics
|
|
*/
|
|
private static function calculate_revenue_metrics($granularity = 'day', $period = '30') {
|
|
global $wpdb;
|
|
|
|
// Determine date range
|
|
if ($period === 'all') {
|
|
$date_query = "1=1"; // All time
|
|
$days = 365; // Limit to 1 year for performance
|
|
} else {
|
|
$days = intval($period);
|
|
$date_query = "date_created_gmt >= DATE_SUB(NOW(), INTERVAL {$days} DAY)";
|
|
}
|
|
|
|
// Query HPOS orders table
|
|
$orders_table = $wpdb->prefix . 'wc_orders';
|
|
|
|
// Get chart data grouped by date
|
|
$chart_data_raw = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
DATE(date_created_gmt) as date,
|
|
SUM(total_amount) as gross,
|
|
SUM(total_amount - tax_amount) as net,
|
|
SUM(tax_amount) as tax,
|
|
0 as refunds,
|
|
0 as shipping
|
|
FROM {$orders_table}
|
|
WHERE status IN ('wc-completed', 'wc-processing')
|
|
AND {$date_query}
|
|
GROUP BY DATE(date_created_gmt)
|
|
ORDER BY date ASC
|
|
LIMIT %d
|
|
", $days));
|
|
|
|
// Calculate overview metrics and format chart data
|
|
$total_gross = 0;
|
|
$total_net = 0;
|
|
$total_tax = 0;
|
|
$total_refunds = 0;
|
|
$order_count = 0;
|
|
$chart_data = [];
|
|
|
|
foreach ($chart_data_raw as $row) {
|
|
$gross = round(floatval($row->gross), 2);
|
|
$net = round(floatval($row->net), 2);
|
|
$tax = round(floatval($row->tax), 2);
|
|
|
|
$total_gross += $gross;
|
|
$total_net += $net;
|
|
$total_tax += $tax;
|
|
|
|
// Format for frontend
|
|
$chart_data[] = [
|
|
'date' => $row->date,
|
|
'gross' => $gross,
|
|
'net' => $net,
|
|
'tax' => $tax,
|
|
'refunds' => 0,
|
|
'shipping' => 0,
|
|
];
|
|
}
|
|
|
|
// Get order count
|
|
$order_count = $wpdb->get_var($wpdb->prepare("
|
|
SELECT COUNT(*)
|
|
FROM {$orders_table}
|
|
WHERE status IN ('wc-completed', 'wc-processing')
|
|
AND {$date_query}
|
|
"));
|
|
|
|
$avg_order_value = $order_count > 0 ? round($total_gross / $order_count, 2) : 0;
|
|
|
|
// Get top products by revenue
|
|
$products_table = $wpdb->prefix . 'wc_order_product_lookup';
|
|
$by_product = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
product_id,
|
|
SUM(product_gross_revenue) as revenue,
|
|
SUM(product_qty) as quantity
|
|
FROM {$products_table}
|
|
WHERE date_created >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
GROUP BY product_id
|
|
ORDER BY revenue DESC
|
|
LIMIT 10
|
|
", $days));
|
|
|
|
// Format product data
|
|
$formatted_products = [];
|
|
foreach ($by_product as $product) {
|
|
$wc_product = wc_get_product($product->product_id);
|
|
if ($wc_product) {
|
|
$formatted_products[] = [
|
|
'product_id' => intval($product->product_id),
|
|
'name' => $wc_product->get_name(),
|
|
'revenue' => round(floatval($product->revenue), 2),
|
|
'quantity' => intval($product->quantity),
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'overview' => [
|
|
'gross_revenue' => round($total_gross, 2),
|
|
'net_revenue' => round($total_net, 2),
|
|
'tax' => round($total_tax, 2),
|
|
'shipping' => 0, // TODO: Calculate
|
|
'refunds' => round($total_refunds, 2),
|
|
'change_percent' => 0, // TODO: Calculate
|
|
'previous_gross_revenue' => 0, // TODO: Calculate
|
|
'previous_net_revenue' => 0, // TODO: Calculate
|
|
],
|
|
'chart_data' => $chart_data,
|
|
'by_product' => $formatted_products,
|
|
'by_category' => [], // TODO: Implement
|
|
'by_payment_method' => [], // TODO: Implement
|
|
'by_shipping_method' => [], // TODO: Implement
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate products metrics
|
|
*/
|
|
private static function calculate_products_metrics($period = '30') {
|
|
global $wpdb;
|
|
$products_table = $wpdb->prefix . 'wc_order_product_lookup';
|
|
|
|
$days = $period === 'all' ? 365 : intval($period);
|
|
|
|
// Get top products
|
|
$top_products = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
product_id,
|
|
SUM(product_qty) as items_sold,
|
|
SUM(product_gross_revenue) as revenue,
|
|
SUM(product_net_revenue) as net_revenue
|
|
FROM {$products_table}
|
|
WHERE date_created >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
GROUP BY product_id
|
|
ORDER BY revenue DESC
|
|
LIMIT 20
|
|
", $days));
|
|
|
|
// Format products
|
|
$formatted_products = [];
|
|
$total_revenue = 0;
|
|
$total_items = 0;
|
|
|
|
foreach ($top_products as $product) {
|
|
$wc_product = wc_get_product($product->product_id);
|
|
if ($wc_product) {
|
|
$revenue = round(floatval($product->revenue), 2);
|
|
$items_sold = intval($product->items_sold);
|
|
|
|
$total_revenue += $revenue;
|
|
$total_items += $items_sold;
|
|
|
|
$formatted_products[] = [
|
|
'id' => intval($product->product_id),
|
|
'product_id' => intval($product->product_id),
|
|
'name' => $wc_product->get_name(),
|
|
'image' => '📦',
|
|
'sku' => $wc_product->get_sku() ?: 'N/A',
|
|
'items_sold' => $items_sold,
|
|
'revenue' => $revenue,
|
|
'net_revenue' => round(floatval($product->net_revenue), 2),
|
|
'stock' => $wc_product->get_stock_quantity() ?: 0,
|
|
'stock_status' => $wc_product->get_stock_status(),
|
|
'stock_quantity' => $wc_product->get_stock_quantity() ?: 0,
|
|
'views' => 0,
|
|
'conversion_rate' => 0.0,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Calculate average price
|
|
$avg_price = $total_items > 0 ? round($total_revenue / $total_items, 2) : 0;
|
|
|
|
// Get low stock and out of stock counts
|
|
$low_stock_count = $wpdb->get_var("
|
|
SELECT COUNT(*)
|
|
FROM {$wpdb->prefix}posts p
|
|
INNER JOIN {$wpdb->prefix}postmeta pm ON p.ID = pm.post_id
|
|
WHERE p.post_type = 'product'
|
|
AND p.post_status = 'publish'
|
|
AND pm.meta_key = '_stock_status'
|
|
AND pm.meta_value = 'onbackorder'
|
|
");
|
|
|
|
$out_of_stock_count = $wpdb->get_var("
|
|
SELECT COUNT(*)
|
|
FROM {$wpdb->prefix}posts p
|
|
INNER JOIN {$wpdb->prefix}postmeta pm ON p.ID = pm.post_id
|
|
WHERE p.post_type = 'product'
|
|
AND p.post_status = 'publish'
|
|
AND pm.meta_key = '_stock_status'
|
|
AND pm.meta_value = 'outofstock'
|
|
");
|
|
|
|
return [
|
|
'overview' => [
|
|
'items_sold' => $total_items,
|
|
'revenue' => $total_revenue,
|
|
'avg_price' => $avg_price,
|
|
'low_stock_count' => intval($low_stock_count),
|
|
'out_of_stock_count' => intval($out_of_stock_count),
|
|
'change_percent' => 0, // TODO: Calculate
|
|
],
|
|
'top_products' => $formatted_products,
|
|
'by_category' => [], // TODO: Implement
|
|
'stock_analysis' => [
|
|
'low_stock' => [],
|
|
'out_of_stock' => [],
|
|
'slow_movers' => [],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate taxes metrics
|
|
*/
|
|
private static function calculate_taxes_metrics($period = '30') {
|
|
global $wpdb;
|
|
$orders_table = $wpdb->prefix . 'wc_orders';
|
|
$days = $period === 'all' ? 365 : intval($period);
|
|
|
|
// Get tax data over time - ONLY orders with tax > 0
|
|
$chart_data = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
DATE(date_created_gmt) as date,
|
|
SUM(tax_amount) as tax,
|
|
COUNT(*) as orders
|
|
FROM {$orders_table}
|
|
WHERE status IN ('wc-completed', 'wc-processing')
|
|
AND date_created_gmt >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
AND tax_amount > 0
|
|
GROUP BY DATE(date_created_gmt)
|
|
ORDER BY date ASC
|
|
", $days));
|
|
|
|
// Format chart data
|
|
$formatted_chart = [];
|
|
$total_tax = 0;
|
|
$total_orders_with_tax = 0;
|
|
|
|
foreach ($chart_data as $row) {
|
|
$tax = round(floatval($row->tax), 2);
|
|
$orders = intval($row->orders);
|
|
|
|
$total_tax += $tax;
|
|
$total_orders_with_tax += $orders;
|
|
|
|
$formatted_chart[] = [
|
|
'date' => $row->date,
|
|
'tax' => $tax,
|
|
'orders' => $orders,
|
|
];
|
|
}
|
|
|
|
// Calculate average tax per order (only for orders with tax)
|
|
$avg_tax_per_order = $total_orders_with_tax > 0 ? round($total_tax / $total_orders_with_tax, 2) : 0;
|
|
|
|
return [
|
|
'overview' => [
|
|
'total_tax' => $total_tax,
|
|
'avg_tax_per_order' => $avg_tax_per_order,
|
|
'orders_with_tax' => $total_orders_with_tax,
|
|
],
|
|
'chart_data' => $formatted_chart,
|
|
'by_rate' => [], // TODO: Implement if needed
|
|
'by_location' => [], // TODO: Implement if needed
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate customers metrics
|
|
*/
|
|
private static function calculate_customers_metrics($period = '30') {
|
|
global $wpdb;
|
|
$orders_table = $wpdb->prefix . 'wc_orders';
|
|
$days = $period === 'all' ? 365 : intval($period);
|
|
|
|
// Get top customers by revenue
|
|
$top_customers = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
customer_id,
|
|
billing_email,
|
|
COUNT(*) as order_count,
|
|
SUM(total_amount) as total_spent
|
|
FROM {$orders_table}
|
|
WHERE status IN ('wc-completed', 'wc-processing')
|
|
AND date_created_gmt >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
AND customer_id > 0
|
|
GROUP BY customer_id
|
|
ORDER BY total_spent DESC
|
|
LIMIT 20
|
|
", $days));
|
|
|
|
// Format customers
|
|
$formatted_customers = [];
|
|
$total_customers = 0;
|
|
$total_revenue = 0;
|
|
$formatted_customers = [];
|
|
foreach ($top_customers as $customer) {
|
|
$user = get_user_by('id', $customer->customer_id);
|
|
$total_spent = round(floatval($customer->total_spent), 2);
|
|
$order_count = intval($customer->order_count);
|
|
|
|
$total_customers++;
|
|
$total_revenue += $total_spent;
|
|
|
|
$formatted_customers[] = [
|
|
'id' => intval($customer->customer_id),
|
|
'customer_id' => intval($customer->customer_id),
|
|
'name' => $user ? $user->display_name : 'Guest',
|
|
'email' => $customer->billing_email,
|
|
'orders' => $order_count,
|
|
'order_count' => $order_count,
|
|
'total_spent' => $total_spent,
|
|
'avg_order_value' => round($total_spent / $order_count, 2),
|
|
'last_order_date' => '', // TODO: Implement
|
|
'segment' => 'returning', // TODO: Calculate
|
|
'days_since_last_order' => 0, // TODO: Calculate
|
|
];
|
|
}
|
|
|
|
// Get new vs returning
|
|
$new_customers = $wpdb->get_var($wpdb->prepare("
|
|
SELECT COUNT(DISTINCT customer_id)
|
|
FROM {$orders_table}
|
|
WHERE customer_id > 0
|
|
AND date_created_gmt >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
AND customer_id IN (
|
|
SELECT customer_id
|
|
FROM {$orders_table}
|
|
WHERE customer_id > 0
|
|
GROUP BY customer_id
|
|
HAVING MIN(date_created_gmt) >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
)
|
|
", $days, $days));
|
|
|
|
// Calculate additional metrics
|
|
$returning_customers = $total_customers - intval($new_customers);
|
|
$avg_ltv = $total_customers > 0 ? round($total_revenue / $total_customers, 2) : 0;
|
|
$avg_orders_per_customer = $total_customers > 0 ? round(array_sum(array_column($formatted_customers, 'order_count')) / $total_customers, 2) : 0;
|
|
|
|
return [
|
|
'overview' => [
|
|
'total_customers' => $total_customers,
|
|
'new_customers' => intval($new_customers),
|
|
'returning_customers' => $returning_customers,
|
|
'avg_ltv' => $avg_ltv,
|
|
'retention_rate' => 0, // TODO: Calculate
|
|
'avg_orders_per_customer' => $avg_orders_per_customer,
|
|
'change_percent' => 0, // TODO: Calculate
|
|
],
|
|
'segments' => [
|
|
'new' => intval($new_customers),
|
|
'returning' => $returning_customers,
|
|
'vip' => 0, // TODO: Calculate
|
|
'at_risk' => 0, // TODO: Calculate
|
|
],
|
|
'top_customers' => $formatted_customers,
|
|
'acquisition_chart' => [], // TODO: Implement
|
|
'ltv_distribution' => [], // TODO: Implement
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate orders metrics
|
|
*/
|
|
private static function calculate_orders_metrics($period = '30') {
|
|
global $wpdb;
|
|
$orders_table = $wpdb->prefix . 'wc_orders';
|
|
|
|
// Determine date range
|
|
if ($period === 'all') {
|
|
$date_query = "1=1";
|
|
$days = 365;
|
|
} else {
|
|
$days = intval($period);
|
|
$date_query = "date_created_gmt >= DATE_SUB(NOW(), INTERVAL {$days} DAY)";
|
|
}
|
|
|
|
// Get orders by status
|
|
$by_status = $wpdb->get_results("
|
|
SELECT
|
|
status,
|
|
COUNT(*) as count,
|
|
SUM(total_amount) as total
|
|
FROM {$orders_table}
|
|
WHERE {$date_query}
|
|
GROUP BY status
|
|
ORDER BY count DESC
|
|
");
|
|
|
|
// Format status data
|
|
$formatted_status = [];
|
|
$total_orders = 0;
|
|
$total_revenue = 0;
|
|
|
|
foreach ($by_status as $status) {
|
|
$status_name = str_replace('wc-', '', $status->status);
|
|
$count = intval($status->count);
|
|
$total = round(floatval($status->total), 2);
|
|
|
|
$total_orders += $count;
|
|
if (in_array($status->status, ['wc-completed', 'wc-processing'])) {
|
|
$total_revenue += $total;
|
|
}
|
|
|
|
$formatted_status[] = [
|
|
'status' => $status_name,
|
|
'status_label' => ucfirst($status_name),
|
|
'count' => $count,
|
|
'total' => $total,
|
|
];
|
|
}
|
|
|
|
// Get orders over time
|
|
$chart_data = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
DATE(date_created_gmt) as date,
|
|
COUNT(*) as orders,
|
|
SUM(CASE WHEN status = 'wc-completed' THEN 1 ELSE 0 END) as completed,
|
|
SUM(CASE WHEN status = 'wc-processing' THEN 1 ELSE 0 END) as processing,
|
|
SUM(CASE WHEN status = 'wc-pending' THEN 1 ELSE 0 END) as pending,
|
|
SUM(CASE WHEN status = 'wc-cancelled' THEN 1 ELSE 0 END) as cancelled,
|
|
SUM(CASE WHEN status = 'wc-refunded' THEN 1 ELSE 0 END) as refunded,
|
|
SUM(CASE WHEN status = 'wc-failed' THEN 1 ELSE 0 END) as failed
|
|
FROM {$orders_table}
|
|
WHERE {$date_query}
|
|
GROUP BY DATE(date_created_gmt)
|
|
ORDER BY date ASC
|
|
LIMIT %d
|
|
", $days));
|
|
|
|
// Format chart data
|
|
$formatted_chart = [];
|
|
foreach ($chart_data as $row) {
|
|
$formatted_chart[] = [
|
|
'date' => $row->date,
|
|
'orders' => intval($row->orders),
|
|
'completed' => intval($row->completed),
|
|
'processing' => intval($row->processing),
|
|
'pending' => intval($row->pending),
|
|
'cancelled' => intval($row->cancelled),
|
|
'refunded' => intval($row->refunded),
|
|
'failed' => intval($row->failed),
|
|
];
|
|
}
|
|
|
|
// Calculate average order value and rates
|
|
$avg_order_value = $total_orders > 0 ? round($total_revenue / $total_orders, 2) : 0;
|
|
|
|
// Calculate fulfillment and cancellation rates
|
|
$completed_orders = 0;
|
|
$cancelled_orders = 0;
|
|
foreach ($formatted_status as $status) {
|
|
if ($status['status'] === 'completed') {
|
|
$completed_orders = $status['count'];
|
|
}
|
|
if (in_array($status['status'], ['cancelled', 'failed', 'refunded'])) {
|
|
$cancelled_orders += $status['count'];
|
|
}
|
|
}
|
|
|
|
$fulfillment_rate = $total_orders > 0 ? round(($completed_orders / $total_orders) * 100, 2) : 0;
|
|
$cancellation_rate = $total_orders > 0 ? round(($cancelled_orders / $total_orders) * 100, 2) : 0;
|
|
|
|
// Calculate average processing time (time from pending/processing to completed)
|
|
$avg_processing_time_seconds = $wpdb->get_var($wpdb->prepare("
|
|
SELECT AVG(TIMESTAMPDIFF(SECOND, date_created_gmt, date_updated_gmt))
|
|
FROM {$orders_table}
|
|
WHERE status = 'wc-completed'
|
|
AND {$date_query}
|
|
AND date_updated_gmt > date_created_gmt
|
|
"));
|
|
|
|
// Convert to human-readable format
|
|
$avg_processing_time = '0 hours';
|
|
if ($avg_processing_time_seconds > 0) {
|
|
$hours = floor($avg_processing_time_seconds / 3600);
|
|
$minutes = floor(($avg_processing_time_seconds % 3600) / 60);
|
|
|
|
if ($hours > 24) {
|
|
$days = floor($hours / 24);
|
|
$remaining_hours = $hours % 24;
|
|
$avg_processing_time = $days . ' day' . ($days > 1 ? 's' : '');
|
|
if ($remaining_hours > 0) {
|
|
$avg_processing_time .= ' ' . $remaining_hours . ' hour' . ($remaining_hours > 1 ? 's' : '');
|
|
}
|
|
} elseif ($hours > 0) {
|
|
$avg_processing_time = $hours . ' hour' . ($hours > 1 ? 's' : '');
|
|
if ($minutes > 0) {
|
|
$avg_processing_time .= ' ' . $minutes . ' min';
|
|
}
|
|
} else {
|
|
$avg_processing_time = $minutes . ' minutes';
|
|
}
|
|
}
|
|
|
|
// Add color and percentage to status data - SORT BY IMPORTANCE
|
|
$status_colors = [
|
|
'completed' => '#10b981',
|
|
'processing' => '#3b82f6',
|
|
'pending' => '#f59e0b',
|
|
'cancelled' => '#ef4444',
|
|
'refunded' => '#8b5cf6',
|
|
'failed' => '#dc2626',
|
|
'on-hold' => '#6b7280',
|
|
];
|
|
|
|
// Define sort order (most important first)
|
|
$status_order = ['completed', 'processing', 'pending', 'on-hold', 'cancelled', 'refunded', 'failed'];
|
|
|
|
foreach ($formatted_status as &$status) {
|
|
$status['color'] = $status_colors[$status['status']] ?? '#6b7280';
|
|
$status['percentage'] = $total_orders > 0 ? round(($status['count'] / $total_orders) * 100, 2) : 0;
|
|
}
|
|
|
|
// Sort by predefined order
|
|
usort($formatted_status, function($a, $b) use ($status_order) {
|
|
$pos_a = array_search($a['status'], $status_order);
|
|
$pos_b = array_search($b['status'], $status_order);
|
|
$pos_a = $pos_a !== false ? $pos_a : 999;
|
|
$pos_b = $pos_b !== false ? $pos_b : 999;
|
|
return $pos_a - $pos_b;
|
|
});
|
|
|
|
// Get orders by hour of day
|
|
$by_hour = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
HOUR(date_created_gmt) as hour,
|
|
COUNT(*) as orders
|
|
FROM {$orders_table}
|
|
WHERE {$date_query}
|
|
GROUP BY HOUR(date_created_gmt)
|
|
ORDER BY hour ASC
|
|
"));
|
|
|
|
$formatted_by_hour = [];
|
|
for ($h = 0; $h < 24; $h++) {
|
|
$formatted_by_hour[] = [
|
|
'hour' => $h,
|
|
'orders' => 0,
|
|
];
|
|
}
|
|
foreach ($by_hour as $row) {
|
|
$hour = intval($row->hour);
|
|
$formatted_by_hour[$hour]['orders'] = intval($row->orders);
|
|
}
|
|
|
|
// Get orders by day of week
|
|
$by_day = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
DAYOFWEEK(date_created_gmt) as day_number,
|
|
COUNT(*) as orders
|
|
FROM {$orders_table}
|
|
WHERE {$date_query}
|
|
GROUP BY DAYOFWEEK(date_created_gmt)
|
|
ORDER BY day_number ASC
|
|
"));
|
|
|
|
$day_names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
$formatted_by_day = [];
|
|
for ($d = 1; $d <= 7; $d++) {
|
|
$formatted_by_day[] = [
|
|
'day' => $day_names[$d - 1],
|
|
'day_number' => $d,
|
|
'orders' => 0,
|
|
];
|
|
}
|
|
foreach ($by_day as $row) {
|
|
$day_num = intval($row->day_number);
|
|
$formatted_by_day[$day_num - 1]['orders'] = intval($row->orders);
|
|
}
|
|
|
|
return [
|
|
'overview' => [
|
|
'total_orders' => $total_orders,
|
|
'avg_order_value' => $avg_order_value,
|
|
'fulfillment_rate' => $fulfillment_rate,
|
|
'cancellation_rate' => $cancellation_rate,
|
|
'avg_processing_time' => $avg_processing_time,
|
|
'change_percent' => 0, // TODO: Calculate
|
|
'previous_total_orders' => 0, // TODO: Calculate
|
|
],
|
|
'by_status' => $formatted_status,
|
|
'chart_data' => $formatted_chart,
|
|
'by_hour' => $formatted_by_hour,
|
|
'by_day_of_week' => $formatted_by_day,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate coupons metrics
|
|
*/
|
|
private static function calculate_coupons_metrics($period = '30') {
|
|
global $wpdb;
|
|
$orders_table = $wpdb->prefix . 'wc_orders';
|
|
$order_items_table = $wpdb->prefix . 'woocommerce_order_items';
|
|
$order_itemmeta_table = $wpdb->prefix . 'woocommerce_order_itemmeta';
|
|
$days = $period === 'all' ? 365 : intval($period);
|
|
|
|
// Get coupon usage data
|
|
$coupon_data = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
oi.order_item_name as coupon_code,
|
|
COUNT(DISTINCT oi.order_id) as uses,
|
|
SUM(oim.meta_value) as total_discount
|
|
FROM {$order_items_table} oi
|
|
INNER JOIN {$order_itemmeta_table} oim ON oi.order_item_id = oim.order_item_id
|
|
INNER JOIN {$orders_table} o ON oi.order_id = o.id
|
|
WHERE oi.order_item_type = 'coupon'
|
|
AND oim.meta_key = 'discount_amount'
|
|
AND o.status IN ('wc-completed', 'wc-processing')
|
|
AND o.date_created_gmt >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
GROUP BY oi.order_item_name
|
|
ORDER BY total_discount DESC
|
|
", $days));
|
|
|
|
// Format coupon performance data
|
|
$formatted_coupons = [];
|
|
$total_discount = 0;
|
|
$total_uses = 0;
|
|
|
|
foreach ($coupon_data as $coupon) {
|
|
$discount = round(floatval($coupon->total_discount), 2);
|
|
$uses = intval($coupon->uses);
|
|
|
|
$total_discount += $discount;
|
|
$total_uses += $uses;
|
|
|
|
// Get coupon details from posts table
|
|
$coupon_post = $wpdb->get_row($wpdb->prepare("
|
|
SELECT ID, post_title
|
|
FROM {$wpdb->prefix}posts
|
|
WHERE post_title = %s
|
|
AND post_type = 'shop_coupon'
|
|
LIMIT 1
|
|
", $coupon->coupon_code));
|
|
|
|
$coupon_id = $coupon_post ? intval($coupon_post->ID) : 0;
|
|
$wc_coupon = $coupon_id ? new \WC_Coupon($coupon_id) : null;
|
|
|
|
// Get revenue generated from orders with this coupon
|
|
$revenue_generated = $wpdb->get_var($wpdb->prepare("
|
|
SELECT SUM(o.total_amount)
|
|
FROM {$order_items_table} oi
|
|
INNER JOIN {$orders_table} o ON oi.order_id = o.id
|
|
WHERE oi.order_item_type = 'coupon'
|
|
AND oi.order_item_name = %s
|
|
AND o.status IN ('wc-completed', 'wc-processing')
|
|
AND o.date_created_gmt >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
", $coupon->coupon_code, $days));
|
|
|
|
$revenue_generated = round(floatval($revenue_generated), 2);
|
|
|
|
// Calculate ROI: (Revenue Generated - Discount Given) / Discount Given * 100
|
|
// ROI shows how much revenue was generated per dollar of discount
|
|
$roi = $discount > 0 ? round((($revenue_generated - $discount) / $discount) * 100, 2) : 0;
|
|
|
|
$formatted_coupons[] = [
|
|
'id' => $coupon_id,
|
|
'code' => $coupon->coupon_code,
|
|
'type' => $wc_coupon ? $wc_coupon->get_discount_type() : 'fixed_cart',
|
|
'amount' => $wc_coupon ? floatval($wc_coupon->get_amount()) : 0,
|
|
'uses' => $uses,
|
|
'discount_amount' => $discount,
|
|
'revenue_generated' => $revenue_generated,
|
|
'roi' => $roi,
|
|
'usage_limit' => $wc_coupon ? $wc_coupon->get_usage_limit() : null,
|
|
'expiry_date' => $wc_coupon && $wc_coupon->get_date_expires() ? $wc_coupon->get_date_expires()->format('Y-m-d') : null,
|
|
];
|
|
}
|
|
|
|
// Get usage chart data over time
|
|
$usage_chart = $wpdb->get_results($wpdb->prepare("
|
|
SELECT
|
|
DATE(o.date_created_gmt) as date,
|
|
COUNT(DISTINCT oi.order_id) as uses,
|
|
SUM(oim.meta_value) as discount,
|
|
SUM(o.total_amount) as revenue
|
|
FROM {$order_items_table} oi
|
|
INNER JOIN {$order_itemmeta_table} oim ON oi.order_item_id = oim.order_item_id
|
|
INNER JOIN {$orders_table} o ON oi.order_id = o.id
|
|
WHERE oi.order_item_type = 'coupon'
|
|
AND oim.meta_key = 'discount_amount'
|
|
AND o.status IN ('wc-completed', 'wc-processing')
|
|
AND o.date_created_gmt >= DATE_SUB(NOW(), INTERVAL %d DAY)
|
|
GROUP BY DATE(o.date_created_gmt)
|
|
ORDER BY date ASC
|
|
", $days));
|
|
|
|
$formatted_chart = [];
|
|
$revenue_with_coupons = 0;
|
|
|
|
foreach ($usage_chart as $row) {
|
|
$revenue = round(floatval($row->revenue), 2);
|
|
$revenue_with_coupons += $revenue;
|
|
|
|
$formatted_chart[] = [
|
|
'date' => $row->date,
|
|
'uses' => intval($row->uses),
|
|
'discount' => round(floatval($row->discount), 2),
|
|
'revenue' => $revenue,
|
|
];
|
|
}
|
|
|
|
$avg_discount_per_order = $total_uses > 0 ? round($total_discount / $total_uses, 2) : 0;
|
|
|
|
return [
|
|
'overview' => [
|
|
'total_discount' => $total_discount,
|
|
'coupons_used' => $total_uses,
|
|
'revenue_with_coupons' => $revenue_with_coupons,
|
|
'avg_discount_per_order' => $avg_discount_per_order,
|
|
'change_percent' => 0, // TODO: Calculate
|
|
],
|
|
'usage_chart' => $formatted_chart,
|
|
'coupons' => $formatted_coupons,
|
|
];
|
|
}
|
|
}
|
|
|