/** * 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, but allow URL override const urlParams = new URLSearchParams(window.location.search); this.config.businessId = parseInt(urlParams.get('bid')) || parseInt(savedBusiness); 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 '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 business info await this.loadBusinessInfo(); // Check Stripe status await this.checkStripeStatus(); }, // 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; document.getElementById('settingBusinessName').value = biz.BusinessName || ''; document.getElementById('settingDescription').value = biz.BusinessDescription || ''; document.getElementById('settingAddress').value = biz.BusinessAddress || ''; document.getElementById('settingPhone').value = biz.BusinessPhone || ''; document.getElementById('settingEmail').value = biz.BusinessEmail || ''; } } catch (err) { console.error('[Portal] Error loading business info:', err); } }, // 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 = 'Invite Team Member'; document.getElementById('modalBody').innerHTML = `
`; this.showModal(); document.getElementById('inviteForm').addEventListener('submit', (e) => { e.preventDefault(); const email = document.getElementById('inviteEmail').value; this.toast(`Invitation sent to ${email}`, 'success'); this.closeModal(); }); } }; // Initialize on load document.addEventListener('DOMContentLoaded', () => { Portal.init(); });