/** * Payfrit Business Portal * Modern admin interface for business management */ // Detect base path for API calls (handles local dev at /biz.payfrit.com/) const BASE_PATH = (() => { const path = window.location.pathname; const portalIndex = path.indexOf('/portal/'); if (portalIndex > 0) { return path.substring(0, portalIndex); } return ''; })(); const Portal = { // Configuration config: { apiBaseUrl: BASE_PATH + '/api', businessId: null, userId: null, }, // State currentPage: 'dashboard', businessData: null, menuData: null, // Initialize async init() { console.log('[Portal] Initializing...'); // Check auth await this.checkAuth(); // Setup navigation this.setupNavigation(); // Setup sidebar toggle this.setupSidebarToggle(); // Load initial data await this.loadDashboard(); // Handle hash navigation this.handleHashChange(); window.addEventListener('hashchange', () => this.handleHashChange()); console.log('[Portal] Ready'); }, // Check authentication async checkAuth() { // Check if user is logged in const token = localStorage.getItem('payfrit_portal_token'); const savedBusiness = localStorage.getItem('payfrit_portal_business'); const userId = localStorage.getItem('payfrit_portal_userid'); if (!token || !savedBusiness) { // Not logged in - redirect to login localStorage.removeItem('payfrit_portal_token'); localStorage.removeItem('payfrit_portal_userid'); localStorage.removeItem('payfrit_portal_business'); window.location.href = BASE_PATH + '/portal/login.html'; return; } // Use saved business ID from localStorage this.config.businessId = parseInt(savedBusiness) || null; this.config.userId = parseInt(userId) || 1; this.config.token = token; // Verify user has access to this business try { const response = await fetch(`${this.config.apiBaseUrl}/portal/myBusinesses.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': token }, body: JSON.stringify({ UserID: this.config.userId }) }); const data = await response.json(); if (data.OK && data.BUSINESSES) { const hasAccess = data.BUSINESSES.some(b => b.BusinessID === this.config.businessId); if (!hasAccess && data.BUSINESSES.length > 0) { // User doesn't have access to requested business, use their first business this.config.businessId = data.BUSINESSES[0].BusinessID; localStorage.setItem('payfrit_portal_business', this.config.businessId); } else if (!hasAccess) { // User has no businesses this.toast('No businesses associated with your account', 'error'); this.logout(); return; } } } catch (err) { console.error('[Portal] Auth verification error:', err); } // Fetch actual business info try { const response = await fetch(`${this.config.apiBaseUrl}/businesses/get.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK && data.BUSINESS) { const biz = data.BUSINESS; this.businessData = biz; // Store for later use document.getElementById('businessName').textContent = biz.BusinessName || 'Business'; document.getElementById('businessAvatar').textContent = (biz.BusinessName || '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 = '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 = 'U'; } }, // Logout logout() { localStorage.removeItem('payfrit_portal_token'); localStorage.removeItem('payfrit_portal_userid'); localStorage.removeItem('payfrit_portal_business'); window.location.href = BASE_PATH + '/portal/login.html'; }, // Setup navigation setupNavigation() { document.querySelectorAll('.nav-item[data-page]').forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const page = item.dataset.page; if (page === 'logout') { this.logout(); } else { this.navigate(page); } }); }); }, // Setup sidebar toggle setupSidebarToggle() { const toggle = document.getElementById('sidebarToggle'); const sidebar = document.getElementById('sidebar'); toggle.addEventListener('click', () => { sidebar.classList.toggle('collapsed'); }); }, // Handle hash change handleHashChange() { const hash = window.location.hash.slice(1) || 'dashboard'; this.navigate(hash); }, // Navigate to page navigate(page) { console.log('[Portal] Navigating to:', page); // Update active nav item document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); if (item.dataset.page === page) { item.classList.add('active'); } }); // Show page document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); const pageEl = document.getElementById(`page-${page}`); if (pageEl) { pageEl.classList.add('active'); } // Update title const titles = { dashboard: 'Dashboard', orders: 'Orders', menu: 'Menu Management', reports: 'Reports', team: 'Team', settings: 'Settings' }; document.getElementById('pageTitle').textContent = titles[page] || page; // Update URL window.location.hash = page; // Load page data this.loadPageData(page); this.currentPage = page; }, // Load page data async loadPageData(page) { switch (page) { case 'dashboard': await this.loadDashboard(); break; case 'orders': await this.loadOrders(); break; case 'menu': await this.loadMenu(); break; case 'reports': await this.loadReports(); break; case 'team': await this.loadTeam(); break; case 'beacons': await this.loadBeaconsPage(); break; case 'settings': await this.loadSettings(); break; } }, // Load dashboard async loadDashboard() { console.log('[Portal] Loading dashboard...'); try { // Load stats const stats = await this.fetchStats(); document.getElementById('statOrdersToday').textContent = stats.ordersToday || 0; document.getElementById('statRevenueToday').textContent = '$' + (stats.revenueToday || 0).toFixed(2); document.getElementById('statPendingOrders').textContent = stats.pendingOrders || 0; document.getElementById('statMenuItems').textContent = stats.menuItems || 0; // Load recent orders const orders = await this.fetchRecentOrders(); this.renderRecentOrders(orders); } catch (err) { console.error('[Portal] Dashboard error:', err); // Show demo data document.getElementById('statOrdersToday').textContent = '12'; document.getElementById('statRevenueToday').textContent = '$342.50'; document.getElementById('statPendingOrders').textContent = '3'; document.getElementById('statMenuItems').textContent = '24'; } }, // Fetch stats async fetchStats() { const response = await fetch(`${this.config.apiBaseUrl}/portal/stats.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK) return data.STATS; throw new Error(data.ERROR); }, // Fetch recent orders async fetchRecentOrders() { const response = await fetch(`${this.config.apiBaseUrl}/orders/listForKDS.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK) return data.ORDERS || []; throw new Error(data.ERROR); }, // Render recent orders renderRecentOrders(orders) { const container = document.getElementById('recentOrdersList'); if (!orders || orders.length === 0) { container.innerHTML = '
No recent orders
'; return; } container.innerHTML = orders.slice(0, 5).map(order => ` `).join(''); }, // Get status class for orders getStatusClass(statusId) { const classes = { 0: 'pending', // Employee: Pending 1: 'submitted', // Order: Submitted, Employee: Invited 2: 'active', // Order: Preparing, Employee: Active 3: 'suspended', // Order: Ready, Employee: Suspended 4: 'completed' // Order: Completed }; return classes[statusId] || 'pending'; }, // Get status text getStatusText(statusId) { const texts = { 1: 'Submitted', 2: 'Preparing', 3: 'Ready', 4: 'Completed' }; return texts[statusId] || 'Unknown'; }, // Load orders async loadOrders() { console.log('[Portal] Loading orders...'); try { const response = await fetch(`${this.config.apiBaseUrl}/orders/listForKDS.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK) { this.renderOrdersTable(data.ORDERS || []); } } catch (err) { console.error('[Portal] Orders error:', err); } }, // Render orders table renderOrdersTable(orders) { const tbody = document.getElementById('ordersTableBody'); if (!orders || orders.length === 0) { tbody.innerHTML = 'No orders found'; return; } tbody.innerHTML = orders.map(order => ` #${order.OrderID} ${order.UserFirstName || 'Guest'} ${order.UserLastName || ''} ${order.LineItems?.length || 0} items $${(order.OrderTotal || 0).toFixed(2)} ${this.getStatusText(order.OrderStatusID)} ${this.formatTime(order.OrderSubmittedOn)} `).join(''); }, // Format time formatTime(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }, // Format phone number formatPhone(phone) { if (!phone) return '-'; // Remove all non-digits const digits = phone.replace(/\D/g, ''); if (digits.length === 10) { return `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`; } if (digits.length === 11 && digits[0] === '1') { return `(${digits.slice(1,4)}) ${digits.slice(4,7)}-${digits.slice(7)}`; } return phone; }, // Load menu - show menu builder options async loadMenu() { console.log('[Portal] Loading menu...'); // Show links to menu tools const container = document.getElementById('menuGrid'); container.innerHTML = `
`; }, // Render menu renderMenu() { const container = document.getElementById('menuGrid'); if (!this.menuData || this.menuData.length === 0) { container.innerHTML = '
No menu items. Click "Add Category" to get started.
'; return; } // Group by category const categories = {}; this.menuData.forEach(item => { if (item.ItemParentItemID === 0) { // This is a top-level item, find its category const catId = item.ItemCategoryID || 0; if (!categories[catId]) { categories[catId] = { name: item.CategoryName || 'Uncategorized', items: [] }; } categories[catId].items.push(item); } }); container.innerHTML = Object.entries(categories).map(([catId, cat]) => ` `).join(''); }, // Load reports async loadReports() { console.log('[Portal] Loading reports...'); // TODO: Implement reports loading }, // Load team async loadTeam() { console.log('[Portal] Loading team for business:', this.config.businessId); const tbody = document.getElementById('teamTableBody'); tbody.innerHTML = 'Loading team...'; // Load hiring toggle state from business data this.loadHiringToggle(); try { const url = `${this.config.apiBaseUrl}/portal/team.cfm`; console.log('[Portal] Fetching team from:', url); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); console.log('[Portal] Team response:', data); if (data.OK && data.TEAM) { if (data.TEAM.length === 0) { tbody.innerHTML = 'No team members yet'; return; } tbody.innerHTML = data.TEAM.map(member => `
${(member.FirstName || '?').charAt(0)}${(member.LastName || '').charAt(0)} ${member.Name || 'Unknown'}
${member.Email || '-'} ${this.formatPhone(member.Phone)} ${member.StatusName || 'Unknown'} `).join(''); } else { tbody.innerHTML = 'Failed to load team'; } } catch (err) { console.error('[Portal] Error loading team:', err); tbody.innerHTML = 'Error loading team'; } }, // Load hiring toggle from business data loadHiringToggle() { const toggle = document.getElementById('hiringToggle'); if (!toggle) return; // Remove existing listener if any toggle.onchange = null; // Set initial state from business data if (this.businessData && typeof this.businessData.IsHiring !== 'undefined') { toggle.checked = this.businessData.IsHiring; console.log('[Portal] Hiring toggle set to:', this.businessData.IsHiring); } else { console.log('[Portal] No business data for hiring toggle, fetching...'); // Fetch fresh business data if not available this.refreshBusinessData(); } // Wire up the toggle toggle.onchange = () => this.toggleHiring(toggle.checked); }, // Refresh business data async refreshBusinessData() { try { const response = await fetch(`${this.config.apiBaseUrl}/businesses/get.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK && data.BUSINESS) { this.businessData = data.BUSINESS; const toggle = document.getElementById('hiringToggle'); if (toggle) { toggle.checked = this.businessData.IsHiring; console.log('[Portal] Hiring toggle refreshed to:', this.businessData.IsHiring); } } } catch (err) { console.error('[Portal] Error refreshing business data:', err); } }, // Toggle hiring status async toggleHiring(isHiring) { console.log('[Portal] Setting hiring to:', isHiring); try { const response = await fetch(`${this.config.apiBaseUrl}/businesses/setHiring.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId, IsHiring: isHiring }) }); const data = await response.json(); console.log('[Portal] setHiring response:', data); if (data.OK) { // Update local state if (this.businessData) { this.businessData.IsHiring = data.IsHiring; } this.toast(isHiring ? 'Now accepting applications' : 'Applications disabled', 'success'); } else { // Revert toggle const toggle = document.getElementById('hiringToggle'); if (toggle) toggle.checked = !isHiring; this.toast(data.ERROR || 'Failed to update hiring status', 'error'); } } catch (err) { console.error('[Portal] Error toggling hiring:', err); // Revert toggle const toggle = document.getElementById('hiringToggle'); if (toggle) toggle.checked = !isHiring; this.toast('Error updating hiring status', 'error'); } }, // Edit team member (placeholder) editTeamMember(employeeId) { this.showToast('Team member editing coming soon', 'info'); }, // Load settings async loadSettings() { console.log('[Portal] Loading settings...'); // Load states dropdown first await this.loadStatesDropdown(); // Load business info await this.loadBusinessInfo(); // Check Stripe status await this.checkStripeStatus(); }, // Load states for dropdown async loadStatesDropdown() { try { const response = await fetch(`${this.config.apiBaseUrl}/addresses/states.cfm`); const data = await response.json(); if (data.OK && data.STATES) { const select = document.getElementById('settingState'); select.innerHTML = ''; data.STATES.forEach(state => { select.innerHTML += ``; }); } } catch (err) { console.error('[Portal] Error loading states:', err); } }, // Load business info for settings async loadBusinessInfo() { try { const response = await fetch(`${this.config.apiBaseUrl}/businesses/get.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK && data.BUSINESS) { const biz = data.BUSINESS; this.currentBusiness = biz; // Populate form fields document.getElementById('settingBusinessName').value = biz.BusinessName || ''; document.getElementById('settingPhone').value = biz.BusinessPhone || ''; document.getElementById('settingAddressLine1').value = biz.AddressLine1 || ''; document.getElementById('settingCity').value = biz.AddressCity || ''; document.getElementById('settingState').value = biz.AddressState || ''; document.getElementById('settingZip').value = biz.AddressZip || ''; // Render hours editor this.renderHoursEditor(biz.BusinessHoursDetail || []); } } catch (err) { console.error('[Portal] Error loading business info:', err); } }, // Render hours editor renderHoursEditor(hours) { const container = document.getElementById('hoursEditor'); const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; const dayIds = [1, 2, 3, 4, 5, 6, 7]; // Monday=1 through Sunday=7 // Create a map of existing hours by day ID const hoursMap = {}; hours.forEach(h => { hoursMap[h.dayId] = h; }); let html = '
'; dayIds.forEach((dayId, idx) => { const dayName = dayNames[idx]; const existing = hoursMap[dayId]; const isClosed = !existing; const openTime = existing ? this.formatTimeFor24Input(existing.open) : '09:00'; const closeTime = existing ? this.formatTimeFor24Input(existing.close) : '17:00'; html += `
`; }); html += '
'; container.innerHTML = html; }, // Helper to convert "7:00 AM" format to "07:00" for time input formatTimeFor24Input(timeStr) { if (!timeStr) return '09:00'; // If already in 24h format (HH:MM), return as-is if (/^\d{2}:\d{2}$/.test(timeStr)) return timeStr; // Parse "7:00 AM" or "7:30 PM" format const match = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i); if (match) { let hours = parseInt(match[1]); const minutes = match[2]; const ampm = match[3].toUpperCase(); if (ampm === 'PM' && hours < 12) hours += 12; if (ampm === 'AM' && hours === 12) hours = 0; return `${String(hours).padStart(2, '0')}:${minutes}`; } return timeStr; }, // Toggle hours day closed/open toggleHoursDay(dayId) { const isClosed = document.getElementById(`hours_closed_${dayId}`).checked; document.getElementById(`hours_open_${dayId}`).disabled = isClosed; document.getElementById(`hours_close_${dayId}`).disabled = isClosed; }, // Save business info async saveBusinessInfo(event) { event.preventDefault(); const btn = document.getElementById('saveBusinessBtn'); const originalText = btn.textContent; btn.textContent = 'Saving...'; btn.disabled = true; try { const response = await fetch(`${this.config.apiBaseUrl}/businesses/update.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId, BusinessName: document.getElementById('settingBusinessName').value, BusinessPhone: document.getElementById('settingPhone').value, AddressLine1: document.getElementById('settingAddressLine1').value, City: document.getElementById('settingCity').value, State: document.getElementById('settingState').value, Zip: document.getElementById('settingZip').value }) }); const data = await response.json(); if (data.OK) { this.showToast('Business info saved!', 'success'); // Reload to refresh sidebar etc await this.loadBusinessInfo(); } else { this.showToast(data.ERROR || 'Failed to save', 'error'); } } catch (err) { console.error('[Portal] Error saving business info:', err); this.showToast('Error saving business info', 'error'); } finally { btn.textContent = originalText; btn.disabled = false; } }, // Save hours async saveHours() { const dayIds = [1, 2, 3, 4, 5, 6, 7]; const hours = []; dayIds.forEach(dayId => { const isClosed = document.getElementById(`hours_closed_${dayId}`).checked; if (!isClosed) { hours.push({ dayId: dayId, open: document.getElementById(`hours_open_${dayId}`).value, close: document.getElementById(`hours_close_${dayId}`).value }); } }); try { const response = await fetch(`${this.config.apiBaseUrl}/businesses/updateHours.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId, Hours: hours }) }); const data = await response.json(); if (data.OK) { this.showToast('Hours saved!', 'success'); } else { this.showToast(data.ERROR || 'Failed to save hours', 'error'); } } catch (err) { console.error('[Portal] Error saving hours:', err); this.showToast('Error saving hours', 'error'); } }, // Check Stripe Connect status async checkStripeStatus() { const statusContainer = document.getElementById('stripeStatus'); try { const response = await fetch(`${this.config.apiBaseUrl}/stripe/status.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK) { if (data.CONNECTED) { // Stripe is connected and active statusContainer.innerHTML = `
Stripe Connected

Your account is active and ready to accept payments

View Dashboard `; } else if (data.ACCOUNT_STATUS === 'pending_verification') { // Waiting for Stripe verification statusContainer.innerHTML = `
Verification Pending

Stripe is reviewing your account. This usually takes 1-2 business days.

`; } else if (data.ACCOUNT_STATUS === 'incomplete') { // Started but not finished onboarding statusContainer.innerHTML = `
Setup Incomplete

Please complete your Stripe account setup to accept payments

`; } else { // Not started this.renderStripeNotConnected(); } } else { this.renderStripeNotConnected(); } } catch (err) { console.error('[Portal] Stripe status error:', err); this.renderStripeNotConnected(); } }, // Render Stripe not connected state renderStripeNotConnected() { const statusContainer = document.getElementById('stripeStatus'); statusContainer.innerHTML = `
Stripe Not Connected

Connect your Stripe account to accept payments

`; }, // Refresh Stripe status async refreshStripeStatus() { this.toast('Checking status...', 'info'); await this.checkStripeStatus(); }, // Show add category modal showAddCategoryModal() { document.getElementById('modalTitle').textContent = 'Add Category'; document.getElementById('modalBody').innerHTML = `
`; this.showModal(); document.getElementById('addCategoryForm').addEventListener('submit', (e) => { e.preventDefault(); this.addCategory(); }); }, // Show add item modal showAddItemModal() { document.getElementById('modalTitle').textContent = 'Add Menu Item'; document.getElementById('modalBody').innerHTML = `
`; this.showModal(); document.getElementById('addItemForm').addEventListener('submit', (e) => { e.preventDefault(); this.addItem(); }); }, // Show modal showModal() { document.getElementById('modalOverlay').classList.add('visible'); }, // Close modal closeModal() { document.getElementById('modalOverlay').classList.remove('visible'); }, // Show toast toast(message, type = 'info') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); }, // Connect Stripe - initiate onboarding async connectStripe() { this.toast('Starting Stripe setup...', 'info'); try { const response = await fetch(`${this.config.apiBaseUrl}/stripe/onboard.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK && data.ONBOARDING_URL) { // Redirect to Stripe onboarding window.location.href = data.ONBOARDING_URL; } else { this.toast(data.ERROR || 'Failed to start Stripe setup', 'error'); } } catch (err) { console.error('[Portal] Stripe connect error:', err); this.toast('Error connecting to Stripe', 'error'); } }, // View order async viewOrder(orderId) { this.toast('Loading order...', 'info'); try { const response = await fetch(`${this.config.apiBaseUrl}/orders/getDetail.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ OrderID: orderId }) }); const data = await response.json(); if (data.OK && data.ORDER) { this.showOrderDetailModal(data.ORDER); } else { this.toast(data.ERROR || 'Failed to load order', 'error'); } } catch (err) { console.error('[Portal] Error loading order:', err); this.toast('Error loading order details', 'error'); } }, // Show order detail modal showOrderDetailModal(order) { document.getElementById('modalTitle').textContent = `Order #${order.OrderID}`; const customerName = [order.Customer.FirstName, order.Customer.LastName].filter(Boolean).join(' ') || 'Guest'; const servicePoint = order.ServicePoint.Name || 'Not assigned'; // Build line items HTML const lineItemsHtml = order.LineItems.map(item => { const modifiersHtml = item.Modifiers && item.Modifiers.length > 0 ? `
${item.Modifiers.map(mod => `
+ ${mod.ItemName} ${mod.UnitPrice > 0 ? `+$${mod.UnitPrice.toFixed(2)}` : ''}
`).join('')}
` : ''; const remarksHtml = item.Remarks ? `
"${item.Remarks}"
` : ''; return `
${item.Quantity}x ${item.ItemName} $${(item.UnitPrice * item.Quantity).toFixed(2)}
${modifiersHtml} ${remarksHtml}
`; }).join(''); document.getElementById('modalBody').innerHTML = `
Status
${order.StatusText}
Customer
${customerName}
${order.Customer.Phone ? `
${order.Customer.Phone}
` : ''} ${order.Customer.Email ? `
${order.Customer.Email}
` : ''}
Service Point
${servicePoint}
${order.ServicePoint.Type ? `
${order.ServicePoint.Type}
` : ''}
Items
${lineItemsHtml || '
No items
'}
${order.Notes ? `
Notes
${order.Notes}
` : ''}
Subtotal $${order.Subtotal.toFixed(2)}
Tax $${order.Tax.toFixed(2)}
${order.Tip > 0 ? `
Tip $${order.Tip.toFixed(2)}
` : ''}
Total $${order.Total.toFixed(2)}
`; this.showModal(); }, // Format date time formatDateTime(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }, // Edit item editItem(itemId) { this.toast(`Editing item #${itemId}`, 'info'); // TODO: Implement item editing }, // Delete item deleteItem(itemId) { if (confirm('Are you sure you want to delete this item?')) { this.toast(`Deleting item #${itemId}`, 'info'); // TODO: Implement item deletion } }, // Edit category editCategory(catId) { this.toast(`Editing category #${catId}`, 'info'); // TODO: Implement category editing }, // Add category async addCategory() { const name = document.getElementById('categoryName').value; // TODO: Implement API call this.toast(`Category "${name}" added!`, 'success'); this.closeModal(); this.loadMenu(); }, // Add item async addItem() { const name = document.getElementById('itemName').value; // TODO: Implement API call this.toast(`Item "${name}" added!`, 'success'); this.closeModal(); this.loadMenu(); }, // Show invite modal showInviteModal() { document.getElementById('modalTitle').textContent = 'Add Team Member'; document.getElementById('modalBody').innerHTML = `
Enter the person's phone number or email address
`; this.showModal(); let foundUserId = null; document.getElementById('inviteForm').addEventListener('submit', async (e) => { e.preventDefault(); const contact = document.getElementById('inviteContact').value.trim(); const btn = document.getElementById('inviteSubmitBtn'); const resultsDiv = document.getElementById('userSearchResults'); if (foundUserId) { // Actually add the team member btn.disabled = true; btn.textContent = 'Adding...'; try { const response = await fetch(`${this.config.apiBaseUrl}/portal/addTeamMember.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId, UserID: foundUserId }) }); const data = await response.json(); if (data.OK) { this.toast('Team member added!', 'success'); this.closeModal(); this.loadTeamPage(); } else { this.toast(data.MESSAGE || 'Failed to add', 'error'); } } catch (err) { this.toast('Error adding team member', 'error'); } btn.disabled = false; btn.textContent = 'Add Team Member'; return; } // Search for user btn.disabled = true; btn.textContent = 'Searching...'; try { const response = await fetch(`${this.config.apiBaseUrl}/portal/searchUser.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Query: contact, BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK && data.USER) { foundUserId = data.USER.UserID; resultsDiv.style.display = 'block'; resultsDiv.innerHTML = `
Found: ${data.USER.Name}
${data.USER.Phone || data.USER.Email || ''}
`; btn.textContent = 'Add Team Member'; } else { resultsDiv.style.display = 'block'; resultsDiv.innerHTML = `
Not found
No user with that phone/email. They need to create an account first.
`; btn.textContent = 'Search & Add'; } } catch (err) { resultsDiv.style.display = 'block'; resultsDiv.innerHTML = `
Search failed: ${err.message}
`; } btn.disabled = false; }); }, // ========== BEACONS PAGE ========== beacons: [], servicePoints: [], assignments: [], // Load beacons page data async loadBeaconsPage() { await Promise.all([ this.loadBeacons(), this.loadServicePoints(), this.loadAssignments() ]); }, // Load beacons list async loadBeacons() { const container = document.getElementById('beaconsList'); try { const response = await fetch(`${this.config.apiBaseUrl}/beacons/list.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify({ onlyActive: true }) }); const data = await response.json(); if (data.OK) { this.beacons = data.BEACONS || []; this.renderBeaconsList(); } else { console.error('[Portal] Beacons API error:', data.ERROR); container.innerHTML = `
Error: ${data.ERROR || 'Unknown error'}
`; } } catch (err) { console.error('[Portal] Error loading beacons:', err); container.innerHTML = `
Failed to load beacons
`; } }, // Render beacons list renderBeaconsList() { const container = document.getElementById('beaconsList'); if (this.beacons.length === 0) { container.innerHTML = '
No beacons yet. Add your first beacon!
'; return; } container.innerHTML = this.beacons.map(b => `
${this.escapeHtml(b.BeaconName)}
${b.UUID || b.NamespaceId || 'No UUID'}
`).join(''); }, // Load service points list async loadServicePoints() { try { const response = await fetch(`${this.config.apiBaseUrl}/servicepoints/list.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK) { this.servicePoints = data.SERVICEPOINTS || data.ServicePoints || []; this.renderServicePointsList(); } } catch (err) { console.error('[Portal] Error loading service points:', err); } }, // Render service points list renderServicePointsList() { const container = document.getElementById('servicePointsList'); if (this.servicePoints.length === 0) { container.innerHTML = '
No service points yet. Add tables, counters, etc.
'; return; } container.innerHTML = this.servicePoints.map(sp => `
${this.escapeHtml(sp.ServicePointName)}
`).join(''); }, // Load assignments list async loadAssignments() { const container = document.getElementById('assignmentsList'); try { const response = await fetch(`${this.config.apiBaseUrl}/assignments/list.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify({}) }); const data = await response.json(); if (data.OK) { this.assignments = data.ASSIGNMENTS || []; this.renderAssignmentsList(); } else { console.error('[Portal] Assignments API error:', data.ERROR); container.innerHTML = `
Error: ${data.ERROR || 'Unknown error'}
`; } } catch (err) { console.error('[Portal] Error loading assignments:', err); container.innerHTML = `
Failed to load assignments
`; } }, // Render assignments list renderAssignmentsList() { const container = document.getElementById('assignmentsList'); if (this.assignments.length === 0) { container.innerHTML = '
No assignments yet. Link beacons to service points.
'; return; } container.innerHTML = this.assignments.map(a => `
${this.escapeHtml(a.BeaconName)} → ${this.escapeHtml(a.ServicePointName)}
${a.lt_Beacon_Businesses_ServicePointNotes || ''}
`).join(''); }, // Show beacon modal (add/edit) showBeaconModal(beaconId = null) { const isEdit = beaconId !== null; const beacon = isEdit ? this.beacons.find(b => b.BeaconID === beaconId) : {}; document.getElementById('modalTitle').textContent = isEdit ? 'Edit Beacon' : 'Add Beacon'; document.getElementById('modalBody').innerHTML = `
`; this.showModal(); document.getElementById('beaconForm').addEventListener('submit', (e) => { e.preventDefault(); this.saveBeacon(); }); }, // Save beacon async saveBeacon() { const beaconId = document.getElementById('beaconId').value; const payload = { BeaconName: document.getElementById('beaconName').value, UUID: document.getElementById('beaconUUID').value, IsActive: document.getElementById('beaconIsActive').checked }; if (beaconId) { payload.BeaconID = parseInt(beaconId); } try { const response = await fetch(`${this.config.apiBaseUrl}/beacons/save.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify(payload) }); const data = await response.json(); if (data.OK) { this.toast('Beacon saved!', 'success'); this.closeModal(); await this.loadBeacons(); } else { this.toast(data.ERROR || 'Failed to save beacon', 'error'); } } catch (err) { console.error('[Portal] Error saving beacon:', err); this.toast('Error saving beacon', 'error'); } }, // Edit beacon editBeacon(beaconId) { this.showBeaconModal(beaconId); }, // Delete beacon async deleteBeacon(beaconId) { if (!confirm('Are you sure you want to deactivate this beacon?')) return; try { const response = await fetch(`${this.config.apiBaseUrl}/beacons/delete.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify({ BeaconID: beaconId }) }); const data = await response.json(); if (data.OK) { this.toast('Beacon deactivated', 'success'); await this.loadBeacons(); } else { this.toast(data.ERROR || 'Failed to delete beacon', 'error'); } } catch (err) { console.error('[Portal] Error deleting beacon:', err); this.toast('Error deleting beacon', 'error'); } }, // Show service point modal (add/edit) showServicePointModal(servicePointId = null) { const isEdit = servicePointId !== null; const sp = isEdit ? this.servicePoints.find(s => s.ServicePointID === servicePointId) : {}; document.getElementById('modalTitle').textContent = isEdit ? 'Edit Service Point' : 'Add Service Point'; document.getElementById('modalBody').innerHTML = `
`; this.showModal(); document.getElementById('servicePointForm').addEventListener('submit', (e) => { e.preventDefault(); this.saveServicePoint(); }); }, // Save service point async saveServicePoint() { const spId = document.getElementById('servicePointId').value; const payload = { ServicePointName: document.getElementById('servicePointName').value, ServicePointCode: document.getElementById('servicePointCode').value }; if (spId) { payload.ServicePointID = parseInt(spId); } try { const response = await fetch(`${this.config.apiBaseUrl}/servicepoints/save.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify(payload) }); const data = await response.json(); if (data.OK) { this.toast('Service point saved!', 'success'); this.closeModal(); await this.loadServicePoints(); } else { this.toast(data.ERROR || 'Failed to save service point', 'error'); } } catch (err) { console.error('[Portal] Error saving service point:', err); this.toast('Error saving service point', 'error'); } }, // Edit service point editServicePoint(servicePointId) { this.showServicePointModal(servicePointId); }, // Delete service point async deleteServicePoint(servicePointId) { if (!confirm('Are you sure you want to deactivate this service point?')) return; try { const response = await fetch(`${this.config.apiBaseUrl}/servicepoints/delete.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify({ ServicePointID: servicePointId }) }); const data = await response.json(); if (data.OK) { this.toast('Service point deactivated', 'success'); await this.loadServicePoints(); } else { this.toast(data.ERROR || 'Failed to delete service point', 'error'); } } catch (err) { console.error('[Portal] Error deleting service point:', err); this.toast('Error deleting service point', 'error'); } }, // Show assignment modal showAssignmentModal() { // Filter out beacons and service points that are already assigned const assignedBeaconIds = new Set(this.assignments.map(a => a.BeaconID)); const assignedSPIds = new Set(this.assignments.map(a => a.ServicePointID)); const availableBeacons = this.beacons.filter(b => b.IsActive && !assignedBeaconIds.has(b.BeaconID)); const availableSPs = this.servicePoints.filter(sp => !assignedSPIds.has(sp.ServicePointID)); if (availableBeacons.length === 0) { this.toast('No unassigned beacons available', 'warning'); return; } if (availableSPs.length === 0) { this.toast('No unassigned service points available', 'warning'); return; } document.getElementById('modalTitle').textContent = 'Assign Beacon to Service Point'; document.getElementById('modalBody').innerHTML = `
`; this.showModal(); document.getElementById('assignmentForm').addEventListener('submit', (e) => { e.preventDefault(); this.saveAssignment(); }); }, // Save assignment async saveAssignment() { const payload = { BeaconID: parseInt(document.getElementById('assignBeaconId').value), ServicePointID: parseInt(document.getElementById('assignServicePointId').value), Notes: document.getElementById('assignNotes').value }; try { const response = await fetch(`${this.config.apiBaseUrl}/assignments/save.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify(payload) }); const data = await response.json(); if (data.OK) { this.toast('Assignment created!', 'success'); this.closeModal(); await this.loadAssignments(); } else { this.toast(data.ERROR || 'Failed to create assignment', 'error'); } } catch (err) { console.error('[Portal] Error saving assignment:', err); this.toast('Error creating assignment', 'error'); } }, // Delete assignment async deleteAssignment(assignmentId) { if (!confirm('Are you sure you want to remove this assignment?')) return; try { const response = await fetch(`${this.config.apiBaseUrl}/assignments/delete.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-Token': this.config.token, 'X-Business-ID': this.config.businessId }, body: JSON.stringify({ lt_Beacon_Businesses_ServicePointID: assignmentId }) }); const data = await response.json(); if (data.OK) { this.toast('Assignment removed', 'success'); await this.loadAssignments(); } else { this.toast(data.ERROR || 'Failed to remove assignment', 'error'); } } catch (err) { console.error('[Portal] Error deleting assignment:', err); this.toast('Error removing assignment', 'error'); } }, // Escape HTML helper escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); } }; // Initialize on load document.addEventListener('DOMContentLoaded', () => { Portal.init(); });