Dashboard
Live data from your Supabase database
☀️ Light
Active Loads
In system
Total Revenue
All loads
Gross Profit
Revenue minus carrier pay
GP %
Overall margin
Carriers
In network
Customers
Active accounts
Recent Loads
Loading loads…
Alerts
📊 Customer Profitability
Loading…
Loading dispatch board…
All Loads
Loading…
Carriers
Loading…
Customers
Loading…
Paid
Pending
Overdue
Invoice Register
Loading…
🎯 Pipeline
🏢 Companies
✅ Tasks
📧 Templates
📊 Reports
Companies
Active
Pipeline Value
Weighted Forecast
Follow-ups Today
Won This Month
Total Users
Active
Admins
Last Login
Team Members
Loading users…
🏢 Company Profile
🎨 Branding
🔐 Permissions
💳 Subscription
Company Profile
🔌
Integration Hub
Connect FreightFlowHQ to the tools your operation already uses. Each integration is configured with your own API key — your data stays yours.
✅ Available Now
📧
Resend
Email delivery
Coming Soon
Send rate confirmations, invoices, and notifications directly from FreightFlowHQ. Free up to 3,000 emails/month.
Free · resend.com
📱
Twilio
SMS check calls
Coming Soon
Automated driver check call texts. Drivers reply naturally and FreightFlowHQ logs the update automatically.
~$0.0075/SMS · twilio.com
💳
Stripe
Customer payments
Coming Soon
Let customers pay invoices directly in the portal via card or ACH bank transfer. 2.9% + 30¢ per transaction.
2.9% + $0.30/txn · stripe.com
🗺️ On the Roadmap
📊
DAT RateView
Live market rates
V1.1
Pull real-time spot rates by lane and equipment type while building a load. See low, average, and high market rates instantly.
~$150/mo · dat.com
💰
QuickBooks
Accounting sync
V1.1
Two-way sync between FreightFlowHQ invoices and QuickBooks. Eliminates double entry. Payments sync both ways automatically.
Free API · quickbooks.com
🎯
Apollo.io
CRM enrichment
V1.1
Enrich CRM company records with verified contacts, emails, phone numbers, and firmographic data. One click to populate a full company profile.
From $49/mo · apollo.io
🚛
Truckstop
Load board posting
V1.1
Post available loads directly to Truckstop load board from within FreightFlowHQ. Carrier responses come back into your dispatch board.
Subscription required · truckstop.com
📍
MacroPoint
GPS load tracking
V1.2
Real-time GPS tracking using driver's phone or ELD. Live truck position shows in FreightFlowHQ and the customer portal automatically.
$3–8/load · macropoint.com
📋
RMIS
Carrier onboarding
V1.2
Digital carrier packets, W9 collection, insurance verification, and authority monitoring. New carriers onboard themselves in minutes.
$50–150/mo · rmissolutions.com
🤖
Greenscreens.ai
AI rate optimization
V1.2
AI-powered rate recommendations factoring in market trends, seasonality, and your own historical lane data. Price every load optimally.
$200–500/mo · greenscreens.ai
🔗
Project44
Advanced tracking
V1.3
Enterprise-grade load visibility connecting directly to ELDs and carrier systems. Most accurate tracking available in the market.
Enterprise pricing · project44.com
📦
Xero
Accounting sync
V1.3
QuickBooks alternative. Two-way invoice and payment sync for brokers using Xero for accounting.
Free API · xero.com
💡
Don't see an integration you need?
We prioritize integrations based on customer demand. Tell us what tools you use and we'll add it to the roadmap.
Total Loads
Total Revenue
Gross Profit
GP %
Avg Load Value
Delivered
Revenue by Customer
Loads by Status
Pending
Awaiting pricing
Quoted
Waiting on customer
Approved
Ready to convert
Win Rate
Approved / Total
Welcome back
Sign in to your FreightFlow account
FreightFlow
Customer Portal
Sign In
Access your shipments, invoices, and quotes.
Email
Password
Welcome
Your live shipment tracker
Active Loads
Delivered
Open Invoices
Amount Due
Your Loads
Invoices & Payments
📋 Request a Quote
No quote requests yet
// ════════════════════════════════════════════════ let allQuoteRequests = []; let activeQuotesTab = 'pending'; let portalSession = null; // ── LOAD QUOTE REQUESTS ─────────────────────── async function loadQuoteRequests() { const { data, error } = await db.from('quote_requests') .select('*, customers(name, assigned_to)') .order('created_at', { ascending: false }); if (!error) allQuoteRequests = data || []; updateQuotesBadge(); } function updateQuotesBadge() { const badge = document.getElementById('quotes-sidebar-badge'); if (!badge) return; const pending = allQuoteRequests.filter(q => q.status === 'Pending').length; badge.textContent = pending; badge.style.display = pending > 0 ? 'flex' : 'none'; } // ── QUOTES PAGE ─────────────────────────────── function switchQuoteTab(tab) { activeQuotesTab = tab; ['pending','all'].forEach(t => { const btn = document.getElementById('qtab-' + t); if (btn) { btn.style.background = t === tab ? 'var(--accent)' : 'transparent'; btn.style.color = t === tab ? 'white' : 'var(--text2)'; } }); renderQuotesPage(); } function renderQuotesPage() { const pending = allQuoteRequests.filter(q => q.status === 'Pending'); const quoted = allQuoteRequests.filter(q => q.status === 'Quoted'); const approved = allQuoteRequests.filter(q => q.status === 'Approved'); const total = allQuoteRequests.length; const winRate = total > 0 ? ((approved.length / total) * 100).toFixed(0) + '%' : '—'; const setEl = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; setEl('qkpi-pending', pending.length); setEl('qkpi-quoted', quoted.length); setEl('qkpi-approved', approved.length); setEl('qkpi-winrate', winRate); let toShow = activeQuotesTab === 'pending' ? allQuoteRequests.filter(q => q.status === 'Pending') : allQuoteRequests; // Broker visibility filter if (currentUserRole !== 'admin') { toShow = toShow.filter(q => { const customer = allCustomers.find(c => c.id === q.customer_id); return !customer?.assigned_to || customer.assigned_to === currentUser?.id; }); } const container = document.getElementById('quotes-list-container'); if (!container) return; if (toShow.length === 0) { container.innerHTML = `
📋

