| | |
| | | |
| | | use JVBase\managers\MagicLinkManager; |
| | | use JVBase\integrations\Cloudflare; |
| | | use JVBase\meta\MetaForm; |
| | | use JVBase\meta\Form; |
| | | use JVBase\ui\CRUDSkeleton; |
| | | use JVBase\ui\Tabs; |
| | | use JVBase\utility\Features; |
| | | use WP_User; |
| | | use WP_Error; |
| | |
| | | { |
| | | protected $wpdb; |
| | | protected MagicLinkManager $magic_link; |
| | | protected CacheManager $cache; |
| | | protected Cache $cache; |
| | | protected Cache $requestCache; |
| | | protected Cache $statsCache; |
| | | protected string $referrals_table; |
| | | protected ?int $referralPage = null; |
| | | protected string $rewards_table; |
| | | |
| | | protected CustomTable $referrals; |
| | | protected CustomTable $codes; |
| | | protected CustomTable $janeClients; |
| | | protected CustomTable $rewards; |
| | | protected CustomTable $treatments; |
| | | |
| | | // Default reward settings |
| | | protected array $default_settings = [ |
| | | 'referrer_reward_applies_to' => 'per_user', // 'per_user' or 'flat_total' |
| | |
| | | 'referrer_reward_type' => 'fixed', |
| | | 'referee_reward_type' => 'percentage', // 'percentage' or 'fixed' |
| | | 'referee_reward_amount' => 20, // 20% or $20 |
| | | 'referee_reward_applies_to' => 'first_order' // 'first_order' or 'all_orders' |
| | | 'referee_reward_applies_to' => 'first_order', // 'first_order' or 'all_orders' |
| | | 'referral_role' => BASE.'client' |
| | | ]; |
| | | |
| | | protected string $role = BASE.'client'; |
| | | |
| | | protected array $settings; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->cache = new CacheManager('referrals'); |
| | | $this->referrals_table = BASE . 'referrals'; |
| | | $this->rewards_table = BASE . 'referral_rewards'; |
| | | $this->magic_link = new MagicLinkManager(); |
| | | $this->cache = Cache::for('referrals', WEEK_IN_SECONDS); |
| | | $this->requestCache = Cache::for('referral_requests', WEEK_IN_SECONDS)->connect('referrals', true); |
| | | $this->statsCache = Cache::for('referral_stats', WEEK_IN_SECONDS)->connect('referrals', true); |
| | | |
| | | // Hook into user registration to track referrals |
| | | $this->referrals_table = $wpdb->prefix . BASE . 'referrals'; |
| | | $this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards'; |
| | | |
| | | $this->referralPage = $this->getReferralPageId(); |
| | | $this->settings = $this->getRewardSettings(); |
| | | |
| | | |
| | | add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3); |
| | | add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1); |
| | | add_action('user_register', [$this, 'processReferral'], 10, 1); |
| | | |
| | | // Add meta boxes for admin to manage referrals |
| | |
| | | add_action('personal_options_update', [$this, 'saveUserReferralCode']); |
| | | add_action('edit_user_profile_update', [$this, 'saveUserReferralCode']); |
| | | |
| | | add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2); |
| | | add_filter('jvbNewUserEmail', [$this, 'addReferralToWelcomeEmail'], 99, 2); |
| | | |
| | | |
| | | add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']); |
| | |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); |
| | | // Schedule cron jobs for reports |
| | | $this->registerCronJobs(); |
| | | |
| | | // Add admin bar label for referral page |
| | | add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999); |
| | | |
| | | // Add admin notice to referral page edit screen |
| | | add_action('admin_notices', [$this, 'showReferralPageNotice']); |
| | | |
| | | |
| | | add_filter('jvbDashboardPage', [$this, 'renderDashPage'], 10, 2); |
| | | |
| | | // Handle settings save |
| | | add_action('admin_init', [$this, 'registerSettings']); |
| | | // Handle admin page form submission |
| | | add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3); |
| | | } |
| | | |
| | | protected function defineTables():void |
| | | { |
| | | $this->defineReferralsTable(); |
| | | $this->defineCodeTable(); |
| | | $this->defineJaneClientsTable(); |
| | | $this->defineRewardsTable(); |
| | | $this->defineTreatmentsTable(); |
| | | } |
| | | protected function defineReferralsTable():void |
| | | { |
| | | $table = CustomTable::for('referrals'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'from_user' => "{$table->getUserIDType()} NOT NULL", |
| | | 'to_user' => "{$table->getUserIDType()} NOT NULL", |
| | | 'to_name' => 'varchar(255) NOT NULL', |
| | | 'to_email' => 'varchar(255) NOT NULL', |
| | | 'to_phone' => 'varchar(50) NOT NULL', |
| | | 'referral_code'=> 'varchar(50) NOT NULL', |
| | | 'status' => "ENUM('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'", |
| | | 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'consulted_at'=> 'datetime DEFAULT NULL', |
| | | 'treated_at'=> 'datetime DEFAULT NULL', |
| | | 'treatment_count' => 'int DEFAULT 0', |
| | | 'notes' => 'text DEFAULT NULL', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'UNIQUE', 'value' => 'to_user (`to_user`)'], |
| | | 'from_user (`from_user`)', |
| | | 'status (`status`)', |
| | | 'code (`referral_code`)', |
| | | 'date (`created_at`)', |
| | | 'consult (`consulted_at`)' |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}referral_from_user_fk` FOREIGN KEY (`from_user`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}referral_to_user_fk` FOREIGN KEY (`to_user`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->referrals = $table; |
| | | } |
| | | |
| | | protected function defineCodeTable():void |
| | | { |
| | | $table = CustomTable::for('referrals_codes'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'code' => 'varchar(50) NOT NULL', |
| | | 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'UNIQUE', 'value' => 'code (`code`)'], |
| | | 'user (`user_id`)', |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}referral_code_user_fk` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->codes = $table; |
| | | } |
| | | protected function defineJaneClientsTable():void |
| | | { |
| | | $table = CustomTable::for('referrals_jane_clients'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'patient_guid' => 'varchar(50) NOT NULL', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'first_name' => 'varchar(100) NOT NULL', |
| | | 'last_name' => 'varchar(100) NOT NULL', |
| | | 'email' => 'varchar(255) NOT NULL', |
| | | 'imported_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => 'patient_guid (`patient_guid`)'], |
| | | 'user (`user_id`)', |
| | | 'email (`email`)', |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}jane_clients_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->janeClients = $table; |
| | | } |
| | | protected function defineRewardsTable():void |
| | | { |
| | | $table = CustomTable::for('referrals_rewards'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'referral_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'reward_type' => "ENUM('referrer', 'referee') NOT NULL", |
| | | 'amount' => 'decimal(10,2) NOT NULL', |
| | | 'reward_calculation'=> "ENUM('percentage', 'fixed')", |
| | | 'status' => "ENUM('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available'", |
| | | 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | 'redeemed_at' => 'datetime DEFAULT NULL', |
| | | 'expires_at' => 'datetime DEFAULT NULL', |
| | | 'notes' => 'text DEFAULT NULL', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | 'referral (`referral_id`)', |
| | | 'user (`user_id`)', |
| | | 'status (`status`)', |
| | | 'type (`reward_type`)' |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}reward_referral` FOREIGN KEY (`referral_id`) |
| | | REFERENCES {$this->referrals->getFullTableName()} (`id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}reward_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES {$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->rewards = $table; |
| | | } |
| | | protected function defineTreatmentsTable():void |
| | | { |
| | | $table = CustomTable::for('referral_treatments'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'referral_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'treatment_type'=> 'varchar(100) NOT NULL', //Tier 1-6, Brows, etc |
| | | 'treatment_date'=> 'datetime NOT NULL', |
| | | 'invoice_number'=> 'varchar(50) DEFAULT NULL', |
| | | 'amount' => 'decimal(10,2) DEFAULT NULL', |
| | | 'status' => "ENUM('completed', 'no_show', 'cancelled') DEFAULT 'completed'", |
| | | 'imported_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | 'referral (`referral_id`)', |
| | | 'user (`user_id`)', |
| | | 'date (`treatment_date`)', |
| | | 'type (`treatment_type`)', |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}treatment_referral` FOREIGN KEY (`referral_id`) |
| | | REFERENCES `{$this->referrals->getFullTableName()}` (`id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}treatment_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->treatments = $table; |
| | | } |
| | | |
| | | public function getSettings():array |
| | | { |
| | | return $this->settings; |
| | | } |
| | | public function getRole():string |
| | | { |
| | | return $this->role; |
| | | } |
| | | public function addLoginInputs(string $action):void |
| | | { |
| | | if (array_key_exists('ref', $_GET)) { |
| | | echo '<input type="hidden" name="referral_code" value="'.$_GET['ref'].'">'; |
| | | } |
| | | } |
| | | |
| | | public function modifyLoginText(array $defaults):array |
| | | { |
| | | if (!array_key_exists('ref', $_GET)){ |
| | | return $defaults; |
| | | } |
| | | |
| | | $code = $_GET['ref']; |
| | | $user = $this->getUserByReferralCode($code); |
| | | |
| | | $desc = ($user) ? strtok($user->display_name, ' ') . ' invited you ' : 'You\'ve been invited '; |
| | | |
| | | $defaults['title'] = 'Register your Account. Get Your Reward.'; |
| | | $defaults['description'] = [ |
| | | $desc.' to see the difference with us.', |
| | | 'Oh, and you\'ll get 20% off your first treatment!', |
| | | 'Finish this account creation (you\'ll still need to make an account on Jane App to book), or let us know when you come in for your appointment.' |
| | | ]; |
| | | $defaults['extra'] = [ |
| | | 'Once you\'re in, you\'ll get your own code you can share.', |
| | | 'Get a $25 credit for <b>each</b> person you send our way who comes in for their first treatment!' |
| | | ]; |
| | | return $defaults; |
| | | } |
| | | |
| | | public function enqueueScripts():void |
| | |
| | | 'jvb-a11y', |
| | | 'jvb-popup', |
| | | 'jvb-tabs', |
| | | 'jvb-data-store', |
| | | ]; |
| | | |
| | | if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) { |
| | | $requirements[] = 'cloudflare-turnstile'; |
| | | JVB()->connect('cloudflare')->enqueueTurnstileScripts(); |
| | | } |
| | | if (is_singular(BASE.'dash')) { |
| | | $requirements[] = 'jvb-form'; |
| | | $requirements[] = 'jvb-view'; |
| | | |
| | | wp_enqueue_script('jvb-referral-admin', |
| | | JVB_URL.'assets/js/min/referralAdmin.min.js', |
| | | ['jvb-referral'], |
| | | '1.0.0', |
| | | true); |
| | | } |
| | | wp_enqueue_script( |
| | | 'jvb-referral', |
| | |
| | | } |
| | | } |
| | | |
| | | public function createCode(int $user_id, string $code):string|false |
| | | { |
| | | $code = sanitize_title($code); |
| | | $existing = $this->codes->get(['code' => $code]); |
| | | if ($existing) { |
| | | if ($existing['user_id'] !== $user_id) { |
| | | return false; |
| | | } |
| | | return $code; |
| | | } |
| | | $success = $this->codes->insert([ |
| | | 'user_id' => $user_id, |
| | | 'code' => $code |
| | | ]); |
| | | if ($success) { |
| | | return $code; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Generate or get existing referral code for a user |
| | | * |
| | |
| | | * @param string|null $custom_code Optional custom code |
| | | * @return string|WP_Error |
| | | */ |
| | | public function getUserReferralCode(int $user_id, ?string $custom_code = null) |
| | | public function getUserReferralCode(int $user_id, ?string $custom_code = null):array|wp_error |
| | | { |
| | | $user = get_user_by('ID', $user_id); |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return new WP_Error('invalid_user', 'User not found'); |
| | | } |
| | | |
| | | // Check if user already has a code |
| | | $existing_code = get_user_meta($user_id, BASE . 'referral_code', true); |
| | | |
| | | if ($existing_code && !$custom_code) { |
| | | return $existing_code; |
| | | $existing = $this->codes->pluck('code', ['user_id' => $user_id],'created_at', 'DESC'); |
| | | if ($existing && !$custom_code) { |
| | | return $existing; |
| | | } |
| | | if ($custom_code && !in_array($custom_code, $existing)) { |
| | | $test = $this->createCode($user_id, $custom_code); |
| | | if ($test) { |
| | | return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC'); |
| | | } |
| | | } else { |
| | | return $existing; |
| | | } |
| | | |
| | | // Generate new code if custom provided or none exists |
| | | $code = $custom_code ?: $this->generateReferralCode($user); |
| | | $code = $this->generateReferralCode($user); |
| | | |
| | | // Validate uniqueness |
| | | if ($this->isCodeTaken($code, $user_id)) { |
| | | return new WP_Error('code_taken', 'This referral code is already in use'); |
| | | $success = $this->createCode($user_id, $code); |
| | | if ($success) { |
| | | return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC'); |
| | | } |
| | | |
| | | // Save the code |
| | | update_user_meta($user_id, BASE . 'referral_code', $code); |
| | | |
| | | return $code; |
| | | } |
| | | |
| | |
| | | * Check if a referral code is already taken |
| | | * |
| | | * @param string $code |
| | | * @param int|null $exclude_user_id |
| | | * @return bool |
| | | */ |
| | | protected function isCodeTaken(string $code, ?int $exclude_user_id = null): bool |
| | | protected function isCodeTaken(string $code): bool |
| | | { |
| | | $args = [ |
| | | 'meta_key' => BASE . 'referral_code', |
| | | 'meta_value' => $code, |
| | | 'fields' => 'ID', |
| | | 'number' => 1 |
| | | ]; |
| | | |
| | | if ($exclude_user_id) { |
| | | $args['exclude'] = [$exclude_user_id]; |
| | | } |
| | | |
| | | $users = get_users($args); |
| | | return !empty($users); |
| | | return (bool) $this->codes->get(['code' => $code]); |
| | | } |
| | | |
| | | public function processRegistrationToken(int $user_id, string $email, array $data): void |
| | | { |
| | | // Check for referral code in data |
| | | $code = $data['referral_code'] ?? ''; |
| | | if (empty($code)) { |
| | | return; |
| | | } |
| | | |
| | | // Store in session/cookie for processReferral to pick up |
| | | if (session_status() === PHP_SESSION_NONE) { |
| | | session_start(); |
| | | } |
| | | $_SESSION[BASE . 'referral_code'] = sanitize_text_field($code); |
| | | setcookie( |
| | | BASE . 'referral_code', |
| | | sanitize_text_field($code), |
| | | time() + (86400 * 30), |
| | | '/' |
| | | ); |
| | | } |
| | | /** |
| | | * Track a new referral when user registers |
| | | * |
| | | * @param int $user_id |
| | | * @param array $userData |
| | | * @return bool; |
| | | */ |
| | | public function processReferral(int $user_id): void |
| | | public function processReferral(int $user_id, array $userData): bool |
| | | { |
| | | // Check if user was created via referral magic link |
| | | $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true); |
| | | $referral = $this->referrals->get(['to_user' => $user_id]); |
| | | |
| | | if (!$referral_code) { |
| | | return; |
| | | if (empty($referral)) { |
| | | $referral = $this->referrals->get(['to_email' => $userData['email']]); |
| | | } |
| | | if (empty($referral)) { |
| | | // Check session/cookie if not in meta |
| | | if (session_status() === PHP_SESSION_NONE) { |
| | | session_start(); |
| | | } |
| | | $referral_code = $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? ''; |
| | | if (!empty ($referral_code)) { |
| | | $referral = [ |
| | | 'to_user' => $user_id, |
| | | 'referral_code' => $referral_code, |
| | | 'to_email' => $userData['user_email'] |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | if (empty($referral)) { |
| | | return false; // No referral code - regular registration |
| | | } |
| | | |
| | | // Find the referrer |
| | | $referrer = $this->getUserByReferralCode($referral_code); |
| | | $referrer = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]); |
| | | if (empty($referrer)) { |
| | | //This should not happen, but whatever |
| | | return false; |
| | | } |
| | | $referrer = $referrer[0]; |
| | | $record = $this->referrals->findOrCreate([ |
| | | 'to_user' => $user_id, |
| | | 'referral_code' => $referral['referral_code'], |
| | | ], [ |
| | | 'from_user' => $referrer, |
| | | 'to_email' => $referral['to_email'], |
| | | 'to_name' => $userData['first_name'], |
| | | // 'to_phone' => |
| | | 'status' => 'pending' |
| | | ]); |
| | | |
| | | if (!$referrer) { |
| | | delete_user_meta($user_id, BASE . 'pending_referral_code'); |
| | | return; |
| | | if (!$record) { |
| | | error_log('[ReferralManager]::processReferral Could not update record for user: '.print_r($referral, true)); |
| | | return false; |
| | | } |
| | | |
| | | // Check for duplicates |
| | | $existing = $this->getReferralByReferee($user_id); |
| | | if ($existing) { |
| | | delete_user_meta($user_id, BASE . 'pending_referral_code'); |
| | | return; |
| | | |
| | | // Clean up temp data |
| | | delete_user_meta($user_id, BASE . 'pending_referral_code'); |
| | | if (isset($_SESSION[BASE . 'referral_code'])) { |
| | | unset($_SESSION[BASE . 'referral_code']); |
| | | } |
| | | if (isset($_COOKIE[BASE . 'referral_code'])) { |
| | | setcookie(BASE . 'referral_code', '', time() - 3600, '/'); |
| | | } |
| | | |
| | | // Create referral record |
| | | $result = $this->createReferral($referrer->ID, $user_id, $referral_code); |
| | | // Clear caches |
| | | $this->cache->flush(); |
| | | |
| | | if ($result) { |
| | | // Clean up temp meta |
| | | delete_user_meta($user_id, BASE . 'pending_referral_code'); |
| | | // Fire action for tracking |
| | | do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral['referral_code']); |
| | | |
| | | // Fire action for tracking |
| | | do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code); |
| | | } |
| | | // Send notification to referrer |
| | | $this->sendReferrerNotification($referrer->ID, $userData['display_name']); |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | |
| | | * @param string $code |
| | | * @return int|false |
| | | */ |
| | | protected function createReferral(int $referrer_id, int $referee_id, string $code) |
| | | public function createReferral(int $referrer_id, int $referee_id, string $code) |
| | | { |
| | | $user = get_user_by('ID', $referee_id); |
| | | |
| | |
| | | */ |
| | | public function getUserByReferralCode(string $code): ?WP_User |
| | | { |
| | | $users = get_users([ |
| | | 'meta_key' => BASE . 'referral_code', |
| | | 'meta_value' => $code, |
| | | 'number' => 1 |
| | | ]); |
| | | |
| | | return $users[0] ?? null; |
| | | return $this->cache->remember( |
| | | $code, |
| | | function () use ($code) { |
| | | $users = get_users([ |
| | | 'meta_key' => BASE . 'referral_code', |
| | | 'meta_value' => $code, |
| | | 'number' => 1 |
| | | ]); |
| | | return $users[0] ?? null; |
| | | } |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | return; |
| | | } |
| | | |
| | | $settings = $this->getRewardSettings(); |
| | | |
| | | // Create referrer reward |
| | | $this->wpdb->insert( |
| | | $this->rewards_table, |
| | |
| | | 'referral_id' => $referral_id, |
| | | 'user_id' => $referral->referrer_id, |
| | | 'reward_type' => 'referrer', |
| | | 'amount' => $settings['referrer_reward_amount'], |
| | | 'amount' => $this->settings['referrer_reward_amount'], |
| | | 'status' => 'available', |
| | | 'created_at' => current_time('mysql') |
| | | ], |
| | |
| | | ); |
| | | |
| | | // Create referee reward |
| | | $referee_amount = $settings['referee_reward_type'] === 'percentage' |
| | | ? $settings['referee_reward_amount'] // Store as percentage |
| | | : $settings['referee_reward_amount']; // Store as fixed amount |
| | | $referee_amount = $this->settings['referee_reward_type'] === 'percentage' |
| | | ? $this->settings['referee_reward_amount'] // Store as percentage |
| | | : $this->settings['referee_reward_amount']; // Store as fixed amount |
| | | |
| | | $this->wpdb->insert( |
| | | $this->rewards_table, |
| | |
| | | 'user_id' => $referral->referee_id, |
| | | 'reward_type' => 'referee', |
| | | 'amount' => $referee_amount, |
| | | 'reward_calculation' => $settings['referee_reward_type'], |
| | | 'reward_calculation' => $this->settings['referee_reward_type'], |
| | | 'status' => 'available', |
| | | 'created_at' => current_time('mysql') |
| | | ], |
| | |
| | | |
| | | $args = wp_parse_args($args, $defaults); |
| | | |
| | | $where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id); |
| | | return $this->requestCache->remember( |
| | | $this->requestCache->generateKey(array_merge(['user'=>$user_id], $args)), |
| | | function() use ($user_id, $args) { |
| | | $where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id); |
| | | |
| | | if ($args['status'] !== 'all') { |
| | | $where .= $this->wpdb->prepare(" AND status = %s", $args['status']); |
| | | } |
| | | if ($args['status'] !== 'all') { |
| | | $where .= $this->wpdb->prepare(" AND status = %s", $args['status']); |
| | | } |
| | | |
| | | $query = "SELECT * FROM {$this->referrals_table} |
| | | $query = "SELECT * FROM {$this->referrals_table} |
| | | {$where} |
| | | ORDER BY {$args['orderby']} {$args['order']} |
| | | LIMIT {$args['limit']} OFFSET {$args['offset']}"; |
| | | |
| | | return $this->wpdb->get_results($query); |
| | | $results = $this->wpdb->get_results($query); |
| | | |
| | | return array_map(function($referral) { |
| | | $last_invite = get_transient('referral_last_invite_' . md5($referral->referee_email)); |
| | | $can_resend = !$last_invite || (time() - $last_invite) > WEEK_IN_SECONDS; |
| | | $status = match($referral->status) { |
| | | 'consulted' => 'Awaiting Treatment', |
| | | 'treated' => 'Rewarded!', |
| | | default => 'Pending', |
| | | }; |
| | | return [ |
| | | 'id' => $referral->id, |
| | | 'referee_name' => $referral->referee_name, |
| | | 'referee_email' => $referral->referee_email, |
| | | 'referred_at' => JVB()->routes('referral')->formatTimestamp($referral->referred_at), |
| | | 'referral_status'=> $status, |
| | | 'can_resend' => $can_resend |
| | | ]; |
| | | }, $results); |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function getUserStats(int $user_id): array |
| | | { |
| | | $cache_key = 'stats_' . $user_id; |
| | | $cached = $this->cache->get($cache_key); |
| | | return $this->statsCache->remember( |
| | | $user_id, |
| | | function() use ($user_id) { |
| | | $stats = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT |
| | | COUNT(*) as code_used, |
| | | SUM(CASE WHEN status IN ('consulted', 'treated') THEN 1 ELSE 0 END) as consultations, |
| | | SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treatments, |
| | | SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending |
| | | FROM {$this->referrals_table} |
| | | WHERE referrer_id = %d", |
| | | $user_id |
| | | ), ARRAY_A); |
| | | |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | // Get total rewards earned (available + redeemed) |
| | | $rewards = $this->wpdb->get_var($this->wpdb->prepare( |
| | | "SELECT SUM(amount) |
| | | FROM {$this->rewards_table} |
| | | WHERE user_id = %d AND reward_type = 'referrer'", |
| | | $user_id |
| | | )); |
| | | |
| | | $stats = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT |
| | | COUNT(*) as total_referrals, |
| | | SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count, |
| | | SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count |
| | | FROM {$this->referrals_table} |
| | | WHERE referrer_id = %d", |
| | | $user_id |
| | | ), ARRAY_A); |
| | | |
| | | // Get total rewards |
| | | $rewards = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT |
| | | SUM(CASE WHEN status = 'available' THEN amount ELSE 0 END) as available_rewards, |
| | | SUM(CASE WHEN status = 'redeemed' THEN amount ELSE 0 END) as redeemed_rewards |
| | | FROM {$this->rewards_table} |
| | | WHERE user_id = %d AND reward_type = 'referrer'", |
| | | $user_id |
| | | ), ARRAY_A); |
| | | |
| | | $stats = array_merge($stats, $rewards); |
| | | |
| | | $this->cache->set($cache_key, $stats, HOUR_IN_SECONDS); |
| | | |
| | | return $stats; |
| | | $stats['total_rewards'] = floatval($rewards ?? 0); |
| | | $stats['user_id'] = $user_id; |
| | | return $stats; |
| | | } |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function getTopReferrers(int $limit = 10, string $period = 'all'): array |
| | | { |
| | | $where = ''; |
| | | return $this->statsCache->remember( |
| | | $this->statsCache->generateKey(['limit'=>$limit, 'period' => $period]), |
| | | function() use ($limit, $period) { |
| | | $where = ''; |
| | | |
| | | if ($period !== 'all') { |
| | | $date_where = match($period) { |
| | | 'day' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)", |
| | | 'week' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)", |
| | | 'month' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)", |
| | | default => "1=1" |
| | | }; |
| | | if ($period !== 'all') { |
| | | $date_where = match($period) { |
| | | 'day' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)", |
| | | 'week' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)", |
| | | 'month' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)", |
| | | default => "1=1" |
| | | }; |
| | | |
| | | $where = "WHERE {$date_where}"; |
| | | } |
| | | $where = "WHERE {$date_where}"; |
| | | } |
| | | |
| | | $query = "SELECT |
| | | $query = "SELECT |
| | | referrer_id, |
| | | COUNT(*) as referral_count, |
| | | SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count |
| | |
| | | ORDER BY referral_count DESC |
| | | LIMIT {$limit}"; |
| | | |
| | | $results = $this->wpdb->get_results($query); |
| | | $results = $this->wpdb->get_results($query); |
| | | |
| | | // Enrich with user data |
| | | foreach ($results as &$result) { |
| | | $user = get_user_by('ID', $result->referrer_id); |
| | | $result->user_name = $user ? $user->display_name : 'Unknown'; |
| | | $result->user_email = $user ? $user->user_email : ''; |
| | | } |
| | | // Enrich with user data |
| | | foreach ($results as &$result) { |
| | | $user = get_user_by('ID', $result->referrer_id); |
| | | $result->user_name = $user ? $user->display_name : 'Unknown'; |
| | | $result->user_email = $user ? $user->user_email : ''; |
| | | } |
| | | |
| | | return $results; |
| | | return $results; |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | $yesterday = date('Y-m-d', strtotime('-1 day')); |
| | | |
| | | // Get new referrals from yesterday |
| | | $new_referrals = $this->wpdb->get_results($this->wpdb->prepare( |
| | | "SELECT |
| | | r.*, |
| | | u.display_name as referrer_name |
| | | "SELECT r.*, u.display_name as referrer_name |
| | | FROM {$this->referrals_table} r |
| | | JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID |
| | | WHERE DATE(r.referred_at) = %s |
| | |
| | | $yesterday |
| | | )); |
| | | |
| | | // Only send if there's at least 1 new referral |
| | | if (empty($new_referrals)) { |
| | | return; |
| | | } |
| | | |
| | | // Build email content |
| | | $content = '<h2>Daily Referral Report</h2>'; |
| | | $content .= '<p><strong>' . count($new_referrals) . '</strong> new referral' . |
| | | (count($new_referrals) !== 1 ? 's' : '') . ' yesterday (' . $yesterday . ')</p>'; |
| | | $content = JVB()->email()->h1('Daily Referral Report'); |
| | | $content .= JVB()->email()->stat( |
| | | count($new_referrals), |
| | | count($new_referrals) === 1 ? 'New Referral' : 'New Referrals', |
| | | 'From ' . $yesterday |
| | | ); |
| | | |
| | | $content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">'; |
| | | $content .= '<thead><tr style="background: #f5f5f5;">'; |
| | | $content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referee</th>'; |
| | | $content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Email</th>'; |
| | | $content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referrer</th>'; |
| | | $content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Code</th>'; |
| | | $content .= '</tr></thead><tbody>'; |
| | | $content .= JVB()->email()->spacer(20); |
| | | $content .= JVB()->email()->h2('New Referrals'); |
| | | |
| | | // Build list of referrals |
| | | foreach ($new_referrals as $ref) { |
| | | $content .= '<tr>'; |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html($ref->referee_name)); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html($ref->referee_email)); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html($ref->referrer_name)); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html($ref->referral_code)); |
| | | $content .= '</tr>'; |
| | | $cardContent = sprintf( |
| | | '<p><strong>%s</strong> (%s)</p>', |
| | | esc_html($ref->referee_name), |
| | | esc_html($ref->referee_email) |
| | | ); |
| | | $cardContent .= sprintf( |
| | | '<p style="font-size:13px;color:%s;">Referred by: %s | Code: %s</p>', |
| | | JVB()->email()->colours['dark-200'], |
| | | esc_html($ref->referrer_name), |
| | | JVB()->email()->badge($ref->referral_code, 'info') |
| | | ); |
| | | |
| | | $content .= JVB()->email()->card($cardContent); |
| | | } |
| | | |
| | | $content .= '</tbody></table>'; |
| | | |
| | | // Get admin email |
| | | $to = get_option('admin_email'); |
| | | $subject = sprintf('[%s] %d New Referral%s', |
| | | $subject = sprintf( |
| | | '[%s] %d New Referral%s', |
| | | get_bloginfo('name'), |
| | | count($new_referrals), |
| | | count($new_referrals) !== 1 ? 's' : ''); |
| | | count($new_referrals) !== 1 ? 's' : '' |
| | | ); |
| | | |
| | | |
| | | jvbMail($to, $subject, $content); |
| | | JVB()->email()->sendEmail($to, $subject, $content, 'DAILY REPORT'); |
| | | } |
| | | |
| | | /** |
| | |
| | | $top_referrers = $this->getTopReferrers(10, 'week'); |
| | | $total_referrals = $this->wpdb->get_var( |
| | | "SELECT COUNT(*) FROM {$this->referrals_table} |
| | | WHERE referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)" |
| | | WHERE referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)" |
| | | ); |
| | | |
| | | if ($total_referrals == 0) { |
| | | return; |
| | | } |
| | | |
| | | $content = JVB()->email()->h1('Weekly Referral Summary'); |
| | | $content .= JVB()->email()->stat( |
| | | $total_referrals, |
| | | 'Total Referrals', |
| | | 'This week' |
| | | ); |
| | | |
| | | $content .= JVB()->email()->spacer(30); |
| | | $content .= JVB()->email()->h2('Top 10 Referrers'); |
| | | |
| | | // Leaderboard style |
| | | $rank = 1; |
| | | foreach ($top_referrers as $referrer) { |
| | | $rankBadge = $rank <= 3 |
| | | ? JVB()->email()->badge('#' . $rank, $rank === 1 ? 'success' : 'info') |
| | | : '<span style="font-weight:600;color:' . JVB()->email()->colours['dark-200'] . ';">#' . $rank . '</span>'; |
| | | |
| | | $cardContent = sprintf( |
| | | '<p>%s <strong>%s</strong></p>', |
| | | $rankBadge, |
| | | esc_html($referrer->user_name) |
| | | ); |
| | | |
| | | $stats = [ |
| | | JVB()->email()->stat($referrer->referral_count, 'Total Referrals'), |
| | | JVB()->email()->stat($referrer->treated_count, 'Treated') |
| | | ]; |
| | | $cardContent .= JVB()->email()->grid($stats, 2); |
| | | |
| | | $content .= JVB()->email()->card($cardContent); |
| | | $rank++; |
| | | } |
| | | |
| | | $to = get_option('admin_email'); |
| | | $subject = '[' . get_bloginfo('name') . '] Weekly Referral Summary - ' . date('F j, Y'); |
| | | |
| | | $message = $this->generateWeeklyReportEmail($top_referrers, $total_referrals); |
| | | |
| | | wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8']); |
| | | JVB()->email()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY'); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | protected function generateCSV(array $referrals): string |
| | | { |
| | | $csv = "Referred By,Referee Name,Referee Email,Referee Phone,Referral Code,Status,Referred At,Treated At\n"; |
| | | $cache = Cache::for('referralCSV', HOUR_IN_SECONDS)->connect('referrals'); |
| | | return $cache->remember( |
| | | 'csv', |
| | | function () use ($referrals) { |
| | | $csv = "Referred By,Referee Name,Referee Email,Referee Phone,Referral Code,Status,Referred At,Treated At\n"; |
| | | |
| | | foreach ($referrals as $referral) { |
| | | $csv .= sprintf( |
| | | '"%s","%s","%s","%s","%s","%s","%s","%s"' . "\n", |
| | | $referral->referrer_name ?? 'Unknown', |
| | | $referral->referee_name, |
| | | $referral->referee_email, |
| | | $referral->referee_phone, |
| | | $referral->referral_code, |
| | | $referral->status, |
| | | $referral->referred_at, |
| | | $referral->treated_at ?? 'Not yet' |
| | | ); |
| | | } |
| | | foreach ($referrals as $referral) { |
| | | $csv .= sprintf( |
| | | '"%s","%s","%s","%s","%s","%s","%s","%s"' . "\n", |
| | | $referral->referrer_name ?? 'Unknown', |
| | | $referral->referee_name, |
| | | $referral->referee_email, |
| | | $referral->referee_phone, |
| | | $referral->referral_code, |
| | | $referral->status, |
| | | $referral->referred_at, |
| | | $referral->treated_at ?? 'Not yet' |
| | | ); |
| | | } |
| | | |
| | | return $csv; |
| | | } |
| | | |
| | | /** |
| | | * Generate HTML email for daily report |
| | | * |
| | | * @param array $referrals |
| | | * @param string $period |
| | | * @return string |
| | | */ |
| | | protected function generateReportEmail(array $referrals, string $period): string |
| | | { |
| | | $count = count($referrals); |
| | | |
| | | $content = sprintf('<p>You have <strong>%d new referral%s</strong> today.</p>', |
| | | $count, |
| | | $count !== 1 ? 's' : '' |
| | | return $csv; |
| | | } |
| | | ); |
| | | |
| | | $content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">'; |
| | | $content .= '<thead><tr style="background: #f5f5f5; text-align: left;">'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">Referred By</th>'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">New User</th>'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">Email</th>'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">Status</th>'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">Time</th>'; |
| | | $content .= '</tr></thead><tbody>'; |
| | | |
| | | foreach ($referrals as $referral) { |
| | | $content .= '<tr>'; |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html($referral->referrer_name ?? 'Unknown')); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html($referral->referee_name)); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html($referral->referee_email)); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html(ucfirst($referral->status))); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html(date('g:i A', strtotime($referral->referred_at)))); |
| | | $content .= '</tr>'; |
| | | } |
| | | |
| | | $content .= '</tbody></table>'; |
| | | $content .= '<p><small>See attached CSV for full details.</small></p>'; |
| | | |
| | | return jvbGetEmailTemplate($content, 'Daily Referral Report'); |
| | | } |
| | | |
| | | /** |
| | |
| | | $total_referrals !== 1 ? 's' : '' |
| | | ); |
| | | |
| | | $content .= '<h3>Top 10 Referrers This Week</h3>'; |
| | | $content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">'; |
| | | $content .= '<thead><tr style="background: #f5f5f5; text-align: left;">'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">Rank</th>'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">User</th>'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">Total Referrals</th>'; |
| | | $content .= '<th style="padding: 10px; border: 1px solid #ddd;">Treated</th>'; |
| | | $content .= '</tr></thead><tbody>'; |
| | | |
| | | $referrers = []; |
| | | $rank = 1; |
| | | foreach ($top_referrers as $referrer) { |
| | | $content .= '<tr>'; |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>', $rank++); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>', |
| | | esc_html($referrer->user_name)); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>', |
| | | $referrer->referral_count); |
| | | $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>', |
| | | $referrer->treated_count); |
| | | $content .= '</tr>'; |
| | | $referrers[] = [ |
| | | 'label' => '#' . $rank++ . ' - ' . esc_html($referrer->user_name), |
| | | 'value' => sprintf( |
| | | '<strong>Total Referrals:</strong> %d | <strong>Treated:</strong> %d', |
| | | $referrer->referral_count, |
| | | $referrer->treated_count |
| | | ) |
| | | ]; |
| | | } |
| | | |
| | | $content .= '</tbody></table>'; |
| | | $content .= JVB()->email()->table($referrers, 'Top 10 Referrers This Week'); |
| | | |
| | | return jvbGetEmailTemplate($content, 'Weekly Referral Summary'); |
| | | return $content; |
| | | } |
| | | |
| | | /** |
| | |
| | | </table> |
| | | <?php endif; ?> |
| | | |
| | | <?php /** |
| | | <script> |
| | | function markReferralTreated(referralId) { |
| | | if (!confirm('Mark this referral as treated? This will create reward records.')) { |
| | |
| | | } |
| | | </script> |
| | | <?php |
| | | */ |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | |
| | | $user_id = get_current_user_id(); |
| | | $content = '<aside class="jvb-referral right">'; |
| | | $content = '<aside class="main referral right">'; |
| | | if (!$user_id) { |
| | | $content .= $this->getUnloggedInReferral(); |
| | | } else { |
| | |
| | | $content .= '</aside>'; |
| | | |
| | | $actions[] =[ |
| | | 'button' => '<button type="button" class="toggle-referral row" title="Your Referrals" data-action="toggle-referral" aria-label="Open Referral Sidebar" aria-controls="referral" aria-expanded="false"> |
| | | 'button' => '<button type="button" class="attn toggle-referral row" title="Your Referrals" data-action="toggle-referral" aria-label="Open Referral Sidebar" aria-controls="referral" aria-expanded="false"> |
| | | '.jvbIcon('hand-heart').'<span class="screen-reader-text"></span> |
| | | </button>', |
| | | 'content' => $content |
| | |
| | | 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> |
| | | |
| | | <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>'; |
| | | $reward_text = $this->getRewardText(true); |
| | | |
| | | $loginForm = '<form id ="login-form"> |
| | | '.jvbFormStatus().$meta->return('login_email', null, [ |
| | | // 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, ' ') : ''; |
| | | } |
| | | |
| | | $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(). ' |
| | | <input type="hidden" name="user_select" value="' . esc_attr(get_option(BASE.'referral_role','client')) . '"> |
| | | ' .Form::render('referral_name', '', [ |
| | | 'required' => true, |
| | | 'type' => 'text', |
| | | 'label' => 'Your Name', |
| | | 'placeholder'=> 'Mister Meeseeks', |
| | | 'autocomplete'=>'name' |
| | | ]). |
| | | Form::render('referral_email', '', [ |
| | | 'required' => true, |
| | | 'type' => 'email', |
| | | 'label' => 'Your Email', |
| | | 'placeholder'=> 'look@me.com', |
| | | 'autocomplete'=> 'email' |
| | | ]). |
| | | Form::render('referral_code', $prefill_code, [ |
| | | 'required' => true, |
| | | 'type' => 'text', |
| | | 'label' => 'Referral Code', |
| | | 'pattern' => '[A-Za-z0-9]+', |
| | | 'maxLength' => 20, |
| | | 'autocomplete'=>'off', |
| | | 'data-referrer' => $referrer_name |
| | | ]).' |
| | | '.$turnstile.' |
| | | <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> |
| | | </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().Form::render('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 ''; |
| | | } |
| | | |
| | | $settings = $this->getRewardSettings(); |
| | | $reward_amount = $settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $settings['referee_reward_type'] ?? 'percentage'; |
| | | $reward_amount = $this->settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $this->settings['referee_reward_type'] ?? 'percentage'; |
| | | |
| | | $reward_text = $reward_type === 'percentage' |
| | | ? $reward_amount . '% off' |
| | |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | 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 = home_url('/?ref=' . $referral_code); |
| | | |
| | | $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', ['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', ['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 row btw nowrap"> |
| | | <code id="referral-link" class="copy-target"><?= esc_url($share_url) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link"> |
| | | <?= jvbIcon('copy'); ?> |
| | | <?= jvbIcon('check-circle'); ?> |
| | | </button> |
| | | </div> |
| | | <p class="hint">Quickest and easiest: autofills your code.</p> |
| | | |
| | | |
| | | <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 row btw nowrap"> |
| | | <code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code"> |
| | | <?= jvbIcon('copy'); ?> |
| | | <?= jvbIcon('check-circle'); ?> |
| | | </button> |
| | | </div> |
| | | <p class="hint">Manually copy and paste the code</p> |
| | | |
| | | <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> |
| | | |
| | | <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> |
| | | <p class="hint">Bulk-invite your friends via email - the link will pre-fill their name, email, and code!</p> |
| | | |
| | | <?php |
| | | return ob_get_clean(); |
| | |
| | | return $content; |
| | | } |
| | | |
| | | $settings = get_option(BASE . 'referral_settings', []); |
| | | $reward_amount = $settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $settings['referee_reward_type'] ?? 'percentage'; |
| | | $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'; |
| | | |
| | | $bonus_content = '<div style="background: #e7f5ff; padding: 20px; border-radius: 8px; margin: 20px 0;">'; |
| | | $bonus_content .= '<h3 style="margin-top: 0; color: #2271b1;">🎉 Welcome Bonus!</h3>'; |
| | | $bonus_content .= '<p>Since you were referred by a friend, you\'ve earned <strong>' . $reward_text . '</strong> your first booking!</p>'; |
| | | $bonus_content .= '<p>Your reward will be automatically applied when you book.</p>'; |
| | | $bonus_content .= '</div>'; |
| | | $bonus_content = sprintf( |
| | | ' |
| | | <h3>Thanks for the ♡</h3> |
| | | <p>Since you were referred by a friend, you\'ve earned <strong>%s</strong> your first booking!</p> |
| | | <p>Your reward will be automatically applied when you book.</p>', |
| | | $reward_text, |
| | | ); |
| | | |
| | | // Insert bonus content after the first paragraph |
| | | $parts = explode('</p>', $content, 2); |
| | | if (count($parts) === 2) { |
| | | return $parts[0] . '</p>' . $bonus_content . $parts[1]; |
| | | |
| | | $code = $this->getUserReferralCode($user->ID); |
| | | $yourCode = ''; |
| | | if (!is_wp_error($code)) { |
| | | $share_url = $this->getShareURL($code); |
| | | $yourCode = sprintf( |
| | | '<div class="callout"> |
| | | <h3>Share the ♡ with Friends</h3> |
| | | <p>If you find you love what we can do for you, you can share your own code!</p> |
| | | <p>Your Referral Code: <strong>%s</strong></p> |
| | | <p>Or click the button below:</p> |
| | | %s |
| | | </div>', |
| | | JVB()->email()->link($code), |
| | | JVB()->email()->button($share_url, 'Share Your Code') |
| | | ); |
| | | } |
| | | |
| | | return $content . $bonus_content; |
| | | |
| | | return $content . $bonus_content . $yourCode; |
| | | } |
| | | |
| | | public function getShareURL(string $code):string |
| | | { |
| | | return add_query_arg( |
| | | [ |
| | | 'ref' => $code |
| | | ], |
| | | get_home_url() |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | * @param int $user_id Referrer's user ID |
| | | * @param string $invitee_email Email of person to invite |
| | | * @param string $invitee_name Name of person to invite |
| | | * @param string $subject |
| | | * @param string $message |
| | | * @return array|WP_Error Result with success/error |
| | | */ |
| | | public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name):array|WP_Error |
| | | public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name, string $subject, string $message):array|WP_Error |
| | | { |
| | | // Verify user exists |
| | | if (!$this->checkUser($user_id)) { |
| | | return new WP_Error('invalid_user', 'Invalid user ID'); |
| | | } |
| | | |
| | | // Check email rate limit (15/hour) |
| | | $rate_check = $this->checkEmailRateLimit($user_id); |
| | | if ($rate_check !== true) { |
| | |
| | | return new WP_Error('invalid_email', 'Invalid email address'); |
| | | } |
| | | |
| | | // Check if this email has already been invited or registered |
| | | if ($this->isEmailInvited($invitee_email)) { |
| | | return new WP_Error('already_invited', 'This person has already been invited'); |
| | | } |
| | | |
| | | // Check if already registered |
| | | if (email_exists($invitee_email)) { |
| | | return new WP_Error('user_exists', 'This person already has an account'); |
| | | } |
| | |
| | | return $referral_code; |
| | | } |
| | | |
| | | // Get reward text for email |
| | | $settings = $this->getRewardSettings(); |
| | | $reward_text = $settings['referee_reward_type'] === 'percentage' |
| | | ? "Get {$settings['referee_reward_amount']}% off your first treatment!" |
| | | : "Get \${$settings['referee_reward_amount']} off your first treatment!"; |
| | | |
| | | // Record the invitation attempt (for tracking) |
| | | // Record the invitation attempt (for rate limiting only) |
| | | $this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name); |
| | | |
| | | // Send magic link via MagicLinkManager |
| | | $result = $this->magic_link->sendMagicLink( |
| | | // Create registration URL with token (opens sidebar with prefilled form) |
| | | $token_data = [ |
| | | 'name' => sanitize_text_field($invitee_name), |
| | | 'email' => $invitee_email, |
| | | 'expires' => time() + (30 * DAY_IN_SECONDS) |
| | | ]; |
| | | |
| | | // Encode the token |
| | | $token = base64_encode(json_encode($token_data)); |
| | | $registration_url = add_query_arg([ |
| | | 'ref' => $referral_code, |
| | | 'rname' => sanitize_text_field($invitee_name), |
| | | 'remail'=> rawurlencode($invitee_email), |
| | | ], home_url('/')); |
| | | |
| | | // Get reward text for email |
| | | $reward_text = $this->settings['referee_reward_type'] === 'percentage' |
| | | ? "{$this->settings['referee_reward_amount']}% off" |
| | | : "\${$this->settings['referee_reward_amount']} off"; |
| | | |
| | | // Build email content |
| | | $email_content = |
| | | sprintf( |
| | | '<h2>%s invited you to %s!</h2> |
| | | <p>%s</p> |
| | | <div class="callout"> |
| | | <h3>Get %s your first treatment!</h3> |
| | | </div> |
| | | <p>Click the button below to register and claim your reward:</p> |
| | | %s |
| | | <p><small>This invitation expires in 30 days.</small></p>', |
| | | esc_html($referrer->display_name), |
| | | esc_html(get_bloginfo('name')), |
| | | nl2br(esc_html($message)), |
| | | esc_html($reward_text), |
| | | JVB()->email()->button($registration_url, 'Register & Get Your Reward') |
| | | ); |
| | | |
| | | // Send email |
| | | $sent = JVB()->email()->sendEmail( |
| | | $invitee_email, |
| | | MagicLinkManager::TYPE_REFERRAL, |
| | | [ |
| | | 'name' => sanitize_text_field($invitee_name), |
| | | 'referral_code' => $referral_code, |
| | | 'referrer_id' => $user_id, |
| | | 'referrer_name' => $referrer->display_name, |
| | | 'reward_text' => $reward_text |
| | | ] |
| | | $subject, |
| | | $email_content |
| | | ); |
| | | |
| | | if (is_wp_error($result)) { |
| | | return $result; |
| | | if (!$sent) { |
| | | return new WP_Error('email_failed', 'Failed to send invitation email'); |
| | | } |
| | | |
| | | return [ |
| | |
| | | * @param array $invitations Array of ['email' => '', 'name' => ''] |
| | | * @return array Results with success/failed arrays |
| | | */ |
| | | public function sendBatchReferralInvitations(int $user_id, array $invitations): array |
| | | public function sendBatchReferralInvitations(int $user_id, array $invitations, string $subject, string $message): array |
| | | { |
| | | $results = [ |
| | | 'success' => [], |
| | |
| | | continue; |
| | | } |
| | | |
| | | $result = $this->sendReferralInvitation($user_id, $email, $name); |
| | | $result = $this->sendReferralInvitation($user_id, $email, $name, $subject, $message); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $results['failed'][] = [ |
| | |
| | | |
| | | return [ |
| | | 'success' => !empty($results['success']), |
| | | 'results' => $results, |
| | | 'result' => $results, |
| | | 'summary' => sprintf( |
| | | 'Sent %d invitations, %d failed', |
| | | count($results['success']), |
| | |
| | | |
| | | return $csv_content; |
| | | } |
| | | |
| | | /** |
| | | * Add referral settings subpage to admin menu |
| | | * Add referral settings subpage to admin menu |
| | | * |
| | | * @param array $subpages |
| | | * @return array |
| | | */ |
| | | public static function addSubpage():void |
| | | { |
| | | $subpage = [ |
| | | 'page_title' => 'Referral System', |
| | | 'menu_title' => 'Referrals', |
| | | 'capability' => 'manage_options', |
| | | 'menu_slug' => BASE . 'referral-admin', |
| | | 'callback' => [self::class, 'renderAdminPageStatic'] |
| | | ]; |
| | | AdminPages::addSubPage(BASE.'referral-admin', $subpage); |
| | | } |
| | | |
| | | /** |
| | | * 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(); |
| | | } |
| | | |
| | | /** |
| | | * Register settings |
| | | */ |
| | | public function registerSettings(): void |
| | | { |
| | | register_setting( |
| | | BASE . 'referral_settings', |
| | | BASE . 'referral_page_id', |
| | | [ |
| | | 'type' => 'integer', |
| | | 'sanitize_callback' => 'absint', |
| | | 'default' => 0 |
| | | ] |
| | | ); |
| | | |
| | | register_setting( |
| | | BASE . 'referral_settings', |
| | | BASE . 'referral_reward_settings', |
| | | [ |
| | | 'type' => 'array', |
| | | 'sanitize_callback' => [$this, 'sanitizeRewardSettings'], |
| | | 'default' => $this->default_settings |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Sanitize reward settings |
| | | */ |
| | | public function sanitizeRewardSettings(array $settings): array |
| | | { |
| | | return [ |
| | | 'referrer_reward_applies_to' => in_array($settings['referrer_reward_applies_to'] ?? '', ['per_user', 'flat_total']) |
| | | ? $settings['referrer_reward_applies_to'] |
| | | : 'per_user', |
| | | 'referrer_reward_amount' => floatval($settings['referrer_reward_amount'] ?? 25.00), |
| | | 'referrer_reward_type' => in_array($settings['referrer_reward_type'] ?? '', ['fixed', 'percentage']) |
| | | ? $settings['referrer_reward_type'] |
| | | : 'fixed', |
| | | 'referee_reward_type' => in_array($settings['referee_reward_type'] ?? '', ['percentage', 'fixed']) |
| | | ? $settings['referee_reward_type'] |
| | | : 'percentage', |
| | | 'referee_reward_amount' => floatval($settings['referee_reward_amount'] ?? 20), |
| | | 'referee_reward_applies_to' => in_array($settings['referee_reward_applies_to'] ?? '', ['first_order', 'all_orders']) |
| | | ? $settings['referee_reward_applies_to'] |
| | | : 'first_order', |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Render the admin settings page |
| | | */ |
| | | public function renderAdminPage(): void |
| | | { |
| | | ?> |
| | | <div class="wrap jvb-admin-wrap"> |
| | | <h1>Referral System Management</h1> |
| | | |
| | | <!-- 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> |
| | | |
| | | <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> |
| | | |
| | | <!-- 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> |
| | | |
| | | <!-- 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> |
| | | <?php /** |
| | | <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> |
| | | */ |
| | | if (is_admin()) { |
| | | ?> |
| | | <script> |
| | | jQuery(document).ready(function($) { |
| | | // 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') ?>', |
| | | method: 'GET', |
| | | data: { |
| | | offset: page -1, |
| | | limit: 20, |
| | | status: status === '' ? 'all' : 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.items.length === 0) { |
| | | html += '<tr><td colspan="7" style="text-align: center;">No referrals found</td></tr>'; |
| | | } else { |
| | | data.items.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') ?>', // Changed from /mark-consulted |
| | | method: 'POST', |
| | | data: JSON.stringify({ |
| | | action: 'consulted', // Added action parameter |
| | | 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') ?>', // Changed from /mark-treated |
| | | method: 'POST', |
| | | data: JSON.stringify({ |
| | | action: 'treated', // Added action parameter |
| | | 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 |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <div class="wrap"> |
| | | <h1>Referral Settings</h1> |
| | | |
| | | <form method="post" action=""> |
| | | <?php wp_nonce_field(BASE . 'admin_page_nonce'); ?> |
| | | |
| | | <div class="card"> |
| | | <h2>Referral Page</h2> |
| | | <p>Select the page where users can access their referral dashboard.</p> |
| | | |
| | | <table class="form-table"> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="<?= BASE ?>referral_page_id">Referral Page</label> |
| | | </th> |
| | | <td> |
| | | <?php |
| | | if (!$this->referralPage) { |
| | | $this->referralPage = $this->getReferralPageId(); |
| | | } |
| | | wp_dropdown_pages([ |
| | | 'name' => BASE . 'referral_page_id', |
| | | 'id' => BASE . 'referral_page_id', |
| | | 'selected' => $this->referralPage, |
| | | 'show_option_none' => __('— Select —', 'jvbase'), |
| | | 'option_none_value' => '0' |
| | | ]); |
| | | ?> |
| | | <p class="description"> |
| | | This page will show "Referral Page" in the admin bar when editing. |
| | | </p> |
| | | </td> |
| | | </tr> |
| | | </table> |
| | | </div> |
| | | |
| | | <div class="card"> |
| | | <h2>Reward Settings</h2> |
| | | |
| | | <table class="form-table"> |
| | | <tr> |
| | | <th colspan="2"><h3>Referrer Rewards</h3></th> |
| | | </tr> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="referrer_reward_type">Reward Type</label> |
| | | </th> |
| | | <td> |
| | | <select name="referrer_reward_type" id="referrer_reward_type"> |
| | | <option value="fixed" <?php selected($this->settings['referrer_reward_type'], 'fixed'); ?>>Fixed Amount</option> |
| | | <option value="percentage" <?php selected($this->settings['referrer_reward_type'], 'percentage'); ?>>Percentage</option> |
| | | </select> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="referrer_reward_amount">Reward Amount</label> |
| | | </th> |
| | | <td> |
| | | <input type="number" |
| | | name="referrer_reward_amount" |
| | | id="referrer_reward_amount" |
| | | value="<?= esc_attr($this->settings['referrer_reward_amount']) ?>" |
| | | step="0.01" |
| | | min="0"> |
| | | <p class="description">Amount in dollars or percentage</p> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="referrer_reward_applies_to">Applies To</label> |
| | | </th> |
| | | <td> |
| | | <select name="referrer_reward_applies_to" id="referrer_reward_applies_to"> |
| | | <option value="per_user" <?php selected($this->settings['referrer_reward_applies_to'], 'per_user'); ?>>Per User Referred</option> |
| | | <option value="flat_total" <?php selected($this->settings['referrer_reward_applies_to'], 'flat_total'); ?>>Flat Total</option> |
| | | </select> |
| | | </td> |
| | | </tr> |
| | | |
| | | <tr> |
| | | <th colspan="2"><h3>Referee (New User) Rewards</h3></th> |
| | | </tr> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="referee_reward_type">Reward Type</label> |
| | | </th> |
| | | <td> |
| | | <select name="referee_reward_type" id="referee_reward_type"> |
| | | <option value="percentage" <?php selected($this->settings['referee_reward_type'], 'percentage'); ?>>Percentage</option> |
| | | <option value="fixed" <?php selected($this->settings['referee_reward_type'], 'fixed'); ?>>Fixed Amount</option> |
| | | </select> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="referee_reward_amount">Reward Amount</label> |
| | | </th> |
| | | <td> |
| | | <input type="number" |
| | | name="referee_reward_amount" |
| | | id="referee_reward_amount" |
| | | value="<?= esc_attr($this->settings['referee_reward_amount']) ?>" |
| | | step="0.01" |
| | | min="0"> |
| | | <p class="description">Amount in dollars or percentage</p> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="referee_reward_applies_to">Applies To</label> |
| | | </th> |
| | | <td> |
| | | <select name="referee_reward_applies_to" id="referee_reward_applies_to"> |
| | | <option value="first_order" <?php selected($this->settings['referee_reward_applies_to'], 'first_order'); ?>>First Order Only</option> |
| | | <option value="all_orders" <?php selected($this->settings['referee_reward_applies_to'], 'all_orders'); ?>>All Orders</option> |
| | | </select> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <th scope="row"> |
| | | <label for="<?= BASE ?>referral_role">Client Import Role</label> |
| | | </th> |
| | | <td> |
| | | <?php |
| | | $selected_role = get_option(BASE . 'referral_role', ''); |
| | | $roles = wp_roles()->get_names(); |
| | | ?> |
| | | <select name="<?= BASE ?>referral_role" id="<?= BASE ?>referral_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> |
| | | |
| | | <p class="submit"> |
| | | <button type="submit" name="submit" class="button button-primary">Save Settings</button> |
| | | </p> |
| | | </form> |
| | | |
| | | <?= $this->renderReferralStats(true) ?> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | /** |
| | | * Render referral statistics |
| | | */ |
| | | protected function renderReferralStats(bool $wrapCard = false): string |
| | | { |
| | | global $wpdb; // Use fresh global instead of stored reference |
| | | |
| | | 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 ?? 0) ?></td> |
| | | </tr> |
| | | <tr> |
| | | <th>Pending</th> |
| | | <td><?= esc_html($pending_referrals ?? 0) ?></td> |
| | | </tr> |
| | | <tr> |
| | | <th>Treated</th> |
| | | <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>'; |
| | | } |
| | | |
| | | return $table; |
| | | } |
| | | |
| | | /** |
| | | * Add "Referral Page" label to admin bar |
| | | * |
| | | * @param WP_Admin_Bar $wp_admin_bar |
| | | */ |
| | | public function addReferralPageLabel($wp_admin_bar): void |
| | | { |
| | | if (!is_admin()) { |
| | | return; |
| | | } |
| | | |
| | | if (!$this->referralPage) { |
| | | $this->referralPage = $this->getReferralPageId(); |
| | | } |
| | | if (!$this->referralPage) { |
| | | return; |
| | | } |
| | | |
| | | global $pagenow, $post; |
| | | |
| | | // Check if we're editing the referral page |
| | | if ('post.php' === $pagenow && $post && $post->ID === $this->referralPage) { |
| | | $wp_admin_bar->add_node([ |
| | | 'id' => 'referral-page', |
| | | 'parent' => 'top-secondary', |
| | | 'title' => __('Referral Page', 'jvbase'), |
| | | 'meta' => [ |
| | | 'class' => 'referral-page-notice' |
| | | ] |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the referral page ID |
| | | * |
| | | * @return int|null |
| | | */ |
| | | public function getReferralPageId(): ?int |
| | | { |
| | | $page_id = get_option(BASE . 'referral_page_id'); |
| | | return $page_id ? (int) $page_id : null; |
| | | } |
| | | |
| | | /** |
| | | * Show admin notice on referral page edit screen |
| | | */ |
| | | public function showReferralPageNotice(): void |
| | | { |
| | | global $pagenow, $post; |
| | | |
| | | 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>'; |
| | | echo '</div>'; |
| | | } |
| | | } |
| | | |
| | | public function renderDashPage(string $content, string $page): string |
| | | { |
| | | if ($page !== 'Referrals') { |
| | | return $content; |
| | | } |
| | | |
| | | // 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); |
| | | } |
| | | |
| | | $referrals = $this->getUserReferrals($user_id, ['limit' => 20]); |
| | | |
| | | ob_start(); |
| | | |
| | | $tabs = new Tabs(); |
| | | $tabs->addTab('share') |
| | | ->title('Share') |
| | | ->icon('share-fat') |
| | | ->description('Share your code and earn rewards when your referrals complete their first treatment!') |
| | | ->content($this->shareDashboard($user_id, $referral_code)); |
| | | $tabs->addTab('referrals') |
| | | ->title('Your Referrals') |
| | | ->icon('hand-heart') |
| | | ->content($this->referralCRUD($user_id)); |
| | | |
| | | ?> |
| | | <div class="referral-dashboard"> |
| | | <?= $tabs->render(true);?> |
| | | </div> |
| | | |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function shareDashboard(int $user_id, string $referral_code):string |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <?php $this->getShareButtons($user_id); ?> |
| | | |
| | | <!-- Referral Code Card --> |
| | | <div class="card"> |
| | | <h3>Share Link</h3> |
| | | <div class="row btw nowrap"> |
| | | <code id="referral-link" class="copy-target"><?= home_url('/?ref=' . $referral_code) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link"> |
| | | <?= jvbIcon('copy'); ?> |
| | | <?= jvbIcon('check-circle'); ?> |
| | | </button> |
| | | </div> |
| | | <h3>Share Code</h3> |
| | | <div class="row btw nowrap"> |
| | | <code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code"> |
| | | <?= jvbIcon('copy'); ?> |
| | | <?= jvbIcon('check-circle'); ?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | <form class="invite"> |
| | | <h2>Invite your Friends</h2> |
| | | <p>Or, if you prefer, enter your friends name(s) and email(s), and we'll send off some emails.</p> |
| | | <p><small>(No data is stored. Your friends will get an email from our email.)</small></p> |
| | | <?php |
| | | $invite = [ |
| | | 'type' => 'taglist', |
| | | 'label' => 'Invite Your Friends', |
| | | 'hint' => 'Add friends to send them a referral link', |
| | | 'add_label' => 'Add Invite', |
| | | 'tag_format' => '{{name}} ({{email}})', // or 'first_field', 'all_fields', 'email', etc. |
| | | 'fields' => [ |
| | | 'name' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Name', |
| | | 'placeholder' => 'Full Name', |
| | | 'required' => true |
| | | ], |
| | | 'email' => [ |
| | | 'type' => 'email', |
| | | 'label' => 'Email', |
| | | 'placeholder' => 'email@example.com', |
| | | 'required' => true |
| | | ] |
| | | ] |
| | | ]; |
| | | $fields = [ |
| | | 'subject' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Email Subject', |
| | | 'value' => 'Try Legacy for Tattoo Removal', |
| | | ], |
| | | 'message' => [ |
| | | 'type' => 'textarea', |
| | | 'label' => 'Customize message', |
| | | 'value' => 'I had a great experience at Legacy Tattoo Removal! |
| | | |
| | | If you click the link below, you can get 20% off your first treatment with them.', |
| | | 'hint' => 'We\'ll add your code and a link automatically.' |
| | | ] |
| | | ]; |
| | | echo Form::render('invite', '', $invite); |
| | | ?> |
| | | <details> |
| | | <summary class="icon icon-caret-down">Customize Message</summary> |
| | | <?php |
| | | foreach ($fields as $fieldName => $field) { |
| | | $value = (array_key_exists('value', $field)) ? $field['value'] : []; |
| | | echo Form::render($fieldName, $value, $field); |
| | | } |
| | | ?> |
| | | </details> |
| | | |
| | | <button type="submit"><?=jvbIcon('envelope')?>Send Invites</button> |
| | | </form> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function referralCRUD(int $user_id):string |
| | | { |
| | | $stats = $this->getUserStats($user_id); |
| | | ob_start(); |
| | | ?> |
| | | <!-- Stats Grid with Updated Labels --> |
| | | <div class="item-grid stats"> |
| | | <div class="card"> |
| | | <h4>Code Used</h4> |
| | | <span class="stat-number" data-stat="code_used"><?= esc_html($stats['code_used'] ?? 0) ?></span> |
| | | <p class="hint">People who used your code</p> |
| | | </div> |
| | | <div class="card"> |
| | | <h4>Treatments</h4> |
| | | <span class="stat-number" data-stat="treatments"><?= esc_html($stats['treatments'] ?? 0) ?></span> |
| | | <p class="hint">Completed first treatment</p> |
| | | </div> |
| | | <div class="card highlight"> |
| | | <h4>Total Rewards</h4> |
| | | <span class="stat-number" data-stat="total_rewards">$<?= number_format($stats['total_rewards'] ?? 0, 2) ?></span> |
| | | <p class="hint">Earned from referrals</p> |
| | | </div> |
| | | </div> |
| | | |
| | | <?php |
| | | // Configure CRUDSkeleton for referrals |
| | | $crud = new CRUDSkeleton(); |
| | | $crud->title('Your Referrals', 'Track friends you\'ve invited and rewards earned') |
| | | ->content('referral', 'Referral', 'Referrals') |
| | | // ->initMeta('custom', 'referral') |
| | | ->setFields([ |
| | | 'referee_name' => [ |
| | | 'label' => 'Name', |
| | | 'type' => 'text', |
| | | ], |
| | | 'referee_email' => [ |
| | | 'label' => 'Email', |
| | | 'type' => 'text', |
| | | ], |
| | | 'referred_at' => [ |
| | | 'label' => 'Code Used', |
| | | 'type' => 'date', |
| | | ], |
| | | 'referral_status' => [ |
| | | 'label' => 'Status', |
| | | 'type' => 'text', |
| | | ] |
| | | ]) |
| | | ->setStatuses(['all', 'unused', 'registered', 'consulted', 'completed']) |
| | | ->addViews(['table', 'list']) |
| | | ->defaultView('table') |
| | | ->addCapabilities(['view']) |
| | | ->addDateFilter('referred_at') |
| | | ->showBulkControls(false) |
| | | ->showFilters(false) |
| | | ->useCRUDjs(false); // We'll use our custom Referral.js with DataStore |
| | | |
| | | // Add custom template for actions column |
| | | $crud->addItemActions(['resend', 'trash']); |
| | | $crud->defineItemAction('resend', [ |
| | | 'title' => 'Resend Invitation', |
| | | 'icon' => 'paper-plane-tilt' |
| | | ]); |
| | | $crud->defineItemAction('trash', [ |
| | | 'title' => 'Remove from List' |
| | | ]); |
| | | |
| | | // Custom empty state |
| | | $crud->addTemplate('empty', ' |
| | | <template class="emptyState"> |
| | | <div class="empty-state"> |
| | | <h3>' . jvbDashIcon('hand-heart') . 'Nothing Yet' . jvbDashIcon('hand-heart') . '</h3> |
| | | <p>Start sharing your referral code to earn rewards!</p> |
| | | <p><small><i>Share your code using the "Share" tab below.</i></small></p> |
| | | </div> |
| | | </template> |
| | | '); |
| | | |
| | | $crud->render(); |
| | | |
| | | 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 . 'referral_role'] ?? JVB_USER); |
| | | update_option(BASE . 'referral_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 |
| | | } |
| | | |
| | | /** |
| | | * Send notification to referrer when someone registers |
| | | * |
| | | * @param int $referrer_id |
| | | * @param string $referee_name |
| | | */ |
| | | protected function sendReferrerNotification(int $referrer_id, string $referee_name): void |
| | | { |
| | | $referrer = get_userdata($referrer_id); |
| | | if (!$referrer) { |
| | | return; |
| | | } |
| | | |
| | | $subject = sprintf('%s signed up with your referral code!', $referee_name); |
| | | $message = sprintf( |
| | | "Great news! %s just signed up using your referral code.\n\n" . |
| | | "View your referrals: %s", |
| | | $referee_name, |
| | | home_url('/dash/referrals') |
| | | ); |
| | | |
| | | JVB()->email()->sendEmail( |
| | | $referrer->user_email, |
| | | $subject, |
| | | $message |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Get welcome message for newly referred user |
| | | * |
| | | * @param int $user_id |
| | | * @return string HTML content for welcome message |
| | | */ |
| | | public function getReferralWelcomeMessage(int $user_id): string |
| | | { |
| | | // Check if user was referred |
| | | $referral = $this->getReferralByReferee($user_id); |
| | | |
| | | if (!$referral || $referral->status !== 'pending') { |
| | | return ''; |
| | | } |
| | | |
| | | // Only show for recent registrations (within 7 days) |
| | | $registered_time = strtotime($referral->referred_at); |
| | | if ((time() - $registered_time) > (7 * DAY_IN_SECONDS)) { |
| | | return ''; |
| | | } |
| | | |
| | | // Get referrer name |
| | | $referrer = get_userdata($referral->referrer_id); |
| | | $referrer_first_name = $referrer ? strtok($referrer->display_name, ' ') : 'Your friend'; |
| | | |
| | | // Get reward text |
| | | $reward_text = $this->getRewardText(); // Just "20% off" or "$25 off" |
| | | |
| | | $booking_url = apply_filters('jvb_referral_booking_url', home_url('/contact')); |
| | | $estimate_url = apply_filters('jvb_referral_estimate_url', home_url('/estimate')); |
| | | |
| | | ob_start(); |
| | | ?> |
| | | <div class="welcome-banner referral-welcome"> |
| | | <div class="banner-content"> |
| | | <h3><?= jvbIcon('confetti') ?>Welcome! <small><b><?= esc_html($referrer_first_name) ?></b> invited you to save <b><?= esc_html($reward_text) ?></b>!</small></h3> |
| | | <p>But we're not done yet! Here's what happens next:</p> |
| | | <div class="callout"> |
| | | <ol> |
| | | <li>Book your <b>free consultation</b></li> |
| | | <li>Come in and we'll assess your tattoo</li> |
| | | <li>Get <?= esc_html($reward_text) ?> your first treatment!</li> |
| | | </ol> |
| | | </div> |
| | | <p class="hint"> |
| | | <strong>Important:</strong> If you book with a different email than |
| | | <strong><?= esc_html(wp_get_current_user()->user_email) ?></strong>, |
| | | please let us know so we can apply your reward! |
| | | </p> |
| | | <ul class="buttons"> |
| | | <li><a href="<?= esc_url($estimate_url) ?>" class="button-secondary"> |
| | | <?= jvbIcon('calculator') ?> Get an Estimate First |
| | | </a></li> |
| | | <li><a href="<?= esc_url($booking_url) ?>" class="button-primary"> |
| | | <?= jvbIcon('calendar') ?> Book Free Consult |
| | | </a></li> |
| | | </ul> |
| | | |
| | | |
| | | </div> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | | } |
| | | |