📊 فصل ۱۳: گزارش و آنالیتیکس پیشرفته
در این فصل یاد میگیرید چطور گزارشهای حرفهای و داشبوردهای آنالیتیکس برای ووکامرس بسازید — با 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;
});
📑 صفحه گزارش سفارشی
برای گزارشهای اختصاصی فارسی با تقویم شمسی:
<?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 گزارش
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,
];
}
📤 صادرات 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(
'
%s
%s
%s
',
$order->get_order_number(),
$order->get_billing_first_name(),
wc_price($order->get_total())
);
}
$html .= '