Plugin Directory

Changeset 3395155


Ignore:
Timestamp:
11/13/2025 03:52:21 PM (3 months ago)
Author:
2wstechnologies
Message:

version 2 of plugin with new export method and annual payment and many others optimizations

Location:
askeet/trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • askeet/trunk/README.txt

    r3340838 r3395155  
    55Requires at least: 6.2
    66Tested up to: 6.8
    7 Stable tag: 1.0
     7Stable tag: 2.0
    88License: GPLv2 or later
    99License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    223223* Export functionality (Pro)
    224224
     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
    225236= Upcoming Features =
    226237
     
    237248Initial release. Install now to start getting instant insights from your WooCommerce data!
    238249
     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
    239260== Source Code ==
    240261The 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.
     
    251272* **Website:** [https://www.askeet.ai/](https://www.askeet.ai/)
    252273* **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)
    253276
    254277== Why Choose Askeet? ==
  • askeet/trunk/askeet.php

    r3389781 r3395155  
    33 * Plugin Name: Askeet
    44 * 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.0
     5 * Version: 2.0
    66 * Author: Reach Technologies
    77 * License: GPL-2.0+
     
    3737        update_option('askeet_install_id', wp_generate_password(24, false));
    3838    }
    39     define('ASKEET_VERSION', '1.0');
     39    define('ASKEET_VERSION', '2.0');
    4040
    4141    $install_id = get_option('askeet_install_id');
     
    105105        add_action('wp_ajax_askeet_process_query_request', array($this, 'askeet_process_query_request'));
    106106        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'));
    107108        add_action('wp_ajax_askeet_save_query_history', array($this, 'askeet_save_query_history'));
    108109        add_action('wp_ajax_askeet_delete_query_history', array($this, 'askeet_delete_query_history'));
     
    163164        }
    164165        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);
    166167        $current_plan = '';
    167168        $install_id = get_option('askeet_install_id');
     
    446447        }
    447448        $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;
    449450        $offset = ($page - 1) * $per_page;
     451        $export_mode = isset($_POST['export_mode']) && $_POST['export_mode'];
    450452        $disable_pagination = false;
    451453        $results = [];
    452454        $total_count = 0;
     455        $actual_rows = $per_page; // Actual rows fetched (before display limit)
     456        $display_limited = false;
    453457        $start_time = microtime(true);
    454458
     
    459463            $execution_time = round(microtime(true) - $start_time, 4);
    460464            $total_count = count($results);
     465            $actual_rows = count($results);
    461466        } else {
    462467            // Toujours retirer LIMIT/OFFSET existant
     
    468473            $results = $wpdb->get_results($paginated_query, ARRAY_A);
    469474            $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
    470483            // Compter le total (sans LIMIT)
    471484            $count_query = $sql_query_no_limit ?? $sql_query;
     
    513526            'page' => $page,
    514527            'per_page' => $per_page,
     528            'actual_rows' => $actual_rows,
     529            'display_limited' => $display_limited,
    515530            '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
    516587        ));
    517588        exit;
     
    735806          </div>
    736807          <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>
    739810            <a class="btn" href="mailto:[email protected]" id="btn-support"><?php echo esc_html__('Contact Support', 'askeet'); ?></a>
    740811          </div>
     
    919990        return;
    920991    }
     992
     993    // Get current plan
    921994    $api_url = askeet_get_api_url() . '/get-plan';
    922995    $response = wp_remote_post($api_url, [
     
    925998        'timeout' => 15,
    926999    ]);
    927     $plan = $next_payment = $price = '';
     1000
     1001    $plan_code = 'free';
     1002    $free_queries_left = 10;
     1003    $subscription_end_date = null;
    9281004    if (!is_wp_error($response)) {
    9291005        $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
    9361016    $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
    9691322}
    9701323
  • askeet/trunk/assets/css/style.css

    r3323715 r3395155  
    4848  color: white;
    4949  border: none;
    50   padding: 32px 34px;
     50  padding: 17px 34px;
    5151  cursor: pointer;
    5252  font-weight: 600;
     
    5656  transition: background 0.3s ease;
    5757  border-radius: 10px;
    58   background: linear-gradient(180deg, rgba(0, 90, 158, 0.74) 0%, #005A9E 100%);
     58  background: linear-gradient(180deg, rgb(61 63 66 / 74%) 0%, #13191e 100%);
    5959}
    6060
    6161.btn:hover {
    62   background: linear-gradient(90deg, #004d8c 74%, #004d8c 100%);
     62  background: #004d8c;
     63  color: white!important;
    6364}
    6465
     
    528529    margin-bottom: 8px;
    529530}
     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  
    77    let lastPage = 1;
    88    let lastPerPage = 20;
     9    let currentPerPage = 20; // User-selected rows per page
     10    let lastTotalCount = 0; // Total results count
    911    let chatHistory = [];
    1012    let lastQueryRequest = '';
     
    380382                sql_query: currentSqlQuery,
    381383                nonce: askeet_query_assistant.nonce,
    382                 page: pageNum
     384                page: pageNum,
     385                per_page: currentPerPage
    383386            },
    384387            success: function(response) {
    385388                // console.log('[WCQA] AJAX success:', response); // Debug log
    386389                removeLoading();
     390
     391                // Check for upgrade requirement
     392                if (handleLimitModals(response)) return;
     393
    387394                if (response.success) {
    388395                    lastResults = response.data.results;
     
    396403                    lastPage = response.data.page;
    397404                    lastPerPage = response.data.per_page;
     405                    lastTotalCount = response.data.total_count;
    398406                    const totalPages = Math.max(1, Math.ceil(response.data.total_count / response.data.per_page));
    399407                    let html = '';
    400408                    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">&times;</button>';
     453                            html += '</div>';
     454                        }
     455
    401456                        // Show summary message
    402457                        const startIdx = (response.data.page - 1) * response.data.per_page + 1;
    403458                        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
    405462                        html += '<div class="results-block">' + renderResultsTable(response.data.results, response.data.total_count, response.data.page) + '</div>';
     463
    406464                        if (!response.data.disable_pagination) {
    407465                            html += renderPagination(response.data.total_count, response.data.page, response.data.per_page);
    408466                        }
    409                         html += '<button id="export-csv" class="button" style="margin-top:10px;">'+askeet_query_assistant_i18n.export_csv+'</button>';
    410467                    } else {
    411468                        html += '<div class="query-success">'+askeet_query_assistant_i18n.no_result_found+'</div>';
    412469                    }
    413470                    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                    }
    414495                }
    415496                else {
     
    453534        }
    454535    });
    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) {
    456547        e.preventDefault();
     548        if (currentSqlQuery === '') {
     549            alert(askeet_query_assistant_i18n.no_sql_to_execute);
     550            return;
     551        }
     552
    457553        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);
    470630            return;
    471631        }
    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        });
    502715    });
    503716
     
    8121025    // PATCH: Detect limit errors in AJAX responses and show only one modal
    8131026    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
    8141046        if (
    8151047            response &&
Note: See TracChangeset for help on using the changeset viewer.