<?php
|
namespace JVBase\managers;
|
|
use JVBase\meta\Form;
|
use JVBase\ui\CRUDSkeleton;
|
use JVBase\ui\Tabs;
|
use JVBase\base\Site;
|
use WP_User;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Referral Tracking Manager
|
*
|
* Handles user referral codes, tracking, rewards, and reporting.
|
* Keeps referrals separate from invitations system.
|
*/
|
class ReferralManager
|
{
|
protected $wpdb;
|
protected MagicLinkManager $magic_link;
|
protected Cache $cache;
|
protected Cache $requestCache;
|
protected Cache $statsCache;
|
protected string $referrals_table;
|
protected ?int $referralPage = null;
|
protected string $rewards_table;
|
|
protected CustomTable $referrals;
|
protected CustomTable $codes;
|
protected CustomTable $janeClients;
|
protected CustomTable $rewards;
|
protected CustomTable $treatments;
|
|
// Default reward settings
|
protected array $default_settings = [
|
'referrer_reward_applies_to' => 'per_user', // 'per_user' or 'flat_total'
|
'referrer_reward_amount' => 25.00,
|
'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'
|
];
|
|
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 = 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';
|
|
$this->referralPage = $this->getReferralPageId();
|
$this->settings = $this->getRewardSettings();
|
|
|
add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3);
|
add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1);
|
add_action('user_register', [$this, 'processReferral'], 10, 1);
|
|
// Add meta boxes for admin to manage referrals
|
add_action('show_user_profile', [$this, 'displayUserReferralInfo']);
|
add_action('edit_user_profile', [$this, 'displayUserReferralInfo']);
|
|
// Save referral code changes
|
add_action('personal_options_update', [$this, 'saveUserReferralCode']);
|
add_action('edit_user_profile_update', [$this, 'saveUserReferralCode']);
|
|
add_filter('jvbNewUserEmail', [$this, 'addReferralToWelcomeEmail'], 99, 2);
|
|
|
add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']);
|
|
add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
|
// Schedule cron jobs for reports
|
$this->registerCronJobs();
|
|
// Add admin bar label for referral page
|
add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999);
|
|
// Add admin notice to referral page edit screen
|
add_action('admin_notices', [$this, 'showReferralPageNotice']);
|
|
|
add_filter('jvbDashboardPage', [$this, 'renderDashPage'], 10, 2);
|
|
// Handle settings save
|
add_action('admin_init', [$this, 'registerSettings']);
|
// Handle admin page form submission
|
add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3);
|
}
|
|
protected function defineTables():void
|
{
|
$this->defineReferralsTable();
|
$this->defineCodeTable();
|
$this->defineJaneClientsTable();
|
$this->defineRewardsTable();
|
$this->defineTreatmentsTable();
|
}
|
protected function defineReferralsTable():void
|
{
|
$table = CustomTable::for('referrals');
|
|
$table->setColumns([
|
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
|
'from_user' => "{$table->getUserIDType()} NOT NULL",
|
'to_user' => "{$table->getUserIDType()} NOT NULL",
|
'to_name' => 'varchar(255) NOT NULL',
|
'to_email' => 'varchar(255) NOT NULL',
|
'to_phone' => 'varchar(50) NOT NULL',
|
'referral_code'=> 'varchar(50) NOT NULL',
|
'status' => "ENUM('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'",
|
'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP',
|
'consulted_at'=> 'datetime DEFAULT NULL',
|
'treated_at'=> 'datetime DEFAULT NULL',
|
'treatment_count' => 'int DEFAULT 0',
|
'notes' => 'text DEFAULT NULL',
|
]);
|
|
$table->setKeys([
|
['key' => 'PRIMARY', 'value' => 'id'],
|
['key' => 'UNIQUE', 'value' => '(`to_user`)'],
|
'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
|
{
|
$requirements = [
|
'jvb-utility',
|
'jvb-a11y',
|
'jvb-popup',
|
'jvb-tabs',
|
'jvb-data-store',
|
];
|
|
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',
|
JVB_URL . 'assets/js/min/referral.min.js',
|
$requirements,
|
'1.0.0',
|
true
|
);
|
}
|
|
/**
|
* Register cron jobs for automated reporting
|
*/
|
protected function registerCronJobs(): void
|
{
|
add_action(BASE . 'referral_daily_report', [$this, 'sendDailyReport']);
|
add_action(BASE . 'referral_weekly_report', [$this, 'sendWeeklyReport']);
|
|
// Schedule if not already scheduled
|
if (!wp_next_scheduled(BASE . 'referral_daily_report')) {
|
wp_schedule_event(strtotime('tomorrow 9:00'), 'daily', BASE . 'referral_daily_report');
|
}
|
|
if (!wp_next_scheduled(BASE . 'referral_weekly_report')) {
|
wp_schedule_event(strtotime('next Monday 9:00'), 'weekly', BASE . 'referral_weekly_report');
|
}
|
}
|
|
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 int $user_id
|
* @param string|null $custom_code Optional custom code
|
* @return string|WP_Error
|
*/
|
public function getUserReferralCode(int $user_id, ?string $custom_code = null):string|false
|
{
|
$user = get_userdata($user_id);
|
if (!$user) {
|
return false;
|
}
|
|
$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 = $this->generateReferralCode($user);
|
|
$success = $this->createCode($user_id, $code);
|
if ($success) {
|
return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC')[0];
|
}
|
|
return false;
|
}
|
|
/**
|
* Generate a referral code based on user info
|
*
|
* @param WP_User $user
|
* @return string
|
*/
|
protected function generateReferralCode(WP_User $user): string
|
{
|
// Create code from first and last name
|
$first = sanitize_title($user->first_name ?: 'user');
|
$last = sanitize_title($user->last_name ?: wp_generate_password(4, false));
|
|
$base_code = strtoupper(substr($first, 0, 4) . substr($last, 0, 4));
|
|
// Ensure uniqueness
|
$code = $base_code;
|
$suffix = 1;
|
|
while ($this->isCodeTaken($code)) {
|
$code = $base_code . $suffix;
|
$suffix++;
|
}
|
|
return $code;
|
}
|
|
/**
|
* Check if a referral code is already taken
|
*
|
* @param string $code
|
* @return bool
|
*/
|
protected function isCodeTaken(string $code): bool
|
{
|
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, array $userData): bool
|
{
|
$referral = $this->referrals->get(['to_user' => $user_id]);
|
|
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->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;
|
}
|
|
|
// 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, '/');
|
}
|
|
// Clear caches
|
$this->cache->flush();
|
|
// Fire action for tracking
|
do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral['referral_code']);
|
|
// Send notification to referrer
|
$this->sendReferrerNotification($referrer->ID, $userData['display_name']);
|
return true;
|
}
|
|
/**
|
* Create a referral record in the database
|
*
|
* @param int $referrer_id
|
* @param int $referee_id
|
* @param string $code
|
* @return int|false
|
*/
|
public function createReferral(int $referrer_id, int $referee_id, string $code)
|
{
|
$user = get_user_by('ID', $referee_id);
|
|
return $this->wpdb->insert(
|
$this->referrals_table,
|
[
|
'referrer_id' => $referrer_id,
|
'referee_id' => $referee_id,
|
'referee_name' => $user->display_name,
|
'referee_email' => $user->user_email,
|
'referee_phone' => get_user_meta($referee_id, BASE . 'phone', true) ?: '',
|
'referral_code' => $code,
|
'status' => 'pending',
|
'referred_at' => current_time('mysql')
|
],
|
['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']
|
);
|
}
|
|
/**
|
* Get user by referral code
|
*
|
* @param string $code
|
* @return WP_User|null
|
*/
|
public function getUserByReferralCode(string $code): ?WP_User
|
{
|
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;
|
}
|
);
|
}
|
|
/**
|
* Get referral record by referee ID
|
*
|
* @param int $referee_id
|
* @return object|null
|
*/
|
public function getReferralByReferee(int $referee_id): ?object
|
{
|
$result = $this->wpdb->get_row($this->wpdb->prepare(
|
"SELECT * FROM {$this->referrals_table} WHERE referee_id = %d",
|
$referee_id
|
));
|
|
return $result ?: null;
|
}
|
|
/**
|
* Mark a referral as treated/rewarded
|
*
|
* @param int $referral_id
|
* @param bool $treated
|
* @return bool
|
*/
|
public function markAsTreated(int $referral_id, bool $treated = true): bool
|
{
|
$status = $treated ? 'treated' : 'pending';
|
|
$result = $this->wpdb->update(
|
$this->referrals_table,
|
[
|
'status' => $status,
|
'treated_at' => $treated ? current_time('mysql') : null
|
],
|
['id' => $referral_id],
|
['%s', '%s'],
|
['%d']
|
);
|
|
if ($result && $treated) {
|
// Create reward records when marking as treated
|
$this->createRewardRecords($referral_id);
|
}
|
|
return $result !== false;
|
}
|
|
/**
|
* Create reward records for both referrer and referee
|
*
|
* @param int $referral_id
|
*/
|
protected function createRewardRecords(int $referral_id): void
|
{
|
$referral = $this->wpdb->get_row($this->wpdb->prepare(
|
"SELECT * FROM {$this->referrals_table} WHERE id = %d",
|
$referral_id
|
));
|
|
if (!$referral) {
|
return;
|
}
|
|
// Create referrer reward
|
$this->wpdb->insert(
|
$this->rewards_table,
|
[
|
'referral_id' => $referral_id,
|
'user_id' => $referral->referrer_id,
|
'reward_type' => 'referrer',
|
'amount' => $this->settings['referrer_reward_amount'],
|
'status' => 'available',
|
'created_at' => current_time('mysql')
|
],
|
['%d', '%d', '%s', '%f', '%s', '%s']
|
);
|
|
// Create referee reward
|
$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,
|
[
|
'referral_id' => $referral_id,
|
'user_id' => $referral->referee_id,
|
'reward_type' => 'referee',
|
'amount' => $referee_amount,
|
'reward_calculation' => $this->settings['referee_reward_type'],
|
'status' => 'available',
|
'created_at' => current_time('mysql')
|
],
|
['%d', '%d', '%s', '%f', '%s', '%s', '%s']
|
);
|
}
|
|
/**
|
* Get referrals for a user
|
*
|
* @param int $user_id
|
* @param array $args
|
* @return array
|
*/
|
public function getUserReferrals(int $user_id, array $args = []): array
|
{
|
$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) {
|
$where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id);
|
|
if ($args['status'] !== 'all') {
|
$where .= $this->wpdb->prepare(" AND status = %s", $args['status']);
|
}
|
|
$query = "SELECT * FROM {$this->referrals_table}
|
{$where}
|
ORDER BY {$args['orderby']} {$args['order']}
|
LIMIT {$args['limit']} OFFSET {$args['offset']}";
|
|
$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);
|
}
|
);
|
|
}
|
|
/**
|
* Get referral statistics for a user
|
*
|
* @param int $user_id
|
* @return array
|
*/
|
public function getUserStats(int $user_id): array
|
{
|
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);
|
|
// 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['total_rewards'] = floatval($rewards ?? 0);
|
$stats['user_id'] = $user_id;
|
return $stats;
|
}
|
);
|
}
|
|
/**
|
* Get top referrers for a time period
|
*
|
* @param int $limit
|
* @param string $period 'day'|'week'|'month'|'all'
|
* @return array
|
*/
|
public function getTopReferrers(int $limit = 10, string $period = 'all'): array
|
{
|
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"
|
};
|
|
$where = "WHERE {$date_where}";
|
}
|
|
$query = "SELECT
|
referrer_id,
|
COUNT(*) as referral_count,
|
SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count
|
FROM {$this->referrals_table}
|
{$where}
|
GROUP BY referrer_id
|
ORDER BY referral_count DESC
|
LIMIT {$limit}";
|
|
$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 : '';
|
}
|
|
return $results;
|
}
|
);
|
|
}
|
|
/**
|
* Send daily report if there are new referrals
|
*/
|
public function sendDailyReport(): void
|
{
|
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
|
$new_referrals = $this->wpdb->get_results($this->wpdb->prepare(
|
"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
|
ORDER BY r.referred_at DESC",
|
$yesterday
|
));
|
|
if (empty($new_referrals)) {
|
return;
|
}
|
|
$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 .= JVB()->email()->spacer(20);
|
$content .= JVB()->email()->h2('New Referrals');
|
|
// Build list of referrals
|
foreach ($new_referrals as $ref) {
|
$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);
|
}
|
|
$to = get_option('admin_email');
|
$subject = sprintf(
|
'[%s] %d New Referral%s',
|
get_bloginfo('name'),
|
count($new_referrals),
|
count($new_referrals) !== 1 ? 's' : ''
|
);
|
|
JVB()->email()->sendEmail($to, $subject, $content, 'DAILY REPORT');
|
}
|
|
/**
|
* Send weekly report with top referrers
|
*/
|
public function sendWeeklyReport(): void
|
{
|
$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)"
|
);
|
|
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');
|
|
JVB()->email()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY');
|
}
|
|
/**
|
* Generate CSV content from referrals
|
*
|
* @param array $referrals
|
* @return string
|
*/
|
protected function generateCSV(array $referrals): string
|
{
|
$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'
|
);
|
}
|
|
return $csv;
|
}
|
);
|
|
}
|
|
/**
|
* Generate HTML email for weekly report
|
*
|
* @param array $top_referrers
|
* @param int $total_referrals
|
* @return string
|
*/
|
protected function generateWeeklyReportEmail(array $top_referrers, int $total_referrals): string
|
{
|
$content = sprintf(
|
'<p>This week you had <strong>%d total referral%s</strong>.</p>',
|
$total_referrals,
|
$total_referrals !== 1 ? 's' : ''
|
);
|
|
$referrers = [];
|
$rank = 1;
|
foreach ($top_referrers as $referrer) {
|
$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 .= JVB()->email()->table($referrers, 'Top 10 Referrers This Week');
|
|
return $content;
|
}
|
|
/**
|
* Get reward settings
|
*
|
* @return array
|
*/
|
public function getRewardSettings(): array
|
{
|
$saved = get_option(BASE . 'referral_settings', []);
|
return wp_parse_args($saved, $this->default_settings);
|
}
|
|
/**
|
* Display referral info in user profile
|
*
|
* @param WP_User $user
|
*/
|
public function displayUserReferralInfo(WP_User $user): void
|
{
|
if (!current_user_can('edit_user', $user->ID)) {
|
return;
|
}
|
|
$referral_code = get_user_meta($user->ID, BASE . 'referral_code', true);
|
$stats = $this->getUserStats($user->ID);
|
$referrals = $this->getUserReferrals($user->ID, ['limit' => 10]);
|
|
?>
|
<h2>Referral Information</h2>
|
<table class="form-table">
|
<tr>
|
<th><label for="referral_code">Referral Code</label></th>
|
<td>
|
<input type="text"
|
name="referral_code"
|
id="referral_code"
|
value="<?php echo esc_attr($referral_code); ?>"
|
class="regular-text" />
|
<p class="description">
|
Users can sign up with this code.
|
<?php if ($referral_code): ?>
|
Share link: <?php echo home_url('/?ref=' . $referral_code); ?>
|
<?php endif; ?>
|
</p>
|
</td>
|
</tr>
|
</table>
|
|
<h3>Referral Statistics</h3>
|
<table class="form-table">
|
<tr>
|
<th>Total Referrals:</th>
|
<td><?php echo $stats['total_referrals'] ?? 0; ?></td>
|
</tr>
|
<tr>
|
<th>Treated:</th>
|
<td><?php echo $stats['treated_count'] ?? 0; ?></td>
|
</tr>
|
<tr>
|
<th>Pending:</th>
|
<td><?php echo $stats['pending_count'] ?? 0; ?></td>
|
</tr>
|
<tr>
|
<th>Available Rewards:</th>
|
<td>$<?php echo number_format($stats['available_rewards'] ?? 0, 2); ?></td>
|
</tr>
|
<tr>
|
<th>Redeemed Rewards:</th>
|
<td>$<?php echo number_format($stats['redeemed_rewards'] ?? 0, 2); ?></td>
|
</tr>
|
</table>
|
|
<?php if (!empty($referrals)): ?>
|
<h3>Recent Referrals</h3>
|
<table class="widefat">
|
<thead>
|
<tr>
|
<th>Name</th>
|
<th>Email</th>
|
<th>Status</th>
|
<th>Referred At</th>
|
<th>Actions</th>
|
</tr>
|
</thead>
|
<tbody>
|
<?php foreach ($referrals as $referral): ?>
|
<tr>
|
<td><?php echo esc_html($referral->referee_name); ?></td>
|
<td><?php echo esc_html($referral->referee_email); ?></td>
|
<td><?php echo esc_html(ucfirst($referral->status)); ?></td>
|
<td><?php echo esc_html($referral->referred_at); ?></td>
|
<td>
|
<?php if ($referral->status === 'pending'): ?>
|
<button type="button"
|
onclick="markReferralTreated(<?php echo $referral->id; ?>)">
|
Mark as Treated
|
</button>
|
<?php endif; ?>
|
</td>
|
</tr>
|
<?php endforeach; ?>
|
</tbody>
|
</table>
|
<?php endif; ?>
|
|
<?php /**
|
<script>
|
function markReferralTreated(referralId) {
|
if (!confirm('Mark this referral as treated? This will create reward records.')) {
|
return;
|
}
|
|
fetch('<?php echo rest_url(BASE . '/v1/referrals/' ); ?>' + referralId + '/treat', {
|
method: 'POST',
|
headers: {
|
'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
|
}
|
})
|
.then(response => response.json())
|
.then(data => {
|
if (data.success) {
|
alert('Referral marked as treated!');
|
location.reload();
|
} else {
|
alert('Error: ' + (data.message || 'Unknown error'));
|
}
|
});
|
}
|
</script>
|
<?php
|
*/
|
}
|
|
/**
|
* Save custom referral code
|
*
|
* @param int $user_id
|
*/
|
public function saveUserReferralCode(int $user_id): void
|
{
|
if (!current_user_can('edit_user', $user_id)) {
|
return;
|
}
|
|
if (!isset($_POST['referral_code'])) {
|
return;
|
}
|
|
$code = sanitize_text_field($_POST['referral_code']);
|
|
if (empty($code)) {
|
delete_user_meta($user_id, BASE . 'referral_code');
|
return;
|
}
|
|
// Validate code format (alphanumeric only)
|
if (!preg_match('/^[A-Z0-9]+$/i', $code)) {
|
add_action('user_profile_update_errors', function($errors) {
|
$errors->add('invalid_referral_code',
|
'Referral code can only contain letters and numbers.');
|
});
|
return;
|
}
|
|
// Check if code is unique
|
if ($this->isCodeTaken($code, $user_id)) {
|
add_action('user_profile_update_errors', function($errors) {
|
$errors->add('referral_code_taken',
|
'This referral code is already in use.');
|
});
|
return;
|
}
|
|
update_user_meta($user_id, BASE . 'referral_code', strtoupper($code));
|
}
|
|
|
/**
|
* Display user's referral code and share options
|
* Use in templates or dashboard with: echo jvbReferralShareWidget();
|
*
|
* @return string HTML output
|
*/
|
public function outputShareWidget(array $actions):array
|
{
|
|
$user_id = get_current_user_id();
|
$content = '<aside class="main referral right">';
|
if (!$user_id) {
|
$content .= $this->getUnloggedInReferral();
|
} else {
|
$content .= $this->getLoggedInReferral($user_id);
|
}
|
$content .= '</aside>';
|
|
$actions[] =[
|
'button' => '<button type="button" class="attn toggle-referral row" title="Your Referrals" data-action="toggle-referral" aria-label="Open Referral Sidebar" aria-controls="referral" aria-expanded="false">
|
'.jvbIcon('hand-heart').'<span class="screen-reader-text"></span>
|
</button>',
|
'content' => $content
|
];
|
|
return $actions;
|
}
|
|
/**
|
* Display referral sidebar for non-logged-in users
|
*/
|
function getUnloggedInReferral(): string
|
{
|
ob_start();
|
JVB()->connect('cloudflare')->renderTurnstile();
|
$turnstile = ob_get_clean();
|
|
$reward_text = $this->getRewardText(true);
|
|
// 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>
|
|
<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'
|
]),
|
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 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'
|
],
|
'content' => $loginForm.$footer
|
]
|
];
|
|
|
|
return jvbRenderTabs($tabs, true);
|
}
|
|
|
protected function getReferralSuccessMessage(string $code): string
|
{
|
$referrer = $this->getUserByReferralCode($code);
|
|
if (!$referrer) {
|
return '';
|
}
|
|
$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';
|
|
$booking_url = apply_filters('jvb_referral_booking_url', home_url('/contact'));
|
|
ob_start();
|
?>
|
<div class="success-icon">
|
✓
|
</div>
|
|
<div class="success-content">
|
<h3>Success! Your Reward is Ready!</h3>
|
|
<div class="reward-highlight">
|
<p style="margin: 0 0 8px 0;">You'll receive:</p>
|
<p style="margin: 0;"><strong><?php echo esc_html($reward_text); ?> your first treatment!</strong></p>
|
</div>
|
|
<p>Your referral code <strong><?php echo esc_html($code); ?></strong> has been applied. Book your free consultation now to claim your reward!</p>
|
|
<a href="<?php echo esc_url($booking_url); ?>" class="cta-button">
|
Book Your Free Consultation
|
</a>
|
|
<div class="referred-by">
|
Referred by <strong><?php echo esc_html($referrer->display_name); ?></strong>
|
</div>
|
</div>
|
<?php
|
return ob_get_clean();
|
}
|
|
/**
|
* Display referral sidebar for logged-in users
|
*/
|
public function getLoggedInReferral(int $user_id): string
|
{
|
$referral_code = $this->getUserReferralCode($user_id);
|
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>
|
</header>
|
|
<?php $this->getShareButtons($user_id); ?>
|
|
<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" 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" title="Copy referral code">
|
<?= jvbIcon('copy'); ?>
|
<?= jvbIcon('check-circle'); ?>
|
</button>
|
</div>
|
<p class="hint">Manually copy and paste the code</p>
|
|
</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 btw">
|
<span class="stat-label">Total Referrals</span>
|
<span class="stat-value" data-stat="total">-</span>
|
</div>
|
<div class="row btw">
|
<span class="stat-label">Successful</span>
|
<span class="stat-value" data-stat="treated">-</span>
|
</div>
|
<div class="row btw">
|
<span class="stat-label">Pending</span>
|
<span class="stat-value" data-stat="pending">-</span>
|
</div>
|
<div class="row 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();
|
}
|
|
function addReferralToWelcomeEmail(string $content, WP_User $user): string
|
{
|
$referral = $this->getReferralByReferee($user->ID);
|
|
if (!$referral) {
|
return $content;
|
}
|
|
$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 = 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,
|
);
|
|
|
$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 . $yourCode;
|
}
|
|
public function getShareURL(string $code):string
|
{
|
return add_query_arg(
|
[
|
'ref' => $code
|
],
|
get_home_url()
|
);
|
}
|
|
/**
|
* Send referral invitation via email with magic link
|
*
|
* @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, string $subject, string $message):array|WP_Error
|
{
|
// Check email rate limit (15/hour)
|
$rate_check = $this->checkEmailRateLimit($user_id);
|
if ($rate_check !== true) {
|
return new WP_Error('rate_limit', 'You can only send 15 invitations per hour. Please try again later.');
|
}
|
|
// Validate email
|
$invitee_email = sanitize_email($invitee_email);
|
if (!is_email($invitee_email)) {
|
return new WP_Error('invalid_email', 'Invalid email address');
|
}
|
|
// Check if already registered
|
if (email_exists($invitee_email)) {
|
return new WP_Error('user_exists', 'This person already has an account');
|
}
|
|
// Get referrer info
|
$referrer = get_user_by('ID', $user_id);
|
$referral_code = $this->getUserReferralCode($user_id);
|
|
if ($referral_code) {
|
return $referral_code;
|
}
|
|
// Record the invitation attempt (for rate limiting only)
|
$this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name);
|
|
// 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,
|
$subject,
|
$email_content
|
);
|
|
if (!$sent) {
|
return new WP_Error('email_failed', 'Failed to send invitation email');
|
}
|
|
return [
|
'success' => true,
|
'message' => 'Invitation sent successfully',
|
'email' => $invitee_email,
|
'name' => $invitee_name
|
];
|
}
|
|
/**
|
* Send multiple referral invitations
|
*
|
* @param int $user_id Referrer's user ID
|
* @param array $invitations Array of ['email' => '', 'name' => '']
|
* @return array Results with success/failed arrays
|
*/
|
public function sendBatchReferralInvitations(int $user_id, array $invitations, string $subject, string $message): array
|
{
|
$results = [
|
'success' => [],
|
'failed' => []
|
];
|
|
foreach ($invitations as $invite) {
|
$email = $invite['email'] ?? '';
|
$name = $invite['name'] ?? '';
|
|
if (empty($email) || empty($name)) {
|
$results['failed'][] = [
|
'email' => $email,
|
'name' => $name,
|
'reason' => 'Missing email or name'
|
];
|
continue;
|
}
|
|
$result = $this->sendReferralInvitation($user_id, $email, $name, $subject, $message);
|
|
if (is_wp_error($result)) {
|
$results['failed'][] = [
|
'email' => $email,
|
'name' => $name,
|
'reason' => $result->get_error_message()
|
];
|
} else {
|
$results['success'][] = [
|
'email' => $email,
|
'name' => $name
|
];
|
}
|
|
// Small delay between sends to be respectful
|
usleep(100000); // 0.1 seconds
|
}
|
|
return [
|
'success' => !empty($results['success']),
|
'result' => $results,
|
'summary' => sprintf(
|
'Sent %d invitations, %d failed',
|
count($results['success']),
|
count($results['failed'])
|
)
|
];
|
}
|
|
/**
|
* Check email invitation rate limit (15 per hour)
|
*
|
* @param int $user_id
|
* @return true|string True if allowed, error message if limited
|
*/
|
protected function checkEmailRateLimit(int $user_id):bool|string
|
{
|
$hourly_key = 'referral_invites_hour_' . $user_id;
|
$count = (int) get_transient($hourly_key);
|
|
if ($count >= 15) {
|
return 'hourly_limit_reached';
|
}
|
|
set_transient($hourly_key, $count + 1, HOUR_IN_SECONDS);
|
return true;
|
}
|
|
/**
|
* Check if an email has already been invited
|
*
|
* @param string $email
|
* @return bool
|
*/
|
protected function isEmailInvited(string $email): bool
|
{
|
// Check invitation tracking table
|
$invitation_key = 'referral_invite_' . md5($email);
|
$invited = get_transient($invitation_key);
|
|
if ($invited) {
|
return true;
|
}
|
|
// Check if there's a pending referral for this email
|
$existing = $this->wpdb->get_var($this->wpdb->prepare(
|
"SELECT id FROM {$this->referrals_table} WHERE referee_email = %s",
|
$email
|
));
|
|
return !empty($existing);
|
}
|
|
/**
|
* Record invitation attempt (for tracking and preventing duplicates)
|
*
|
* @param int $user_id
|
* @param string $email
|
* @param string $name
|
*/
|
protected function recordInvitationAttempt(int $user_id, string $email, string $name): void
|
{
|
// Store for 30 days (same as magic link invitation validity)
|
$invitation_key = 'referral_invite_' . md5($email);
|
$data = [
|
'inviter_id' => $user_id,
|
'email' => $email,
|
'name' => $name,
|
'sent_at' => current_time('mysql')
|
];
|
|
set_transient($invitation_key, $data, 30 * DAY_IN_SECONDS);
|
|
// Also log in user meta for tracking
|
$sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: [];
|
$sent_invites[] = [
|
'email' => $email,
|
'name' => $name,
|
'sent_at' => current_time('mysql')
|
];
|
update_user_meta($user_id, BASE . 'referral_invites_sent', $sent_invites);
|
}
|
|
/**
|
* Get user's invitation stats
|
*
|
* @param int $user_id
|
* @return array
|
*/
|
public function getUserInvitationStats(int $user_id): array
|
{
|
$sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: [];
|
|
// Count invites sent in last hour
|
$one_hour_ago = strtotime('-1 hour');
|
$recent_count = 0;
|
|
foreach ($sent_invites as $invite) {
|
if (strtotime($invite['sent_at']) > $one_hour_ago) {
|
$recent_count++;
|
}
|
}
|
|
return [
|
'total_sent' => count($sent_invites),
|
'sent_last_hour' => $recent_count,
|
'remaining_this_hour' => max(0, 15 - $recent_count),
|
'can_send_more' => $recent_count < 15
|
];
|
}
|
|
/**
|
* Export referrals for Jane App cross-reference
|
*
|
* @param string $start_date Y-m-d format
|
* @param string $end_date Y-m-d format
|
* @return string CSV content
|
*/
|
public function exportReferrals(string $start_date, string $end_date): string
|
{
|
$referrals = $this->wpdb->get_results($this->wpdb->prepare(
|
"SELECT
|
r.id,
|
r.referee_name,
|
r.referee_email,
|
r.referee_phone,
|
r.referral_code,
|
r.referred_at,
|
r.status,
|
r.treated_at,
|
u.display_name as referrer_name,
|
u.user_email as referrer_email
|
FROM {$this->referrals_table} r
|
JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
|
WHERE DATE(r.referred_at) BETWEEN %s AND %s
|
ORDER BY r.referred_at DESC",
|
$start_date,
|
$end_date
|
));
|
|
// Build CSV
|
$csv_lines = [];
|
|
// Headers
|
$csv_lines[] = [
|
'Referral ID',
|
'Referee Name',
|
'Referee Email',
|
'Referee Phone',
|
'Referral Code',
|
'Referred Date',
|
'Status',
|
'Treated Date',
|
'Referrer Name',
|
'Referrer Email'
|
];
|
|
// Data rows
|
foreach ($referrals as $ref) {
|
$csv_lines[] = [
|
$ref->id,
|
$ref->referee_name,
|
$ref->referee_email,
|
$ref->referee_phone ?: 'N/A',
|
$ref->referral_code,
|
$ref->referred_at,
|
ucfirst($ref->status),
|
$ref->treated_at ?: 'N/A',
|
$ref->referrer_name,
|
$ref->referrer_email
|
];
|
}
|
|
// Convert to CSV string
|
$output = fopen('php://temp', 'r+');
|
foreach ($csv_lines as $line) {
|
fputcsv($output, $line);
|
}
|
rewind($output);
|
$csv_content = stream_get_contents($output);
|
fclose($output);
|
|
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 btw nowrap">
|
<code id="referral-link" class="copy-target"><?= home_url('/?ref=' . $referral_code) ?></code>
|
<button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link">
|
<?= jvbIcon('copy'); ?>
|
<?= jvbIcon('check-circle'); ?>
|
</button>
|
</div>
|
<h3>Share Code</h3>
|
<div class="row btw nowrap">
|
<code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code>
|
<button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code">
|
<?= jvbIcon('copy'); ?>
|
<?= jvbIcon('check-circle'); ?>
|
</button>
|
</div>
|
</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();
|
}
|
}
|