From 46d681c6b825d21b3f698d793c4e630c687d90ad Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 21 May 2026 21:41:53 +0000
Subject: [PATCH] =Major CustomBlocks.php overhaul, expanding block support and customization from the editor. theme.json should now be updated on new themes to set brand colours, etc. Also note: major change to .col vs .row alignment: simplifying it to .top .bottom vs the confusion of the differences for .col/.row .start and .a-start
---
inc/managers/ReferralManager.php | 2469 ++++++++++++++++++++++++++++++++++++++++++++++++----------
1 files changed, 2,035 insertions(+), 434 deletions(-)
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 429ea99..c521c15 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -1,9 +1,10 @@
<?php
namespace JVBase\managers;
-use JVBase\managers\MagicLinkManager;
-use JVBase\integrations\Cloudflare;
-use JVBase\utility\Features;
+use JVBase\meta\Form;
+use JVBase\ui\CRUDSkeleton;
+use JVBase\ui\Tabs;
+use JVBase\base\Site;
use WP_User;
use WP_Error;
@@ -21,10 +22,19 @@
{
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'
@@ -32,19 +42,38 @@
'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'
];
+ 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 = 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);
+ if (JVB_TESTING) {
+ $this->cache->flush();
+ $this->requestCache->flush();
+ $this->statsCache->flush();
+ }
- // 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
@@ -55,14 +84,243 @@
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_action('jvbAdditionalActions', [$this, 'outputShareWidget']);
+ 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`)'],
+ '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;
+ }
+ 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
@@ -72,10 +330,21 @@
'jvb-a11y',
'jvb-popup',
'jvb-tabs',
+ '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',
@@ -104,6 +373,26 @@
}
}
+ 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
*
@@ -111,32 +400,35 @@
* @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_user_by('ID', $user_id);
+ $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;
}
/**
@@ -169,65 +461,108 @@
* 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;
}
/**
@@ -238,7 +573,7 @@
* @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);
@@ -266,13 +601,17 @@
*/
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;
+ }
+ );
}
/**
@@ -337,8 +676,6 @@
return;
}
- $settings = $this->getRewardSettings();
-
// Create referrer reward
$this->wpdb->insert(
$this->rewards_table,
@@ -346,7 +683,7 @@
'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')
],
@@ -354,9 +691,9 @@
);
// 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,
@@ -365,7 +702,7 @@
'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')
],
@@ -392,18 +729,42 @@
$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);
+ }
+ );
+
}
/**
@@ -414,38 +775,33 @@
*/
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;
+ }
+ );
}
/**
@@ -457,20 +813,23 @@
*/
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
@@ -480,16 +839,19 @@
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;
+ }
+ );
+
}
/**
@@ -499,11 +861,8 @@
{
$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
@@ -511,48 +870,46 @@
$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');
}
/**
@@ -563,19 +920,50 @@
$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');
}
/**
@@ -586,69 +974,30 @@
*/
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');
}
/**
@@ -666,31 +1015,22 @@
$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;
}
/**
@@ -797,6 +1137,7 @@
</table>
<?php endif; ?>
+ <?php /**
<script>
function markReferralTreated(referralId) {
if (!confirm('Mark this referral as treated? This will create reward records.')) {
@@ -821,6 +1162,7 @@
}
</script>
<?php
+ */
}
/**
@@ -877,17 +1219,16 @@
{
$user_id = get_current_user_id();
- jvbDump($user_id);
- $content = '<aside class="jvb-referral right">';
+ $content = '<aside class="main referral right">';
if (!$user_id) {
$content .= $this->getUnloggedInReferral();
} else {
$content .= $this->getLoggedInReferral($user_id);
}
- $content .= '<button type="button" class="close">'.jvbIcon('close').'</button></aside>';
+ $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
@@ -896,101 +1237,142 @@
return $actions;
}
+ /**
+ * Display referral sidebar for non-logged-in users
+ */
function getUnloggedInReferral(): string
{
ob_start();
JVB()->connect('cloudflare')->renderTurnstile();
$turnstile = ob_get_clean();
- $codeForm = '<form id="referral-code-form">
- <div class="status" hidden>
- <div class="spinner"></div>
- <p class="message"></p>
- </div>
- <div class="field text">
- <label for="referral-name">Your Name</label>
- <input type="text"
- id="referral-name"
- name="name"
- placeholder="Mister Meeseeks"
- autocomplete="name"
- required>
- </div>
- <div class="field email">
- <label for="referral-email">Your Email</label>
- <input type="email"
- id="referral-email"
- name="email"
- placeholder="look@me.com"
- autocomplete="email"
- required>
- </div>
+ $reward_text = $this->getRewardText(true);
- <div class="field text">
- <label for="referral-code-input">Referral Code</label>
- <input type="text"
- id="referral-code-input"
- name="referral_code"
- placeholder="e.g., THISISFAKE1234"
- required
- pattern="[A-Za-z0-9]+"
- maxlength="20"
- autocomplete="off"
- style="text-transform: uppercase;">
- </div>
+ // Pre-fill code if from referral link
+ $prefill_code = $_GET['ref'] ?? '';
+ $referrer_name = '';
+ if ($prefill_code) {
+ $referrer = $this->getUserByReferralCode($prefill_code);
+ $referrer_name = $referrer ? strtok($referrer->display_name, ' ') : '';
+ }
+ $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>
- <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>';
+ <div class="code-status" hidden></div>
- $loginForm = '<form id ="login-form">
- <div class="status" hidden>
- <div class="spinner"></div>
- <p class="message"></p>
- </div>
- <div class="field email">
- <label for="login-email">Your Email</label>
- <input id="login-email" name="login-email" type="email" autocomplete="email">
- </div>
- '.$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>';
+ <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'
+ ]),
+ 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,
+ jvbIcon('check-circle')
+ );
+ $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')
+ );
+
+
+ $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 the code given to you to get 20% off your first treatment!'
+ 'Enter your referral code to get started'
],
+ 'header' => $header,
'content' => $codeForm
],
'login' => [
+ 'header' => $loginHeader,
'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);
@@ -999,9 +1381,8 @@
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'
@@ -1037,87 +1418,82 @@
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 (!$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>
+ <div class="wrap">
+ <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); ?>
+ <section class="copy">
<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 x-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 x-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>
+ </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>
+ </section>
+ <section class="stats-summary">
+ <div class="row x-btw">
+ <span class="stat-label">Total Referrals</span>
+ <span class="stat-value" data-stat="total">-</span>
+ </div>
+ <div class="row x-btw">
+ <span class="stat-label">Successful</span>
+ <span class="stat-value" data-stat="treated">-</span>
+ </div>
+ <div class="row x-btw">
+ <span class="stat-label">Pending</span>
+ <span class="stat-value" data-stat="pending">-</span>
+ </div>
+ <div class="row x-btw highlight">
+ <span class="stat-label">Available Rewards</span>
+ <span class="stat-value" data-stat="rewards">$0.00</span>
+ </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();
}
@@ -1130,27 +1506,51 @@
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 ($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()
+ );
}
/**
@@ -1159,15 +1559,12 @@
* @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) {
@@ -1180,11 +1577,7 @@
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');
}
@@ -1193,34 +1586,60 @@
$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;
}
- // 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 [
@@ -1238,7 +1657,7 @@
* @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' => [],
@@ -1258,7 +1677,7 @@
continue;
}
- $result = $this->sendReferralInvitation($user_id, $email, $name);
+ $result = $this->sendReferralInvitation($user_id, $email, $name, $subject, $message);
if (is_wp_error($result)) {
$results['failed'][] = [
@@ -1279,7 +1698,7 @@
return [
'success' => !empty($results['success']),
- 'results' => $results,
+ 'result' => $results,
'summary' => sprintf(
'Sent %d invitations, %d failed',
count($results['success']),
@@ -1463,5 +1882,1187 @@
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 = $this->getUserReferralCode($user_id);
+ if (!$referral_code) {
+ $user = get_userdata($user_id);
+ $referral_code = $this->generateReferralCode($user);
+ }
+
+ $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 -->
+ <details open>
+ <summary>Your Code</summary>
+ <h3>Share Link</h3>
+ <div class="row x-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 x-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
+ $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'] ?? Site::getDefaultReferralRole());
+ 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 (!$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>
+ <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();
+ }
}
--
Gitblit v1.10.0