feat: Implement upsell functionality with repository and service layers
This commit is contained in:
@@ -6,23 +6,49 @@ if (!defined('ABSPATH')) {
|
|||||||
|
|
||||||
use Sodino\Controllers\AdminController;
|
use Sodino\Controllers\AdminController;
|
||||||
use Sodino\Repositories\RuleRepository;
|
use Sodino\Repositories\RuleRepository;
|
||||||
|
use Sodino\Repositories\UpsellRepository;
|
||||||
|
|
||||||
// Initialize admin
|
// Initialize admin
|
||||||
$ruleRepository = new RuleRepository();
|
$ruleRepository = new RuleRepository();
|
||||||
$adminController = new AdminController($ruleRepository);
|
$upsellRepository = new UpsellRepository();
|
||||||
|
$adminController = new AdminController($ruleRepository, $upsellRepository);
|
||||||
|
|
||||||
// Add menu
|
// Add menu
|
||||||
add_action('admin_menu', [$adminController, 'addMenu']);
|
add_action('admin_menu', [$adminController, 'addMenu']);
|
||||||
|
|
||||||
|
// Admin AJAX handlers
|
||||||
|
add_action('wp_ajax_sodino_search_products', [$adminController, 'searchProductsAjax']);
|
||||||
|
|
||||||
// Enqueue admin assets
|
// Enqueue admin assets
|
||||||
add_action('admin_enqueue_scripts', function($hook) {
|
add_action('admin_enqueue_scripts', function($hook) use ($adminController) {
|
||||||
if (strpos($hook, 'sodino') === false) {
|
if (strpos($hook, 'sodino') === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enqueue Tailwind via CDN script
|
||||||
|
wp_enqueue_script('sodino-tailwind', 'https://cdn.tailwindcss.com', [], null);
|
||||||
|
|
||||||
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($hook, 'sodino_page_sodino-add-upsell') !== false) {
|
||||||
|
wp_enqueue_script('sodino-upsell-admin', plugin_dir_url(__FILE__) . 'js/upsell-admin.js', [], SODINO_VERSION, true);
|
||||||
|
wp_localize_script('sodino-upsell-admin', 'sodinoUpsellAdmin', [
|
||||||
|
'nonce' => wp_create_nonce('sodino_search_products'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle delete for any Sodino admin page
|
// Handle delete for any Sodino admin page
|
||||||
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && $_GET['action'] === 'delete') {
|
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && $_GET['action'] === 'delete') {
|
||||||
add_action('admin_init', [$adminController, 'handleDelete']);
|
add_action('admin_init', [$adminController, 'handleDelete']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
|
add_action('admin_init', [$adminController, 'handleUpsellActions']);
|
||||||
|
}
|
||||||
138
admin/class-upsell-list-table.php
Normal file
138
admin/class-upsell-list-table.php
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists('WP_List_Table')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sodino_Upsell_List_Table extends WP_List_Table {
|
||||||
|
private $repository;
|
||||||
|
private $items_per_page = 20;
|
||||||
|
|
||||||
|
public function __construct($repository) {
|
||||||
|
parent::__construct([
|
||||||
|
'singular' => 'sodino_upsell',
|
||||||
|
'plural' => 'sodino_upsells',
|
||||||
|
'ajax' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->repository = $repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_columns() {
|
||||||
|
return [
|
||||||
|
'cb' => '<input type="checkbox" />',
|
||||||
|
'title' => __('عنوان', 'sodino'),
|
||||||
|
'trigger' => __('شرط فعالسازی', 'sodino'),
|
||||||
|
'suggested_product'=> __('محصول پیشنهادی', 'sodino'),
|
||||||
|
'discount' => __('تخفیف', 'sodino'),
|
||||||
|
'status' => __('وضعیت', 'sodino'),
|
||||||
|
'actions' => __('عملیات', 'sodino'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_sortable_columns() {
|
||||||
|
return [
|
||||||
|
'title' => ['title', true],
|
||||||
|
'priority' => ['priority', true],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function column_cb($item) {
|
||||||
|
return sprintf('<input type="checkbox" name="upsell_ids[]" value="%d" />', $item->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_bulk_actions() {
|
||||||
|
return [
|
||||||
|
'delete' => __('حذف گروهی', 'sodino'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_title($item) {
|
||||||
|
$edit_url = admin_url('admin.php?page=sodino-add-upsell&action=edit&id=' . $item->id);
|
||||||
|
$title = sprintf('<strong><a href="%s">%s</a></strong>', esc_url($edit_url), esc_html($item->title));
|
||||||
|
return $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_trigger($item) {
|
||||||
|
switch ($item->trigger_type) {
|
||||||
|
case 'product':
|
||||||
|
$product = wc_get_product(intval($item->trigger_value));
|
||||||
|
return $product ? esc_html($product->get_name()) : __('محصول خاص', 'sodino');
|
||||||
|
case 'category':
|
||||||
|
$term = get_term(intval($item->trigger_value));
|
||||||
|
return $term && !is_wp_error($term) ? esc_html($term->name) : __('دستهبندی', 'sodino');
|
||||||
|
case 'cart_total':
|
||||||
|
return sprintf('%s %s', esc_html(number_format_i18n(floatval($item->trigger_value))), __('تومان', 'sodino'));
|
||||||
|
default:
|
||||||
|
return __('نامشخص', 'sodino');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_suggested_product($item) {
|
||||||
|
$product = wc_get_product($item->target_product_id);
|
||||||
|
return $product ? esc_html($product->get_name()) : __('نامشخص', 'sodino');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_discount($item) {
|
||||||
|
if ($item->discount_type === 'fixed') {
|
||||||
|
return sprintf('%s %s', esc_html(number_format_i18n($item->discount_value)), __('تومان', 'sodino'));
|
||||||
|
}
|
||||||
|
if ($item->discount_type === 'percentage') {
|
||||||
|
return sprintf('%s %%', esc_html($item->discount_value));
|
||||||
|
}
|
||||||
|
return __('بدون تخفیف', 'sodino');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_status($item) {
|
||||||
|
return $item->status ? __('فعال', 'sodino') : __('غیرفعال', 'sodino');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_actions($item) {
|
||||||
|
$edit_url = admin_url('admin.php?page=sodino-add-upsell&action=edit&id=' . $item->id);
|
||||||
|
$toggle_url = wp_nonce_url(admin_url('admin.php?page=sodino-upsells&action=toggle_upsell_status&id=' . $item->id), 'toggle_upsell_status');
|
||||||
|
$delete_url = wp_nonce_url(admin_url('admin.php?page=sodino-upsells&action=delete_upsell&id=' . $item->id), 'delete_upsell');
|
||||||
|
|
||||||
|
$toggle_label = $item->status ? __('غیرفعال کردن', 'sodino') : __('فعال کردن', 'sodino');
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'<a href="%s">%s</a> | <a href="%s">%s</a> | <a href="%s" onclick="return confirm(\'%s\');">%s</a>',
|
||||||
|
esc_url($edit_url),
|
||||||
|
esc_html__('ویرایش', 'sodino'),
|
||||||
|
esc_url($toggle_url),
|
||||||
|
esc_html($toggle_label),
|
||||||
|
esc_url($delete_url),
|
||||||
|
esc_js(__('آیا از حذف این پیشنهاد آپسل مطمئن هستید؟', 'sodino')),
|
||||||
|
esc_html__('حذف', 'sodino')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepare_items() {
|
||||||
|
$this->_column_headers = [$this->get_columns(), [], $this->get_sortable_columns()];
|
||||||
|
$this->process_bulk_action();
|
||||||
|
|
||||||
|
$all_items = $this->repository->getAll();
|
||||||
|
$current_page = $this->get_pagenum();
|
||||||
|
$total_items = count($all_items);
|
||||||
|
$this->items = array_slice($all_items, ($current_page - 1) * $this->items_per_page, $this->items_per_page);
|
||||||
|
|
||||||
|
$this->set_pagination_args([
|
||||||
|
'total_items' => $total_items,
|
||||||
|
'per_page' => $this->items_per_page,
|
||||||
|
'total_pages' => ceil($total_items / $this->items_per_page),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process_bulk_action() {
|
||||||
|
if ('delete' === $this->current_action()) {
|
||||||
|
$upsell_ids = isset($_POST['upsell_ids']) ? array_map('intval', $_POST['upsell_ids']) : [];
|
||||||
|
if (!empty($upsell_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
|
||||||
|
foreach ($upsell_ids as $id) {
|
||||||
|
$this->repository->delete($id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,296 @@
|
|||||||
.wrap {
|
#sodino-app {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sodino-admin-table th,
|
#sodino-app * {
|
||||||
.sodino-admin-table td {
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-app {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-header {
|
||||||
|
background: #eef2ff;
|
||||||
|
border: 1px solid #c7d2fe;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-header-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-header-subtitle {
|
||||||
|
color: #475569;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-page-layout {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
#sodino-app .sd-page-layout {
|
||||||
|
grid-template-columns: 260px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-sidebar {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 18px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-sidebar-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-nav {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-nav-link {
|
||||||
|
display: block;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title-action {
|
#sodino-app .sd-nav-link:hover,
|
||||||
float: left;
|
#sodino-app .sd-nav-link.active {
|
||||||
|
background: #e0e7ff;
|
||||||
|
border-color: #c7d2fe;
|
||||||
|
color: #1e3a8a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rtl .page-title-action {
|
#sodino-app .sd-nav-link.disabled {
|
||||||
float: left;
|
color: #94a3b8;
|
||||||
|
pointer-events: none;
|
||||||
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.regular-text,
|
#sodino-app .sd-main {
|
||||||
select.regular-text,
|
display: grid;
|
||||||
input.small-text {
|
gap: 24px;
|
||||||
direction: rtl;
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-stat-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
#sodino-app .sd-stat-grid {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #334155;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-card-amount {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.85rem;
|
||||||
|
padding: 0.75rem 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-btn-primary {
|
||||||
|
background: #4338ca;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-btn-primary:hover {
|
||||||
|
background: #3730a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-btn-secondary {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #0f172a;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-btn-secondary:hover {
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-btn-danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-form-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-label {
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: right;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-input,
|
||||||
|
#sodino-app .sd-select,
|
||||||
|
#sodino-app .sd-textarea {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #0f172a;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-input:focus,
|
||||||
|
#sodino-app .sd-select:focus,
|
||||||
|
#sodino-app .sd-textarea:focus {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-input[readonly],
|
||||||
|
#sodino-app .sd-select[disabled] {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-helper {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app table.sd-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
min-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app table.sd-table th,
|
||||||
|
#sodino-app table.sd-table td {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
text-align: right;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app table.sd-table thead th {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app table.sd-table tbody tr:nth-child(odd) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app table.sd-table tbody tr:hover {
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.35rem 0.8rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-badge--success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-badge--danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-alert {
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-alert-success {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #064e3b;
|
||||||
|
border: 1px solid #d1fae5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-alert-error {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #7f1d1d;
|
||||||
|
border: 1px solid #fecdd3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-grid-3 {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
#sodino-app .sd-grid-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#sodino-app .sd-chart-card {
|
||||||
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|||||||
175
admin/js/dashboard.js
Normal file
175
admin/js/dashboard.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const dataEl = document.getElementById('sodino-dashboard-data');
|
||||||
|
if (!dataEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dashboardData;
|
||||||
|
try {
|
||||||
|
dashboardData = JSON.parse(dataEl.textContent || '{}');
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Chart === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const salesChartContext = document.getElementById('sodinoSalesChart');
|
||||||
|
const discountChartContext = document.getElementById('sodinoDiscountChart');
|
||||||
|
const ruleChartContext = document.getElementById('sodinoRuleChart');
|
||||||
|
|
||||||
|
if (salesChartContext && dashboardData.salesChart) {
|
||||||
|
new Chart(salesChartContext.getContext('2d'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: dashboardData.salesChart.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: dashboardData.translations.afterApplying,
|
||||||
|
data: dashboardData.salesChart.after,
|
||||||
|
borderColor: '#0ea5e9',
|
||||||
|
backgroundColor: 'rgba(14, 165, 233, 0.15)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.35,
|
||||||
|
pointRadius: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: dashboardData.translations.beforeApplying,
|
||||||
|
data: dashboardData.salesChart.before,
|
||||||
|
borderColor: '#ef4444',
|
||||||
|
backgroundColor: 'rgba(239, 68, 68, 0.15)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.35,
|
||||||
|
pointRadius: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: '#334155',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: '#475569',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(148, 163, 184, 0.15)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: '#475569',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(148, 163, 184, 0.15)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discountChartContext && dashboardData.summary) {
|
||||||
|
new Chart(discountChartContext.getContext('2d'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [dashboardData.translations.totalDiscount, dashboardData.translations.totalRevenue],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: dashboardData.translations.discountEffect,
|
||||||
|
data: [dashboardData.summary.total_discount, dashboardData.summary.total_revenue],
|
||||||
|
backgroundColor: ['#fbbf24', '#0ea5e9'],
|
||||||
|
borderRadius: 999,
|
||||||
|
borderSkipped: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: '#475569',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: '#475569',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(148, 163, 184, 0.15)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ruleChartContext && dashboardData.rulePerformance) {
|
||||||
|
new Chart(ruleChartContext.getContext('2d'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: dashboardData.rulePerformance.names,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: dashboardData.translations.ruleRevenue,
|
||||||
|
data: dashboardData.rulePerformance.revenue,
|
||||||
|
backgroundColor: '#0ea5e9',
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: dashboardData.translations.ruleDiscount,
|
||||||
|
data: dashboardData.rulePerformance.discount,
|
||||||
|
backgroundColor: '#f59e0b',
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: {
|
||||||
|
color: '#475569',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: '#475569',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(148, 163, 184, 0.15)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: '#475569',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
106
admin/js/upsell-admin.js
Normal file
106
admin/js/upsell-admin.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
(function () {
|
||||||
|
const triggerTypeInputs = document.querySelectorAll('.trigger-type');
|
||||||
|
const triggerProductWrapper = document.getElementById('trigger-product-wrapper');
|
||||||
|
const triggerCategoryWrapper = document.getElementById('trigger-category-wrapper');
|
||||||
|
const triggerAmountWrapper = document.getElementById('trigger-amount-wrapper');
|
||||||
|
const triggerValueInput = document.getElementById('trigger_value');
|
||||||
|
const triggerProductSearch = document.getElementById('trigger_product_search');
|
||||||
|
const triggerProductResults = document.getElementById('trigger_product_results');
|
||||||
|
|
||||||
|
const targetProductSearch = document.getElementById('target_product_search');
|
||||||
|
const targetProductResults = document.getElementById('target_product_results');
|
||||||
|
const targetProductIdInput = document.getElementById('target_product_id');
|
||||||
|
|
||||||
|
function setTriggerVisibility() {
|
||||||
|
const selected = document.querySelector('input[name="trigger_type"]:checked');
|
||||||
|
const value = selected ? selected.value : 'product';
|
||||||
|
|
||||||
|
triggerProductWrapper.classList.toggle('hidden', value !== 'product');
|
||||||
|
triggerCategoryWrapper.classList.toggle('hidden', value !== 'category');
|
||||||
|
triggerAmountWrapper.classList.toggle('hidden', value !== 'cart_total');
|
||||||
|
|
||||||
|
if (value !== 'product' && triggerProductSearch) {
|
||||||
|
triggerValueInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchProducts(inputElement, resultsContainer, targetInput) {
|
||||||
|
const term = inputElement.value.trim();
|
||||||
|
if (!term) {
|
||||||
|
resultsContainer.classList.add('hidden');
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
formData.append('action', 'sodino_search_products');
|
||||||
|
formData.append('security', sodinoUpsellAdmin.nonce);
|
||||||
|
formData.append('term', term);
|
||||||
|
|
||||||
|
fetch(ajaxurl, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
resultsContainer.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = data.map(product => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = 'w-full text-right px-4 py-3 hover:bg-blue-50 focus:bg-blue-50';
|
||||||
|
button.textContent = product.label;
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
if (targetInput) {
|
||||||
|
targetInput.value = product.id;
|
||||||
|
inputElement.value = product.label;
|
||||||
|
} else {
|
||||||
|
triggerValueInput.value = product.id;
|
||||||
|
inputElement.value = product.label;
|
||||||
|
}
|
||||||
|
resultsContainer.classList.add('hidden');
|
||||||
|
});
|
||||||
|
return button;
|
||||||
|
});
|
||||||
|
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
items.forEach(item => resultsContainer.appendChild(item));
|
||||||
|
resultsContainer.classList.toggle('hidden', items.length === 0);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
resultsContainer.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggerTypeInputs.length) {
|
||||||
|
triggerTypeInputs.forEach(input => {
|
||||||
|
input.addEventListener('change', setTriggerVisibility);
|
||||||
|
});
|
||||||
|
setTriggerVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggerProductSearch) {
|
||||||
|
triggerProductSearch.addEventListener('input', function () {
|
||||||
|
searchProducts(triggerProductSearch, triggerProductResults, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetProductSearch) {
|
||||||
|
targetProductSearch.addEventListener('input', function () {
|
||||||
|
searchProducts(targetProductSearch, targetProductResults, targetProductIdInput);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
if (triggerProductResults && !triggerProductResults.contains(event.target) && event.target !== triggerProductSearch) {
|
||||||
|
triggerProductResults.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (targetProductResults && !targetProductResults.contains(event.target) && event.target !== targetProductSearch) {
|
||||||
|
targetProductResults.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
145
admin/views/competitor-price.php
Normal file
145
admin/views/competitor-price.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-competitor-price');
|
||||||
|
?>
|
||||||
|
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
|
||||||
|
<div class="bg-white border-b border-gray-200">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900"><?php _e('سودینو', 'sodino'); ?></h1>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('بهینهسازی هوشمند فروش', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<aside class="w-64 flex-shrink-0">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('منوی سودینو', 'sodino'); ?></h2>
|
||||||
|
<nav class="space-y-2">
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-dashboard'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-dashboard' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('داشبورد', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-rules'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-rules' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('قوانین', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-add-rule'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-rule' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('افزودن قانون', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-upsells'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-upsells' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('آپسل (پیشنهاد فروش)', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-add-upsell'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-upsell' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('افزودن آپسل', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('قیمت رقبا (بهزودی)', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('تنظیمات', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="flex-1 min-w-0">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||||
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('قیمت رقبا', 'sodino'); ?></h2>
|
||||||
|
<p class="mt-2 text-gray-600"><?php _e('بهزودی بخش مانیتورینگ قیمت رقبا فعال خواهد شد.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center rounded-full bg-yellow-50 px-3 py-1 text-sm font-medium text-yellow-700"><?php _e('بهزودی', 'sodino'); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-6 lg:grid-cols-3">
|
||||||
|
<div class="lg:col-span-2 bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('مشخصات رقیب', 'sodino'); ?></h3>
|
||||||
|
<p class="text-sm text-gray-500 mb-6"><?php _e('این قابلیت به شما امکان میدهد قیمت محصولات خود را با رقبا مقایسه کرده و بهینهسازی کنید. (در نسخههای آینده فعال خواهد شد)', 'sodino'); ?></p>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl border border-gray-200">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 text-right text-sm text-gray-700">
|
||||||
|
<thead class="bg-gray-50 text-gray-500">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 font-medium"><?php _e('محصول', 'sodino'); ?></th>
|
||||||
|
<th class="px-4 py-3 font-medium"><?php _e('قیمت شما', 'sodino'); ?></th>
|
||||||
|
<th class="px-4 py-3 font-medium"><?php _e('قیمت رقبا', 'sodino'); ?></th>
|
||||||
|
<th class="px-4 py-3 font-medium"><?php _e('اختلاف قیمت', 'sodino'); ?></th>
|
||||||
|
<th class="px-4 py-3 font-medium"><?php _e('وضعیت', 'sodino'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 bg-white">
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3"><?php _e('کفش دویدن', 'sodino'); ?></td>
|
||||||
|
<td class="px-4 py-3">23,500 تومان</td>
|
||||||
|
<td class="px-4 py-3">24,900 تومان</td>
|
||||||
|
<td class="px-4 py-3 text-green-600">1,400 تومان کمتر</td>
|
||||||
|
<td class="px-4 py-3"><span class="rounded-full bg-green-50 px-3 py-1 text-xs font-semibold text-green-700"><?php _e('رقبتی', 'sodino'); ?></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3"><?php _e('کرم محافظ پوست', 'sodino'); ?></td>
|
||||||
|
<td class="px-4 py-3">78,000 تومان</td>
|
||||||
|
<td class="px-4 py-3">82,000 تومان</td>
|
||||||
|
<td class="px-4 py-3 text-green-600">4,000 تومان کمتر</td>
|
||||||
|
<td class="px-4 py-3"><span class="rounded-full bg-green-50 px-3 py-1 text-xs font-semibold text-green-700"><?php _e('رقبتی', 'sodino'); ?></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3"><?php _e('کیف دستی', 'sodino'); ?></td>
|
||||||
|
<td class="px-4 py-3">155,000 تومان</td>
|
||||||
|
<td class="px-4 py-3">161,000 تومان</td>
|
||||||
|
<td class="px-4 py-3 text-red-600">6,000 تومان بیشتر</td>
|
||||||
|
<td class="px-4 py-3"><span class="rounded-full bg-gray-50 px-3 py-1 text-xs font-semibold text-gray-700"><?php _e('هشدار', 'sodino'); ?></span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900"><?php _e('افزودن رقیب', 'sodino'); ?></h3>
|
||||||
|
<span class="rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700"><?php _e('بهزودی', 'sodino'); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2"><?php _e('URL سایت رقیب', 'sodino'); ?></label>
|
||||||
|
<input type="url" disabled class="w-full rounded-lg border border-gray-200 bg-gray-100 px-4 py-3 text-gray-500 shadow-sm" placeholder="https://example.com">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2"><?php _e('نام فروشگاه', 'sodino'); ?></label>
|
||||||
|
<input type="text" disabled class="w-full rounded-lg border border-gray-200 bg-gray-100 px-4 py-3 text-gray-500 shadow-sm" placeholder="<?php _e('مثلاً اسم فروشگاه رقبا', 'sodino'); ?>">
|
||||||
|
</div>
|
||||||
|
<button disabled class="w-full rounded-full bg-gray-300 px-4 py-3 text-sm font-semibold text-gray-700"><?php _e('افزودن', 'sodino'); ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-2xl border border-gray-200 shadow-sm p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900"><?php _e('فعالسازی مانیتورینگ قیمت', 'sodino'); ?></h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('این ویژگی در نسخههای آینده فعال میشود.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="px-3 py-1 rounded-full bg-gray-100 text-xs font-semibold text-gray-600"><?php _e('غیرفعال', 'sodino'); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-gray-200 bg-gray-50 p-5 text-sm text-gray-500">
|
||||||
|
<?php _e('در اینجا میتوانید فعالسازی خودکار تحلیل قیمت رقبا را مشاهده کنید؛ در حال حاضر این بخش در دست ساخت است.', 'sodino'); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
170
admin/views/dashboard.php
Normal file
170
admin/views/dashboard.php
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = $dashboardData['summary'];
|
||||||
|
$salesChart = $dashboardData['sales_chart'];
|
||||||
|
$rulePerformance = $dashboardData['rule_performance'];
|
||||||
|
$userBehavior = $dashboardData['user_behavior'];
|
||||||
|
$insights = $dashboardData['insights'];
|
||||||
|
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-dashboard');
|
||||||
|
?>
|
||||||
|
<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">
|
||||||
|
<!-- Overview Card -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-2"><?php _e('نمای کلی داشبورد', 'sodino'); ?></h2>
|
||||||
|
<p class="text-gray-600"><?php _e('نمایش سریع KPIها، عملکرد قوانین و رفتار مشتریان.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div class="bg-gradient-to-br from-blue-600 to-blue-700 rounded-lg p-6 text-white">
|
||||||
|
<h3 class="text-sm font-medium opacity-90"><?php _e('درآمد کل', 'sodino'); ?></h3>
|
||||||
|
<div class="text-2xl font-bold mt-2"><?php echo wc_price($summary['total_revenue']); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-600"><?php _e('میزان تخفیف داده شده', 'sodino'); ?></h3>
|
||||||
|
<div class="text-2xl font-bold text-gray-900 mt-2"><?php echo wc_price($summary['total_discount']); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-600"><?php _e('نرخ تبدیل تقریبی', 'sodino'); ?></h3>
|
||||||
|
<div class="text-2xl font-bold text-gray-900 mt-2"><?php echo esc_html($summary['conversion_rate']); ?>%</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-600"><?php _e('بهترین قانون', 'sodino'); ?></h3>
|
||||||
|
<div class="text-2xl font-bold text-gray-900 mt-2"><?php echo esc_html($summary['best_rule'] ?? __('بدون داده', 'sodino')); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Grid -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('نمودار فروش (قبل و بعد)', 'sodino'); ?></h3>
|
||||||
|
<div class="h-64">
|
||||||
|
<canvas id="sodinoSalesChart" class="w-full h-full"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('تاثیر تخفیف', 'sodino'); ?></h3>
|
||||||
|
<div class="h-64">
|
||||||
|
<canvas id="sodinoDiscountChart" class="w-full h-full"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('عملکرد قوانین', 'sodino'); ?></h3>
|
||||||
|
<div class="h-64">
|
||||||
|
<canvas id="sodinoRuleChart" class="w-full h-full"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Behavior and Insights -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('رفتار کاربران', 'sodino'); ?></h3>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div class="text-sm font-medium text-gray-600"><?php _e('بازدید محصول', 'sodino'); ?></div>
|
||||||
|
<div class="text-2xl font-bold text-gray-900 mt-1"><?php echo esc_html($userBehavior['product_views']); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div class="text-sm font-medium text-gray-600"><?php _e('افزودن به سبد', 'sodino'); ?></div>
|
||||||
|
<div class="text-2xl font-bold text-gray-900 mt-1"><?php echo esc_html($userBehavior['add_to_cart']); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div class="text-sm font-medium text-gray-600"><?php _e('شروع پرداخت', 'sodino'); ?></div>
|
||||||
|
<div class="text-2xl font-bold text-gray-900 mt-1"><?php echo esc_html($userBehavior['checkout_start']); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div class="text-sm font-medium text-gray-600"><?php _e('خرید', 'sodino'); ?></div>
|
||||||
|
<div class="text-2xl font-bold text-gray-900 mt-1"><?php echo esc_html($userBehavior['purchases']); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('بینشها', 'sodino'); ?></h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<?php foreach ($insights as $insight) : ?>
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div class="text-sm text-gray-600"><?php echo esc_html($insight); ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script id="sodino-dashboard-data" type="application/json">
|
||||||
|
<?php echo wp_json_encode([
|
||||||
|
'salesChart' => $salesChart,
|
||||||
|
'summary' => $summary,
|
||||||
|
'rulePerformance' => [
|
||||||
|
'names' => array_column($rulePerformance, 'name'),
|
||||||
|
'revenue' => array_column($rulePerformance, 'revenue'),
|
||||||
|
'discount' => array_column($rulePerformance, 'discount'),
|
||||||
|
],
|
||||||
|
'translations' => [
|
||||||
|
'afterApplying' => __('پس از اعمال سودینو', 'sodino'),
|
||||||
|
'beforeApplying' => __('قبل از اعمال سودینو', 'sodino'),
|
||||||
|
'totalDiscount' => __('مجموع تخفیف', 'sodino'),
|
||||||
|
'totalRevenue' => __('درآمد پس از تخفیف', 'sodino'),
|
||||||
|
'discountEffect' => __('تاثیر تخفیف', 'sodino'),
|
||||||
|
'ruleRevenue' => __('درآمد ایجاد شده', 'sodino'),
|
||||||
|
'ruleDiscount' => __('تخفیف ثبت شده', 'sodino'),
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE); ?>
|
||||||
|
</script>
|
||||||
@@ -3,52 +3,165 @@
|
|||||||
if (!defined('ABSPATH')) {
|
if (!defined('ABSPATH')) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
<div class="wrap">
|
|
||||||
<h1><?php echo $rule->id ? __('ویرایش قانون', 'sodino') : __('افزودن قانون جدید', 'sodino'); ?></h1>
|
|
||||||
|
|
||||||
|
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-add-rule');
|
||||||
|
?>
|
||||||
|
<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 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900"><?php echo $rule->id ? __('ویرایش قانون', 'sodino') : __('افزودن قانون جدید', 'sodino'); ?></h2>
|
||||||
|
<p class="mt-2 text-gray-600"><?php _e('قانون قیمتگذاری پویا ایجاد یا ویرایش کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-rules'); ?>" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
|
||||||
|
<svg class="-ml-1 mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<?php _e('بازگشت به قوانین', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<?php wp_nonce_field('gheymatyar_save_rule', 'gheymatyar_rule_nonce'); ?>
|
<?php wp_nonce_field('gheymatyar_save_rule', 'gheymatyar_rule_nonce'); ?>
|
||||||
<table class="form-table">
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><label for="name"><?php _e('عنوان قانون', 'sodino'); ?></label></th>
|
|
||||||
<td><input type="text" name="name" id="name" value="<?php echo esc_attr($rule->name); ?>" class="regular-text" required></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><label for="condition_type"><?php _e('نوع شرط', 'sodino'); ?></label></th>
|
|
||||||
<td>
|
|
||||||
<select name="condition_type" id="condition_type" class="regular-text" required>
|
|
||||||
<option value="user_type" <?php selected($rule->condition_type, 'user_type'); ?>><?php _e('نوع کاربر', 'sodino'); ?></option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><label for="condition_value"><?php _e('نوع کاربر', 'sodino'); ?></label></th>
|
|
||||||
<td>
|
|
||||||
<select name="condition_value" id="condition_value" class="regular-text" required>
|
|
||||||
<option value="new" <?php selected($rule->condition_value, 'new'); ?>><?php _e('کاربر جدید', 'sodino'); ?></option>
|
|
||||||
<option value="returning" <?php selected($rule->condition_value, 'returning'); ?>><?php _e('کاربر بازگشتی', 'sodino'); ?></option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><label for="action_type"><?php _e('نوع عملیات', 'sodino'); ?></label></th>
|
|
||||||
<td>
|
|
||||||
<select name="action_type" id="action_type" class="regular-text" required>
|
|
||||||
<option value="discount_percent" <?php selected($rule->action_type, 'discount_percent'); ?>><?php _e('درصد تخفیف', 'sodino'); ?></option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><label for="action_value"><?php _e('درصد تخفیف', 'sodino'); ?></label></th>
|
|
||||||
<td><input type="number" name="action_value" id="action_value" value="<?php echo esc_attr($rule->action_value); ?>" min="0" max="100" step="0.01" class="small-text" required> %</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><label for="enabled"><?php _e('وضعیت', 'sodino'); ?></label></th>
|
|
||||||
<td><label><input type="checkbox" name="enabled" id="enabled" value="1" <?php checked($rule->enabled, 1); ?>> <?php _e('فعال باشد', 'sodino'); ?></label></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<?php submit_button($rule->id ? __('بهروزرسانی قانون', 'sodino') : __('افزودن قانون', 'sودino'), 'primary'); ?>
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
|
<!-- Rule Name -->
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('عنوان قانون', 'sodino'); ?></label>
|
||||||
|
<input type="text" name="name" id="name" value="<?php echo esc_attr($rule->name); ?>" 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" required>
|
||||||
|
<p class="mt-2 text-sm text-gray-500"><?php _e('نامی کوتاه و قابل فهم برای قانون انتخاب کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Priority -->
|
||||||
|
<div>
|
||||||
|
<label for="priority" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('اولویت', 'sodino'); ?></label>
|
||||||
|
<input type="number" name="priority" id="priority" value="<?php echo esc_attr($rule->priority ?: 10); ?>" min="1" 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-2 text-sm text-gray-500"><?php _e('عدد بالاتر، اولویت بیشتری دارد.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Usage Limit -->
|
||||||
|
<div>
|
||||||
|
<label for="usage_limit" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('محدودیت تعداد استفاده', 'sodino'); ?></label>
|
||||||
|
<input type="number" name="usage_limit" id="usage_limit" value="<?php echo esc_attr($rule->usage_limit ?? 0); ?>" min="0" 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-2 text-sm text-gray-500"><?php _e('0 به معنی بدون محدودیت است.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Roles -->
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="user_roles" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('محدودیت نقش کاربری', 'sodino'); ?></label>
|
||||||
|
<select name="user_roles[]" id="user_roles" 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" multiple size="4">
|
||||||
|
<?php foreach (wp_roles()->roles as $role_key => $role_data) : ?>
|
||||||
|
<option value="<?php echo esc_attr($role_key); ?>" <?php echo in_array($role_key, (array) $rule->user_roles, true) ? 'selected' : ''; ?>><?php echo esc_html($role_data['name']); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<p class="mt-2 text-sm text-gray-500"><?php _e('نقشهایی که این قانون باید برای آنها اعمال شود.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Condition Type -->
|
||||||
|
<div>
|
||||||
|
<label for="condition_type" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('نوع شرط', 'sodino'); ?></label>
|
||||||
|
<select name="condition_type" id="condition_type" 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" required>
|
||||||
|
<option value="user_type" <?php selected($rule->condition_type, 'user_type'); ?>><?php _e('نوع کاربر', 'sodino'); ?></option>
|
||||||
|
<option value="product_category" <?php selected($rule->condition_type, 'product_category'); ?>><?php _e('دستهبندی محصول', 'sodino'); ?></option>
|
||||||
|
<option value="product_ids" <?php selected($rule->condition_type, 'product_ids'); ?>><?php _e('محصولات خاص', 'sodino'); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="mt-2 text-sm text-gray-500"><?php _e('نوع شرطی که باید قبل از اعمال قانون بررسی شود.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Condition Value -->
|
||||||
|
<div>
|
||||||
|
<label for="condition_value" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('مقدار شرط', 'sodino'); ?></label>
|
||||||
|
<input type="text" name="condition_value" id="condition_value" value="<?php echo esc_attr($rule->condition_value); ?>" 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" required>
|
||||||
|
<p class="mt-2 text-sm text-gray-500"><?php _e('برای کاربر جدید/بازگشتی: new یا returning. برای دستهبندی: شناسه دستهبندی، برای محصولات: شناسه محصول.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Type -->
|
||||||
|
<div>
|
||||||
|
<label for="action_type" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('نوع عملیات', 'sodino'); ?></label>
|
||||||
|
<select name="action_type" id="action_type" 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" required>
|
||||||
|
<option value="discount_percent" <?php selected($rule->action_type, 'discount_percent'); ?>><?php _e('درصد تخفیف', 'sodino'); ?></option>
|
||||||
|
<option value="discount_fixed" <?php selected($rule->action_type, 'discount_fixed'); ?>><?php _e('تخفیف ثابت', 'sodino'); ?></option>
|
||||||
|
<option value="free_shipping" <?php selected($rule->action_type, 'free_shipping'); ?>><?php _e('ارسال رایگان', 'sodino'); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Value -->
|
||||||
|
<div>
|
||||||
|
<label for="action_value" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('مقدار عملیات', 'sodino'); ?></label>
|
||||||
|
<input type="number" name="action_value" id="action_value" value="<?php echo esc_attr($rule->action_value); ?>" 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-2 text-sm text-gray-500"><?php _e('برای درصد یا مقدار ثابت وارد کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="flex items-center gap-3 text-gray-700">
|
||||||
|
<input type="checkbox" name="enabled" id="enabled" value="1" <?php checked($rule->enabled, 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
<span><?php _e('فعال باشد', 'sodino'); ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="mt-8 flex justify-end">
|
||||||
|
<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">
|
||||||
|
<?php echo $rule->id ? __('بهروزرسانی قانون', 'sodino') : __('افزودن قانون', 'sodino'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3,13 +3,81 @@
|
|||||||
if (!defined('ABSPATH')) {
|
if (!defined('ABSPATH')) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-rules');
|
||||||
?>
|
?>
|
||||||
<div class="wrap">
|
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
|
||||||
<h1><?php _e('قوانین قیمتگذاری', 'sodino'); ?></h1>
|
<!-- 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>
|
||||||
|
|
||||||
<a href="<?php echo admin_url('admin.php?page=sodino-add-rule'); ?>" class="page-title-action"><?php _e('افزودن قانون جدید', 'sodino'); ?></a>
|
<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('تنظیمات', 'sودینو'); ?>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 min-w-0">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('قوانین قیمتگذاری', 'sodino'); ?></h2>
|
||||||
|
<p class="mt-2 text-gray-600"><?php _e('مدیریت قوانین قیمتگذاری پویا و تخفیفهای هوشمند.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-add-rule'); ?>" class="inline-flex items-center px-4 py-2 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">
|
||||||
|
<svg class="-ml-1 mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<?php _e('افزودن قانون جدید', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rules Table -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<?php $rulesTable->display(); ?>
|
<?php $rulesTable->display(); ?>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3,10 +3,198 @@
|
|||||||
if (!defined('ABSPATH')) {
|
if (!defined('ABSPATH')) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-settings');
|
||||||
?>
|
?>
|
||||||
<div class="wrap">
|
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
|
||||||
<h1><?php _e('تنظیمات قیمتیار', 'sodino'); ?></h1>
|
<!-- Header -->
|
||||||
<div class="notice notice-info">
|
<div class="bg-white border-b border-gray-200">
|
||||||
<p><?php _e('در اینجا تنظیمات افزونه در آینده قرار خواهد گرفت.', 'sodino'); ?></p>
|
<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 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('تنظیمات سودینو', 'sodino'); ?></h2>
|
||||||
|
<p class="mt-2 text-gray-600"><?php _e('کنترل کامل تجربه قیمتگذاری و بهینهسازی درآمد را از اینجا انجام دهید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['updated']) && $_GET['updated'] === 'true') : ?>
|
||||||
|
<div class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<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'); ?>
|
||||||
|
|
||||||
|
<!-- General Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900"><?php _e('عمومی', 'sodino'); ?></h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('تنظیمات کلی فعالسازی پلاگین و ویژگیهای اصلی.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="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 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="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">
|
||||||
|
<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="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">
|
||||||
|
<span><?php _e('فعالسازی سیستم آپسل', 'sodino'); ?></span>
|
||||||
|
</label>
|
||||||
|
<p class="mt-3 text-sm text-gray-500"><?php _e('پیشنهادهای درآمدی اضافه را نمایش میدهد.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pricing Behavior Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900"><?php _e('رفتار قیمتگذاری', 'sodino'); ?></h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('چگونگی اجرای قوانین در فرآیند قیمتگذاری.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="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="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">
|
||||||
|
<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 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900"><?php _e('محدودیتها', 'sodino'); ?></h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('پارامترهای محدودسازی برای حفظ حاشیه سود.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="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 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900"><?php _e('ویژگیها', 'sodino'); ?></h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('گزینههای پیشرفته برای شخصیسازی رفتار درآمدی.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="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>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<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">
|
||||||
|
<?php _e('ذخیره تنظیمات', 'sodino'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
187
admin/views/upsell-form.php
Normal file
187
admin/views/upsell-form.php
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-add-upsell');
|
||||||
|
$trigger_product = null;
|
||||||
|
$trigger_product_label = '';
|
||||||
|
$target_product = null;
|
||||||
|
$target_product_label = '';
|
||||||
|
|
||||||
|
if ($upsell->trigger_type === 'product' && $upsell->trigger_value) {
|
||||||
|
$trigger_product = wc_get_product(intval($upsell->trigger_value));
|
||||||
|
$trigger_product_label = $trigger_product ? $trigger_product->get_name() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($upsell->target_product_id) {
|
||||||
|
$target_product = wc_get_product($upsell->target_product_id);
|
||||||
|
$target_product_label = $target_product ? $target_product->get_name() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$product_categories = get_terms([
|
||||||
|
'taxonomy' => 'product_cat',
|
||||||
|
'hide_empty' => false,
|
||||||
|
]);
|
||||||
|
?>
|
||||||
|
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
|
||||||
|
<div class="bg-white border-b border-gray-200">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900"><?php _e('سودینو', 'sodino'); ?></h1>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('بهینهسازی هوشمند فروش', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<aside class="w-64 flex-shrink-0">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('منوی سودینو', 'sodino'); ?></h2>
|
||||||
|
<nav class="space-y-2">
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-dashboard'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-dashboard' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('داشبورد', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-rules'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-rules' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('قوانین', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-add-rule'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-rule' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('افزودن قانون', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-upsells'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-upsells' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('آپسل (پیشنهاد فروش)', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-add-upsell'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-upsell' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('افزودن آپسل', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('قیمت رقبا (بهزودی)', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('تنظیمات', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="flex-1 min-w-0">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||||
|
<div class="flex items-center justify-between gap-4 flex-col sm:flex-row sm:items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900"><?php echo $upsell->id ? __('ویرایش آپسل', 'sodino') : __('افزودن آپسل جدید', 'sodino'); ?></h2>
|
||||||
|
<p class="mt-2 text-gray-600"><?php _e('یک پیشنهاد فروش مکمل هوشمند برای مشتریان ایجاد یا ویرایش کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-upsells'); ?>" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
|
||||||
|
<?php _e('بازگشت به آپسلها', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<form method="post" class="space-y-6">
|
||||||
|
<?php wp_nonce_field('sodino_save_upsell', 'sodino_upsell_nonce'); ?>
|
||||||
|
|
||||||
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="title" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('عنوان', 'sodino'); ?></label>
|
||||||
|
<input type="text" name="title" id="title" value="<?php echo esc_attr($upsell->title); ?>" 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" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2"><?php _e('نوع شرط', 'sodino'); ?></label>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="flex items-center gap-3 rounded-lg border border-gray-200 bg-white px-4 py-3 cursor-pointer hover:border-blue-300">
|
||||||
|
<input type="radio" name="trigger_type" value="product" class="trigger-type" <?php checked($upsell->trigger_type, 'product'); ?>>
|
||||||
|
<span><?php _e('محصول خاص', 'sodino'); ?></span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-3 rounded-lg border border-gray-200 bg-white px-4 py-3 cursor-pointer hover:border-blue-300">
|
||||||
|
<input type="radio" name="trigger_type" value="category" class="trigger-type" <?php checked($upsell->trigger_type, 'category'); ?>>
|
||||||
|
<span><?php _e('دستهبندی', 'sodino'); ?></span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-3 rounded-lg border border-gray-200 bg-white px-4 py-3 cursor-pointer hover:border-blue-300">
|
||||||
|
<input type="radio" name="trigger_type" value="cart_total" class="trigger-type" <?php checked($upsell->trigger_type, 'cart_total'); ?>>
|
||||||
|
<span><?php _e('مبلغ سبد خرید', 'sodino'); ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="trigger_value" id="trigger_value" value="<?php echo esc_attr($upsell->trigger_value); ?>">
|
||||||
|
|
||||||
|
<div id="trigger-product-wrapper" class="md:col-span-2 <?php echo $upsell->trigger_type === 'product' ? '' : 'hidden'; ?>">
|
||||||
|
<label for="trigger_product_search" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('محصول فعالساز', 'sodino'); ?></label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text" id="trigger_product_search" 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" placeholder="<?php _e('جستجوی محصول...', 'sodino'); ?>" value="<?php echo esc_attr($trigger_product_label); ?>">
|
||||||
|
<div id="trigger_product_results" class="absolute z-10 mt-1 w-full rounded-xl border border-gray-200 bg-white shadow-lg hidden"></div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500"><?php _e('محصولی که باعث نمایش پیشنهاد آپسل میشود را انتخاب کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="trigger-category-wrapper" class="md:col-span-2 <?php echo $upsell->trigger_type === 'category' ? '' : 'hidden'; ?>">
|
||||||
|
<label for="trigger_value_category" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('دستهبندی', 'sodino'); ?></label>
|
||||||
|
<select name="trigger_value" id="trigger_value_category" 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=""><?php _e('دستهبندی را انتخاب کنید', 'sodino'); ?></option>
|
||||||
|
<?php foreach ($product_categories as $category) : ?>
|
||||||
|
<option value="<?php echo esc_attr($category->term_id); ?>" <?php selected($upsell->trigger_type, 'category'); selected($upsell->trigger_value, $category->term_id); ?>><?php echo esc_html($category->name); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="trigger-amount-wrapper" class="md:col-span-2 <?php echo $upsell->trigger_type === 'cart_total' ? '' : 'hidden'; ?>">
|
||||||
|
<label for="trigger_value_amount" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('مبلغ سبد خرید', 'sodino'); ?></label>
|
||||||
|
<input type="number" name="trigger_value" id="trigger_value_amount" value="<?php echo esc_attr($upsell->trigger_type === 'cart_total' ? $upsell->trigger_value : ''); ?>" 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">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="target_product_search" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('محصول پیشنهادی', 'sodino'); ?></label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="hidden" name="target_product_id" id="target_product_id" value="<?php echo esc_attr($upsell->target_product_id); ?>">
|
||||||
|
<input type="text" id="target_product_search" 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" placeholder="<?php _e('جستجوی محصول پیشنهادی...', 'sodino'); ?>" value="<?php echo esc_attr($target_product_label); ?>">
|
||||||
|
<div id="target_product_results" class="absolute z-10 mt-1 w-full rounded-xl border border-gray-200 bg-white shadow-lg hidden"></div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500"><?php _e('محصولی که به مشتری پیشنهاد میشود را انتخاب کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="discount_type" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('نوع تخفیف', 'sodino'); ?></label>
|
||||||
|
<select name="discount_type" id="discount_type" 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="percentage" <?php selected($upsell->discount_type, 'percentage'); ?>><?php _e('درصدی', 'sodino'); ?></option>
|
||||||
|
<option value="fixed" <?php selected($upsell->discount_type, 'fixed'); ?>><?php _e('مبلغ ثابت', 'sodino'); ?></option>
|
||||||
|
<option value="none" <?php selected($upsell->discount_type, 'none'); ?>><?php _e('بدون تخفیف', 'sodino'); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="discount_value" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('مقدار تخفیف', 'sodino'); ?></label>
|
||||||
|
<input type="number" name="discount_value" id="discount_value" value="<?php echo esc_attr($upsell->discount_value); ?>" 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">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="priority" class="block text-sm font-medium text-gray-700 mb-2"><?php _e('اولویت', 'sodino'); ?></label>
|
||||||
|
<input type="number" name="priority" id="priority" value="<?php echo esc_attr($upsell->priority); ?>" min="1" 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">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="flex items-center gap-3 text-gray-700">
|
||||||
|
<input type="checkbox" name="status" id="status" value="1" <?php checked($upsell->status, 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
<span><?php _e('فعال باشد', 'sodino'); ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<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">
|
||||||
|
<?php echo $upsell->id ? __('ذخیره آپسل', 'sodino') : __('افزودن آپسل', 'sodino'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
78
admin/views/upsell-list.php
Normal file
78
admin/views/upsell-list.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-upsells');
|
||||||
|
?>
|
||||||
|
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
|
||||||
|
<div class="bg-white border-b border-gray-200">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900"><?php _e('سودینو', 'sodino'); ?></h1>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('بهینهسازی هوشمند فروش', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<aside class="w-64 flex-shrink-0">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('منوی سودینو', 'sodino'); ?></h2>
|
||||||
|
<nav class="space-y-2">
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-dashboard'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-dashboard' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('داشبورد', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-rules'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-rules' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('قوانین', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-add-rule'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-rule' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('افزودن قانون', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-upsells'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-upsells' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('آپسل (پیشنهاد فروش)', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-add-upsell'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-upsell' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('افزودن آپسل', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('قیمت رقبا (بهزودی)', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
|
||||||
|
<?php _e('تنظیمات', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="flex-1 min-w-0">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||||
|
<div class="flex items-center justify-between gap-4 flex-col sm:flex-row sm:items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('مدیریت آپسلها', 'sodino'); ?></h2>
|
||||||
|
<p class="mt-2 text-gray-600"><?php _e('ایجاد، ویرایش و مدیریت پیشنهادهای فروش مکمل.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=sodino-add-upsell'); ?>" class="inline-flex items-center px-4 py-2 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">
|
||||||
|
<svg class="-ml-1 mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<?php _e('افزودن آپسل جدید', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
<form method="post">
|
||||||
|
<?php $upsellTable->display(); ?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -2,16 +2,20 @@
|
|||||||
namespace Sodino\Controllers;
|
namespace Sodino\Controllers;
|
||||||
|
|
||||||
use Sodino\Repositories\RuleRepository;
|
use Sodino\Repositories\RuleRepository;
|
||||||
|
use Sodino\Repositories\UpsellRepository;
|
||||||
use Sodino\Models\Rule;
|
use Sodino\Models\Rule;
|
||||||
|
use Sodino\Models\Upsell;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin Controller
|
* Admin Controller
|
||||||
*/
|
*/
|
||||||
class AdminController {
|
class AdminController {
|
||||||
private $ruleRepository;
|
private $ruleRepository;
|
||||||
|
private $upsellRepository;
|
||||||
|
|
||||||
public function __construct(RuleRepository $ruleRepository) {
|
public function __construct(RuleRepository $ruleRepository, UpsellRepository $upsellRepository) {
|
||||||
$this->ruleRepository = $ruleRepository;
|
$this->ruleRepository = $ruleRepository;
|
||||||
|
$this->upsellRepository = $upsellRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,8 +23,8 @@ class AdminController {
|
|||||||
*/
|
*/
|
||||||
public function addMenu() {
|
public function addMenu() {
|
||||||
add_menu_page(
|
add_menu_page(
|
||||||
__('قیمتیار', 'sodino'),
|
__('سودینو', 'sodino'),
|
||||||
__('قیمتیار', 'sodino'),
|
__('سودینو', 'sodino'),
|
||||||
'manage_options',
|
'manage_options',
|
||||||
'sodino-rules',
|
'sodino-rules',
|
||||||
[$this, 'rulesPage'],
|
[$this, 'rulesPage'],
|
||||||
@@ -48,7 +52,43 @@ class AdminController {
|
|||||||
|
|
||||||
add_submenu_page(
|
add_submenu_page(
|
||||||
'sodino-rules',
|
'sodino-rules',
|
||||||
__('تنظیمات', 'sodino'),
|
__('آپسل (پیشنهاد فروش)', 'sodino'),
|
||||||
|
__('آپسل (پیشنهاد فروش)', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-upsells',
|
||||||
|
[$this, 'upsellsPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-rules',
|
||||||
|
__('افزودن آپسل', 'sodino'),
|
||||||
|
__('افزودن آپسل', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-add-upsell',
|
||||||
|
[$this, 'addUpsellPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-rules',
|
||||||
|
__('قیمت رقبا (بهزودی)', 'sodino'),
|
||||||
|
__('قیمت رقبا (بهزودی)', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-competitor-price',
|
||||||
|
[$this, 'competitorPricePage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-rules',
|
||||||
|
__('داشبورد سودینو', 'sودino'),
|
||||||
|
__('داشبورد سودینو', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-dashboard',
|
||||||
|
[$this, 'dashboardPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-rules',
|
||||||
|
__('تنظیمات', 'sودino'),
|
||||||
__('تنظیمات', 'sودینو'),
|
__('تنظیمات', 'sودینو'),
|
||||||
'manage_options',
|
'manage_options',
|
||||||
'sodino-settings',
|
'sodino-settings',
|
||||||
@@ -63,6 +103,32 @@ class AdminController {
|
|||||||
$this->listRulesPage();
|
$this->listRulesPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard page
|
||||||
|
*/
|
||||||
|
public function dashboardPage() {
|
||||||
|
$settings = $this->getSettings();
|
||||||
|
$analyticsService = new \Sodino\Services\AnalyticsService(new \Sodino\Repositories\EventRepository(), $this->ruleRepository);
|
||||||
|
|
||||||
|
$filters = [
|
||||||
|
'range' => isset($_GET['range']) ? sanitize_text_field($_GET['range']) : '7d',
|
||||||
|
'start_date' => isset($_GET['start_date']) ? sanitize_text_field($_GET['start_date']) : '',
|
||||||
|
'end_date' => isset($_GET['end_date']) ? sanitize_text_field($_GET['end_date']) : '',
|
||||||
|
'product_id' => isset($_GET['product_id']) ? intval($_GET['product_id']) : 0,
|
||||||
|
'category_id' => isset($_GET['category_id']) ? intval($_GET['category_id']) : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($filters['product_id'])) {
|
||||||
|
$filters['product_ids'] = [$filters['product_id']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dashboardData = $analyticsService->getDashboardData($filters);
|
||||||
|
$productOptions = $analyticsService->getProductOptions();
|
||||||
|
$categoryOptions = $analyticsService->getCategoryOptions();
|
||||||
|
|
||||||
|
include SODINO_PLUGIN_DIR . 'admin/views/dashboard.php';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List rules page
|
* List rules page
|
||||||
*/
|
*/
|
||||||
@@ -95,9 +161,191 @@ class AdminController {
|
|||||||
* Settings page
|
* Settings page
|
||||||
*/
|
*/
|
||||||
public function settingsPage() {
|
public function settingsPage() {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$this->saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $this->getSettings();
|
||||||
include SODINO_PLUGIN_DIR . 'admin/views/settings.php';
|
include SODINO_PLUGIN_DIR . 'admin/views/settings.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsell list page
|
||||||
|
*/
|
||||||
|
public function upsellsPage() {
|
||||||
|
$this->listUpsellsPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or edit upsell page
|
||||||
|
*/
|
||||||
|
public function addUpsellPage() {
|
||||||
|
if (isset($_GET['action']) && $_GET['action'] === 'edit') {
|
||||||
|
return $this->editUpsellPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$this->saveUpsell();
|
||||||
|
} else {
|
||||||
|
$upsell = new Upsell();
|
||||||
|
include SODINO_PLUGIN_DIR . 'admin/views/upsell-form.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Competitor price page
|
||||||
|
*/
|
||||||
|
public function competitorPricePage() {
|
||||||
|
include SODINO_PLUGIN_DIR . 'admin/views/competitor-price.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function listUpsellsPage() {
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'admin/class-upsell-list-table.php';
|
||||||
|
$upsellTable = new \Sodino_Upsell_List_Table($this->upsellRepository);
|
||||||
|
$upsellTable->prepare_items();
|
||||||
|
include SODINO_PLUGIN_DIR . 'admin/views/upsell-list.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function editUpsellPage() {
|
||||||
|
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||||
|
$upsell = $this->upsellRepository->getById($id);
|
||||||
|
|
||||||
|
if (!$upsell) {
|
||||||
|
wp_die(__('Upsell not found', 'sodino'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$this->saveUpsell($upsell);
|
||||||
|
} else {
|
||||||
|
include SODINO_PLUGIN_DIR . 'admin/views/upsell-form.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveUpsell($upsell = null) {
|
||||||
|
if (!isset($_POST['sodino_upsell_nonce']) || !wp_verify_nonce($_POST['sodino_upsell_nonce'], 'sodino_save_upsell')) {
|
||||||
|
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$upsell) {
|
||||||
|
$upsell = new Upsell();
|
||||||
|
}
|
||||||
|
|
||||||
|
$upsell->title = sanitize_text_field($_POST['title'] ?? '');
|
||||||
|
$upsell->trigger_type = sanitize_text_field($_POST['trigger_type'] ?? 'product');
|
||||||
|
$upsell->trigger_value = sanitize_text_field($_POST['trigger_value'] ?? '');
|
||||||
|
$upsell->target_product_id = max(0, intval($_POST['target_product_id'] ?? 0));
|
||||||
|
$upsell->discount_type = sanitize_text_field($_POST['discount_type'] ?? 'percentage');
|
||||||
|
$upsell->discount_value = max(0, floatval($_POST['discount_value'] ?? 0));
|
||||||
|
$upsell->priority = max(1, intval($_POST['priority'] ?? 10));
|
||||||
|
$upsell->status = isset($_POST['status']) ? 1 : 0;
|
||||||
|
|
||||||
|
$this->upsellRepository->save($upsell);
|
||||||
|
|
||||||
|
wp_safe_redirect(admin_url('admin.php?page=sodino-upsells'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleUpsellActions() {
|
||||||
|
if (!isset($_GET['_wpnonce']) || !in_array($_GET['action'], ['delete_upsell', 'toggle_upsell_status'], true) || !wp_verify_nonce($_GET['_wpnonce'], $_GET['action'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||||
|
if (!$id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_GET['action'] === 'delete_upsell') {
|
||||||
|
$this->upsellRepository->delete($id);
|
||||||
|
wp_safe_redirect(admin_url('admin.php?page=sodino-upsells'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_GET['action'] === 'toggle_upsell_status') {
|
||||||
|
$upsell = $this->upsellRepository->getById($id);
|
||||||
|
if ($upsell) {
|
||||||
|
$upsell->status = $upsell->status ? 0 : 1;
|
||||||
|
$this->upsellRepository->save($upsell);
|
||||||
|
}
|
||||||
|
wp_safe_redirect(admin_url('admin.php?page=sodino-upsells'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchProductsAjax() {
|
||||||
|
if (!check_ajax_referer('sodino_search_products', 'security', false)) {
|
||||||
|
wp_send_json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$term = sanitize_text_field($_POST['term'] ?? '');
|
||||||
|
if (empty($term) || !function_exists('wc_get_products')) {
|
||||||
|
wp_send_json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = wc_get_products([
|
||||||
|
'limit' => 10,
|
||||||
|
'status' => 'publish',
|
||||||
|
'search' => $term,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$results[] = [
|
||||||
|
'id' => $product->get_id(),
|
||||||
|
'label' => $product->get_name(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json($results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSettingsDefaults() {
|
||||||
|
return [
|
||||||
|
'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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSettings() {
|
||||||
|
return wp_parse_args(get_option('sodino_settings', []), $this->getSettingsDefaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveSettings() {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die(__('دسترسی کافی ندارید.', 'sodino'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_POST['sodino_settings_nonce']) || !wp_verify_nonce($_POST['sodino_settings_nonce'], 'sodino_save_settings')) {
|
||||||
|
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$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,
|
||||||
|
'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,
|
||||||
|
];
|
||||||
|
|
||||||
|
update_option('sodino_settings', $settings);
|
||||||
|
|
||||||
|
wp_safe_redirect(add_query_arg('updated', 'true', admin_url('admin.php?page=sodino-settings')));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit rule page
|
* Edit rule page
|
||||||
*/
|
*/
|
||||||
@@ -129,6 +377,9 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rule->name = sanitize_text_field($_POST['name'] ?? '');
|
$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->condition_type = sanitize_text_field($_POST['condition_type'] ?? 'user_type');
|
$rule->condition_type = sanitize_text_field($_POST['condition_type'] ?? 'user_type');
|
||||||
$rule->condition_value = sanitize_text_field($_POST['condition_value'] ?? 'new');
|
$rule->condition_value = sanitize_text_field($_POST['condition_value'] ?? 'new');
|
||||||
$rule->action_type = sanitize_text_field($_POST['action_type'] ?? 'discount_percent');
|
$rule->action_type = sanitize_text_field($_POST['action_type'] ?? 'discount_percent');
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class Rule {
|
|||||||
public $conditions;
|
public $conditions;
|
||||||
public $actions;
|
public $actions;
|
||||||
public $priority;
|
public $priority;
|
||||||
|
public $usage_limit;
|
||||||
|
public $user_roles;
|
||||||
public $start_date;
|
public $start_date;
|
||||||
public $end_date;
|
public $end_date;
|
||||||
public $enabled;
|
public $enabled;
|
||||||
@@ -29,6 +31,8 @@ class Rule {
|
|||||||
$this->conditions = $this->parseJsonField($data['conditions'] ?? '[]');
|
$this->conditions = $this->parseJsonField($data['conditions'] ?? '[]');
|
||||||
$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->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;
|
||||||
@@ -61,6 +65,18 @@ class Rule {
|
|||||||
return is_array($decoded) ? $decoded : [];
|
return is_array($decoded) ? $decoded : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function parseRolesField($value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
return array_filter(array_map('trim', $value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter(array_map('trim', explode(',', $value)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert to array
|
* Convert to array
|
||||||
*/
|
*/
|
||||||
@@ -71,6 +87,8 @@ class Rule {
|
|||||||
'conditions' => wp_json_encode($this->conditions),
|
'conditions' => wp_json_encode($this->conditions),
|
||||||
'actions' => wp_json_encode($this->actions),
|
'actions' => wp_json_encode($this->actions),
|
||||||
'priority' => $this->priority,
|
'priority' => $this->priority,
|
||||||
|
'usage_limit' => $this->usage_limit,
|
||||||
|
'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,
|
||||||
|
|||||||
53
app/Models/Upsell.php
Normal file
53
app/Models/Upsell.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsell Model
|
||||||
|
*/
|
||||||
|
class Upsell {
|
||||||
|
public $id;
|
||||||
|
public $title;
|
||||||
|
public $trigger_type;
|
||||||
|
public $trigger_value;
|
||||||
|
public $target_product_id;
|
||||||
|
public $discount_type;
|
||||||
|
public $discount_value;
|
||||||
|
public $status;
|
||||||
|
public $priority;
|
||||||
|
public $created_at;
|
||||||
|
public $updated_at;
|
||||||
|
|
||||||
|
public function __construct($data = []) {
|
||||||
|
$this->id = isset($data['id']) ? (int) $data['id'] : null;
|
||||||
|
$this->title = $data['title'] ?? '';
|
||||||
|
$this->trigger_type = $data['trigger_type'] ?? 'product';
|
||||||
|
$this->trigger_value = isset($data['trigger_value']) ? (string) $data['trigger_value'] : '';
|
||||||
|
$this->target_product_id = isset($data['target_product_id']) ? (int) $data['target_product_id'] : 0;
|
||||||
|
$this->discount_type = $data['discount_type'] ?? 'percentage';
|
||||||
|
$this->discount_value = isset($data['discount_value']) ? floatval($data['discount_value']) : 0;
|
||||||
|
$this->status = isset($data['status']) ? (int) $data['status'] : 1;
|
||||||
|
$this->priority = isset($data['priority']) ? (int) $data['priority'] : 10;
|
||||||
|
$this->created_at = $data['created_at'] ?? null;
|
||||||
|
$this->updated_at = $data['updated_at'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isActive() {
|
||||||
|
return (bool) $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray() {
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'title' => sanitize_text_field($this->title),
|
||||||
|
'trigger_type' => sanitize_text_field($this->trigger_type),
|
||||||
|
'trigger_value' => sanitize_text_field($this->trigger_value),
|
||||||
|
'target_product_id' => $this->target_product_id,
|
||||||
|
'discount_type' => sanitize_text_field($this->discount_type),
|
||||||
|
'discount_value' => floatval($this->discount_value),
|
||||||
|
'status' => $this->status,
|
||||||
|
'priority' => $this->priority,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
98
app/Repositories/EventRepository.php
Normal file
98
app/Repositories/EventRepository.php
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Repositories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Repository
|
||||||
|
*/
|
||||||
|
class EventRepository {
|
||||||
|
private $table_name;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
global $wpdb;
|
||||||
|
$this->table_name = $wpdb->prefix . 'sodino_events';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insert(array $data) {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->insert($this->table_name, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEvents(array $filters = []) {
|
||||||
|
global $wpdb;
|
||||||
|
$params = [];
|
||||||
|
$where = $this->buildWhereClauses($filters, $params);
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM {$this->table_name} WHERE " . implode(' AND ', $where) . " ORDER BY created_at ASC";
|
||||||
|
return $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCount(array $filters = []) {
|
||||||
|
global $wpdb;
|
||||||
|
$params = [];
|
||||||
|
$where = $this->buildWhereClauses($filters, $params);
|
||||||
|
|
||||||
|
$sql = "SELECT COUNT(*) FROM {$this->table_name} WHERE " . implode(' AND ', $where);
|
||||||
|
return (int) $wpdb->get_var($wpdb->prepare($sql, $params));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSum($field, array $filters = []) {
|
||||||
|
global $wpdb;
|
||||||
|
if (!in_array($field, ['value', 'discount_value'], true)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
$where = $this->buildWhereClauses($filters, $params);
|
||||||
|
|
||||||
|
$sql = "SELECT SUM({$field}) FROM {$this->table_name} WHERE " . implode(' AND ', $where);
|
||||||
|
return floatval($wpdb->get_var($wpdb->prepare($sql, $params)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRuleUsageCount($rule_id) {
|
||||||
|
global $wpdb;
|
||||||
|
return (int) $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->table_name} WHERE event_type = %s AND rule_id = %d",
|
||||||
|
'discount_applied',
|
||||||
|
$rule_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildWhereClauses(array $filters, array &$params) {
|
||||||
|
$where = ['1=1'];
|
||||||
|
|
||||||
|
if (!empty($filters['event_type'])) {
|
||||||
|
if (is_array($filters['event_type'])) {
|
||||||
|
$placeholders = implode(', ', array_fill(0, count($filters['event_type']), '%s'));
|
||||||
|
$where[] = "event_type IN ($placeholders)";
|
||||||
|
$params = array_merge($params, $filters['event_type']);
|
||||||
|
} else {
|
||||||
|
$where[] = 'event_type = %s';
|
||||||
|
$params[] = $filters['event_type'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['product_ids'])) {
|
||||||
|
$ids = array_map('intval', (array) $filters['product_ids']);
|
||||||
|
$placeholders = implode(', ', array_fill(0, count($ids), '%d'));
|
||||||
|
$where[] = "product_id IN ($placeholders)";
|
||||||
|
$params = array_merge($params, $ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['rule_id'])) {
|
||||||
|
$where[] = 'rule_id = %d';
|
||||||
|
$params[] = intval($filters['rule_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['from'])) {
|
||||||
|
$where[] = 'created_at >= %s';
|
||||||
|
$params[] = $filters['from'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['to'])) {
|
||||||
|
$where[] = 'created_at <= %s';
|
||||||
|
$params[] = $filters['to'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $where;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Repositories/UpsellRepository.php
Normal file
61
app/Repositories/UpsellRepository.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Repositories;
|
||||||
|
|
||||||
|
use Sodino\Models\Upsell;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsell Repository
|
||||||
|
*/
|
||||||
|
class UpsellRepository {
|
||||||
|
private $table_name;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
global $wpdb;
|
||||||
|
$this->table_name = $wpdb->prefix . 'sodino_upsells';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll() {
|
||||||
|
global $wpdb;
|
||||||
|
$results = $wpdb->get_results("SELECT * FROM {$this->table_name} ORDER BY priority DESC, id ASC", ARRAY_A);
|
||||||
|
$items = [];
|
||||||
|
foreach ($results as $result) {
|
||||||
|
$items[] = new Upsell($result);
|
||||||
|
}
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById($id) {
|
||||||
|
global $wpdb;
|
||||||
|
$result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id), ARRAY_A);
|
||||||
|
return $result ? new Upsell($result) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActive() {
|
||||||
|
global $wpdb;
|
||||||
|
$results = $wpdb->get_results("SELECT * FROM {$this->table_name} WHERE status = 1 ORDER BY priority DESC, id ASC", ARRAY_A);
|
||||||
|
$items = [];
|
||||||
|
foreach ($results as $result) {
|
||||||
|
$items[] = new Upsell($result);
|
||||||
|
}
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Upsell $upsell) {
|
||||||
|
global $wpdb;
|
||||||
|
$data = $upsell->toArray();
|
||||||
|
unset($data['id'], $data['created_at'], $data['updated_at']);
|
||||||
|
|
||||||
|
if ($upsell->id) {
|
||||||
|
$wpdb->update($this->table_name, $data, ['id' => $upsell->id]);
|
||||||
|
return $upsell->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$wpdb->insert($this->table_name, $data);
|
||||||
|
return $wpdb->insert_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id) {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->delete($this->table_name, ['id' => $id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
281
app/Services/AnalyticsService.php
Normal file
281
app/Services/AnalyticsService.php
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Services;
|
||||||
|
|
||||||
|
use Sodino\Repositories\EventRepository;
|
||||||
|
use Sodino\Repositories\RuleRepository;
|
||||||
|
|
||||||
|
class AnalyticsService {
|
||||||
|
private $eventRepository;
|
||||||
|
private $ruleRepository;
|
||||||
|
|
||||||
|
public function __construct(EventRepository $eventRepository, RuleRepository $ruleRepository) {
|
||||||
|
$this->eventRepository = $eventRepository;
|
||||||
|
$this->ruleRepository = $ruleRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function primeCache() {
|
||||||
|
$cache_keys = [
|
||||||
|
'sodino_dashboard_summary',
|
||||||
|
'sodino_dashboard_sales_chart',
|
||||||
|
'sodino_dashboard_rule_performance',
|
||||||
|
'sodino_dashboard_user_behavior',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($cache_keys as $key) {
|
||||||
|
delete_transient($key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDashboardData(array $filters = []) {
|
||||||
|
$cache_key = 'sodino_dashboard_' . md5(wp_json_encode($filters));
|
||||||
|
$cached = get_transient($cache_key);
|
||||||
|
if ($cached !== false) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$range = $this->getDateRange($filters['range'] ?? '7d', $filters['start_date'] ?? '', $filters['end_date'] ?? '');
|
||||||
|
$filters['from'] = $range['start'];
|
||||||
|
$filters['to'] = $range['end'];
|
||||||
|
|
||||||
|
if (!empty($filters['category_id'])) {
|
||||||
|
$filters['product_ids'] = $this->getProductIdsByCategory($filters['category_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = $this->getSummary($filters);
|
||||||
|
$salesChart = $this->getSalesChart($filters);
|
||||||
|
$rulePerformance = $this->getRulePerformance($filters);
|
||||||
|
$userBehavior = $this->getUserBehavior($filters);
|
||||||
|
$insights = $this->getInsights($summary, $filters);
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'summary' => $summary,
|
||||||
|
'sales_chart' => $salesChart,
|
||||||
|
'rule_performance' => $rulePerformance,
|
||||||
|
'user_behavior' => $userBehavior,
|
||||||
|
'insights' => $insights,
|
||||||
|
];
|
||||||
|
|
||||||
|
set_transient($cache_key, $result, 10 * MINUTE_IN_SECONDS);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSummary(array $filters = []) {
|
||||||
|
$purchaseFilters = array_merge($filters, ['event_type' => 'purchase']);
|
||||||
|
$discountFilters = array_merge($filters, ['event_type' => 'discount_applied']);
|
||||||
|
|
||||||
|
$purchaseCount = $this->eventRepository->getCount($purchaseFilters);
|
||||||
|
$totalRevenue = $this->eventRepository->getSum('value', $purchaseFilters);
|
||||||
|
$totalDiscount = $this->eventRepository->getSum('discount_value', $discountFilters);
|
||||||
|
|
||||||
|
$productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view']));
|
||||||
|
$addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart']));
|
||||||
|
$checkoutStartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'checkout_start']));
|
||||||
|
|
||||||
|
$conversionRate = 0;
|
||||||
|
if ($checkoutStartCount > 0) {
|
||||||
|
$conversionRate = round(($purchaseCount / $checkoutStartCount) * 100, 2);
|
||||||
|
} elseif ($addToCartCount > 0) {
|
||||||
|
$conversionRate = round(($purchaseCount / $addToCartCount) * 100, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$addToCartRate = 0;
|
||||||
|
if ($productViewCount > 0) {
|
||||||
|
$addToCartRate = round(($addToCartCount / $productViewCount) * 100, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$bestRule = $this->getBestRule($filters);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_revenue' => $totalRevenue,
|
||||||
|
'total_discount' => $totalDiscount,
|
||||||
|
'purchase_count' => $purchaseCount,
|
||||||
|
'conversion_rate' => $conversionRate,
|
||||||
|
'add_to_cart_rate' => $addToCartRate,
|
||||||
|
'best_rule' => $bestRule,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSalesChart(array $filters = []) {
|
||||||
|
$filters = array_merge($filters, $this->getDateRange($filters['range'] ?? '7d', $filters['start_date'] ?? '', $filters['end_date'] ?? ''));
|
||||||
|
|
||||||
|
if (!empty($filters['category_id'])) {
|
||||||
|
$filters['product_ids'] = $this->getProductIdsByCategory($filters['category_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = new \DateTime($filters['start']);
|
||||||
|
$end = new \DateTime($filters['end']);
|
||||||
|
$days = [];
|
||||||
|
$series = [ 'before' => [], 'after' => [], 'labels' => [] ];
|
||||||
|
|
||||||
|
while ($start <= $end) {
|
||||||
|
$date = $start->format('Y-m-d');
|
||||||
|
$series['labels'][] = $date;
|
||||||
|
$dayFilters = array_merge($filters, ['from' => $date . ' 00:00:00', 'to' => $date . ' 23:59:59']);
|
||||||
|
$purchases = $this->eventRepository->getEvents(array_merge($dayFilters, ['event_type' => 'purchase']));
|
||||||
|
$dayRevenue = 0;
|
||||||
|
$dayDiscount = 0;
|
||||||
|
foreach ($purchases as $purchase) {
|
||||||
|
$dayRevenue += floatval($purchase['value']);
|
||||||
|
}
|
||||||
|
$discountEvents = $this->eventRepository->getEvents(array_merge($dayFilters, ['event_type' => 'discount_applied']));
|
||||||
|
foreach ($discountEvents as $discount) {
|
||||||
|
$dayDiscount += floatval($discount['discount_value']);
|
||||||
|
}
|
||||||
|
$series['after'][] = round($dayRevenue, 2);
|
||||||
|
$series['before'][] = round($dayRevenue + $dayDiscount, 2);
|
||||||
|
$start->modify('+1 day');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $series;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRulePerformance(array $filters = []) {
|
||||||
|
$rules = $this->ruleRepository->getAll();
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($rules as $rule) {
|
||||||
|
$ruleFilters = array_merge($filters, ['event_type' => 'discount_applied', 'rule_id' => $rule->id]);
|
||||||
|
$count = $this->eventRepository->getCount($ruleFilters);
|
||||||
|
$revenue = $this->eventRepository->getSum('value', $ruleFilters);
|
||||||
|
$totalDiscount = $this->eventRepository->getSum('discount_value', $ruleFilters);
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
$result[] = [
|
||||||
|
'name' => $rule->name,
|
||||||
|
'count' => $count,
|
||||||
|
'revenue' => round($revenue, 2),
|
||||||
|
'discount' => round($totalDiscount, 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserBehavior(array $filters = []) {
|
||||||
|
$productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view']));
|
||||||
|
$addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart']));
|
||||||
|
$checkoutStartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'checkout_start']));
|
||||||
|
$purchaseCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'purchase']));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'product_views' => $productViewCount,
|
||||||
|
'add_to_cart' => $addToCartCount,
|
||||||
|
'checkout_start' => $checkoutStartCount,
|
||||||
|
'purchases' => $purchaseCount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getBestRule(array $filters = []) {
|
||||||
|
$performance = $this->getRulePerformance($filters);
|
||||||
|
if (empty($performance)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($performance, function ($a, $b) {
|
||||||
|
return $b['revenue'] <=> $a['revenue'];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $performance[0]['name'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getInsights(array $summary, array $filters = []) {
|
||||||
|
$insights = [];
|
||||||
|
|
||||||
|
if (!empty($summary['best_rule'])) {
|
||||||
|
$insights[] = sprintf('%s %s', __('قانون برتر:', 'sodino'), esc_html($summary['best_rule']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($summary['total_discount'] > 0 && $summary['total_revenue'] > 0) {
|
||||||
|
$discountShare = round(($summary['total_discount'] / ($summary['total_revenue'] + $summary['total_discount'])) * 100, 2);
|
||||||
|
$insights[] = sprintf(
|
||||||
|
'%s %s%% %s',
|
||||||
|
__('تخفیفها باعث ایجاد', 'sodino'),
|
||||||
|
esc_html($discountShare),
|
||||||
|
__('درصد از درآمد شدهاند.', 'sodino')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($summary['conversion_rate'] > 0) {
|
||||||
|
$insights[] = sprintf('%s %s%% %s', __('نرخ تبدیل تقریبی', 'sodino'), esc_html($summary['conversion_rate']), __('است.', 'sodino'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($insights)) {
|
||||||
|
$insights[] = __('هیچ دادهٔ قابل تحلیلی هنوز ثبت نشده است.', 'sodino');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $insights;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDateRange($range, $start, $end) {
|
||||||
|
$result = [
|
||||||
|
'start' => date('Y-m-d', strtotime('-6 days')),
|
||||||
|
'end' => date('Y-m-d'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($range === '30d') {
|
||||||
|
$result['start'] = date('Y-m-d', strtotime('-29 days'));
|
||||||
|
$result['end'] = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($range === 'custom' && !empty($start) && !empty($end)) {
|
||||||
|
$result['start'] = date('Y-m-d', strtotime($start));
|
||||||
|
$result['end'] = date('Y-m-d', strtotime($end));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductIdsByCategory($category_id) {
|
||||||
|
$products = get_posts([
|
||||||
|
'post_type' => 'product',
|
||||||
|
'numberposts' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'tax_query' => [
|
||||||
|
[
|
||||||
|
'taxonomy' => 'product_cat',
|
||||||
|
'field' => 'term_id',
|
||||||
|
'terms' => intval($category_id),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $products ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductOptions() {
|
||||||
|
$products = get_posts([
|
||||||
|
'post_type' => 'product',
|
||||||
|
'numberposts' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
foreach ($products as $product_id) {
|
||||||
|
$options[] = [
|
||||||
|
'id' => $product_id,
|
||||||
|
'name' => get_the_title($product_id),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCategoryOptions() {
|
||||||
|
$categories = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => false]);
|
||||||
|
$options = [];
|
||||||
|
|
||||||
|
if (!is_wp_error($categories)) {
|
||||||
|
foreach ($categories as $category) {
|
||||||
|
$options[] = [
|
||||||
|
'id' => $category->term_id,
|
||||||
|
'name' => $category->name,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,54 +2,98 @@
|
|||||||
namespace Sodino\Services;
|
namespace Sodino\Services;
|
||||||
|
|
||||||
use Sodino\Repositories\RuleRepository;
|
use Sodino\Repositories\RuleRepository;
|
||||||
|
use Sodino\Services\TrackingService;
|
||||||
|
|
||||||
/**
|
|
||||||
* Pricing Service
|
|
||||||
*/
|
|
||||||
class PricingService {
|
class PricingService {
|
||||||
private $ruleRepository;
|
private $ruleRepository;
|
||||||
|
private $trackingService;
|
||||||
private $rulesCache = null;
|
private $rulesCache = null;
|
||||||
private $freeShipping = false;
|
|
||||||
|
|
||||||
public function __construct(RuleRepository $ruleRepository) {
|
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
|
||||||
$this->ruleRepository = $ruleRepository;
|
$this->ruleRepository = $ruleRepository;
|
||||||
|
$this->trackingService = $trackingService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply dynamic pricing to a product price
|
|
||||||
*/
|
|
||||||
public function applyDynamicPricing($price, $product) {
|
public function applyDynamicPricing($price, $product) {
|
||||||
|
$settings = $this->getSettings();
|
||||||
|
if (empty($settings['plugin_enabled']) || empty($settings['pricing_enabled'])) {
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$price || !is_numeric($price)) {
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
$price = $this->normalizePrice($price);
|
$price = $this->normalizePrice($price);
|
||||||
|
if (!$settings['cart_pricing_enabled'] && is_cart()) {
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalPrice = $price;
|
||||||
$rules = $this->getEnabledRules();
|
$rules = $this->getEnabledRules();
|
||||||
$matchedRule = null;
|
$matchedRules = [];
|
||||||
|
|
||||||
foreach ($rules as $rule) {
|
foreach ($rules as $rule) {
|
||||||
if ($this->ruleMatches($rule, $product)) {
|
if ($this->ruleMatches($rule, $product)) {
|
||||||
if ($matchedRule === null || $rule->priority > $matchedRule->priority) {
|
$matchedRules[] = $rule;
|
||||||
$matchedRule = $rule;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($matchedRule) {
|
if (empty($matchedRules)) {
|
||||||
$price = $this->applyActions($matchedRule, $price);
|
return $price;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$settings['allow_multiple_rules']) {
|
||||||
|
$chosenRule = $this->chooseRule($matchedRules, $price, $settings['strategy']);
|
||||||
|
$matchedRules = $chosenRule ? [$chosenRule] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($matchedRules as $rule) {
|
||||||
|
$oldPrice = $price;
|
||||||
|
$price = $this->applyActions($rule, $price);
|
||||||
|
if ($price < $oldPrice) {
|
||||||
|
$this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $rule->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$price = $this->enforceLimits($originalPrice, $price, $settings);
|
||||||
|
|
||||||
return max(0, $price);
|
return max(0, $price);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function shouldApplyFreeShipping() {
|
private function chooseRule(array $rules, $price, $strategy) {
|
||||||
$rules = $this->getEnabledRules();
|
if ($strategy === 'highest_discount') {
|
||||||
foreach ($rules as $rule) {
|
usort($rules, function ($a, $b) use ($price) {
|
||||||
if ($this->ruleMatches($rule, null) && $this->ruleHasFreeShipping($rule)) {
|
return $this->estimateRuleDiscount($b, $price) <=> $this->estimateRuleDiscount($a, $price);
|
||||||
return true;
|
});
|
||||||
}
|
return $rules[0] ?? null;
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resetFreeShippingFlag() {
|
if ($strategy === 'first_valid') {
|
||||||
$this->freeShipping = false;
|
return $rules[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($rules, function ($a, $b) {
|
||||||
|
return $b->priority <=> $a->priority;
|
||||||
|
});
|
||||||
|
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() {
|
private function getEnabledRules() {
|
||||||
@@ -67,22 +111,29 @@ class PricingService {
|
|||||||
return floatval($price);
|
return floatval($price);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getUserType() {
|
|
||||||
if (!is_user_logged_in()) {
|
|
||||||
return 'guest';
|
|
||||||
}
|
|
||||||
|
|
||||||
$user_id = get_current_user_id();
|
|
||||||
$order_count = wc_get_customer_order_count($user_id);
|
|
||||||
|
|
||||||
return $order_count > 0 ? 'returning' : 'new';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ruleMatches($rule, $product = null) {
|
private function ruleMatches($rule, $product = null) {
|
||||||
|
if (!$rule->enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rule->usage_limit > 0 && $this->trackingService->getRuleUsageCount($rule->id) >= $rule->usage_limit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($rule->user_roles) && is_array($rule->user_roles)) {
|
||||||
|
if (!$this->userHasAllowedRole($rule->user_roles)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->isRuleActive($rule)) {
|
if (!$this->isRuleActive($rule)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (empty($rule->conditions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($rule->conditions as $condition) {
|
foreach ($rule->conditions as $condition) {
|
||||||
if (!$this->evaluateCondition($condition, $product)) {
|
if (!$this->evaluateCondition($condition, $product)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -93,10 +144,6 @@ class PricingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function isRuleActive($rule) {
|
private function isRuleActive($rule) {
|
||||||
if (!$rule->enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$now = current_time('Y-m-d H:i:s');
|
$now = current_time('Y-m-d H:i:s');
|
||||||
|
|
||||||
if (!empty($rule->start_date) && $now < $rule->start_date) {
|
if (!empty($rule->start_date) && $now < $rule->start_date) {
|
||||||
@@ -134,6 +181,32 @@ class PricingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getUserType() {
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
return 'guest';
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
$order_count = wc_get_customer_order_count($user_id);
|
||||||
|
|
||||||
|
return $order_count > 0 ? 'returning' : 'new';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function userHasAllowedRole($roles) {
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = wp_get_current_user();
|
||||||
|
foreach ($roles as $role) {
|
||||||
|
if (in_array($role, $user->roles, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private function getCartTotal() {
|
private function getCartTotal() {
|
||||||
if (!WC()->cart) {
|
if (!WC()->cart) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -170,20 +243,9 @@ class PricingService {
|
|||||||
private function applyActions($rule, $price) {
|
private function applyActions($rule, $price) {
|
||||||
foreach ($rule->actions as $action) {
|
foreach ($rule->actions as $action) {
|
||||||
$price = $this->applyAction($action, $price);
|
$price = $this->applyAction($action, $price);
|
||||||
if (($action['type'] ?? '') === 'free_shipping') {
|
|
||||||
$this->freeShipping = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $price;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ruleHasFreeShipping($rule) {
|
return $price;
|
||||||
foreach ($rule->actions as $action) {
|
|
||||||
if (($action['type'] ?? '') === 'free_shipping') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyAction($action, $price) {
|
private function applyAction($action, $price) {
|
||||||
@@ -207,4 +269,29 @@ class PricingService {
|
|||||||
return $price;
|
return $price;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function enforceLimits($originalPrice, $price, array $settings) {
|
||||||
|
$minPrice = max(0, floatval($settings['min_product_price']));
|
||||||
|
$price = max($price, $minPrice);
|
||||||
|
|
||||||
|
$maxDiscountPercent = floatval($settings['max_discount_percent']);
|
||||||
|
if ($maxDiscountPercent > 0 && $maxDiscountPercent < 100) {
|
||||||
|
$limit = $originalPrice * ($maxDiscountPercent / 100);
|
||||||
|
$price = max($originalPrice - $limit, $price);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function estimateRuleDiscount($rule, $price) {
|
||||||
|
foreach ($rule->actions as $action) {
|
||||||
|
if (($action['type'] ?? '') === 'discount_percent') {
|
||||||
|
return $price * floatval($action['value']) / 100;
|
||||||
|
}
|
||||||
|
if (($action['type'] ?? '') === 'discount_fixed') {
|
||||||
|
return floatval($action['value']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
app/Services/TrackingService.php
Normal file
137
app/Services/TrackingService.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Services;
|
||||||
|
|
||||||
|
use Sodino\Repositories\EventRepository;
|
||||||
|
|
||||||
|
class TrackingService {
|
||||||
|
private $eventRepository;
|
||||||
|
private $loggedEvents = [];
|
||||||
|
|
||||||
|
public function __construct(EventRepository $eventRepository) {
|
||||||
|
$this->eventRepository = $eventRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function trackProductView($product_id) {
|
||||||
|
if (!$product_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$session_key = 'sodino_viewed_' . intval($product_id);
|
||||||
|
if ($this->hasLogged($session_key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logEvent('product_view', [
|
||||||
|
'product_id' => $product_id,
|
||||||
|
]);
|
||||||
|
$this->markLogged($session_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function trackAddToCart($product_id, $variation_id = null, $quantity = 1) {
|
||||||
|
if (!$product_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logEvent('add_to_cart', [
|
||||||
|
'product_id' => $product_id,
|
||||||
|
'variation_id' => $variation_id,
|
||||||
|
'value' => floatval($quantity),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function trackCheckoutStart() {
|
||||||
|
$this->logEvent('checkout_start', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function trackPurchase($order_id) {
|
||||||
|
if (!$order_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = wc_get_order($order_id);
|
||||||
|
if (!$order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = floatval($order->get_total());
|
||||||
|
$discount = 0;
|
||||||
|
if (method_exists($order, 'get_total_discount')) {
|
||||||
|
$discount = floatval($order->get_total_discount());
|
||||||
|
} else {
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$discount += floatval($item->get_subtotal()) - floatval($item->get_total());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logEvent('purchase', [
|
||||||
|
'value' => $total,
|
||||||
|
'discount_value' => max(0, $discount),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recordDiscountApplied($product, $original_price, $discounted_price, $rule_id = null) {
|
||||||
|
if (!$product || $original_price <= 0 || $discounted_price >= $original_price) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product_id = $product->get_id();
|
||||||
|
$variation_id = $product->is_type('variation') ? $product_id : 0;
|
||||||
|
$discount_value = round($original_price - $discounted_price, 2);
|
||||||
|
|
||||||
|
$key = 'discount_applied_' . $product_id . '_' . $rule_id . '_' . $discount_value;
|
||||||
|
if ($this->hasLogged($key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logEvent('discount_applied', [
|
||||||
|
'product_id' => $product_id,
|
||||||
|
'variation_id' => $variation_id,
|
||||||
|
'rule_id' => $rule_id,
|
||||||
|
'value' => $discounted_price,
|
||||||
|
'discount_value' => $discount_value,
|
||||||
|
]);
|
||||||
|
$this->markLogged($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRuleUsageCount($rule_id) {
|
||||||
|
return $this->eventRepository->getRuleUsageCount($rule_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function logEvent($type, array $data = []) {
|
||||||
|
$event = [
|
||||||
|
'event_type' => $type,
|
||||||
|
'product_id' => isset($data['product_id']) ? intval($data['product_id']) : null,
|
||||||
|
'variation_id' => isset($data['variation_id']) ? intval($data['variation_id']) : null,
|
||||||
|
'user_id' => get_current_user_id() ?: null,
|
||||||
|
'session_id' => $this->getSessionId(),
|
||||||
|
'rule_id' => isset($data['rule_id']) ? intval($data['rule_id']) : null,
|
||||||
|
'value' => isset($data['value']) ? floatval($data['value']) : 0,
|
||||||
|
'discount_value' => isset($data['discount_value']) ? floatval($data['discount_value']) : 0,
|
||||||
|
'metadata' => isset($data['metadata']) ? wp_json_encode($data['metadata']) : null,
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->eventRepository->insert($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSessionId() {
|
||||||
|
if (function_exists('WC') && WC()->session) {
|
||||||
|
$session_id = WC()->session->get('sodino_session_id');
|
||||||
|
if (!$session_id) {
|
||||||
|
$session_id = uniqid('sodino_', true);
|
||||||
|
WC()->session->set('sodino_session_id', $session_id);
|
||||||
|
}
|
||||||
|
return $session_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'guest_' . md5($_SERVER['REMOTE_ADDR'] . '|' . $_SERVER['HTTP_USER_AGENT']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasLogged($key) {
|
||||||
|
return isset($this->loggedEvents[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markLogged($key) {
|
||||||
|
$this->loggedEvents[$key] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
149
app/Services/UpsellService.php
Normal file
149
app/Services/UpsellService.php
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
namespace Sodino\Services;
|
||||||
|
|
||||||
|
use Sodino\Repositories\UpsellRepository;
|
||||||
|
|
||||||
|
class UpsellService {
|
||||||
|
private $upsellRepository;
|
||||||
|
private $cache = null;
|
||||||
|
|
||||||
|
public function __construct(UpsellRepository $upsellRepository) {
|
||||||
|
$this->upsellRepository = $upsellRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActiveUpsells() {
|
||||||
|
if ($this->cache === null) {
|
||||||
|
$this->cache = $this->upsellRepository->getActive();
|
||||||
|
}
|
||||||
|
return $this->cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMatchingUpsells($cart) {
|
||||||
|
if (!$cart || $cart->is_empty()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$matches = [];
|
||||||
|
foreach ($this->getActiveUpsells() as $upsell) {
|
||||||
|
if ($this->cartMatchesTrigger($upsell, $cart) && !$this->isProductAlreadyInCart($cart, $upsell->target_product_id)) {
|
||||||
|
$matches[] = $upsell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($matches, function ($a, $b) {
|
||||||
|
return $b->priority <=> $a->priority;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyUpsellDiscount($product, $upsell) {
|
||||||
|
if (!$product || !$upsell) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$price = floatval($product->get_price());
|
||||||
|
if ($upsell->discount_type === 'percentage') {
|
||||||
|
return max(0, $price * (1 - floatval($upsell->discount_value) / 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($upsell->discount_type === 'fixed') {
|
||||||
|
return max(0, $price - floatval($upsell->discount_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTriggerLabel($upsell) {
|
||||||
|
switch ($upsell->trigger_type) {
|
||||||
|
case 'product':
|
||||||
|
return __('محصول خاص', 'sodino');
|
||||||
|
case 'category':
|
||||||
|
return __('دستهبندی', 'sodino');
|
||||||
|
case 'cart_total':
|
||||||
|
return __('مبلغ سبد خرید', 'sodino');
|
||||||
|
default:
|
||||||
|
return __('نامشخص', 'sodino');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDiscountLabel($upsell) {
|
||||||
|
if ($upsell->discount_type === 'fixed') {
|
||||||
|
return sprintf('%s تومان', number_format_i18n($upsell->discount_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($upsell->discount_type === 'percentage') {
|
||||||
|
return sprintf('%s %%', esc_html($upsell->discount_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return __('بدون تخفیف', 'sodino');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cartMatchesTrigger($upsell, $cart) {
|
||||||
|
if (!$upsell->isActive()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$triggerType = $upsell->trigger_type;
|
||||||
|
$triggerValue = $upsell->trigger_value;
|
||||||
|
|
||||||
|
switch ($triggerType) {
|
||||||
|
case 'product':
|
||||||
|
return $this->cartContainsProduct($cart, intval($triggerValue));
|
||||||
|
case 'category':
|
||||||
|
return $this->cartContainsCategory($cart, intval($triggerValue));
|
||||||
|
case 'cart_total':
|
||||||
|
return floatval($cart->get_cart_contents_total()) >= floatval($triggerValue);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cartContainsProduct($cart, $productId) {
|
||||||
|
if (!$productId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($cart->get_cart() as $cartItem) {
|
||||||
|
if ((int) $cartItem['product_id'] === $productId || (int) $cartItem['variation_id'] === $productId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cartContainsCategory($cart, $categoryId) {
|
||||||
|
if (!$categoryId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($cart->get_cart() as $cartItem) {
|
||||||
|
$product = wc_get_product($cartItem['product_id']);
|
||||||
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$terms = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'ids']);
|
||||||
|
if (is_array($terms) && in_array($categoryId, $terms, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isProductAlreadyInCart($cart, $productId) {
|
||||||
|
if (!$productId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($cart->get_cart() as $cartItem) {
|
||||||
|
if ((int) $cartItem['product_id'] === $productId || (int) $cartItem['variation_id'] === $productId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ function sodino_create_tables() {
|
|||||||
conditions longtext NOT NULL,
|
conditions longtext NOT NULL,
|
||||||
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,
|
||||||
|
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,
|
||||||
@@ -33,16 +35,34 @@ function sodino_create_tables() {
|
|||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
|
|
||||||
|
// Events table
|
||||||
|
$events_table = $wpdb->prefix . 'sodino_events';
|
||||||
|
$events_sql = "CREATE TABLE $events_table (
|
||||||
|
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||||||
|
event_type varchar(100) NOT NULL,
|
||||||
|
product_id mediumint(9) DEFAULT NULL,
|
||||||
|
variation_id mediumint(9) DEFAULT NULL,
|
||||||
|
user_id bigint(20) DEFAULT NULL,
|
||||||
|
session_id varchar(255) DEFAULT NULL,
|
||||||
|
rule_id mediumint(9) DEFAULT NULL,
|
||||||
|
value decimal(10,2) DEFAULT 0,
|
||||||
|
discount_value decimal(10,2) DEFAULT 0,
|
||||||
|
metadata longtext DEFAULT NULL,
|
||||||
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
// Upsell table
|
// Upsell table
|
||||||
$upsell_table = $wpdb->prefix . 'sodino_upsells';
|
$upsell_table = $wpdb->prefix . 'sodino_upsells';
|
||||||
$upsell_sql = "CREATE TABLE $upsell_table (
|
$upsell_sql = "CREATE TABLE $upsell_table (
|
||||||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||||||
name varchar(255) NOT NULL,
|
title varchar(255) NOT NULL,
|
||||||
triggers longtext NOT NULL,
|
trigger_type varchar(50) NOT NULL,
|
||||||
suggestions longtext NOT NULL,
|
trigger_value varchar(255) NOT NULL,
|
||||||
|
target_product_id bigint(20) NOT NULL DEFAULT 0,
|
||||||
discount_type varchar(50) DEFAULT 'percentage',
|
discount_type varchar(50) DEFAULT 'percentage',
|
||||||
discount_value varchar(50) DEFAULT '0',
|
discount_value decimal(10,2) DEFAULT 0,
|
||||||
enabled tinyint(1) DEFAULT 1,
|
status tinyint(1) DEFAULT 1,
|
||||||
priority int(11) NOT NULL DEFAULT 10,
|
priority int(11) NOT NULL DEFAULT 10,
|
||||||
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,
|
||||||
@@ -51,8 +71,9 @@ function sodino_create_tables() {
|
|||||||
|
|
||||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||||
dbDelta($rules_sql);
|
dbDelta($rules_sql);
|
||||||
|
dbDelta($events_sql);
|
||||||
dbDelta($upsell_sql);
|
dbDelta($upsell_sql);
|
||||||
|
|
||||||
// Add version option
|
// Add version option
|
||||||
add_option('sodino_db_version', '1.1');
|
add_option('sodino_db_version', '1.2');
|
||||||
}
|
}
|
||||||
53
public/hooks/analytics-hooks.php
Normal file
53
public/hooks/analytics-hooks.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use Sodino\Repositories\EventRepository;
|
||||||
|
use Sodino\Services\TrackingService;
|
||||||
|
|
||||||
|
function sodino_get_tracking_service() {
|
||||||
|
static $service = null;
|
||||||
|
|
||||||
|
if ($service === null) {
|
||||||
|
$eventRepository = new EventRepository();
|
||||||
|
$service = new TrackingService($eventRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $service;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('template_redirect', 'sodino_track_product_view');
|
||||||
|
add_action('woocommerce_add_to_cart', 'sodino_track_add_to_cart', 10, 6);
|
||||||
|
add_action('woocommerce_before_checkout_form', 'sodino_track_checkout_start');
|
||||||
|
add_action('woocommerce_thankyou', 'sodino_track_purchase', 10, 1);
|
||||||
|
|
||||||
|
function sodino_track_product_view() {
|
||||||
|
if (is_admin() || !is_singular('product')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product_id = get_the_ID();
|
||||||
|
if (!$product_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sodino_get_tracking_service()->trackProductView($product_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sodino_track_add_to_cart($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
|
||||||
|
sodino_get_tracking_service()->trackAddToCart($product_id, $variation_id, $quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sodino_track_checkout_start() {
|
||||||
|
if (is_admin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sodino_get_tracking_service()->trackCheckoutStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sodino_track_purchase($order_id) {
|
||||||
|
sodino_get_tracking_service()->trackPurchase($order_id);
|
||||||
|
}
|
||||||
@@ -5,12 +5,16 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use Sodino\Services\PricingService;
|
use Sodino\Services\PricingService;
|
||||||
|
use Sodino\Services\TrackingService;
|
||||||
use Sodino\Repositories\RuleRepository;
|
use Sodino\Repositories\RuleRepository;
|
||||||
|
use Sodino\Repositories\EventRepository;
|
||||||
|
|
||||||
// Initialize pricing service
|
// Initialize pricing service
|
||||||
global $sodino_pricing_service;
|
global $sodino_pricing_service;
|
||||||
$ruleRepository = new RuleRepository();
|
$ruleRepository = new RuleRepository();
|
||||||
$sodino_pricing_service = new PricingService($ruleRepository);
|
$eventRepository = new EventRepository();
|
||||||
|
$trackingService = new TrackingService($eventRepository);
|
||||||
|
$sodino_pricing_service = new PricingService($ruleRepository, $trackingService);
|
||||||
|
|
||||||
// Hook into WooCommerce price filter
|
// Hook into WooCommerce price filter
|
||||||
add_filter('woocommerce_product_get_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
add_filter('woocommerce_product_get_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
||||||
|
|||||||
91
public/hooks/upsell-hooks.php
Normal file
91
public/hooks/upsell-hooks.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use Sodino\Repositories\UpsellRepository;
|
||||||
|
use Sodino\Services\UpsellService;
|
||||||
|
|
||||||
|
global $sodino_upsell_service;
|
||||||
|
$upsellRepository = new UpsellRepository();
|
||||||
|
$sodino_upsell_service = new UpsellService($upsellRepository);
|
||||||
|
|
||||||
|
add_action('woocommerce_before_cart', 'sodino_render_upsell_suggestions');
|
||||||
|
|
||||||
|
function sodino_render_upsell_suggestions() {
|
||||||
|
if (is_admin() || !is_cart()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = wp_parse_args(get_option('sodino_settings', []), [
|
||||||
|
'upsell_enabled' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($settings['upsell_enabled'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $sodino_upsell_service;
|
||||||
|
if (!isset($sodino_upsell_service)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart = WC()->cart;
|
||||||
|
if (!$cart || $cart->is_empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$upsells = $sodino_upsell_service->getMatchingUpsells($cart);
|
||||||
|
if (empty($upsells)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="sodino-upsell-panel bg-white rounded-2xl border border-gray-200 p-6 mb-8 shadow-sm">';
|
||||||
|
echo '<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">';
|
||||||
|
echo '<div>';
|
||||||
|
echo '<p class="text-sm font-semibold text-blue-600">' . esc_html__('پیشنهاد ویژه آپسل', 'sodino') . '</p>';
|
||||||
|
echo '<h2 class="mt-2 text-xl font-bold text-gray-900">' . esc_html__('این محصول را همراه خرید خود با تخفیف ویژه دریافت کنید', 'sodino') . '</h2>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '<span class="inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700">' . count($upsells) . ' ' . esc_html__('پیشنهاد فعال', 'sodino') . '</span>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '<div class="mt-6 grid gap-4 lg:grid-cols-'.min(2, count($upsells)).'">';
|
||||||
|
|
||||||
|
foreach ($upsells as $upsell) {
|
||||||
|
$product = wc_get_product($upsell->target_product_id);
|
||||||
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$discountedPrice = $sodino_upsell_service->applyUpsellDiscount($product, $upsell);
|
||||||
|
$originalPrice = floatval($product->get_price());
|
||||||
|
$priceHtml = wc_price($discountedPrice);
|
||||||
|
if ($discountedPrice < $originalPrice) {
|
||||||
|
$priceHtml .= ' <span class="mr-2 text-sm text-gray-500 line-through">' . wc_price($originalPrice) . '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$addToCartUrl = esc_url(add_query_arg('add-to-cart', $product->get_id(), wc_get_cart_url()));
|
||||||
|
$image = $product->get_image('woocommerce_thumbnail', ['class' => 'h-20 w-20 rounded-xl object-cover']);
|
||||||
|
|
||||||
|
echo '<div class="rounded-2xl border border-gray-200 p-5 bg-gray-50">';
|
||||||
|
echo '<div class="flex gap-4">';
|
||||||
|
echo '<div class="flex-shrink-0">' . $image . '</div>';
|
||||||
|
echo '<div class="flex-1">';
|
||||||
|
echo '<p class="text-sm font-medium text-gray-700">' . esc_html($upsell->title) . '</p>';
|
||||||
|
echo '<h3 class="mt-2 text-lg font-semibold text-gray-900">' . esc_html($product->get_name()) . '</h3>';
|
||||||
|
echo '<div class="mt-3 flex items-center gap-3">';
|
||||||
|
echo '<span class="rounded-full bg-green-50 px-3 py-1 text-sm font-medium text-green-700">' . esc_html($sodino_upsell_service->getDiscountLabel($upsell)) . '</span>';
|
||||||
|
echo '<span class="text-sm text-gray-600">' . esc_html($sodino_upsell_service->getTriggerLabel($upsell)) . '</span>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '<div class="mt-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">';
|
||||||
|
echo '<div class="text-lg font-semibold text-gray-900">' . $priceHtml . '</div>';
|
||||||
|
echo '<a href="' . $addToCartUrl . '" class="inline-flex items-center justify-center rounded-full bg-blue-600 px-5 py-3 text-sm font-semibold text-white hover:bg-blue-700">' . esc_html__('افزودن به سبد', 'sodino') . '</a>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
32
sodino.php
32
sodino.php
@@ -67,6 +67,9 @@ register_deactivation_hook(__FILE__, 'sodino_deactivate');
|
|||||||
function sodino_deactivate() {
|
function sodino_deactivate() {
|
||||||
// Flush rewrite rules
|
// Flush rewrite rules
|
||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
|
|
||||||
|
// Clear analytics cron
|
||||||
|
wp_clear_scheduled_hook('sodino_hourly_analytics');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bootstrap the plugin
|
// Bootstrap the plugin
|
||||||
@@ -84,12 +87,41 @@ function sodino_init() {
|
|||||||
|
|
||||||
// Initialize public hooks
|
// Initialize public hooks
|
||||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/analytics-hooks.php';
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/upsell-hooks.php';
|
||||||
|
|
||||||
|
// Schedule analytics aggregation if needed
|
||||||
|
sodino_schedule_analytics();
|
||||||
|
|
||||||
// Load text domain
|
// Load text domain
|
||||||
load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
|
load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
|
||||||
}
|
}
|
||||||
add_action('plugins_loaded', 'sodino_init');
|
add_action('plugins_loaded', 'sodino_init');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule analytics cron job
|
||||||
|
*/
|
||||||
|
function sodino_schedule_analytics() {
|
||||||
|
if (!wp_next_scheduled('sodino_hourly_analytics')) {
|
||||||
|
wp_schedule_event(time(), 'hourly', 'sodino_hourly_analytics');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run analytics aggregation cron job
|
||||||
|
*/
|
||||||
|
function sodino_run_analytics_aggregation() {
|
||||||
|
if (!class_exists('Sodino\Services\AnalyticsService')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventRepository = new Sodino\Repositories\EventRepository();
|
||||||
|
$ruleRepository = new Sodino\Repositories\RuleRepository();
|
||||||
|
$analyticsService = new Sodino\Services\AnalyticsService($eventRepository, $ruleRepository);
|
||||||
|
$analyticsService->primeCache();
|
||||||
|
}
|
||||||
|
add_action('sodino_hourly_analytics', 'sodino_run_analytics_aggregation');
|
||||||
|
|
||||||
// WooCommerce missing notice
|
// WooCommerce missing notice
|
||||||
function sodino_woocommerce_missing_notice() {
|
function sodino_woocommerce_missing_notice() {
|
||||||
?>
|
?>
|
||||||
|
|||||||
Reference in New Issue
Block a user