| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | 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 JVBase\base\Site; |
| | | 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' |
| | |
| | | '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' |
| | | 'referral_role' => BASE.'client' |
| | | ]; |
| | | |
| | | protected string $role = BASE.'client'; |
| | | protected string $role; |
| | | |
| | | protected array $settings; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | $this->role = Site::getDefaultReferralRole(); |
| | | $this->default_settings['referral_role'] = $this->role; |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->cache = CacheManager::for('referrals', WEEK_IN_SECONDS); |
| | | $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); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | $this->requestCache->flush(); |
| | | $this->statsCache->flush(); |
| | | } |
| | | |
| | | $this->referrals_table = $wpdb->prefix . BASE . 'referrals'; |
| | | $this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards'; |
| | | |
| | |
| | | |
| | | add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3); |
| | | add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1); |
| | | add_action('jvbUserRegistered', [$this, 'processReferral'], 10, 1); |
| | | add_action('user_register', [$this, 'processReferral'], 10, 1); |
| | | |
| | | // Add meta boxes for admin to manage referrals |
| | | add_action('show_user_profile', [$this, 'displayUserReferralInfo']); |
| | |
| | | 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`)'], |
| | | '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`)'], |
| | | '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`)'], |
| | | '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; |
| | |
| | | 'jvb-data-store', |
| | | ]; |
| | | |
| | | if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) { |
| | | $requirements[] = 'cloudflare-turnstile'; |
| | | if (Site::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) { |
| | | 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):string|false |
| | | { |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return new WP_Error('invalid_user', 'User not found'); |
| | | return false; |
| | | } |
| | | |
| | | // 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 (!empty($existing) && !$custom_code) { |
| | | return $existing[0]; |
| | | } |
| | | if ($custom_code && !empty($existing) && !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')[0]; |
| | | } else { |
| | | return $existing[0]; |
| | | } |
| | | } |
| | | |
| | | // 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')[0]; |
| | | } |
| | | |
| | | // Save the code |
| | | update_user_meta($user_id, BASE . 'referral_code', $code); |
| | | |
| | | return $code; |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | |
| | | * 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 |
| | |
| | | * Track a new referral when user registers |
| | | * |
| | | * @param int $user_id |
| | | * @param array $userData |
| | | * @return bool; |
| | | */ |
| | | public function processReferral(int $user_id): bool |
| | | public function processReferral(int $user_id, array $userData): bool |
| | | { |
| | | // Try to get code from user meta first (set during registration) |
| | | $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true); |
| | | $referral = $this->referrals->get(['to_user' => $user_id]); |
| | | |
| | | if (empty($referral_code)) { |
| | | 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_code)) { |
| | | if (empty($referral)) { |
| | | return false; // No referral code - regular registration |
| | | } |
| | | |
| | | // Find the referrer |
| | | $referrer = $this->getUserByReferralCode($referral_code); |
| | | if (!$referrer) { |
| | | delete_user_meta($user_id, BASE . 'pending_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 (!$record) { |
| | | error_log('[ReferralManager]::processReferral Could not update record for user: '.print_r($referral, true)); |
| | | return false; |
| | | } |
| | | |
| | | $user = get_userdata($user_id); |
| | | |
| | | // Check if referral already exists for this user |
| | | $existing = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT * FROM {$this->referrals_table} |
| | | WHERE referrer_id = %d AND (referee_email = %s OR referee_id = %d)", |
| | | $referrer->ID, |
| | | $user->user_email, |
| | | $user_id |
| | | )); |
| | | |
| | | if (!$existing) { |
| | | // Create new referral record - referred_at captures registration time |
| | | $this->wpdb->insert( |
| | | $this->referrals_table, |
| | | [ |
| | | 'referrer_id' => $referrer->ID, |
| | | 'referee_id' => $user_id, |
| | | 'referee_name' => $user->display_name, |
| | | 'referee_email' => $user->user_email, |
| | | 'referee_phone' => get_user_meta($user_id, BASE . 'phone', true) ?: '', |
| | | 'referral_code' => $referral_code, |
| | | 'status' => 'pending', // pending first treatment |
| | | 'referred_at' => current_time('mysql') // When they registered |
| | | ], |
| | | ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s'] |
| | | ); |
| | | } |
| | | |
| | | // Clean up temp data |
| | | delete_user_meta($user_id, BASE . 'pending_referral_code'); |
| | |
| | | } |
| | | |
| | | // Clear caches |
| | | $this->cache->clear(); |
| | | $this->cache->flush(); |
| | | |
| | | // Fire action for tracking |
| | | do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code); |
| | | do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral['referral_code']); |
| | | |
| | | // Send notification to referrer |
| | | $this->sendReferrerNotification($referrer->ID, $user->display_name); |
| | | $this->sendReferrerNotification($referrer->ID, $userData['display_name']); |
| | | return true; |
| | | } |
| | | |
| | |
| | | */ |
| | | public function getUserReferrals(int $user_id, array $args = []): array |
| | | { |
| | | return $this->cache->remember( |
| | | $user_id, |
| | | $defaults = [ |
| | | 'status' => 'all', |
| | | 'limit' => 100, |
| | | 'offset' => 0, |
| | | 'orderby' => 'referred_at', |
| | | 'order' => 'DESC' |
| | | ]; |
| | | |
| | | $args = wp_parse_args($args, $defaults); |
| | | |
| | | return $this->requestCache->remember( |
| | | $this->requestCache->generateKey(array_merge(['user'=>$user_id], $args)), |
| | | function() use ($user_id, $args) { |
| | | $defaults = [ |
| | | 'status' => 'all', |
| | | 'limit' => 100, |
| | | 'offset' => 0, |
| | | 'orderby' => 'referred_at', |
| | | 'order' => 'DESC' |
| | | ]; |
| | | |
| | | $args = wp_parse_args($args, $defaults); |
| | | |
| | | $where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id); |
| | | |
| | | if ($args['status'] !== 'all') { |
| | |
| | | */ |
| | | public function getUserStats(int $user_id): array |
| | | { |
| | | $cache_key = 'stats_' . $user_id; |
| | | $cached = $this->cache->get($cache_key); |
| | | |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | |
| | | $stats = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT |
| | | 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); |
| | | $user_id |
| | | ), ARRAY_A); |
| | | |
| | | // Get total rewards earned (available + redeemed) |
| | | $rewards = $this->wpdb->get_var($this->wpdb->prepare( |
| | | "SELECT SUM(amount) |
| | | // 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 |
| | | )); |
| | | $user_id |
| | | )); |
| | | |
| | | $stats['total_rewards'] = floatval($rewards ?? 0); |
| | | $stats['user_id'] = $user_id; |
| | | $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' : '' |
| | | ); |
| | | |
| | | |
| | | JVB()->email()->sendEmail($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); |
| | | |
| | | JVB()->email()->sendEmail($to, $subject, $message); |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | JVB()->connect('cloudflare')->renderTurnstile(); |
| | | $turnstile = ob_get_clean(); |
| | | |
| | | $meta = new MetaForm(); |
| | | $reward_text = $this->getRewardText(true); |
| | | |
| | | // Pre-fill code if from referral link |
| | |
| | | $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')) . '"> |
| | | ' .$meta->return('referral_name', null, [ |
| | | $header = sprintf( |
| | | '<header><h2>%sGet %s.</h2></header><h3>Have a code?</h3>%s<p>Enter your referral code to get started!</p>', |
| | | jvbIcon('confetti'), |
| | | esc_html($reward_text), |
| | | ($referrer_name ? '<p>' . esc_html($referrer_name) . ' invited you to join us</p>' : '') |
| | | ); |
| | | |
| | | $codeForm = sprintf( |
| | | '<form id="referral-code-form"> |
| | | %s |
| | | <input type="hidden" name="user_select" value="%s"> |
| | | %s%s%s%s |
| | | <div class="row"> |
| | | <button type="button" class="button-secondary check-code-btn"> |
| | | %s Verify Code |
| | | </button> |
| | | <button type="submit"> |
| | | Get Started |
| | | </button> |
| | | </div> |
| | | |
| | | <div class="code-status" hidden></div> |
| | | |
| | | <p class="hint"> |
| | | 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>', |
| | | jvbFormStatus(), |
| | | esc_attr(get_option(BASE.'referral_role','client')), |
| | | Form::render('referral_name', '', [ |
| | | 'required' => true, |
| | | 'type' => 'text', |
| | | 'label' => 'Your Name', |
| | | 'placeholder'=> 'Mister Meeseeks', |
| | | 'autocomplete'=>'name' |
| | | ]). |
| | | $meta->return('referral_email', null, [ |
| | | ]), |
| | | Form::render('referral_email', '', [ |
| | | 'required' => true, |
| | | 'type' => 'email', |
| | | 'label' => 'Your Email', |
| | | 'placeholder'=> 'look@me.com', |
| | | 'autocomplete'=> 'email' |
| | | ]). |
| | | $meta->return('referral_code', $prefill_code, [ |
| | | ]), |
| | | Form::render('referral_code', $prefill_code, [ |
| | | 'required' => true, |
| | | 'type' => 'text', |
| | | 'label' => 'Referral Code', |
| | |
| | | 'maxLength' => 20, |
| | | 'autocomplete'=>'off', |
| | | 'data-referrer' => $referrer_name |
| | | ]).' |
| | | <button type="button" class="button-secondary check-code-btn"> |
| | | '.jvbIcon('check-circle', ['size' => 16]).' Verify Code |
| | | </button> |
| | | <div class="code-status" hidden></div> |
| | | <button type="submit"> |
| | | Get Started |
| | | </button> |
| | | ]), |
| | | $turnstile, |
| | | jvbIcon('check-circle') |
| | | ); |
| | | |
| | | <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>'; |
| | | $loginHeader = sprintf( |
| | | '<header><h2>%sLogin</h2></header><p>Already have an account?<br>Log in to see your rewards!</p>', |
| | | jvbIcon('sign-in') |
| | | ); |
| | | $loginForm = sprintf( |
| | | '<form id="login-form">%s%s%s |
| | | <button type="submit">%sLogin 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>', |
| | | jvbFormStatus(), |
| | | Form::render('login_email', null, [ |
| | | 'required' => true, |
| | | 'type' => 'email', |
| | | 'label' => 'Your Email', |
| | | 'autocomplete'=>'email' |
| | | ]), |
| | | $turnstile, |
| | | jvbIcon('magic-wand') |
| | | ); |
| | | |
| | | $loginForm = '<form id="login-form"> |
| | | '.jvbFormStatus().$meta->return('login_email', null, [ |
| | | 'required' => true, |
| | | 'type' => 'email', |
| | | 'label' => 'Your Email', |
| | | 'autocomplete'=>'email' |
| | | ]).' |
| | | '.$turnstile.' |
| | | <button type="submit">Login With Magic Link</button> |
| | | </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>'; |
| | | |
| | | $footer = '<div class="referral-footer"> |
| | | <a href="' . wp_login_url() . '" class="text-link">Prefer to use a password?</a> |
| | | </div>'; |
| | | $footer = sprintf( |
| | | '<p class="hint"> |
| | | <a href="%s" class="text-link">Prefer to use a password?</a> |
| | | </p>', |
| | | wp_login_url() |
| | | ); |
| | | $tabs = [ |
| | | 'enterCode' => [ |
| | | 'title' => 'Have a Code?', |
| | | 'description' => [ |
| | | 'Enter your referral code to get started' |
| | | ], |
| | | 'header' => $header, |
| | | 'content' => $codeForm |
| | | ], |
| | | 'login' => [ |
| | | 'header' => $loginHeader, |
| | | 'title' => 'Login', |
| | | 'description' => [ |
| | | 'Already have an account? Log in to see your rewards' |
| | |
| | | public function getLoggedInReferral(int $user_id): string |
| | | { |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | if (is_wp_error($referral_code)) { |
| | | if (!$referral_code) { |
| | | return ''; |
| | | } |
| | | |
| | | $share_url = $this->getShareURL($referral_code); |
| | | ob_start(); |
| | | ?> |
| | | <div class="wrap"> |
| | | <header> |
| | | <h3>Share the ♡</h3> |
| | | <p>Invite friends. Earn rewards.</p> |
| | |
| | | |
| | | <?php $this->getShareButtons($user_id); ?> |
| | | |
| | | <div class="copy-section"> |
| | | <section class="copy"> |
| | | <h4>Your Referral Link</h4> |
| | | <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" aria-label="Copy referral link"> |
| | | <?php echo jvbIcon('copy', ['size' => 16]); ?> |
| | | <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="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" aria-label="Copy referral code"> |
| | | <?php echo jvbIcon('copy', ['size' => 16]); ?> |
| | | <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> |
| | | </section> |
| | | |
| | | <div class="recent-referrals-section"> |
| | | <section class="recent-referrals"> |
| | | <h4>Recent Referrals</h4> |
| | | <div class="recent-referrals-list" data-user-id="<?= $user_id ?>"> |
| | | <div class="loading">Loading...</div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <div class="stats-summary"> |
| | | <div class="stat-row"> |
| | | <section class="stats-summary"> |
| | | <div class="row btw"> |
| | | <span class="stat-label">Total Referrals</span> |
| | | <span class="stat-value" data-stat="total">-</span> |
| | | </div> |
| | | <div class="stat-row"> |
| | | <div class="row btw"> |
| | | <span class="stat-label">Successful</span> |
| | | <span class="stat-value" data-stat="treated">-</span> |
| | | </div> |
| | | <div class="stat-row"> |
| | | <div class="row btw"> |
| | | <span class="stat-label">Pending</span> |
| | | <span class="stat-value" data-stat="pending">-</span> |
| | | </div> |
| | | <div class="stat-row highlight"> |
| | | <div class="row btw highlight"> |
| | | <span class="stat-label">Available Rewards</span> |
| | | <span class="stat-value" data-stat="rewards">$0.00</span> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <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> |
| | | |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | } |
| | |
| | | |
| | | $code = $this->getUserReferralCode($user->ID); |
| | | $yourCode = ''; |
| | | if (!is_wp_error($code)) { |
| | | if ($code) { |
| | | $share_url = $this->getShareURL($code); |
| | | $yourCode = sprintf( |
| | | '<div class="callout"> |
| | |
| | | $referrer = get_user_by('ID', $user_id); |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | |
| | | if (is_wp_error($referral_code)) { |
| | | if ($referral_code) { |
| | | return $referral_code; |
| | | } |
| | | |
| | |
| | | |
| | | // Regular users get their referral dashboard |
| | | $user_id = get_current_user_id(); |
| | | $referral_code = get_user_meta($user_id, BASE . 'referral_code', true); |
| | | |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | if (!$referral_code) { |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | $user = get_userdata($user_id); |
| | | $referral_code = $this->generateReferralCode($user); |
| | | } |
| | | |
| | | $referrals = $this->getUserReferrals($user_id, ['limit' => 20]); |
| | |
| | | <?php $this->getShareButtons($user_id); ?> |
| | | |
| | | <!-- Referral Code Card --> |
| | | <div class="card"> |
| | | <h3>Share Code</h3> |
| | | <div class="row btw nowrap"> |
| | | <code class="code"><?= esc_html($referral_code) ?></code> |
| | | <button class="button copy-btn" data-code="<?= esc_attr($referral_code) ?>"> |
| | | Copy Code |
| | | </button> |
| | | </div> |
| | | <details open> |
| | | <summary>Your Code</summary> |
| | | <h3>Share Link</h3> |
| | | <div class="row btw nowrap"> |
| | | <code class="share-link"> |
| | | <?= home_url('/?ref=' . $referral_code) ?> |
| | | </code> |
| | | <button class="button copy-btn" data-code="<?= home_url('/?ref=' . $referral_code) ?>"> |
| | | Copy Link |
| | | <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> |
| | | </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> |
| | | </details> |
| | | <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 |
| | | $meta = new MetaForm(); |
| | | $invite = [ |
| | | 'type' => 'tag_list', |
| | | '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. |
| | | 'tag_format' => '{{name}} ({{email}})', // or 'first_field', 'all_fields', 'email', etc. |
| | | 'fields' => [ |
| | | 'name' => [ |
| | | 'type' => 'text', |
| | |
| | | 'hint' => 'We\'ll add your code and a link automatically.' |
| | | ] |
| | | ]; |
| | | $meta->render('invite', [], $invite); |
| | | 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'] : []; |
| | | $meta->render($fieldName, $value, $field); |
| | | echo Form::render($fieldName, $value, $field); |
| | | } |
| | | ?> |
| | | </details> |
| | |
| | | $crud = new CRUDSkeleton(); |
| | | $crud->title('Your Referrals', 'Track friends you\'ve invited and rewards earned') |
| | | ->content('referral', 'Referral', 'Referrals') |
| | | ->initMeta('custom', 'referral') |
| | | // ->initMeta('custom', 'referral') |
| | | ->setFields([ |
| | | 'referee_name' => [ |
| | | 'label' => 'Name', |
| | |
| | | update_option(BASE . 'referral_page_id', $page_id); |
| | | |
| | | // Save client import role |
| | | $import_role = sanitize_text_field($post_data[BASE . 'referral_role'] ?? JVB_USER); |
| | | $import_role = sanitize_text_field($post_data[BASE . 'referral_role'] ?? Site::getDefaultReferralRole()); |
| | | update_option(BASE . 'referral_role', $import_role); |
| | | |
| | | // Save reward settings |
| | |
| | | public function getShareButtons(int $user_id):void |
| | | { |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | if (is_wp_error($referral_code)) { |
| | | if (!$referral_code) { |
| | | return; |
| | | } |
| | | |
| | |
| | | ?> |
| | | <nav class="share"> |
| | | <h4>Quick Share</h4> |
| | | <ul class="share-buttons-grid"> |
| | | <ul> |
| | | <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'); ?> |
| | |
| | | $referrer_first_name = $referrer ? strtok($referrer->display_name, ' ') : 'Your friend'; |
| | | |
| | | // Get reward text |
| | | $reward_text = $this->getRewardText(false); // Just "20% off" or "$25 off" |
| | | $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')); |