| | |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->cache = CacheManager::for('referrals', WEEK_IN_SECONDS); |
| | | $this->referrals_table = BASE . 'referrals'; |
| | | $this->rewards_table = BASE . 'referral_rewards'; |
| | | $this->referrals_table = $wpdb->prefix . BASE . 'referrals'; |
| | | $this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards'; |
| | | $this->magic_link = new MagicLinkManager(); |
| | | |
| | | $this->referralPage = $this->getReferralPageId(); |
| | |
| | | // Schedule cron jobs for reports |
| | | $this->registerCronJobs(); |
| | | |
| | | // Register admin subpage |
| | | // add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1); |
| | | |
| | | // Add admin bar label for referral page |
| | | add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999); |
| | | |
| | |
| | | |
| | | // Handle settings save |
| | | add_action('admin_init', [$this, 'registerSettings']); |
| | | // Handle admin page form submission |
| | | add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3); |
| | | } |
| | | |
| | | public function addLoginInputs(string $action):void |
| | |
| | | */ |
| | | public function getUserReferralCode(int $user_id, ?string $custom_code = null) |
| | | { |
| | | $user = get_user_by('ID', $user_id); |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return new WP_Error('invalid_user', 'User not found'); |
| | | } |
| | |
| | | return $actions; |
| | | } |
| | | |
| | | /** |
| | | * Display referral sidebar for non-logged-in users |
| | | */ |
| | | function getUnloggedInReferral(): string |
| | | { |
| | | ob_start(); |
| | | JVB()->connect('cloudflare')->renderTurnstile(); |
| | | $turnstile = ob_get_clean(); |
| | | |
| | | $meta = new MetaForm(); |
| | | $codeForm = '<form id="referral-code-form"> |
| | | '.jvbFormStatus().$meta->return('referral_name', null, [ |
| | | 'required' => true, |
| | | 'type' => 'text', |
| | | 'label' => 'Your Name', |
| | | 'placeholder'=> 'Mister Meeseeks', |
| | | 'autocomplete'=>'name' |
| | | ]). |
| | | $meta->return('referral_email', null, [ |
| | | 'required' => true, |
| | | 'type' => 'email', |
| | | 'label' => 'Your Email', |
| | | 'placeholder'=> 'look@me.com', |
| | | 'autocomplete'=> 'email' |
| | | ]). |
| | | $meta->return('referral_code', null, [ |
| | | 'required' => true, |
| | | 'type' => 'text', |
| | | 'label' => 'Referral Code', |
| | | 'pattern' => '[A-Za-z0-9]+', |
| | | 'maxLength' => 20, |
| | | 'autocomplete'=>'off' |
| | | ]).' |
| | | <button type="submit"> |
| | | Get Started |
| | | </button> |
| | | $reward_text = $this->getRewardText(true); |
| | | |
| | | <p class="helper-text"> |
| | | We\'ll send you a link to complete your registration. |
| | | </p> |
| | | '.$turnstile.' |
| | | </form><div class="success-content" hidden> |
| | | <h3>Check Your Email!</h3> |
| | | <p>We\'ve sent you a magic link to complete your registration. Click the link to activate your account and claim your reward!</p> |
| | | <p class="hint">Can\'t find it? Check your spam folder.</p> |
| | | </div>'; |
| | | // Pre-fill code if from referral link |
| | | $prefill_code = $_GET['ref'] ?? ''; |
| | | $referrer_name = ''; |
| | | if ($prefill_code) { |
| | | $referrer = $this->getUserByReferralCode($prefill_code); |
| | | $referrer_name = $referrer ? strtok($referrer->display_name, ' ') : ''; |
| | | } |
| | | |
| | | $loginForm = '<form id ="login-form"> |
| | | '.jvbFormStatus().$meta->return('login_email', null, [ |
| | | $codeForm = '<div class="referral-reward-banner"> |
| | | '.jvbIcon('confetti').' |
| | | <h4>Get ' . esc_html($reward_text) . '!</h4> |
| | | ' . ($referrer_name ? '<p>' . esc_html($referrer_name) . ' invited you to join us</p>' : '') . ' |
| | | </div> |
| | | <form id="referral-code-form"> |
| | | '.jvbFormStatus().$meta->return('referral_name', null, [ |
| | | 'required' => true, |
| | | 'type' => 'text', |
| | | 'label' => 'Your Name', |
| | | 'placeholder'=> 'Mister Meeseeks', |
| | | 'autocomplete'=>'name' |
| | | ]). |
| | | $meta->return('referral_email', null, [ |
| | | 'required' => true, |
| | | 'type' => 'email', |
| | | 'label' => 'Your Email', |
| | | 'placeholder'=> 'look@me.com', |
| | | 'autocomplete'=> 'email' |
| | | ]). |
| | | $meta->return('referral_code', $prefill_code, [ |
| | | 'required' => true, |
| | | 'type' => 'text', |
| | | 'label' => 'Referral Code', |
| | | 'pattern' => '[A-Za-z0-9]+', |
| | | 'maxLength' => 20, |
| | | 'autocomplete'=>'off', |
| | | 'data-referrer' => $referrer_name |
| | | ]).' |
| | | <button type="button" class="button-secondary check-code-btn"> |
| | | '.jvbIcon('check-circle', ['size' => 16]).' Verify Code |
| | | </button> |
| | | <div class="code-status" hidden></div> |
| | | <button type="submit"> |
| | | Get Started |
| | | </button> |
| | | |
| | | <p class="helper-text"> |
| | | We\'ll send you a link to complete your registration. |
| | | </p> |
| | | '.$turnstile.' |
| | | </form> |
| | | <div class="success-content" hidden> |
| | | <h3>Check Your Email!</h3> |
| | | <p>We\'ve sent you a magic link to complete your registration. Click the link to activate your account and claim your reward!</p> |
| | | <p class="hint">Can\'t find it? Check your spam folder.</p> |
| | | </div>'; |
| | | |
| | | $loginForm = '<form id="login-form"> |
| | | '.jvbFormStatus().$meta->return('login_email', null, [ |
| | | 'required' => true, |
| | | 'type' => 'email', |
| | | 'label' => 'Your Email', |
| | | 'autocomplete'=>'email' |
| | | ]).' |
| | | '.$turnstile.' |
| | | <button type="submit">Login With Magic Link</button> |
| | | '.$turnstile.' |
| | | <button type="submit">Login With Magic Link</button> |
| | | </form> |
| | | <div class="success-content" hidden> |
| | | <h3>Check Your Email!</h3> |
| | | <p>We\'ve sent you a magic link to log in - no password required! Click the link in your email to log in.</p> |
| | | <p class="hint">Can\'t find it? Check your spam folder.</p> |
| | | </div>'; |
| | | <div class="success-content" hidden> |
| | | <h3>Check Your Email!</h3> |
| | | <p>We\'ve sent you a magic link to log in - no password required! Click the link in your email to log in.</p> |
| | | <p class="hint">Can\'t find it? Check your spam folder.</p> |
| | | </div>'; |
| | | |
| | | $footer = '<div class="referral-footer"> |
| | | <a href="' . wp_login_url() . '" class="text-link">Prefer to use a password?</a> |
| | | </div>'; |
| | | $tabs = [ |
| | | 'enterCode' => [ |
| | | 'title' => 'Have a Code?', |
| | | 'description' => [ |
| | | 'Enter the code given to you to get 20% off your first treatment!' |
| | | 'Enter your referral code to get started' |
| | | ], |
| | | 'content' => $codeForm |
| | | ], |
| | | 'login' => [ |
| | | 'title' => 'Login', |
| | | 'description' => [ |
| | | 'Login to see your rewards' |
| | | 'Already have an account? Log in to see your rewards' |
| | | ], |
| | | 'content' => $loginForm |
| | | 'content' => $loginForm.$footer |
| | | ] |
| | | ]; |
| | | |
| | | |
| | | |
| | | return jvbRenderTabs($tabs, true); |
| | | } |
| | | |
| | | |
| | | protected function getReferralSuccessMessage(string $code): string |
| | | { |
| | | $referrer = $this->getUserByReferralCode($code); |
| | |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | public function getLoggedInReferral(int $user_id):string |
| | | /** |
| | | * Display referral sidebar for logged-in users |
| | | */ |
| | | public function getLoggedInReferral(int $user_id): string |
| | | { |
| | | // Logged-in user widget |
| | | $referral_code = get_user_meta($user_id, BASE . 'referral_code', true); |
| | | |
| | | // Generate code if user doesn't have one |
| | | if (empty($referral_code)) { |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | if (is_wp_error($referral_code)) { |
| | | return ''; |
| | | } |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | if (is_wp_error($referral_code)) { |
| | | return ''; |
| | | } |
| | | |
| | | $share_url = $this->getShareURL($referral_code); |
| | | |
| | | ob_start(); |
| | | ?> |
| | | <header> |
| | | <h3>Share the ♡</h3> |
| | | <p>Invite your friends.</p> |
| | | <p>Earn rewards when they book!</p> |
| | | </header> |
| | | <header> |
| | | <h3>Share the ♡</h3> |
| | | <p>Invite friends. Earn rewards.</p> |
| | | </header> |
| | | |
| | | <div class="row even share-buttons"> |
| | | <a href="mailto:?subject=<?php echo urlencode('Check out ' . get_bloginfo('name')); ?>&body=<?php echo urlencode('I thought you might be interested: ' . $share_url); ?>" |
| | | class="share-btn email-share"> |
| | | <?php echo jvbIcon('envelope', ['size' => 20]); ?> |
| | | Email |
| | | </a> |
| | | <a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode($share_url); ?>" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="share-btn facebook-share"> |
| | | <?php echo jvbIcon('facebook-logo', ['size' => 20]); ?> |
| | | Facebook |
| | | </a> |
| | | <a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode('Check this out!'); ?>" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="share-btn twitter-share"> |
| | | <?php echo jvbIcon('twitter-logo', ['size' => 20]); ?> |
| | | Twitter |
| | | </a> |
| | | </div> |
| | | <?php $this->getShareButtons($user_id); ?> |
| | | |
| | | <div class="copy-section"> |
| | | <h4>Your Referral Link</h4> |
| | | <div class="row btw"> |
| | | <code id="your-referral-link"><?= esc_url($share_url)?></code> |
| | | <button type="button" class="copy" data-target="your-referral-link"> |
| | | Copy Link |
| | | <div class="copy-group"> |
| | | <code id="referral-link" class="copy-target"><?= esc_url($share_url) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-link" aria-label="Copy referral link"> |
| | | <?php echo jvbIcon('copy', ['size' => 16]); ?> |
| | | </button> |
| | | </div> |
| | | |
| | | |
| | | <h4>Your Code</h4> |
| | | <div class="row btw"> |
| | | <code id="your-referral-code"><?=esc_html($referral_code)?></code> |
| | | <button type="button" class="copy" data-target="your-referral-code"> |
| | | Copy Code |
| | | <div class="copy-group"> |
| | | <code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-code" aria-label="Copy referral code"> |
| | | <?php echo jvbIcon('copy', ['size' => 16]); ?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row btw referral-stats"> |
| | | <div class="stat-item"> |
| | | <span class="stat-value" data-stat="total">-</span> |
| | | <span class="stat-label">Total Referrals</span> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-value" data-stat="treated">-</span> |
| | | <span class="stat-label">Successful</span> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-value" data-stat="pending">-</span> |
| | | <span class="stat-label">Pending</span> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-value" data-stat="rewards">$0.00</span> |
| | | <span class="stat-label">Available Rewards</span> |
| | | </div> |
| | | <div class="recent-referrals-section"> |
| | | <h4>Recent Referrals</h4> |
| | | <div class="recent-referrals-list" data-user-id="<?= $user_id ?>"> |
| | | <div class="loading">Loading...</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="stats-summary"> |
| | | <div class="stat-row"> |
| | | <span class="stat-label">Total Referrals</span> |
| | | <span class="stat-value" data-stat="total">-</span> |
| | | </div> |
| | | <div class="stat-row"> |
| | | <span class="stat-label">Successful</span> |
| | | <span class="stat-value" data-stat="treated">-</span> |
| | | </div> |
| | | <div class="stat-row"> |
| | | <span class="stat-label">Pending</span> |
| | | <span class="stat-value" data-stat="pending">-</span> |
| | | </div> |
| | | <div class="stat-row highlight"> |
| | | <span class="stat-label">Available Rewards</span> |
| | | <span class="stat-value" data-stat="rewards">$0.00</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <a href="<?= get_home_url(null, '/dash/referrals')?>" class="view-dashboard-btn"> |
| | | Dashboard <?= jvbIcon('arrow-right', ['size' => 16]); ?> |
| | | </a> |
| | | |
| | | <?php |
| | | return ob_get_clean(); |
| | |
| | | * @param array $subpages |
| | | * @return array |
| | | */ |
| | | public function addSubpage(array $subpages): array |
| | | public static function addSubpage():void |
| | | { |
| | | $subpages[] = [ |
| | | 'page_title' => 'Referral Settings', |
| | | $subpage = [ |
| | | 'page_title' => 'Referral System', |
| | | 'menu_title' => 'Referrals', |
| | | 'capability' => 'manage_options', |
| | | 'menu_slug' => 'jvb-referrals', |
| | | 'callback' => [$this, 'renderAdminPage'], |
| | | 'icon' => 'users', |
| | | 'menu_slug' => BASE . 'referral-admin', |
| | | 'callback' => [self::class, 'renderAdminPageStatic'] |
| | | ]; |
| | | AdminPages::addSubPage(BASE.'referral-admin', $subpage); |
| | | } |
| | | |
| | | return $subpages; |
| | | /** |
| | | * Static wrapper for renderAdminPage |
| | | * Called by WordPress when admin page is rendered |
| | | */ |
| | | public static function renderAdminPageStatic(): void |
| | | { |
| | | // Get the properly initialized instance from JVB singleton |
| | | JVB()->referrals()->renderAdminPage(); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function renderAdminPage(): void |
| | | { |
| | | // Handle form submission |
| | | if (isset($_POST['submit']) && check_admin_referer(BASE . 'referral_settings_nonce')) { |
| | | update_option(BASE . 'referral_page_id', absint($_POST[BASE . 'referral_page_id'] ?? 0)); |
| | | ?> |
| | | <div class="wrap jvb-admin-wrap"> |
| | | <h1>Referral System Management</h1> |
| | | |
| | | $reward_settings = [ |
| | | 'referrer_reward_applies_to' => sanitize_text_field($_POST['referrer_reward_applies_to'] ?? 'per_user'), |
| | | 'referrer_reward_amount' => floatval($_POST['referrer_reward_amount'] ?? 25.00), |
| | | 'referrer_reward_type' => sanitize_text_field($_POST['referrer_reward_type'] ?? 'fixed'), |
| | | 'referee_reward_type' => sanitize_text_field($_POST['referee_reward_type'] ?? 'percentage'), |
| | | 'referee_reward_amount' => floatval($_POST['referee_reward_amount'] ?? 20), |
| | | 'referee_reward_applies_to' => sanitize_text_field($_POST['referee_reward_applies_to'] ?? 'first_order'), |
| | | ]; |
| | | <!-- CSV Upload Section --> |
| | | <div class="card"> |
| | | <h2>Import Data from Jane App</h2> |
| | | <p>Upload your exported CSV files from Jane App to sync client and sales data.</p> |
| | | |
| | | update_option(BASE . 'referral_reward_settings', $this->sanitizeRewardSettings($reward_settings)); |
| | | <div class="jvb-upload-section" style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;"> |
| | | <!-- Client List Upload --> |
| | | <div class="jvb-upload-box"> |
| | | <h3>Client List</h3> |
| | | <form id="client-upload-form" enctype="multipart/form-data"> |
| | | <input type="file" |
| | | name="client_file" |
| | | id="client_file" |
| | | accept=".csv" |
| | | required /> |
| | | <button type="submit" class="button button-primary" style="margin-top: 10px;"> |
| | | Upload Clients |
| | | </button> |
| | | </form> |
| | | <div id="client-upload-status" style="margin-top: 10px;"></div> |
| | | </div> |
| | | |
| | | echo '<div class="notice notice-success is-dismissible"><p>Settings saved successfully.</p></div>'; |
| | | } |
| | | <!-- Sales Export Upload --> |
| | | <div class="jvb-upload-box"> |
| | | <h3>Sales Export</h3> |
| | | <form id="sales-upload-form" enctype="multipart/form-data"> |
| | | <input type="file" |
| | | name="sales_file" |
| | | id="sales_file" |
| | | accept=".csv" |
| | | required /> |
| | | <button type="submit" class="button button-primary" style="margin-top: 10px;"> |
| | | Upload Sales |
| | | </button> |
| | | </form> |
| | | <div id="sales-upload-status" style="margin-top: 10px;"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | echo $this->renderAdminHTML(); |
| | | <!-- Referrals Table --> |
| | | <div class="card" style="margin-top: 20px;"> |
| | | <h2>Referrals Management</h2> |
| | | |
| | | <div class="jvb-table-controls" style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center;"> |
| | | <label> |
| | | Filter by Status: |
| | | <select id="referral-status-filter"> |
| | | <option value="">All Statuses</option> |
| | | <option value="pending">Pending</option> |
| | | <option value="consulted">Consulted</option> |
| | | <option value="treated">Treated</option> |
| | | <option value="cancelled">Cancelled</option> |
| | | </select> |
| | | </label> |
| | | <input type="text" |
| | | id="referral-search" |
| | | placeholder="Search by name or email..." |
| | | style="min-width: 250px;" /> |
| | | <button type="button" class="button" id="refresh-table">Refresh</button> |
| | | </div> |
| | | |
| | | <div id="referrals-table-container"> |
| | | <div class="jvb-loading">Loading referrals...</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- Settings Section --> |
| | | <?= $this->renderAdminHTML() ?> |
| | | </div> |
| | | |
| | | <style> |
| | | .jvb-upload-box { |
| | | padding: 20px; |
| | | background: #f9f9f9; |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | } |
| | | .jvb-upload-box h3 { |
| | | margin-top: 0; |
| | | } |
| | | .referrals-table { |
| | | width: 100%; |
| | | border-collapse: collapse; |
| | | } |
| | | .referrals-table th, |
| | | .referrals-table td { |
| | | padding: 12px; |
| | | text-align: left; |
| | | border-bottom: 1px solid #ddd; |
| | | } |
| | | .referrals-table th { |
| | | background: #f5f5f5; |
| | | font-weight: 600; |
| | | } |
| | | .referrals-table tr:hover { |
| | | background: #f9f9f9; |
| | | } |
| | | .referral-status { |
| | | padding: 4px 8px; |
| | | border-radius: 3px; |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | } |
| | | .referral-status.pending { |
| | | background: #fff3cd; |
| | | color: #856404; |
| | | } |
| | | .referral-status.consulted { |
| | | background: #d1ecf1; |
| | | color: #0c5460; |
| | | } |
| | | .referral-status.treated { |
| | | background: #d4edda; |
| | | color: #155724; |
| | | } |
| | | .referral-actions { |
| | | display: flex; |
| | | gap: 5px; |
| | | } |
| | | .notice.notice-success, |
| | | .notice.notice-error { |
| | | margin: 10px 0; |
| | | } |
| | | </style> |
| | | |
| | | <script> |
| | | jQuery(document).ready(function($) { |
| | | // Client upload |
| | | // Client upload |
| | | $('#client-upload-form').on('submit', function(e) { |
| | | e.preventDefault(); |
| | | const formData = new FormData(this); |
| | | formData.append('file', $('#client_file')[0].files[0]); |
| | | |
| | | $('#client-upload-status').html('<span class="spinner is-active"></span> Uploading...'); |
| | | |
| | | $.ajax({ |
| | | url: '<?= rest_url('jvb/v1/referrals/upload-clients') ?>', |
| | | method: 'POST', |
| | | data: formData, |
| | | processData: false, |
| | | contentType: false, |
| | | beforeSend: function(xhr) { |
| | | xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>'); |
| | | }, |
| | | success: function(response) { |
| | | if (response.success) { |
| | | let message = '<div class="notice notice-success"><p>' + response.message; |
| | | message += '<br>Created: ' + (response.stats.created || 0); |
| | | message += ', Updated: ' + (response.stats.updated || 0); |
| | | message += ', Skipped: ' + (response.stats.skipped || 0) + '</p>'; |
| | | |
| | | // Show skipped details if any |
| | | if (response.stats.skipped_details && response.stats.skipped_details.length > 0) { |
| | | message += '<details style="margin-top: 10px;"><summary>View skipped records (' + response.stats.skipped_details.length + ')</summary>'; |
| | | message += '<table class="widefat" style="margin-top: 10px;"><thead><tr>'; |
| | | message += '<th>Line</th><th>Name</th><th>Email</th><th>GUID</th><th>Reason</th>'; |
| | | message += '</tr></thead><tbody>'; |
| | | response.stats.skipped_details.forEach(function(item) { |
| | | message += '<tr>'; |
| | | message += '<td>' + (item.line || '-') + '</td>'; |
| | | message += '<td>' + (item.name || '-') + '</td>'; |
| | | message += '<td>' + (item.email || '-') + '</td>'; |
| | | message += '<td>' + (item.guid || '-') + '</td>'; |
| | | message += '<td>' + item.reason + '</td>'; |
| | | message += '</tr>'; |
| | | }); |
| | | message += '</tbody></table></details>'; |
| | | } |
| | | |
| | | message += '</div>'; |
| | | |
| | | $('#client-upload-status').html(message); |
| | | $('#client-upload-form')[0].reset(); |
| | | loadReferralsTable(); |
| | | } else { |
| | | $('#client-upload-status').html( |
| | | '<div class="notice notice-error"><p>' + response.message + '</p></div>' |
| | | ); |
| | | } |
| | | }, |
| | | error: function(xhr) { |
| | | $('#client-upload-status').html( |
| | | '<div class="notice notice-error"><p>Upload failed</p></div>' |
| | | ); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | // Sales upload |
| | | $('#sales-upload-form').on('submit', function(e) { |
| | | e.preventDefault(); |
| | | const formData = new FormData(this); |
| | | formData.append('file', $('#sales_file')[0].files[0]); |
| | | |
| | | $('#sales-upload-status').html('<span class="spinner is-active"></span> Uploading...'); |
| | | |
| | | $.ajax({ |
| | | url: '<?= rest_url('jvb/v1/referrals/upload-sales') ?>', |
| | | method: 'POST', |
| | | data: formData, |
| | | processData: false, |
| | | contentType: false, |
| | | beforeSend: function(xhr) { |
| | | xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>'); |
| | | }, |
| | | success: function(response) { |
| | | if (response.success) { |
| | | $('#sales-upload-status').html( |
| | | '<div class="notice notice-success"><p>' + response.message + |
| | | '<br>Consultations: ' + response.stats.consultations + |
| | | ', Treatments: ' + response.stats.treatments + |
| | | ', Skipped: ' + response.stats.skipped + '</p></div>' |
| | | ); |
| | | $('#sales-upload-form')[0].reset(); |
| | | loadReferralsTable(); |
| | | } else { |
| | | $('#sales-upload-status').html( |
| | | '<div class="notice notice-error"><p>' + response.message + '</p></div>' |
| | | ); |
| | | } |
| | | }, |
| | | error: function(xhr) { |
| | | $('#sales-upload-status').html( |
| | | '<div class="notice notice-error"><p>Upload failed</p></div>' |
| | | ); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | // Load referrals table |
| | | function loadReferralsTable(page = 1) { |
| | | const status = $('#referral-status-filter').val(); |
| | | const search = $('#referral-search').val(); |
| | | |
| | | $.ajax({ |
| | | url: '<?= rest_url('jvb/v1/referrals/list') ?>', |
| | | method: 'GET', |
| | | data: { |
| | | page: page, |
| | | per_page: 20, |
| | | status: status, |
| | | search: search |
| | | }, |
| | | beforeSend: function(xhr) { |
| | | xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>'); |
| | | $('#referrals-table-container').html('<div class="jvb-loading">Loading...</div>'); |
| | | }, |
| | | success: function(response) { |
| | | if (response.success) { |
| | | renderReferralsTable(response); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // Render table |
| | | function renderReferralsTable(data) { |
| | | let html = '<table class="referrals-table widefat">'; |
| | | html += '<thead><tr>'; |
| | | html += '<th>Referrer</th>'; |
| | | html += '<th>Referee</th>'; |
| | | html += '<th>Email</th>'; |
| | | html += '<th>Status</th>'; |
| | | html += '<th>Referred Date</th>'; |
| | | html += '<th>Total Referrals</th>'; |
| | | html += '<th>Actions</th>'; |
| | | html += '</tr></thead><tbody>'; |
| | | |
| | | if (data.referrals.length === 0) { |
| | | html += '<tr><td colspan="7" style="text-align: center;">No referrals found</td></tr>'; |
| | | } else { |
| | | data.referrals.forEach(function(ref) { |
| | | html += '<tr>'; |
| | | html += '<td>' + (ref.referrer_name || 'Unknown') + '</td>'; |
| | | html += '<td>' + (ref.referee_display_name || ref.referee_name) + '</td>'; |
| | | html += '<td>' + (ref.referee_display_email || ref.referee_email) + '</td>'; |
| | | html += '<td><span class="referral-status ' + ref.status + '">' + ref.status + '</span></td>'; |
| | | html += '<td>' + new Date(ref.referred_at).toLocaleDateString() + '</td>'; |
| | | html += '<td>' + (ref.referrer_total_referrals || 0) + '</td>'; |
| | | html += '<td class="referral-actions">'; |
| | | |
| | | if (ref.status === 'pending') { |
| | | html += '<button class="button button-small mark-consulted" data-id="' + ref.id + '">Mark Consulted</button>'; |
| | | } |
| | | if (ref.status !== 'treated') { |
| | | html += '<button class="button button-small mark-treated" data-id="' + ref.id + '">Mark Treated</button>'; |
| | | } |
| | | |
| | | html += '</td>'; |
| | | html += '</tr>'; |
| | | }); |
| | | } |
| | | |
| | | html += '</tbody></table>'; |
| | | |
| | | // Add pagination |
| | | if (data.total_pages > 1) { |
| | | html += '<div class="tablenav"><div class="tablenav-pages">'; |
| | | for (let i = 1; i <= data.total_pages; i++) { |
| | | const active = i === data.page ? 'button-primary' : 'button'; |
| | | html += '<button class="button ' + active + ' page-link" data-page="' + i + '">' + i + '</button> '; |
| | | } |
| | | html += '</div></div>'; |
| | | } |
| | | |
| | | $('#referrals-table-container').html(html); |
| | | } |
| | | |
| | | // Event handlers for actions |
| | | $(document).on('click', '.mark-consulted', function() { |
| | | const id = $(this).data('id'); |
| | | if (!confirm('Mark this referral as consulted? This will create the consultation reward.')) return; |
| | | |
| | | $.ajax({ |
| | | url: '<?= rest_url('jvb/v1/referrals/mark-consulted') ?>', |
| | | method: 'POST', |
| | | data: JSON.stringify({ referral_id: id }), |
| | | contentType: 'application/json', |
| | | beforeSend: function(xhr) { |
| | | xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>'); |
| | | }, |
| | | success: function(response) { |
| | | if (response.success) { |
| | | alert(response.message); |
| | | loadReferralsTable(); |
| | | } else { |
| | | alert('Error: ' + response.message); |
| | | } |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | $(document).on('click', '.mark-treated', function() { |
| | | const id = $(this).data('id'); |
| | | if (!confirm('Mark this referral as treated? This will create rewards for both parties.')) return; |
| | | |
| | | $.ajax({ |
| | | url: '<?= rest_url('jvb/v1/referrals/mark-treated') ?>', |
| | | method: 'POST', |
| | | data: JSON.stringify({ referral_id: id }), |
| | | contentType: 'application/json', |
| | | beforeSend: function(xhr) { |
| | | xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>'); |
| | | }, |
| | | success: function(response) { |
| | | if (response.success) { |
| | | alert(response.message); |
| | | loadReferralsTable(); |
| | | } else { |
| | | alert('Error: ' + response.message); |
| | | } |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | $(document).on('click', '.page-link', function() { |
| | | loadReferralsTable($(this).data('page')); |
| | | }); |
| | | |
| | | $('#referral-status-filter, #refresh-table').on('change click', function() { |
| | | loadReferralsTable(); |
| | | }); |
| | | |
| | | // Search with debounce |
| | | let searchTimeout; |
| | | $('#referral-search').on('keyup', function() { |
| | | clearTimeout(searchTimeout); |
| | | searchTimeout = setTimeout(function() { |
| | | loadReferralsTable(); |
| | | }, 500); |
| | | }); |
| | | |
| | | // Initial load |
| | | loadReferralsTable(); |
| | | }); |
| | | </script> |
| | | <?php |
| | | } |
| | | |
| | | protected function renderAdminHTML():string |
| | |
| | | <h1>Referral Settings</h1> |
| | | |
| | | <form method="post" action=""> |
| | | <?php wp_nonce_field(BASE . 'referral_settings_nonce'); ?> |
| | | <?php wp_nonce_field(BASE . 'admin_page_nonce'); ?> |
| | | |
| | | <div class="card"> |
| | | <h2>Referral Page</h2> |
| | |
| | | </th> |
| | | <td> |
| | | <?php |
| | | if (!$this->referralPage) { |
| | | $this->referralPage = $this->getReferralPageId(); |
| | | } |
| | | wp_dropdown_pages([ |
| | | 'name' => BASE . 'referral_page_id', |
| | | 'id' => BASE . 'referral_page_id', |
| | |
| | | </select> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="<?= BASE ?>client_import_role">Client Import Role</label> |
| | | </th> |
| | | <td> |
| | | <?php |
| | | $selected_role = get_option(BASE . 'client_import_role', ''); |
| | | $roles = wp_roles()->get_names(); |
| | | ?> |
| | | <select name="<?= BASE ?>client_import_role" id="<?= BASE ?>client_import_role"> |
| | | <?php foreach ($roles as $role_value => $role_name): ?> |
| | | <option value="<?= esc_attr($role_value) ?>" <?php selected($selected_role, $role_value); ?>> |
| | | <?= esc_html($role_name) ?> |
| | | </option> |
| | | <?php endforeach; ?> |
| | | </select> |
| | | <p class="description"> |
| | | Role assigned to users imported from Jane App client list. |
| | | </p> |
| | | </td> |
| | | </tr> |
| | | </table> |
| | | </div> |
| | | |
| | |
| | | /** |
| | | * Render referral statistics |
| | | */ |
| | | protected function renderReferralStats(bool $wrapCard = false):string |
| | | protected function renderReferralStats(bool $wrapCard = false): string |
| | | { |
| | | ob_start(); |
| | | global $wpdb; |
| | | global $wpdb; // Use fresh global instead of stored reference |
| | | |
| | | $total_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table}"); |
| | | $pending_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table} WHERE status = 'pending'"); |
| | | $treated_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table} WHERE status = 'treated'"); |
| | | ob_start(); |
| | | |
| | | // Get fresh table name references |
| | | $referrals_table = $wpdb->prefix . BASE . 'referrals'; |
| | | |
| | | // Use proper WordPress prepare even for COUNT |
| | | $total_referrals = $wpdb->get_var( |
| | | $wpdb->prepare( |
| | | "SELECT COUNT(*) FROM `{$referrals_table}` WHERE 1=%d", |
| | | 1 |
| | | ) |
| | | ); |
| | | |
| | | $pending_referrals = $wpdb->get_var( |
| | | $wpdb->prepare( |
| | | "SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s", |
| | | 'pending' |
| | | ) |
| | | ); |
| | | |
| | | $treated_referrals = $wpdb->get_var( |
| | | $wpdb->prepare( |
| | | "SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s", |
| | | 'treated' |
| | | ) |
| | | ); |
| | | |
| | | ?> |
| | | <table class="widefat"> |
| | | <tr> |
| | | <th>Total Referrals</th> |
| | | <td><?= esc_html($total_referrals) ?></td> |
| | | <td><?= esc_html($total_referrals ?? 0) ?></td> |
| | | </tr> |
| | | <tr> |
| | | <th>Pending</th> |
| | | <td><?= esc_html($pending_referrals) ?></td> |
| | | <td><?= esc_html($pending_referrals ?? 0) ?></td> |
| | | </tr> |
| | | <tr> |
| | | <th>Treated</th> |
| | | <td><?= esc_html($treated_referrals) ?></td> |
| | | <td><?= esc_html($treated_referrals ?? 0) ?></td> |
| | | </tr> |
| | | </table> |
| | | <?php |
| | | $table = ob_get_clean(); |
| | | |
| | | if ($wrapCard) { |
| | | $table = '<div class="card"> |
| | | <h2>Referral Statistics</h2> |
| | | '.$table.' |
| | | </div>'; |
| | | <h2>Referral Statistics</h2> |
| | | ' . $table . ' |
| | | </div>'; |
| | | } |
| | | |
| | | return $table; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | if (!$this->referralPage) { |
| | | $this->referralPage = $this->getReferralPageId(); |
| | | } |
| | | if (!$this->referralPage) { |
| | | return; |
| | | } |
| | | |
| | |
| | | if ('post.php' !== $pagenow || !$post) { |
| | | return; |
| | | } |
| | | |
| | | if (!$this->referralPage) { |
| | | $this->referralPage = $this->getReferralPageId(); |
| | | } |
| | | if ($post->ID === $this->referralPage) { |
| | | echo '<div class="notice notice-info">'; |
| | | echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvbase') . '</p>'; |
| | |
| | | } |
| | | } |
| | | |
| | | public function renderDashPage(string $content, string $page):string |
| | | public function renderDashPage(string $content, string $page): string |
| | | { |
| | | if ($page !== 'referrals') { |
| | | if ($page !== 'Referrals') { |
| | | return $content; |
| | | } |
| | | $out = ''; |
| | | if (current_user_can('manage_options')) { |
| | | $out .= $this->renderAdminHTML(); |
| | | } else { |
| | | $out .= $this->renderReferralStats(true); |
| | | |
| | | // Regular users get their referral dashboard |
| | | $user_id = get_current_user_id(); |
| | | $referral_code = get_user_meta($user_id, BASE . 'referral_code', true); |
| | | |
| | | if (!$referral_code) { |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | } |
| | | return ($out === '') ? $content : '<form id="referrals" class="col" data-save="referrals">'.$out.'</form>'; |
| | | |
| | | $stats = $this->getUserStats($user_id); |
| | | $referrals = $this->getUserReferrals($user_id, ['limit' => 20]); |
| | | |
| | | ob_start(); |
| | | ?> |
| | | <div class="referral-dashboard"> |
| | | <div class="referral-header"> |
| | | <h2>Your Referrals</h2> |
| | | <p>Share your code and earn rewards when your referrals complete their first treatment!</p> |
| | | </div> |
| | | |
| | | <?php $this->getShareButtons($user_id); ?> |
| | | |
| | | <!-- Referral Code Card --> |
| | | <div class="referral-code-card"> |
| | | <h3>Your Referral Code</h3> |
| | | <div class="code-display"> |
| | | <span class="code"><?= esc_html($referral_code) ?></span> |
| | | <button class="button copy-code" data-code="<?= esc_attr($referral_code) ?>"> |
| | | Copy Code |
| | | </button> |
| | | </div> |
| | | <p class="share-link"> |
| | | Share link: <input type="text" readonly value="<?= home_url('/?ref=' . $referral_code) ?>" |
| | | onclick="this.select()" style="width: 100%; margin-top: 5px;" /> |
| | | </p> |
| | | </div> |
| | | <form class="invite"> |
| | | <?php |
| | | $meta = new MetaForm(); |
| | | $field = [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Invite Your Friends', |
| | | 'fields' => [ |
| | | 'name' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'name', |
| | | ], |
| | | 'email' => [ |
| | | 'type' => 'email', |
| | | 'label' => 'email', |
| | | ] |
| | | ] |
| | | ]; |
| | | $meta->render('invite', [], $field); |
| | | ?> |
| | | </form> |
| | | |
| | | <!-- Stats Grid --> |
| | | <div class="stats-grid"> |
| | | <div class="stat-card"> |
| | | <h4>Total Referrals</h4> |
| | | <span class="stat-number"><?= esc_html($stats['total_referrals'] ?? 0) ?></span> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <h4>Completed Treatments</h4> |
| | | <span class="stat-number"><?= esc_html($stats['treated_count'] ?? 0) ?></span> |
| | | </div> |
| | | <div class="stat-card"> |
| | | <h4>Pending</h4> |
| | | <span class="stat-number"><?= esc_html($stats['pending_count'] ?? 0) ?></span> |
| | | </div> |
| | | <div class="stat-card highlight"> |
| | | <h4>Available Rewards</h4> |
| | | <span class="stat-number">$<?= number_format($stats['available_rewards'] ?? 0, 2) ?></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- Referrals List --> |
| | | <div class="referrals-list-card"> |
| | | <h3>Your Referrals</h3> |
| | | <?php if (empty($referrals)): ?> |
| | | <p>You haven't referred anyone yet. Share your code to get started!</p> |
| | | <?php else: ?> |
| | | <table class="referrals-table"> |
| | | <thead> |
| | | <tr> |
| | | <th>Name</th> |
| | | <th>Email</th> |
| | | <th>Status</th> |
| | | <th>Referred Date</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <?php foreach ($referrals as $ref): ?> |
| | | <tr> |
| | | <td><?= esc_html($ref->referee_name) ?></td> |
| | | <td><?= esc_html($ref->referee_email) ?></td> |
| | | <td><span class="status-badge <?= esc_attr($ref->status) ?>"><?= esc_html(ucfirst($ref->status)) ?></span></td> |
| | | <td><?= date('M j, Y', strtotime($ref->referred_at)) ?></td> |
| | | </tr> |
| | | <?php endforeach; ?> |
| | | </tbody> |
| | | </table> |
| | | <?php endif; ?> |
| | | </div> |
| | | </div> |
| | | |
| | | |
| | | <script> |
| | | jQuery(document).ready(function($) { |
| | | $('.copy-code').on('click', function() { |
| | | const code = $(this).data('code'); |
| | | navigator.clipboard.writeText(code).then(function() { |
| | | alert('Code copied to clipboard!'); |
| | | }); |
| | | }); |
| | | }); |
| | | </script> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | /** |
| | | * Handle admin page form submission |
| | | * |
| | | * @param mixed $result Previous result |
| | | * @param string $page_slug Current page slug |
| | | * @param array $post_data POST data |
| | | * @return array|null Result array or null if not our page |
| | | */ |
| | | public function handleAdminSubmission($result, string $page_slug, array $post_data): ?array |
| | | { |
| | | // Only handle our page |
| | | if ($page_slug !== BASE . 'referral-admin') { |
| | | return $result; |
| | | } |
| | | |
| | | try { |
| | | // Save referral page |
| | | $page_id = isset($post_data[BASE . 'referral_page_id']) ? absint($post_data[BASE . 'referral_page_id']) : 0; |
| | | update_option(BASE . 'referral_page_id', $page_id); |
| | | |
| | | // Save client import role |
| | | $import_role = sanitize_text_field($post_data[BASE . 'client_import_role'] ?? JVB_USER); |
| | | update_option(BASE . 'client_import_role', $import_role); |
| | | |
| | | // Save reward settings |
| | | $settings = [ |
| | | 'referrer_reward_type' => sanitize_text_field($post_data['referrer_reward_type'] ?? 'fixed'), |
| | | 'referrer_reward_amount' => floatval($post_data['referrer_reward_amount'] ?? 25.00), |
| | | 'referrer_reward_applies_to' => sanitize_text_field($post_data['referrer_reward_applies_to'] ?? 'per_user'), |
| | | 'referee_reward_type' => sanitize_text_field($post_data['referee_reward_type'] ?? 'percentage'), |
| | | 'referee_reward_amount' => floatval($post_data['referee_reward_amount'] ?? 20), |
| | | 'referee_reward_applies_to' => sanitize_text_field($post_data['referee_reward_applies_to'] ?? 'first_order') |
| | | ]; |
| | | |
| | | update_option(BASE . 'referral_settings', $settings); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'message' => 'Referral settings saved successfully!' |
| | | ]; |
| | | |
| | | } catch (\Exception $e) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Failed to save settings: ' . $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get formatted reward text for referee |
| | | * |
| | | * @param bool $full Include "off your first treatment" text |
| | | * @return string |
| | | */ |
| | | public function getRewardText(bool $full = true): string |
| | | { |
| | | $reward_amount = $this->settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $this->settings['referee_reward_type'] ?? 'percentage'; |
| | | |
| | | $reward_text = $reward_type === 'percentage' |
| | | ? $reward_amount . '% off' |
| | | : '$' . number_format($reward_amount, 2) . ' off'; |
| | | |
| | | if ($full) { |
| | | $reward_text .= ' your first treatment'; |
| | | } |
| | | |
| | | return $reward_text; |
| | | } |
| | | |
| | | public function getShareButtons(int $user_id):void |
| | | { |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | if (is_wp_error($referral_code)) { |
| | | return; |
| | | } |
| | | |
| | | $share_url = $this->getShareURL($referral_code); |
| | | $referral_page_id = $this->getReferralPageId(); |
| | | |
| | | // SMS share text |
| | | $sms_text = urlencode("Check out " . get_bloginfo('name') . "! " . $share_url); |
| | | |
| | | // Share message |
| | | $share_message = urlencode("I love " . get_bloginfo('name') . "! Thought you might want to check them out."); |
| | | |
| | | ?> |
| | | <nav class="share"> |
| | | <h4>Quick Share</h4> |
| | | <ul class="share-buttons-grid"> |
| | | <a href="mailto:?subject=<?php echo urlencode('Check out ' . get_bloginfo('name')); ?>&body=<?php echo urlencode($share_message . ' ' . $share_url); ?>" |
| | | class="button" title="Email"> |
| | | <?php echo jvbIcon('envelope'); ?> |
| | | </a> |
| | | <a href="sms:?&body=<?php echo $sms_text; ?>" |
| | | class="button" title="Text"> |
| | | <?php echo jvbIcon('chat'); ?> |
| | | </a> |
| | | <a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode($share_url); ?>" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="button" title="Facebook"> |
| | | <?php echo jvbIcon('facebook-logo'); ?> |
| | | </a> |
| | | <a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode($share_message); ?>" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="button" title="Twitter"> |
| | | <?php echo jvbIcon('twitter-logo'); ?> |
| | | </a> |
| | | <a href="https://wa.me/?text=<?php echo $sms_text; ?>" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="button" title="WhatsApp"> |
| | | <?php echo jvbIcon('whatsapp-logo'); ?> |
| | | </a> |
| | | </ul> |
| | | </nav> |
| | | <?php |
| | | } |
| | | } |
| | | |