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);