diff --git a/admin/admin.php b/admin/admin.php index 87340f8..222119e 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -25,6 +25,86 @@ $dashboardController = new DashboardController($eventRepository, $ruleRepository $settingsController = new SettingsController(); $adminController = new AdminController($ruleRepository, $upsellRepository, $bannerRepository); +function sodino_admin_notice_key() { + return 'sodino_admin_notice_' . get_current_user_id(); +} + +function sodino_old_input_key() { + return 'sodino_old_input_' . get_current_user_id(); +} + +function sodino_get_admin_notice($delete = false) { + $notice = get_transient(sodino_admin_notice_key()); + + if (!$notice) { + $notice = get_transient('sodino_admin_notice'); + } + + if ($notice && $delete) { + delete_transient(sodino_admin_notice_key()); + delete_transient('sodino_admin_notice'); + } + + return is_array($notice) ? $notice : null; +} + +function sodino_get_old_input($delete = false) { + $old = get_transient(sodino_old_input_key()); + + if (is_array($old) && isset($old['_sodino_page'])) { + $current_page = isset($_GET['page']) ? sanitize_key($_GET['page']) : ''; + if ($current_page !== sanitize_key($old['_sodino_page'])) { + return []; + } + } + + if ($old && $delete) { + delete_transient(sodino_old_input_key()); + } + + return is_array($old) ? $old : []; +} + +function sodino_old_input($key, $default = '') { + static $old = null; + + if ($old === null) { + $old = sodino_get_old_input(false); + } + + if (strpos($key, '.') === false) { + return array_key_exists($key, $old) ? $old[$key] : $default; + } + + $value = $old; + foreach (explode('.', $key) as $part) { + if (!is_array($value) || !array_key_exists($part, $value)) { + return $default; + } + $value = $value[$part]; + } + + return $value; +} + +function sodino_render_admin_notice() { + $notice = sodino_get_admin_notice(true); + if (!$notice) { + return; + } + + $type = $notice['type'] ?? 'success'; + $class = $type === 'error' ? 'sodino-form-notice sodino-form-notice-error' : 'sodino-form-notice sodino-form-notice-success'; + $title = $type === 'error' ? __('خطای فرم', 'sodino') : __('عملیات موفق', 'sodino'); + + printf( + '', + esc_attr($class), + esc_html($title), + esc_html($notice['message'] ?? '') + ); +} + /** * Add admin menu */ @@ -102,6 +182,15 @@ add_action('admin_menu', function() use ($adminController) { [$adminController, 'addBannerPage'] ); + add_submenu_page( + 'sodino-dashboard', + __('قیمت رقبا (به‌زودی)', 'sodino'), + __('قیمت رقبا (به‌زودی)', 'sodino'), + 'manage_options', + 'sodino-competitor-price', + [$adminController, 'competitorPricePage'] + ); + add_submenu_page( 'sodino-dashboard', __('تنظیمات', 'sodino'), @@ -125,16 +214,12 @@ add_action('admin_enqueue_scripts', function($hook) { return; } - // Enqueue Tailwind via CDN - wp_enqueue_script('sodino-tailwind', 'https://cdn.tailwindcss.com', [], SODINO_VERSION); - // Admin CSS wp_enqueue_style('sodino-admin', plugin_dir_url(__FILE__) . 'css/admin.css', [], SODINO_VERSION); // Dashboard specific scripts if (strpos($hook, 'sodino-dashboard') !== false || strpos($hook, 'sodino_page_sodino-dashboard') !== false) { - wp_enqueue_script('sodino-chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); - wp_enqueue_script('sodino-dashboard-js', plugin_dir_url(__FILE__) . 'js/dashboard.js', ['sodino-chart-js'], SODINO_VERSION, true); + wp_enqueue_script('sodino-dashboard-js', plugin_dir_url(__FILE__) . 'js/dashboard.js', [], SODINO_VERSION, true); } // Upsell specific scripts @@ -185,7 +270,12 @@ add_action('admin_init', function() use ($ruleController, $settingsController, $ * Show admin notices */ add_action('admin_notices', function() { - $notice = get_transient('sodino_admin_notice'); + $page = isset($_GET['page']) ? sanitize_key($_GET['page']) : ''; + if (strpos($page, 'sodino') === 0) { + return; + } + + $notice = sodino_get_admin_notice(true); if ($notice) { $class = $notice['type'] === 'error' ? 'notice-error' : 'notice-success'; @@ -194,6 +284,5 @@ add_action('admin_notices', function() { esc_attr($class), esc_html($notice['message']) ); - delete_transient('sodino_admin_notice'); } }); diff --git a/admin/class-rules-list-table.php b/admin/class-rules-list-table.php index e4e1e96..2f89f0a 100644 --- a/admin/class-rules-list-table.php +++ b/admin/class-rules-list-table.php @@ -25,8 +25,8 @@ class Sodino_Rules_List_Table extends WP_List_Table { return [ 'cb' => '', 'name' => __('عنوان قانون', 'sodino'), - 'condition_type' => __('نوع کاربر', 'sodino'), - 'action_value' => __('درصد تخفیف', 'sodino'), + 'condition_type' => __('شرط', 'sodino'), + 'action_value' => __('عملیات', 'sodino'), 'enabled' => __('وضعیت', 'sodino'), 'actions' => __('عملیات', 'sodino'), ]; @@ -69,15 +69,30 @@ class Sodino_Rules_List_Table extends WP_List_Table { } public function column_condition_type($item) { - $value = __('کاربر جدید', 'sodino'); - if ($item->condition_value === 'returning') { - $value = __('کاربر بازگشتی', 'sodino'); - } - return esc_html($value); + $labels = [ + 'user_type' => __('نوع کاربر', 'sodino'), + 'product_category' => __('دسته‌بندی محصول', 'sodino'), + 'product_ids' => __('محصولات خاص', 'sodino'), + 'cart_total_min' => __('حداقل مبلغ سبد', 'sodino'), + 'cart_total_max' => __('حداکثر مبلغ سبد', 'sodino'), + 'cart_item_count_min' => __('حداقل تعداد سبد', 'sodino'), + 'cart_item_count_max' => __('حداکثر تعداد سبد', 'sodino'), + ]; + + $type = $labels[$item->condition_type] ?? $item->condition_type; + return esc_html(sprintf('%s: %s', $type, $item->condition_value)); } public function column_action_value($item) { - return sprintf('%s %%', esc_html($item->action_value)); + $labels = [ + 'discount_percent' => __('درصد تخفیف', 'sodino'), + 'discount_fixed' => __('تخفیف ثابت', 'sodino'), + 'set_price' => __('قیمت ثابت', 'sodino'), + 'free_shipping' => __('ارسال رایگان', 'sodino'), + ]; + + $type = $labels[$item->action_type] ?? $item->action_type; + return esc_html(sprintf('%s: %s', $type, $item->action_value)); } public function column_enabled($item) { diff --git a/admin/components/layout.php b/admin/components/layout.php index e5964ad..fefa89c 100644 --- a/admin/components/layout.php +++ b/admin/components/layout.php @@ -14,6 +14,7 @@ function sodino_admin_layout($current_page, $content_callback) {
+
diff --git a/admin/components/sidebar.php b/admin/components/sidebar.php index 8ce478e..b4c4b27 100644 --- a/admin/components/sidebar.php +++ b/admin/components/sidebar.php @@ -13,6 +13,7 @@ $menu_items = [ 'sodino-add-upsell' => __('افزودن آپسل', 'sodino'), 'sodino-banners' => __('بنرهای هوشمند', 'sodino'), 'sodino-add-banner' => __('افزودن بنر', 'sodino'), + 'sodino-competitor-price' => __('قیمت رقبا (به‌زودی)', 'sodino'), 'sodino-settings' => __('تنظیمات', 'sodino'), ]; ?> diff --git a/admin/css/admin.css b/admin/css/admin.css index 50c73be..87464e9 100644 --- a/admin/css/admin.css +++ b/admin/css/admin.css @@ -385,3 +385,383 @@ #sodino-app .wp-list-table .column-title { font-weight: 600; } + +#sodino-app .sodino-form-notice { + border-radius: 8px; + margin-bottom: 20px; + padding: 16px 18px; + border: 1px solid; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06); +} + +#sodino-app .sodino-form-notice strong { + display: block; + margin-bottom: 6px; + font-size: 0.95rem; +} + +#sodino-app .sodino-form-notice p { + margin: 0; + font-size: 0.92rem; +} + +#sodino-app .sodino-form-notice-error { + background: #fff1f2; + border-color: #fecdd3; + color: #9f1239; +} + +#sodino-app .sodino-form-notice-success { + background: #ecfdf3; + border-color: #bbf7d0; + color: #166534; +} + +/* Local utility layer for the Sodino admin. This replaces the previous Tailwind CDN dependency. */ +#sodino-app { + min-height: 100vh; + padding: 28px 18px 48px; + background: #f4f7fb; + color: #172033; + font-family: Tahoma, Arial, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +#sodino-app *, +#sodino-app *::before, +#sodino-app *::after { + box-sizing: border-box; +} + +#sodino-app h1, +#sodino-app h2, +#sodino-app h3, +#sodino-app h4, +#sodino-app p { + margin-top: 0; + letter-spacing: 0; +} + +#sodino-app a { + text-decoration: none; +} + +#sodino-app input, +#sodino-app select, +#sodino-app textarea, +#sodino-app button { + font: inherit; +} + +#sodino-app .min-h-screen { min-height: 100vh; } +#sodino-app .max-w-7xl { max-width: 1280px; } +#sodino-app .mx-auto { margin-left: auto; margin-right: auto; } +#sodino-app .min-w-0 { min-width: 0; } +#sodino-app .min-w-full { min-width: 100%; } +#sodino-app .w-full { width: 100%; } +#sodino-app .w-64 { width: 16rem; } +#sodino-app .w-4 { width: 1rem; } +#sodino-app .w-5 { width: 1.25rem; } +#sodino-app .h-4 { height: 1rem; } +#sodino-app .h-5 { height: 1.25rem; } +#sodino-app .h-64 { height: 16rem; } +#sodino-app .h-full { height: 100%; } +#sodino-app .block { display: block; } +#sodino-app .hidden { display: none; } +#sodino-app .inline-flex { display: inline-flex; } +#sodino-app .flex { display: flex; } +#sodino-app .grid { display: grid; } +#sodino-app .flex-1 { flex: 1 1 0%; } +#sodino-app .flex-col { flex-direction: column; } +#sodino-app .flex-shrink-0 { flex-shrink: 0; } +#sodino-app .items-center { align-items: center; } +#sodino-app .justify-between { justify-content: space-between; } +#sodino-app .justify-center { justify-content: center; } +#sodino-app .justify-end { justify-content: flex-end; } +#sodino-app .gap-2 { gap: 0.5rem; } +#sodino-app .gap-3 { gap: 0.75rem; } +#sodino-app .gap-4 { gap: 1rem; } +#sodino-app .gap-6 { gap: 1.5rem; } +#sodino-app .gap-8 { gap: 2rem; } +#sodino-app .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } +#sodino-app .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +#sodino-app .relative { position: relative; } +#sodino-app .absolute { position: absolute; } +#sodino-app .z-10 { z-index: 10; } +#sodino-app .overflow-hidden { overflow: hidden; } +#sodino-app .overflow-x-auto { overflow-x: auto; } +#sodino-app .cursor-pointer { cursor: pointer; } + +#sodino-app .p-4 { padding: 1rem; } +#sodino-app .p-5 { padding: 1.25rem; } +#sodino-app .p-6 { padding: 1.5rem; } +#sodino-app .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; } +#sodino-app .px-4 { padding-left: 1rem; padding-right: 1rem; } +#sodino-app .px-5 { padding-left: 1.25rem; padding-right: 1.25rem; } +#sodino-app .px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } +#sodino-app .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; } +#sodino-app .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; } +#sodino-app .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } +#sodino-app .py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; } +#sodino-app .py-8 { padding-top: 2rem; padding-bottom: 2rem; } +#sodino-app .pb-6 { padding-bottom: 1.5rem; } +#sodino-app .pt-6 { padding-top: 1.5rem; } +#sodino-app .mt-1 { margin-top: 0.25rem; } +#sodino-app .mt-2 { margin-top: 0.5rem; } +#sodino-app .mt-8 { margin-top: 2rem; } +#sodino-app .mb-2 { margin-bottom: 0.5rem; } +#sodino-app .mb-4 { margin-bottom: 1rem; } +#sodino-app .mb-6 { margin-bottom: 1.5rem; } +#sodino-app .mb-8 { margin-bottom: 2rem; } +#sodino-app .mr-2 { margin-right: 0.5rem; } +#sodino-app .-ml-1 { margin-left: -0.25rem; } +#sodino-app .space-y-2 > * + * { margin-top: 0.5rem; } +#sodino-app .space-y-3 > * + * { margin-top: 0.75rem; } +#sodino-app .space-y-4 > * + * { margin-top: 1rem; } +#sodino-app .space-y-6 > * + * { margin-top: 1.5rem; } +#sodino-app .space-y-8 > * + * { margin-top: 2rem; } + +#sodino-app .rounded { border-radius: 4px; } +#sodino-app .rounded-md, +#sodino-app .rounded-lg, +#sodino-app .rounded-xl, +#sodino-app .rounded-2xl, +#sodino-app .sd-sidebar, +#sodino-app .sd-card, +#sodino-app .sd-chart-card { + border-radius: 8px !important; +} +#sodino-app .rounded-full { border-radius: 9999px; } + +#sodino-app .border { border: 1px solid #d8e0ea; } +#sodino-app .border-b { border-bottom: 1px solid #d8e0ea; } +#sodino-app .border-t { border-top: 1px solid #d8e0ea; } +#sodino-app .border-r-2 { border-right: 2px solid currentColor; } +#sodino-app .border-transparent { border-color: transparent; } +#sodino-app .border-gray-200 { border-color: #d8e0ea; } +#sodino-app .border-gray-300 { border-color: #c7d2df; } +#sodino-app .border-blue-700 { border-color: #1769aa; } +#sodino-app .divide-y > * + * { border-top: 1px solid #d8e0ea; } +#sodino-app .divide-gray-200 > * + * { border-color: #d8e0ea; } + +#sodino-app .bg-white { + background: #ffffff; +} +#sodino-app .bg-gray-50 { + background: #f6f8fb !important; +} +#sodino-app .bg-gray-100 { + background: #eef3f8; +} +#sodino-app .bg-gray-300 { + background: #c7d2df; +} +#sodino-app .bg-blue-50 { + background: #e8f3fb; +} +#sodino-app .bg-blue-600, +#sodino-app .from-blue-600 { + background: #1769aa; +} +#sodino-app .to-blue-700 { + background: #115487; +} +#sodino-app .bg-gradient-to-br { + background: linear-gradient(135deg, #1769aa, #115487); +} +#sodino-app .bg-green-50 { + background: #e9f8ef; +} +#sodino-app .bg-yellow-50 { + background: #fff7df; +} + +#sodino-app .text-xs { font-size: 0.75rem; line-height: 1rem; } +#sodino-app .text-sm { font-size: 0.875rem; line-height: 1.5; } +#sodino-app .text-lg { font-size: 1.125rem; line-height: 1.6; } +#sodino-app .text-xl { font-size: 1.25rem; line-height: 1.5; } +#sodino-app .text-2xl { font-size: 1.5rem; line-height: 1.35; } +#sodino-app .text-3xl { font-size: 1.875rem; line-height: 1.25; } +#sodino-app .font-medium { font-weight: 500; } +#sodino-app .font-semibold { font-weight: 600; } +#sodino-app .font-bold { font-weight: 700; } +#sodino-app .text-right { text-align: right; } +#sodino-app .text-white { color: #ffffff; } +#sodino-app .text-gray-500 { color: #64748b !important; } +#sodino-app .text-gray-600 { color: #475569 !important; } +#sodino-app .text-gray-700 { color: #334155 !important; } +#sodino-app .text-gray-900 { color: #172033 !important; } +#sodino-app .text-blue-600 { color: #1769aa; } +#sodino-app .text-blue-700 { color: #115487; } +#sodino-app .text-green-600 { color: #198754; } +#sodino-app .text-green-700 { color: #146c43; } +#sodino-app .text-red-500 { color: #dc3545; } +#sodino-app .text-red-600 { color: #c82333; } +#sodino-app .text-yellow-700 { color: #946200; } +#sodino-app .opacity-90 { opacity: 0.9; } + +#sodino-app .shadow-sm, +#sodino-app .shadow-lg, +#sodino-app .bg-white.rounded-lg.shadow-sm.border, +#sodino-app .bg-white.rounded-2xl.border { + box-shadow: 0 10px 26px rgba(15, 23, 42, 0.06) !important; +} + +#sodino-app .transition-colors { + transition-property: color, background-color, border-color, box-shadow, transform; +} +#sodino-app .duration-200 { + transition-duration: 160ms; +} +#sodino-app .hover\:bg-gray-50:hover { + background: #eef3f8; +} +#sodino-app .hover\:bg-blue-700:hover { + background: #115487; +} +#sodino-app .hover\:text-gray-900:hover { + color: #172033; +} +#sodino-app .hover\:border-blue-300:hover { + border-color: #7db8df; +} +#sodino-app .focus\:outline-none:focus { + outline: none; +} +#sodino-app .focus\:border-blue-500:focus { + border-color: #2488d1; +} +#sodino-app .focus\:ring-2:focus { + box-shadow: 0 0 0 3px rgba(36, 136, 209, 0.18); +} +#sodino-app .focus\:ring-blue-100:focus, +#sodino-app .focus\:ring-blue-500:focus { + box-shadow: 0 0 0 3px rgba(36, 136, 209, 0.18); +} +#sodino-app .focus\:ring-offset-2:focus { + outline-offset: 2px; +} + +#sodino-app .bg-white.border-b.border-gray-200 { + border: 1px solid #d8e0ea; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 10px 26px rgba(15, 23, 42, 0.05); +} + +#sodino-app aside .bg-white, +#sodino-app main > .bg-white, +#sodino-app .grid > .bg-white, +#sodino-app .bg-white.rounded-lg.shadow-sm.border, +#sodino-app .bg-white.rounded-2xl.border { + border: 1px solid #d8e0ea; + border-radius: 8px !important; + background: #ffffff; +} + +#sodino-app aside { + position: sticky; + top: 42px; + align-self: flex-start; +} + +#sodino-app nav.space-y-2 a { + border: 1px solid transparent; + border-radius: 8px !important; + padding: 0.7rem 0.85rem; +} + +#sodino-app nav.space-y-2 a.bg-blue-50, +#sodino-app nav.space-y-2 a:hover { + background: #e8f3fb; + color: #115487 !important; + border-color: #b9d9ee; +} + +#sodino-app input[type="text"], +#sodino-app input[type="number"], +#sodino-app input[type="url"], +#sodino-app input[type="datetime-local"], +#sodino-app input[type="date"], +#sodino-app select, +#sodino-app textarea { + min-height: 42px; + border-radius: 8px !important; + border: 1px solid #c7d2df !important; + background: #ffffff; + color: #172033; + box-shadow: none; +} + +#sodino-app input[type="checkbox"] { + border-radius: 4px; + border-color: #9fb0c1; +} + +#sodino-app button, +#sodino-app .inline-flex[href], +#sodino-app input[type="submit"] { + border-radius: 8px !important; +} + +#sodino-app canvas { + display: block; +} + +#sodino-app .wp-list-table { + border: 1px solid #d8e0ea; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 10px 26px rgba(15, 23, 42, 0.05); +} + +#sodino-app .wp-list-table thead th { + background: #eef3f8; + color: #334155 !important; + font-weight: 700; +} + +#sodino-app .wp-list-table tbody tr:hover { + background: #f6f8fb; +} + +@media (min-width: 640px) { + #sodino-app .sm\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } + #sodino-app .sm\:flex-row { flex-direction: row; } + #sodino-app .sm\:items-center { align-items: center; } + #sodino-app .sm\:justify-between { justify-content: space-between; } +} + +@media (min-width: 768px) { + #sodino-app .md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } + #sodino-app .md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } + #sodino-app .md\:col-span-2 { grid-column: span 2 / span 2; } +} + +@media (min-width: 1024px) { + #sodino-app .lg\:px-8 { padding-left: 2rem; padding-right: 2rem; } + #sodino-app .lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } + #sodino-app .lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } + #sodino-app .lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } + #sodino-app .lg\:grid-cols-\[280px_1fr\] { grid-template-columns: 280px minmax(0, 1fr); } + #sodino-app .lg\:col-span-2 { grid-column: span 2 / span 2; } + #sodino-app .lg\:flex-row { flex-direction: row; } + #sodino-app .lg\:items-center { align-items: center; } + #sodino-app .lg\:justify-between { justify-content: space-between; } +} + +@media (max-width: 960px) { + #sodino-app { + padding: 18px 10px 36px; + } + + #sodino-app > .max-w-7xl > .flex, + #sodino-app .max-w-7xl > .flex { + flex-direction: column; + } + + #sodino-app aside, + #sodino-app .w-64 { + width: 100%; + position: static; + } +} diff --git a/admin/js/dashboard.js b/admin/js/dashboard.js index e5c8aee..05ca5f5 100644 --- a/admin/js/dashboard.js +++ b/admin/js/dashboard.js @@ -11,165 +11,198 @@ document.addEventListener('DOMContentLoaded', function () { 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)', - }, - }, - }, - }, + function toNumbers(values) { + return (values || []).map(function (value) { + const number = Number(value); + return Number.isFinite(number) ? number : 0; }); } - 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)', - }, - }, - }, - }, + function fitCanvas(canvas) { + const ratio = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.max(1, Math.floor(rect.width * ratio)); + canvas.height = Math.max(1, Math.floor(rect.height * ratio)); + + const ctx = canvas.getContext('2d'); + ctx.setTransform(ratio, 0, 0, ratio, 0, 0); + return { ctx, width: rect.width, height: rect.height }; + } + + function drawGrid(ctx, area, lines) { + ctx.strokeStyle = 'rgba(148, 163, 184, 0.22)'; + ctx.lineWidth = 1; + + for (let i = 0; i <= lines; i++) { + const y = area.top + (area.height / lines) * i; + ctx.beginPath(); + ctx.moveTo(area.left, y); + ctx.lineTo(area.left + area.width, y); + ctx.stroke(); + } + } + + function drawLineChart(canvas, series) { + if (!canvas) { + return; + } + + const fitted = fitCanvas(canvas); + const ctx = fitted.ctx; + const width = fitted.width; + const height = fitted.height; + const area = { left: 18, top: 18, width: width - 36, height: height - 42 }; + const allValues = series.reduce(function (values, item) { + return values.concat(item.values); + }, []); + const max = Math.max(1, ...allValues); + + ctx.clearRect(0, 0, width, height); + drawGrid(ctx, area, 4); + + series.forEach(function (item) { + const values = item.values; + if (!values.length) { + return; + } + + ctx.strokeStyle = item.color; + ctx.fillStyle = item.fill; + ctx.lineWidth = 2.5; + ctx.beginPath(); + + values.forEach(function (value, index) { + const x = area.left + (values.length === 1 ? area.width / 2 : (area.width / (values.length - 1)) * index); + const y = area.top + area.height - ((value / max) * area.height); + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + + ctx.stroke(); + + const lastX = area.left + area.width; + ctx.lineTo(lastX, area.top + area.height); + ctx.lineTo(area.left, area.top + area.height); + ctx.closePath(); + ctx.fill(); }); } - 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, - }, - }, - }, - }, + function drawVerticalBars(canvas, labels, values, colors) { + if (!canvas) { + return; + } + + const fitted = fitCanvas(canvas); + const ctx = fitted.ctx; + const width = fitted.width; + const height = fitted.height; + const area = { left: 24, top: 18, width: width - 48, height: height - 52 }; + const max = Math.max(1, ...values); + const gap = 18; + const barWidth = Math.max(18, (area.width - gap * (values.length - 1)) / Math.max(1, values.length)); + + ctx.clearRect(0, 0, width, height); + drawGrid(ctx, area, 4); + ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillStyle = '#475569'; + + values.forEach(function (value, index) { + const x = area.left + index * (barWidth + gap); + const barHeight = (value / max) * area.height; + const y = area.top + area.height - barHeight; + + ctx.fillStyle = colors[index % colors.length]; + roundRect(ctx, x, y, barWidth, barHeight, 8); + ctx.fill(); + + ctx.fillStyle = '#475569'; + ctx.fillText(labels[index] || '', x + barWidth / 2, height - 16); }); } + + function drawHorizontalBars(canvas, labels, revenue, discount) { + if (!canvas) { + return; + } + + const fitted = fitCanvas(canvas); + const ctx = fitted.ctx; + const width = fitted.width; + const height = fitted.height; + const area = { left: 18, top: 18, width: width - 36, height: height - 36 }; + const rows = Math.max(1, labels.length); + const max = Math.max(1, ...revenue, ...discount); + const rowHeight = area.height / rows; + + ctx.clearRect(0, 0, width, height); + ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; + ctx.textAlign = 'right'; + + labels.forEach(function (label, index) { + const y = area.top + rowHeight * index + 10; + const labelWidth = Math.min(110, area.width * 0.35); + const chartLeft = area.left; + const chartWidth = area.width - labelWidth - 12; + const revenueWidth = (Number(revenue[index] || 0) / max) * chartWidth; + const discountWidth = (Number(discount[index] || 0) / max) * chartWidth; + + ctx.fillStyle = '#475569'; + ctx.fillText(String(label || '').slice(0, 18), width - 18, y + 14); + + ctx.fillStyle = '#0ea5e9'; + roundRect(ctx, chartLeft, y, revenueWidth, 8, 6); + ctx.fill(); + + ctx.fillStyle = '#f59e0b'; + roundRect(ctx, chartLeft, y + 13, discountWidth, 8, 6); + ctx.fill(); + }); + } + + function roundRect(ctx, x, y, width, height, radius) { + const r = Math.min(radius, Math.abs(width) / 2, Math.abs(height) / 2); + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + width - r, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + r); + ctx.lineTo(x + width, y + height - r); + ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height); + ctx.lineTo(x + r, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - r); + ctx.lineTo(x, y + r); + ctx.quadraticCurveTo(x, y, x + r, y); + ctx.closePath(); + } + + function render() { + const sales = dashboardData.salesChart || {}; + drawLineChart(document.getElementById('sodinoSalesChart'), [ + { values: toNumbers(sales.after), color: '#0ea5e9', fill: 'rgba(14, 165, 233, 0.12)' }, + { values: toNumbers(sales.before), color: '#ef4444', fill: 'rgba(239, 68, 68, 0.10)' }, + ]); + + const summary = dashboardData.summary || {}; + drawVerticalBars( + document.getElementById('sodinoDiscountChart'), + [dashboardData.translations.totalDiscount, dashboardData.translations.totalRevenue], + toNumbers([summary.total_discount, summary.total_revenue]), + ['#f59e0b', '#0ea5e9'] + ); + + const rules = dashboardData.rulePerformance || {}; + drawHorizontalBars( + document.getElementById('sodinoRuleChart'), + rules.names || [], + toNumbers(rules.revenue), + toNumbers(rules.discount) + ); + } + + render(); + window.addEventListener('resize', render); }); diff --git a/admin/views/banner-form.php b/admin/views/banner-form.php index 6cbab9a..3028dfb 100644 --- a/admin/views/banner-form.php +++ b/admin/views/banner-form.php @@ -5,6 +5,8 @@ if (!defined('ABSPATH')) { } $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-add-banner'); +$form_content_type = function_exists('sodino_old_input') ? sodino_old_input('content_type', $banner->content_type) : $banner->content_type; +$form_display_type = function_exists('sodino_old_input') ? sodino_old_input('display_type', $banner->display_type) : $banner->display_type; ?>
@@ -22,6 +24,7 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-add-banner');
+