${activeQuotesTab === 'pending' ? 'No pending quote requests.' : 'No quote requests yet.'}

`; return; } container.innerHTML = toShow.map(q => renderQuoteCard(q)).join(''); } function renderQuoteCard(q) { const customer = allCustomers.find(c => c.id === q.customer_id); const statusColors = { 'Pending':'badge-amber','Quoted':'badge-blue','Approved':'badge-green','Customer Declined':'badge-red','Declined':'badge-gray' }; const isPending = q.status === 'Pending'; const isQuoted = q.status === 'Quoted'; return `
${q.origin_city||'?'}, ${q.origin_state||''} → ${q.destination_city||'?'}, ${q.destination_state||''}
${customer?.name||'Unknown'} · ${q.equipment_type||'—'} · Pickup: ${fmtDate(q.pickup_date)}${q.weight?' · '+Number(q.weight).toLocaleString()+' lbs':''}
${q.commodity?`
📦 ${q.commodity}
`:''} ${q.notes?`
"${q.notes}"
`:''}
${q.status}
Submitted ${fmtDate(q.submitted_at||q.created_at)}
${isPending ? `
💰 Price This Quote
` : ''} ${isQuoted ? `
Quoted Rate
${fmt$(q.quoted_rate)}
${q.broker_note?`
${q.broker_note}
`:''}
Waiting on customer response
Sent ${fmtDate(q.quoted_at)}
` : ''} ${q.status === 'Approved' ? `
✅ Customer Accepted
Rate: ${fmt$(q.quoted_rate)} · Carrier Pay: ${fmt$(q.quoted_carrier_pay)}
` : ''} ${q.status === 'Customer Declined' ? `
❌ Customer declined${q.declined_reason?': '+q.declined_reason:'.'}
` : ''}
`; } function calcQuoteMarginInline(quoteId) { const rate = Number(document.getElementById('qprice-rate-' + quoteId)?.value) || 0; const pay = Number(document.getElementById('qprice-pay-' + quoteId)?.value) || 0; const el = document.getElementById('qmargin-' + quoteId); if (!el) return; if (rate > 0 && pay > 0) { const gp = rate - pay, pct = gp / rate * 100; const color = pct >= 18 ? 'var(--green)' : pct >= 12 ? 'var(--amber)' : 'var(--red)'; el.innerHTML = `Margin: ${fmt$(gp)} (${pct.toFixed(1)}%)`; } else { el.innerHTML = ''; } } function showDeclineForm(quoteId) { const f = document.getElementById('decline-form-' + quoteId); if (f) f.style.display = f.style.display === 'none' ? 'block' : 'none'; } async function sendPricedQuote(quoteId) { const rate = document.getElementById('qprice-rate-' + quoteId)?.value; const pay = document.getElementById('qprice-pay-' + quoteId)?.value; const note = document.getElementById('qprice-note-' + quoteId)?.value; if (!rate) { toast('Please enter a customer rate', 'error'); return; } const { error } = await db.from('quote_requests').update({ status: 'Quoted', quoted_rate: parseFloat(rate), quoted_carrier_pay: pay ? parseFloat(pay) : null, broker_note: note || null, quoted_at: new Date().toISOString(), }).eq('id', quoteId); if (error) { toast('Error: ' + error.message, 'error'); return; } toast('Quote sent to customer!', 'success'); await loadQuoteRequests(); renderQuotesPage(); renderDashboardQuoteQueue(); } async function declineQuote(quoteId) { const reason = document.getElementById('decline-reason-' + quoteId)?.value?.trim(); const { error } = await db.from('quote_requests').update({ status: 'Declined', declined_reason: reason||null }).eq('id', quoteId); if (error) { toast('Error: ' + error.message, 'error'); return; } toast('Quote declined', 'info'); await loadQuoteRequests(); renderQuotesPage(); renderDashboardQuoteQueue(); } async function convertQuoteToLoad(quoteId) { const q = allQuoteRequests.find(x => x.id === quoteId); if (!q) return; const sv = (id, val) => { const el=document.getElementById(id); if(el) el.value=val||''; }; document.getElementById('load-id').value = ''; document.getElementById('load-modal-title').textContent = 'New Load from Quote'; sv('load-origin-city', q.origin_city); sv('load-origin-state', q.origin_state); sv('load-dest-city', q.destination_city); sv('load-dest-state', q.destination_state); sv('load-equipment', q.equipment_type); sv('load-commodity', q.commodity); sv('load-weight', q.weight); sv('load-pickup', q.pickup_date); sv('load-customer', q.customer_id); sv('load-rate', q.quoted_rate); sv('load-carrier-pay', q.quoted_carrier_pay); sv('load-status', 'Available'); sv('load-notes', 'Converted from Quote ' + q.id + (q.notes?'\nCustomer notes: '+q.notes:'')); switchPage('loads'); setTimeout(() => openModal('load'), 200); toast('Pre-filled from quote — review and save', 'info'); } // ── DASHBOARD QUOTE QUEUE ───────────────────── function renderDashboardQuoteQueue() { let pending = allQuoteRequests.filter(q => q.status === 'Pending'); if (currentUserRole !== 'admin') { pending = pending.filter(q => { const c = allCustomers.find(x => x.id === q.customer_id); return !c?.assigned_to || c.assigned_to === currentUser?.id; }); } const el = document.getElementById('dash-quote-queue'); if (!el) return; if (pending.length === 0) { el.style.display = 'none'; return; } el.style.display = 'block'; el.innerHTML = `
📋 Pending Quote Requests ${pending.length}
${pending.slice(0,3).map(q => { const c = allCustomers.find(x => x.id === q.customer_id); return `
${q.origin_city||'?'}, ${q.origin_state||''} → ${q.destination_city||'?'}, ${q.destination_state||''}
${c?.name||'Unknown'} · ${q.equipment_type||'—'} · ${fmtDate(q.pickup_date)}
`; }).join('')} ${pending.length > 3 ? `
+${pending.length-3} more pending
` : ''}
`; } // ── PORTAL AUTH ─────────────────────────────── function showPortalLogin() { const el = document.getElementById('portal-login-overlay'); if (el) el.style.display = 'flex'; } function hidePortalLogin() { const el = document.getElementById('portal-login-overlay'); if (el) el.style.display = 'none'; } async function portalSignIn() { const email = document.getElementById('portal-login-email')?.value?.trim(); const password = document.getElementById('portal-login-password')?.value; const errorEl = document.getElementById('portal-login-error'); const btn = document.getElementById('portal-login-btn'); if (!email || !password) { errorEl.textContent='Please enter your email and password.'; errorEl.style.display='block'; return; } if (btn) { btn.disabled=true; btn.textContent='Signing in…'; } errorEl.style.display = 'none'; const { data, error } = await db.auth.signInWithPassword({ email, password }); if (error) { errorEl.textContent = 'Incorrect email or password.'; errorEl.style.display='block'; if (btn) { btn.disabled=false; btn.textContent='Sign In →'; } return; } const { data: customer } = await db.from('customers').select('*').eq('portal_user_id', data.user.id).single(); if (!customer) { errorEl.textContent = 'No customer portal account found for this email.'; errorEl.style.display='block'; if (btn) { btn.disabled=false; btn.textContent='Sign In →'; } await db.auth.signOut(); return; } portalSession = { user: data.user, customer }; hidePortalLogin(); openCustomerPortal(customer.id); } async function portalForgotPassword() { const email = document.getElementById('portal-login-email')?.value?.trim(); if (!email) { alert('Please enter your email address first.'); return; } const { error } = await db.auth.resetPasswordForEmail(email); if (error) { alert('Error: ' + error.message); return; } alert('Reset link sent to ' + email + '. Check your inbox.'); } // ── PORTAL QUOTE RESPONSE ───────────────────── async function customerRespondToQuote(quoteId, response) { const newStatus = response === 'accept' ? 'Approved' : 'Customer Declined'; const { error } = await db.from('quote_requests').update({ status: newStatus, customer_response: response, customer_responded_at: new Date().toISOString(), }).eq('id', quoteId); if (error) { alert('Error: ' + error.message); return; } await loadQuoteRequests(); renderPortalQuotes(); renderDashboardQuoteQueue(); if (response === 'accept') alert('Quote accepted! Your broker will be in touch shortly.'); } // ── PORTAL CITY AUTOCOMPLETE FIX ───────────── // US_CITIES is array of arrays ['City','ST'] — this replaces the broken version function portalCityAutoFixed(inputId, stateId, dropId, val) { const drop = document.getElementById(dropId); if (!drop) return; if (!val || val.length < 2) { drop.innerHTML=''; drop.classList.remove('visible'); return; } const matches = US_CITIES .filter(([city]) => city.toLowerCase().startsWith(val.toLowerCase())) .slice(0, 8); if (!matches.length) { drop.innerHTML=''; drop.classList.remove('visible'); return; } drop.innerHTML = matches.map(([city, state]) => `
${city}, ${state}
` ).join(''); drop.classList.add('visible'); } // ── SUBMIT QUOTE FROM PORTAL ────────────────── async function submitQuoteRequest() { const originCity = document.getElementById('quote-origin-city')?.value?.trim(); const originState = document.getElementById('quote-origin-state')?.value?.trim(); const destCity = document.getElementById('quote-dest-city')?.value?.trim(); const destState = document.getElementById('quote-dest-state')?.value?.trim(); if (!originCity || !destCity) { alert('Please enter origin and destination cities.'); return; } let customerId = portalSession?.customer?.id || allCustomers[0]?.id || null; const payload = { customer_id: customerId, origin_city: originCity, origin_state: originState?.toUpperCase(), destination_city: destCity, destination_state: destState?.toUpperCase(), equipment_type: document.getElementById('quote-equipment')?.value, pickup_date: document.getElementById('quote-pickup')?.value || null, commodity: document.getElementById('quote-commodity')?.value || null, weight: document.getElementById('quote-weight')?.value || null, notes: document.getElementById('quote-notes')?.value || null, status: 'Pending', submitted_at: new Date().toISOString(), }; const { error } = await db.from('quote_requests').insert(payload); if (error) { // Demo fallback if (!window.portalQuotes) window.portalQuotes = []; window.portalQuotes.unshift({ ...payload, id: 'Q-' + Date.now() }); } else { await loadQuoteRequests(); renderDashboardQuoteQueue(); } togglePortalQuoteForm(); ['quote-origin-city','quote-origin-state','quote-dest-city','quote-dest-state', 'quote-commodity','quote-weight','quote-notes'].forEach(id => { const el=document.getElementById(id); if(el) el.value=''; }); toast('Quote request submitted!', 'success'); renderPortalQuotes(); } // ── INVITE CUSTOMER TO PORTAL ───────────────── async function inviteCustomerToPortal(customerId) { if (!customerId) return; const customer = allCustomers.find(c => c.id === customerId); if (!customer) { toast('Customer not found', 'error'); return; } if (!customer.email) { toast('Customer has no email on file', 'error'); return; } if (!confirm(`Send portal invite to ${customer.name} (${customer.email})?`)) return; try { const { error } = await db.auth.admin.inviteUserByEmail(customer.email, { data: { role: 'customer', customer_name: customer.name }, redirectTo: window.location.origin + '/?portal=true', }); if (error) { toast('Use Supabase Dashboard → Auth → Users → Invite User to send the invite manually', 'info'); return; } toast('Portal invite sent to ' + customer.email + '!', 'success'); await loadData(); } catch(e) { toast('Use Supabase Dashboard to invite: ' + e.message, 'info'); } } // ── WIRE UP EXISTING renderDashboard ───────── const _ffOrigRenderDashboard = renderDashboard; renderDashboard = function() { _ffOrigRenderDashboard(); setTimeout(renderDashboardQuoteQueue, 150); }; // ── WIRE UP loadData ────────────────────────── const _ffOrigLoadData = loadData; loadData = async function() { await _ffOrigLoadData(); await loadQuoteRequests(); }; // ── WIRE UP renderCurrentPage ───────────────── const _ffOrigRenderCurrentPage = renderCurrentPage; renderCurrentPage = function() { if (currentPage === 'quotes') { renderQuotesPage(); return; } _ffOrigRenderCurrentPage(); }; // ── SHOW INVITE BUTTON WHEN EDITING EXISTING CUSTOMER ── const _ffOrigEditCustomer = typeof editCustomer === 'function' ? editCustomer : null; if (_ffOrigEditCustomer) { editCustomer = async function(id) { await _ffOrigEditCustomer(id); const invBtn = document.getElementById('invite-portal-btn'); if (invBtn) invBtn.style.display = 'inline-flex'; }; } // ── LOG LOAD EVENTS (if table missing, silently skip) ── async function logLoadEvent(loadId, eventType, description) { try { await db.from('load_events').insert({ load_id: loadId, event_type: eventType, event_description: description, performed_by: currentUser?.id, performed_by_name: currentUser?.user_metadata?.full_name || currentUser?.email, }); } catch(e) { /* non-critical */ } } async function loadEventHistory() { if (!currentDetailLoadId) return; const { data } = await db.from('load_events').select('*').eq('load_id', currentDetailLoadId).order('created_at', {ascending:false}).limit(20); const el = document.getElementById('load-event-timeline'); if (!el) return; if (!data || !data.length) { el.innerHTML='
No history yet
'; return; } el.innerHTML = data.map(e => `
${new Date(e.created_at).toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}
${e.event_description||e.event_type}
${e.performed_by_name?`
by ${e.performed_by_name}
`:''}
`).join(''); } // ── RENDER PORTAL QUOTES (updated version) ──── function renderPortalQuotes() { const el = document.getElementById('portal-quotes-list'); if (!el) return; const quotes = window.portalCustomerQuotes || window.portalQuotes || []; if (!quotes.length) { el.innerHTML='
No quote requests yet
'; return; } el.innerHTML = quotes.map(q => { const isQuoted = q.status === 'Quoted'; const statusColors = {'Pending':'#D97706','Quoted':'#2563EB','Approved':'#059669','Customer Declined':'#DC2626','Declined':'#64748b'}; const color = statusColors[q.status]||'#D97706'; return `
${q.origin_city||'?'}, ${q.origin_state||''} → ${q.destination_city||'?'}, ${q.destination_state||''}
${q.equipment_type||''}${q.pickup_date?' · Pickup: '+fmtDate(q.pickup_date):''}
${q.status}
${isQuoted&&q.quoted_rate?`
Your Quote
$${Number(q.quoted_rate).toLocaleString()}
${q.broker_note?`
${q.broker_note}
`:''}
`:''}
Submitted ${fmtDate(q.submitted_at||q.created_at)}
`; }).join(''); } // ── MARK INVOICE PAID ───────────────────────── async function markInvoicePaid(id) { await db.from('invoices').update({ status:'Paid', paid_at:new Date().toISOString() }).eq('id',id); toast('Invoice marked as paid!','success'); await loadData(); } // ── INVOICE AGING ───────────────────────────── function renderInvoiceAging() { const now = new Date(); const bands = {current:[],d30:[],d60:[],d90:[]}; allInvoices.filter(i=>i.status!=='Paid').forEach(inv => { if (!inv.due_date) { bands.current.push(inv); return; } const days = Math.floor((now - new Date(inv.due_date)) / 86400000); if (days<=0) bands.current.push(inv); else if (days<=30) bands.d30.push(inv); else if (days<=60) bands.d60.push(inv); else bands.d90.push(inv); }); const sum = arr => arr.reduce((s,i)=>s+(Number(i.amount)||0),0); const setEl = (id,val)=>{const el=document.getElementById(id);if(el)el.textContent=val;}; setEl('aging-current',fmt$(sum(bands.current))); setEl('aging-current-count',bands.current.length+' invoices'); setEl('aging-30',fmt$(sum(bands.d30))); setEl('aging-30-count',bands.d30.length+' invoices'); setEl('aging-60',fmt$(sum(bands.d60))); setEl('aging-60-count',bands.d60.length+' invoices'); setEl('aging-90',fmt$(sum(bands.d90))); setEl('aging-90-count',bands.d90.length+' invoices'); } function exportAging() { exportData('invoices'); } // ── RENDER LOADS TABLE (for filterLoads) ────── function renderLoadsTable(loads) { const body = document.getElementById('loads-body'); if(!body) return; if (!loads.length) { body.innerHTML=`
📦

