## 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
1223 lines
35 KiB
PHP
1223 lines
35 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 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,
|
||
];
|
||
}
|
||
}
|
||
|