~/icsd.ir — bash
SYSTEM_ONLINE

📊 فصل ۱۳: گزارش و آنالیتیکس پیشرفته

در این فصل یاد می‌گیرید چطور گزارش‌های حرفه‌ای و داشبوردهای آنالیتیکس برای ووکامرس بسازید — با Chart.js لوکال، تقویم شمسی، صادرات Excel/PDF و پیش‌بینی فروش.

📈 WC Analytics پیش‌فرض

از ووکامرس ۴ به بعد، یک ماژول Analytics قدرتمند اضافه شد:

  • WooCommerce → Analytics در منوی ادمین
  • گزارش فروش، سفارش‌ها، محصولات، مالیات
  • قابل فیلتر بر اساس تاریخ، وضعیت، دسته
  • API REST: /wc-analytics/

افزودن گزارش به WC Analytics

add_filter('woocommerce_analytics_report_menu_items', function($items) {
    $items[] = [
        'id'     => 'icsd-vendors-report',
        'title'  => 'گزارش فروشندگان',
        'parent' => 'woocommerce-analytics',
        'path'   => '/analytics/vendors',
    ];
    return $items;
});

📑 صفحه گزارش سفارشی

برای گزارش‌های اختصاصی فارسی با تقویم شمسی:

includes/class-icsd-reports.php
<?php
defined('ABSPATH') || exit;

class ICSD_Reports {
    
    public function __construct() {
        add_action('admin_menu', [$this, 'add_menu']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
    }
    
    public function add_menu() {
        add_submenu_page(
            'woocommerce',
            'گزارش‌های ICSD',
            '📊 گزارش‌های ICSD',
            'view_woocommerce_reports',
            'icsd-reports',
            [$this, 'render_page']
        );
    }
    
    public function enqueue_assets($hook) {
        if (strpos($hook, 'icsd-reports') === false) return;
        
        // Chart.js لوکال (بدون CDN)
        wp_enqueue_script('chartjs', 
            plugins_url('assets/chart.min.js', dirname(__FILE__)),
            [], '4.4.0', true);
        
        wp_enqueue_script('icsd-reports',
            plugins_url('assets/reports.js', dirname(__FILE__)),
            ['chartjs', 'jquery'], '1.0', true);
        
        wp_localize_script('icsd-reports', 'icsdReports', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce'   => wp_create_nonce('icsd_reports'),
        ]);
        
        wp_enqueue_style('icsd-reports',
            plugins_url('assets/reports.css', dirname(__FILE__)));
    }
    
