/* Layout */ .container { max-width: 1200px; margin: 0 auto; padding: 20px; } .dashboard { display: flex; min-height: 100vh; } .sidebar { width: 240px; background: #1a1a1a; color: white; padding: 20px; } .main { flex: 1; background: white; padding: 30px; } /* Navigation */ .logo { font-size: 24px; font-weight: bold; margin-bottom: 30px; } .nav-item { padding: 12px 16px; margin: 8px 0; cursor: pointer; border-radius: 4px; } .nav-item:hover { background: #333; } .nav-item.active { background: #0066cc; } /* Top bar */ .topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #ddd; } .notifications { cursor: pointer; position: relative; font-size: 24px; } .notif-badge { position: absolute; top: -5px; right: -5px; background: red; color: white; border-radius: 50%; width: 18px; height: 18px; font-size: 11px; display: flex; align-items: center; justify-content: center; } .notif-dropdown { position: absolute; top: 40px; right: 0; background: white; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); width: 350px; max-height: 400px; overflow-y: auto; z-index: 1000; } .notif-item { padding: 12px 16px; border-bottom: 1px solid #eee; cursor: pointer; transition: background 0.2s; } .notif-item:hover { background: #f5f5f5; } .notif-item.unread { background: #e8f4fd; } .notif-item.unread:hover { background: #d6ebfa; } .notif-title { font-weight: 600; margin-bottom: 4px; font-size: 14px; } .notif-message { color: #666; font-size: 13px; margin-bottom: 4px; } .notif-time { color: #999; font-size: 12px; } .notif-empty { padding: 40px 20px; text-align: center; color: #999; } /* Forms */ .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input, select, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } textarea { resize: vertical; min-height: 80px; } button { padding: 10px 20px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; } button:hover { background: #0052a3; } button:disabled { background: #ccc; cursor: not-allowed; } button.active { background: #0066cc; } .btn-secondary { background: #666; } .btn-secondary.active { background: #0066cc; } .btn-secondary:hover { background: #555; } .btn-danger { background: #dc3545; } .btn-danger:hover { background: #c82333; } /* Tables */ table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { text-align: left; padding: 12px; border-bottom: 1px solid #ddd; } th { background: #f8f8f8; font-weight: 600; } tr:hover { background: #f9f9f9; } /* Cards */ .card { background: white; border: 1px solid #ddd; border-radius: 4px; padding: 20px; margin-bottom: 20px; } .card-title { font-size: 18px; font-weight: 600; margin-bottom: 10px; } /* Badges */ .badge { display: inline-block; padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; } .badge-urgent { background: #ff4444; color: white; } .badge-future { background: #4CAF50; color: white; } .badge-pending { background: #ff9800; color: white; } .badge-selected { background: #2196F3; color: white; } .badge-confirmed { background: #4CAF50; color: white; } /* Star Rating */ .stars { color: #ffa500; font-size: 14px; } .stars-large { color: #ffa500; font-size: 24px; cursor: pointer; } .star-clickable:hover { opacity: 0.7; } .rating-count { color: #999; font-size: 12px; margin-left: 5px; } /* Utilities */ .hidden { display: none; } .error { color: #dc3545; margin-top: 10px; } .success { color: #28a745; margin-top: 10px; } .text-center { text-align: center; } .mt-20 { margin-top: 20px; } .mb-10 { margin-bottom: 10px; } /* Onboarding Checklist */ .onboarding-checklist { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; } .onboarding-checklist h3 { margin-top: 0; font-size: 18px; } .progress-bar { background: rgba(255,255,255,0.3); height: 8px; border-radius: 4px; margin: 15px 0; overflow: hidden; } .progress-fill { background: #48bb78; height: 100%; transition: width 0.3s ease; border-radius: 4px; } .checklist-item { padding: 8px 0; display: flex; align-items: center; } .checklist-item.completed { opacity: 0.7; } .checklist-icon { margin-right: 10px; font-size: 18px; } /* Login page */ .login-container { display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #0b0f19; } .login-box { background: white; padding: 40px; border-radius: 8px; width: 400px; max-width: 90%; } .login-title { font-size: 28px; margin-bottom: 30px; text-align: center; }
let currentPage = 'dashboard'; // Router function navigate(page) { currentPage = page; render(); } // API calls async function api(endpoint, options = {}) { const headers = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(`${API_URL}${endpoint}`, { ...options, headers: { ...headers, ...options.headers } }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Request failed'); } return response.json(); } // Login async function login(email, password) { try { const data = await api('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }); token = data.token; localStorage.setItem('token', token); await loadCurrentUser(); startNotificationPolling(); navigate('dashboard'); } catch (err) { throw err; } } // Load current user async function loadCurrentUser() { currentUser = await api('/me'); } // Logout function logout() { token = null; currentUser = null; localStorage.removeItem('token'); stopNotificationPolling(); render(); } // Notifications let notificationsOpen = false; let notifications = []; async function fetchNotifications() { try { const data = await api('/notifications?unreadOnly=false'); notifications = data.notifications; updateNotificationBadge(data.unreadCount); if (notificationsOpen) { renderNotificationDropdown(); } } catch (err) { console.error('Failed to fetch notifications:', err); } } function updateNotificationBadge(count) { const badge = document.getElementById('notifBadge'); if (badge) { if (count > 0) { badge.textContent = count > 99 ? '99+' : count; badge.style.display = 'flex'; } else { badge.style.display = 'none'; } } } function toggleNotifications(event) { event.stopPropagation(); notificationsOpen = !notificationsOpen; const dropdown = document.getElementById('notifDropdown'); if (dropdown) { if (notificationsOpen) { dropdown.classList.remove('hidden'); renderNotificationDropdown(); } else { dropdown.classList.add('hidden'); } } } function renderNotificationDropdown() { const dropdown = document.getElementById('notifDropdown'); if (!dropdown) return; if (notifications.length === 0) { dropdown.innerHTML = '
No notifications
'; return; } dropdown.innerHTML = notifications.map(notif => { const isUnread = !notif.isRead; const timeAgo = formatTimeAgo(new Date(notif.createdAt)); return `
${escapeHtml(notif.title)}
${escapeHtml(notif.message)}
${timeAgo}
`; }).join(''); } async function handleNotificationClick(notificationId, requestId) { try { // Mark as read await api(`/notifications/${notificationId}/read`, { method: 'POST' }); // Close dropdown notificationsOpen = false; const dropdown = document.getElementById('notifDropdown'); if (dropdown) dropdown.classList.add('hidden'); // Navigate to request if available if (requestId) { viewRequest(requestId); } // Refresh notifications await fetchNotifications(); } catch (err) { console.error('Failed to mark notification as read:', err); } } function formatTimeAgo(date) { const seconds = Math.floor((new Date() - date) / 1000); if (seconds < 60) return 'just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; return date.toLocaleDateString(); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Star rating helpers function renderStars(rating, totalRatings = 0) { if (!rating) return 'No ratings yet'; const fullStars = Math.floor(rating); const halfStar = rating % 1 >= 0.5; const emptyStars = 5 - fullStars - (halfStar ? 1 : 0); let html = ''; for (let i = 0; i < fullStars; i++) html += '★'; if (halfStar) html += '☆'; for (let i = 0; i < emptyStars; i++) html += '☆'; html += ''; html += ` ${rating.toFixed(1)}`; if (totalRatings > 0) { html += ` (${totalRatings})`; } return html; } function renderStarsClickable(currentRating = 0) { let html = '
'; for (let i = 1; i <= 5; i++) { const filled = i <= currentRating ? '★' : '☆'; html += `${filled}`; } html += '
'; return html; } let currentRatingSelection = 0; let currentRatingRequestId = null; let currentRatingCompanyId = null; function selectRating(rating) { currentRatingSelection = rating; document.getElementById('ratingStarsDisplay').innerHTML = renderStarsClickable(rating); document.getElementById('ratingValue').textContent = rating; } function showRatingModal(requestId, companyId, companyName) { currentRatingRequestId = requestId; currentRatingCompanyId = companyId; currentRatingSelection = 0; const modal = `

Rate ${companyName}

${renderStarsClickable(0)}

Selected: 0 stars

`; const modalDiv = document.createElement('div'); modalDiv.id = 'ratingModal'; modalDiv.innerHTML = modal; document.body.appendChild(modalDiv); } function closeRatingModal() { const modal = document.getElementById('ratingModal'); if (modal) modal.remove(); } async function submitRating() { if (currentRatingSelection === 0) { document.getElementById('ratingError').textContent = 'Please select a rating'; document.getElementById('ratingError').classList.remove('hidden'); return; } try { await api('/ratings', { method: 'POST', body: JSON.stringify({ requestId: currentRatingRequestId, toCompanyId: currentRatingCompanyId, stars: currentRatingSelection, notes: document.getElementById('ratingNotes').value || null }) }); closeRatingModal(); alert('Rating submitted successfully!'); if (currentPage === 'request-detail') { loadRequestDetail(); } } catch (err) { document.getElementById('ratingError').textContent = err.message; document.getElementById('ratingError').classList.remove('hidden'); } } // Poll notifications every 30 seconds let notificationPollInterval = null; function startNotificationPolling() { if (notificationPollInterval) clearInterval(notificationPollInterval); fetchNotifications(); // Initial fetch notificationPollInterval = setInterval(fetchNotifications, 30000); } function stopNotificationPolling() { if (notificationPollInterval) { clearInterval(notificationPollInterval); notificationPollInterval = null; } } // Create company async function createCompany(formData) { await api('/company', { method: 'POST', body: JSON.stringify(formData) }); await loadCurrentUser(); navigate('dashboard'); } // Components function LoginPage() { return `

b2blimo

`; } function OnboardingPage() { return `

Create Your Company

Complete your company profile to start using b2blimo

`; } function Dashboard() { return `

${getPageTitle()}

🔔
`; } function getPageTitle() { const titles = { inbox: 'Inbox', sent: 'Sent Requests', 'new-request': 'New Request', settings: 'Company Settings', notifications: 'Notifications', dashboard: 'Dashboard' }; return titles[currentPage] || 'Dashboard'; } // Get onboarding progress function getOnboardingProgress() { if (!currentUser) return { items: [], percent: 0 }; const items = [ { id: 'email', label: 'Verify your email', completed: !!currentUser.emailVerifiedAt }, { id: 'company', label: 'Create your company', completed: !!currentUser.company }, { id: 'service-area', label: 'Add service area', completed: currentUser.company?.serviceAreas?.length > 0 }, { id: 'airport', label: 'Add airport coverage', completed: currentUser.company?.airports?.length > 0 }, { id: 'request', label: 'Send your first request', completed: false // Will be updated after loading requests }, { id: 'offer', label: 'Receive your first offer', completed: false // Will be updated after loading requests } ]; const completed = items.filter(i => i.completed).length; const percent = Math.round((completed / items.length) * 100); return { items, percent, completed, total: items.length }; } // Render onboarding checklist function renderOnboardingChecklist(hasCreatedRequest = false, hasReceivedOffer = false) { const progress = getOnboardingProgress(); // Update dynamic items const requestItem = progress.items.find(i => i.id === 'request'); const offerItem = progress.items.find(i => i.id === 'offer'); if (requestItem) requestItem.completed = hasCreatedRequest; if (offerItem) offerItem.completed = hasReceivedOffer; // Recalculate percentage const completed = progress.items.filter(i => i.completed).length; const percent = Math.round((completed / progress.items.length) * 100); // Don't show if 100% complete if (percent === 100) return ''; return `

🚀 Getting Started

${completed} of ${progress.items.length} steps complete (${percent}%)

${progress.items.map(item => `
${item.completed ? '✅' : '⬜'} ${item.label}
`).join('')}
`; } // Inbox page function InboxPage() { return `
Loading...
`; } async function loadInbox() { try { const data = await api('/requests?filter=received'); // Check if user has received any offers const hasReceivedOffer = data.requests.some(req => req.offers && req.offers.length > 0); // Render checklist const checklistHtml = renderOnboardingChecklist(false, hasReceivedOffer); const checklistEl = document.getElementById('onboardingChecklist'); if (checklistEl) checklistEl.innerHTML = checklistHtml; const html = ` ${data.requests.map(req => ` `).join('')}
Type Route Pickup Date Status Action
${req.type} ${req.pickupLocation || req.pickupAirportCode} → ${req.dropoffLocation || req.dropoffAirportCode} ${new Date(req.pickupDateTime).toLocaleString()} ${req.status}
`; document.getElementById('inboxContent').innerHTML = html; } catch (err) { document.getElementById('inboxContent').innerHTML = `
${err.message}
`; } } // Sent requests page function SentPage() { return `
Loading...
`; } async function loadSent() { try { const data = await api('/requests?filter=created'); // Check if user has created any requests and received offers const hasCreatedRequest = data.requests.length > 0; const hasReceivedOffer = data.requests.some(req => req.offers && req.offers.length > 0); // Render checklist const checklistHtml = renderOnboardingChecklist(hasCreatedRequest, hasReceivedOffer); const checklistEl = document.getElementById('onboardingChecklistSent'); if (checklistEl) checklistEl.innerHTML = checklistHtml; const html = ` ${data.requests.map(req => ` `).join('')}
Type Destination Date Offers Status Action
${req.type} ${req.dropoffLocation || req.dropoffAirportCode} ${new Date(req.pickupDateTime).toLocaleString()} ${req.offers?.length || 0} ${req.status}
`; document.getElementById('sentContent').innerHTML = html; } catch (err) { document.getElementById('sentContent').innerHTML = `
${err.message}
`; } } // View request detail function viewRequest(id) { currentPage = 'request-detail'; currentRequestId = id; render(); } let currentRequestId = null; function RequestDetailPage() { return `
Loading...
`; } async function loadRequestDetail() { try { const req = await api(`/requests/${currentRequestId}`); const isCreator = req.requestingCompany.id === currentUser.companyId; // Load ratings for companies with offers const companyRatings = {}; for (const offer of req.offers) { try { const ratingData = await api(`/company/${offer.company.id}/rating`); companyRatings[offer.company.id] = ratingData; } catch (err) { console.error('Failed to load rating for company', offer.company.id, err); } } // Check if user has already rated let userRatings = []; if (req.status === 'CONFIRMED') { try { const ratingsData = await api(`/requests/${currentRequestId}/ratings`); userRatings = ratingsData.ratings; } catch (err) { console.error('Failed to load user ratings', err); } } const partnerCompanyId = isCreator ? req.selectedOffer?.company.id : req.requestingCompany.id; const partnerCompanyName = isCreator ? req.selectedOffer?.company.name : req.requestingCompany.name; const hasRated = userRatings.some(r => r.fromCompany === currentUser.company.name); let html = `

Request Details

Type: ${req.type}

Status: ${req.status}

From: ${req.pickupLocation || req.pickupAirportCode}

To: ${req.dropoffLocation || req.dropoffAirportCode}

Pickup: ${new Date(req.pickupDateTime).toLocaleString()}

Passengers: ${req.passengerCount}

Luggage: ${req.luggageCount}

Vehicle Type: ${req.requestedFleetType}

${req.specialRequirements ? `

Special Requirements: ${req.specialRequirements}

` : ''}

Offers (${req.offers.length})

${req.offers.length === 0 ? '

No offers yet

' : ` ${isCreator && req.status === 'PENDING' ? '' : ''} ${req.offers.map(offer => { const rating = companyRatings[offer.company.id]; return ` ${isCreator && req.status === 'PENDING' && !offer.isExpired ? ` ` : ''} `}).join('')}
Company Rating Action Price Notes StatusSelect
${offer.company.name} ${renderStars(rating?.averageRating, rating?.totalRatings)} ${offer.action} $${offer.price?.toFixed(2) || 'N/A'} ${offer.notes || '-'} ${offer.isSelected ? '✅ Selected' : offer.isExpired ? '❌ Expired' : '⏳ Active'}
`}
${!isCreator && req.status === 'PENDING' ? `

Submit Offer

` : ''} ${isCreator && req.status === 'SELECTED' && req.selectedOffer ? `

Confirm Request

You selected ${req.selectedOffer.company.name} for $${req.selectedOffer.price?.toFixed(2)}

` : ''} ${req.status === 'CONFIRMED' && req.selectedOffer ? `

✅ Confirmed

Affiliate Company: ${req.selectedOffer.company.name}

Dispatch Contact: ${req.selectedOffer.company.dispatchContactName}

Dispatch Phone: ${req.selectedOffer.company.dispatchContactPhone}

${!hasRated && partnerCompanyId ? `

Rate Your Experience

Help build trust in the network by rating ${partnerCompanyName}

` : hasRated ? '

✅ You have rated this request

' : ''}
` : ''} `; document.getElementById('requestDetail').innerHTML = html; // Attach offer form handler const offerForm = document.getElementById('offerForm'); if (offerForm) { offerForm.onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); try { await api(`/requests/${currentRequestId}/offers`, { method: 'POST', body: JSON.stringify({ action: formData.get('action'), price: parseFloat(formData.get('price')) || null, notes: formData.get('notes') }) }); loadRequestDetail(); } catch (err) { alert(err.message); } }; } } catch (err) { document.getElementById('requestDetail').innerHTML = `
${err.message}
`; } } async function selectOffer(requestId, offerId) { try { await api(`/requests/${requestId}/select-offer`, { method: 'POST', body: JSON.stringify({ offerId }) }); loadRequestDetail(); } catch (err) { alert(err.message); } } async function confirmRequest(requestId) { try { await api(`/requests/${requestId}/confirm`, { method: 'POST' }); loadRequestDetail(); } catch (err) { alert(err.message); } } // New request page function NewRequestPage() { return `

Only companies you've added to your trusted list will receive this request

`; } // Settings page function SettingsPage() { if (!currentUser || !currentUser.company) return '

Loading...

'; return `
`; } let currentSettingsTab = 'company'; let trustedCompanies = []; let blockedCompanies = []; async function loadCompanyTab() { try { const ratingData = await api(`/company/${currentUser.companyId}/rating`); const html = `

Company Information

Name: ${currentUser.company.name}

Status: ${currentUser.company.status}

Rating: ${renderStars(ratingData.averageRating, ratingData.totalRatings)}

Dispatch Phone: ${currentUser.company.dispatchPhone}

HQ: ${currentUser.company.hqCity}, ${currentUser.company.hqState}, ${currentUser.company.hqCountry}

Notification Emails: ${currentUser.company.notificationEmails.join(', ')}

`; document.getElementById('companyTab').innerHTML = html; } catch (err) { document.getElementById('companyTab').innerHTML = `
${err.message}
`; } } function showSettingsTab(tab) { currentSettingsTab = tab; document.getElementById('tabCompany')?.classList.remove('active'); document.getElementById('tabUser')?.classList.remove('active'); document.getElementById('tabNetwork')?.classList.remove('active'); document.getElementById(`tab${tab.charAt(0).toUpperCase() + tab.slice(1)}`)?.classList.add('active'); const content = document.getElementById('settingsContent'); if (!content) return; if (tab === 'company') { content.innerHTML = '
Loading...
'; loadCompanyTab(); } else if (tab === 'user') { content.innerHTML = `

User Information

Name: ${currentUser.firstName} ${currentUser.lastName}

Email: ${currentUser.email}

Email Verified: ${currentUser.emailVerifiedAt ? '✅ Yes' : '❌ No'}

Phone Verified: ${currentUser.phoneVerifiedAt ? '✅ Yes' : '❌ No'}

`; } else if (tab === 'network') { content.innerHTML = '
Loading...
'; loadNetworkTab(); } } async function loadCompanyTab() { try { const ratingData = await api(`/company/${currentUser.companyId}/rating`); const html = `

Company Information

Name: ${currentUser.company.name}

Status: ${currentUser.company.status}

Dispatch Phone: ${currentUser.company.dispatchPhone}

HQ: ${currentUser.company.hqCity}, ${currentUser.company.hqState}, ${currentUser.company.hqCountry}

Notification Emails: ${currentUser.company.notificationEmails.join(', ')}

Trust Rating

${renderStars(ratingData.averageRating, ratingData.totalRatings)}
${ratingData.totalRatings > 0 ? `

Based on ${ratingData.totalRatings} completed ${ratingData.totalRatings === 1 ? 'request' : 'requests'}

` : '

No ratings yet

'}
`; document.getElementById('companyTab').innerHTML = html; } catch (err) { document.getElementById('companyTab').innerHTML = `
${err.message}
`; } } async function loadNetworkTab() { try { const [trustedData, blockedData] = await Promise.all([ api('/company/network/trusted'), api('/company/network/blocked') ]); trustedCompanies = trustedData.trusted; blockedCompanies = blockedData.blocked; renderNetworkTab(); } catch (err) { document.getElementById('networkTab').innerHTML = `
${err.message}
`; } } function renderNetworkTab() { const html = `

Trusted Companies

Requests sent to "network only" will go only to these companies.

${trustedCompanies.length === 0 ? '

No trusted companies yet

' : ` ${trustedCompanies.map(c => ` `).join('')}
CompanyLocationAction
${c.name} ${c.hqCity || ''}, ${c.hqState || ''}, ${c.hqCountry || ''}
`}

Blocked Companies

Blocked companies cannot see your requests and you won't see theirs.

${blockedCompanies.length === 0 ? '

No blocked companies

' : ` ${blockedCompanies.map(c => ` `).join('')}
CompanyLocationAction
${c.name} ${c.hqCity || ''}, ${c.hqState || ''}, ${c.hqCountry || ''}
`}
`; document.getElementById('networkTab').innerHTML = html; } async function searchCompaniesToTrust() { const query = document.getElementById('searchTrusted').value; if (query.length < 2) { document.getElementById('searchResults').innerHTML = '

Enter at least 2 characters

'; return; } try { const data = await api(`/companies/search?query=${encodeURIComponent(query)}`); const alreadyTrusted = new Set(trustedCompanies.map(c => c.id)); const alreadyBlocked = new Set(blockedCompanies.map(c => c.id)); const html = data.companies.length === 0 ? '

No companies found

' : ` ${data.companies.map(c => ` `).join('')}
CompanyLocationAction
${c.name} ${c.hqCity || ''}, ${c.hqState || ''}, ${c.hqCountry || ''} ${alreadyTrusted.has(c.id) ? '✓ Trusted' : alreadyBlocked.has(c.id) ? 'Blocked' : ` `}
`; document.getElementById('searchResults').innerHTML = html; } catch (err) { document.getElementById('searchResults').innerHTML = `
${err.message}
`; } } async function addTrust(companyId, companyName) { if (!confirm(`Add ${companyName} to trusted companies?`)) return; try { await api(`/company/network/trust/${companyId}`, { method: 'POST' }); await loadNetworkTab(); } catch (err) { alert(err.message); } } async function removeTrust(companyId) { if (!confirm('Remove from trusted companies?')) return; try { await api(`/company/network/trust/${companyId}`, { method: 'DELETE' }); await loadNetworkTab(); } catch (err) { alert(err.message); } } async function addBlock(companyId, companyName) { if (!confirm(`Block ${companyName}? They will not see your requests and you won't see theirs.`)) return; try { await api(`/company/network/block/${companyId}`, { method: 'POST' }); await loadNetworkTab(); } catch (err) { alert(err.message); } } async function removeBlock(companyId) { if (!confirm('Unblock this company?')) return; try { await api(`/company/network/block/${companyId}`, { method: 'DELETE' }); await loadNetworkTab(); } catch (err) { alert(err.message); } } // Main render async function render() { const app = document.getElementById('app'); // Check auth if (!token) { app.innerHTML = LoginPage(); attachLoginHandler(); return; } // Load user if not loaded if (!currentUser) { try { await loadCurrentUser(); } catch (err) { logout(); return; } } // Check if needs onboarding if (!currentUser.companyId) { app.innerHTML = OnboardingPage(); attachOnboardingHandler(); return; } // Render dashboard app.innerHTML = Dashboard(); // Render page content const pageContent = document.getElementById('pageContent'); if (currentPage === 'inbox') { pageContent.innerHTML = InboxPage(); } else if (currentPage === 'sent') { pageContent.innerHTML = SentPage(); } else if (currentPage === 'new-request') { pageContent.innerHTML = NewRequestPage(); attachNewRequestHandler(); } else if (currentPage === 'settings') { pageContent.innerHTML = SettingsPage(); } else if (currentPage === 'request-detail') { pageContent.innerHTML = RequestDetailPage(); } else { pageContent.innerHTML = '

Welcome to b2blimo!

Use the menu to navigate.

'; } // Load notifications and start polling if (!notificationPollInterval) { startNotificationPolling(); } } // Notifications let notificationsOpen = false; let notifications = []; let notificationPollInterval = null; async function fetchNotifications() { try { const data = await api('/notifications?unreadOnly=false'); notifications = data.notifications; updateNotificationBadge(data.unreadCount); if (notificationsOpen) { renderNotificationDropdown(); } } catch (err) { console.error('Failed to fetch notifications:', err); } } function updateNotificationBadge(count) { const badge = document.getElementById('notifBadge'); if (badge) { if (count > 0) { badge.textContent = count > 99 ? '99+' : count; badge.style.display = 'flex'; } else { badge.style.display = 'none'; } } } function toggleNotifications(event) { event.stopPropagation(); notificationsOpen = !notificationsOpen; const dropdown = document.getElementById('notifDropdown'); if (dropdown) { if (notificationsOpen) { dropdown.classList.remove('hidden'); renderNotificationDropdown(); } else { dropdown.classList.add('hidden'); } } } function renderNotificationDropdown() { const dropdown = document.getElementById('notifDropdown'); if (!dropdown) return; if (notifications.length === 0) { dropdown.innerHTML = '
No notifications
'; return; } dropdown.innerHTML = notifications.map(notif => { const isUnread = !notif.isRead; const timeAgo = formatTimeAgo(new Date(notif.createdAt)); return `
${escapeHtml(notif.title)}
${escapeHtml(notif.message)}
${timeAgo}
`; }).join(''); } async function handleNotificationClick(notificationId, requestId) { try { // Mark as read await api(`/notifications/${notificationId}/read`, { method: 'POST' }); // Close dropdown notificationsOpen = false; const dropdown = document.getElementById('notifDropdown'); if (dropdown) dropdown.classList.add('hidden'); // Navigate to request if available if (requestId) { viewRequest(requestId); } // Refresh notifications await fetchNotifications(); } catch (err) { console.error('Failed to mark notification as read:', err); } } function formatTimeAgo(date) { const seconds = Math.floor((new Date() - date) / 1000); if (seconds < 60) return 'just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; return date.toLocaleDateString(); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Poll notifications every 30 seconds function startNotificationPolling() { if (notificationPollInterval) clearInterval(notificationPollInterval); fetchNotifications(); // Initial fetch notificationPollInterval = setInterval(fetchNotifications, 30000); } function stopNotificationPolling() { if (notificationPollInterval) { clearInterval(notificationPollInterval); notificationPollInterval = null; } } async function loadNotificationCount() { // Deprecated - now using fetchNotifications await fetchNotifications(); } function attachLoginHandler() { document.getElementById('loginForm').onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const errorDiv = document.getElementById('loginError'); try { await login(formData.get('email'), formData.get('password')); } catch (err) { errorDiv.textContent = err.message; errorDiv.classList.remove('hidden'); } }; } function attachOnboardingHandler() { document.getElementById('onboardingForm').onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const errorDiv = document.getElementById('onboardingError'); const emails = formData.get('notificationEmails'); const notificationEmails = emails ? emails.split(',').map(e => e.trim()) : []; try { await createCompany({ name: formData.get('name'), dispatchPhone: formData.get('dispatchPhone'), website: formData.get('website') || null, hqCountry: formData.get('hqCountry'), hqState: formData.get('hqState'), hqCity: formData.get('hqCity'), notificationEmails }); } catch (err) { errorDiv.textContent = err.message; errorDiv.classList.remove('hidden'); } }; } function attachNewRequestHandler() { document.getElementById('newRequestForm').onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const errorDiv = document.getElementById('requestError'); try { await api('/requests', { method: 'POST', body: JSON.stringify({ type: formData.get('type'), pickupAirportCode: formData.get('pickupAirportCode'), pickupLocation: formData.get('pickupLocation'), pickupDateTime: new Date(formData.get('pickupDateTime')).toISOString(), dropoffLocation: formData.get('dropoffLocation'), dropoffAirportCode: formData.get('dropoffAirportCode') || null, passengerCount: parseInt(formData.get('passengerCount')), luggageCount: parseInt(formData.get('luggageCount')), requestedFleetType: formData.get('requestedFleetType'), specialRequirements: formData.get('specialRequirements') || null, sendToNetworkOnly: formData.get('sendToNetworkOnly') === 'on' }) }); navigate('sent'); } catch (err) { errorDiv.textContent = err.message; errorDiv.classList.remove('hidden'); } }; } // Initial render render(); // Close dropdown when clicking outside document.addEventListener('click', function(event) { if (notificationsOpen) { const dropdown = document.getElementById('notifDropdown'); if (dropdown && !dropdown.contains(event.target)) { notificationsOpen = false; dropdown.classList.add('hidden'); } } });