refactor(Core): refactor and optimize code
This commit is contained in:
71
.gitignore
vendored
Normal file
71
.gitignore
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# WordPress
|
||||||
|
.htaccess
|
||||||
|
wp-config.php
|
||||||
|
wp-content/uploads/
|
||||||
|
wp-content/blogs.dir/
|
||||||
|
wp-content/upgrade/
|
||||||
|
wp-content/backup-db/
|
||||||
|
wp-content/advanced-cache.php
|
||||||
|
wp-content/wp-cache-config.php
|
||||||
|
wp-content/cache/
|
||||||
|
wp-content/cache/supercache/
|
||||||
|
|
||||||
|
# WP-CLI
|
||||||
|
wp-cli.local.yml
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Build
|
||||||
|
/dist/
|
||||||
|
/build/
|
||||||
|
*.min.js
|
||||||
|
*.min.css
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
/vendor/
|
||||||
|
composer.lock
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
error_log
|
||||||
|
debug.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.cache
|
||||||
|
|
||||||
|
# OS
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
/tests/coverage/
|
||||||
|
.phpunit.result.cache
|
||||||
|
|
||||||
|
# Deployment
|
||||||
|
deploy.sh
|
||||||
|
.deployment
|
||||||
178
admin/admin.php
178
admin/admin.php
@@ -4,60 +4,196 @@ if (!defined('ABSPATH')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use Sodino\Controllers\RuleController;
|
||||||
|
use Sodino\Controllers\DashboardController;
|
||||||
|
use Sodino\Controllers\SettingsController;
|
||||||
use Sodino\Controllers\AdminController;
|
use Sodino\Controllers\AdminController;
|
||||||
use Sodino\Repositories\BannerRepository;
|
use Sodino\Repositories\BannerRepository;
|
||||||
use Sodino\Repositories\RuleRepository;
|
use Sodino\Repositories\RuleRepository;
|
||||||
use Sodino\Repositories\UpsellRepository;
|
use Sodino\Repositories\UpsellRepository;
|
||||||
|
use Sodino\Repositories\EventRepository;
|
||||||
|
|
||||||
// Initialize admin
|
// Initialize repositories
|
||||||
$ruleRepository = new RuleRepository();
|
$ruleRepository = new RuleRepository();
|
||||||
$upsellRepository = new UpsellRepository();
|
$upsellRepository = new UpsellRepository();
|
||||||
$bannerRepository = new BannerRepository();
|
$bannerRepository = new BannerRepository();
|
||||||
|
$eventRepository = new EventRepository();
|
||||||
|
|
||||||
|
// Initialize controllers
|
||||||
|
$ruleController = new RuleController($ruleRepository);
|
||||||
|
$dashboardController = new DashboardController($eventRepository, $ruleRepository);
|
||||||
|
$settingsController = new SettingsController();
|
||||||
$adminController = new AdminController($ruleRepository, $upsellRepository, $bannerRepository);
|
$adminController = new AdminController($ruleRepository, $upsellRepository, $bannerRepository);
|
||||||
|
|
||||||
// Add menu
|
/**
|
||||||
add_action('admin_menu', [$adminController, 'addMenu']);
|
* Add admin menu
|
||||||
|
*/
|
||||||
|
add_action('admin_menu', function() use ($adminController) {
|
||||||
|
add_menu_page(
|
||||||
|
__('سودینو', 'sodino'),
|
||||||
|
__('سودینو', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-dashboard',
|
||||||
|
[$adminController, 'dashboardPage'],
|
||||||
|
'dashicons-money-alt',
|
||||||
|
56
|
||||||
|
);
|
||||||
|
|
||||||
// Admin AJAX handlers
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('داشبورد', 'sodino'),
|
||||||
|
__('داشبورد', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-dashboard',
|
||||||
|
[$adminController, 'dashboardPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('قوانین قیمتگذاری', 'sodino'),
|
||||||
|
__('قوانین قیمتگذاری', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-rules',
|
||||||
|
[$adminController, 'rulesPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('افزودن قانون', 'sodino'),
|
||||||
|
__('افزودن قانون', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-add-rule',
|
||||||
|
[$adminController, 'addRulePage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('آپسل (پیشنهاد فروش)', 'sodino'),
|
||||||
|
__('آپسل (پیشنهاد فروش)', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-upsells',
|
||||||
|
[$adminController, 'upsellsPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('افزودن آپسل', 'sodino'),
|
||||||
|
__('افزودن آپسل', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-add-upsell',
|
||||||
|
[$adminController, 'addUpsellPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('بنرهای هوشمند', 'sodino'),
|
||||||
|
__('بنرهای هوشمند', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-banners',
|
||||||
|
[$adminController, 'bannersPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('افزودن بنر', 'sodino'),
|
||||||
|
__('افزودن بنر', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-add-banner',
|
||||||
|
[$adminController, 'addBannerPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('تنظیمات', 'sodino'),
|
||||||
|
__('تنظیمات', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-settings',
|
||||||
|
[$adminController, 'settingsPage']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin AJAX handlers
|
||||||
|
*/
|
||||||
add_action('wp_ajax_sodino_search_products', [$adminController, 'searchProductsAjax']);
|
add_action('wp_ajax_sodino_search_products', [$adminController, 'searchProductsAjax']);
|
||||||
|
|
||||||
// Enqueue admin assets
|
/**
|
||||||
add_action('admin_enqueue_scripts', function($hook) use ($adminController) {
|
* Enqueue admin assets
|
||||||
|
*/
|
||||||
|
add_action('admin_enqueue_scripts', function($hook) {
|
||||||
if (strpos($hook, 'sodino') === false) {
|
if (strpos($hook, 'sodino') === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enqueue Tailwind via CDN script
|
// Enqueue Tailwind via CDN
|
||||||
wp_enqueue_script('sodino-tailwind', 'https://cdn.tailwindcss.com', [], null);
|
wp_enqueue_script('sodino-tailwind', 'https://cdn.tailwindcss.com', [], SODINO_VERSION);
|
||||||
|
|
||||||
|
// Admin CSS
|
||||||
wp_enqueue_style('sodino-admin', plugin_dir_url(__FILE__) . 'css/admin.css', [], SODINO_VERSION);
|
wp_enqueue_style('sodino-admin', plugin_dir_url(__FILE__) . 'css/admin.css', [], SODINO_VERSION);
|
||||||
|
|
||||||
if (strpos($hook, 'sodino_page_sodino-dashboard') !== false) {
|
// Dashboard specific scripts
|
||||||
|
if (strpos($hook, 'sodino-dashboard') !== false || strpos($hook, 'sodino_page_sodino-dashboard') !== false) {
|
||||||
wp_enqueue_script('sodino-chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
|
wp_enqueue_script('sodino-chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
|
||||||
wp_enqueue_script('sodino-dashboard-js', plugin_dir_url(__FILE__) . 'js/dashboard.js', ['sodino-chart-js'], null, true);
|
wp_enqueue_script('sodino-dashboard-js', plugin_dir_url(__FILE__) . 'js/dashboard.js', ['sodino-chart-js'], SODINO_VERSION, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strpos($hook, 'sodino_page_sodino-add-upsell') !== false) {
|
// Upsell specific scripts
|
||||||
wp_enqueue_script('sodino-upsell-admin', plugin_dir_url(__FILE__) . 'js/upsell-admin.js', [], SODINO_VERSION, true);
|
if (strpos($hook, 'sodino-add-upsell') !== false || strpos($hook, 'sodino_page_sodino-add-upsell') !== false) {
|
||||||
|
wp_enqueue_script('sodino-upsell-admin', plugin_dir_url(__FILE__) . 'js/upsell-admin.js', ['jquery'], SODINO_VERSION, true);
|
||||||
wp_localize_script('sodino-upsell-admin', 'sodinoUpsellAdmin', [
|
wp_localize_script('sodino-upsell-admin', 'sodinoUpsellAdmin', [
|
||||||
'nonce' => wp_create_nonce('sodino_search_products'),
|
'nonce' => wp_create_nonce('sodino_search_products'),
|
||||||
|
'ajaxUrl' => admin_url('admin-ajax.php')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strpos($hook, 'sodino_page_sodino-add-banner') !== false) {
|
// Banner specific scripts
|
||||||
|
if (strpos($hook, 'sodino-add-banner') !== false || strpos($hook, 'sodino_page_sodino-add-banner') !== false) {
|
||||||
wp_enqueue_media();
|
wp_enqueue_media();
|
||||||
wp_enqueue_script('sodino-banner-admin', plugin_dir_url(__FILE__) . 'js/banner-admin.js', ['jquery'], SODINO_VERSION, true);
|
wp_enqueue_script('sodino-banner-admin', plugin_dir_url(__FILE__) . 'js/banner-admin.js', ['jquery'], SODINO_VERSION, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle delete for any Sodino admin page
|
/**
|
||||||
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && $_GET['action'] === 'delete') {
|
* Handle admin actions
|
||||||
add_action('admin_init', [$adminController, 'handleDelete']);
|
*/
|
||||||
|
add_action('admin_init', function() use ($ruleController, $settingsController, $adminController) {
|
||||||
|
$page = $_GET['page'] ?? '';
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
// Rule actions
|
||||||
|
if ($page === 'sodino-rules' && $action === 'delete') {
|
||||||
|
$ruleController->delete();
|
||||||
}
|
}
|
||||||
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && in_array($_GET['action'], ['delete_banner', 'toggle_banner_status'], true)) {
|
|
||||||
add_action('admin_init', [$adminController, 'handleBannerActions']);
|
// Settings actions
|
||||||
|
if ($page === 'sodino-settings' && $action === 'clear_cache') {
|
||||||
|
$settingsController->clearCache();
|
||||||
}
|
}
|
||||||
// Handle upsell actions
|
|
||||||
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && in_array($_GET['action'], ['delete_upsell', 'toggle_upsell_status'], true)) {
|
// Banner actions
|
||||||
add_action('admin_init', [$adminController, 'handleUpsellActions']);
|
if (strpos($page, 'sodino') === 0 && in_array($action, ['delete_banner', 'toggle_banner_status'], true)) {
|
||||||
|
$adminController->handleBannerActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upsell actions
|
||||||
|
if (strpos($page, 'sodino') === 0 && in_array($action, ['delete_upsell', 'toggle_upsell_status'], true)) {
|
||||||
|
$adminController->handleUpsellActions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show admin notices
|
||||||
|
*/
|
||||||
|
add_action('admin_notices', function() {
|
||||||
|
$notice = get_transient('sodino_admin_notice');
|
||||||
|
|
||||||
|
if ($notice) {
|
||||||
|
$class = $notice['type'] === 'error' ? 'notice-error' : 'notice-success';
|
||||||
|
printf(
|
||||||
|
'<div class="notice %s is-dismissible"><p>%s</p></div>',
|
||||||
|
esc_attr($class),
|
||||||
|
esc_html($notice['message'])
|
||||||
|
);
|
||||||
|
delete_transient('sodino_admin_notice');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
18
admin/components/header.php
Normal file
18
admin/components/header.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<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>
|
||||||
181
admin/components/layout.php
Normal file
181
admin/components/layout.php
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin Layout Component
|
||||||
|
* Usage: sodino_admin_layout($current_page, $content_callback)
|
||||||
|
*/
|
||||||
|
function sodino_admin_layout($current_page, $content_callback) {
|
||||||
|
?>
|
||||||
|
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
|
||||||
|
<?php include SODINO_PLUGIN_DIR . 'admin/components/header.php'; ?>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<?php include SODINO_PLUGIN_DIR . 'admin/components/sidebar.php'; ?>
|
||||||
|
|
||||||
|
<main class="flex-1 min-w-0">
|
||||||
|
<?php call_user_func($content_callback); ?>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card Component
|
||||||
|
*/
|
||||||
|
function sodino_card($title = '', $description = '', $content_callback = null, $classes = '') {
|
||||||
|
?>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 <?php echo esc_attr($classes); ?>">
|
||||||
|
<?php if ($title): ?>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-2"><?php echo esc_html($title); ?></h2>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($description): ?>
|
||||||
|
<p class="text-gray-600 mb-4"><?php echo esc_html($description); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($content_callback): ?>
|
||||||
|
<?php call_user_func($content_callback); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats Card Component
|
||||||
|
*/
|
||||||
|
function sodino_stat_card($title, $value, $type = 'default') {
|
||||||
|
$classes = [
|
||||||
|
'primary' => 'bg-gradient-to-br from-blue-600 to-blue-700 text-white',
|
||||||
|
'default' => 'bg-white border border-gray-200'
|
||||||
|
];
|
||||||
|
|
||||||
|
$class = $classes[$type] ?? $classes['default'];
|
||||||
|
$text_class = $type === 'primary' ? 'text-white opacity-90' : 'text-gray-600';
|
||||||
|
$value_class = $type === 'primary' ? 'text-white' : 'text-gray-900';
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr($class); ?> rounded-lg p-6">
|
||||||
|
<h3 class="text-sm font-medium <?php echo esc_attr($text_class); ?>"><?php echo esc_html($title); ?></h3>
|
||||||
|
<div class="text-2xl font-bold <?php echo esc_attr($value_class); ?> mt-2"><?php echo $value; ?></div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button Component
|
||||||
|
*/
|
||||||
|
function sodino_button($text, $url = '#', $type = 'primary', $icon = '') {
|
||||||
|
$classes = [
|
||||||
|
'primary' => 'bg-blue-600 text-white hover:bg-blue-700',
|
||||||
|
'secondary' => 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50',
|
||||||
|
'danger' => 'bg-red-600 text-white hover:bg-red-700'
|
||||||
|
];
|
||||||
|
|
||||||
|
$class = $classes[$type] ?? $classes['primary'];
|
||||||
|
?>
|
||||||
|
<a href="<?php echo esc_url($url); ?>"
|
||||||
|
class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg <?php echo esc_attr($class); ?> focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
|
||||||
|
<?php if ($icon): ?>
|
||||||
|
<svg class="-ml-1 mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<?php echo $icon; ?>
|
||||||
|
</svg>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php echo esc_html($text); ?>
|
||||||
|
</a>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form Field Component
|
||||||
|
*/
|
||||||
|
function sodino_form_field($args) {
|
||||||
|
$defaults = [
|
||||||
|
'type' => 'text',
|
||||||
|
'name' => '',
|
||||||
|
'label' => '',
|
||||||
|
'value' => '',
|
||||||
|
'placeholder' => '',
|
||||||
|
'required' => false,
|
||||||
|
'description' => '',
|
||||||
|
'options' => [],
|
||||||
|
'class' => ''
|
||||||
|
];
|
||||||
|
|
||||||
|
$args = wp_parse_args($args, $defaults);
|
||||||
|
extract($args);
|
||||||
|
|
||||||
|
$required_attr = $required ? 'required' : '';
|
||||||
|
$field_id = 'sodino_' . $name;
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr($class); ?>">
|
||||||
|
<?php if ($label): ?>
|
||||||
|
<label for="<?php echo esc_attr($field_id); ?>" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<?php echo esc_html($label); ?>
|
||||||
|
<?php if ($required): ?>
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</label>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($type === 'textarea'): ?>
|
||||||
|
<textarea
|
||||||
|
id="<?php echo esc_attr($field_id); ?>"
|
||||||
|
name="<?php echo esc_attr($name); ?>"
|
||||||
|
rows="4"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="<?php echo esc_attr($placeholder); ?>"
|
||||||
|
<?php echo $required_attr; ?>
|
||||||
|
><?php echo esc_textarea($value); ?></textarea>
|
||||||
|
|
||||||
|
<?php elseif ($type === 'select'): ?>
|
||||||
|
<select
|
||||||
|
id="<?php echo esc_attr($field_id); ?>"
|
||||||
|
name="<?php echo esc_attr($name); ?>"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
<?php echo $required_attr; ?>
|
||||||
|
>
|
||||||
|
<?php foreach ($options as $opt_value => $opt_label): ?>
|
||||||
|
<option value="<?php echo esc_attr($opt_value); ?>" <?php selected($value, $opt_value); ?>>
|
||||||
|
<?php echo esc_html($opt_label); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<?php elseif ($type === 'checkbox'): ?>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="<?php echo esc_attr($field_id); ?>"
|
||||||
|
name="<?php echo esc_attr($name); ?>"
|
||||||
|
value="1"
|
||||||
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
<?php checked($value, 1); ?>
|
||||||
|
<?php echo $required_attr; ?>
|
||||||
|
/>
|
||||||
|
<span class="mr-2 text-sm text-gray-700"><?php echo esc_html($label); ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<input
|
||||||
|
type="<?php echo esc_attr($type); ?>"
|
||||||
|
id="<?php echo esc_attr($field_id); ?>"
|
||||||
|
name="<?php echo esc_attr($name); ?>"
|
||||||
|
value="<?php echo esc_attr($value); ?>"
|
||||||
|
placeholder="<?php echo esc_attr($placeholder); ?>"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
<?php echo $required_attr; ?>
|
||||||
|
/>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($description): ?>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php echo esc_html($description); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
31
admin/components/sidebar.php
Normal file
31
admin/components/sidebar.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_page = $current_page ?? '';
|
||||||
|
$menu_items = [
|
||||||
|
'sodino-dashboard' => __('داشبورد', 'sodino'),
|
||||||
|
'sodino-rules' => __('قوانین', 'sodino'),
|
||||||
|
'sodino-add-rule' => __('افزودن قانون', 'sodino'),
|
||||||
|
'sodino-upsells' => __('آپسل (پیشنهاد فروش)', 'sodino'),
|
||||||
|
'sodino-add-upsell' => __('افزودن آپسل', 'sodino'),
|
||||||
|
'sodino-banners' => __('بنرهای هوشمند', 'sodino'),
|
||||||
|
'sodino-add-banner' => __('افزودن بنر', 'sodino'),
|
||||||
|
'sodino-settings' => __('تنظیمات', 'sodino'),
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<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">
|
||||||
|
<?php foreach ($menu_items as $page => $label): ?>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=' . $page); ?>"
|
||||||
|
class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === $page ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php echo esc_html($label); ?>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
@@ -4,197 +4,185 @@ if (!defined('ABSPATH')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-settings');
|
// Load components
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'admin/components/layout.php';
|
||||||
|
|
||||||
|
sodino_admin_layout($current_page ?? 'sodino-settings', function() use ($settings) {
|
||||||
?>
|
?>
|
||||||
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
|
|
||||||
<!-- Header -->
|
|
||||||
<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">
|
|
||||||
<div class="flex gap-8">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<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-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 Content -->
|
|
||||||
<main class="flex-1 min-w-0">
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
<?php sodino_card(
|
||||||
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('تنظیمات سودینو', 'sodino'); ?></h2>
|
__('تنظیمات سودینو', 'sodino'),
|
||||||
<p class="mt-2 text-gray-600"><?php _e('کنترل کامل تجربه قیمتگذاری و بهینهسازی درآمد را از اینجا انجام دهید.', 'sodino'); ?></p>
|
__('تنظیمات عمومی پلاگین را مدیریت کنید.', 'sodino'),
|
||||||
</div>
|
null,
|
||||||
|
'mb-8'
|
||||||
|
); ?>
|
||||||
|
|
||||||
<?php if (isset($_GET['updated']) && $_GET['updated'] === 'true') : ?>
|
<!-- Settings Form -->
|
||||||
<div class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
<div class="flex">
|
<form method="post" class="space-y-8">
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="mr-3">
|
|
||||||
<p class="text-sm font-medium text-green-800"><?php _e('تنظیمات با موفقیت ذخیره شد.', 'sodino'); ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<form method="post" class="space-y-6">
|
|
||||||
<?php wp_nonce_field('sodino_save_settings', 'sodino_settings_nonce'); ?>
|
<?php wp_nonce_field('sodino_save_settings', 'sodino_settings_nonce'); ?>
|
||||||
|
|
||||||
<!-- General Section -->
|
<!-- General Settings -->
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
<div class="border-b border-gray-200 pb-6">
|
||||||
<div class="mb-6">
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('تنظیمات عمومی', 'sodino'); ?></h3>
|
||||||
<h3 class="text-lg font-semibold text-gray-900"><?php _e('عمومی', 'sodino'); ?></h3>
|
<div class="space-y-4">
|
||||||
<p class="mt-1 text-sm text-gray-500"><?php _e('تنظیمات کلی فعالسازی پلاگین و ویژگیهای اصلی.', 'sodino'); ?></p>
|
<?php sodino_form_field([
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'name' => 'plugin_enabled',
|
||||||
|
'label' => __('فعالسازی پلاگین', 'sodino'),
|
||||||
|
'value' => $settings['plugin_enabled'] ?? 1,
|
||||||
|
'description' => __('پلاگین سودینو را فعال یا غیرفعال کنید.', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'name' => 'pricing_enabled',
|
||||||
|
'label' => __('فعالسازی قیمتگذاری پویا', 'sodino'),
|
||||||
|
'value' => $settings['pricing_enabled'] ?? 1,
|
||||||
|
'description' => __('قیمتگذاری پویا بر اساس قوانین تعریفشده.', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'name' => 'upsell_enabled',
|
||||||
|
'label' => __('فعالسازی آپسل', 'sodino'),
|
||||||
|
'value' => $settings['upsell_enabled'] ?? 1,
|
||||||
|
'description' => __('نمایش پیشنهادات فروش به مشتریان.', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'name' => 'banner_enabled',
|
||||||
|
'label' => __('فعالسازی بنرهای هوشمند', 'sodino'),
|
||||||
|
'value' => $settings['banner_enabled'] ?? 1,
|
||||||
|
'description' => __('نمایش بنرهای هدفمند به کاربران.', 'sodino')
|
||||||
|
]); ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
|
||||||
<label class="flex items-center gap-3 text-gray-700">
|
|
||||||
<input type="checkbox" name="plugin_enabled" value="1" <?php checked($settings['plugin_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
||||||
<span><?php _e('فعالسازی کل پلاگین', 'sodino'); ?></span>
|
|
||||||
</label>
|
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('اگر غیرفعال باشد، هیچ قاعدهای اعمال نخواهد شد.', 'sodino'); ?></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
|
||||||
<label class="flex items-center gap-3 text-gray-700">
|
<!-- Pricing Strategy -->
|
||||||
<input type="checkbox" name="pricing_enabled" value="1" <?php checked($settings['pricing_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
<div class="border-b border-gray-200 pb-6">
|
||||||
<span><?php _e('فعالسازی قیمتگذاری پویا', 'sodino'); ?></span>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('استراتژی قیمتگذاری', 'sodino'); ?></h3>
|
||||||
</label>
|
<div class="space-y-4">
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('این گزینه، اعمال قوانین قیمتگذاری را کنترل میکند.', 'sodino'); ?></p>
|
<?php sodino_form_field([
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'name' => 'allow_multiple_rules',
|
||||||
|
'label' => __('اجازه اعمال چند قانون همزمان', 'sodino'),
|
||||||
|
'value' => $settings['allow_multiple_rules'] ?? 0,
|
||||||
|
'description' => __('اگر فعال باشد، چند قانون میتواند روی یک محصول اعمال شود.', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'select',
|
||||||
|
'name' => 'strategy',
|
||||||
|
'label' => __('استراتژی انتخاب قانون', 'sodino'),
|
||||||
|
'value' => $settings['strategy'] ?? 'priority',
|
||||||
|
'options' => [
|
||||||
|
'priority' => __('بر اساس اولویت', 'sodino'),
|
||||||
|
'highest_discount' => __('بیشترین تخفیف', 'sodino'),
|
||||||
|
'first_valid' => __('اولین قانون معتبر', 'sodino')
|
||||||
|
],
|
||||||
|
'description' => __('نحوه انتخاب قانون زمانی که چند قانون معتبر وجود دارد.', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'number',
|
||||||
|
'name' => 'max_discount_percent',
|
||||||
|
'label' => __('حداکثر درصد تخفیف', 'sodino'),
|
||||||
|
'value' => $settings['max_discount_percent'] ?? 100,
|
||||||
|
'placeholder' => '100',
|
||||||
|
'description' => __('حداکثر درصد تخفیفی که میتواند اعمال شود (0-100).', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'number',
|
||||||
|
'name' => 'min_product_price',
|
||||||
|
'label' => __('حداقل قیمت محصول', 'sodino'),
|
||||||
|
'value' => $settings['min_product_price'] ?? 0,
|
||||||
|
'placeholder' => '0',
|
||||||
|
'description' => __('حداقل قیمتی که یک محصول میتواند داشته باشد.', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'name' => 'cart_pricing_enabled',
|
||||||
|
'label' => __('قیمتگذاری در سبد خرید', 'sodino'),
|
||||||
|
'value' => $settings['cart_pricing_enabled'] ?? 1,
|
||||||
|
'description' => __('اعمال قیمتگذاری پویا در صفحه سبد خرید.', 'sodino')
|
||||||
|
]); ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
</div>
|
||||||
<label class="flex items-center gap-3 text-gray-700">
|
|
||||||
<input type="checkbox" name="upsell_enabled" value="1" <?php checked($settings['upsell_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
<!-- Performance Settings -->
|
||||||
<span><?php _e('فعالسازی سیستم آپسل', 'sodino'); ?></span>
|
<div class="border-b border-gray-200 pb-6">
|
||||||
</label>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('تنظیمات عملکرد', 'sodino'); ?></h3>
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('پیشنهادهای درآمدی اضافه را نمایش میدهد.', 'sodino'); ?></p>
|
<div class="space-y-4">
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'name' => 'cache_enabled',
|
||||||
|
'label' => __('فعالسازی کش', 'sodino'),
|
||||||
|
'value' => $settings['cache_enabled'] ?? 1,
|
||||||
|
'description' => __('استفاده از کش برای بهبود عملکرد.', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<?php sodino_form_field([
|
||||||
|
'type' => 'number',
|
||||||
|
'name' => 'cache_duration',
|
||||||
|
'label' => __('مدت زمان کش (ثانیه)', 'sodino'),
|
||||||
|
'value' => $settings['cache_duration'] ?? 3600,
|
||||||
|
'placeholder' => '3600',
|
||||||
|
'description' => __('مدت زمان نگهداری دادهها در کش (پیشفرض: 3600 ثانیه = 1 ساعت).', 'sodino')
|
||||||
|
]); ?>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<?php sodino_button(
|
||||||
|
__('پاک کردن کش', 'sodino'),
|
||||||
|
wp_nonce_url(admin_url('admin.php?page=sodino-settings&action=clear_cache'), 'clear_cache'),
|
||||||
|
'secondary'
|
||||||
|
); ?>
|
||||||
|
<span class="text-sm text-gray-500"><?php _e('تمام کشهای سودینو را پاک میکند.', 'sodino'); ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pricing Behavior Section -->
|
<!-- Advanced Settings -->
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
<div>
|
||||||
<div class="mb-6">
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('تنظیمات پیشرفته', 'sodino'); ?></h3>
|
||||||
<h3 class="text-lg font-semibold text-gray-900"><?php _e('رفتار قیمتگذاری', 'sodino'); ?></h3>
|
<div class="space-y-4">
|
||||||
<p class="mt-1 text-sm text-gray-500"><?php _e('چگونگی اجرای قوانین در فرآیند قیمتگذاری.', 'sodino'); ?></p>
|
<?php sodino_form_field([
|
||||||
</div>
|
'type' => 'checkbox',
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
'name' => 'ab_testing_enabled',
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
'label' => __('فعالسازی A/B Testing', 'sodino'),
|
||||||
<label class="flex items-center gap-3 text-gray-700">
|
'value' => $settings['ab_testing_enabled'] ?? 0,
|
||||||
<input type="checkbox" name="allow_multiple_rules" value="1" <?php checked($settings['allow_multiple_rules'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
'description' => __('تست A/B برای قوانین قیمتگذاری (قابلیت آزمایشی).', 'sodino')
|
||||||
<span><?php _e('اجازه اعمال چند قانون همزمان', 'sodino'); ?></span>
|
]); ?>
|
||||||
</label>
|
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('قوانین معتبر به صورت متوالی اعمال میشوند.', 'sodino'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('استراتژی اعمال', 'sodino'); ?></label>
|
|
||||||
<select name="strategy" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-100">
|
|
||||||
<option value="highest_discount" <?php selected($settings['strategy'], 'highest_discount'); ?>><?php _e('بالاترین تخفیف', 'sodino'); ?></option>
|
|
||||||
<option value="first_valid" <?php selected($settings['strategy'], 'first_valid'); ?>><?php _e('اولین قانون معتبر', 'sodino'); ?></option>
|
|
||||||
<option value="priority" <?php selected($settings['strategy'], 'priority'); ?>><?php _e('بر اساس اولویت', 'sodino'); ?></option>
|
|
||||||
</select>
|
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('استراتژی انتخاب قانون زمانی که بیش از یک قانون معتبر وجود داشته باشد.', 'sodino'); ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Limits Section -->
|
<?php sodino_form_field([
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
'type' => 'checkbox',
|
||||||
<div class="mb-6">
|
'name' => 'scheduled_campaigns_enabled',
|
||||||
<h3 class="text-lg font-semibold text-gray-900"><?php _e('محدودیتها', 'sodino'); ?></h3>
|
'label' => __('فعالسازی کمپینهای زمانبندی شده', 'sodino'),
|
||||||
<p class="mt-1 text-sm text-gray-500"><?php _e('پارامترهای محدودسازی برای حفظ حاشیه سود.', 'sodino'); ?></p>
|
'value' => $settings['scheduled_campaigns_enabled'] ?? 1,
|
||||||
</div>
|
'description' => __('اجرای خودکار قوانین بر اساس تاریخ شروع و پایان.', 'sodino')
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
]); ?>
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('حداکثر درصد تخفیف', 'sodino'); ?></label>
|
|
||||||
<input type="number" name="max_discount_percent" value="<?php echo esc_attr($settings['max_discount_percent']); ?>" min="0" max="100" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-100">
|
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('حداکثر تخفیف مجاز برای هر محصول را تعیین میکند.', 'sodino'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('حداقل قیمت محصول', 'sodino'); ?></label>
|
|
||||||
<input type="number" name="min_product_price" value="<?php echo esc_attr($settings['min_product_price']); ?>" min="0" step="0.01" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-100">
|
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('از کاهش قیمت زیر این مقدار جلوگیری میکند.', 'sodino'); ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Features Section -->
|
<?php sodino_form_field([
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
'type' => 'checkbox',
|
||||||
<div class="mb-6">
|
'name' => 'debug_mode',
|
||||||
<h3 class="text-lg font-semibold text-gray-900"><?php _e('ویژگیها', 'sodino'); ?></h3>
|
'label' => __('حالت دیباگ', 'sodino'),
|
||||||
<p class="mt-1 text-sm text-gray-500"><?php _e('گزینههای پیشرفته برای شخصیسازی رفتار درآمدی.', 'sodino'); ?></p>
|
'value' => $settings['debug_mode'] ?? 0,
|
||||||
</div>
|
'description' => __('فعالسازی لاگهای دیباگ (فقط برای توسعهدهندگان).', 'sodino')
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
]); ?>
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
|
||||||
<label class="flex items-center gap-3 text-gray-700">
|
|
||||||
<input type="checkbox" name="ab_testing_enabled" value="1" <?php checked($settings['ab_testing_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
||||||
<span><?php _e('فعالسازی تست A/B', 'sodino'); ?></span>
|
|
||||||
</label>
|
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('امکان فعالسازی سناریوهای آزمایشی را اضافه میکند.', 'sodino'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
|
||||||
<label class="flex items-center gap-3 text-gray-700">
|
|
||||||
<input type="checkbox" name="cart_pricing_enabled" value="1" <?php checked($settings['cart_pricing_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
||||||
<span><?php _e('اعمال قیمتگذاری در سبد خرید', 'sodino'); ?></span>
|
|
||||||
</label>
|
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('در صورت خاموش بودن، قیمتگذاری پویا فقط در نمایش محصول انجام میشود.', 'sodino'); ?></p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
|
||||||
<label class="flex items-center gap-3 text-gray-700">
|
|
||||||
<input type="checkbox" name="scheduled_campaigns_enabled" value="1" <?php checked($settings['scheduled_campaigns_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
||||||
<span><?php _e('فعالسازی کمپینهای زمانبندی شده', 'sodino'); ?></span>
|
|
||||||
</label>
|
|
||||||
<p class="mt-3 text-sm text-gray-500"><?php _e('با فعال کردن، میتوانید قوانین را به صورت زمانبندیشده اجرا کنید.', 'sodino'); ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<div class="flex justify-end">
|
<div class="flex items-center justify-end gap-4 pt-6 border-t border-gray-200">
|
||||||
<button type="submit" class="inline-flex items-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
|
<button type="submit" class="inline-flex items-center px-6 py-3 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
|
||||||
<?php _e('ذخیره تنظیمات', 'sodino'); ?>
|
<?php _e('ذخیره تنظیمات', 'sodino'); ?>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<?php
|
||||||
|
});
|
||||||
|
?>
|
||||||
|
|||||||
94
app/Controllers/BaseController.php
Normal file
94
app/Controllers/BaseController.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Controllers;
|
||||||
|
|
||||||
|
use Sodino\Core\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Controller
|
||||||
|
*/
|
||||||
|
abstract class BaseController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify nonce
|
||||||
|
*/
|
||||||
|
protected function verifyNonce($nonce_field, $nonce_action) {
|
||||||
|
if (!isset($_POST[$nonce_field]) || !wp_verify_nonce($_POST[$nonce_field], $nonce_action)) {
|
||||||
|
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect with message
|
||||||
|
*/
|
||||||
|
protected function redirect($url, $message = '', $type = 'success') {
|
||||||
|
if ($message) {
|
||||||
|
set_transient('sodino_admin_notice', [
|
||||||
|
'message' => $message,
|
||||||
|
'type' => $type
|
||||||
|
], 30);
|
||||||
|
}
|
||||||
|
wp_safe_redirect($url);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sanitized POST data
|
||||||
|
*/
|
||||||
|
protected function getPostData($key, $default = '') {
|
||||||
|
return isset($_POST[$key]) ? sanitize_text_field($_POST[$key]) : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sanitized GET data
|
||||||
|
*/
|
||||||
|
protected function getQueryData($key, $default = '') {
|
||||||
|
return isset($_GET[$key]) ? sanitize_text_field($_GET[$key]) : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate data
|
||||||
|
*/
|
||||||
|
protected function validate(array $data) {
|
||||||
|
return Validator::make($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render view
|
||||||
|
*/
|
||||||
|
protected function render($view, $data = []) {
|
||||||
|
extract($data);
|
||||||
|
$view_file = SODINO_PLUGIN_DIR . 'admin/views/' . $view . '.php';
|
||||||
|
|
||||||
|
if (file_exists($view_file)) {
|
||||||
|
include $view_file;
|
||||||
|
} else {
|
||||||
|
wp_die(sprintf(__('View file not found: %s', 'sodino'), $view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check user capability
|
||||||
|
*/
|
||||||
|
protected function checkCapability($capability = 'manage_options') {
|
||||||
|
if (!current_user_can($capability)) {
|
||||||
|
wp_die(__('شما دسترسی لازم برای این عملیات را ندارید.', 'sodino'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show admin notice
|
||||||
|
*/
|
||||||
|
public function showAdminNotice() {
|
||||||
|
$notice = get_transient('sodino_admin_notice');
|
||||||
|
|
||||||
|
if ($notice) {
|
||||||
|
$class = $notice['type'] === 'error' ? 'notice-error' : 'notice-success';
|
||||||
|
printf(
|
||||||
|
'<div class="notice %s is-dismissible"><p>%s</p></div>',
|
||||||
|
esc_attr($class),
|
||||||
|
esc_html($notice['message'])
|
||||||
|
);
|
||||||
|
delete_transient('sodino_admin_notice');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Controllers/DashboardController.php
Normal file
48
app/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Controllers;
|
||||||
|
|
||||||
|
use Sodino\Repositories\EventRepository;
|
||||||
|
use Sodino\Repositories\RuleRepository;
|
||||||
|
use Sodino\Services\AnalyticsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard Controller
|
||||||
|
*/
|
||||||
|
class DashboardController extends BaseController {
|
||||||
|
private $analyticsService;
|
||||||
|
|
||||||
|
public function __construct(EventRepository $eventRepository, RuleRepository $ruleRepository) {
|
||||||
|
$this->analyticsService = new AnalyticsService($eventRepository, $ruleRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard page
|
||||||
|
*/
|
||||||
|
public function index() {
|
||||||
|
$this->checkCapability();
|
||||||
|
|
||||||
|
$filters = [
|
||||||
|
'range' => $this->getQueryData('range', '7d'),
|
||||||
|
'start_date' => $this->getQueryData('start_date', ''),
|
||||||
|
'end_date' => $this->getQueryData('end_date', ''),
|
||||||
|
'product_id' => intval($this->getQueryData('product_id', 0)),
|
||||||
|
'category_id' => intval($this->getQueryData('category_id', 0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($filters['product_id'])) {
|
||||||
|
$filters['product_ids'] = [$filters['product_id']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dashboardData = $this->analyticsService->getDashboardData($filters);
|
||||||
|
$productOptions = $this->analyticsService->getProductOptions();
|
||||||
|
$categoryOptions = $this->analyticsService->getCategoryOptions();
|
||||||
|
|
||||||
|
$this->render('dashboard', [
|
||||||
|
'dashboardData' => $dashboardData,
|
||||||
|
'productOptions' => $productOptions,
|
||||||
|
'categoryOptions' => $categoryOptions,
|
||||||
|
'filters' => $filters,
|
||||||
|
'current_page' => 'sodino-dashboard'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
189
app/Controllers/RuleController.php
Normal file
189
app/Controllers/RuleController.php
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Controllers;
|
||||||
|
|
||||||
|
use Sodino\Repositories\RuleRepository;
|
||||||
|
use Sodino\Models\Rule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rule Controller
|
||||||
|
*/
|
||||||
|
class RuleController extends BaseController {
|
||||||
|
private $ruleRepository;
|
||||||
|
|
||||||
|
public function __construct(RuleRepository $ruleRepository) {
|
||||||
|
$this->ruleRepository = $ruleRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List rules page
|
||||||
|
*/
|
||||||
|
public function index() {
|
||||||
|
$this->checkCapability();
|
||||||
|
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'admin/class-rules-list-table.php';
|
||||||
|
$rulesTable = new \Sodino_Rules_List_Table($this->ruleRepository);
|
||||||
|
$rulesTable->prepare_items();
|
||||||
|
|
||||||
|
$this->render('rules-list', [
|
||||||
|
'rulesTable' => $rulesTable,
|
||||||
|
'current_page' => 'sodino-rules'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create rule page
|
||||||
|
*/
|
||||||
|
public function create() {
|
||||||
|
$this->checkCapability();
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
return $this->store();
|
||||||
|
}
|
||||||
|
|
||||||
|
$rule = new Rule();
|
||||||
|
$this->render('rule-form', [
|
||||||
|
'rule' => $rule,
|
||||||
|
'current_page' => 'sodino-add-rule'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit rule page
|
||||||
|
*/
|
||||||
|
public function edit() {
|
||||||
|
$this->checkCapability();
|
||||||
|
|
||||||
|
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||||
|
$rule = $this->ruleRepository->getById($id);
|
||||||
|
|
||||||
|
if (!$rule) {
|
||||||
|
$this->redirect(
|
||||||
|
admin_url('admin.php?page=sodino-rules'),
|
||||||
|
__('قانون یافت نشد.', 'sodino'),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
return $this->update($rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->render('rule-form', [
|
||||||
|
'rule' => $rule,
|
||||||
|
'current_page' => 'sodino-add-rule'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store new rule
|
||||||
|
*/
|
||||||
|
private function store() {
|
||||||
|
$this->verifyNonce('sodino_rule_nonce', 'sodino_save_rule');
|
||||||
|
|
||||||
|
$validator = $this->validate($_POST);
|
||||||
|
$validator->required('name', __('نام قانون الزامی است.', 'sodino'))
|
||||||
|
->numeric('priority')
|
||||||
|
->min('priority', 1)
|
||||||
|
->numeric('usage_limit')
|
||||||
|
->min('usage_limit', 0);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
$this->redirect(
|
||||||
|
admin_url('admin.php?page=sodino-add-rule'),
|
||||||
|
$validator->firstError(),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rule = new Rule();
|
||||||
|
$this->fillRuleFromPost($rule);
|
||||||
|
$this->ruleRepository->save($rule);
|
||||||
|
|
||||||
|
$this->redirect(
|
||||||
|
admin_url('admin.php?page=sodino-rules'),
|
||||||
|
__('قانون با موفقیت ایجاد شد.', 'sodino')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update existing rule
|
||||||
|
*/
|
||||||
|
private function update($rule) {
|
||||||
|
$this->verifyNonce('sodino_rule_nonce', 'sodino_save_rule');
|
||||||
|
|
||||||
|
$validator = $this->validate($_POST);
|
||||||
|
$validator->required('name', __('نام قانون الزامی است.', 'sodino'))
|
||||||
|
->numeric('priority')
|
||||||
|
->min('priority', 1)
|
||||||
|
->numeric('usage_limit')
|
||||||
|
->min('usage_limit', 0);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
$this->redirect(
|
||||||
|
admin_url('admin.php?page=sodino-add-rule&action=edit&id=' . $rule->id),
|
||||||
|
$validator->firstError(),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fillRuleFromPost($rule);
|
||||||
|
$this->ruleRepository->save($rule);
|
||||||
|
|
||||||
|
$this->redirect(
|
||||||
|
admin_url('admin.php?page=sodino-rules'),
|
||||||
|
__('قانون با موفقیت بهروزرسانی شد.', 'sodino')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete rule
|
||||||
|
*/
|
||||||
|
public function delete() {
|
||||||
|
$this->checkCapability();
|
||||||
|
|
||||||
|
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'delete_rule')) {
|
||||||
|
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||||
|
$this->ruleRepository->delete($id);
|
||||||
|
|
||||||
|
$this->redirect(
|
||||||
|
admin_url('admin.php?page=sodino-rules'),
|
||||||
|
__('قانون با موفقیت حذف شد.', 'sodino')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill rule from POST data
|
||||||
|
*/
|
||||||
|
private function fillRuleFromPost($rule) {
|
||||||
|
$rule->name = sanitize_text_field($_POST['name'] ?? '');
|
||||||
|
$rule->priority = max(1, intval($_POST['priority'] ?? 10));
|
||||||
|
$rule->usage_limit = max(0, intval($_POST['usage_limit'] ?? 0));
|
||||||
|
$rule->user_roles = array_map('sanitize_text_field', (array) ($_POST['user_roles'] ?? []));
|
||||||
|
$rule->start_date = !empty($_POST['start_date']) ? sanitize_text_field($_POST['start_date']) : null;
|
||||||
|
$rule->end_date = !empty($_POST['end_date']) ? sanitize_text_field($_POST['end_date']) : null;
|
||||||
|
$rule->enabled = isset($_POST['enabled']) ? 1 : 0;
|
||||||
|
|
||||||
|
// Parse conditions
|
||||||
|
if (isset($_POST['conditions']) && is_array($_POST['conditions'])) {
|
||||||
|
$rule->conditions = array_map(function($condition) {
|
||||||
|
return [
|
||||||
|
'type' => sanitize_text_field($condition['type'] ?? ''),
|
||||||
|
'value' => sanitize_text_field($condition['value'] ?? '')
|
||||||
|
];
|
||||||
|
}, $_POST['conditions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse actions
|
||||||
|
if (isset($_POST['actions']) && is_array($_POST['actions'])) {
|
||||||
|
$rule->actions = array_map(function($action) {
|
||||||
|
return [
|
||||||
|
'type' => sanitize_text_field($action['type'] ?? ''),
|
||||||
|
'value' => sanitize_text_field($action['value'] ?? '')
|
||||||
|
];
|
||||||
|
}, $_POST['actions']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
app/Controllers/SettingsController.php
Normal file
87
app/Controllers/SettingsController.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Controllers;
|
||||||
|
|
||||||
|
use Sodino\Core\Settings;
|
||||||
|
use Sodino\Core\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings Controller
|
||||||
|
*/
|
||||||
|
class SettingsController extends BaseController {
|
||||||
|
private $settings;
|
||||||
|
private $cache;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->settings = Settings::getInstance();
|
||||||
|
$this->cache = Cache::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings page
|
||||||
|
*/
|
||||||
|
public function index() {
|
||||||
|
$this->checkCapability();
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
return $this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $this->settings->all();
|
||||||
|
$this->render('settings', [
|
||||||
|
'settings' => $settings,
|
||||||
|
'current_page' => 'sodino-settings'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save settings
|
||||||
|
*/
|
||||||
|
private function save() {
|
||||||
|
$this->verifyNonce('sodino_settings_nonce', 'sodino_save_settings');
|
||||||
|
|
||||||
|
$settings = [
|
||||||
|
'plugin_enabled' => isset($_POST['plugin_enabled']) ? 1 : 0,
|
||||||
|
'pricing_enabled' => isset($_POST['pricing_enabled']) ? 1 : 0,
|
||||||
|
'upsell_enabled' => isset($_POST['upsell_enabled']) ? 1 : 0,
|
||||||
|
'banner_enabled' => isset($_POST['banner_enabled']) ? 1 : 0,
|
||||||
|
'allow_multiple_rules' => isset($_POST['allow_multiple_rules']) ? 1 : 0,
|
||||||
|
'strategy' => sanitize_text_field($_POST['strategy'] ?? 'priority'),
|
||||||
|
'max_discount_percent' => max(0, min(100, floatval($_POST['max_discount_percent'] ?? 100))),
|
||||||
|
'min_product_price' => max(0, floatval($_POST['min_product_price'] ?? 0)),
|
||||||
|
'ab_testing_enabled' => isset($_POST['ab_testing_enabled']) ? 1 : 0,
|
||||||
|
'cart_pricing_enabled' => isset($_POST['cart_pricing_enabled']) ? 1 : 0,
|
||||||
|
'scheduled_campaigns_enabled' => isset($_POST['scheduled_campaigns_enabled']) ? 1 : 0,
|
||||||
|
'cache_enabled' => isset($_POST['cache_enabled']) ? 1 : 0,
|
||||||
|
'cache_duration' => max(60, intval($_POST['cache_duration'] ?? 3600)),
|
||||||
|
'debug_mode' => isset($_POST['debug_mode']) ? 1 : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->settings->update($settings);
|
||||||
|
|
||||||
|
// Clear cache when settings change
|
||||||
|
$this->cache->clearAll();
|
||||||
|
|
||||||
|
$this->redirect(
|
||||||
|
admin_url('admin.php?page=sodino-settings'),
|
||||||
|
__('تنظیمات با موفقیت ذخیره شد.', 'sodino')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache action
|
||||||
|
*/
|
||||||
|
public function clearCache() {
|
||||||
|
$this->checkCapability();
|
||||||
|
|
||||||
|
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'clear_cache')) {
|
||||||
|
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cache->clearAll();
|
||||||
|
|
||||||
|
$this->redirect(
|
||||||
|
admin_url('admin.php?page=sodino-settings'),
|
||||||
|
__('کش با موفقیت پاک شد.', 'sodino')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
137
app/Core/Cache.php
Normal file
137
app/Core/Cache.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache Manager
|
||||||
|
* Handles caching with WordPress transients and custom database cache
|
||||||
|
*/
|
||||||
|
class Cache {
|
||||||
|
private static $instance = null;
|
||||||
|
private $memory_cache = [];
|
||||||
|
|
||||||
|
public static function getInstance() {
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached value
|
||||||
|
*/
|
||||||
|
public function get($key, $group = 'sodino') {
|
||||||
|
$full_key = $this->buildKey($key, $group);
|
||||||
|
|
||||||
|
// Check memory cache first
|
||||||
|
if (isset($this->memory_cache[$full_key])) {
|
||||||
|
return $this->memory_cache[$full_key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check WordPress transient
|
||||||
|
$value = get_transient($full_key);
|
||||||
|
|
||||||
|
if ($value !== false) {
|
||||||
|
$this->memory_cache[$full_key] = $value;
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cached value
|
||||||
|
*/
|
||||||
|
public function set($key, $value, $expiration = 3600, $group = 'sodino') {
|
||||||
|
$full_key = $this->buildKey($key, $group);
|
||||||
|
|
||||||
|
// Set in memory cache
|
||||||
|
$this->memory_cache[$full_key] = $value;
|
||||||
|
|
||||||
|
// Set in WordPress transient
|
||||||
|
return set_transient($full_key, $value, $expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete cached value
|
||||||
|
*/
|
||||||
|
public function delete($key, $group = 'sodino') {
|
||||||
|
$full_key = $this->buildKey($key, $group);
|
||||||
|
|
||||||
|
// Remove from memory cache
|
||||||
|
unset($this->memory_cache[$full_key]);
|
||||||
|
|
||||||
|
// Remove from WordPress transient
|
||||||
|
return delete_transient($full_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cache for a group
|
||||||
|
*/
|
||||||
|
public function clearGroup($group = 'sodino') {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Clear memory cache for group
|
||||||
|
foreach ($this->memory_cache as $key => $value) {
|
||||||
|
if (strpos($key, "sodino_{$group}_") === 0) {
|
||||||
|
unset($this->memory_cache[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear transients for group
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$wpdb->esc_like('_transient_sodino_' . $group . '_') . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$wpdb->esc_like('_transient_timeout_sodino_' . $group . '_') . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all Sodino cache
|
||||||
|
*/
|
||||||
|
public function clearAll() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Clear memory cache
|
||||||
|
$this->memory_cache = [];
|
||||||
|
|
||||||
|
// Clear all Sodino transients
|
||||||
|
$wpdb->query(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_sodino_%' OR option_name LIKE '_transient_timeout_sodino_%'"
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remember pattern - get from cache or execute callback
|
||||||
|
*/
|
||||||
|
public function remember($key, $callback, $expiration = 3600, $group = 'sodino') {
|
||||||
|
$value = $this->get($key, $group);
|
||||||
|
|
||||||
|
if ($value !== false) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = call_user_func($callback);
|
||||||
|
$this->set($key, $value, $expiration, $group);
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build cache key
|
||||||
|
*/
|
||||||
|
private function buildKey($key, $group) {
|
||||||
|
return "sodino_{$group}_{$key}";
|
||||||
|
}
|
||||||
|
}
|
||||||
125
app/Core/Settings.php
Normal file
125
app/Core/Settings.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings Manager
|
||||||
|
*/
|
||||||
|
class Settings {
|
||||||
|
private static $instance = null;
|
||||||
|
private $settings = null;
|
||||||
|
private $option_name = 'sodino_settings';
|
||||||
|
|
||||||
|
private $defaults = [
|
||||||
|
'plugin_enabled' => 1,
|
||||||
|
'pricing_enabled' => 1,
|
||||||
|
'upsell_enabled' => 1,
|
||||||
|
'banner_enabled' => 1,
|
||||||
|
'allow_multiple_rules' => 0,
|
||||||
|
'strategy' => 'priority',
|
||||||
|
'max_discount_percent' => 100,
|
||||||
|
'min_product_price' => 0,
|
||||||
|
'ab_testing_enabled' => 0,
|
||||||
|
'cart_pricing_enabled' => 1,
|
||||||
|
'scheduled_campaigns_enabled' => 1,
|
||||||
|
'cache_enabled' => 1,
|
||||||
|
'cache_duration' => 3600,
|
||||||
|
'debug_mode' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function getInstance() {
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all settings
|
||||||
|
*/
|
||||||
|
public function all() {
|
||||||
|
if ($this->settings === null) {
|
||||||
|
$this->settings = wp_parse_args(
|
||||||
|
get_option($this->option_name, []),
|
||||||
|
$this->defaults
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $this->settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single setting
|
||||||
|
*/
|
||||||
|
public function get($key, $default = null) {
|
||||||
|
$settings = $this->all();
|
||||||
|
return $settings[$key] ?? $default ?? $this->defaults[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set single setting
|
||||||
|
*/
|
||||||
|
public function set($key, $value) {
|
||||||
|
$settings = $this->all();
|
||||||
|
$settings[$key] = $value;
|
||||||
|
$this->settings = $settings;
|
||||||
|
return update_option($this->option_name, $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update multiple settings
|
||||||
|
*/
|
||||||
|
public function update(array $settings) {
|
||||||
|
$current = $this->all();
|
||||||
|
$this->settings = array_merge($current, $settings);
|
||||||
|
return update_option($this->option_name, $this->settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset to defaults
|
||||||
|
*/
|
||||||
|
public function reset() {
|
||||||
|
$this->settings = $this->defaults;
|
||||||
|
return update_option($this->option_name, $this->defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if plugin is enabled
|
||||||
|
*/
|
||||||
|
public function isEnabled() {
|
||||||
|
return (bool) $this->get('plugin_enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if pricing is enabled
|
||||||
|
*/
|
||||||
|
public function isPricingEnabled() {
|
||||||
|
return $this->isEnabled() && (bool) $this->get('pricing_enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if upsell is enabled
|
||||||
|
*/
|
||||||
|
public function isUpsellEnabled() {
|
||||||
|
return $this->isEnabled() && (bool) $this->get('upsell_enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if banner is enabled
|
||||||
|
*/
|
||||||
|
public function isBannerEnabled() {
|
||||||
|
return $this->isEnabled() && (bool) $this->get('banner_enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if cache is enabled
|
||||||
|
*/
|
||||||
|
public function isCacheEnabled() {
|
||||||
|
return (bool) $this->get('cache_enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if debug mode is enabled
|
||||||
|
*/
|
||||||
|
public function isDebugMode() {
|
||||||
|
return (bool) $this->get('debug_mode');
|
||||||
|
}
|
||||||
|
}
|
||||||
132
app/Core/Validator.php
Normal file
132
app/Core/Validator.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation Helper
|
||||||
|
*/
|
||||||
|
class Validator {
|
||||||
|
private $errors = [];
|
||||||
|
private $data = [];
|
||||||
|
|
||||||
|
public function __construct(array $data) {
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate required field
|
||||||
|
*/
|
||||||
|
public function required($field, $message = null) {
|
||||||
|
if (!isset($this->data[$field]) || empty($this->data[$field])) {
|
||||||
|
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s الزامی است.', 'sodino'), $field);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate numeric field
|
||||||
|
*/
|
||||||
|
public function numeric($field, $message = null) {
|
||||||
|
if (isset($this->data[$field]) && !is_numeric($this->data[$field])) {
|
||||||
|
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید عدد باشد.', 'sodino'), $field);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate min value
|
||||||
|
*/
|
||||||
|
public function min($field, $min, $message = null) {
|
||||||
|
if (isset($this->data[$field]) && $this->data[$field] < $min) {
|
||||||
|
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید حداقل %s باشد.', 'sodino'), $field, $min);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate max value
|
||||||
|
*/
|
||||||
|
public function max($field, $max, $message = null) {
|
||||||
|
if (isset($this->data[$field]) && $this->data[$field] > $max) {
|
||||||
|
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید حداکثر %s باشد.', 'sodino'), $field, $max);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate email
|
||||||
|
*/
|
||||||
|
public function email($field, $message = null) {
|
||||||
|
if (isset($this->data[$field]) && !filter_var($this->data[$field], FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید یک ایمیل معتبر باشد.', 'sodino'), $field);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate URL
|
||||||
|
*/
|
||||||
|
public function url($field, $message = null) {
|
||||||
|
if (isset($this->data[$field]) && !filter_var($this->data[$field], FILTER_VALIDATE_URL)) {
|
||||||
|
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید یک URL معتبر باشد.', 'sodino'), $field);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate in array
|
||||||
|
*/
|
||||||
|
public function in($field, array $values, $message = null) {
|
||||||
|
if (isset($this->data[$field]) && !in_array($this->data[$field], $values, true)) {
|
||||||
|
$this->errors[$field][] = $message ?? sprintf(__('مقدار فیلد %s نامعتبر است.', 'sodino'), $field);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom validation
|
||||||
|
*/
|
||||||
|
public function custom($field, callable $callback, $message = null) {
|
||||||
|
if (isset($this->data[$field]) && !call_user_func($callback, $this->data[$field])) {
|
||||||
|
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s نامعتبر است.', 'sodino'), $field);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if validation passed
|
||||||
|
*/
|
||||||
|
public function passes() {
|
||||||
|
return empty($this->errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if validation failed
|
||||||
|
*/
|
||||||
|
public function fails() {
|
||||||
|
return !$this->passes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all errors
|
||||||
|
*/
|
||||||
|
public function errors() {
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get first error
|
||||||
|
*/
|
||||||
|
public function firstError() {
|
||||||
|
foreach ($this->errors as $field => $messages) {
|
||||||
|
return $messages[0] ?? '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static factory method
|
||||||
|
*/
|
||||||
|
public static function make(array $data) {
|
||||||
|
return new self($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,14 +11,11 @@ class Rule {
|
|||||||
public $actions;
|
public $actions;
|
||||||
public $priority;
|
public $priority;
|
||||||
public $usage_limit;
|
public $usage_limit;
|
||||||
|
public $usage_count;
|
||||||
public $user_roles;
|
public $user_roles;
|
||||||
public $start_date;
|
public $start_date;
|
||||||
public $end_date;
|
public $end_date;
|
||||||
public $enabled;
|
public $enabled;
|
||||||
public $condition_type;
|
|
||||||
public $condition_value;
|
|
||||||
public $action_type;
|
|
||||||
public $action_value;
|
|
||||||
public $created_at;
|
public $created_at;
|
||||||
public $updated_at;
|
public $updated_at;
|
||||||
|
|
||||||
@@ -32,28 +29,13 @@ class Rule {
|
|||||||
$this->actions = $this->parseJsonField($data['actions'] ?? '[]');
|
$this->actions = $this->parseJsonField($data['actions'] ?? '[]');
|
||||||
$this->priority = isset($data['priority']) ? (int) $data['priority'] : 10;
|
$this->priority = isset($data['priority']) ? (int) $data['priority'] : 10;
|
||||||
$this->usage_limit = isset($data['usage_limit']) ? (int) $data['usage_limit'] : 0;
|
$this->usage_limit = isset($data['usage_limit']) ? (int) $data['usage_limit'] : 0;
|
||||||
|
$this->usage_count = isset($data['usage_count']) ? (int) $data['usage_count'] : 0;
|
||||||
$this->user_roles = $this->parseRolesField($data['user_roles'] ?? '');
|
$this->user_roles = $this->parseRolesField($data['user_roles'] ?? '');
|
||||||
$this->start_date = $data['start_date'] ?? null;
|
$this->start_date = $data['start_date'] ?? null;
|
||||||
$this->end_date = $data['end_date'] ?? null;
|
$this->end_date = $data['end_date'] ?? null;
|
||||||
$this->enabled = isset($data['enabled']) ? (int) $data['enabled'] : 1;
|
$this->enabled = isset($data['enabled']) ? (int) $data['enabled'] : 1;
|
||||||
$this->condition_type = $data['condition_type'] ?? '';
|
|
||||||
$this->condition_value = $data['condition_value'] ?? '';
|
|
||||||
$this->action_type = $data['action_type'] ?? '';
|
|
||||||
$this->action_value = $data['action_value'] ?? '';
|
|
||||||
$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;
|
||||||
|
|
||||||
if (empty($this->conditions) && !empty($this->condition_type)) {
|
|
||||||
$this->conditions = [
|
|
||||||
['type' => $this->condition_type, 'value' => $this->condition_value],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($this->actions) && !empty($this->action_type)) {
|
|
||||||
$this->actions = [
|
|
||||||
['type' => $this->action_type, 'value' => $this->action_value],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseJsonField($value) {
|
private function parseJsonField($value) {
|
||||||
@@ -88,16 +70,41 @@ class Rule {
|
|||||||
'actions' => wp_json_encode($this->actions),
|
'actions' => wp_json_encode($this->actions),
|
||||||
'priority' => $this->priority,
|
'priority' => $this->priority,
|
||||||
'usage_limit' => $this->usage_limit,
|
'usage_limit' => $this->usage_limit,
|
||||||
|
'usage_count' => $this->usage_count,
|
||||||
'user_roles' => is_array($this->user_roles) ? implode(',', $this->user_roles) : $this->user_roles,
|
'user_roles' => is_array($this->user_roles) ? implode(',', $this->user_roles) : $this->user_roles,
|
||||||
'start_date' => $this->start_date,
|
'start_date' => $this->start_date,
|
||||||
'end_date' => $this->end_date,
|
'end_date' => $this->end_date,
|
||||||
'enabled' => $this->enabled,
|
'enabled' => $this->enabled,
|
||||||
'condition_type' => $this->condition_type,
|
|
||||||
'condition_value' => $this->condition_value,
|
|
||||||
'action_type' => $this->action_type,
|
|
||||||
'action_value' => $this->action_value,
|
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'updated_at' => $this->updated_at,
|
'updated_at' => $this->updated_at,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if rule is active
|
||||||
|
*/
|
||||||
|
public function isActive() {
|
||||||
|
if (!$this->enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = current_time('mysql');
|
||||||
|
|
||||||
|
if (!empty($this->start_date) && $now < $this->start_date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->end_date) && $now > $this->end_date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if usage limit reached
|
||||||
|
*/
|
||||||
|
public function hasReachedLimit() {
|
||||||
|
return $this->usage_limit > 0 && $this->usage_count >= $this->usage_limit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,51 +2,80 @@
|
|||||||
namespace Sodino\Repositories;
|
namespace Sodino\Repositories;
|
||||||
|
|
||||||
use Sodino\Models\Rule;
|
use Sodino\Models\Rule;
|
||||||
|
use Sodino\Core\Cache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rule Repository
|
* Rule Repository
|
||||||
*/
|
*/
|
||||||
class RuleRepository {
|
class RuleRepository {
|
||||||
private $table_name;
|
private $table_name;
|
||||||
|
private $cache;
|
||||||
|
private $cache_group = 'rules';
|
||||||
|
private $cache_duration = 3600;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$this->table_name = $wpdb->prefix . 'sodino_rules';
|
$this->table_name = $wpdb->prefix . 'sodino_rules';
|
||||||
|
$this->cache = Cache::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all rules
|
* Get all rules
|
||||||
*/
|
*/
|
||||||
public function getAll() {
|
public function getAll() {
|
||||||
|
return $this->cache->remember('all_rules', function() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$results = $wpdb->get_results("SELECT * FROM {$this->table_name} ORDER BY priority DESC, id ASC", ARRAY_A);
|
$results = $wpdb->get_results(
|
||||||
|
"SELECT * FROM {$this->table_name} ORDER BY priority DESC, id ASC",
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
$rules = [];
|
$rules = [];
|
||||||
foreach ($results as $result) {
|
foreach ($results as $result) {
|
||||||
$rules[] = new Rule($result);
|
$rules[] = new Rule($result);
|
||||||
}
|
}
|
||||||
return $rules;
|
return $rules;
|
||||||
|
}, $this->cache_duration, $this->cache_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get rule by ID
|
* Get rule by ID
|
||||||
*/
|
*/
|
||||||
public function getById($id) {
|
public function getById($id) {
|
||||||
|
return $this->cache->remember("rule_{$id}", function() use ($id) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id), ARRAY_A);
|
$result = $wpdb->get_row(
|
||||||
|
$wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id),
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
return $result ? new Rule($result) : null;
|
return $result ? new Rule($result) : null;
|
||||||
|
}, $this->cache_duration, $this->cache_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get enabled rules
|
* Get enabled rules
|
||||||
*/
|
*/
|
||||||
public function getEnabled() {
|
public function getEnabled() {
|
||||||
|
return $this->cache->remember('enabled_rules', function() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$results = $wpdb->get_results("SELECT * FROM {$this->table_name} WHERE enabled = 1 ORDER BY priority DESC, id ASC", ARRAY_A);
|
$now = current_time('mysql');
|
||||||
|
$results = $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT * FROM {$this->table_name}
|
||||||
|
WHERE enabled = 1
|
||||||
|
AND (start_date IS NULL OR start_date <= %s)
|
||||||
|
AND (end_date IS NULL OR end_date >= %s)
|
||||||
|
ORDER BY priority DESC, id ASC",
|
||||||
|
$now,
|
||||||
|
$now
|
||||||
|
),
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
$rules = [];
|
$rules = [];
|
||||||
foreach ($results as $result) {
|
foreach ($results as $result) {
|
||||||
$rules[] = new Rule($result);
|
$rules[] = new Rule($result);
|
||||||
}
|
}
|
||||||
return $rules;
|
return $rules;
|
||||||
|
}, $this->cache_duration, $this->cache_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,11 +88,16 @@ class RuleRepository {
|
|||||||
|
|
||||||
if ($rule->id) {
|
if ($rule->id) {
|
||||||
$wpdb->update($this->table_name, $data, ['id' => $rule->id]);
|
$wpdb->update($this->table_name, $data, ['id' => $rule->id]);
|
||||||
return $rule->id;
|
$id = $rule->id;
|
||||||
} else {
|
} else {
|
||||||
$wpdb->insert($this->table_name, $data);
|
$wpdb->insert($this->table_name, $data);
|
||||||
return $wpdb->insert_id;
|
$id = $wpdb->insert_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
$this->clearCache();
|
||||||
|
|
||||||
|
return $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +105,31 @@ class RuleRepository {
|
|||||||
*/
|
*/
|
||||||
public function delete($id) {
|
public function delete($id) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
return $wpdb->delete($this->table_name, ['id' => $id]);
|
$result = $wpdb->delete($this->table_name, ['id' => $id]);
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
$this->clearCache();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment usage count
|
||||||
|
*/
|
||||||
|
public function incrementUsage($id) {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"UPDATE {$this->table_name} SET usage_count = usage_count + 1 WHERE id = %d",
|
||||||
|
$id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache
|
||||||
|
*/
|
||||||
|
private function clearCache() {
|
||||||
|
$this->cache->clearGroup($this->cache_group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,20 +3,24 @@ namespace Sodino\Services;
|
|||||||
|
|
||||||
use Sodino\Repositories\RuleRepository;
|
use Sodino\Repositories\RuleRepository;
|
||||||
use Sodino\Services\TrackingService;
|
use Sodino\Services\TrackingService;
|
||||||
|
use Sodino\Core\Settings;
|
||||||
|
use Sodino\Core\Cache;
|
||||||
|
|
||||||
class PricingService {
|
class PricingService {
|
||||||
private $ruleRepository;
|
private $ruleRepository;
|
||||||
private $trackingService;
|
private $trackingService;
|
||||||
private $rulesCache = null;
|
private $settings;
|
||||||
|
private $cache;
|
||||||
|
|
||||||
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
|
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
|
||||||
$this->ruleRepository = $ruleRepository;
|
$this->ruleRepository = $ruleRepository;
|
||||||
$this->trackingService = $trackingService;
|
$this->trackingService = $trackingService;
|
||||||
|
$this->settings = Settings::getInstance();
|
||||||
|
$this->cache = Cache::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyDynamicPricing($price, $product) {
|
public function applyDynamicPricing($price, $product) {
|
||||||
$settings = $this->getSettings();
|
if (!$this->settings->isPricingEnabled()) {
|
||||||
if (empty($settings['plugin_enabled']) || empty($settings['pricing_enabled'])) {
|
|
||||||
return $price;
|
return $price;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,43 +29,58 @@ class PricingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$price = $this->normalizePrice($price);
|
$price = $this->normalizePrice($price);
|
||||||
if (!$settings['cart_pricing_enabled'] && is_cart()) {
|
|
||||||
|
if (!$this->settings->get('cart_pricing_enabled') && is_cart()) {
|
||||||
return $price;
|
return $price;
|
||||||
}
|
}
|
||||||
|
|
||||||
$originalPrice = $price;
|
$originalPrice = $price;
|
||||||
$rules = $this->getEnabledRules();
|
$rules = $this->getApplicableRules($product);
|
||||||
$matchedRules = [];
|
|
||||||
|
|
||||||
foreach ($rules as $rule) {
|
if (empty($rules)) {
|
||||||
if ($this->ruleMatches($rule, $product)) {
|
|
||||||
$matchedRules[] = $rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($matchedRules)) {
|
|
||||||
return $price;
|
return $price;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$settings['allow_multiple_rules']) {
|
if (!$this->settings->get('allow_multiple_rules')) {
|
||||||
$chosenRule = $this->chooseRule($matchedRules, $price, $settings['strategy']);
|
$chosenRule = $this->chooseRule($rules, $price);
|
||||||
$matchedRules = $chosenRule ? [$chosenRule] : [];
|
$rules = $chosenRule ? [$chosenRule] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($matchedRules as $rule) {
|
foreach ($rules as $rule) {
|
||||||
$oldPrice = $price;
|
$oldPrice = $price;
|
||||||
$price = $this->applyActions($rule, $price);
|
$price = $this->applyRuleActions($rule, $price);
|
||||||
|
|
||||||
if ($price < $oldPrice) {
|
if ($price < $oldPrice) {
|
||||||
$this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $rule->id);
|
$this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $rule->id);
|
||||||
|
$this->ruleRepository->incrementUsage($rule->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$price = $this->enforceLimits($originalPrice, $price, $settings);
|
$price = $this->enforceLimits($originalPrice, $price);
|
||||||
|
|
||||||
return max(0, $price);
|
return max(0, $price);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function chooseRule(array $rules, $price, $strategy) {
|
private function getApplicableRules($product) {
|
||||||
|
$cache_key = 'applicable_rules_' . ($product ? $product->get_id() : 'all');
|
||||||
|
|
||||||
|
return $this->cache->remember($cache_key, function() use ($product) {
|
||||||
|
$rules = $this->ruleRepository->getEnabled();
|
||||||
|
$applicable = [];
|
||||||
|
|
||||||
|
foreach ($rules as $rule) {
|
||||||
|
if ($this->ruleMatches($rule, $product)) {
|
||||||
|
$applicable[] = $rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $applicable;
|
||||||
|
}, 300, 'pricing');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function chooseRule(array $rules, $price) {
|
||||||
|
$strategy = $this->settings->get('strategy', 'priority');
|
||||||
|
|
||||||
if ($strategy === 'highest_discount') {
|
if ($strategy === 'highest_discount') {
|
||||||
usort($rules, function ($a, $b) use ($price) {
|
usort($rules, function ($a, $b) use ($price) {
|
||||||
return $this->estimateRuleDiscount($b, $price) <=> $this->estimateRuleDiscount($a, $price);
|
return $this->estimateRuleDiscount($b, $price) <=> $this->estimateRuleDiscount($a, $price);
|
||||||
@@ -79,44 +98,19 @@ class PricingService {
|
|||||||
return $rules[0] ?? null;
|
return $rules[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSettings() {
|
|
||||||
$defaults = [
|
|
||||||
'plugin_enabled' => 1,
|
|
||||||
'pricing_enabled' => 1,
|
|
||||||
'upsell_enabled' => 1,
|
|
||||||
'allow_multiple_rules' => 0,
|
|
||||||
'strategy' => 'priority',
|
|
||||||
'max_discount_percent' => 100,
|
|
||||||
'min_product_price' => 0,
|
|
||||||
'ab_testing_enabled' => 0,
|
|
||||||
'cart_pricing_enabled' => 1,
|
|
||||||
'scheduled_campaigns_enabled' => 1,
|
|
||||||
];
|
|
||||||
|
|
||||||
return wp_parse_args(get_option('sodino_settings', []), $defaults);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getEnabledRules() {
|
|
||||||
if ($this->rulesCache === null) {
|
|
||||||
$this->rulesCache = $this->ruleRepository->getEnabled();
|
|
||||||
}
|
|
||||||
return $this->rulesCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizePrice($price) {
|
private function normalizePrice($price) {
|
||||||
if ($price === '' || $price === null) {
|
if ($price === '' || $price === null) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return floatval($price);
|
return floatval($price);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ruleMatches($rule, $product = null) {
|
private function ruleMatches($rule, $product = null) {
|
||||||
if (!$rule->enabled) {
|
if (!$rule->isActive()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($rule->usage_limit > 0 && $this->trackingService->getRuleUsageCount($rule->id) >= $rule->usage_limit) {
|
if ($rule->hasReachedLimit()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,10 +120,6 @@ class PricingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->isRuleActive($rule)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($rule->conditions)) {
|
if (empty($rule->conditions)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -143,20 +133,6 @@ class PricingService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isRuleActive($rule) {
|
|
||||||
$now = current_time('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
if (!empty($rule->start_date) && $now < $rule->start_date) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($rule->end_date) && $now > $rule->end_date) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function evaluateCondition($condition, $product = null) {
|
private function evaluateCondition($condition, $product = null) {
|
||||||
$type = $condition['type'] ?? '';
|
$type = $condition['type'] ?? '';
|
||||||
$value = $condition['value'] ?? null;
|
$value = $condition['value'] ?? null;
|
||||||
@@ -203,24 +179,21 @@ class PricingService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCartTotal() {
|
private function getCartTotal() {
|
||||||
if (!WC()->cart) {
|
if (!function_exists('WC') || !WC()->cart) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
return floatval(WC()->cart->get_subtotal());
|
||||||
return floatval(WC()->cart->get_cart_contents_total());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCartItemCount() {
|
private function getCartItemCount() {
|
||||||
if (!WC()->cart) {
|
if (!function_exists('WC') || !WC()->cart) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
return intval(WC()->cart->get_cart_contents_count());
|
||||||
return WC()->cart->get_cart_contents_count();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function productHasCategory($product, $categories) {
|
private function productHasCategory($product, $categories) {
|
||||||
@@ -229,7 +202,11 @@ class PricingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$product_cats = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'ids']);
|
$product_cats = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'ids']);
|
||||||
return (bool) array_intersect($product_cats, $categories);
|
if (is_wp_error($product_cats)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) array_intersect($product_cats, array_map('intval', $categories));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function productIsInIds($product, $ids) {
|
private function productIsInIds($product, $ids) {
|
||||||
@@ -237,14 +214,13 @@ class PricingService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return in_array($product->get_id(), $ids, true);
|
return in_array($product->get_id(), array_map('intval', $ids), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyActions($rule, $price) {
|
private function applyRuleActions($rule, $price) {
|
||||||
foreach ($rule->actions as $action) {
|
foreach ($rule->actions as $action) {
|
||||||
$price = $this->applyAction($action, $price);
|
$price = $this->applyAction($action, $price);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $price;
|
return $price;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +239,8 @@ class PricingService {
|
|||||||
return $price;
|
return $price;
|
||||||
}
|
}
|
||||||
return $price - $value;
|
return $price - $value;
|
||||||
|
case 'set_price':
|
||||||
|
return $value > 0 ? $value : $price;
|
||||||
case 'free_shipping':
|
case 'free_shipping':
|
||||||
return $price;
|
return $price;
|
||||||
default:
|
default:
|
||||||
@@ -270,14 +248,15 @@ class PricingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function enforceLimits($originalPrice, $price, array $settings) {
|
private function enforceLimits($originalPrice, $price) {
|
||||||
$minPrice = max(0, floatval($settings['min_product_price']));
|
$minPrice = max(0, floatval($this->settings->get('min_product_price', 0)));
|
||||||
$price = max($price, $minPrice);
|
$price = max($price, $minPrice);
|
||||||
|
|
||||||
$maxDiscountPercent = floatval($settings['max_discount_percent']);
|
$maxDiscountPercent = floatval($this->settings->get('max_discount_percent', 100));
|
||||||
if ($maxDiscountPercent > 0 && $maxDiscountPercent < 100) {
|
if ($maxDiscountPercent > 0 && $maxDiscountPercent < 100) {
|
||||||
$limit = $originalPrice * ($maxDiscountPercent / 100);
|
$maxDiscount = $originalPrice * ($maxDiscountPercent / 100);
|
||||||
$price = max($originalPrice - $limit, $price);
|
$minAllowedPrice = $originalPrice - $maxDiscount;
|
||||||
|
$price = max($minAllowedPrice, $price);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $price;
|
return $price;
|
||||||
|
|||||||
39
composer.json
Normal file
39
composer.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "sodino/wordpress-plugin",
|
||||||
|
"description": "افزونه هوشمند قیمتگذاری و بهینهسازی درآمد برای ووکامرس",
|
||||||
|
"type": "wordpress-plugin",
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Your Name",
|
||||||
|
"email": "your.email@example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.6"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Sodino\\": "app/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Sodino\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit",
|
||||||
|
"phpcs": "phpcs --standard=WordPress app/",
|
||||||
|
"phpcbf": "phpcbf --standard=WordPress app/"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"sort-packages": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,11 @@ if (!defined('ABSPATH')) {
|
|||||||
/**
|
/**
|
||||||
* Database migrations for Sodino plugin
|
* Database migrations for Sodino plugin
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function sodino_create_tables() {
|
function sodino_create_tables() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$charset_collate = $wpdb->get_charset_collate();
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
$current_version = get_option('sodino_db_version', '0');
|
||||||
|
|
||||||
// Rules table
|
// Rules table
|
||||||
$rules_table = $wpdb->prefix . 'sodino_rules';
|
$rules_table = $wpdb->prefix . 'sodino_rules';
|
||||||
@@ -22,23 +22,22 @@ function sodino_create_tables() {
|
|||||||
actions longtext NOT NULL,
|
actions longtext NOT NULL,
|
||||||
priority int(11) NOT NULL DEFAULT 10,
|
priority int(11) NOT NULL DEFAULT 10,
|
||||||
usage_limit int(11) NOT NULL DEFAULT 0,
|
usage_limit int(11) NOT NULL DEFAULT 0,
|
||||||
|
usage_count int(11) NOT NULL DEFAULT 0,
|
||||||
user_roles varchar(255) DEFAULT '',
|
user_roles varchar(255) DEFAULT '',
|
||||||
start_date datetime NULL,
|
start_date datetime NULL,
|
||||||
end_date datetime NULL,
|
end_date datetime NULL,
|
||||||
enabled tinyint(1) DEFAULT 1,
|
enabled tinyint(1) DEFAULT 1,
|
||||||
condition_type varchar(100) DEFAULT NULL,
|
|
||||||
condition_value varchar(255) DEFAULT NULL,
|
|
||||||
action_type varchar(100) DEFAULT NULL,
|
|
||||||
action_value varchar(255) DEFAULT NULL,
|
|
||||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id),
|
||||||
|
KEY enabled_priority (enabled, priority),
|
||||||
|
KEY start_end_dates (start_date, end_date)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
// Events table
|
// Events table
|
||||||
$events_table = $wpdb->prefix . 'sodino_events';
|
$events_table = $wpdb->prefix . 'sodino_events';
|
||||||
$events_sql = "CREATE TABLE $events_table (
|
$events_sql = "CREATE TABLE $events_table (
|
||||||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
event_type varchar(100) NOT NULL,
|
event_type varchar(100) NOT NULL,
|
||||||
product_id mediumint(9) DEFAULT NULL,
|
product_id mediumint(9) DEFAULT NULL,
|
||||||
variation_id mediumint(9) DEFAULT NULL,
|
variation_id mediumint(9) DEFAULT NULL,
|
||||||
@@ -49,7 +48,12 @@ function sodino_create_tables() {
|
|||||||
discount_value decimal(10,2) DEFAULT 0,
|
discount_value decimal(10,2) DEFAULT 0,
|
||||||
metadata longtext DEFAULT NULL,
|
metadata longtext DEFAULT NULL,
|
||||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id),
|
||||||
|
KEY event_type_created (event_type, created_at),
|
||||||
|
KEY product_id (product_id),
|
||||||
|
KEY rule_id (rule_id),
|
||||||
|
KEY session_id (session_id),
|
||||||
|
KEY created_at (created_at)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
// Upsell table
|
// Upsell table
|
||||||
@@ -64,9 +68,13 @@ function sodino_create_tables() {
|
|||||||
discount_value decimal(10,2) DEFAULT 0,
|
discount_value decimal(10,2) DEFAULT 0,
|
||||||
status tinyint(1) DEFAULT 1,
|
status tinyint(1) DEFAULT 1,
|
||||||
priority int(11) NOT NULL DEFAULT 10,
|
priority int(11) NOT NULL DEFAULT 10,
|
||||||
|
impressions bigint(20) NOT NULL DEFAULT 0,
|
||||||
|
conversions bigint(20) NOT NULL DEFAULT 0,
|
||||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id),
|
||||||
|
KEY status_priority (status, priority),
|
||||||
|
KEY trigger_type (trigger_type)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
// Banner table
|
// Banner table
|
||||||
@@ -88,7 +96,25 @@ function sodino_create_tables() {
|
|||||||
impressions bigint(20) NOT NULL DEFAULT 0,
|
impressions bigint(20) NOT NULL DEFAULT 0,
|
||||||
clicks bigint(20) NOT NULL DEFAULT 0,
|
clicks bigint(20) NOT NULL DEFAULT 0,
|
||||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY status_priority (status, priority),
|
||||||
|
KEY position (position),
|
||||||
|
KEY start_end_time (start_time, end_time)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
|
// Analytics cache table
|
||||||
|
$analytics_table = $wpdb->prefix . 'sodino_analytics_cache';
|
||||||
|
$analytics_sql = "CREATE TABLE $analytics_table (
|
||||||
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
cache_key varchar(255) NOT NULL,
|
||||||
|
cache_value longtext NOT NULL,
|
||||||
|
cache_group varchar(100) NOT NULL DEFAULT 'general',
|
||||||
|
expires_at datetime NOT NULL,
|
||||||
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY cache_key_group (cache_key, cache_group),
|
||||||
|
KEY expires_at (expires_at)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||||
@@ -96,7 +122,42 @@ function sodino_create_tables() {
|
|||||||
dbDelta($events_sql);
|
dbDelta($events_sql);
|
||||||
dbDelta($upsell_sql);
|
dbDelta($upsell_sql);
|
||||||
dbDelta($banner_sql);
|
dbDelta($banner_sql);
|
||||||
|
dbDelta($analytics_sql);
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
sodino_run_migrations($current_version);
|
||||||
|
|
||||||
// Add version option
|
// Add version option
|
||||||
update_option('sodino_db_version', '1.3');
|
update_option('sodino_db_version', '2.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run incremental migrations
|
||||||
|
*/
|
||||||
|
function sodino_run_migrations($from_version) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Migration from 1.x to 2.0
|
||||||
|
if (version_compare($from_version, '2.0', '<')) {
|
||||||
|
// Add usage_count column if not exists
|
||||||
|
$rules_table = $wpdb->prefix . 'sodino_rules';
|
||||||
|
$column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$rules_table} LIKE 'usage_count'");
|
||||||
|
|
||||||
|
if (empty($column_exists)) {
|
||||||
|
$wpdb->query("ALTER TABLE {$rules_table} ADD COLUMN usage_count int(11) NOT NULL DEFAULT 0 AFTER usage_limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deprecated columns
|
||||||
|
$deprecated_columns = ['condition_type', 'condition_value', 'action_type', 'action_value'];
|
||||||
|
foreach ($deprecated_columns as $col) {
|
||||||
|
$col_exists = $wpdb->get_results("SHOW COLUMNS FROM {$rules_table} LIKE '{$col}'");
|
||||||
|
if (!empty($col_exists)) {
|
||||||
|
$wpdb->query("ALTER TABLE {$rules_table} DROP COLUMN {$col}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add indexes for better performance
|
||||||
|
$wpdb->query("ALTER TABLE {$rules_table} ADD INDEX enabled_priority (enabled, priority)");
|
||||||
|
$wpdb->query("ALTER TABLE {$rules_table} ADD INDEX start_end_dates (start_date, end_date)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
=== Sodino ===
|
=== Sodino ===
|
||||||
Contributors: yourname
|
Contributors: Soheil khaledabadi
|
||||||
Tags: woocommerce, pricing, dynamic pricing, revenue optimization
|
Tags: woocommerce, pricing, dynamic pricing, revenue optimization
|
||||||
Requires at least: 5.0
|
Requires at least: 5.0
|
||||||
Tested up to: 6.0
|
Tested up to: 6.0
|
||||||
|
|||||||
62
sodino.php
62
sodino.php
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: Sodino (سودینو)
|
* Plugin Name: Sodino (سودینو)
|
||||||
* Plugin URI: https://example.com/sodino
|
* Plugin URI: https://example.com/sodino
|
||||||
* Description: افزونه هوشمند قیمتگذاری و بهینهسازی درآمد برای ووکامرس. قیمت محصولات را بر اساس رفتار کاربر و قوانین تعریفشده به صورت پویا تنظیم میکند.
|
* Description: افزونه هوشمند قیمتگذاری و بهینهسازی درآمد برای ووکامرس. قیمت محصولات را بر اساس رفتار کاربر و قوانین تعریفشده به صورت پویا تنظیم میکند.
|
||||||
* Version: 1.0.0
|
* Version: 2.0.0
|
||||||
* Author: Your Name
|
* Author: Your Name
|
||||||
* License: GPL v2 or later
|
* License: GPL v2 or later
|
||||||
* Text Domain: sodino
|
* Text Domain: sodino
|
||||||
@@ -20,7 +20,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define plugin constants
|
// Define plugin constants
|
||||||
define('SODINO_VERSION', '1.0.0');
|
define('SODINO_VERSION', '2.0.0');
|
||||||
define('SODINO_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('SODINO_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('SODINO_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('SODINO_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
define('SODINO_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
define('SODINO_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||||
@@ -60,6 +60,18 @@ function sodino_activate() {
|
|||||||
|
|
||||||
// Flush rewrite rules if needed
|
// Flush rewrite rules if needed
|
||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
|
|
||||||
|
// Set default settings
|
||||||
|
if (!get_option('sodino_settings')) {
|
||||||
|
update_option('sodino_settings', [
|
||||||
|
'plugin_enabled' => 1,
|
||||||
|
'pricing_enabled' => 1,
|
||||||
|
'upsell_enabled' => 1,
|
||||||
|
'banner_enabled' => 1,
|
||||||
|
'cache_enabled' => 1,
|
||||||
|
'cache_duration' => 3600,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deactivation hook
|
// Deactivation hook
|
||||||
@@ -70,6 +82,10 @@ function sodino_deactivate() {
|
|||||||
|
|
||||||
// Clear analytics cron
|
// Clear analytics cron
|
||||||
wp_clear_scheduled_hook('sodino_hourly_analytics');
|
wp_clear_scheduled_hook('sodino_hourly_analytics');
|
||||||
|
|
||||||
|
// Clear all cache
|
||||||
|
$cache = \Sodino\Core\Cache::getInstance();
|
||||||
|
$cache->clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bootstrap the plugin
|
// Bootstrap the plugin
|
||||||
@@ -80,25 +96,44 @@ function sodino_init() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load text domain
|
||||||
|
load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
|
||||||
|
|
||||||
// Initialize admin
|
// Initialize admin
|
||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
require_once SODINO_PLUGIN_DIR . 'admin/admin.php';
|
require_once SODINO_PLUGIN_DIR . 'admin/admin.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize public hooks
|
// Initialize public hooks
|
||||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
|
sodino_init_public_hooks();
|
||||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/analytics-hooks.php';
|
|
||||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/upsell-hooks.php';
|
|
||||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/banner-hooks.php';
|
|
||||||
|
|
||||||
// Schedule analytics aggregation if needed
|
// Schedule analytics aggregation if needed
|
||||||
sodino_schedule_analytics();
|
sodino_schedule_analytics();
|
||||||
|
|
||||||
// Load text domain
|
|
||||||
load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
|
|
||||||
}
|
}
|
||||||
add_action('plugins_loaded', 'sodino_init');
|
add_action('plugins_loaded', 'sodino_init');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize public hooks
|
||||||
|
*/
|
||||||
|
function sodino_init_public_hooks() {
|
||||||
|
$settings = \Sodino\Core\Settings::getInstance();
|
||||||
|
|
||||||
|
if ($settings->isPricingEnabled()) {
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($settings->isUpsellEnabled()) {
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/upsell-hooks.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($settings->isBannerEnabled()) {
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/banner-hooks.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always load analytics
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/analytics-hooks.php';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule analytics cron job
|
* Schedule analytics cron job
|
||||||
*/
|
*/
|
||||||
@@ -131,3 +166,12 @@ function sodino_woocommerce_missing_notice() {
|
|||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add settings link on plugin page
|
||||||
|
*/
|
||||||
|
add_filter('plugin_action_links_' . SODINO_PLUGIN_BASENAME, function($links) {
|
||||||
|
$settings_link = '<a href="' . admin_url('admin.php?page=sodino-settings') . '">' . __('تنظیمات', 'sodino') . '</a>';
|
||||||
|
array_unshift($links, $settings_link);
|
||||||
|
return $links;
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user