    public function render_page() {
        ?>
        

💰 درآمد کل

-

📦 سفارش‌ها

-

👥 مشتریان جدید

-

📊 میانگین سبد

-

روند فروش روزانه

فروش بر اساس دسته

محصولات پرفروش

توزیع جغرافیایی

📈 Chart.js محلی (بدون CDN)

برای جلوگیری از وابستگی به CDN خارجی (مهم برای ایران):

cd wp-content/plugins/icsd-reports/assets/
wget https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js -O chart.min.js

JavaScript گزارش

assets/reports.js
jQuery(function($) {
    let dailyChart, categoryChart, topProductsChart, geoChart;
    
    function loadData() {
        const period = $('#period').val();
        $.post(icsdReports.ajaxUrl, {
            action: 'icsd_get_report_data',
            nonce: icsdReports.nonce,
            period: period,
            date_from: $('#date_from').val(),
            date_to: $('#date_to').val(),
        }, function(response) {
            if (!response.success) return;
            renderKPIs(response.data.kpis);
            renderDailySales(response.data.daily);
            renderCategoryChart(response.data.categories);
            renderTopProducts(response.data.top_products);
            renderGeoChart(response.data.geo);
        });
    }
    
    function renderKPIs(kpis) {
        $('#kpi-revenue').text(formatPrice(kpis.revenue));
        $('#kpi-orders').text(kpis.orders.toLocaleString('fa-IR'));
        $('#kpi-new-customers').text(kpis.new_customers.toLocaleString('fa-IR'));
        $('#kpi-aov').text(formatPrice(kpis.aov));
        
        const change = kpis.revenue_change;
        const arrow = change >= 0 ? '⬆' : '⬇';
        const color = change >= 0 ? 'green' : 'red';
        $('#kpi-revenue-change').html(
            `${arrow} ${Math.abs(change)}٪ نسبت به قبل`
        );
    }
    
    function renderDailySales(data) {
        if (dailyChart) dailyChart.destroy();
        const ctx = document.getElementById('dailySalesChart').getContext('2d');
        
        dailyChart = new Chart(ctx, {
            type: 'line',
            data: {
                labels: data.labels, // تاریخ‌های شمسی
                datasets: [{
                    label: 'فروش (تومان)',
                    data: data.values,
                    borderColor: '#7f54b3',
                    backgroundColor: 'rgba(127, 84, 179, 0.1)',
                    fill: true,
                    tension: 0.3,
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: { rtl: true, labels: { font: { family: 'Vazirmatn' } } },
                    tooltip: { rtl: true, titleFont: { family: 'Vazirmatn' } },
                },
                scales: {
                    y: {
                        ticks: { callback: v => formatPrice(v), font: { family: 'Vazirmatn' } }
                    },
                    x: { ticks: { font: { family: 'Vazirmatn' } } }
                }
            }
        });
    }
    
    function renderCategoryChart(data) {
        if (categoryChart) categoryChart.destroy();
        const ctx = document.getElementById('categoryChart').getContext('2d');
        
        categoryChart = new Chart(ctx, {
            type: 'doughnut',
            data: {
                labels: data.labels,
                datasets: [{
                    data: data.values,
                    backgroundColor: ['#7f54b3', '#0073aa', '#46b450', 
                                      '#ffba00', '#dc3232', '#9b51e0'],
                }]
            },
            options: {
                responsive: true,
                plugins: { legend: { position: 'bottom', rtl: true } }
            }
        });
    }
    
    function renderTopProducts(data) {
        if (topProductsChart) topProductsChart.destroy();
        const ctx = document.getElementById('topProductsChart').getContext('2d');
        
        topProductsChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: data.labels,
                datasets: [{
                    label: 'تعداد فروش',
                    data: data.values,
                    backgroundColor: '#7f54b3',
                }]
            },
            options: {
                indexAxis: 'y', // افقی برای راحتی نمایش
                responsive: true,
                plugins: { legend: { display: false } }
            }
        });
    }
    
    function formatPrice(amount) {
        return new Intl.NumberFormat('fa-IR').format(Math.round(amount)) + ' تومان';
    }
    
    $('#apply-filter').on('click', loadData);
    $('#period').on('change', function() {
        if ($(this).val() === 'custom') {
            $('#date_from, #date_to').removeClass('hidden');
        } else {
            $('#date_from, #date_to').addClass('hidden');
        }
    });
    
    loadData(); // اولین بار
});

📅 تقویم شمسی

برای استفاده از تاریخ شمسی در گزارش:

// تابع تبدیل گرگوری به شمسی
function gregorian_to_jalali($gy, $gm, $gd) {
    $g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
    $gy2 = ($gm > 2) ? ($gy + 1) : $gy;
    $days = 355666 + (365 * $gy) + ((int)(($gy2 + 3) / 4)) - ((int)(($gy2 + 99) / 100)) 
            + ((int)(($gy2 + 399) / 400)) + $gd + $g_d_m[$gm - 1];
    $jy = -1595 + (33 * ((int)($days / 12053)));
    $days %= 12053;
    $jy += 4 * ((int)($days / 1461));
    $days %= 1461;
    if ($days > 365) {
        $jy += (int)(($days - 1) / 365);
        $days = ($days - 1) % 365;
    }
    if ($days < 186) {
        $jm = 1 + (int)($days / 31);
        $jd = 1 + ($days % 31);
    } else {
        $jm = 7 + (int)(($days - 186) / 30);
        $jd = 1 + (($days - 186) % 30);
    }
    return [$jy, $jm, $jd];
}