No loads match your filters.

`; return; } body.innerHTML=`${loads.map(l=>{const isPOD=l.status==='Delivered'&&!allInvoices.find(i=>i.load_id===l.id);return``;}).join('')}
Load #CustomerOriginDestinationEquipmentPickupCarrierRateMarginStatus
${l.load_number}${l.customers?.name||'—'}${l.origin_city||'—'}, ${l.origin_state||''}${l.destination_city||'—'}, ${l.destination_state||''}${l.equipment_type||'—'}${fmtDate(l.pickup_date)}${l.carriers?.name||''}${fmt$(l.customer_rate)}${margin(l.customer_rate,l.carrier_pay)}${statusBadge(l.status)}${isPOD?' POD':''}
`; } // ── SQL SCHEMA NOTE ─────────────────────────── /* Run this in Supabase SQL editor to enable quote_requests: create table if not exists quote_requests ( id uuid default gen_random_uuid() primary key, customer_id uuid references customers(id) on delete cascade, origin_city text, origin_state text, destination_city text, destination_state text, equipment_type text, pickup_date date, commodity text, weight numeric, notes text, status text default 'Pending', quoted_rate numeric, quoted_carrier_pay numeric, broker_note text, declined_reason text, customer_response text, customer_responded_at timestamptz, quoted_at timestamptz, submitted_at timestamptz default now(), created_at timestamptz default now() ); alter table quote_requests enable row level security; create policy "Allow all" on quote_requests for all using (true); alter table customers add column if not exists portal_user_id uuid references auth.users(id), add column if not exists portal_invited_at timestamptz, add column if not exists portal_status text default 'not_invited'; */ applyTheme(); initRolePerms(); checkAuth(); loadRememberedEmail(); // Init city autocomplete after modals are in DOM document.addEventListener('DOMContentLoaded', initCityAutocomplete); setTimeout(initCityAutocomplete, 500);