Jake Vanderwerf
2025-12-21 3aada9949d51024a92a8b5c6cb70d12f9c3cac16
assets/js/concise/Referral.js
@@ -5,7 +5,7 @@
class Referral {
   constructor() {
      this.container = document.querySelector('.jvb-referral');
      this.container = document.querySelector('aside.referral');
      if (!this.container) {
         return;
      }
@@ -13,15 +13,12 @@
      this.a11y = window.jvbA11y;
      this.toggle = document.querySelector('button[data-action="toggle-referral"]');
      this.hasCopy = navigator.clipboard && navigator.clipboard.writeText;
      this.initElements();
      this.storesInited = false;
      this.initStore();
      this.initListeners();
      this.checkForReferral();
      // Load additional data for logged-in users
      if (this.isLoggedIn()) {
         this.loadStats();
         this.loadRecentReferrals();
      }
   }
   initElements() {
@@ -29,6 +26,17 @@
         copyBtn: '.copy-btn',
         checkCode: '.check-code-btn',
         submit: '[type=submit]',
         recentList: '.recent-referrals-list',
         invite: 'form.invite',
         adminList: '.items-list.referral',
         dash: '.replace .referral-dashboard',
         stats: {
            codeUsed: '[data-stat="code_used"]',
            consultations: '[data-stat="consultations"]',
            treatments: '[data-stat="treatments"]',
            rewards: '[data-stat="total_rewards"]'
         },
         list: '.referrals-list'
      };
      this.forms = this.container.querySelectorAll('form');
@@ -45,11 +53,120 @@
      });
      this.tabs = null;
      if (this.container.querySelector('nav.tabs')) {
         this.tabs = new window.jvbTabs(this.container, {updateURL: false});
      }
      this.ui = window.uiFromSelectors(this.selectors, this.container);
      this.ui = window.uiFromSelectors(this.selectors);
      this.dashTabs = null;
      if (this.ui.dash) {
         this.dashTabs = new window.jvbTabs(this.ui.dash);
      }
      if (!this.hasCopy) {
         document.querySelectorAll(this.selectors.copyBtn).forEach(btn => {
            btn.remove();
         });
      }
      this.formController = null;
      if (this.ui.invite) {
         this.formController = new window.jvbForm();
         this.formController.registerForm(
            this.ui.invite,
            {
               autosave: true,
               endpoint: 'referrals',
               formStatus: false,
            }
         );
         this.formController.subscribe((event, data) => {
            if (event === 'form-submit') {
               data = data.fullData;
               data.action = 'invite';
               window.jvbQueue.addToQueue(
                  {
                     endpoint: 'referrals',
                     data: data,
                     title: 'Submitting invitations',
                  }
               );
            }
         });
      }
   }
   initStore() {
      if (!this.isLoggedIn()) return;
      const stores = window.jvbStore.register(
         'referrals',
         [
            // Dashboard stats store
            {
               storeName: 'stats',
               keyPath: 'user_id',
               endpoint: 'referrals/stats',
               TTL: 5 * 60 * 1000,
               showLoading: false,
               delayFetch: false,
               filters: {
                  type: 'dashboard',
                  user: window.auth.getUser()
               }
            },
            // Referrals list store
            {
               storeName: 'list',
               keyPath: 'id',
               endpoint: 'referrals',
               TTL: 10 * 60 * 1000,
               showLoading: false,
               delayFetch: false,
               filters: {
                  user: window.auth.getUser(),
                  status: 'all',
                  limit: 50,
                  offset: 0
               }
            }
         ]
      );
      this.statsStore = stores.stats;
      this.listStore = stores.list;
      // Subscribe to store events
      if (this.statsStore) {
         this.statsStore.subscribe(this.handleStatsEvent.bind(this));
      }
      if (this.listStore) {
         this.listStore.subscribe(this.handleListEvent.bind(this));
      }
      if (this.ui.dash) {
         this.initViewController();
      }
   }
   initViewController() {
      if (!this.listStore || !this.ui.adminList) return;
      this.view = new window.jvbViews(this.ui.adminList, this.listStore);
      this.view.subscribe((event, data) => {
         switch(event) {
            case 'item-action':
               this.handleItemAction(data);
               break;
            case 'bulk-action':
               this.handleBulkAction(data);
               break;
         }
      });
   }
   initListeners() {
@@ -70,9 +187,151 @@
   }
   isLoggedIn() {
      return Boolean(jvbSettings.currentUser);
      return Boolean(window.auth.getUser());
   }
   /**
    * Handle DataStore stats events
    */
   handleStatsEvent(event, data) {
      switch(event) {
         case 'data-loaded':
            if (data.items && data.items.length > 0) {
               this.updateStatsDisplay();
            }
            break;
         case 'fetch-error':
            console.error('Error loading stats:', data.error);
            break;
      }
   }
   /**
    * Handle DataStore list events
    */
   handleListEvent(event, data) {
      switch(event) {
         case 'data-loaded':
            // Let ViewController handle main list rendering
            // Only update sidebar preview if it exists
            if (this.ui.recentList) {
               this.renderRecentReferrals();
            }
            break;
         case 'fetch-error':
            console.error('Error loading referrals:', data.error);
            break;
      }
   }
   /**
    * Update stats display
    */
   updateStatsDisplay() {
      if (!this.statsStore.data.size === 0) return;
      let stats = this.statsStore.data.get(parseInt(window.auth.getUser()));
      const updates = {
         total: stats['code_used'] || 0,
         treated: stats.treatments || 0,
         pending: stats.pending || 0,
         rewards: '$' + parseFloat(stats['total_rewards'] || 0).toFixed(2)
      };
      Object.entries(updates).forEach(([key, value]) => {
         const element = this.container.querySelector(`[data-stat="${key}"]`);
         if (element) {
            element.textContent = value;
         }
      });
      // Also update stat cards if on dashboard
      const statCards = this.container.querySelectorAll('.stats .card');
      if (statCards.length >= 4) {
         statCards[0].querySelector('.stat-number').textContent = updates.code_used;
         statCards[1].querySelector('.stat-number').textContent = updates.consultations;
         statCards[2].querySelector('.stat-number').textContent = updates.treatments;
         statCards[3].querySelector('.stat-number').textContent = updates.total_rewards;
      }
   }
   /**
    * Handle item actions (remove, resend)
    */
   handleItemAction(data) {
      const { action, itemId } = data;
      switch(action) {
         case 'remove':
            this.removeReferral(itemId);
            break;
         case 'resend':
            this.resendInvite(itemId);
            break;
      }
   }
   /**
    * Remove referral from list
    */
   async removeReferral(id) {
      if (!confirm('Remove this referral from your list?')) return;
      try {
         const response = await fetch(`${jvbSettings.api}referrals`, {
            method: 'POST',
            headers: {
               'Content-Type': 'application/json',
               'X-WP-Nonce': window.auth.getNonce()
            },
            body: JSON.stringify({
               action: 'remove',
               referral_id: id
            })
         });
         const result = await response.json();
         if (result.success) {
            // Refresh DataStore
            if (this.listStore) this.listStore.fetch();
            if (this.statsStore) this.statsStore.fetch();
            this.a11y?.announce('Referral removed');
         }
      } catch (error) {
         console.error('Error removing referral:', error);
      }
   }
   /**
    * Resend invite email
    */
   async resendInvite(id) {
      try {
         const response = await fetch(`${jvbSettings.api}referrals`, {
            method: 'POST',
            headers: {
               'Content-Type': 'application/json',
               'X-WP-Nonce': window.auth.getNonce()
            },
            body: JSON.stringify({
               action: 'resend',
               referral_id: id
            })
         });
         const result = await response.json();
         if (result.success) {
            this.a11y?.announce('Invitation resent');
         } else {
            alert(result.message || 'Cannot resend yet. Wait 7 days between invites.');
         }
      } catch (error) {
         console.error('Error resending invite:', error);
      }
   }
   handleClick(e) {
      const target = e.target.closest('.copy-btn, .check-code-btn, .attn');
      if (!target) return;
@@ -98,7 +357,7 @@
      const text = codeElement.textContent.trim();
      // Try clipboard API first
      if (navigator.clipboard && navigator.clipboard.writeText) {
      if (this.hasCopy) {
         navigator.clipboard.writeText(text).then(() => {
            this.showCopySuccess(button);
         }).catch(() => {
@@ -106,10 +365,6 @@
            this.selectText(codeElement);
            this.showCopyFallback(button);
         });
      } else {
         // Fallback to selection
         this.selectText(codeElement);
         this.showCopyFallback(button);
      }
   }
@@ -226,15 +481,19 @@
    * Check for ?ref parameter in URL and pre-fill code
    */
   async checkForReferral() {
      const isLoggedIn = this.getUrlParameter('seeReferral');
      const refCode = this.getUrlParameter('ref');
      const refName = this.getUrlParameter('rname');
      const refEmail = this.getUrlParameter('remail');
      const seeReferral = this.getUrlParameter('seeReferral');
      if (!isLoggedIn && !refCode) {
      if (!refCode && !seeReferral) {
         return;
      }
      if (!refCode) {
      // If logged in user just wants to see referral popup
      if (seeReferral && !refCode) {
         this.popup.openPopup();
         this.removeUrlParameter('seeReferral');
         return;
      }
@@ -248,7 +507,21 @@
      codeInput.value = code;
      codeInput.readOnly = true;
      this.popup.togglePopup();
      // If we have token data, prefill name and email too
      if (refName || refEmail) {
         const nameInput = this.container.querySelector('[name="referral_name"]');
         if (nameInput) {
            nameInput.value = refName;
         }
         const emailInput = this.container.querySelector('[name="referral_email"]');
         if (emailInput) {
            emailInput.value = refEmail;
         }
      }
      // Open the sidebar popup
      this.popup.openPopup();
      // Validate the code immediately
      try {
@@ -264,9 +537,9 @@
               );
            }
            // Focus on name input
            // Focus on name input if not prefilled
            const nameInput = this.container.querySelector('[name="referral_name"]');
            if (nameInput) {
            if (nameInput && !nameInput.value) {
               nameInput.focus();
            }
         } else {
@@ -280,6 +553,8 @@
      // Clean up URL
      this.removeUrlParameter('ref');
      this.removeUrlParameter('rname');
      this.removeUrlParameter('remail');
   }
   getUrlParameter(name) {
@@ -297,11 +572,11 @@
    * Validate code without registering
    */
   async validateCodeOnly(code) {
      const response = await fetch(`${jvbSettings.api}referrals/check-code`, {
      const response = await fetch(`${jvbSettings.api}referrals/code`, {
         method: 'POST',
         headers: {
            'Content-Type': 'application/json',
            'X-WP-Nonce': jvbSettings.nonce
            'X-WP-Nonce': window.auth.getNonce()
         },
         body: JSON.stringify({ code: code })
      });
@@ -317,8 +592,8 @@
      if (!statsContainer) return;
      try {
         const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${jvbSettings.currentUser}`, {
            headers: { 'X-WP-Nonce': jvbSettings.nonce }
         const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${window.auth.getUser()}`, {
            headers: { 'X-WP-Nonce': window.auth.getNonce() }
         });
         const data = await response.json();
@@ -330,6 +605,23 @@
      }
   }
   async loadSidebarStats() {
      try {
         const response = await fetch(
            `${jvbSettings.api}referrals/stats?user=${window.auth.getUser()}&type=quick`,
            { headers: { 'X-WP-Nonce': window.auth.getNonce() } }
         );
         const data = await response.json();
         if (data.success && data.stats) {
            this.updateSidebarStats(data.stats);
         }
      } catch (error) {
         console.error('Error loading sidebar stats:', error);
      }
   }
   /**
    * Update stats display
    */
@@ -350,49 +642,25 @@
   }
   /**
    * Load recent referrals (last 5)
    */
   async loadRecentReferrals() {
      const container = this.container.querySelector('.recent-referrals-list');
      if (!container) return;
      try {
         const response = await fetch(`${jvbSettings.api}referrals/my-referrals?limit=5&user=${jvbSettings.currentUser}`, {
            headers: { 'X-WP-Nonce': jvbSettings.nonce }
         });
         const data = await response.json();
         if (data.success && data.referrals) {
            this.renderRecentReferrals(container, data.referrals);
         } else {
            container.innerHTML = '<p class="no-referrals">No referrals yet</p>';
         }
      } catch (error) {
         console.error('Error loading referrals:', error);
         container.innerHTML = '<p class="error">Failed to load referrals</p>';
      }
   }
   /**
    * Render recent referrals list
    */
   renderRecentReferrals(container, referrals) {
   renderRecentReferrals() {
      let container = this.ui.recentList;
      let referrals = Array.from(this.listStore.data.values());
      if (!referrals || referrals.length === 0) {
         container.innerHTML = '<p class="no-referrals">Share your code to get started!</p>';
         return;
      }
      const html = referrals.map(ref => `
      container.innerHTML = referrals.map(ref => `
         <div class="referral-item">
            <div class="referral-info">
               <strong>${window.escapeHtml(ref.referee_name)}</strong>
               <span class="status-badge ${ref.status}">${ref.status}</span>
               <span class="status-badge">${ref.referral_status}</span>
            </div>
            <div class="referral-date">${this.formatDate(ref.referred_at)}</div>
            <div class="referral-date">${window.formatTimeAgo(ref.referred_at)}</div>
         </div>
      `).join('');
      container.innerHTML = html;
   }
   /**
@@ -420,23 +688,23 @@
      const form = event.target;
      const formData = new FormData(form);
      // Disable form
      this.setFormLoading(true, form);
      try {
         let result = { success: false, message: '' };
         if (form.id === 'referral-code-form') {
            // Registration with referral code - goes to LoginRoutes
            const data = {
               name: formData.get('referral_name'),
               email: formData.get('referral_email'),
               code: formData.get('referral_code')
               referral_code: formData.get('referral_code')
            };
            if (!data.name || !data.email || !data.code) {
            if (!data.name || !data.email || !data.referral_code) {
               result.message = 'Please fill in all fields';
            } else {
               result = await this.makeRequest('referrals/register', data);
               result = await this.makeRequest('auth/register', data); // UPDATED endpoint
            }
         } else if (form.id === 'login-form') {
            const data = {
@@ -465,8 +733,7 @@
   async makeRequest(endpoint, data) {
      const validEndpoints = [
         'magic',
         'referrals/register',
         'referrals/check-code'
         'auth/register'
      ];
      if (!validEndpoints.includes(endpoint)) {
@@ -477,11 +744,22 @@
         method: 'POST',
         headers: {
            'Content-Type': 'application/json',
            'X-WP-Nonce': jvbSettings.nonce,
            'X-WP-Nonce': window.auth.getNonce(),
         },
         body: JSON.stringify(data)
      });
      // Add error handling to see the actual response
      if (!response.ok) {
         const errorText = await response.text();
         console.error('Error response:', response.status, errorText);
         try {
            return JSON.parse(errorText);
         } catch {
            return { success: false, message: 'Server error' };
         }
      }
      return await response.json();
   }
@@ -565,6 +843,10 @@
   }
}
document.addEventListener('DOMContentLoaded', () => {
   window.jvbReferral = new Referral();
document.addEventListener('DOMContentLoaded', async function () {
   window.auth.subscribe((event) => {
      if (event === 'auth-loaded') {
         window.jvbReferral = new Referral();
      }
   });
});