feat(upsell): apply real cart discounts and track performance

This commit is contained in:
2026-05-08 19:16:01 +03:30
parent 8345e94a1b
commit fd9d29a0ee
19 changed files with 747 additions and 206 deletions

View File

@@ -184,11 +184,11 @@ add_action('admin_menu', function() use ($adminController) {
add_submenu_page( add_submenu_page(
'sodino-dashboard', 'sodino-dashboard',
__('قیمت رقبا (به‌زودی)', 'sodino'), __('ابزارها و سلامت', 'sodino'),
__('قیمت رقبا (به‌زودی)', 'sodino'), __('ابزارها و سلامت', 'sodino'),
'manage_options', 'manage_options',
'sodino-competitor-price', 'sodino-tools',
[$adminController, 'competitorPricePage'] [$adminController, 'toolsPage']
); );
add_submenu_page( add_submenu_page(
@@ -255,6 +255,11 @@ add_action('admin_init', function() use ($ruleController, $settingsController, $
$settingsController->clearCache(); $settingsController->clearCache();
} }
// Tools actions
if ($page === 'sodino-tools') {
$adminController->handleToolsActions();
}
// Banner actions // Banner actions
if (strpos($page, 'sodino') === 0 && in_array($action, ['delete_banner', 'toggle_banner_status'], true)) { if (strpos($page, 'sodino') === 0 && in_array($action, ['delete_banner', 'toggle_banner_status'], true)) {
$adminController->handleBannerActions(); $adminController->handleBannerActions();

View File

@@ -28,6 +28,7 @@ class Sodino_Upsell_List_Table extends WP_List_Table {
'trigger' => __('شرط فعال‌سازی', 'sodino'), 'trigger' => __('شرط فعال‌سازی', 'sodino'),
'suggested_product'=> __('محصول پیشنهادی', 'sodino'), 'suggested_product'=> __('محصول پیشنهادی', 'sodino'),
'discount' => __('تخفیف', 'sodino'), 'discount' => __('تخفیف', 'sodino'),
'performance' => __('عملکرد', 'sodino'),
'status' => __('وضعیت', 'sodino'), 'status' => __('وضعیت', 'sodino'),
'actions' => __('عملیات', 'sodino'), 'actions' => __('عملیات', 'sodino'),
]; ];
@@ -86,6 +87,22 @@ class Sodino_Upsell_List_Table extends WP_List_Table {
return __('بدون تخفیف', 'sodino'); return __('بدون تخفیف', 'sodino');
} }
public function column_performance($item) {
$impressions = max(0, (int) ($item->impressions ?? 0));
$conversions = max(0, (int) ($item->conversions ?? 0));
$rate = $impressions > 0 ? round(($conversions / $impressions) * 100, 2) : 0;
return sprintf(
'%s: %s<br>%s: %s<br>%s: %s%%',
esc_html__('نمایش', 'sodino'),
esc_html(number_format_i18n($impressions)),
esc_html__('افزودن', 'sodino'),
esc_html(number_format_i18n($conversions)),
esc_html__('نرخ', 'sodino'),
esc_html(number_format_i18n($rate, 2))
);
}
public function column_status($item) { public function column_status($item) {
return $item->status ? __('فعال', 'sodino') : __('غیرفعال', 'sodino'); return $item->status ? __('فعال', 'sodino') : __('غیرفعال', 'sodino');
} }

View File

@@ -13,7 +13,7 @@ $menu_items = [
'sodino-add-upsell' => __('افزودن آپسل', 'sodino'), 'sodino-add-upsell' => __('افزودن آپسل', 'sodino'),
'sodino-banners' => __('بنرهای هوشمند', 'sodino'), 'sodino-banners' => __('بنرهای هوشمند', 'sodino'),
'sodino-add-banner' => __('افزودن بنر', 'sodino'), 'sodino-add-banner' => __('افزودن بنر', 'sodino'),
'sodino-competitor-price' => __('قیمت رقبا (به‌زودی)', 'sodino'), 'sodino-tools' => __('ابزارها و سلامت', 'sodino'),
'sodino-settings' => __('تنظیمات', 'sodino'), 'sodino-settings' => __('تنظیمات', 'sodino'),
]; ];
?> ?>

View File

@@ -50,8 +50,8 @@ $form_display_type = function_exists('sodino_old_input') ? sodino_old_input('dis
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن بنر', 'sodino'); ?> <?php _e('افزودن بنر', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-tools'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-tools' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?> <?php _e('ابزارها و سلامت', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?> <?php _e('تنظیمات', 'sodino'); ?>

View File

@@ -48,8 +48,8 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-banners');
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن بنر', 'sodino'); ?> <?php _e('افزودن بنر', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-tools'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-tools' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?> <?php _e('ابزارها و سلامت', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-xl text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?> <?php _e('تنظیمات', 'sodino'); ?>

View File

@@ -1,152 +0,0 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-competitor-price');
?>
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
<div class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900"><?php _e('سودینو', 'sodino'); ?></h1>
<p class="mt-1 text-sm text-gray-500"><?php _e('بهینه‌سازی هوشمند فروش', 'sodino'); ?></p>
</div>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<?php if (function_exists('sodino_render_admin_notice')) { sodino_render_admin_notice(); } ?>
<div class="flex gap-8">
<aside class="w-64 flex-shrink-0">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('منوی سودینو', 'sodino'); ?></h2>
<nav class="space-y-2">
<a href="<?php echo admin_url('admin.php?page=sodino-dashboard'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-dashboard' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('داشبورد', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-rules'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-rules' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قوانین', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-add-rule'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-rule' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن قانون', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-upsells'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-upsells' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('آپسل (پیشنهاد فروش)', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-add-upsell'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-upsell' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن آپسل', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-banners'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-banners' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('بنرهای هوشمند', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن بنر', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?>
</a>
</nav>
</div>
</aside>
<main class="flex-1 min-w-0">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('قیمت رقبا', 'sodino'); ?></h2>
<p class="mt-2 text-gray-600"><?php _e('به‌زودی بخش مانیتورینگ قیمت رقبا فعال خواهد شد.', 'sodino'); ?></p>
</div>
<span class="inline-flex items-center rounded-full bg-yellow-50 px-3 py-1 text-sm font-medium text-yellow-700"><?php _e('به‌زودی', 'sodino'); ?></span>
</div>
</div>
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('مشخصات رقیب', 'sodino'); ?></h3>
<p class="text-sm text-gray-500 mb-6"><?php _e('این قابلیت به شما امکان می‌دهد قیمت محصولات خود را با رقبا مقایسه کرده و بهینه‌سازی کنید. (در نسخه‌های آینده فعال خواهد شد)', 'sodino'); ?></p>
<div class="overflow-x-auto rounded-xl border border-gray-200">
<table class="min-w-full divide-y divide-gray-200 text-right text-sm text-gray-700">
<thead class="bg-gray-50 text-gray-500">
<tr>
<th class="px-4 py-3 font-medium"><?php _e('محصول', 'sodino'); ?></th>
<th class="px-4 py-3 font-medium"><?php _e('قیمت شما', 'sodino'); ?></th>
<th class="px-4 py-3 font-medium"><?php _e('قیمت رقبا', 'sodino'); ?></th>
<th class="px-4 py-3 font-medium"><?php _e('اختلاف قیمت', 'sodino'); ?></th>
<th class="px-4 py-3 font-medium"><?php _e('وضعیت', 'sodino'); ?></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<tr>
<td class="px-4 py-3"><?php _e('کفش دویدن', 'sodino'); ?></td>
<td class="px-4 py-3">23,500 تومان</td>
<td class="px-4 py-3">24,900 تومان</td>
<td class="px-4 py-3 text-green-600">1,400 تومان کمتر</td>
<td class="px-4 py-3"><span class="rounded-full bg-green-50 px-3 py-1 text-xs font-semibold text-green-700"><?php _e('رقبتی', 'sodino'); ?></span></td>
</tr>
<tr>
<td class="px-4 py-3"><?php _e('کرم محافظ پوست', 'sodino'); ?></td>
<td class="px-4 py-3">78,000 تومان</td>
<td class="px-4 py-3">82,000 تومان</td>
<td class="px-4 py-3 text-green-600">4,000 تومان کمتر</td>
<td class="px-4 py-3"><span class="rounded-full bg-green-50 px-3 py-1 text-xs font-semibold text-green-700"><?php _e('رقبتی', 'sodino'); ?></span></td>
</tr>
<tr>
<td class="px-4 py-3"><?php _e('کیف دستی', 'sodino'); ?></td>
<td class="px-4 py-3">155,000 تومان</td>
<td class="px-4 py-3">161,000 تومان</td>
<td class="px-4 py-3 text-red-600">6,000 تومان بیشتر</td>
<td class="px-4 py-3"><span class="rounded-full bg-gray-50 px-3 py-1 text-xs font-semibold text-gray-700"><?php _e('هشدار', 'sodino'); ?></span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="space-y-6">
<div class="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900"><?php _e('افزودن رقیب', 'sodino'); ?></h3>
<span class="rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700"><?php _e('به‌زودی', 'sodino'); ?></span>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"><?php _e('URL سایت رقیب', 'sodino'); ?></label>
<input type="url" disabled class="w-full rounded-lg border border-gray-200 bg-gray-100 px-4 py-3 text-gray-500 shadow-sm" placeholder="https://example.com">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"><?php _e('نام فروشگاه', 'sodino'); ?></label>
<input type="text" disabled class="w-full rounded-lg border border-gray-200 bg-gray-100 px-4 py-3 text-gray-500 shadow-sm" placeholder="<?php _e('مثلاً اسم فروشگاه رقبا', 'sodino'); ?>">
</div>
<button disabled class="w-full rounded-full bg-gray-300 px-4 py-3 text-sm font-semibold text-gray-700"><?php _e('افزودن', 'sodino'); ?></button>
</div>
</div>
<div class="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-900"><?php _e('فعال‌سازی مانیتورینگ قیمت', 'sodino'); ?></h3>
<p class="mt-1 text-sm text-gray-500"><?php _e('این ویژگی در نسخه‌های آینده فعال می‌شود.', 'sodino'); ?></p>
</div>
<div class="flex items-center gap-3">
<span class="px-3 py-1 rounded-full bg-gray-100 text-xs font-semibold text-gray-600"><?php _e('غیرفعال', 'sodino'); ?></span>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-gray-50 p-5 text-sm text-gray-500">
<?php _e('در اینجا می‌توانید فعال‌سازی خودکار تحلیل قیمت رقبا را مشاهده کنید؛ در حال حاضر این بخش در دست ساخت است.', 'sodino'); ?>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</div>

View File

@@ -55,8 +55,8 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-dashboard');
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن بنر', 'sodino'); ?> <?php _e('افزودن بنر', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-tools'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-tools' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?> <?php _e('ابزارها و سلامت', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?> <?php _e('تنظیمات', 'sodino'); ?>

View File

@@ -52,8 +52,8 @@ $form_action_type = function_exists('sodino_old_input') ? sodino_old_input('acti
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن بنر', 'sodino'); ?> <?php _e('افزودن بنر', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-tools'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-tools' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?> <?php _e('ابزارها و سلامت', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?> <?php _e('تنظیمات', 'sodino'); ?>

View File

@@ -50,8 +50,8 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-rules');
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن بنر', 'sodino'); ?> <?php _e('افزودن بنر', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-tools'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-tools' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?> <?php _e('ابزارها و سلامت', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?> <?php _e('تنظیمات', 'sodino'); ?>

131
admin/views/tools.php Normal file
View File

@@ -0,0 +1,131 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-tools');
$toolsData = $toolsData ?? [];
$tables = $toolsData['tables'] ?? [];
$actions = $toolsData['actions'] ?? [];
$settings = $toolsData['settings'] ?? [];
$db_version = $toolsData['db_version'] ?? '0';
$expected_db_version = $toolsData['expected_db_version'] ?? SODINO_VERSION;
$old_event_count = (int) ($toolsData['old_event_count'] ?? 0);
$oldest_event = $toolsData['oldest_event'] ?? '';
?>
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
<div class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6">
<h1 class="text-3xl font-bold text-gray-900"><?php _e('سودینو', 'sodino'); ?></h1>
<p class="mt-1 text-sm text-gray-500"><?php _e('ابزارهای نگهداری، سلامت و بهینه‌سازی افزونه', 'sodino'); ?></p>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<?php if (function_exists('sodino_render_admin_notice')) { sodino_render_admin_notice(); } ?>
<div class="flex gap-8">
<?php include SODINO_PLUGIN_DIR . 'admin/components/sidebar.php'; ?>
<main class="flex-1 min-w-0 space-y-6">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('ابزارها و سلامت', 'sodino'); ?></h2>
<p class="mt-2 text-gray-600"><?php _e('وضعیت دیتابیس، کش و داده‌های تحلیلی سودینو را از اینجا مدیریت کنید.', 'sodino'); ?></p>
</div>
<div class="flex flex-wrap gap-3">
<a href="<?php echo esc_url($actions['clear_cache'] ?? '#'); ?>" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">
<?php _e('پاک‌سازی کش', 'sodino'); ?>
</a>
<a href="<?php echo esc_url($actions['run_migrations'] ?? '#'); ?>" class="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50">
<?php _e('بررسی دیتابیس', 'sodino'); ?>
</a>
</div>
</div>
</div>
<div class="grid gap-6 lg:grid-cols-3">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500"><?php _e('نسخه دیتابیس', 'sodino'); ?></p>
<div class="mt-3 flex items-center justify-between">
<strong class="text-2xl text-gray-900"><?php echo esc_html($db_version); ?></strong>
<?php if (version_compare($db_version, $expected_db_version, '>=')) : ?>
<span class="rounded-full bg-green-50 px-3 py-1 text-xs font-semibold text-green-700"><?php _e('به‌روز', 'sodino'); ?></span>
<?php else : ?>
<span class="rounded-full bg-yellow-50 px-3 py-1 text-xs font-semibold text-yellow-700"><?php _e('نیازمند بررسی', 'sodino'); ?></span>
<?php endif; ?>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500"><?php _e('رویدادهای تحلیلی', 'sodino'); ?></p>
<strong class="mt-3 block text-2xl text-gray-900"><?php echo esc_html(number_format_i18n($tables['events']['count'] ?? 0)); ?></strong>
<p class="mt-2 text-sm text-gray-500">
<?php echo $oldest_event ? esc_html(sprintf(__('قدیمی‌ترین رویداد: %s', 'sodino'), mysql2date('Y/m/d H:i', $oldest_event))) : esc_html__('هنوز رویدادی ثبت نشده است.', 'sodino'); ?>
</p>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500"><?php _e('وضعیت قابلیت‌ها', 'sodino'); ?></p>
<div class="mt-3 flex flex-wrap gap-2">
<span class="rounded-full px-3 py-1 text-xs font-semibold <?php echo !empty($settings['pricing_enabled']) ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-600'; ?>"><?php _e('قیمت‌گذاری', 'sodino'); ?></span>
<span class="rounded-full px-3 py-1 text-xs font-semibold <?php echo !empty($settings['upsell_enabled']) ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-600'; ?>"><?php _e('آپسل', 'sodino'); ?></span>
<span class="rounded-full px-3 py-1 text-xs font-semibold <?php echo !empty($settings['banner_enabled']) ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-600'; ?>"><?php _e('بنر', 'sodino'); ?></span>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-5 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-900"><?php _e('جدول‌های سودینو', 'sodino'); ?></h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-right text-sm">
<thead class="bg-gray-50 text-gray-500">
<tr>
<th class="px-6 py-3 font-medium"><?php _e('بخش', 'sodino'); ?></th>
<th class="px-6 py-3 font-medium"><?php _e('جدول', 'sodino'); ?></th>
<th class="px-6 py-3 font-medium"><?php _e('تعداد رکورد', 'sodino'); ?></th>
<th class="px-6 py-3 font-medium"><?php _e('وضعیت', 'sodino'); ?></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white text-gray-700">
<?php foreach ($tables as $table) : ?>
<tr>
<td class="px-6 py-4 font-medium text-gray-900"><?php echo esc_html($table['label']); ?></td>
<td class="px-6 py-4"><code><?php echo esc_html($table['name']); ?></code></td>
<td class="px-6 py-4"><?php echo esc_html(number_format_i18n($table['count'])); ?></td>
<td class="px-6 py-4">
<?php if (!empty($table['exists'])) : ?>
<span class="rounded-full bg-green-50 px-3 py-1 text-xs font-semibold text-green-700"><?php _e('موجود', 'sodino'); ?></span>
<?php else : ?>
<span class="rounded-full bg-red-50 px-3 py-1 text-xs font-semibold text-red-700"><?php _e('نیازمند ساخت', 'sodino'); ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900"><?php _e('پاک‌سازی داده‌های قدیمی', 'sodino'); ?></h3>
<p class="mt-2 text-sm text-gray-500">
<?php echo esc_html(sprintf(__('رویدادهای قدیمی‌تر از ۹۰ روز آماده حذف: %s', 'sodino'), number_format_i18n($old_event_count))); ?>
</p>
</div>
<a href="<?php echo esc_url($actions['prune_events'] ?? '#'); ?>" onclick="return confirm('<?php echo esc_js(__('رویدادهای قدیمی حذف شوند؟ این عملیات قابل بازگشت نیست.', 'sodino')); ?>');" class="inline-flex items-center justify-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-semibold text-red-700 hover:bg-red-100">
<?php _e('حذف رویدادهای قدیمی', 'sodino'); ?>
</a>
</div>
</div>
</main>
</div>
</div>
</div>

View File

@@ -69,8 +69,8 @@ $product_categories = get_terms([
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن بنر', 'sodino'); ?> <?php _e('افزودن بنر', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-tools'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-tools' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?> <?php _e('ابزارها و سلامت', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?> <?php _e('تنظیمات', 'sodino'); ?>

View File

@@ -48,8 +48,8 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-upsells');
<a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-add-banner'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-banner' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن بنر', 'sodino'); ?> <?php _e('افزودن بنر', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-tools'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-tools' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?> <?php _e('ابزارها و سلامت', 'sodino'); ?>
</a> </a>
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> <a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('تنظیمات', 'sodino'); ?> <?php _e('تنظیمات', 'sodino'); ?>

View File

@@ -102,11 +102,11 @@ class AdminController {
add_submenu_page( add_submenu_page(
'sodino-rules', 'sodino-rules',
__('قیمت رقبا (به‌زودی)', 'sodino'), __('ابزارها و سلامت', 'sodino'),
__('قیمت رقبا (به‌زودی)', 'sodino'), __('ابزارها و سلامت', 'sodino'),
'manage_options', 'manage_options',
'sodino-competitor-price', 'sodino-tools',
[$this, 'competitorPricePage'] [$this, 'toolsPage']
); );
add_submenu_page( add_submenu_page(
@@ -295,10 +295,49 @@ class AdminController {
} }
/** /**
* Competitor price page * Tools and health page
*/ */
public function competitorPricePage() { public function toolsPage() {
include SODINO_PLUGIN_DIR . 'admin/views/competitor-price.php'; $toolsData = $this->getToolsData();
include SODINO_PLUGIN_DIR . 'admin/views/tools.php';
}
public function handleToolsActions() {
if (!current_user_can('manage_options')) {
return;
}
if (($_GET['page'] ?? '') !== 'sodino-tools' || empty($_GET['tool_action'])) {
return;
}
$action = sanitize_key($_GET['tool_action']);
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'sodino_tools_' . $action)) {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
if ($action === 'clear_cache') {
\Sodino\Core\Cache::getInstance()->clearAll();
$this->redirectWithNotice(admin_url('admin.php?page=sodino-tools'), __('کش سودینو با موفقیت پاک شد.', 'sodino'), 'success');
}
if ($action === 'run_migrations') {
require_once SODINO_PLUGIN_DIR . 'database/migrations.php';
sodino_create_tables();
$this->redirectWithNotice(admin_url('admin.php?page=sodino-tools'), __('ساختار دیتابیس سودینو بررسی و به‌روزرسانی شد.', 'sodino'), 'success');
}
if ($action === 'prune_events') {
$deleted = $this->deleteOldEvents(90);
$this->redirectWithNotice(
admin_url('admin.php?page=sodino-tools'),
sprintf(__('پاک‌سازی انجام شد. %d رویداد قدیمی حذف شد.', 'sodino'), $deleted),
'success'
);
}
wp_safe_redirect(admin_url('admin.php?page=sodino-tools'));
exit;
} }
private function listUpsellsPage() { private function listUpsellsPage() {
@@ -385,6 +424,9 @@ class AdminController {
$upsell->target_product_id = max(0, intval($_POST['target_product_id'] ?? 0)); $upsell->target_product_id = max(0, intval($_POST['target_product_id'] ?? 0));
$upsell->discount_type = $discountType; $upsell->discount_type = $discountType;
$upsell->discount_value = max(0, floatval($_POST['discount_value'] ?? 0)); $upsell->discount_value = max(0, floatval($_POST['discount_value'] ?? 0));
if ($discountType === 'percentage') {
$upsell->discount_value = min(100, $upsell->discount_value);
}
$upsell->priority = max(1, intval($_POST['priority'] ?? 10)); $upsell->priority = max(1, intval($_POST['priority'] ?? 10));
$upsell->status = isset($_POST['status']) ? 1 : 0; $upsell->status = isset($_POST['status']) ? 1 : 0;
@@ -551,6 +593,80 @@ class AdminController {
wp_send_json($results); wp_send_json($results);
} }
private function getToolsData() {
global $wpdb;
$tables = [
'rules' => [
'label' => __('قوانین قیمت‌گذاری', 'sodino'),
'name' => $wpdb->prefix . 'sodino_rules',
],
'upsells' => [
'label' => __('آپسل‌ها', 'sodino'),
'name' => $wpdb->prefix . 'sodino_upsells',
],
'banners' => [
'label' => __('بنرها', 'sodino'),
'name' => $wpdb->prefix . 'sodino_banners',
],
'events' => [
'label' => __('رویدادهای تحلیلی', 'sodino'),
'name' => $wpdb->prefix . 'sodino_events',
],
'analytics_cache' => [
'label' => __('کش تحلیلی', 'sodino'),
'name' => $wpdb->prefix . 'sodino_analytics_cache',
],
];
foreach ($tables as $key => $table) {
$exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table['name']));
$tables[$key]['exists'] = (bool) $exists;
$tables[$key]['count'] = $exists ? (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table['name']}") : 0;
}
$eventsTable = $tables['events']['name'];
$oldEventCount = 0;
$oldestEvent = '';
if ($tables['events']['exists']) {
$cutoff = date('Y-m-d H:i:s', current_time('timestamp') - (90 * DAY_IN_SECONDS));
$oldEventCount = (int) $wpdb->get_var(
$wpdb->prepare("SELECT COUNT(*) FROM {$eventsTable} WHERE created_at < %s", $cutoff)
);
$oldestEvent = (string) $wpdb->get_var("SELECT MIN(created_at) FROM {$eventsTable}");
}
return [
'db_version' => get_option('sodino_db_version', '0'),
'expected_db_version' => defined('SODINO_DB_VERSION') ? SODINO_DB_VERSION : SODINO_VERSION,
'settings' => $this->getSettings(),
'tables' => $tables,
'old_event_count' => $oldEventCount,
'oldest_event' => $oldestEvent,
'actions' => [
'clear_cache' => wp_nonce_url(admin_url('admin.php?page=sodino-tools&tool_action=clear_cache'), 'sodino_tools_clear_cache'),
'run_migrations' => wp_nonce_url(admin_url('admin.php?page=sodino-tools&tool_action=run_migrations'), 'sodino_tools_run_migrations'),
'prune_events' => wp_nonce_url(admin_url('admin.php?page=sodino-tools&tool_action=prune_events'), 'sodino_tools_prune_events'),
],
];
}
private function deleteOldEvents($days) {
global $wpdb;
$days = max(1, (int) $days);
$eventsTable = $wpdb->prefix . 'sodino_events';
$exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $eventsTable));
if (!$exists) {
return 0;
}
$cutoff = date('Y-m-d H:i:s', current_time('timestamp') - ($days * DAY_IN_SECONDS));
$deleted = $wpdb->query($wpdb->prepare("DELETE FROM {$eventsTable} WHERE created_at < %s", $cutoff));
return $deleted === false ? 0 : (int) $deleted;
}
private function getSettingsDefaults() { private function getSettingsDefaults() {
return [ return [
'plugin_enabled' => 1, 'plugin_enabled' => 1,

View File

@@ -14,6 +14,8 @@ class Upsell {
public $discount_value; public $discount_value;
public $status; public $status;
public $priority; public $priority;
public $impressions;
public $conversions;
public $created_at; public $created_at;
public $updated_at; public $updated_at;
@@ -27,6 +29,8 @@ class Upsell {
$this->discount_value = isset($data['discount_value']) ? floatval($data['discount_value']) : 0; $this->discount_value = isset($data['discount_value']) ? floatval($data['discount_value']) : 0;
$this->status = isset($data['status']) ? (int) $data['status'] : 1; $this->status = isset($data['status']) ? (int) $data['status'] : 1;
$this->priority = isset($data['priority']) ? (int) $data['priority'] : 10; $this->priority = isset($data['priority']) ? (int) $data['priority'] : 10;
$this->impressions = isset($data['impressions']) ? (int) $data['impressions'] : 0;
$this->conversions = isset($data['conversions']) ? (int) $data['conversions'] : 0;
$this->created_at = $data['created_at'] ?? null; $this->created_at = $data['created_at'] ?? null;
$this->updated_at = $data['updated_at'] ?? null; $this->updated_at = $data['updated_at'] ?? null;
} }
@@ -46,6 +50,8 @@ class Upsell {
'discount_value' => floatval($this->discount_value), 'discount_value' => floatval($this->discount_value),
'status' => $this->status, 'status' => $this->status,
'priority' => $this->priority, 'priority' => $this->priority,
'impressions' => $this->impressions,
'conversions' => $this->conversions,
'created_at' => $this->created_at, 'created_at' => $this->created_at,
'updated_at' => $this->updated_at, 'updated_at' => $this->updated_at,
]; ];

View File

@@ -66,4 +66,18 @@ class UpsellRepository {
global $wpdb; global $wpdb;
return $wpdb->delete($this->table_name, ['id' => $id]); return $wpdb->delete($this->table_name, ['id' => $id]);
} }
public function incrementImpression($id) {
global $wpdb;
return $wpdb->query(
$wpdb->prepare("UPDATE {$this->table_name} SET impressions = impressions + 1 WHERE id = %d", $id)
);
}
public function incrementConversion($id) {
global $wpdb;
return $wpdb->query(
$wpdb->prepare("UPDATE {$this->table_name} SET conversions = conversions + 1 WHERE id = %d", $id)
);
}
} }

View File

@@ -43,8 +43,18 @@ class UpsellService {
} }
$price = floatval($product->get_price()); $price = floatval($product->get_price());
return $this->calculateDiscountedPrice($price, $upsell);
}
public function calculateDiscountedPrice($price, $upsell) {
$price = max(0, floatval($price));
if (!$upsell) {
return $price;
}
if ($upsell->discount_type === 'percentage') { if ($upsell->discount_type === 'percentage') {
return max(0, $price * (1 - floatval($upsell->discount_value) / 100)); $percent = max(0, min(100, floatval($upsell->discount_value)));
return max(0, $price * (1 - $percent / 100));
} }
if ($upsell->discount_type === 'fixed') { if ($upsell->discount_type === 'fixed') {
@@ -54,6 +64,31 @@ class UpsellService {
return $price; return $price;
} }
public function getById($id) {
return $this->upsellRepository->getById((int) $id);
}
public function isValidForCart($upsell, $cart) {
return $cart && !$cart->is_empty() && $this->cartMatchesTrigger($upsell, $cart);
}
public function canApplyToProduct($upsell, $productId, $variationId = 0) {
if (!$upsell || !$upsell->isActive()) {
return false;
}
$targetProductId = (int) $upsell->target_product_id;
return $targetProductId > 0 && ($targetProductId === (int) $productId || $targetProductId === (int) $variationId);
}
public function incrementImpression($upsellId) {
return $this->upsellRepository->incrementImpression((int) $upsellId);
}
public function incrementConversion($upsellId) {
return $this->upsellRepository->incrementConversion((int) $upsellId);
}
public function getTriggerLabel($upsell) { public function getTriggerLabel($upsell) {
switch ($upsell->trigger_type) { switch ($upsell->trigger_type) {
case 'product': case 'product':

View File

@@ -214,5 +214,16 @@ function sodino_run_migrations($from_version) {
if ($has_column($rules_table, 'actions')) { if ($has_column($rules_table, 'actions')) {
$wpdb->query("UPDATE {$rules_table} SET actions = '[]' WHERE actions IS NULL OR actions = ''"); $wpdb->query("UPDATE {$rules_table} SET actions = '[]' WHERE actions IS NULL OR actions = ''");
} }
$upsell_table = $wpdb->prefix . 'sodino_upsells';
if ($has_column($upsell_table, 'id')) {
if (!$has_column($upsell_table, 'impressions')) {
$wpdb->query("ALTER TABLE {$upsell_table} ADD COLUMN impressions bigint(20) NOT NULL DEFAULT 0 AFTER priority");
}
if (!$has_column($upsell_table, 'conversions')) {
$wpdb->query("ALTER TABLE {$upsell_table} ADD COLUMN conversions bigint(20) NOT NULL DEFAULT 0 AFTER impressions");
}
}
} }
} }

View File

@@ -0,0 +1,213 @@
.sodino-upsell-panel {
direction: rtl;
margin: 0 0 28px;
padding: 22px;
color: #172033;
background: #ffffff;
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 18px;
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08);
}
.sodino-upsell-panel *,
.sodino-upsell-panel *::before,
.sodino-upsell-panel *::after {
box-sizing: border-box;
}
.sodino-upsell-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.sodino-upsell-eyebrow {
margin: 0 0 6px;
color: #2563eb;
font-size: 13px;
font-weight: 700;
}
.sodino-upsell-header h2 {
margin: 0;
color: #111827;
font-size: 22px;
line-height: 1.45;
font-weight: 800;
}
.sodino-upsell-count {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 6px 12px;
color: #1d4ed8;
background: #eff6ff;
border-radius: 999px;
font-size: 13px;
font-weight: 700;
}
.sodino-upsell-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.sodino-upsell-card {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
gap: 16px;
min-height: 168px;
padding: 16px;
background: #f8fafc;
border: 1px solid rgba(15, 23, 42, 0.1);
border-radius: 16px;
}
.sodino-upsell-media {
width: 96px;
height: 96px;
overflow: hidden;
background: #ffffff;
border-radius: 14px;
}
.sodino-upsell-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.sodino-upsell-body {
min-width: 0;
}
.sodino-upsell-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.sodino-upsell-meta span {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 4px 9px;
color: #047857;
background: #ecfdf5;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.sodino-upsell-meta span + span {
color: #475569;
background: #e2e8f0;
}
.sodino-upsell-title {
margin: 0 0 4px;
color: #475569;
font-size: 13px;
font-weight: 700;
}
.sodino-upsell-card h3 {
margin: 0;
color: #111827;
font-size: 17px;
line-height: 1.45;
font-weight: 800;
}
.sodino-upsell-footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
}
.sodino-upsell-price {
display: grid;
gap: 4px;
color: #111827;
font-size: 17px;
font-weight: 800;
}
.sodino-upsell-price del {
margin-right: 8px;
color: #94a3b8;
font-size: 13px;
font-weight: 600;
}
.sodino-upsell-price small {
color: #047857;
font-size: 12px;
font-weight: 700;
}
.sodino-upsell-button {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 10px 16px;
color: #ffffff !important;
background: #2563eb;
border-radius: 999px;
font-size: 13px;
font-weight: 800;
text-decoration: none !important;
transition: background-color 160ms ease, transform 160ms ease;
}
.sodino-upsell-button:hover,
.sodino-upsell-button:focus {
color: #ffffff !important;
background: #1d4ed8;
transform: translateY(-1px);
}
@media (max-width: 900px) {
.sodino-upsell-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 560px) {
.sodino-upsell-panel {
padding: 16px;
border-radius: 14px;
}
.sodino-upsell-header,
.sodino-upsell-footer {
flex-direction: column;
align-items: stretch;
}
.sodino-upsell-card {
grid-template-columns: 76px minmax(0, 1fr);
gap: 12px;
padding: 12px;
}
.sodino-upsell-media {
width: 76px;
height: 76px;
}
.sodino-upsell-button {
width: 100%;
}
}

View File

@@ -12,6 +12,21 @@ $upsellRepository = new UpsellRepository();
$sodino_upsell_service = new UpsellService($upsellRepository); $sodino_upsell_service = new UpsellService($upsellRepository);
add_action('woocommerce_before_cart', 'sodino_render_upsell_suggestions'); add_action('woocommerce_before_cart', 'sodino_render_upsell_suggestions');
add_action('wp_enqueue_scripts', 'sodino_enqueue_upsell_assets');
add_filter('woocommerce_add_cart_item_data', 'sodino_add_upsell_cart_item_data', 10, 4);
add_filter('woocommerce_get_cart_item_from_session', 'sodino_restore_upsell_cart_item_data', 10, 2);
add_action('woocommerce_before_calculate_totals', 'sodino_apply_upsell_cart_prices', 20);
add_filter('woocommerce_get_item_data', 'sodino_display_upsell_cart_item_data', 10, 2);
add_action('woocommerce_checkout_create_order_line_item', 'sodino_add_upsell_order_item_meta', 10, 4);
add_action('woocommerce_add_to_cart', 'sodino_track_upsell_conversion', 20, 6);
function sodino_enqueue_upsell_assets() {
if (is_admin()) {
return;
}
wp_enqueue_style('sodino-upsell-frontend', plugin_dir_url(__FILE__) . '../css/upsell-frontend.css', [], SODINO_VERSION);
}
function sodino_render_upsell_suggestions() { function sodino_render_upsell_suggestions() {
if (is_admin() || !is_cart()) { if (is_admin() || !is_cart()) {
@@ -41,19 +56,19 @@ function sodino_render_upsell_suggestions() {
return; return;
} }
echo '<div class="sodino-upsell-panel bg-white rounded-2xl border border-gray-200 p-6 mb-8 shadow-sm">'; echo '<section class="sodino-upsell-panel" aria-label="' . esc_attr__('پیشنهادهای ویژه سودینو', 'sodino') . '">';
echo '<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">'; echo '<div class="sodino-upsell-header">';
echo '<div>'; echo '<div>';
echo '<p class="text-sm font-semibold text-blue-600">' . esc_html__('پیشنهاد ویژه آپسل', 'sodino') . '</p>'; echo '<p class="sodino-upsell-eyebrow">' . esc_html__('پیشنهاد ویژه', 'sodino') . '</p>';
echo '<h2 class="mt-2 text-xl font-bold text-gray-900">' . esc_html__('این محصول را همراه خرید خود با تخفیف ویژه دریافت کنید', 'sodino') . '</h2>'; echo '<h2>' . esc_html__('این پیشنهادها را با تخفیف به سبد خود اضافه کنید', 'sodino') . '</h2>';
echo '</div>'; echo '</div>';
echo '<span class="inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700">' . count($upsells) . ' ' . esc_html__('پیشنهاد فعال', 'sodino') . '</span>'; echo '<span class="sodino-upsell-count">' . esc_html(sprintf(__('%d پیشنهاد فعال', 'sodino'), count($upsells))) . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="mt-6 grid gap-4 lg:grid-cols-'.min(2, count($upsells)).'">'; echo '<div class="sodino-upsell-grid">';
foreach ($upsells as $upsell) { foreach ($upsells as $upsell) {
$product = wc_get_product($upsell->target_product_id); $product = wc_get_product($upsell->target_product_id);
if (!$product) { if (!$product || !$product->is_purchasable() || !$product->is_in_stock() || !$product->is_type(['simple', 'variation'])) {
continue; continue;
} }
@@ -61,31 +76,161 @@ function sodino_render_upsell_suggestions() {
$originalPrice = floatval($product->get_price()); $originalPrice = floatval($product->get_price());
$priceHtml = wc_price($discountedPrice); $priceHtml = wc_price($discountedPrice);
if ($discountedPrice < $originalPrice) { if ($discountedPrice < $originalPrice) {
$priceHtml .= ' <span class="mr-2 text-sm text-gray-500 line-through">' . wc_price($originalPrice) . '</span>'; $priceHtml .= ' <del>' . wc_price($originalPrice) . '</del>';
} }
$addToCartUrl = esc_url(add_query_arg('add-to-cart', $product->get_id(), wc_get_cart_url())); $addToCartUrl = sodino_get_upsell_add_to_cart_url($product, $upsell);
$image = $product->get_image('woocommerce_thumbnail', ['class' => 'h-20 w-20 rounded-xl object-cover']); $image = $product->get_image('woocommerce_thumbnail', ['class' => 'sodino-upsell-image']);
$savings = max(0, $originalPrice - $discountedPrice);
$sodino_upsell_service->incrementImpression($upsell->id);
echo '<div class="rounded-2xl border border-gray-200 p-5 bg-gray-50">'; echo '<article class="sodino-upsell-card">';
echo '<div class="flex gap-4">'; echo '<div class="sodino-upsell-media">' . $image . '</div>';
echo '<div class="flex-shrink-0">' . $image . '</div>'; echo '<div class="sodino-upsell-body">';
echo '<div class="flex-1">'; echo '<div class="sodino-upsell-meta">';
echo '<p class="text-sm font-medium text-gray-700">' . esc_html($upsell->title) . '</p>'; echo '<span>' . esc_html($sodino_upsell_service->getDiscountLabel($upsell)) . '</span>';
echo '<h3 class="mt-2 text-lg font-semibold text-gray-900">' . esc_html($product->get_name()) . '</h3>'; echo '<span>' . esc_html($sodino_upsell_service->getTriggerLabel($upsell)) . '</span>';
echo '<div class="mt-3 flex items-center gap-3">';
echo '<span class="rounded-full bg-green-50 px-3 py-1 text-sm font-medium text-green-700">' . esc_html($sodino_upsell_service->getDiscountLabel($upsell)) . '</span>';
echo '<span class="text-sm text-gray-600">' . esc_html($sodino_upsell_service->getTriggerLabel($upsell)) . '</span>';
echo '</div>';
echo '</div>';
echo '</div>';
echo '<div class="mt-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">';
echo '<div class="text-lg font-semibold text-gray-900">' . $priceHtml . '</div>';
echo '<a href="' . $addToCartUrl . '" class="inline-flex items-center justify-center rounded-full bg-blue-600 px-5 py-3 text-sm font-semibold text-white hover:bg-blue-700">' . esc_html__('افزودن به سبد', 'sodino') . '</a>';
echo '</div>'; echo '</div>';
echo '<p class="sodino-upsell-title">' . esc_html($upsell->title) . '</p>';
echo '<h3>' . esc_html($product->get_name()) . '</h3>';
echo '<div class="sodino-upsell-footer">';
echo '<div class="sodino-upsell-price">' . $priceHtml;
if ($savings > 0) {
echo '<small>' . esc_html(sprintf(__('صرفه‌جویی: %s', 'sodino'), wp_strip_all_tags(wc_price($savings)))) . '</small>';
}
echo '</div>'; echo '</div>';
echo '<a href="' . esc_url($addToCartUrl) . '" class="sodino-upsell-button">' . esc_html__('افزودن با تخفیف', 'sodino') . '</a>';
echo '</div></div>';
echo '</article>';
} }
echo '</div>'; echo '</div>';
echo '</div>'; echo '</section>';
}
function sodino_get_upsell_add_to_cart_url($product, $upsell) {
return add_query_arg(
[
'add-to-cart' => $product->get_id(),
'quantity' => 1,
'sodino_upsell_id' => $upsell->id,
'sodino_upsell_nonce' => wp_create_nonce('sodino_apply_upsell_' . $upsell->id),
],
wc_get_cart_url()
);
}
function sodino_add_upsell_cart_item_data($cart_item_data, $product_id, $variation_id, $quantity) {
if (empty($_REQUEST['sodino_upsell_id'])) {
return $cart_item_data;
}
$upsellId = absint(wp_unslash($_REQUEST['sodino_upsell_id']));
$nonce = sanitize_text_field(wp_unslash($_REQUEST['sodino_upsell_nonce'] ?? ''));
if (!$upsellId || !wp_verify_nonce($nonce, 'sodino_apply_upsell_' . $upsellId)) {
return $cart_item_data;
}
global $sodino_upsell_service;
if (!isset($sodino_upsell_service) || !WC()->cart) {
return $cart_item_data;
}
$upsell = $sodino_upsell_service->getById($upsellId);
if (
!$sodino_upsell_service->canApplyToProduct($upsell, $product_id, $variation_id)
|| !$sodino_upsell_service->isValidForCart($upsell, WC()->cart)
) {
return $cart_item_data;
}
$product = wc_get_product($variation_id ?: $product_id);
if (!$product) {
return $cart_item_data;
}
$originalPrice = floatval($product->get_price());
$discountedPrice = $sodino_upsell_service->calculateDiscountedPrice($originalPrice, $upsell);
$cart_item_data['sodino_upsell'] = [
'id' => (int) $upsell->id,
'title' => sanitize_text_field($upsell->title),
'discount_type' => sanitize_key($upsell->discount_type),
'discount_value' => floatval($upsell->discount_value),
'original_price' => $originalPrice,
'discounted_price' => $discountedPrice,
];
$cart_item_data['sodino_upsell_key'] = md5(wp_json_encode($cart_item_data['sodino_upsell']) . microtime(true));
return $cart_item_data;
}
function sodino_restore_upsell_cart_item_data($cart_item, $values) {
if (!empty($values['sodino_upsell'])) {
$cart_item['sodino_upsell'] = $values['sodino_upsell'];
$cart_item['sodino_upsell_key'] = $values['sodino_upsell_key'] ?? '';
}
return $cart_item;
}
function sodino_apply_upsell_cart_prices($cart) {
if (is_admin() && !defined('DOING_AJAX')) {
return;
}
if (!$cart) {
return;
}
foreach ($cart->get_cart() as $cartItem) {
if (empty($cartItem['sodino_upsell']['discounted_price']) || empty($cartItem['data'])) {
continue;
}
$cartItem['data']->set_price(max(0, floatval($cartItem['sodino_upsell']['discounted_price'])));
}
}
function sodino_display_upsell_cart_item_data($item_data, $cart_item) {
if (empty($cart_item['sodino_upsell'])) {
return $item_data;
}
$upsell = $cart_item['sodino_upsell'];
$item_data[] = [
'key' => __('پیشنهاد سودینو', 'sodino'),
'value' => esc_html($upsell['title']),
];
if (!empty($upsell['original_price']) && floatval($upsell['discounted_price']) < floatval($upsell['original_price'])) {
$item_data[] = [
'key' => __('تخفیف آپسل', 'sodino'),
'value' => wp_kses_post(wc_price(floatval($upsell['original_price']) - floatval($upsell['discounted_price']))),
];
}
return $item_data;
}
function sodino_add_upsell_order_item_meta($item, $cart_item_key, $values, $order) {
if (empty($values['sodino_upsell'])) {
return;
}
$upsell = $values['sodino_upsell'];
$item->add_meta_data('_sodino_upsell_id', (int) $upsell['id'], true);
$item->add_meta_data(__('پیشنهاد سودینو', 'sodino'), sanitize_text_field($upsell['title']), true);
$item->add_meta_data(__('قیمت اصلی آپسل', 'sodino'), wc_price(floatval($upsell['original_price'])), true);
$item->add_meta_data(__('قیمت تخفیفی آپسل', 'sodino'), wc_price(floatval($upsell['discounted_price'])), true);
}
function sodino_track_upsell_conversion($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
if (empty($cart_item_data['sodino_upsell']['id'])) {
return;
}
global $sodino_upsell_service;
if (isset($sodino_upsell_service)) {
$sodino_upsell_service->incrementConversion((int) $cart_item_data['sodino_upsell']['id']);
}
} }