Files
WooNooW/includes/Api/AnalyticsController.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,
];
}
}