Your Agency
Enter password to continue
License activation
Activate your
suite.
Enter your purchase email to activate. Takes 5 seconds — done once, remembered forever.
Verifying with Stripe…
Can't find your purchase? Email [email protected]
Revenue Dashboard
Revenue — Last 6 Months
Recent Invoices
⊳ Nexus Bridge — Completed Projects
◈ Vinculum Bridge — CRM Deals
⚡ Stripe Payment Links
Connect Stripe to generate payment links for each invoice. Clients click to pay instantly by card.
Invoice #DescriptionClientAmountIssuedDueStatus
Estimate #DescriptionClientAmountIssuedValid UntilStatus
Invoice Details
◈ AI Memo Writer
Title / Description
Invoice / Estimate #
Issue Date
Due Date
Line Items
Pricing
Tax (%)
Discount (%)
Notes
Payment Terms
Notes / Memo
Recurring
Invoice Preview
`); w.document.close(); setTimeout(()=>w.print(),500); toast('Print dialog opening…','gold'); } // ══════════════════════════════════════ // CLIENTS VIEW // ══════════════════════════════════════ function renderClients(){ document.getElementById('clientsMeta').textContent=`${S.clients.length} client${S.clients.length!==1?'s':''} · ${S.invoices.length} invoices total`; const grid=document.getElementById('clientsGrid'); grid.innerHTML=S.clients.map(c=>{ const invs=S.invoices.filter(i=>i.clientId==c.id||i.client===c.company||i.client===c.name); const collected=invs.filter(i=>i.status==='paid').reduce((a,i)=>a+calcTotals(i.items,i.tax,i.discount).total,0); return `
${initials(c.name)}
${c.name}
${c.company||'—'} · ${c.email||'—'}
${invs.length}
Invoices
${fmtMoney(collected)}
Collected
`; }).join(''); } function newDocForClient(clientId){ newDoc('invoice'); setTimeout(()=>{ document.getElementById('builderClient').value=clientId; renderInvoice(); },80); } function deleteClient(id){ if(!confirm('Delete this client?')) return; S.clients=S.clients.filter(c=>c.id!=id); save(); renderAll(); toast('Deleted','error'); } // ══════════════════════════════════════ // PAYMENTS VIEW // ══════════════════════════════════════ function renderPayments(){ const overdue=S.invoices.filter(i=>i.status==='overdue'); const partial=S.invoices.filter(i=>i.status==='partial'); const sent=S.invoices.filter(i=>i.status==='sent'); const all=[...overdue,...partial,...sent]; let html=''; if(overdue.length>0){ html+=`
⚠ Overdue (${overdue.length})
`; html+=overdue.map(inv=>paymentRow(inv,'overdue')).join(''); } if(partial.length>0){ html+=`
Partial Payment
`; html+=partial.map(inv=>paymentRow(inv,'partial')).join(''); } if(sent.length>0){ html+=`
Awaiting Payment
`; html+=sent.map(inv=>paymentRow(inv,'sent')).join(''); } if(all.length===0) html='
$
All clear
No outstanding invoices. Well done.
'; html+=`
⚡ Stripe Payment Links
Once Stripe is connected, each invoice will have a unique payment link. Share with clients for instant card payment.
`; document.getElementById('paymentsView').innerHTML=html; } function paymentRow(inv, type){ const t=calcTotals(inv.items,inv.tax,inv.discount); return `
${inv.number}
${inv.title}
${inv.client} · Due ${fmtDate(inv.dueDate)}
${fmtMoney(t.total)}
`; } function generatePaymentLink(id){ const inv=S.invoices.find(i=>i.id==id); if(!inv) return; const t=calcTotals(inv.items,inv.tax,inv.discount); toast(`Payment link ready: pay.youragency.com/${inv.number.toLowerCase()} · ${fmtMoney(t.total)}`,'gold'); } // ══════════════════════════════════════ // BRIDGE HELPERS // ══════════════════════════════════════ function invoiceFromNexus(projectId){ let nexusProject=null; try{ const ns=JSON.parse(localStorage.getItem(NEXUS_KEY)||'{}'); nexusProject=(ns.projects||[]).find(p=>p.id==projectId); }catch(e){} if(!nexusProject) return; newDoc('invoice'); setTimeout(()=>{ document.getElementById('docTitle').value=nexusProject.title||''; if(nexusProject.value>0) lineItems=[{id:1,name:nexusProject.title,desc:'As per project agreement',qty:1,price:nexusProject.value}]; renderLineItemList(); renderInvoice(); },80); toast('Project imported from Nexus','success'); } function invoiceFromVinculum(contactId){ let contact=null; try{ const vs=JSON.parse(localStorage.getItem(VINCULUM_KEY)||'{}'); contact=(vs.contacts||[]).find(c=>String(c.id)===String(contactId)); }catch(e){} if(!contact) return; newDoc('invoice'); setTimeout(()=>{ document.getElementById('docTitle').value=`Services — ${contact.company||contact.name}`; renderInvoice(); },80); toast('Deal imported from Vinculum','success'); } // ══════════════════════════════════════ // AI MEMO WRITER // ══════════════════════════════════════ async function aiWriteMemo(){ const prompt=document.getElementById('aiPromptInput').value.trim(); if(!prompt){ toast('Enter a prompt first','error'); return; } const label=document.getElementById('aiLabel'); label.innerHTML='
Writing…'; const title=document.getElementById('docTitle').value||'invoice'; const clientSel=document.getElementById('builderClient'); const clientId=parseInt(clientSel?.value)||null; const client=S.clients.find(c=>c.id===clientId); const tots=calcTotals(lineItems,parseFloat(document.getElementById('docTax').value)||0,parseFloat(document.getElementById('docDiscount').value)||0); const sys=`You are a professional billing assistant for [Your Agency]. Write invoice notes, payment reminders, and memo text that is warm, professional, and concise. Invoice: ${title} Client: ${client?.company||client?.name||'Unknown'} Total: ${fmtMoney(tots.total)} Write under 60 words unless asked for an email. No markdown.`; try{ const r=await fetch('https://api.ridgelinecrm.com/v1/messages', { method:'POST', headers:{'Content-Type':'application/json'},body:JSON.stringify({model:'claude-sonnet-4-20250514',max_tokens:300,system:sys,messages:[{role:'user',content:prompt}]})}); const d=await r.json(); if(d.content?.[0]?.text){ document.getElementById('docNotes').value=d.content[0].text; document.getElementById('aiPromptInput').value=''; label.innerHTML='◈ AI Memo Writer'; renderInvoice(); toast('AI wrote your memo','success'); } }catch(e){ label.innerHTML='◈ AI Memo Writer'; toast('AI error','error'); } } async function aiReminder(){ const overdue=S.invoices.filter(i=>i.status==='overdue'); if(overdue.length===0){ toast('No overdue invoices','error'); return; } const inv=overdue[0]; const t=calcTotals(inv.items,inv.tax,inv.discount); const prompt=`Write a professional but firm payment reminder email for an overdue invoice. Invoice ${inv.number} for ${inv.client}, amount ${fmtMoney(t.total)}, due ${fmtDate(inv.dueDate)}. Keep it under 80 words. No markdown.`; try{ const r=await fetch('https://api.ridgelinecrm.com/v1/messages',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:'claude-sonnet-4-20250514',max_tokens:200,messages:[{role:'user',content:prompt}]})}); const d=await r.json(); if(d.content?.[0]?.text) alert(`PAYMENT REMINDER EMAIL:\n\n${d.content[0].text}`); }catch(e){ toast('AI error','error'); } } function showStripeSetup(){ openModal('stripe'); } // ══════════════════════════════════════ // SETTINGS (AI proxy URL + Stripe config) // ══════════════════════════════════════ const SETTINGS_KEY = 'calculus_settings_v1'; function getSettings() { try { const r = localStorage.getItem(SETTINGS_KEY); return r ? JSON.parse(r) : {}; } catch(e) { return {}; } } function saveSettings(data) { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify({ ...getSettings(), ...data })); } catch(e) {} } function getProxyUrl() { return 'https://api.ridgelinecrm.com'; } function saveStripeConfig() { const proxy = document.getElementById('cfg_proxy')?.value.trim(); if (proxy) saveSettings({ proxyUrl: proxy }); closeModal(); toast('Settings saved', 'success'); renderAll(); // refresh to show connected state } // ══════════════════════════════════════ // MODALS // ══════════════════════════════════════ function openModal(type, id=null){ const title=document.getElementById('modalTitle'); const body=document.getElementById('modalBody'); const ft=document.getElementById('modalFt'); if(type==='client'){ const c=id?S.clients.find(x=>x.id==id):null; title.innerHTML=c?`Edit Client`:`New Client`; body.innerHTML=`
`; ft.innerHTML=``; } else if(type==='new-estimate'){ title.innerHTML=`New Estimate`; body.innerHTML=`
Create a new estimate using the Invoice Builder. Estimates can be converted to invoices in one click once accepted.
`; ft.innerHTML=``; } else if(type==='stripe'){ const cfg = getSettings(); title.innerHTML=`Stripe Integration`; body.innerHTML=`
AI Proxy URL
Your Cloudflare Worker URL from the setup guide. All AI calls and Stripe requests route through this proxy.
Stripe Setup
Add your Stripe secret key to your AI Proxy Worker as an environment variable named STRIPE_SECRET_KEY. The key never touches the browser — all Stripe calls route through your Worker.
Worker → Settings → Variables & Secrets → Add Variable
Name: STRIPE_SECRET_KEY  |  Value: sk_live_…  |  Encrypt: ✓
Then redeploy your Worker. Once done, "Send Payment Link" on any invoice will create a real Stripe Payment Link automatically.
`; ft.innerHTML=``; } document.getElementById('modalOverlay').classList.add('open'); } function closeModal(){ document.getElementById('modalOverlay').classList.remove('open'); } function bgCloseModal(e){ if(e.target===document.getElementById('modalOverlay')) closeModal(); } function saveClient(id){ const name=document.getElementById('cl_name').value.trim(); if(!name){ toast('Name required','error'); return; } const data={name,company:document.getElementById('cl_company').value.trim(),email:document.getElementById('cl_email').value.trim(),phone:document.getElementById('cl_phone').value.trim(),address:document.getElementById('cl_address').value.trim()}; if(id&&id!='null'){ const c=S.clients.find(x=>x.id==id); if(c) Object.assign(c,data); toast('Updated'); } else { S.clients.push({id:uid(),...data}); toast('Client added','success'); } save(); closeModal(); renderAll(); } function populateClientSelect(){ const sel=document.getElementById('builderClient'); if(!sel) return; const cur=sel.value; sel.innerHTML=''+S.clients.map(c=>``).join(''); sel.value=cur; } // ══════════════════════════════════════ // KEYBOARD // ══════════════════════════════════════ document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ if(document.getElementById('exportOverlay').classList.contains('open')) closeExport(); else if(document.getElementById('modalOverlay').classList.contains('open')) closeModal(); } }); // Init populateClientSelect(); renderInvoice(); renderAll(); // ══════════════════════════════════════ // AI ASSISTANT DRAWER // ══════════════════════════════════════ const aiHistory = []; function openAI() { document.getElementById('aiDrawer').classList.add('open'); document.getElementById('aiOverlay').classList.add('open'); setTimeout(() => document.getElementById('aiInput').focus(), 300); } function closeAI() { document.getElementById('aiDrawer').classList.remove('open'); document.getElementById('aiOverlay').classList.remove('open'); } function aiKey(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAI(); } } function aiPrompt(text) { document.getElementById('aiInput').value = text; document.getElementById('aiSuggestions').style.display = 'none'; sendAI(); } function aiContext() { const invoices = S.invoices || []; const clients = S.clients || []; const total = invoices.length; const paid = invoices.filter(i => i.status === 'paid'); const unpaid = invoices.filter(i => i.status === 'unpaid' || i.status === 'sent'); const overdue = invoices.filter(i => i.status === 'overdue'); const draft = invoices.filter(i => i.status === 'draft'); const totalRevenue = paid.reduce((a, i) => a + (i.total || 0), 0); const outstanding = unpaid.concat(overdue).reduce((a, i) => a + (i.total || 0), 0); const fmt = n => '$' + n.toLocaleString('en-US', {minimumFractionDigits:2, maximumFractionDigits:2}); const recentInvoices = invoices.slice(-5).map(i => `#${i.number||'?'} ${clients.find(c=>c.id===i.clientId)?.name||'?'} ${fmt(i.total||0)} (${i.status})`).join('; '); return `You are Calculus AI, the revenue intelligence assistant for the [Your Agency] suite. You help agencies track invoices, manage cash flow, and communicate professionally with clients about payments. CURRENT BILLING DATA: - Total invoices: ${total} | Paid: ${paid.length} | Unpaid/Sent: ${unpaid.length} | Overdue: ${overdue.length} | Drafts: ${draft.length} - Total collected revenue: ${fmt(totalRevenue)} - Outstanding balance: ${fmt(outstanding)} - Total clients: ${clients.length} - Recent invoices: ${recentInvoices || 'none yet'} Be direct about money. When writing payment reminders, be firm but professional. Flag overdue invoices clearly.`; } async function sendAI() { const input = document.getElementById('aiInput'); const text = input.value.trim(); if (!text) return; document.getElementById('aiSuggestions').style.display = 'none'; input.value = ''; document.getElementById('aiSendBtn').disabled = true; aiHistory.push({ role: 'user', content: text }); renderAIMsgs(); const msgs = document.getElementById('aiMsgs'); const typing = document.createElement('div'); typing.className = 'ai-typing'; typing.id = 'aiTyping'; typing.innerHTML = ''; msgs.appendChild(typing); msgs.scrollTop = msgs.scrollHeight; try { const res = await fetch('https://api.ridgelinecrm.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1000, system: aiContext(), messages: aiHistory }) }); const data = await res.json(); const reply = data.content?.[0]?.text || 'Sorry, I could not get a response.'; aiHistory.push({ role: 'assistant', content: reply }); } catch(e) { aiHistory.push({ role: 'assistant', content: 'Connection error. Check your AI proxy is running.' }); } document.getElementById('aiTyping')?.remove(); renderAIMsgs(); document.getElementById('aiSendBtn').disabled = false; input.focus(); } function renderAIMsgs() { const msgs = document.getElementById('aiMsgs'); const welcome = msgs.querySelector('.ai-msg.assistant'); msgs.innerHTML = ''; if (welcome) msgs.appendChild(welcome); aiHistory.forEach(m => { const div = document.createElement('div'); div.className = `ai-msg ${m.role}`; div.innerHTML = `
${m.role === 'user' ? 'You' : 'Calculus AI'}
${m.content.replace(/\*\*(.+?)\*\*/g,'$1').replace(/\n/g,'
')}
`; msgs.appendChild(div); }); msgs.scrollTop = msgs.scrollHeight; }
Calculus AI Revenue Intelligence
Calculus AI
I can see your full invoicing pipeline — outstanding balances, paid invoices, and client billing history. Ask me about revenue, overdue payments, or how to write a professional payment follow-up.