class ReferralAdmin {
|
constructor() {
|
this.a11y = window.jvbA11y;
|
this.referral = window.jvbReferral;
|
this.hasCopy = navigator.clipboard && navigator.clipboard.writeText;
|
this.initElements();
|
this.initListeners();
|
|
}
|
|
initElements() {
|
this.selectors = {
|
copyBtn: '.copy-btn',
|
invite: 'form.invite',
|
adminList: '.items-list.referral',
|
dash: '.replace .referral-dashboard',
|
list: '.referrals-list'
|
}
|
this.ui = window.uiFromSelectors(this.selectors);
|
|
this.tabs = null;
|
if (this.ui.dash) {
|
this.tabs =window.jvbTabs.registerTab(this.ui.dash);
|
this.initViewController();
|
}
|
if (this.ui.invite) {
|
this.formController = window.jvbForm;
|
this.formConfig = this.formController.registerForm(
|
this.ui.invite,
|
{
|
autosave: true,
|
endpoint: 'referrals',
|
formStatus: false,
|
}
|
);
|
|
this.formController.subscribe((event, payload) => {
|
if (event !== 'form-submit') return;
|
|
const formData = {
|
...payload.data, // ← THIS is your form data
|
action: 'invite'
|
};
|
|
window.jvbQueue.addToQueue({
|
endpoint: 'referrals',
|
data: formData,
|
title: 'Submitting invitations',
|
});
|
|
this.formController.clearForm(this.formConfig.id);
|
let button = document.querySelector('.referral-dashboard button[type="submit"]');
|
let original = button.innerHTML;
|
button.innerText = 'Invites sent to server. In line for processing.';
|
window.debouncer.schedule(
|
'referral-submit',
|
function() {
|
button.innerHTML = original;
|
},
|
3000
|
);
|
|
});
|
}
|
|
}
|
|
initListeners() {
|
this.clickHandler = this.handleClick.bind(this);
|
document.addEventListener('click', this.clickHandler);
|
if (window.jvbQueue) {
|
window.jvbQueue.subscribe(this.handleQueueEvent.bind(this));
|
}
|
}
|
|
handleClick(e) {
|
const target = e.target.closest('.copy-btn');
|
if (target) {
|
this.handleCopyClick(target);
|
}
|
}
|
|
handleCopyClick(button) {
|
const targetId = button.dataset.target;
|
const codeElement = button.closest('.row').querySelector(`#${targetId}`);
|
|
if (!codeElement) return;
|
|
const text = codeElement.textContent.trim();
|
|
// Try clipboard API first
|
if (this.hasCopy) {
|
navigator.clipboard.writeText(text).then(() => {
|
button.classList.toggle('success');
|
setTimeout(() => {
|
button.classList.remove('success');
|
}, 1500);
|
});
|
}
|
}
|
|
initViewController() {
|
if (!this.referral.listStore || !this.ui.adminList) return;
|
|
this.view = new window.jvbViews(this.ui.adminList, this.referral.listStore);
|
this.view.subscribe((event, data) => {
|
switch(event) {
|
case 'item-action':
|
this.handleItemAction(data);
|
break;
|
case 'bulk-action':
|
this.handleBulkAction(data);
|
break;
|
}
|
});
|
}
|
|
/**
|
* 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.referral.listStore) this.referral.listStore.fetch();
|
if (this.referral.statsStore) this.referral.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);
|
}
|
}
|
|
/***************************
|
BACKEND stuff
|
***************************/
|
handleQueueEvent(event, data) {
|
if (event !== 'operation-complete') return;
|
if (!data.details) return; // Not our operation
|
|
// Check if it's our invite operation
|
if (data.details.successful || data.details.failed) {
|
this.showInviteResults(data.details);
|
}
|
}
|
|
// showInviteResults(details) {
|
// const successful = details.successful?.length || 0;
|
// const failed = details.failed?.length || 0;
|
//
|
// if (failed === 0) {
|
// this.a11y?.announce(`All ${successful} invitations sent successfully!`);
|
// // Clear the form
|
// this.ui.invite?.reset();
|
// } else {
|
// // Show which ones failed
|
// const failureList = details.failed
|
// .map(f => `• ${f.name} (${f.email}): ${f.reason}`)
|
// .join('\n');
|
//
|
// const message = `${successful} sent, ${failed} failed:\n${failureList}`;
|
//
|
// // Show in a modal or persistent notification
|
// alert(message); // Or use a nicer notification system
|
// }
|
// }
|
|
showInviteResults(details) {
|
if (!this.ui.invite) return;
|
|
const tagListField = this.ui.invite.querySelector('[data-field="invite"]');
|
if (!tagListField) return;
|
|
const tagList = tagListField.querySelector('.tag-list');
|
if (!tagList) return;
|
|
// Map results by email for easy lookup
|
const resultMap = new Map();
|
|
details.successful?.forEach(item => {
|
resultMap.set(item.email, { status: 'success', name: item.name });
|
});
|
|
details.failed?.forEach(item => {
|
resultMap.set(item.email, {
|
status: 'error',
|
name: item.name,
|
reason: item.reason
|
});
|
});
|
|
// Update each tag with status
|
const tags = tagList.querySelectorAll('.tag');
|
tags.forEach(tag => {
|
const tagData = JSON.parse(tag.dataset.value || '{}');
|
const result = resultMap.get(tagData.email);
|
|
if (result) {
|
this.updateTagStatus(tag, result);
|
}
|
});
|
|
// Show summary notification
|
this.showInviteSummary(details);
|
}
|
|
updateTagStatus(tag, result) {
|
// Remove existing status
|
tag.classList.remove('success', 'error');
|
const existingIcon = tag.querySelector('.status-icon');
|
if (existingIcon) existingIcon.remove();
|
|
// Add new status
|
tag.classList.add(result.status);
|
|
const icon = document.createElement('span');
|
icon.className = 'status-icon';
|
icon.innerHTML = result.status === 'success'
|
? window.jvbIcon('check-circle', { size: 14 })
|
: window.jvbIcon('warning-circle', { size: 14 });
|
|
if (result.reason) {
|
icon.title = result.reason;
|
}
|
|
// Insert icon before the remove button
|
const removeBtn = tag.querySelector('.remove-tag');
|
if (removeBtn) {
|
tag.insertBefore(icon, removeBtn);
|
} else {
|
tag.appendChild(icon);
|
}
|
}
|
|
showInviteSummary(details) {
|
const successful = details.successful?.length || 0;
|
const failed = details.failed?.length || 0;
|
|
let message = `Invites sent! ${successful} successful`;
|
if (failed > 0) {
|
message += `, ${failed} failed`;
|
}
|
|
// Show in form status or as toast notification
|
if (this.formController) {
|
this.formController.showStatus(this.ui.invite, 'submitted', message);
|
}
|
|
// Optionally show detailed failures
|
if (failed > 0) {
|
this.showFailureDetails(details.failed);
|
}
|
}
|
|
showFailureDetails(failed) {
|
// Create a details element or modal showing why each failed
|
const detailsHTML = `
|
<details class="invite-failures" open>
|
<summary>${failed.length} invitation(s) failed - click for details</summary>
|
<ul>
|
${failed.map(item => `
|
<li>
|
<strong>${window.escapeHtml(item.name)}</strong>
|
(${window.escapeHtml(item.email)}):
|
<em>${window.escapeHtml(item.reason)}</em>
|
</li>
|
`).join('')}
|
</ul>
|
</details>
|
`;
|
|
// Insert after the form or in a notification area
|
const statusArea = this.ui.invite.querySelector('.fstatus');
|
if (statusArea) {
|
const existingDetails = statusArea.querySelector('.invite-failures');
|
if (existingDetails) existingDetails.remove();
|
statusArea.insertAdjacentHTML('beforeend', detailsHTML);
|
}
|
}
|
}
|
|
document.addEventListener('DOMContentLoaded', async function () {
|
window.auth.subscribe((event) => {
|
if (event === 'auth-loaded') {
|
window.jvbAdminReferral = new ReferralAdmin();
|
}
|
});
|
});
|