Files
WooNooW/includes/Api/AnalyticsController.php
dwindown 0aafb65ec0 fix: On-hold and trash color conflict, add dashboard tweaks plan
## 1. Fix On-hold/Trash Color Conflict 

**Issue:** Both statuses used same gray color (#6b7280)

**Solution:**
- On-hold: `#64748b` (Slate 500 - lighter)
- Trash: `#475569` (Slate 600 - darker)

**Result:** Distinct visual identity for each status

---

## 2. Dashboard Tweaks Plan 📋

Created `DASHBOARD_TWEAKS_TODO.md` with:

**Pending Tasks:**
1. **No Data State for Charts**
   - Revenue chart (Dashboard → Revenue)
   - Orders chart (Dashboard → Orders)
   - Coupons chart (Dashboard → Coupons)
   - Show friendly message like Overview does

2. **VIP Customer Settings**
   - New page: `/settings/customers`
   - Configure VIP qualification criteria:
     - Minimum total spent
     - Minimum order count
     - Timeframe (all-time, 30/90/365 days)
     - Require both or either
     - Exclude refunded orders
   - VIP detection logic documented

---

## Notification Settings Structure 

**Recommendation:** Separate subpages (not tabs)

**Structure:**
```
/settings/notifications (overview)
├── /settings/notifications/events (What to notify)
├── /settings/notifications/channels (How to notify)
└── /settings/notifications/templates (Email/channel templates)
```

**Reasoning:**
- Cleaner navigation
- Better performance (load only needed)
- Easier maintenance
- Scalability
- Mobile-friendly

---

## Summary

 Color conflict fixed
📋 Dashboard tweaks documented
 Notification structure decided (subpages)

**Next Steps:**
1. Implement no-data states
2. Build VIP settings page
3. Implement notification system
2025-11-11 00:23:35 +07:00

1223 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 and calculate conversion rate
$formatted_status = [];
$status_colors = [
'wc-completed' => '#10b981', // Green
'wc-processing' => '#3b82f6', // Blue
'wc-pending' => '#f59e0b', // Orange/Amber
'wc-on-hold' => '#64748b', // Slate
'wc-cancelled' => '#ef4444', // Red
'wc-refunded' => '#8b5cf6', // Purple
'wc-failed' => '#dc2626', // Dark Red
'wc-trash' => '#475569', // Dark Slate
];
$total_all_orders = 0;
$completed_orders = 0;
foreach ($orderStatusDistribution as $status) {
$status_name = str_replace('wc-', '', $status->status);
$count = intval($status->count);
$total_all_orders += $count;
if ($status->status === 'wc-completed') {
$completed_orders = $count;
}
$formatted_status[] = [
'status' => ucfirst($status_name),
'count' => $count,
'color' => $status_colors[$status->status] ?? '#6b7280',
];
}
// Calculate metrics
$avg_order_value = $total_orders > 0 ? round($total_revenue / $total_orders, 2) : 0;
// Calculate conversion rate: (Completed Orders / Total Orders) × 100
$conversion_rate = $total_all_orders > 0 ? round(($completed_orders / $total_all_orders) * 100, 2) : 0.00;
// 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' => $conversion_rate,
'yesterday' => 0, // TODO: Calculate previous period
'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', // Green
'processing' => '#3b82f6', // Blue
'pending' => '#f59e0b', // Orange/Amber
'cancelled' => '#ef4444', // Red
'refunded' => '#8b5cf6', // Purple
'failed' => '#dc2626', // Dark Red
'on-hold' => '#64748b', // Slate (changed from gray)
'trash' => '#475569', // Dark Slate (distinct from on-hold)
];
// Define sort order (most important first)
$status_order = ['completed', 'processing', 'pending', 'on-hold', 'cancelled', 'refunded', 'failed', 'trash'];
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,
];
}
}