-
+
diff --git a/portal/portal.js b/portal/portal.js
index f56c919..c8a1667 100644
--- a/portal/portal.js
+++ b/portal/portal.js
@@ -111,40 +111,20 @@ const Portal = {
if (data.OK && data.BUSINESS) {
const biz = data.BUSINESS;
this.businessData = biz; // Store for later use
- const bizName = biz.Name || 'Business';
- const bizInitial = bizName.charAt(0).toUpperCase();
- document.getElementById('businessName').textContent = bizName;
- document.getElementById('businessAvatar').textContent = bizInitial;
- document.getElementById('userAvatar').textContent = bizInitial;
+ document.getElementById('businessName').textContent = biz.Name || 'Business';
+ document.getElementById('businessAvatar').textContent = (biz.Name || 'B').charAt(0).toUpperCase();
+ document.getElementById('userAvatar').textContent = 'U';
} else {
this.businessData = null;
document.getElementById('businessName').textContent = 'Business #' + this.config.businessId;
document.getElementById('businessAvatar').textContent = 'B';
- document.getElementById('userAvatar').textContent = 'B';
+ document.getElementById('userAvatar').textContent = 'U';
}
} catch (err) {
console.error('[Portal] Business info error:', err);
document.getElementById('businessName').textContent = 'Business #' + this.config.businessId;
document.getElementById('businessAvatar').textContent = 'B';
- document.getElementById('userAvatar').textContent = 'B';
- }
- },
-
- // Toggle user dropdown menu
- toggleUserMenu() {
- const dd = document.getElementById('userDropdown');
- if (!dd) return;
- const showing = dd.style.display !== 'none';
- dd.style.display = showing ? 'none' : 'block';
- if (!showing) {
- // Close on outside click
- const close = (e) => {
- if (!dd.contains(e.target) && e.target.id !== 'userBtn' && !e.target.closest('#userBtn')) {
- dd.style.display = 'none';
- document.removeEventListener('click', close);
- }
- };
- setTimeout(() => document.addEventListener('click', close), 0);
+ document.getElementById('userAvatar').textContent = 'U';
}
},
@@ -153,7 +133,6 @@ const Portal = {
localStorage.removeItem('payfrit_portal_token');
localStorage.removeItem('payfrit_portal_userid');
localStorage.removeItem('payfrit_portal_business');
- localStorage.removeItem('payfrit_portal_firstname');
window.location.href = BASE_PATH + '/portal/login.html';
},
@@ -239,6 +218,7 @@ const Portal = {
beacons: 'Beacons',
services: 'Service Requests',
'admin-tasks': 'Task Admin',
+ 'sp-sharing': 'SP Sharing',
settings: 'Settings'
};
document.getElementById('pageTitle').textContent = titles[page] || page;
@@ -279,6 +259,9 @@ const Portal = {
case 'admin-tasks':
await this.loadAdminTasksPage();
break;
+ case 'sp-sharing':
+ await this.loadSPSharingPage();
+ break;
case 'settings':
await this.loadSettings();
break;
@@ -745,43 +728,35 @@ const Portal = {
const data = await response.json();
if (data.OK && data.BUSINESS) {
- // Normalize all keys to uppercase for consistent access
- // (Lucee serializeJSON casing varies by server config)
- const raw = data.BUSINESS;
- const biz = {};
- Object.keys(raw).forEach(k => { biz[k.toUpperCase()] = raw[k]; });
+ const biz = data.BUSINESS;
this.currentBusiness = biz;
- // Populate form fields
- document.getElementById('settingBusinessName').value = biz.NAME || biz.BUSINESSNAME || '';
- document.getElementById('settingPhone').value = biz.PHONE || biz.BUSINESSPHONE || '';
- document.getElementById('settingTaxRate').value = biz.TAXRATEPERCENT || '';
- document.getElementById('settingAddressLine1').value = biz.LINE1 || biz.ADDRESSLINE1 || '';
- document.getElementById('settingCity').value = biz.CITY || biz.ADDRESSCITY || '';
- document.getElementById('settingState').value = biz.ADDRESSSTATE || '';
- document.getElementById('settingZip').value = biz.ADDRESSZIP || '';
+ // Populate form fields (Lucee serializes all keys as uppercase)
+ document.getElementById('settingName').value = biz.BUSINESSNAME || biz.Name || '';
+ document.getElementById('settingPhone').value = biz.BUSINESSPHONE || biz.Phone || '';
+ document.getElementById('settingTaxRate').value = biz.TAXRATEPERCENT || biz.TaxRatePercent || '';
+ document.getElementById('settingLine1').value = biz.ADDRESSLINE1 || biz.Line1 || '';
+ document.getElementById('settingCity').value = biz.ADDRESSCITY || biz.City || '';
+ document.getElementById('settingState').value = biz.ADDRESSSTATE || biz.AddressState || '';
+ document.getElementById('settingZip').value = biz.ADDRESSZIP || biz.AddressZip || '';
- // Load brand color if set (DB stores without #, CSS needs it)
- const brandColor = biz.BRANDCOLOR || '';
+ // Load brand color if set
+ const brandColor = biz.BRANDCOLOR || biz.BrandColor;
if (brandColor) {
- this.brandColor = brandColor.charAt(0) === '#' ? brandColor : '#' + brandColor;
+ this.brandColor = brandColor;
const swatch = document.getElementById('brandColorSwatch');
- if (swatch) swatch.style.backgroundColor = this.brandColor;
+ if (swatch) swatch.style.background = brandColor;
}
// Load header preview
const headerPreview = document.getElementById('headerPreview');
- const headerWrapper = document.getElementById('headerPreviewWrapper');
- const headerUrl = biz.HEADERIMAGEURL || '';
+ const headerUrl = biz.HEADERIMAGEURL || biz.HeaderImageURL;
if (headerPreview && headerUrl) {
- headerPreview.onload = function() {
- if (headerWrapper) headerWrapper.style.display = 'block';
- };
- headerPreview.src = `${BASE_PATH}${headerUrl}?t=${Date.now()}`;
+ headerPreview.style.backgroundImage = `url(${headerUrl}?t=${Date.now()})`;
}
// Render hours editor
- this.renderHoursEditor(biz.HOURSDETAIL || biz.BUSINESSHOURSDETAIL || []);
+ this.renderHoursEditor(biz.BUSINESSHOURSDETAIL || biz.HoursDetail || []);
}
} catch (err) {
console.error('[Portal] Error loading business info:', err);
@@ -870,10 +845,10 @@ const Portal = {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
BusinessID: this.config.businessId,
- Name: document.getElementById('settingBusinessName').value,
+ Name: document.getElementById('settingName').value,
Phone: document.getElementById('settingPhone').value,
TaxRatePercent: parseFloat(document.getElementById('settingTaxRate').value) || 0,
- Line1: document.getElementById('settingAddressLine1').value,
+ Line1: document.getElementById('settingLine1').value,
City: document.getElementById('settingCity').value,
State: document.getElementById('settingState').value,
Zip: document.getElementById('settingZip').value
@@ -1132,7 +1107,7 @@ const Portal = {
openCustomerPreview() {
const businessId = this.config.businessId;
const businessName = encodeURIComponent(
- this.currentBusiness?.NAME || this.currentBusiness?.BUSINESSNAME || 'Preview'
+ this.currentBusiness?.BUSINESSNAME || this.currentBusiness?.Name || 'Preview'
);
const deepLink = `payfrit://open?businessId=${businessId}&businessName=${businessName}`;
window.location.href = deepLink;
@@ -1192,12 +1167,8 @@ const Portal = {
this.toast('Header uploaded successfully!', 'success');
// Update preview using the URL from the response
const preview = document.getElementById('headerPreview');
- const wrapper = document.getElementById('headerPreviewWrapper');
if (preview && data.HEADERURL) {
- preview.onload = function() {
- if (wrapper) wrapper.style.display = 'block';
- };
- preview.src = `${BASE_PATH}${data.HEADERURL}?t=${Date.now()}`;
+ preview.style.backgroundImage = `url(${BASE_PATH}${data.HEADERURL}?t=${Date.now()})`;
}
} else {
this.toast(data.MESSAGE || 'Failed to upload header', 'error');
@@ -1275,7 +1246,7 @@ const Portal = {
if (data.OK) {
this.brandColor = color;
const swatch = document.getElementById('brandColorSwatch');
- if (swatch) swatch.style.backgroundColor = color;
+ if (swatch) swatch.style.background = color;
this.closeModal();
this.toast('Brand color saved!', 'success');
} else {
@@ -3810,6 +3781,215 @@ const Portal = {
console.error('[Portal] Error submitting rating:', err);
this.toast('Error submitting rating', 'error');
}
+ },
+
+ // ========== SP SHARING ==========
+
+ _spSharingSelectedBizID: 0,
+ _spSharingSearchTimer: null,
+
+ async loadSPSharingPage() {
+ const bizId = this.config.businessId;
+ if (!bizId) return;
+
+ const STATUS_LABELS = { 0: 'Pending', 1: 'Active', 2: 'Declined', 3: 'Revoked' };
+ const STATUS_COLORS = { 0: '#f59e0b', 1: '#10b981', 2: '#ef4444', 3: '#ef4444' };
+
+ // Load owner grants
+ try {
+ const ownerData = await this.api('/api/grants/list.cfm', { BusinessID: bizId, Role: 'owner' });
+ const ownerEl = document.getElementById('ownerGrantsList');
+ if (ownerData.OK && ownerData.Grants.length) {
+ let html = '
| Guest Business | Service Point | Economics | Status | Actions |
';
+ ownerData.Grants.forEach(g => {
+ let econ = g.EconomicsType === 'flat_fee' ? `$${parseFloat(g.EconomicsValue).toFixed(2)}/order` : g.EconomicsType === 'percent_of_orders' ? `${parseFloat(g.EconomicsValue).toFixed(1)}%` : 'None';
+ let actions = '';
+ if (g.StatusID === 0 || g.StatusID === 1) {
+ actions = ``;
+ }
+ html += `| ${this.escapeHtml(g.GuestBusinessName)} (#${g.GuestBusinessID}) | ${this.escapeHtml(g.ServicePointName)} | ${econ} | ${STATUS_LABELS[g.StatusID]} | ${actions} |
`;
+ });
+ html += '
';
+ ownerEl.innerHTML = html;
+ } else {
+ ownerEl.innerHTML = '
No grants created yet. Use "Invite Business" to share your service points.
';
+ }
+ } catch (err) {
+ console.error('[Portal] Error loading owner grants:', err);
+ }
+
+ // Load guest grants
+ try {
+ const guestData = await this.api('/api/grants/list.cfm', { BusinessID: bizId, Role: 'guest' });
+ const guestEl = document.getElementById('guestGrantsList');
+ if (guestData.OK && guestData.Grants.length) {
+ let html = '
| Owner Business | Service Point | Economics | Eligibility | Status | Actions |
';
+ guestData.Grants.forEach(g => {
+ let econ = g.EconomicsType === 'flat_fee' ? `$${parseFloat(g.EconomicsValue).toFixed(2)}/order` : g.EconomicsType === 'percent_of_orders' ? `${parseFloat(g.EconomicsValue).toFixed(1)}%` : 'None';
+ let actions = '';
+ if (g.StatusID === 0) {
+ actions = ` `;
+ }
+ html += `| ${this.escapeHtml(g.OwnerBusinessName)} (#${g.OwnerBusinessID}) | ${this.escapeHtml(g.ServicePointName)} | ${econ} | ${g.EligibilityScope} | ${STATUS_LABELS[g.StatusID]} | ${actions} |
`;
+ });
+ html += '
';
+ guestEl.innerHTML = html;
+ } else {
+ guestEl.innerHTML = '
No invites or active grants from other businesses.
';
+ }
+ } catch (err) {
+ console.error('[Portal] Error loading guest grants:', err);
+ }
+
+ // Pre-load service points for the invite modal
+ try {
+ const spData = await this.api('/api/servicepoints/list.cfm', { BusinessID: bizId });
+ const select = document.getElementById('inviteSPSelect');
+ if (spData.OK && spData.SERVICEPOINTS) {
+ select.innerHTML = spData.SERVICEPOINTS.map(sp => `
`).join('');
+ }
+ } catch (err) {
+ console.error('[Portal] Error loading service points:', err);
+ }
+ },
+
+ showInviteBusinessModal() {
+ this._spSharingSelectedBizID = 0;
+ document.getElementById('inviteBizSearch').value = '';
+ document.getElementById('inviteBizResults').innerHTML = '';
+ document.getElementById('inviteBizSelected').style.display = 'none';
+ document.getElementById('inviteEconType').value = 'none';
+ document.getElementById('inviteEconValue').style.display = 'none';
+ document.getElementById('inviteEconValue').value = '';
+ document.getElementById('inviteEligibility').value = 'public';
+ document.getElementById('inviteTimePolicy').value = 'always';
+ document.getElementById('inviteBusinessModal').style.display = 'flex';
+ },
+
+ closeInviteModal() {
+ document.getElementById('inviteBusinessModal').style.display = 'none';
+ },
+
+ toggleEconValue() {
+ const type = document.getElementById('inviteEconType').value;
+ const valEl = document.getElementById('inviteEconValue');
+ valEl.style.display = (type === 'none') ? 'none' : 'block';
+ if (type === 'flat_fee') valEl.placeholder = 'Dollar amount per order';
+ else if (type === 'percent_of_orders') valEl.placeholder = 'Percentage (e.g., 10)';
+ },
+
+ async searchBusinessForInvite() {
+ clearTimeout(this._spSharingSearchTimer);
+ const query = document.getElementById('inviteBizSearch').value.trim();
+ if (query.length < 2) {
+ document.getElementById('inviteBizResults').innerHTML = '';
+ return;
+ }
+ this._spSharingSearchTimer = setTimeout(async () => {
+ try {
+ const data = await this.api('/api/grants/searchBusiness.cfm', {
+ Query: query,
+ ExcludeBusinessID: this.config.businessId
+ });
+ const resultsEl = document.getElementById('inviteBizResults');
+ if (data.OK && data.Businesses.length) {
+ resultsEl.innerHTML = data.Businesses.map(b =>
+ `
${this.escapeHtml(b.Name)} (#${b.BusinessID})
`
+ ).join('');
+ } else {
+ resultsEl.innerHTML = '
No businesses found
';
+ }
+ } catch (err) {
+ console.error('[Portal] Business search error:', err);
+ }
+ }, 300);
+ },
+
+ selectInviteBiz(bizID, name) {
+ this._spSharingSelectedBizID = bizID;
+ document.getElementById('inviteBizResults').innerHTML = '';
+ document.getElementById('inviteBizSearch').value = '';
+ document.getElementById('inviteBizSelected').style.display = 'block';
+ document.getElementById('inviteBizSelectedName').textContent = `${name} (#${bizID})`;
+ },
+
+ async submitGrantInvite() {
+ if (!this._spSharingSelectedBizID) {
+ this.toast('Please select a business to invite', 'error');
+ return;
+ }
+ const spID = parseInt(document.getElementById('inviteSPSelect').value);
+ if (!spID) {
+ this.toast('Please select a service point', 'error');
+ return;
+ }
+
+ const payload = {
+ OwnerBusinessID: this.config.businessId,
+ GuestBusinessID: this._spSharingSelectedBizID,
+ ServicePointID: spID,
+ EconomicsType: document.getElementById('inviteEconType').value,
+ EconomicsValue: parseFloat(document.getElementById('inviteEconValue').value) || 0,
+ EligibilityScope: document.getElementById('inviteEligibility').value,
+ TimePolicyType: document.getElementById('inviteTimePolicy').value
+ };
+
+ try {
+ const data = await this.api('/api/grants/create.cfm', payload);
+ if (data.OK) {
+ this.toast('Invite sent successfully', 'success');
+ this.closeInviteModal();
+ await this.loadSPSharingPage();
+ } else {
+ this.toast(data.MESSAGE || data.ERROR || 'Error creating grant', 'error');
+ }
+ } catch (err) {
+ this.toast('Error sending invite', 'error');
+ }
+ },
+
+ async revokeGrant(grantID) {
+ if (!confirm('Revoke this grant? All access will stop immediately.')) return;
+ try {
+ const data = await this.api('/api/grants/revoke.cfm', { GrantID: grantID });
+ if (data.OK) {
+ this.toast('Grant revoked', 'success');
+ await this.loadSPSharingPage();
+ } else {
+ this.toast(data.MESSAGE || 'Error revoking grant', 'error');
+ }
+ } catch (err) {
+ this.toast('Error revoking grant', 'error');
+ }
+ },
+
+ async acceptGrant(grantID) {
+ try {
+ const data = await this.api('/api/grants/accept.cfm', { GrantID: grantID });
+ if (data.OK) {
+ this.toast('Grant accepted! Service point access is now active.', 'success');
+ await this.loadSPSharingPage();
+ } else {
+ this.toast(data.MESSAGE || 'Error accepting grant', 'error');
+ }
+ } catch (err) {
+ this.toast('Error accepting grant', 'error');
+ }
+ },
+
+ async declineGrant(grantID) {
+ if (!confirm('Decline this invite?')) return;
+ try {
+ const data = await this.api('/api/grants/decline.cfm', { GrantID: grantID });
+ if (data.OK) {
+ this.toast('Grant declined', 'success');
+ await this.loadSPSharingPage();
+ } else {
+ this.toast(data.MESSAGE || 'Error declining grant', 'error');
+ }
+ } catch (err) {
+ this.toast('Error declining grant', 'error');
+ }
}
};