function date_jalali($timestamp) {
    list($jy, $jm, $jd) = gregorian_to_jalali(
        (int)date('Y', $timestamp),
        (int)date('n', $timestamp),
        (int)date('j', $timestamp)
    );
    
    $months = ['فروردین','اردیبهشت','خرداد','تیر','مرداد','شهریور',
               'مهر','آبان','آذر','دی','بهمن','اسفند'];
    
    return sprintf('%d %s %d', $jd, $months[$jm-1], $jy);
}

🏆 محصولات پرفروش / کم‌فروش

// AJAX endpoint
add_action('wp_ajax_icsd_get_report_data', function() {
    check_ajax_referer('icsd_reports', 'nonce');
    if (!current_user_can('view_woocommerce_reports')) wp_die();
    
    $period = sanitize_text_field($_POST['period'] ?? 'month');
    list($from, $to) = get_period_dates($period);
    
    wp_send_json_success([
        'kpis'         => get_kpis($from, $to),
        'daily'        => get_daily_sales($from, $to),
        'categories'   => get_category_sales($from, $to),
        'top_products' => get_top_products($from, $to),
        'geo'          => get_geographic_data($from, $to),
    ]);
});

function get_top_products($from, $to, $limit = 10) {
    global $wpdb;
    
    $results = $wpdb->get_results($wpdb->prepare(
        "SELECT p.ID, p.post_title,
                SUM(oim.meta_value) as total_qty,
                SUM(oim2.meta_value) as total_sales
         FROM {$wpdb->prefix}woocommerce_order_items oi
         JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim 
              ON oi.order_item_id = oim.order_item_id AND oim.meta_key = '_qty'
         JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim2 
              ON oi.order_item_id = oim2.order_item_id AND oim2.meta_key = '_line_total'
         JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim3 
              ON oi.order_item_id = oim3.order_item_id AND oim3.meta_key = '_product_id'
         JOIN {$wpdb->posts} p ON p.ID = oim3.meta_value
         JOIN {$wpdb->prefix}wc_orders o ON oi.order_id = o.id
         WHERE o.status IN ('wc-completed', 'wc-processing')
         AND o.date_created_gmt BETWEEN %s AND %s
         GROUP BY p.ID
         ORDER BY total_qty DESC
         LIMIT %d",
        $from, $to, $limit
    ));
    
    $labels = $values = [];
    foreach ($results as $r) {
        $labels[] = wp_trim_words($r->post_title, 5);
        $values[] = (int) $r->total_qty;
    }
    
    return ['labels' => $labels, 'values' => $values];
}

// نکته: HPOS از wc_orders استفاده می‌کند، در صورتی که HPOS غیرفعال باشد از posts

🗺️ آنالیز جغرافیایی

function get_geographic_data($from, $to) {
    $orders = wc_get_orders([
        'date_created' => $from . '...' . $to,
        'status'       => ['completed', 'processing'],
        'limit'        => -1,
        'return'       => 'objects',
    ]);
    
    $by_state = [];
    foreach ($orders as $order) {
        $state = $order->get_billing_state();
        $state_name = WC()->countries->get_states('IR')[$state] ?? $state;
        
        if (!isset($by_state[$state_name])) {
            $by_state[$state_name] = ['orders' => 0, 'revenue' => 0];
        }
        $by_state[$state_name]['orders']++;
        $by_state[$state_name]['revenue'] += $order->get_total();
    }
    
    // مرتب‌سازی بر اساس درآمد
    uasort($by_state, fn($a, $b) => $b['revenue'] - $a['revenue']);
    $top_10 = array_slice($by_state, 0, 10, true);
    
    return [
        'labels' => array_keys($top_10),
        'values' => array_column($top_10, 'revenue'),
    ];
}

🔮 پیش‌بینی فروش

الگوریتم ساده Linear Regression برای پیش‌بینی ۳۰ روز آینده:

function forecast_sales($days_history = 90, $days_forecast = 30) {
    global $wpdb;
    
    // داده‌های تاریخی
    $sales = $wpdb->get_results($wpdb->prepare(
        "SELECT DATE(date_created_gmt) as day, SUM(total_amount) as total
         FROM {$wpdb->prefix}wc_orders
         WHERE status IN ('wc-completed', 'wc-processing')
         AND date_created_gmt > DATE_SUB(NOW(), INTERVAL %d DAY)
         GROUP BY day
         ORDER BY day ASC",
        $days_history
    ));
    
    if (count($sales) < 7) {
        return ['error' => 'داده کافی نیست'];
    }
    
    // محاسبه slope و intercept (Least Squares)
    $n = count($sales);
    $x_sum = $y_sum = $xy_sum = $xx_sum = 0;
    
    foreach ($sales as $i => $row) {
        $x = $i;
        $y = floatval($row->total);
        $x_sum += $x;
        $y_sum += $y;
        $xy_sum += $x * $y;
        $xx_sum += $x * $x;
    }
    
    $slope = ($n * $xy_sum - $x_sum * $y_sum) / ($n * $xx_sum - $x_sum * $x_sum);
    $intercept = ($y_sum - $slope * $x_sum) / $n;
    
    // پیش‌بینی
    $forecast = [];
    for ($i = 0; $i < $days_forecast; $i++) {
        $forecast[] = max(0, $slope * ($n + $i) + $intercept);
    }
    
    return [
        'history'  => array_column($sales, 'total'),
        'forecast' => $forecast,
        'slope'    => $slope,
        'avg_daily'=> $y_sum / $n,
    ];
}

💡 الگوریتم بهتر: برای پیش‌بینی دقیق‌تر، الگوریتم‌های ARIMA یا Prophet (پایتون) قوی‌تر هستند، اما برای فروشگاه‌های متوسط همین Linear Regression کافی است.

📤 صادرات Excel و PDF

صادرات Excel با PhpSpreadsheet

// نیاز: composer require phpoffice/phpspreadsheet
use PhpOfficePhpSpreadsheetSpreadsheet;
use PhpOfficePhpSpreadsheetWriterXlsx;

add_action('wp_ajax_icsd_export_excel', function() {
    if (!current_user_can('view_woocommerce_reports')) wp_die();
    
    $orders = wc_get_orders([
        'date_created' => date('Y-m-01') . '...' . date('Y-m-d'),
        'status'       => 'completed',
        'limit'        => -1,
    ]);
    
    $spreadsheet = new Spreadsheet();
    $sheet = $spreadsheet->getActiveSheet();
    
    // RTL
    $sheet->setRightToLeft(true);
    
    // هدر
    $sheet->setCellValue('A1', 'شماره');
    $sheet->setCellValue('B1', 'تاریخ');
    $sheet->setCellValue('C1', 'مشتری');
    $sheet->setCellValue('D1', 'مبلغ');
    $sheet->setCellValue('E1', 'استان');
    
    $row = 2;
    foreach ($orders as $order) {
        $sheet->setCellValue('A' . $row, $order->get_order_number());
        $sheet->setCellValue('B' . $row, date_jalali(strtotime($order->get_date_created())));
        $sheet->setCellValue('C' . $row, $order->get_billing_first_name() . ' ' . $order->get_billing_last_name());
        $sheet->setCellValue('D' . $row, $order->get_total());
        $sheet->setCellValue('E' . $row, WC()->countries->get_states('IR')[$order->get_billing_state()] ?? '');
        $row++;
    }
    
    // استایل هدر
    $sheet->getStyle('A1:E1')->getFont()->setBold(true);
    
    // خروجی
    header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    header('Content-Disposition: attachment;filename="orders-' . date('Y-m-d') . '.xlsx"');
    $writer = new Xlsx($spreadsheet);
    $writer->save('php://output');
    exit;
});

صادرات PDF با Dompdf

use DompdfDompdf;

add_action('wp_ajax_icsd_export_pdf', function() {
$html = '

';

$orders = wc_get_orders(['limit' => 100, 'status' => 'completed']);
foreach ($orders as $order) {
$html .= sprintf(
'

',
$order->get_order_number(),
$order->get_billing_first_name(),
wc_price($order->get_total())
);
}
$html .= '

شماره مشتری مبلغ
%s %s %s

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.