Changeset 3395155
- Timestamp:
- 11/13/2025 03:52:21 PM (3 months ago)
- Location:
- askeet/trunk
- Files:
-
- 4 edited
-
README.txt (modified) (4 diffs)
-
askeet.php (modified) (11 diffs)
-
assets/css/style.css (modified) (3 diffs)
-
assets/js/script.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
askeet/trunk/README.txt
r3340838 r3395155 5 5 Requires at least: 6.2 6 6 Tested up to: 6.8 7 Stable tag: 1.07 Stable tag: 2.0 8 8 License: GPLv2 or later 9 9 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 223 223 * Export functionality (Pro) 224 224 225 = 2.0 = 226 * NEW: Modern subscription management page with pricing cards and one-click upgrades 227 * NEW: Smart pagination - display 50 rows, export up to 1,500 rows per page 228 * IMPROVED: Export limit increased to 10,000 rows with "Export All" button 229 * IMPROVED: Dynamic rows per page selector (20, 50, 100, 500, 1,500) 230 * IMPROVED: Performance notifications for large datasets 231 * FIXED: Upgrade modal display when reaching plan limits 232 * UPDATED: Community links (Discord + Slack) 233 * UPDATED: UI/UX design optimization with better responsiveness 234 * NEW: Add annual payment and date renew 235 225 236 = Upcoming Features = 226 237 … … 237 248 Initial release. Install now to start getting instant insights from your WooCommerce data! 238 249 250 = 2.0 = 251 * NEW: Modern subscription management page with pricing cards and one-click upgrades 252 * NEW: Smart pagination - display 50 rows, export up to 1,500 rows per page 253 * IMPROVED: Export limit increased to 10,000 rows with "Export All" button 254 * IMPROVED: Dynamic rows per page selector (20, 50, 100, 500, 1,500) 255 * IMPROVED: Performance notifications for large datasets 256 * FIXED: Upgrade modal display when reaching plan limits 257 * UPDATED: Community links (Discord + Slack) 258 * UPDATED: UI/UX design optimization with better responsiveness 259 239 260 == Source Code == 240 261 The unminified source code for JavaScript and CSS is included in the /assets/js/ and /assets/css/ folders of this plugin. If you use a minified file, the original source is present alongside it. … … 251 272 * **Website:** [https://www.askeet.ai/](https://www.askeet.ai/) 252 273 * **Support Email:** [email protected] 274 * **Slack Community:** [https://reach-technologies.slack.com/archives/C09S953P6MD](https://reach-technologies.slack.com/archives/C09S953P6MD) 275 * **Discord Server:** [https://discord.com/channels/1438540885758443570/1438540942973079766](https://discord.com/channels/1438540885758443570/1438540942973079766) 253 276 254 277 == Why Choose Askeet? == -
askeet/trunk/askeet.php
r3389781 r3395155 3 3 * Plugin Name: Askeet 4 4 * Description: Askeet is your AI assistant for WooCommerce: generate, run, and analyze SQL queries securely, no code needed, with a modern multilingual interface. 5 * Version: 1.05 * Version: 2.0 6 6 * Author: Reach Technologies 7 7 * License: GPL-2.0+ … … 37 37 update_option('askeet_install_id', wp_generate_password(24, false)); 38 38 } 39 define('ASKEET_VERSION', ' 1.0');39 define('ASKEET_VERSION', '2.0'); 40 40 41 41 $install_id = get_option('askeet_install_id'); … … 105 105 add_action('wp_ajax_askeet_process_query_request', array($this, 'askeet_process_query_request')); 106 106 add_action('wp_ajax_askeet_execute_sql_query', array($this, 'askeet_execute_sql_query')); 107 add_action('wp_ajax_askeet_export_all_results', array($this, 'askeet_export_all_results')); 107 108 add_action('wp_ajax_askeet_save_query_history', array($this, 'askeet_save_query_history')); 108 109 add_action('wp_ajax_askeet_delete_query_history', array($this, 'askeet_delete_query_history')); … … 163 164 } 164 165 wp_enqueue_style('askeet-style', plugins_url('assets/css/style.css', __FILE__)); 165 wp_enqueue_script('askeet-script', plugins_url('assets/js/script.js', __FILE__), array('jquery'), ' 1.0', true);166 wp_enqueue_script('askeet-script', plugins_url('assets/js/script.js', __FILE__), array('jquery'), '2.0', true); 166 167 $current_plan = ''; 167 168 $install_id = get_option('askeet_install_id'); … … 446 447 } 447 448 $page = isset($_POST['page']) ? max(1, intval(wp_unslash($_POST['page']))) : 1; 448 $per_page = 20;449 $per_page = isset($_POST['per_page']) ? max(1, min(1500, intval(wp_unslash($_POST['per_page'])))) : 20; 449 450 $offset = ($page - 1) * $per_page; 451 $export_mode = isset($_POST['export_mode']) && $_POST['export_mode']; 450 452 $disable_pagination = false; 451 453 $results = []; 452 454 $total_count = 0; 455 $actual_rows = $per_page; // Actual rows fetched (before display limit) 456 $display_limited = false; 453 457 $start_time = microtime(true); 454 458 … … 459 463 $execution_time = round(microtime(true) - $start_time, 4); 460 464 $total_count = count($results); 465 $actual_rows = count($results); 461 466 } else { 462 467 // Toujours retirer LIMIT/OFFSET existant … … 468 473 $results = $wpdb->get_results($paginated_query, ARRAY_A); 469 474 $execution_time = round(microtime(true) - $start_time, 4); 475 $actual_rows = is_array($results) ? count($results) : 0; 476 477 // Smart display limit: only show first 50 rows if per_page is 500 or 1500 (unless export mode) 478 if (!$export_mode && $per_page >= 500 && count($results) > 50) { 479 $results = array_slice($results, 0, 50); 480 $display_limited = true; 481 } 482 470 483 // Compter le total (sans LIMIT) 471 484 $count_query = $sql_query_no_limit ?? $sql_query; … … 513 526 'page' => $page, 514 527 'per_page' => $per_page, 528 'actual_rows' => $actual_rows, 529 'display_limited' => $display_limited, 515 530 'disable_pagination' => $disable_pagination 531 )); 532 exit; 533 } 534 535 public function askeet_export_all_results() { 536 // Nonce and permission check 537 if (!isset($_REQUEST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'])), 'askeet_query_assistant_nonce')) { 538 wp_send_json_error(['message' => 'Invalid nonce.']); 539 exit; 540 } 541 if (!current_user_can('manage_options')) { 542 wp_send_json_error(['message' => 'Insufficient permissions.']); 543 exit; 544 } 545 546 global $wpdb; 547 $sql_query = isset($_POST['sql_query']) ? sanitize_text_field(wp_unslash($_POST['sql_query'])) : ''; 548 if (!empty($sql_query)) { 549 $sql_query = preg_replace('/\s+/', ' ', $sql_query); 550 } 551 552 // Security check: only SELECT queries allowed 553 if (!$this->askeet_is_safe_query($sql_query)) { 554 wp_send_json_error(['message' => 'Only SELECT queries are allowed.']); 555 exit; 556 } 557 558 // Get max rows (default 10000, max 10000 for performance) 559 $max_rows = isset($_POST['max_rows']) ? min(10000, max(1, intval(wp_unslash($_POST['max_rows'])))) : 10000; 560 561 $start_time = microtime(true); 562 563 // Remove any existing LIMIT/OFFSET from the query 564 $sql_query_no_limit = preg_replace('/LIMIT\s+\d+(\s*,\s*\d+)?(\s+OFFSET\s+\d+)?/i', '', $sql_query); 565 $sql_query_no_limit = preg_replace('/OFFSET\s+\d+/i', '', $sql_query_no_limit); 566 $sql_query_no_limit = rtrim($sql_query_no_limit, " ;\n\r"); 567 568 // Add LIMIT for export (no OFFSET, start from beginning) 569 $export_query = $sql_query_no_limit . " LIMIT $max_rows"; 570 571 // Execute query 572 $results = $wpdb->get_results($export_query, ARRAY_A); 573 $execution_time = round(microtime(true) - $start_time, 4); 574 575 if ($wpdb->last_error) { 576 wp_send_json_error(array('message' => __('SQL Error: ', 'askeet') . $wpdb->last_error)); 577 exit; 578 } 579 580 $row_count = is_array($results) ? count($results) : 0; 581 582 wp_send_json_success(array( 583 'results' => $results, 584 'row_count' => $row_count, 585 'execution_time' => $execution_time, 586 'max_rows' => $max_rows 516 587 )); 517 588 exit; … … 735 806 </div> 736 807 <div class="button-group" style="display: flex!important;"> 737 <a class="btn" href="https://discord. gg/yourlink" target="_blank" id="btn-discord"><?php echo esc_html__('Join Discord', 'askeet'); ?></a>738 <a class="btn" href="https:// slack.com/yourlink" target="_blank" id="btn-slack"><?php echo esc_html__('Join Slack', 'askeet'); ?></a>808 <a class="btn" href="https://discord.com/channels/1438540885758443570/1438540942973079766" target="_blank" id="btn-discord"><?php echo esc_html__('Join Discord', 'askeet'); ?></a> 809 <a class="btn" href="https://reach-technologies.slack.com/archives/C09S953P6MD" target="_blank" id="btn-slack"><?php echo esc_html__('Join Slack', 'askeet'); ?></a> 739 810 <a class="btn" href="mailto:[email protected]" id="btn-support"><?php echo esc_html__('Contact Support', 'askeet'); ?></a> 740 811 </div> … … 919 990 return; 920 991 } 992 993 // Get current plan 921 994 $api_url = askeet_get_api_url() . '/get-plan'; 922 995 $response = wp_remote_post($api_url, [ … … 925 998 'timeout' => 15, 926 999 ]); 927 $plan = $next_payment = $price = ''; 1000 1001 $plan_code = 'free'; 1002 $free_queries_left = 10; 1003 $subscription_end_date = null; 928 1004 if (!is_wp_error($response)) { 929 1005 $body = json_decode(wp_remote_retrieve_body($response), true); 930 $plan_code = isset($body['plan']) ? $body['plan'] : (isset($body['current_plan']) ? $body['current_plan'] : ''); 931 $plan = ($plan_code == '20') ? __('Pro', 'askeet') : (($plan_code == '100') ? __('Unlimited', 'askeet') : ''); 932 $next_payment = isset($body['next_payment']) ? $body['next_payment'] : ''; 933 $price = ($plan_code == '20') ? '20€' : (($plan_code == '100') ? '100€' : ''); 934 } 935 // Get Stripe portal URL 1006 $plan_code = isset($body['plan']) ? $body['plan'] : (isset($body['current_plan']) ? $body['current_plan'] : 'free'); 1007 $free_queries_left = isset($body['free_queries_left']) ? $body['free_queries_left'] : 0; 1008 $subscription_end_date = isset($body['subscription_end_date']) ? $body['subscription_end_date'] : null; 1009 1010 // Debug: Log what we received 1011 error_log('[ASKEET DEBUG] Plan: ' . $plan_code); 1012 error_log('[ASKEET DEBUG] Subscription end date from API: ' . ($subscription_end_date ? $subscription_end_date : 'NULL')); 1013 } 1014 1015 // Get Stripe portal URL for manage billing 936 1016 $portal_url = ''; 937 $stripe_api_url = askeet_get_api_url() . '/stripe-portal-url'; 938 $return_url = admin_url('admin.php?page=askeet'); 939 $stripe_body = [ 940 'install_id' => $install_id, 941 'return_url' => $return_url 942 ]; 943 $stripe_response = wp_remote_post($stripe_api_url, [ 944 'body' => json_encode($stripe_body), 945 'headers' => ['Content-Type' => 'application/json'], 946 'timeout' => 15, 947 ]); 948 if (!is_wp_error($stripe_response)) { 949 $stripe_data = json_decode(wp_remote_retrieve_body($stripe_response), true); 950 $portal_url = !empty($stripe_data['portal_url']) ? $stripe_data['portal_url'] : (!empty($stripe_data['url']) ? $stripe_data['url'] : ''); 951 } 952 echo '<div style="max-width:480px;margin:40px auto 0 auto;padding:32px 24px 24px 24px;background:#fff;border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,0.07);text-align:center;">'; 953 echo '<h2 style="margin-bottom:24px;">' . esc_html(__('Your current plan', 'askeet')) . '</h2>'; 954 if ($plan) { 955 echo '<div style="font-size:1.2em;margin-bottom:12px;"><b>' . esc_html($plan) . '</b></div>'; 956 if ($price) { 957 echo '<div style="margin-bottom:8px;">' . esc_html(__('Price', 'askeet')) . ': <b>' . esc_html($price) . '/month</b></div>'; 958 } 959 if ($next_payment) { 960 echo '<div style="margin-bottom:18px;">' . esc_html(__('Next payment date', 'askeet')) . ': <b>' . esc_html($next_payment) . '</b></div>'; 961 } 962 } else { 963 echo '<div style="margin-bottom:18px;color:#c00;">' . esc_html(__('No active plan found.', 'askeet')) . '</div>'; 964 } 965 if ($portal_url) { 966 echo '<a href="' . esc_url($portal_url) . '" target="_blank" style="font-size:1.1em;padding:12px 28px;background:#005a9e;color:#fff;border-radius:8px;text-decoration:none;display:inline-block;margin-top:18px;">' . esc_html(__('Manage billing & invoices', 'askeet')) . '</a>'; 967 } 968 echo '</div>'; 1017 if (in_array($plan_code, ['20', '20_yearly', '100', '100_yearly', 'premium', 'unlimited'])) { 1018 $stripe_api_url = askeet_get_api_url() . '/stripe-portal-url'; 1019 $return_url = admin_url('admin.php?page=askeet-manage-subscription-redirect'); 1020 $stripe_response = wp_remote_post($stripe_api_url, [ 1021 'body' => json_encode(['install_id' => $install_id, 'return_url' => $return_url]), 1022 'headers' => ['Content-Type' => 'application/json'], 1023 'timeout' => 15, 1024 ]); 1025 if (!is_wp_error($stripe_response)) { 1026 $stripe_data = json_decode(wp_remote_retrieve_body($stripe_response), true); 1027 $portal_url = !empty($stripe_data['portal_url']) ? $stripe_data['portal_url'] : (!empty($stripe_data['url']) ? $stripe_data['url'] : ''); 1028 } 1029 } 1030 1031 $success_url = admin_url('admin.php?page=askeet-manage-subscription-redirect&payment=success'); 1032 $cancel_url = admin_url('admin.php?page=askeet-manage-subscription-redirect&payment=cancel'); 1033 ?> 1034 <div class="askeet-subscription-page"> 1035 <div class="askeet-sub-header"> 1036 <h1><?php echo esc_html(__('Manage Your Subscription', 'askeet')); ?></h1> 1037 <p><?php echo esc_html(__('Choose the plan that fits your needs', 'askeet')); ?></p> 1038 1039 <?php if ($subscription_end_date && $plan_code !== 'free'): ?> 1040 <div class="askeet-subscription-info"> 1041 <div class="subscription-info-badge"> 1042 <span class="info-icon">📅</span> 1043 <div class="info-content"> 1044 <span class="info-label"><?php echo esc_html(__('Subscription Renews On', 'askeet')); ?></span> 1045 <span class="info-value"> 1046 <?php 1047 try { 1048 $date = new DateTime($subscription_end_date); 1049 $now = new DateTime(); 1050 1051 // Calculate days until renewal 1052 $interval = $now->diff($date); 1053 $days_left = $interval->days; 1054 1055 echo esc_html($date->format('F j, Y')); 1056 1057 // Show days left if in the future 1058 if ($date > $now && $days_left > 0) { 1059 echo ' <span style="font-size: 0.8em; color: #4caf50;">(' . $days_left . ' days left)</span>'; 1060 } 1061 } catch (Exception $e) { 1062 echo esc_html($subscription_end_date); 1063 } 1064 ?> 1065 </span> 1066 </div> 1067 </div> 1068 </div> 1069 <?php endif; ?> 1070 </div> 1071 1072 <!-- Billing Toggle: Monthly / Yearly --> 1073 <div class="askeet-billing-toggle-wrapper"> 1074 <div class="askeet-billing-toggle"> 1075 <button class="billing-option active" data-billing="monthly"> 1076 <span><?php echo esc_html(__('Monthly', 'askeet')); ?></span> 1077 </button> 1078 <button class="billing-option" data-billing="yearly"> 1079 <span><?php echo esc_html(__('Yearly', 'askeet')); ?></span> 1080 <span class="save-badge"><?php echo esc_html(__('Save 17%', 'askeet')); ?></span> 1081 </button> 1082 </div> 1083 </div> 1084 1085 <div class="askeet-pricing-cards"> 1086 <!-- Free Plan --> 1087 <div class="askeet-price-card <?php echo ($plan_code === 'free') ? 'current-plan' : ''; ?>" data-plan="free"> 1088 <div class="plan-badge"><?php echo ($plan_code === 'free') ? __('Current Plan', 'askeet') : __('Free', 'askeet'); ?></div> 1089 <h2 class="plan-name"><?php echo esc_html(__('Free', 'askeet')); ?></h2> 1090 <div class="plan-price"> 1091 <span class="price">$0</span> 1092 <span class="period">/month</span> 1093 </div> 1094 <ul class="plan-features"> 1095 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('10 queries per day', 'askeet')); ?></li> 1096 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Basic analytics', 'askeet')); ?></li> 1097 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Email support', 'askeet')); ?></li> 1098 </ul> 1099 <?php if ($plan_code === 'free'): ?> 1100 <div class="queries-remaining"><?php echo esc_html(sprintf(__('%d queries left today', 'askeet'), $free_queries_left)); ?></div> 1101 <button class="plan-button current" disabled><?php echo esc_html(__('Current Plan', 'askeet')); ?></button> 1102 <?php else: ?> 1103 <button class="plan-button downgrade" disabled><?php echo esc_html(__('Downgrade to Free', 'askeet')); ?></button> 1104 <?php endif; ?> 1105 </div> 1106 1107 <!-- Pro Plan (20€) --> 1108 <?php 1109 // Determine if this card represents the current plan 1110 $is_current_pro = (in_array($plan_code, ['20', '20_yearly', 'premium'])); 1111 ?> 1112 <div class="askeet-price-card <?php echo $is_current_pro ? 'current-plan' : ''; ?>" data-plan="20" data-plan-yearly="20_yearly"> 1113 <div class="plan-badge popular"><?php echo $is_current_pro ? __('Current Plan', 'askeet') : __('Popular', 'askeet'); ?></div> 1114 <h2 class="plan-name"><?php echo esc_html(__('Pro', 'askeet')); ?></h2> 1115 <div class="plan-price"> 1116 <div class="price-row"> 1117 <span class="price monthly-price">$20</span> 1118 <span class="period monthly-period">/month</span> 1119 </div> 1120 <div class="yearly-price-wrapper" style="display: none;"> 1121 <span class="price-compare">$240</span> 1122 <div class="price-row"> 1123 <span class="price yearly-price">$200</span> 1124 <span class="period yearly-period">/year</span> 1125 </div> 1126 </div> 1127 </div> 1128 <ul class="plan-features"> 1129 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('50 queries per day', 'askeet')); ?></li> 1130 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Advanced analytics', 'askeet')); ?></li> 1131 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Priority support', 'askeet')); ?></li> 1132 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Export up to 10K rows', 'askeet')); ?></li> 1133 </ul> 1134 1135 <!-- Monthly Button --> 1136 <button class="plan-button upgrade askeet-upgrade-btn monthly-btn <?php echo ($plan_code === '20' || $plan_code === 'premium') ? 'current' : ''; ?>" 1137 data-plan="20" 1138 data-plan-yearly="20_yearly" 1139 <?php echo ($plan_code === '20' || $plan_code === 'premium') ? 'disabled' : ''; ?>> 1140 <?php 1141 if ($plan_code === '20' || $plan_code === 'premium') { 1142 echo esc_html(__('Current Plan', 'askeet')); 1143 } elseif ($plan_code === 'free') { 1144 echo esc_html(__('Upgrade to Pro', 'askeet')); 1145 } else { 1146 echo esc_html(__('Switch to Pro', 'askeet')); 1147 } 1148 ?> 1149 </button> 1150 1151 <!-- Yearly Button (hidden by default) --> 1152 <button class="plan-button upgrade askeet-upgrade-btn yearly-btn <?php echo ($plan_code === '20_yearly') ? 'current' : ''; ?>" 1153 style="display: none;" 1154 data-plan="20" 1155 data-plan-yearly="20_yearly" 1156 <?php echo ($plan_code === '20_yearly') ? 'disabled' : ''; ?>> 1157 <?php 1158 if ($plan_code === '20_yearly') { 1159 echo esc_html(__('Current Plan', 'askeet')); 1160 } elseif ($plan_code === 'free') { 1161 echo esc_html(__('Upgrade to Pro', 'askeet')); 1162 } else { 1163 echo esc_html(__('Switch to Pro', 'askeet')); 1164 } 1165 ?> 1166 </button> 1167 </div> 1168 1169 <!-- Unlimited Plan (100€) --> 1170 <?php 1171 // Determine if this card represents the current plan 1172 $is_current_unlimited = (in_array($plan_code, ['100', '100_yearly', 'unlimited'])); 1173 ?> 1174 <div class="askeet-price-card <?php echo $is_current_unlimited ? 'current-plan' : ''; ?>" data-plan="100" data-plan-yearly="100_yearly"> 1175 <div class="plan-badge premium"><?php echo $is_current_unlimited ? __('Current Plan', 'askeet') : __('Best Value', 'askeet'); ?></div> 1176 <h2 class="plan-name"><?php echo esc_html(__('Unlimited', 'askeet')); ?></h2> 1177 <div class="plan-price"> 1178 <div class="price-row"> 1179 <span class="price monthly-price">$100</span> 1180 <span class="period monthly-period">/month</span> 1181 </div> 1182 <div class="yearly-price-wrapper" style="display: none;"> 1183 <span class="price-compare">$1200</span> 1184 <div class="price-row"> 1185 <span class="price yearly-price">$1000</span> 1186 <span class="period yearly-period">/year</span> 1187 </div> 1188 </div> 1189 </div> 1190 <ul class="plan-features"> 1191 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Unlimited queries', 'askeet')); ?></li> 1192 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Advanced analytics', 'askeet')); ?></li> 1193 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('24/7 Priority support', 'askeet')); ?></li> 1194 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Export unlimited rows', 'askeet')); ?></li> 1195 <li><span class="feature-icon">✓</span> <?php echo esc_html(__('Custom integrations', 'askeet')); ?></li> 1196 </ul> 1197 1198 <!-- Monthly Button --> 1199 <button class="plan-button upgrade askeet-upgrade-btn monthly-btn <?php echo ($plan_code === '100' || $plan_code === 'unlimited') ? 'current' : ''; ?>" 1200 data-plan="100" 1201 data-plan-yearly="100_yearly" 1202 <?php echo ($plan_code === '100' || $plan_code === 'unlimited') ? 'disabled' : ''; ?>> 1203 <?php 1204 if ($plan_code === '100' || $plan_code === 'unlimited') { 1205 echo esc_html(__('Current Plan', 'askeet')); 1206 } else { 1207 echo esc_html(__('Upgrade to Unlimited', 'askeet')); 1208 } 1209 ?> 1210 </button> 1211 1212 <!-- Yearly Button (hidden by default) --> 1213 <button class="plan-button upgrade askeet-upgrade-btn yearly-btn <?php echo ($plan_code === '100_yearly') ? 'current' : ''; ?>" 1214 style="display: none;" 1215 data-plan="100" 1216 data-plan-yearly="100_yearly" 1217 <?php echo ($plan_code === '100_yearly') ? 'disabled' : ''; ?>> 1218 <?php 1219 if ($plan_code === '100_yearly') { 1220 echo esc_html(__('Current Plan', 'askeet')); 1221 } else { 1222 echo esc_html(__('Upgrade to Unlimited', 'askeet')); 1223 } 1224 ?> 1225 </button> 1226 </div> 1227 </div> 1228 1229 <?php if ($portal_url && in_array($plan_code, ['20', '20_yearly', '100', '100_yearly', 'premium', 'unlimited'])): ?> 1230 <div class="askeet-manage-billing"> 1231 <a href="<?php echo esc_url($portal_url); ?>" target="_blank" class="manage-billing-btn"> 1232 <span class="icon">💳</span> 1233 <?php echo esc_html(__('Manage Billing & Invoices', 'askeet')); ?> 1234 </a> 1235 </div> 1236 <?php endif; ?> 1237 1238 <div class="askeet-sub-footer"> 1239 <p>🔒 <?php echo esc_html(__('Secure payment powered by Stripe', 'askeet')); ?></p> 1240 <p><?php echo esc_html(__('Cancel anytime. No questions asked.', 'askeet')); ?></p> 1241 </div> 1242 </div> 1243 1244 <script> 1245 jQuery(document).ready(function($) { 1246 // Track current billing mode (monthly or yearly) 1247 let currentBillingMode = 'monthly'; 1248 1249 // Handle billing toggle (monthly/yearly) 1250 $('.billing-option').on('click', function() { 1251 const billingMode = $(this).data('billing'); 1252 1253 // Update button states 1254 $('.billing-option').removeClass('active'); 1255 $(this).addClass('active'); 1256 1257 // Update billing mode 1258 currentBillingMode = billingMode; 1259 1260 // Toggle price visibility 1261 if (billingMode === 'yearly') { 1262 $('.price-row:has(.monthly-price)').hide(); 1263 $('.yearly-price-wrapper').show(); 1264 // Toggle buttons 1265 $('.monthly-btn').hide(); 1266 $('.yearly-btn').show(); 1267 } else { 1268 $('.price-row:has(.monthly-price)').show(); 1269 $('.yearly-price-wrapper').hide(); 1270 // Toggle buttons 1271 $('.monthly-btn').show(); 1272 $('.yearly-btn').hide(); 1273 } 1274 1275 console.log('Billing mode changed to:', billingMode); 1276 }); 1277 1278 // Handle upgrade button clicks 1279 $('.askeet-upgrade-btn').on('click', function() { 1280 const btn = $(this); 1281 const originalText = btn.html(); 1282 1283 // Get the correct plan based on billing mode 1284 let plan; 1285 if (currentBillingMode === 'yearly') { 1286 plan = btn.data('plan-yearly'); // e.g., "20_yearly" or "100_yearly" 1287 } else { 1288 plan = btn.data('plan'); // e.g., "20" or "100" 1289 } 1290 1291 console.log('Selected plan:', plan, 'Billing mode:', currentBillingMode); 1292 1293 btn.prop('disabled', true).html('<?php echo esc_js(__('Processing...', 'askeet')); ?>'); 1294 1295 $.ajax({ 1296 url: '<?php echo esc_js(askeet_get_api_url()); ?>/create-checkout-session', 1297 type: 'POST', 1298 contentType: 'application/json', 1299 data: JSON.stringify({ 1300 install_id: '<?php echo esc_js($install_id); ?>', 1301 plan: plan.toString(), 1302 success_url: '<?php echo esc_js($success_url); ?>', 1303 cancel_url: '<?php echo esc_js($cancel_url); ?>' 1304 }), 1305 success: function(response) { 1306 if (response.checkout_url) { 1307 window.location.href = response.checkout_url; 1308 } else { 1309 alert('<?php echo esc_js(__('Error: No checkout URL received.', 'askeet')); ?>'); 1310 btn.prop('disabled', false).html(originalText); 1311 } 1312 }, 1313 error: function() { 1314 alert('<?php echo esc_js(__('Error creating checkout session. Please try again.', 'askeet')); ?>'); 1315 btn.prop('disabled', false).html(originalText); 1316 } 1317 }); 1318 }); 1319 }); 1320 </script> 1321 <?php 969 1322 } 970 1323 -
askeet/trunk/assets/css/style.css
r3323715 r3395155 48 48 color: white; 49 49 border: none; 50 padding: 32px 34px;50 padding: 17px 34px; 51 51 cursor: pointer; 52 52 font-weight: 600; … … 56 56 transition: background 0.3s ease; 57 57 border-radius: 10px; 58 background: linear-gradient(180deg, rgb a(0, 90, 158, 0.74) 0%, #005A9E100%);58 background: linear-gradient(180deg, rgb(61 63 66 / 74%) 0%, #13191e 100%); 59 59 } 60 60 61 61 .btn:hover { 62 background: linear-gradient(90deg, #004d8c 74%, #004d8c 100%); 62 background: #004d8c; 63 color: white!important; 63 64 } 64 65 … … 528 529 margin-bottom: 8px; 529 530 } 531 532 /* Top controls bar - compact and attractive */ 533 .wcqa-top-controls { 534 display: flex; 535 justify-content: space-between; 536 align-items: center; 537 background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); 538 padding: 10px 16px; 539 border-radius: 8px; 540 margin-bottom: 12px; 541 border: 1px solid #e3e8ef; 542 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 543 } 544 545 .wcqa-rows-selector { 546 display: flex; 547 align-items: center; 548 gap: 8px; 549 } 550 551 .wcqa-rows-selector label { 552 font-weight: 600; 553 color: #555; 554 font-size: 0.85em; 555 white-space: nowrap; 556 } 557 558 .wcqa-per-page-dropdown { 559 padding: 5px 10px; 560 border: 1.5px solid #d0d5dd; 561 border-radius: 5px; 562 font-size: 0.85em; 563 background: white; 564 cursor: pointer; 565 min-width: 70px; 566 font-weight: 600; 567 color: #444; 568 transition: all 0.2s; 569 } 570 571 .wcqa-per-page-dropdown:hover { 572 border-color: #005a9e; 573 background: #f0f7ff; 574 } 575 576 .wcqa-per-page-dropdown:focus { 577 outline: none; 578 border-color: #005a9e; 579 box-shadow: 0 0 0 2px rgba(0, 90, 158, 0.1); 580 } 581 582 .wcqa-export-controls-top { 583 display: flex; 584 gap: 8px; 585 align-items: center; 586 } 587 588 .wcqa-export-btn-compact { 589 display: inline-flex !important; 590 align-items: center; 591 gap: 5px; 592 padding: 6px 12px !important; 593 font-size: 0.80em !important; 594 font-weight: 600 !important; 595 border-radius: 5px !important; 596 transition: all 0.2s ease; 597 white-space: nowrap; 598 height: auto !important; 599 line-height: 1.2 !important; 600 } 601 602 .wcqa-export-btn-compact .dashicons { 603 font-size: 16px; 604 width: 16px; 605 height: 16px; 606 margin-right: 0; 607 } 608 609 .wcqa-export-btn-compact { 610 background: white !important; 611 border: 1.5px solid #005a9e !important; 612 color: #005a9e !important; 613 } 614 615 .wcqa-export-btn-compact:hover { 616 background: #005a9e !important; 617 color: white !important; 618 transform: translateY(-1px); 619 box-shadow: 0 2px 6px rgba(0, 90, 158, 0.2); 620 } 621 622 .wcqa-export-all-btn { 623 background: #005a9e !important; 624 border: 1.5px solid #005a9e !important; 625 color: white !important; 626 } 627 628 .wcqa-export-all-btn:hover { 629 background: #003d6b !important; 630 border-color: #003d6b !important; 631 box-shadow: 0 3px 8px rgba(0, 90, 158, 0.3); 632 } 633 634 .wcqa-export-btn-compact:disabled { 635 opacity: 0.6; 636 cursor: not-allowed; 637 transform: none !important; 638 } 639 640 .wcqa-export-warning-top { 641 background: #fff8e1; 642 border-left: 3px solid #ffc107; 643 color: #856404; 644 padding: 8px 12px; 645 border-radius: 4px; 646 font-size: 0.85em; 647 margin-bottom: 10px; 648 display: flex; 649 align-items: center; 650 gap: 6px; 651 } 652 653 /* Performance notice - beautiful and clear */ 654 .wcqa-performance-notice { 655 background: linear-gradient(135deg, #e8f5e9 0%, #f1f8e9 100%); 656 border-left: 4px solid #4caf50; 657 border-radius: 8px; 658 padding: 14px 40px 14px 16px; 659 margin-bottom: 16px; 660 display: flex; 661 align-items: flex-start; 662 gap: 12px; 663 box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15); 664 animation: slideInFromTop 0.4s ease-out; 665 position: relative; 666 } 667 668 @keyframes slideInFromTop { 669 from { 670 opacity: 0; 671 transform: translateY(-10px); 672 } 673 to { 674 opacity: 1; 675 transform: translateY(0); 676 } 677 } 678 679 .wcqa-notice-icon { 680 font-size: 1.5em; 681 line-height: 1; 682 flex-shrink: 0; 683 } 684 685 .wcqa-notice-content { 686 flex: 1; 687 font-size: 0.9em; 688 line-height: 1.5; 689 color: #2e7d32; 690 } 691 692 .wcqa-notice-content strong { 693 font-weight: 700; 694 color: #1b5e20; 695 } 696 697 .wcqa-notice-close { 698 position: absolute; 699 top: 8px; 700 right: 8px; 701 background: none; 702 border: none; 703 font-size: 1.5em; 704 line-height: 1; 705 color: #4caf50; 706 cursor: pointer; 707 padding: 4px 8px; 708 border-radius: 4px; 709 transition: all 0.2s; 710 } 711 712 .wcqa-notice-close:hover { 713 background: rgba(76, 175, 80, 0.15); 714 color: #2e7d32; 715 } 716 717 /* Responsive design */ 718 @media (max-width: 768px) { 719 .wcqa-top-controls { 720 flex-direction: column; 721 gap: 10px; 722 align-items: stretch; 723 } 724 725 .wcqa-rows-selector { 726 justify-content: space-between; 727 width: 100%; 728 } 729 730 .wcqa-export-controls-top { 731 width: 100%; 732 justify-content: space-between; 733 } 734 735 .wcqa-export-btn-compact { 736 flex: 1; 737 justify-content: center; 738 } 739 740 .wcqa-performance-notice { 741 padding: 12px 14px; 742 } 743 744 .wcqa-notice-content { 745 font-size: 0.85em; 746 } 747 } 748 749 @media (max-width: 480px) { 750 .wcqa-export-controls-top { 751 flex-direction: column; 752 gap: 6px; 753 } 754 755 .wcqa-export-btn-compact { 756 width: 100%; 757 } 758 759 .wcqa-performance-notice { 760 padding: 10px 12px; 761 gap: 8px; 762 } 763 764 .wcqa-notice-icon { 765 font-size: 1.2em; 766 } 767 768 .wcqa-notice-content { 769 font-size: 0.8em; 770 } 771 } 772 773 /* Subscription Page - Modern Pricing Cards */ 774 .askeet-subscription-page { 775 max-width: 1200px; 776 margin: 30px auto; 777 padding: 0 20px; 778 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; 779 } 780 781 .askeet-sub-header { 782 text-align: center; 783 margin-bottom: 50px; 784 } 785 786 .askeet-sub-header h1 { 787 font-size: 2.5em; 788 font-weight: 700; 789 color: #1a1a2e; 790 margin-bottom: 12px; 791 } 792 793 .askeet-sub-header p { 794 font-size: 1.2em; 795 color: #666; 796 } 797 798 /* Subscription Info Badge */ 799 .askeet-subscription-info { 800 display: flex; 801 justify-content: center; 802 margin-top: 24px; 803 } 804 805 .subscription-info-badge { 806 display: inline-flex; 807 align-items: center; 808 gap: 12px; 809 background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); 810 border: 2px solid #005a9e; 811 border-radius: 16px; 812 padding: 16px 28px; 813 box-shadow: 0 4px 12px rgba(0, 90, 158, 0.15); 814 } 815 816 .info-icon { 817 font-size: 2em; 818 line-height: 1; 819 } 820 821 .info-content { 822 display: flex; 823 flex-direction: column; 824 gap: 4px; 825 } 826 827 .info-label { 828 font-size: 0.85em; 829 color: #666; 830 font-weight: 500; 831 text-transform: uppercase; 832 letter-spacing: 0.5px; 833 } 834 835 .info-value { 836 font-size: 1.3em; 837 color: #005a9e; 838 font-weight: 700; 839 } 840 841 /* Billing Toggle (Monthly/Yearly) */ 842 .askeet-billing-toggle-wrapper { 843 display: flex; 844 justify-content: center; 845 margin-bottom: 40px; 846 } 847 848 .askeet-billing-toggle { 849 display: inline-flex; 850 background: #f5f7fa; 851 border-radius: 12px; 852 padding: 6px; 853 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); 854 } 855 856 .billing-option { 857 display: flex; 858 align-items: center; 859 gap: 8px; 860 padding: 12px 24px; 861 border: none; 862 background: transparent; 863 color: #666; 864 font-size: 16px; 865 font-weight: 600; 866 border-radius: 8px; 867 cursor: pointer; 868 transition: all 0.3s ease; 869 position: relative; 870 } 871 872 .billing-option:hover { 873 color: #005a9e; 874 } 875 876 .billing-option.active { 877 background: white; 878 color: #005a9e; 879 box-shadow: 0 2px 6px rgba(0, 90, 158, 0.15); 880 } 881 882 .save-badge { 883 display: inline-block; 884 background: linear-gradient(135deg, #4caf50 0%, #45a049 100%); 885 color: white; 886 font-size: 11px; 887 font-weight: 700; 888 padding: 3px 8px; 889 border-radius: 12px; 890 text-transform: uppercase; 891 letter-spacing: 0.5px; 892 box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3); 893 } 894 895 .askeet-pricing-cards { 896 display: grid; 897 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 898 gap: 30px; 899 margin-bottom: 50px; 900 } 901 902 .askeet-price-card { 903 background: white; 904 border: 2px solid #e0e6ed; 905 border-radius: 16px; 906 padding: 32px 28px; 907 position: relative; 908 transition: all 0.3s ease; 909 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 910 display: flex; 911 flex-direction: column; 912 min-height: 100%; 913 } 914 915 .askeet-price-card:hover { 916 transform: translateY(-5px); 917 box-shadow: 0 12px 24px rgba(0, 90, 158, 0.15); 918 border-color: #005a9e; 919 } 920 921 .askeet-price-card.current-plan { 922 border: 3px solid #4caf50; 923 background: linear-gradient(135deg, #f8fff9 0%, #ffffff 100%); 924 } 925 926 .askeet-price-card.current-plan:hover { 927 transform: none; 928 box-shadow: 0 8px 16px rgba(76, 175, 80, 0.2); 929 } 930 931 .plan-badge { 932 display: inline-block; 933 padding: 6px 16px; 934 background: #e0e6ed; 935 color: #555; 936 font-size: 0.85em; 937 font-weight: 700; 938 border-radius: 20px; 939 text-transform: uppercase; 940 letter-spacing: 0.5px; 941 margin-bottom: 20px; 942 } 943 944 .plan-badge.popular { 945 background: linear-gradient(135deg, #4f8cff 0%, #6f6fff 100%); 946 color: white; 947 } 948 949 .plan-badge.premium { 950 background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 100%); 951 color: white; 952 } 953 954 .current-plan .plan-badge { 955 background: #4caf50; 956 color: white; 957 } 958 959 .plan-name { 960 font-size: 1.8em; 961 font-weight: 700; 962 color: #1a1a2e; 963 margin: 16px 0; 964 } 965 966 .plan-price { 967 margin: 20px 0 30px 0; 968 } 969 970 .plan-price .price { 971 font-size: 3em; 972 font-weight: 800; 973 color: #1a1a2e; 974 line-height: 1; 975 } 976 977 .plan-price .period { 978 font-size: 1.1em; 979 color: #888; 980 font-weight: 500; 981 } 982 983 /* Price Row - keeps price and period inline */ 984 .price-row { 985 display: inline-flex; 986 align-items: baseline; 987 gap: 4px; 988 } 989 990 /* Yearly Price Comparison */ 991 .yearly-price-wrapper { 992 display: flex; 993 flex-direction: column; 994 align-items: center; 995 gap: 8px; 996 } 997 998 .price-compare { 999 font-size: 1.4em; 1000 font-weight: 600; 1001 color: #999; 1002 text-decoration: line-through; 1003 opacity: 0.7; 1004 } 1005 1006 .yearly-price-wrapper .price.yearly-price { 1007 font-size: 3em; 1008 font-weight: 800; 1009 color: #4caf50; 1010 } 1011 1012 .yearly-price-wrapper .period.yearly-period { 1013 font-size: 1.1em; 1014 color: #888; 1015 font-weight: 500; 1016 } 1017 1018 .plan-features { 1019 list-style: none; 1020 padding: 0; 1021 margin: 0 0 30px 0; 1022 flex-grow: 1; 1023 } 1024 1025 .plan-features li { 1026 padding: 12px 0; 1027 border-bottom: 1px solid #f0f0f0; 1028 color: #444; 1029 font-size: 1em; 1030 display: flex; 1031 align-items: center; 1032 gap: 12px; 1033 } 1034 1035 .plan-features li:last-child { 1036 border-bottom: none; 1037 } 1038 1039 .feature-icon { 1040 display: inline-flex; 1041 align-items: center; 1042 justify-content: center; 1043 width: 24px; 1044 height: 24px; 1045 background: #e8f5e9; 1046 color: #4caf50; 1047 border-radius: 50%; 1048 font-weight: 900; 1049 font-size: 0.9em; 1050 flex-shrink: 0; 1051 } 1052 1053 .queries-remaining { 1054 background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); 1055 padding: 12px 16px; 1056 border-radius: 8px; 1057 text-align: center; 1058 font-weight: 700; 1059 color: #e65100; 1060 margin-bottom: 20px; 1061 font-size: 0.95em; 1062 margin-top: auto; 1063 } 1064 1065 .plan-button { 1066 width: 100%; 1067 padding: 16px 24px; 1068 font-size: 1.1em; 1069 font-weight: 700; 1070 border: none; 1071 border-radius: 10px; 1072 cursor: pointer; 1073 transition: all 0.3s ease; 1074 font-family: inherit; 1075 margin-top: auto; 1076 } 1077 1078 .plan-button.upgrade { 1079 background: linear-gradient(135deg, #005a9e 0%, #003d6b 100%); 1080 color: white; 1081 } 1082 1083 .plan-button.upgrade:hover { 1084 background: linear-gradient(135deg, #003d6b 0%, #002a4a 100%); 1085 transform: translateY(-2px); 1086 box-shadow: 0 6px 16px rgba(0, 90, 158, 0.3); 1087 } 1088 1089 .plan-button.change { 1090 background: linear-gradient(135deg, #4f8cff 0%, #6f6fff 100%); 1091 color: white; 1092 } 1093 1094 .plan-button.change:hover { 1095 background: linear-gradient(135deg, #3a7ae0 0%, #5555e0 100%); 1096 transform: translateY(-2px); 1097 box-shadow: 0 6px 16px rgba(79, 140, 255, 0.3); 1098 } 1099 1100 .plan-button.current { 1101 background: #e8f5e9; 1102 color: #4caf50; 1103 cursor: not-allowed; 1104 opacity: 0.8; 1105 } 1106 1107 .plan-button.downgrade { 1108 background: #f5f5f5; 1109 color: #999; 1110 cursor: not-allowed; 1111 opacity: 0.6; 1112 } 1113 1114 .plan-button:disabled { 1115 cursor: not-allowed; 1116 opacity: 0.7; 1117 } 1118 1119 .askeet-manage-billing { 1120 text-align: center; 1121 margin: 40px 0; 1122 } 1123 1124 .manage-billing-btn { 1125 display: inline-flex; 1126 align-items: center; 1127 gap: 12px; 1128 padding: 16px 32px; 1129 background: linear-gradient(135deg, #005a9e 0%, #003d6b 100%); 1130 color: white; 1131 text-decoration: none; 1132 font-size: 1.1em; 1133 font-weight: 700; 1134 border-radius: 10px; 1135 transition: all 0.3s ease; 1136 } 1137 1138 .manage-billing-btn:hover { 1139 background: linear-gradient(135deg, #003d6b 0%, #002a4a 100%); 1140 transform: translateY(-2px); 1141 box-shadow: 0 6px 16px rgba(0, 90, 158, 0.3); 1142 color: white; 1143 } 1144 1145 .manage-billing-btn .icon { 1146 font-size: 1.3em; 1147 } 1148 1149 .askeet-sub-footer { 1150 text-align: center; 1151 margin-top: 60px; 1152 padding-top: 30px; 1153 border-top: 1px solid #e0e6ed; 1154 } 1155 1156 .askeet-sub-footer p { 1157 color: #888; 1158 font-size: 0.95em; 1159 margin: 8px 0; 1160 } 1161 1162 /* Responsive */ 1163 @media (max-width: 900px) { 1164 .askeet-pricing-cards { 1165 grid-template-columns: 1fr; 1166 max-width: 500px; 1167 margin-left: auto; 1168 margin-right: auto; 1169 } 1170 1171 .askeet-sub-header h1 { 1172 font-size: 2em; 1173 } 1174 1175 .plan-price .price { 1176 font-size: 2.5em; 1177 } 1178 } 1179 1180 @media (max-width: 600px) { 1181 .askeet-subscription-page { 1182 padding: 0 15px; 1183 } 1184 1185 .askeet-sub-header h1 { 1186 font-size: 1.8em; 1187 } 1188 1189 .askeet-price-card { 1190 padding: 24px 20px; 1191 } 1192 1193 .plan-price .price { 1194 font-size: 2.2em; 1195 } 1196 } -
askeet/trunk/assets/js/script.js
r3323717 r3395155 7 7 let lastPage = 1; 8 8 let lastPerPage = 20; 9 let currentPerPage = 20; // User-selected rows per page 10 let lastTotalCount = 0; // Total results count 9 11 let chatHistory = []; 10 12 let lastQueryRequest = ''; … … 380 382 sql_query: currentSqlQuery, 381 383 nonce: askeet_query_assistant.nonce, 382 page: pageNum 384 page: pageNum, 385 per_page: currentPerPage 383 386 }, 384 387 success: function(response) { 385 388 // console.log('[WCQA] AJAX success:', response); // Debug log 386 389 removeLoading(); 390 391 // Check for upgrade requirement 392 if (handleLimitModals(response)) return; 393 387 394 if (response.success) { 388 395 lastResults = response.data.results; … … 396 403 lastPage = response.data.page; 397 404 lastPerPage = response.data.per_page; 405 lastTotalCount = response.data.total_count; 398 406 const totalPages = Math.max(1, Math.ceil(response.data.total_count / response.data.per_page)); 399 407 let html = ''; 400 408 if (response.data.results && response.data.results.length > 0) { 409 // Show rows per page selector and export buttons at the top 410 html += '<div class="wcqa-top-controls">'; 411 412 // Rows per page selector 413 html += '<div class="wcqa-rows-selector">'; 414 html += '<label for="wcqa-per-page-select">Rows per page:</label> '; 415 html += '<select id="wcqa-per-page-select" class="wcqa-per-page-dropdown">'; 416 html += '<option value="20"'+(currentPerPage===20?' selected':'')+'>20</option>'; 417 html += '<option value="50"'+(currentPerPage===50?' selected':'')+'>50</option>'; 418 html += '<option value="100"'+(currentPerPage===100?' selected':'')+'>100</option>'; 419 html += '<option value="500"'+(currentPerPage===500?' selected':'')+'>500</option>'; 420 html += '<option value="1500"'+(currentPerPage===1500?' selected':'')+'>1500</option>'; 421 html += '</select>'; 422 html += '</div>'; 423 424 // Export buttons (compact, inline) 425 html += '<div class="wcqa-export-controls-top">'; 426 const actualRowsOnPage = response.data.actual_rows || response.data.results.length; 427 html += '<button id="export-current-page" class="button wcqa-export-btn-compact" title="Export all '+actualRowsOnPage+' rows from this page">'; 428 html += '<span class="dashicons dashicons-download"></span> Export Page ('+actualRowsOnPage+')'; 429 html += '</button>'; 430 const maxExportLimit = 10000; 431 const exportAllCount = Math.min(response.data.total_count, maxExportLimit); 432 html += '<button id="export-all-results" class="button wcqa-export-btn-compact wcqa-export-all-btn" title="Export up to '+maxExportLimit.toLocaleString()+' rows">'; 433 html += '<span class="dashicons dashicons-database-export"></span> Export All ('+exportAllCount.toLocaleString()+')'; 434 html += '</button>'; 435 html += '</div>'; 436 437 html += '</div>'; 438 439 // Warning for large exports 440 if (response.data.total_count > maxExportLimit) { 441 html += '<div class="wcqa-export-warning-top">⚠️ '+response.data.total_count.toLocaleString()+' results found. Export All will download the first '+maxExportLimit.toLocaleString()+' rows.</div>'; 442 } 443 444 // Performance notice for large page sizes 445 if (response.data.display_limited === true || (currentPerPage >= 500 && response.data.actual_rows >= 500)) { 446 html += '<div class="wcqa-performance-notice">'; 447 html += '<div class="wcqa-notice-icon">💡</div>'; 448 html += '<div class="wcqa-notice-content">'; 449 html += '<strong>Performance Mode Active:</strong> For optimal browser performance, we\'re displaying only the first 50 rows. '; 450 html += 'Don\'t worry - you can export all <strong>'+response.data.actual_rows+' rows</strong> from this page using the <strong>"Export Page"</strong> button above!'; 451 html += '</div>'; 452 html += '<button class="wcqa-notice-close" title="Close this message">×</button>'; 453 html += '</div>'; 454 } 455 401 456 // Show summary message 402 457 const startIdx = (response.data.page - 1) * response.data.per_page + 1; 403 458 const endIdx = startIdx + response.data.results.length - 1; 404 html += '<div class="query-success">Displaying '+startIdx+' to '+endIdx+' of '+response.data.total_count+' results (Page '+response.data.page+' of '+totalPages+'). '+askeet_query_assistant_i18n.execution_time+': '+response.data.execution_time+' '+askeet_query_assistant_i18n.seconds+'.</div>'; 459 const displayInfo = response.data.display_limited ? ' (displaying first '+response.data.results.length+' rows for performance)' : ''; 460 html += '<div class="query-success">Page '+response.data.page+' of '+totalPages+': showing rows '+startIdx+' to '+endIdx+' of '+response.data.total_count+' total'+displayInfo+'. '+askeet_query_assistant_i18n.execution_time+': '+response.data.execution_time+'s</div>'; 461 405 462 html += '<div class="results-block">' + renderResultsTable(response.data.results, response.data.total_count, response.data.page) + '</div>'; 463 406 464 if (!response.data.disable_pagination) { 407 465 html += renderPagination(response.data.total_count, response.data.page, response.data.per_page); 408 466 } 409 html += '<button id="export-csv" class="button" style="margin-top:10px;">'+askeet_query_assistant_i18n.export_csv+'</button>';410 467 } else { 411 468 html += '<div class="query-success">'+askeet_query_assistant_i18n.no_result_found+'</div>'; 412 469 } 413 470 renderMessage(html, 'ai'); 471 472 // Add close functionality to performance notice after rendering 473 if (response.data.display_limited === true || (currentPerPage >= 500 && response.data.actual_rows >= 500)) { 474 setTimeout(function() { 475 const $notice = $('.wcqa-performance-notice').last(); 476 477 // Close button handler 478 $notice.find('.wcqa-notice-close').on('click', function(e) { 479 e.preventDefault(); 480 $(this).closest('.wcqa-performance-notice').fadeOut(300, function() { 481 $(this).remove(); 482 }); 483 }); 484 485 // Auto-fade after 12 seconds 486 setTimeout(function() { 487 if ($notice.is(':visible')) { 488 $notice.fadeOut(400, function() { 489 $(this).remove(); 490 }); 491 } 492 }, 12000); 493 }, 200); 494 } 414 495 } 415 496 else { … … 453 534 } 454 535 }); 455 $('#wcqa-messages').on('click', '#export-csv', function(e) { 536 // Handle rows per page selection change 537 $('#wcqa-messages').on('change', '#wcqa-per-page-select', function(e) { 538 const newPerPage = parseInt($(this).val()); 539 currentPerPage = newPerPage; 540 541 // Re-execute query with new page size, starting from page 1 542 executeQueryPage(1, false); 543 }); 544 545 // Export Current Page - exports ALL rows from current page (even if display is limited) 546 $('#wcqa-messages').on('click', '#export-current-page', function(e) { 456 547 e.preventDefault(); 548 if (currentSqlQuery === '') { 549 alert(askeet_query_assistant_i18n.no_sql_to_execute); 550 return; 551 } 552 457 553 const $btn = $(this); 458 // Cherche le tableau de résultats associé au bouton cliqué 459 let $table = $btn.closest('.wcqa-message').find('.results-table').first(); 460 if ($table.length === 0) { 461 // Si pas trouvé dans le même message, cherche juste avant 462 $table = $btn.prevAll('.results-block').find('.results-table').first(); 463 } 464 if ($table.length === 0) { 465 // Fallback : cherche dans le parent direct 466 $table = $btn.parent().find('.results-table').first(); 467 } 468 if ($table.length === 0) { 469 alert(askeet_query_assistant_i18n.no_result_to_export); 554 const originalText = $btn.html(); 555 $btn.prop('disabled', true).html('<span class="spinner is-active" style="float:none;"></span> Exporting...'); 556 557 // Fetch the FULL page data from server (all rows for current page) 558 $.ajax({ 559 url: askeet_query_assistant.ajax_url, 560 type: 'POST', 561 data: { 562 action: 'askeet_execute_sql_query', 563 sql_query: currentSqlQuery, 564 nonce: askeet_query_assistant.nonce, 565 page: lastPage, 566 per_page: currentPerPage, 567 export_mode: true // Request full data without display limit 568 }, 569 success: function(response) { 570 if (response.success && response.data && response.data.results) { 571 const results = response.data.results; 572 if (results.length === 0) { 573 alert('No data to export.'); 574 $btn.prop('disabled', false).html(originalText); 575 return; 576 } 577 578 // Convert to CSV 579 let csv = []; 580 const headers = Object.keys(results[0]); 581 csv.push(headers.map(h => '"' + h.replace(/"/g, '""') + '"').join(',')); 582 583 results.forEach(function(row) { 584 const values = headers.map(function(header) { 585 let val = row[header] !== null ? String(row[header]) : ''; 586 val = val.replace(/"/g, '""'); 587 return '"' + val + '"'; 588 }); 589 csv.push(values.join(',')); 590 }); 591 592 const csvContent = csv.join('\n'); 593 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); 594 const link = document.createElement('a'); 595 const url = URL.createObjectURL(blob); 596 link.setAttribute('href', url); 597 598 var lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase(); 599 var filename = lang.startsWith('fr') 600 ? 'page-' + lastPage + '_' + results.length + '_lignes.csv' 601 : 'page-' + lastPage + '_' + results.length + '_rows.csv'; 602 603 link.setAttribute('download', filename); 604 link.style.visibility = 'hidden'; 605 document.body.appendChild(link); 606 link.click(); 607 document.body.removeChild(link); 608 609 $btn.prop('disabled', false).html('<span class="dashicons dashicons-yes"></span> Done!'); 610 setTimeout(function() { 611 $btn.html(originalText); 612 }, 2000); 613 } else { 614 alert('Export failed. Please try again.'); 615 $btn.prop('disabled', false).html(originalText); 616 } 617 }, 618 error: function() { 619 alert('Export failed. Please try again.'); 620 $btn.prop('disabled', false).html(originalText); 621 } 622 }); 623 }); 624 625 // Export All Results - fetches up to 5000 rows from server 626 $('#wcqa-messages').on('click', '#export-all-results', function(e) { 627 e.preventDefault(); 628 if (currentSqlQuery === '') { 629 alert(askeet_query_assistant_i18n.no_sql_to_execute); 470 630 return; 471 631 } 472 let csv = []; 473 const rows = $table[0].querySelectorAll('tr'); 474 for (const row of rows) { 475 const cells = row.querySelectorAll('th, td'); 476 const csvRow = []; 477 for (const cell of cells) { 478 let text = cell.textContent.trim(); 479 text = text.replace(/"/g, '""'); 480 csvRow.push('"' + text + '"'); 481 } 482 csv.push(csvRow.join(',')); 483 } 484 const csvContent = csv.join('\n'); 485 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); 486 const link = document.createElement('a'); 487 const url = URL.createObjectURL(blob); 488 link.setAttribute('href', url); 489 // Determine language for filename 490 var lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase(); 491 var filename = ''; 492 if (lang.startsWith('fr')) { 493 filename = 'resultat_requete_page-' + lastPage + '.csv'; 494 } else { 495 filename = 'result_query_page-' + lastPage + '.csv'; 496 } 497 link.setAttribute('download', filename); 498 link.style.visibility = 'hidden'; 499 document.body.appendChild(link); 500 link.click(); 501 document.body.removeChild(link); 632 633 const $btn = $(this); 634 const originalText = $btn.html(); 635 const maxRows = Math.min(lastTotalCount, 10000); 636 637 // Confirm for large exports 638 if (maxRows > 1000) { 639 const confirmMsg = 'You are about to export ' + maxRows.toLocaleString() + ' rows. This may take up to 60 seconds. Continue?'; 640 if (!confirm(confirmMsg)) { 641 return; 642 } 643 } 644 645 $btn.prop('disabled', true).html('<span class="spinner is-active" style="float:none;"></span> Exporting...'); 646 647 $.ajax({ 648 url: askeet_query_assistant.ajax_url, 649 type: 'POST', 650 data: { 651 action: 'askeet_export_all_results', 652 sql_query: currentSqlQuery, 653 nonce: askeet_query_assistant.nonce, 654 max_rows: 10000 655 }, 656 success: function(response) { 657 if (response.success && response.data && response.data.results) { 658 // Convert results to CSV 659 const results = response.data.results; 660 if (results.length === 0) { 661 alert('No data to export.'); 662 $btn.prop('disabled', false).html(originalText); 663 return; 664 } 665 666 let csv = []; 667 // Add header row 668 const headers = Object.keys(results[0]); 669 csv.push(headers.map(h => '"' + h.replace(/"/g, '""') + '"').join(',')); 670 671 // Add data rows 672 results.forEach(function(row) { 673 const values = headers.map(function(header) { 674 let val = row[header] !== null ? String(row[header]) : ''; 675 val = val.replace(/"/g, '""'); 676 return '"' + val + '"'; 677 }); 678 csv.push(values.join(',')); 679 }); 680 681 const csvContent = csv.join('\n'); 682 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); 683 const link = document.createElement('a'); 684 const url = URL.createObjectURL(blob); 685 link.setAttribute('href', url); 686 687 // Filename with row count 688 var lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase(); 689 var timestamp = new Date().toISOString().slice(0,10); 690 var filename = lang.startsWith('fr') 691 ? 'export_complet_' + results.length + '_lignes_' + timestamp + '.csv' 692 : 'export_all_' + results.length + '_rows_' + timestamp + '.csv'; 693 694 link.setAttribute('download', filename); 695 link.style.visibility = 'hidden'; 696 document.body.appendChild(link); 697 link.click(); 698 document.body.removeChild(link); 699 700 // Show success message 701 $btn.prop('disabled', false).html('<span class="dashicons dashicons-yes"></span> Done!'); 702 setTimeout(function() { 703 $btn.html(originalText); 704 }, 2000); 705 } else { 706 alert('Error exporting data: ' + (response.data && response.data.message ? response.data.message : 'Unknown error')); 707 $btn.prop('disabled', false).html(originalText); 708 } 709 }, 710 error: function(xhr, status, error) { 711 alert('Export failed. Please try again or reduce the number of results.'); 712 $btn.prop('disabled', false).html(originalText); 713 } 714 }); 502 715 }); 503 716 … … 812 1025 // PATCH: Detect limit errors in AJAX responses and show only one modal 813 1026 function handleLimitModals(response) { 1027 // Check if upgrade is required (even if success is true) 1028 if (response && response.data && response.data.upgrade_required === true) { 1029 const msg = (response.data.error || response.data.message || '').toLowerCase(); 1030 let currentPlan = response.data.current_plan || (typeof askeet_query_assistant !== 'undefined' && askeet_query_assistant.current_plan ? askeet_query_assistant.current_plan : null); 1031 1032 if (msg.includes('daily limit') || msg.includes('50 queries') || msg.includes('upgrade to unlimited')) { 1033 showDailyLimitModal(currentPlan); 1034 return true; 1035 } 1036 if (msg.includes('free limit') || msg.includes('reached your free limit') || msg.includes('upgrade to premium')) { 1037 showPaymentModal(currentPlan); 1038 return true; 1039 } 1040 // Fallback: show payment modal for any upgrade requirement 1041 showPaymentModal(currentPlan); 1042 return true; 1043 } 1044 1045 // Legacy check for success === false 814 1046 if ( 815 1047 response &&
Note: See TracChangeset
for help on using the changeset viewer.