eventRepository = $eventRepository; $this->ruleRepository = $ruleRepository; } public function primeCache() { $cache_keys = [ 'sodino_dashboard_summary', 'sodino_dashboard_sales_chart', 'sodino_dashboard_rule_performance', 'sodino_dashboard_user_behavior', ]; foreach ($cache_keys as $key) { delete_transient($key); } } public function getDashboardData(array $filters = []) { $cache_key = 'sodino_dashboard_' . md5(wp_json_encode($filters)); $cached = get_transient($cache_key); if ($cached !== false) { return $cached; } $range = $this->getDateRange($filters['range'] ?? '7d', $filters['start_date'] ?? '', $filters['end_date'] ?? ''); $filters['from'] = $range['start']; $filters['to'] = $range['end']; if (!empty($filters['category_id'])) { $filters['product_ids'] = $this->getProductIdsByCategory($filters['category_id']); } $summary = $this->getSummary($filters); $salesChart = $this->getSalesChart($filters); $rulePerformance = $this->getRulePerformance($filters); $roiReport = $this->getRoiReport($filters); $upsellPerformance = $this->getUpsellPerformance($filters); $bannerPerformance = $this->getBannerPerformance($filters, $summary); $userBehavior = $this->getUserBehavior($filters); $insights = $this->getInsights($summary, $filters, $roiReport); $result = [ 'summary' => $summary, 'sales_chart' => $salesChart, 'rule_performance' => $rulePerformance, 'roi_report' => $roiReport, 'upsell_performance' => $upsellPerformance, 'banner_performance' => $bannerPerformance, 'user_behavior' => $userBehavior, 'insights' => $insights, ]; set_transient($cache_key, $result, 10 * MINUTE_IN_SECONDS); return $result; } public function getSummary(array $filters = []) { $purchaseFilters = array_merge($filters, ['event_type' => 'purchase']); $discountFilters = array_merge($filters, ['event_type' => 'discount_applied']); $purchaseCount = $this->eventRepository->getCount($purchaseFilters); $totalRevenue = $this->eventRepository->getSum('value', $purchaseFilters); $totalDiscount = $this->eventRepository->getSum('discount_value', $discountFilters); $productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view'])); $addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart'])); $checkoutStartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'checkout_start'])); $conversionRate = 0; if ($checkoutStartCount > 0) { $conversionRate = round(($purchaseCount / $checkoutStartCount) * 100, 2); } elseif ($addToCartCount > 0) { $conversionRate = round(($purchaseCount / $addToCartCount) * 100, 2); } $addToCartRate = 0; if ($productViewCount > 0) { $addToCartRate = round(($addToCartCount / $productViewCount) * 100, 2); } $bestRule = $this->getBestRule($filters); return [ 'total_revenue' => $totalRevenue, 'total_discount' => $totalDiscount, 'purchase_count' => $purchaseCount, 'conversion_rate' => $conversionRate, 'add_to_cart_rate' => $addToCartRate, 'best_rule' => $bestRule, ]; } public function getSalesChart(array $filters = []) { $filters = array_merge($filters, $this->getDateRange($filters['range'] ?? '7d', $filters['start_date'] ?? '', $filters['end_date'] ?? '')); if (!empty($filters['category_id'])) { $filters['product_ids'] = $this->getProductIdsByCategory($filters['category_id']); } $start = new \DateTime($filters['start']); $end = new \DateTime($filters['end']); $days = []; $series = [ 'before' => [], 'after' => [], 'labels' => [] ]; while ($start <= $end) { $date = $start->format('Y-m-d'); $series['labels'][] = $date; $dayFilters = array_merge($filters, ['from' => $date . ' 00:00:00', 'to' => $date . ' 23:59:59']); $purchases = $this->eventRepository->getEvents(array_merge($dayFilters, ['event_type' => 'purchase'])); $dayRevenue = 0; $dayDiscount = 0; foreach ($purchases as $purchase) { $dayRevenue += floatval($purchase['value']); } $discountEvents = $this->eventRepository->getEvents(array_merge($dayFilters, ['event_type' => 'discount_applied'])); foreach ($discountEvents as $discount) { $dayDiscount += floatval($discount['discount_value']); } $series['after'][] = round($dayRevenue, 2); $series['before'][] = round($dayRevenue + $dayDiscount, 2); $start->modify('+1 day'); } return $series; } public function getRulePerformance(array $filters = []) { $rules = $this->ruleRepository->getAll(); $result = []; foreach ($rules as $rule) { $ruleFilters = array_merge($filters, ['event_type' => 'discount_applied', 'rule_id' => $rule->id]); $count = $this->eventRepository->getCount($ruleFilters); $revenue = $this->eventRepository->getSum('value', $ruleFilters); $totalDiscount = $this->eventRepository->getSum('discount_value', $ruleFilters); if ($count > 0) { $result[] = [ 'name' => $rule->name, 'count' => $count, 'revenue' => round($revenue, 2), 'discount' => round($totalDiscount, 2), ]; } } return $result; } public function getRoiReport(array $filters = []) { $orders = $this->getOrdersInRange($filters); $attributedRevenue = 0; $attributedDiscount = 0; $attributedOrders = []; $ruleRows = []; $upsellRows = []; foreach ($orders as $order) { $orderId = $order->get_id(); foreach ($order->get_items() as $item) { $lineRevenue = (float) $item->get_total(); $ruleIds = $this->parseIdList($item->get_meta('_sodino_rule_ids', true)); $ruleDiscount = (float) $item->get_meta('_sodino_rule_discount', true); $upsellId = (int) $item->get_meta('_sodino_upsell_id', true); if (!empty($ruleIds) || $upsellId > 0) { $attributedRevenue += $lineRevenue; $attributedDiscount += $ruleDiscount; $attributedOrders[$orderId] = true; } foreach ($ruleIds as $ruleId) { if (!isset($ruleRows[$ruleId])) { $ruleRows[$ruleId] = [ 'rule_id' => $ruleId, 'name' => $this->getRuleName($ruleId), 'orders' => [], 'revenue' => 0, 'discount' => 0, ]; } $allocation = count($ruleIds) > 0 ? $lineRevenue / count($ruleIds) : $lineRevenue; $discountAllocation = count($ruleIds) > 0 ? $ruleDiscount / count($ruleIds) : $ruleDiscount; $ruleRows[$ruleId]['orders'][$orderId] = true; $ruleRows[$ruleId]['revenue'] += $allocation; $ruleRows[$ruleId]['discount'] += $discountAllocation; } if ($upsellId > 0) { if (!isset($upsellRows[$upsellId])) { $upsellRows[$upsellId] = [ 'upsell_id' => $upsellId, 'title' => $this->getUpsellTitle($upsellId), 'orders' => [], 'revenue' => 0, ]; } $upsellRows[$upsellId]['orders'][$orderId] = true; $upsellRows[$upsellId]['revenue'] += $lineRevenue; } } } $ruleRows = array_map(function ($row) { $row['order_count'] = count($row['orders']); unset($row['orders']); $row['revenue'] = round($row['revenue'], 2); $row['discount'] = round($row['discount'], 2); return $row; }, array_values($ruleRows)); usort($ruleRows, function ($a, $b) { return $b['revenue'] <=> $a['revenue']; }); $upsellRows = array_map(function ($row) { $row['order_count'] = count($row['orders']); unset($row['orders']); $row['revenue'] = round($row['revenue'], 2); return $row; }, array_values($upsellRows)); usort($upsellRows, function ($a, $b) { return $b['revenue'] <=> $a['revenue']; }); return [ 'attributed_revenue' => round($attributedRevenue, 2), 'attributed_discount' => round($attributedDiscount, 2), 'attributed_order_count' => count($attributedOrders), 'rule_rows' => $ruleRows, 'upsell_rows' => $upsellRows, ]; } public function getUpsellPerformance(array $filters = []) { $roi = $this->getRoiReport($filters); $orderRows = []; foreach ($roi['upsell_rows'] as $row) { $orderRows[(int) $row['upsell_id']] = $row; } $repository = new \Sodino\Repositories\UpsellRepository(); $rows = []; foreach ($repository->getAll() as $upsell) { $orderRow = $orderRows[(int) $upsell->id] ?? null; $impressions = max(0, (int) $upsell->impressions); $addToCartConversions = max(0, (int) $upsell->conversions); $orderCount = $orderRow ? (int) $orderRow['order_count'] : 0; $revenue = $orderRow ? (float) $orderRow['revenue'] : 0; $rows[] = [ 'id' => (int) $upsell->id, 'title' => $upsell->title, 'impressions' => $impressions, 'add_to_cart' => $addToCartConversions, 'orders' => $orderCount, 'revenue' => round($revenue, 2), 'cart_conversion_rate' => $impressions > 0 ? round(($addToCartConversions / $impressions) * 100, 2) : 0, 'order_conversion_rate' => $impressions > 0 ? round(($orderCount / $impressions) * 100, 2) : 0, ]; } usort($rows, function ($a, $b) { return $b['revenue'] <=> $a['revenue']; }); return $rows; } public function getBannerPerformance(array $filters = [], array $summary = []) { $repository = new \Sodino\Repositories\BannerRepository(); $purchaseCount = (int) ($summary['purchase_count'] ?? $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'purchase']))); $clickTotal = 0; foreach ($repository->getAll() as $banner) { $clickTotal += max(0, (int) $banner->clicks); } $rows = []; foreach ($repository->getAll() as $banner) { $impressions = max(0, (int) $banner->impressions); $clicks = max(0, (int) $banner->clicks); $estimatedOrders = $clickTotal > 0 ? round(($clicks / $clickTotal) * $purchaseCount, 2) : 0; $rows[] = [ 'id' => (int) $banner->id, 'title' => $banner->title, 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => $impressions > 0 ? round(($clicks / $impressions) * 100, 2) : 0, 'estimated_orders' => $estimatedOrders, ]; } usort($rows, function ($a, $b) { return $b['clicks'] <=> $a['clicks']; }); return $rows; } public function getUserBehavior(array $filters = []) { $productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view'])); $addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart'])); $checkoutStartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'checkout_start'])); $purchaseCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'purchase'])); return [ 'product_views' => $productViewCount, 'add_to_cart' => $addToCartCount, 'checkout_start' => $checkoutStartCount, 'purchases' => $purchaseCount, ]; } private function getBestRule(array $filters = []) { $performance = $this->getRulePerformance($filters); if (empty($performance)) { return null; } usort($performance, function ($a, $b) { return $b['revenue'] <=> $a['revenue']; }); return $performance[0]['name'] ?? null; } private function getInsights(array $summary, array $filters = [], array $roiReport = []) { $insights = []; if (!empty($roiReport['attributed_revenue'])) { $insights[] = sprintf( __('سودینو %s فروش اثرگرفته ثبت کرده است.', 'sodino'), wp_strip_all_tags(wc_price((float) $roiReport['attributed_revenue'])) ); } if (!empty($summary['best_rule'])) { $insights[] = sprintf('%s %s', __('قانون برتر:', 'sodino'), esc_html($summary['best_rule'])); } if ($summary['total_discount'] > 0 && $summary['total_revenue'] > 0) { $discountShare = round(($summary['total_discount'] / ($summary['total_revenue'] + $summary['total_discount'])) * 100, 2); $insights[] = sprintf( '%s %s%% %s', __('تخفیف‌ها باعث ایجاد', 'sodino'), esc_html($discountShare), __('درصد از درآمد شده‌اند.', 'sodino') ); } if ($summary['conversion_rate'] > 0) { $insights[] = sprintf('%s %s%% %s', __('نرخ تبدیل تقریبی', 'sodino'), esc_html($summary['conversion_rate']), __('است.', 'sodino')); } if (empty($insights)) { $insights[] = __('هیچ دادهٔ قابل تحلیلی هنوز ثبت نشده است.', 'sodino'); } return $insights; } private function getDateRange($range, $start, $end) { $today = current_time('Y-m-d'); $result = [ 'start' => date('Y-m-d', strtotime($today . ' -6 days')), 'end' => $today, ]; if ($range === '30d') { $result['start'] = date('Y-m-d', strtotime($today . ' -29 days')); $result['end'] = $today; } if ($range === 'custom' && !empty($start) && !empty($end)) { $startTimestamp = strtotime($start); $endTimestamp = strtotime($end); if ($startTimestamp && $endTimestamp) { if ($endTimestamp < $startTimestamp) { $endTimestamp = $startTimestamp; } $result['start'] = date('Y-m-d', $startTimestamp); $result['end'] = date('Y-m-d', $endTimestamp); } } return $result; } private function getOrdersInRange(array $filters = []) { if (!function_exists('wc_get_orders')) { return []; } $range = $this->getDateRange($filters['range'] ?? '7d', $filters['start_date'] ?? '', $filters['end_date'] ?? ''); $start = $filters['from'] ?? $range['start']; $end = $filters['to'] ?? $range['end']; return wc_get_orders([ 'limit' => -1, 'status' => ['completed', 'processing'], 'date_created' => $start . '...' . $end, 'return' => 'objects', ]); } private function parseIdList($value) { $ids = []; foreach (explode(',', (string) $value) as $id) { $id = absint(trim($id)); if ($id > 0) { $ids[] = $id; } } return array_values(array_unique($ids)); } private function getRuleName($ruleId) { $rule = $this->ruleRepository->getById((int) $ruleId); return $rule ? $rule->name : sprintf(__('قانون #%d', 'sodino'), (int) $ruleId); } private function getUpsellTitle($upsellId) { $repository = new \Sodino\Repositories\UpsellRepository(); $upsell = $repository->getById((int) $upsellId); return $upsell ? $upsell->title : sprintf(__('آپسل #%d', 'sodino'), (int) $upsellId); } public function getProductIdsByCategory($category_id) { $products = get_posts([ 'post_type' => 'product', 'numberposts' => -1, 'fields' => 'ids', 'tax_query' => [ [ 'taxonomy' => 'product_cat', 'field' => 'term_id', 'terms' => intval($category_id), ], ], ]); return $products ?: []; } public function getProductOptions() { $products = get_posts([ 'post_type' => 'product', 'numberposts' => -1, 'fields' => 'ids', 'orderby' => 'title', 'order' => 'ASC', ]); $options = []; foreach ($products as $product_id) { $options[] = [ 'id' => $product_id, 'name' => get_the_title($product_id), ]; } return $options; } public function getCategoryOptions() { $categories = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => false]); $options = []; if (!is_wp_error($categories)) { foreach ($categories as $category) { $options[] = [ 'id' => $category->term_id, 'name' => $category->name, ]; } } return $options; } }