NIL INTELLIGENCE PLATFORM
NOTIFICATIONS
No urgent deadlines
AI READY
Active client:
Command Center
Ask anything about your client — deals, pricing, leverage, strategy
Pipeline Value
$0
Active deals
Active Deals
0
In progress
Clients
0
On roster
AI
Connected
Ask about your client LIVE AI
NILDash Intelligence
Deal Scan
AI-ranked brand opportunities for your selected client
AI Deal Scan
Select a client and click Scan Deals to get AI-ranked recommendations.
Ranked Opportunities
#BrandCampaignCategoryRate RangeScoreTimingAction
Run a deal scan to see recommendations
Rate Calculator
Calculate exactly what to charge — with the math to back it up
Deliverable
Recommended Rate
per deliverable
Rate Justification Script
Calculate a rate above then click "Get Justification Script" for word-for-word language to defend your price on a call.
Negotiation Intel
Get a full playbook for any deal — opening line, pushback, walk-away
Negotiation Playbook
Quick Counter Scripts — click to copy
"I appreciate the offer. Based on current comps for athletes at this engagement tier, we're at $11,000. That's actually below what we're seeing for comparable athletes right now."
"Can you get to $9,500 with a performance kicker? If the campaign clears 2M impressions we'd add $2,000. Aligns your incentives with ours."
"At that level we're pricing below our floor. Let's revisit when your Q3 budget opens. I'd hate for scheduling to be the blocker when the fit is this strong."
Click any script to copy
Contract Generator
Generate a professional NIL contract ready for signature — powered by AI
⚠️ AI-generated contracts are a starting point. Always have a licensed attorney review before signing.
Deal Information
Agent Information
Commission Tracker
Your earnings across all clients and deals
My Commission Rate:
%
Applied to all closed deals
Total Earned
$0
From closed deals
Pending Commission
$0
Deals in progress
Deals Closed
0
This year
Top Earner
By commission
By Athlete
All Deals
Athlete Brand Stage Deal Value Commission Status
No deals found
Deal Pipeline
Every deal, every client, every stage
Total Pipeline
$0
Deals Active
0
Closing Soon
0
My Roster
Your clients — click any card to make them active
No clients yet. Add your first client!
Add Client
Add a new athlete to your roster
Client Details
Brand Outreach
AI writes your cold outreach — email, Instagram DM, and LinkedIn — then tracks every contact
Outreach Tracker
No outreach logged yet.
Deal Calendar
All deadlines, follow-ups, and campaign dates — color-coded by close probability
Closing
Negotiating
Outreach Sent
Prospecting
Upcoming Deadlines
Load your deals to see deadlines.
All Active Deals
No deals yet — add deals in Pipeline.
NIL Compliance Checker
Check any deal against current state NIL laws — flags issues, generates required disclosure language, and saves to deal notes
⚠️ This tool uses AI + live web search to find current NIL rules. Always verify with a licensed attorney before executing deals. NIL laws change frequently.
⚡ SPARTA 72-Hour Compliance
Federal law requires agents to notify the university within 72 hours of signing any NIL agreement. Enter the signing date to track your deadline.
Team Match
Find the best college programs for your client — and what they should be making
Negotiation Playbook
+ b.cpm + '' : '') + contextRow; document.getElementById('rateAI').classList.add('visible'); } catch(e) { showToast('Error: ' + e.message); } } async function getRateScript() { if (!selectedAthleteId) { showToast('Select a client first'); return; } const type = document.getElementById('rateDeliverable').value; const rate = await fetch(`${API_BASE}/api/ai/rate`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ athleteId: selectedAthleteId, deliverableType: type }), }).then(r=>r.json()).catch(()=>({mid:10000})); const txt = document.getElementById('rateText'); const spinner = document.getElementById('rateSpinner'); document.getElementById('rateAI').classList.add('visible'); spinner.style.display = 'block'; txt.textContent = ''; try { const ath = athletes.find(a=>a.id===selectedAthleteId); const msg = `Write a 5-sentence script the agent reads aloud to justify $${rate.mid||10000} for a ${type} deal with ${ath?.name||'this athlete'}. Include engagement rate vs industry average, school premium, CPM comparison to paid digital ads, and close with a question. Use quotation marks. Be confident and factual.`; const r2 = await fetch(`${API_BASE}/api/ai/ask`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ athleteId: selectedAthleteId, message: msg }), }); const data = await r2.json(); if (data.error) { txt.textContent = '❌ ' + data.error; } else { txt.textContent = data.response || 'No response.'; } } catch(e) { txt.textContent = '❌ ' + e.message; } spinner.style.display = 'none'; } // ── AI: NEGOTIATE ──────────────────────────────────────────── async function runNegotiate() { if (!selectedAthleteId) { showToast('Select a client first'); return; } const brand = document.getElementById('negBrand').value; if (!brand) { showToast('Enter a brand name'); return; } const box = document.getElementById('negAI'); const txt = document.getElementById('negText'); box.classList.add('visible'); txt.textContent = ''; document.getElementById('negSpinner').style.display = 'block'; try { const {playbook,error} = await fetch(`${API_BASE}/api/ai/negotiate`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ athleteId: selectedAthleteId, brand, theirOffer: document.getElementById('negOffer').value, agentTarget: document.getElementById('negTarget').value, }), }).then(r=>r.json()); if(error) throw new Error(error); txt.textContent = playbook; } catch(e) { txt.textContent = '❌ ' + e.message; } document.getElementById('negSpinner').style.display = 'none'; } // ── PIPELINE ───────────────────────────────────────────────── async function loadPipeline() { const stages = ['Prospecting','Outreach Sent','Negotiating','Closing','Closed']; let allDeals = []; for (const ath of athletes) { const deals = await fetch(`${API_BASE}/api/athletes/${ath.id}/deals`).then(r=>r.json()).catch(()=>[]); deals.forEach(d => allDeals.push({...d, athleteName: ath.name})); } const total = allDeals.reduce((s,d)=>s+(d.value||0),0); document.getElementById('pipe-total').textContent = '$'+(total/1000).toFixed(0)+'K'; document.getElementById('pipe-count').textContent = allDeals.filter(d=>d.stage!=='Closed').length; document.getElementById('pipe-closing').textContent = allDeals.filter(d=>d.stage==='Closing').length; const board = document.getElementById('pipelineBoard'); board.innerHTML = stages.map(stage => { const stagDeals = allDeals.filter(d=>d.stage===stage); return `
${stage}
${stagDeals.length}
${stagDeals.map(d=>{ const stagesJson = JSON.stringify(stages).replace(/'/g,"\'"); return `
${d.brand}
${d.athleteName}
$${(d.value||0).toLocaleString()}
${stage.split(' ')[0]}
`;}).join('')}
`; }).join(''); } async function moveDeal(dealId, athleteId, currentStage, stages) { const idx = stages.indexOf(currentStage); const nextStage = stages[idx + 1]; if (!nextStage) { showToast('Already at final stage'); return; } await fetch(`${API_BASE}/api/deals/${dealId}`, { method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ stage: nextStage }) }); showToast('Moved to ' + nextStage); loadKPIs(); loadPipeline(); if (nextStage === 'Closed') setTimeout(renderCommission, 200); } async function deleteDealCard(dealId, athleteId) { if (!confirm('Delete this deal?')) return; await fetch(`${API_BASE}/api/deals/${dealId}`, { method:'DELETE' }); showToast('Deal deleted'); loadKPIs(); loadPipeline(); setTimeout(renderCommission, 200); } async function addDeal() { if (!selectedAthleteId) { showToast('Select a client first'); return; } const brand = document.getElementById('d_brand').value.trim(); if (!brand) { showToast('Brand name required'); return; } try { const r = await fetch(`${API_BASE}/api/athletes/${selectedAthleteId}/deals`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ brand, campaign: document.getElementById('d_campaign').value, value: document.getElementById('d_value').value, stage: document.getElementById('d_stage').value, }), }); const data = await r.json(); if (!r.ok) { showToast('Error: ' + (data.error || 'Failed to save deal')); return; } document.getElementById('addDealModal').classList.remove('open'); document.getElementById('d_brand').value = ''; document.getElementById('d_campaign').value = ''; document.getElementById('d_value').value = ''; showToast('✅ Deal added!'); await loadKPIs(); await loadPipeline(); setTimeout(renderCommission, 200); } catch(e) { showToast('Error: ' + e.message); } } // ── UTILS ──────────────────────────────────────────────────── function copyText(el) { navigator.clipboard.writeText(el.textContent.trim()); showToast('Copied to clipboard!'); } function showToast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 2800); } // ── PLAYER URL FETCH ───────────────────────────────────────── async function fetchFromUrl() { const url = document.getElementById('a_url').value.trim(); if (!url) { showToast('Paste a player profile URL first'); return; } const btn = document.getElementById('fetch-btn'); const status = document.getElementById('fetch-status'); btn.disabled = true; btn.textContent = 'Importing...'; status.textContent = 'Fetching real stats from URL...'; try { const r = await fetch(`${API_BASE}/api/ai/player-fetch`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ url }) }); const data = await r.json(); if (!data.found) { status.textContent = 'Could not extract data - try a different URL'; btn.disabled = false; btn.textContent = 'Import'; return; } if (data.name) document.getElementById('a_name').value = data.name; if (data.school) document.getElementById('a_school').value = data.school; if (data.position) document.getElementById('a_pos').value = data.position; if (data.year) { const s = document.getElementById('a_year'); for (let o of s.options) { if (o.value.toLowerCase() === data.year.toLowerCase()) { s.value = o.value; break; } } } if (data.stats) document.getElementById('a_stats').value = data.stats; if (data.notes) document.getElementById('a_notes').value = data.notes; const summary = [data.position, data.year, data.stats].filter(Boolean).join(' - '); status.innerHTML = 'Stats imported: ' + summary; showToast('Real stats imported!'); } catch(e) { const msg = e.message && e.message.includes('blocks') ? e.message : 'ESPN/247Sports block server imports. Use the ⚡ Lookup button instead.'; status.textContent = msg; } btn.disabled = false; btn.textContent = 'Import'; } // ── PLAYER LOOKUP ───────────────────────────────────────────── async function lookupPlayer() { const name = document.getElementById('a_name').value.trim(); if (!name) { showToast('Enter a player name first'); return; } const school = document.getElementById('a_school').value.trim(); const sport = document.getElementById('a_sport').value; const btn = document.getElementById('lookup-btn'); const status = document.getElementById('lookup-status'); btn.disabled = true; btn.textContent = 'Looking up...'; status.textContent = 'Searching for player data...'; try { const r = await fetch(`${API_BASE}/api/ai/player-lookup`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ name, school, sport }) }); const data = await r.json(); if (!data.found) { status.textContent = 'Player not found - fill in manually'; btn.disabled = false; btn.textContent = 'Lookup'; return; } if (data.school) document.getElementById('a_school').value = data.school; if (data.position) document.getElementById('a_pos').value = data.position; if (data.year) { const s = document.getElementById('a_year'); for (let o of s.options) { if (o.value.toLowerCase() === data.year.toLowerCase()) { s.value = o.value; break; } } } if (data.stats) document.getElementById('a_stats').value = data.stats; if (data.instagram) document.getElementById('a_ig').value = data.instagram; if (data.tiktok) document.getElementById('a_tt').value = data.tiktok; if (data.engagement) document.getElementById('a_eng').value = data.engagement; if (data.notes) document.getElementById('a_notes').value = data.notes; if (data.schoolTier) { const s = document.getElementById('a_tier'); for (let o of s.options) { if (o.value === data.schoolTier) { s.value = o.value; break; } } } if (data.sport) { const s = document.getElementById('a_sport'); for (let o of s.options) { if (o.value === data.sport) { s.value = o.value; break; } } } if (data.hometown) { const n = document.getElementById('a_notes'); if (!n.value) n.value = data.hometown; } const summary = [data.school, data.position, data.year, data.stats].filter(Boolean).join(' · '); status.innerHTML = '✓ Loaded — review and save · ' + summary; showToast('Player data loaded — please verify before saving!'); } catch(e) { status.textContent = 'Lookup failed: ' + e.message; } btn.disabled = false; btn.textContent = 'Lookup'; } // ── BRAND OUTREACH ─────────────────────────────────────────── let outreachLogByAthlete = {}; let currentOutreach = null; function copyText(id) { const el = document.getElementById(id); if (el) navigator.clipboard.writeText(el.textContent).then(() => showToast('Copied!')); } let lastComplianceResult = null; let lastContract = ''; let commFilter = 'all'; function setCommFilter(f) { commFilter = f; ['all','Closed','active'].forEach(x => { const el = document.getElementById('cf-' + x); if (!el) return; if (x === f) { el.style.background = 'var(--accent)'; el.style.color = '#000'; el.style.borderColor = 'var(--accent)'; } else { el.style.background = 'transparent'; el.style.color = 'var(--muted)'; el.style.borderColor = 'var(--border)'; } }); renderCommission(); } async function renderCommission() { const rate = parseFloat(document.getElementById('comm-rate')?.value || 15) / 100; // Gather all deals across all athletes const allDeals = []; for (const ath of athletes) { const deals = await fetch(`${API_BASE}/api/athletes/${ath.id}/deals`).then(r=>r.json()).catch(()=>[]); deals.forEach(d => allDeals.push({ ...d, athleteName: ath.name, athleteId: ath.id })); } // Filter let filtered = allDeals; if (commFilter === 'Closed') filtered = allDeals.filter(d => d.stage === 'Closed'); else if (commFilter === 'active') filtered = allDeals.filter(d => d.stage !== 'Closed'); // KPIs const closedDeals = allDeals.filter(d => d.stage === 'Closed'); const activeDeals = allDeals.filter(d => d.stage !== 'Closed'); const totalEarned = closedDeals.reduce((s, d) => s + (parseInt(d.value) || 0), 0) * rate; const totalPending = activeDeals.reduce((s, d) => s + (parseInt(d.value) || 0), 0) * rate; document.getElementById('comm-total-earned').textContent = '$' + Math.round(totalEarned).toLocaleString(); document.getElementById('comm-pending').textContent = '$' + Math.round(totalPending).toLocaleString(); document.getElementById('comm-closed-count').textContent = closedDeals.length; // Top earner by athlete const byAthlete = {}; closedDeals.forEach(d => { if (!byAthlete[d.athleteName]) byAthlete[d.athleteName] = 0; byAthlete[d.athleteName] += (parseInt(d.value) || 0) * rate; }); const topAthlete = Object.entries(byAthlete).sort((a,b) => b[1]-a[1])[0]; document.getElementById('comm-top-athlete').textContent = topAthlete ? topAthlete[0].split(' ')[0] : '—'; // By athlete breakdown const athEl = document.getElementById('comm-by-athlete'); if (Object.keys(byAthlete).length === 0) { athEl.innerHTML = '
No closed deals yet. Close a deal in the Pipeline to see commissions.
'; } else { const maxVal = Math.max(...Object.values(byAthlete)); athEl.innerHTML = Object.entries(byAthlete).sort((a,b)=>b[1]-a[1]).map(([name, comm]) => `
${name} $${Math.round(comm).toLocaleString()}
`).join(''); } // Deals table const tbody = document.getElementById('comm-table-body'); if (filtered.length === 0) { tbody.innerHTML = 'No deals found'; return; } tbody.innerHTML = filtered.sort((a,b) => (parseInt(b.value)||0) - (parseInt(a.value)||0)).map(d => { const val = parseInt(d.value) || 0; const comm = Math.round(val * rate); const isClosed = d.stage === 'Closed'; const statusColor = isClosed ? '#4ade80' : d.stage === 'Negotiating' ? 'var(--accent)' : 'var(--muted)'; return ` ${d.athleteName} ${d.brand || '—'} ${d.stage || '—'} ${val ? '$'+val.toLocaleString() : '—'} ${comm ? '$'+comm.toLocaleString() : '—'} ${isClosed ? 'EARNED' : 'PENDING'} `; }).join(''); } function setCommFilter(f) { commFilter = f; ['all','Closed','active'].forEach(x => { const el = document.getElementById('cf-' + x); if (!el) return; if (x === f) { el.style.background = 'var(--accent)'; el.style.color = '#000'; el.style.borderColor = 'var(--accent)'; } else { el.style.background = 'transparent'; el.style.color = 'var(--muted)'; el.style.borderColor = 'var(--border)'; } }); renderCommission(); } async function renderCommission() { const rate = parseFloat(document.getElementById('comm-rate')?.value || 15) / 100; // Gather all deals across all athletes const allDeals = []; for (const ath of athletes) { const deals = await fetch(`${API_BASE}/api/athletes/${ath.id}/deals`).then(r=>r.json()).catch(()=>[]); deals.forEach(d => allDeals.push({ ...d, athleteName: ath.name, athleteId: ath.id })); } // Filter let filtered = allDeals; if (commFilter === 'Closed') filtered = allDeals.filter(d => d.stage === 'Closed'); else if (commFilter === 'active') filtered = allDeals.filter(d => d.stage !== 'Closed'); // KPIs const closedDeals = allDeals.filter(d => d.stage === 'Closed'); const activeDeals = allDeals.filter(d => d.stage !== 'Closed'); const totalEarned = closedDeals.reduce((s, d) => s + (parseInt(d.value) || 0), 0) * rate; const totalPending = activeDeals.reduce((s, d) => s + (parseInt(d.value) || 0), 0) * rate; document.getElementById('comm-total-earned').textContent = '$' + Math.round(totalEarned).toLocaleString(); document.getElementById('comm-pending').textContent = '$' + Math.round(totalPending).toLocaleString(); document.getElementById('comm-closed-count').textContent = closedDeals.length; // Top earner by athlete const byAthlete = {}; closedDeals.forEach(d => { if (!byAthlete[d.athleteName]) byAthlete[d.athleteName] = 0; byAthlete[d.athleteName] += (parseInt(d.value) || 0) * rate; }); const topAthlete = Object.entries(byAthlete).sort((a,b) => b[1]-a[1])[0]; document.getElementById('comm-top-athlete').textContent = topAthlete ? topAthlete[0].split(' ')[0] : '—'; // By athlete breakdown const athEl = document.getElementById('comm-by-athlete'); if (Object.keys(byAthlete).length === 0) { athEl.innerHTML = '
No closed deals yet. Close a deal in the Pipeline to see commissions.
'; } else { const maxVal = Math.max(...Object.values(byAthlete)); athEl.innerHTML = Object.entries(byAthlete).sort((a,b)=>b[1]-a[1]).map(([name, comm]) => `
${name} $${Math.round(comm).toLocaleString()}
`).join(''); } // Deals table const tbody = document.getElementById('comm-table-body'); if (filtered.length === 0) { tbody.innerHTML = 'No deals found'; return; } tbody.innerHTML = filtered.sort((a,b) => (parseInt(b.value)||0) - (parseInt(a.value)||0)).map(d => { const val = parseInt(d.value) || 0; const comm = Math.round(val * rate); const isClosed = d.stage === 'Closed'; const statusColor = isClosed ? '#4ade80' : d.stage === 'Negotiating' ? 'var(--accent)' : 'var(--muted)'; return ` ${d.athleteName} ${d.brand || '—'} ${d.stage || '—'} ${val ? '$'+val.toLocaleString() : '—'} ${comm ? '$'+comm.toLocaleString() : '—'} ${isClosed ? 'EARNED' : 'PENDING'} `; }).join(''); } async function generateContract() { const brand = document.getElementById('con-brand').value.trim(); const value = document.getElementById('con-value').value; if (!brand || !value) { showToast('Brand and value are required'); return; } if (!selectedAthleteId) { showToast('Select an athlete first'); return; } const btn = document.getElementById('con-btn'); btn.disabled = true; btn.textContent = 'Generating...'; document.getElementById('con-result').style.display = 'none'; try { const r = await fetch(API_BASE + '/api/ai/contract', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ athleteId: selectedAthleteId, brand, value: parseInt(value), dealType: document.getElementById('con-deal-type').value, deliverables: document.getElementById('con-deliverables').value.trim(), startDate: document.getElementById('con-start').value, endDate: document.getElementById('con-end').value, exclusivity: document.getElementById('con-exclusivity').value, state: document.getElementById('con-state').value.trim(), paymentTerms: document.getElementById('con-payment').value, usageRights: document.getElementById('con-usage').value.trim(), agentName: document.getElementById('con-agent-name').value.trim(), agentEmail: document.getElementById('con-agent-email').value.trim(), }) }); const data = await r.json(); if (data.error) { showToast('Error: ' + data.error); return; } lastContract = data.contract; document.getElementById('con-text').textContent = data.contract; document.getElementById('con-result').style.display = 'block'; showToast('Contract generated successfully'); } catch(e) { showToast('Error: ' + e.message); } btn.disabled = false; btn.textContent = '📄 Generate Contract →'; } function copyContract() { navigator.clipboard.writeText(lastContract); showToast('Contract copied to clipboard'); } function downloadContract() { const athlete = athletes.find(a => a.id === selectedAthleteId); const brand = document.getElementById('con-brand').value.trim(); const filename = (athlete ? athlete.name.replace(/\s+/g,'-') : 'athlete') + '-' + brand.replace(/\s+/g,'-') + '-NIL-contract.txt'; const blob = new Blob([lastContract], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); showToast('Contract downloaded'); } async function runCompliance() { const state = document.getElementById('comp-state').value; const dealType = document.getElementById('comp-deal-type').value; const brand = document.getElementById('comp-brand').value.trim(); const value = document.getElementById('comp-value').value; const description = document.getElementById('comp-description').value.trim(); if (!state) { showToast('Select a state'); return; } const btn = document.getElementById('comp-btn'); btn.disabled = true; btn.textContent = 'Checking...'; document.getElementById('comp-results').style.display = 'none'; const athlete = selectedAthleteId ? athletes.find(a => a.id === selectedAthleteId) : null; try { const signingDate = document.getElementById('comp-signing-date').value; const r = await fetch(API_BASE + '/api/ai/compliance', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ state, dealType, brand, value, description, signingDate, athleteName: athlete ? athlete.name : '', sport: athlete ? athlete.sport : '', school: athlete ? athlete.school : '', schoolTier: athlete ? athlete.schoolTier : '' }) }); const data = await r.json(); if (data.error) { showToast('Error: ' + data.error); btn.disabled = false; btn.textContent = 'Check Compliance'; return; } lastComplianceResult = data; renderComplianceResults(data); document.getElementById('comp-results').style.display = 'block'; showToast('Compliance check complete'); } catch(e) { showToast('Error: ' + e.message); } btn.disabled = false; btn.textContent = 'Check Compliance'; } function renderComplianceResults(data) { const banner = document.getElementById('comp-status-banner'); const styles = { clear: 'border-radius:6px;padding:14px 18px;margin-bottom:20px;font-size:13px;font-weight:700;background:rgba(74,222,128,0.1);border:1px solid rgba(74,222,128,0.3);color:#4ade80', warning: 'border-radius:6px;padding:14px 18px;margin-bottom:20px;font-size:13px;font-weight:700;background:rgba(251,191,36,0.1);border:1px solid rgba(251,191,36,0.3);color:#fbbf24', blocked: 'border-radius:6px;padding:14px 18px;margin-bottom:20px;font-size:13px;font-weight:700;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);color:#ef4444' }; const msgs = { clear: 'No major compliance issues found for this deal in ' + data.state, warning: 'Compliance warnings found - review before proceeding', blocked: 'Deal has compliance issues that must be resolved' }; banner.style.cssText = styles[data.status] || styles.warning; banner.textContent = msgs[data.status] || msgs.warning; // SPARTA section const spartaSection = document.getElementById('comp-sparta-section'); if (data.sparta) { spartaSection.style.display = 'block'; const s = data.sparta; const color = s.status === 'overdue' ? '#ef4444' : s.status === 'urgent' ? '#fbbf24' : '#60a5fa'; const label = s.status === 'overdue' ? '🚨 DEADLINE PASSED' : s.status === 'urgent' ? '⚠️ NOTIFY TODAY' : '✅ On Track'; document.getElementById('comp-sparta-timer').innerHTML = '
' + label + '
' + '
72-hour deadline: ' + s.deadlineFormatted + '
' + '
' + (s.hoursLeft > 0 ? s.hoursLeft + ' hours remaining' : Math.abs(s.hoursLeft) + ' hours overdue') + '
'; document.getElementById('comp-sparta-notice').textContent = data.spartaNotice || 'No notification letter generated.'; } else { spartaSection.style.display = 'none'; } const flagsSection = document.getElementById('comp-flags-section'); const flagsEl = document.getElementById('comp-flags'); if (data.flags && data.flags.length) { flagsSection.style.display = 'block'; flagsEl.innerHTML = data.flags.map(f => '
' + (f.severity==="high"?"Blocked: ":"Warning: ") + f.issue + '
' + f.detail + '
').join(''); } else { flagsSection.style.display = 'none'; } const reqsSection = document.getElementById('comp-reqs-section'); const reqsEl = document.getElementById('comp-reqs'); if (data.requirements && data.requirements.length) { reqsSection.style.display = 'block'; reqsEl.innerHTML = data.requirements.map(r => '
-> ' + r + '
').join(''); } else { reqsSection.style.display = 'none'; } document.getElementById('comp-disclosure').textContent = data.disclosure || 'No specific disclosure language required.'; const s = document.getElementById('comp-sources'); if (s) s.textContent = data.sourceNote ? 'Source: ' + data.sourceNote : ''; } async function saveComplianceToNotes() { if (!lastComplianceResult || !selectedAthleteId) { showToast('Select a client first'); return; } const state = document.getElementById('comp-state').value; const dealType = document.getElementById('comp-deal-type').value; const sl = lastComplianceResult.status === 'clear' ? 'CLEAR' : lastComplianceResult.status === 'warning' ? 'WARNING' : 'ISSUES FOUND'; const note = 'COMPLIANCE CHECK (' + state + ' / ' + dealType + ') - ' + sl + '\nDisclosure: ' + (lastComplianceResult.disclosure || 'None') + (lastComplianceResult.flags && lastComplianceResult.flags.length ? '\nFlags: ' + lastComplianceResult.flags.map(f => f.issue).join(', ') : '') + '\nChecked: ' + new Date().toLocaleDateString(); const athlete = athletes.find(a => a.id === selectedAthleteId); if (athlete) { const updatedNotes = (athlete.notes ? athlete.notes + '\n\n' : '') + note; await fetch(API_BASE + '/api/athletes/' + selectedAthleteId, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({...athlete, notes: updatedNotes}) }); athlete.notes = updatedNotes; const ss = document.getElementById('comp-save-status'); if (ss) ss.textContent = 'Saved to ' + athlete.name + ' notes'; showToast('Compliance notes saved!'); } } async function runOutreach() { if (!selectedAthleteId) { showToast('Select a client first'); return; } const brand = document.getElementById('or-brand').value.trim(); if (!brand) { showToast('Enter a brand name'); return; } const btn = document.getElementById('or-run-btn'); btn.disabled = true; btn.textContent = 'Writing...'; document.getElementById('or-results').style.display = 'none'; try { const r = await fetch(`${API_BASE}/api/ai/outreach`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ athleteId: selectedAthleteId, brand, category: document.getElementById('or-category').value, contact: document.getElementById('or-contact').value, goal: document.getElementById('or-goal').value, }) }); const data = await r.json(); if (data.error) { showToast('Error: ' + data.error); btn.disabled = false; btn.textContent = 'Write Outreach'; return; } document.getElementById('or-email-subject').textContent = data.emailSubject || ''; document.getElementById('or-email-body').textContent = data.email || ''; document.getElementById('or-ig-body').textContent = data.instagram || ''; document.getElementById('or-li-body').textContent = data.linkedin || ''; document.getElementById('or-results').style.display = 'block'; currentOutreach = { brand, category: document.getElementById('or-category').value, athleteId: selectedAthleteId }; showToast('Outreach generated!'); } catch(e) { showToast('Error: ' + e.message); } btn.disabled = false; btn.textContent = 'Write Outreach'; } function saveOutreach() { if (!currentOutreach) return; const athlete = athletes.find(a => a.id === currentOutreach.athleteId); const aid = currentOutreach.athleteId; if (!outreachLogByAthlete[aid]) outreachLogByAthlete[aid] = []; outreachLogByAthlete[aid].unshift({ id: Date.now(), brand: currentOutreach.brand, category: currentOutreach.category, athlete: athlete ? athlete.name : 'Unknown', date: new Date().toLocaleDateString(), status: 'Sent', }); renderOutreachTracker(); showToast('Saved to tracker!'); } function updateOutreachStatus(id, status) { const log = selectedAthleteId ? (outreachLogByAthlete[selectedAthleteId] || []) : []; const e = log.find(x => x.id === id); if (e) { e.status = status; renderOutreachTracker(); } } function renderOutreachTracker() { const el = document.getElementById('or-tracker'); if (!el) return; const outreachLog = selectedAthleteId ? (outreachLogByAthlete[selectedAthleteId] || []) : []; if (!outreachLog.length) { el.innerHTML = '
No outreach logged yet.
'; return; } const colors = { Sent: 'var(--muted)', Replied: '#4ade80', 'No Response': 'var(--accent)', Declined: 'var(--red)' }; el.innerHTML = outreachLog.map(e => `
${e.brand}
${e.athlete} · ${e.category} · ${e.date}
`).join(''); } // ── SMART CALENDAR ─────────────────────────────────────────── const stageColors = { 'Closing': '#4ade80', 'Negotiating': '#C8F135', 'Outreach Sent': '#60a5fa', 'Prospecting': '#6b7280' }; const stageProb = { 'Closing': 80, 'Negotiating': 50, 'Outreach Sent': 25, 'Prospecting': 10 }; function getDaysFromCreated(createdAt, offsetDays) { const d = new Date(createdAt || Date.now()); d.setDate(d.getDate() + offsetDays); return d; } function formatDeadlineDate(d) { const now = new Date(); const diff = Math.ceil((d - now) / (1000 * 60 * 60 * 24)); const dateStr = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); if (diff < 0) return { label: dateStr, urgency: 'overdue', badge: 'Overdue' }; if (diff === 0) return { label: dateStr, urgency: 'today', badge: 'Today' }; if (diff <= 2) return { label: dateStr, urgency: 'urgent', badge: diff + 'd left' }; if (diff <= 7) return { label: dateStr, urgency: 'soon', badge: diff + 'd left' }; return { label: dateStr, urgency: 'future', badge: diff + 'd left' }; } function makeGoogleCalLink(title, date, description) { const start = date.toISOString().replace(/-|:|\.\d{3}/g, '').slice(0, 15) + '00Z'; const end = new Date(date.getTime() + 60 * 60 * 1000).toISOString().replace(/-|:|\.\d{3}/g, '').slice(0, 15) + '00Z'; return 'https://calendar.google.com/calendar/render?action=TEMPLATE' + '&text=' + encodeURIComponent(title) + '&dates=' + start + '/' + end + '&details=' + encodeURIComponent(description) + '&sf=true&output=xml'; } async function loadCalendar() { const allDeals = []; for (const ath of athletes) { try { const r = await fetch(`${API_BASE}/api/athletes/${ath.id}/deals`); const deals = await r.json(); deals.forEach(d => { d._athleteName = ath.name; allDeals.push(d); }); } catch(e) {} } if (!allDeals.length) { document.getElementById('cal-deadlines').innerHTML = '
No deals yet — add deals in Pipeline.
'; document.getElementById('cal-deals').innerHTML = ''; return; } // Generate deadlines based on stage const deadlines = []; allDeals.forEach(deal => { const color = stageColors[deal.stage] || '#6b7280'; const prob = stageProb[deal.stage] || 10; if (deal.stage === 'Negotiating') { deadlines.push({ deal, date: getDaysFromCreated(deal.updatedAt || deal.createdAt, 2), label: 'Counter offer deadline', color, prob }); deadlines.push({ deal, date: getDaysFromCreated(deal.updatedAt || deal.createdAt, 7), label: 'Follow-up call', color, prob }); } else if (deal.stage === 'Closing') { deadlines.push({ deal, date: getDaysFromCreated(deal.updatedAt || deal.createdAt, 3), label: 'Contract due', color, prob }); } else if (deal.stage === 'Outreach Sent') { deadlines.push({ deal, date: getDaysFromCreated(deal.updatedAt || deal.createdAt, 7), label: 'Follow-up if no response', color, prob }); } }); deadlines.sort((a, b) => a.date - b.date); const urgencyColors = { overdue: 'var(--red)', today: '#f97316', urgent: '#f97316', soon: 'var(--accent)', future: 'var(--muted)' }; const deadlineHtml = deadlines.slice(0, 8).map(({ deal, date, label, color, prob }) => { const { label: dateLabel, urgency, badge } = formatDeadlineDate(date); const gcLink = makeGoogleCalLink( label + ' — ' + deal.brand + ' (' + deal._athleteName + ')', date, deal.stage + ' deal worth $' + (deal.value || 0).toLocaleString() + '. Close probability: ' + prob + '%.' ); return `
${label}
${deal._athleteName} · ${deal.brand} · ${deal.stage} · ${prob}% close probability
${badge}
${dateLabel}
+ Google Cal
`; }).join(''); document.getElementById('cal-deadlines').innerHTML = deadlineHtml || '
No upcoming deadlines.
'; // All deals const dealsHtml = allDeals.map(deal => { const color = stageColors[deal.stage] || '#6b7280'; const prob = stageProb[deal.stage] || 10; const created = new Date(deal.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); return `
${deal.brand} — ${deal.campaign || 'No campaign set'}
${deal._athleteName} · Added ${created}
${deal.stage}
$${(deal.value||0).toLocaleString()} · ${prob}% close
`; }).join(''); document.getElementById('cal-deals').innerHTML = dealsHtml; } // ── INIT ───────────────────────────────────────────────────── // ── TEAM MATCH ─────────────────────────────────────────────── async function runTeamMatch() { if (!selectedAthleteId) { showToast('Select a client first'); return; } const btn = document.getElementById('tm-run-btn'); btn.disabled = true; btn.textContent = 'Scanning...'; document.getElementById('tm-results').innerHTML = '
Scanning college programs with AI...
'; document.getElementById('tm-summary').style.display = 'none'; document.getElementById('tm-playbook-block').classList.remove('visible'); document.getElementById('tm-playbook-text').textContent = ''; try { const r = await fetch(`${API_BASE}/api/ai/team-match`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ athleteId: selectedAthleteId, conference: document.getElementById('tm-conf').value, minNil: parseInt(document.getElementById('tm-minnil').value), sortBy: document.getElementById('tm-sort').value, }), }); const data = await r.json(); if (data.error) { document.getElementById('tm-results').innerHTML = '
❌ ' + data.error + '
'; return; } renderTeamMatch(data.teams); } catch(e) { document.getElementById('tm-results').innerHTML = '
❌ ' + e.message + '
'; } btn.disabled = false; btn.textContent = 'Scan Teams →'; } function fmtNil(n) { if (n >= 1000000) return '$' + (n/1000000).toFixed(1) + 'M'; if (n >= 1000) return '$' + Math.round(n/1000) + 'K'; return '$' + n; } function renderTeamMatch(teams) { if (!teams || !teams.length) { document.getElementById('tm-results').innerHTML = '
No matching programs found. Try adjusting your filters.
'; return; } const best = teams[0]; const avgNil = Math.round(teams.reduce((s,t) => s + (t.nilHigh||0), 0) / teams.length); const sumEl = document.getElementById('tm-summary'); sumEl.style.display = 'grid'; sumEl.style.gridTemplateColumns = 'repeat(auto-fit,minmax(120px,1fr))'; sumEl.style.gap = '10px'; sumEl.style.marginBottom = '20px'; sumEl.innerHTML = `
Programs found
${teams.length}
Best NIL offer
${fmtNil(best.nilHigh||0)}
Top fit score
${best.fitScore||'-'}
Avg NIL / yr
${fmtNil(avgNil)}
`; document.getElementById('tm-results').innerHTML = teams.map((t, i) => `
${i+1}
${t.name||'Unknown'} ${t.confLabel||t.conference||''} ${i===0?'Best fit':''}
${t.why||''}
${(t.nilBreakdown||[]).map(b=>`${b.label}: ${b.val}`).join('')}
${fmtNil(t.nilLow||0)}–${fmtNil(t.nilHigh||0)}
NIL / year
${t.fitScore||0}
`).join(''); } function toggleTmDetail(id) { const el = document.getElementById(id); if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none'; } async function buildTmPlaybook(schoolName, targetNil) { if (!selectedAthleteId) { showToast('Select a client first'); return; } const ath = athletes.find(a => a.id === selectedAthleteId); const block = document.getElementById('tm-playbook-block'); const txt = document.getElementById('tm-playbook-text'); const spinner = document.getElementById('tm-spinner'); const label = document.getElementById('tm-playbook-label'); block.classList.add('visible'); label.textContent = 'Negotiation Playbook — ' + schoolName; spinner.style.display = 'block'; txt.textContent = 'Building playbook...'; const msg = `Build a complete NIL recruitment negotiation playbook for ${ath ? ath.name : 'this athlete'} to sign with ${schoolName}. Target NIL: ${targetNil}/year. Give: 1. OPENING LINE — exact words the agent says to open negotiation 2. KEY LEVERAGE POINTS — 3 specific data points the agent should cite 3. PUSHBACK RESPONSE — what to say when ${schoolName} says they cannot meet the target 4. CONCESSION MOVE — what non-cash thing to offer to bridge the gap 5. WALK-AWAY LINE — exact sentence if they won't move Be word-for-word specific. This is for a real negotiation call.`; try { const r = await fetch(`${API_BASE}/api/ai/ask`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ athleteId: selectedAthleteId, message: msg }), }); const data = await r.json(); txt.textContent = data.response || 'No response.'; } catch(e) { txt.textContent = '❌ ' + e.message; } spinner.style.display = 'none'; block.scrollIntoView({ behavior: 'smooth', block: 'start' }); } window.addEventListener('DOMContentLoaded', checkSession); // ── ONBOARDING ───────────────────────────────────────────── let obAthleteId = null; function obStep(n) { document.querySelectorAll('.ob-step').forEach(s => s.style.display = 'none'); const step = document.getElementById('ob-step-' + n); if (step) step.style.display = 'block'; } async function obLookupPlayer() { const name = document.getElementById('ob-athlete-name').value.trim(); if (!name) { showToast('Enter athlete name first'); return; } const btn = document.getElementById('ob-lookup-btn'); const status = document.getElementById('ob-lookup-status'); btn.disabled = true; btn.textContent = 'Looking up...'; status.textContent = 'Searching for ' + name + '...'; try { const r = await fetch(`${API_BASE}/api/ai/player-lookup`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ name }) }); const data = await r.json(); if (data.found) { if (data.school) document.getElementById('ob-athlete-school').value = data.school; if (data.sport) document.getElementById('ob-athlete-sport').value = data.sport; if (data.position) document.getElementById('ob-athlete-pos').value = data.position; if (data.year) document.getElementById('ob-athlete-year').value = data.year; if (data.stats) document.getElementById('ob-athlete-stats').value = data.stats; if (data.instagram) document.getElementById('ob-athlete-ig').value = data.instagram; if (data.tiktok) document.getElementById('ob-athlete-tt').value = data.tiktok; if (data.engagement) document.getElementById('ob-athlete-eng').value = data.engagement; if (data.schoolTier) document.getElementById('ob-athlete-tier').value = data.schoolTier; status.textContent = '✅ Found! Review and save.'; status.style.color = 'var(--accent)'; } else { status.textContent = 'Not found — fill in manually.'; } } catch(e) { status.textContent = 'Lookup failed — fill in manually.'; } btn.disabled = false; btn.textContent = '⚡ AI Lookup'; } async function obAddSample() { document.getElementById('ob-athlete-name').value = 'Marcus Johnson'; document.getElementById('ob-athlete-school').value = 'University of Alabama'; document.getElementById('ob-athlete-sport').value = 'football'; document.getElementById('ob-athlete-pos').value = 'WR'; document.getElementById('ob-athlete-tier').value = 'p4-top10'; document.getElementById('ob-athlete-ig').value = '45000'; document.getElementById('ob-athlete-tt').value = '28000'; document.getElementById('ob-athlete-eng').value = '6.2'; document.getElementById('ob-athlete-year').value = 'Junior'; document.getElementById('ob-athlete-stats').value = '42 rec, 680 yds, 7 TD'; await obAddAthlete(); } async function obAddAthlete() { const name = document.getElementById('ob-athlete-name').value.trim(); if (!name) { showToast('Please enter an athlete name'); return; } try { const r = await fetch(`${API_BASE}/api/athletes`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ name, sport: document.getElementById('ob-athlete-sport').value, position: document.getElementById('ob-athlete-pos').value, school: document.getElementById('ob-athlete-school').value, schoolTier: document.getElementById('ob-athlete-tier').value || 'p4-mid', instagram: parseInt(document.getElementById('ob-athlete-ig').value) || 0, tiktok: parseInt(document.getElementById('ob-athlete-tt').value) || 0, engagement: parseFloat(document.getElementById('ob-athlete-eng').value) || 3.0, year: document.getElementById('ob-athlete-year').value, stats: document.getElementById('ob-athlete-stats').value, notes: document.getElementById('ob-athlete-notes').value, }) }); const data = await r.json(); if (!r.ok) { showToast('Error: ' + data.error); return; } obAthleteId = data.id; await loadAthletes(); selectedAthleteId = data.id; document.getElementById('activeAthlete').value = data.id; obStep(3); } catch(e) { showToast('Error: ' + e.message); } } async function obAddDeal() { const brand = document.getElementById('ob-deal-brand').value.trim(); if (!brand || !obAthleteId) { obSkipDeal(); return; } try { await fetch(`${API_BASE}/api/athletes/${obAthleteId}/deals`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ brand, value: parseInt(document.getElementById('ob-deal-value').value) || 0, stage: document.getElementById('ob-deal-stage').value, }) }); document.getElementById('ob-deal-check-icon').textContent = '✓'; document.getElementById('ob-deal-check-icon').style.color = 'var(--accent)'; document.getElementById('ob-deal-check-icon').style.fontWeight = '700'; } catch(e) {} obStep(4); } function obSkipDeal() { document.getElementById('ob-deal-check-icon').textContent = '–'; obStep(4); } async function obFinishRate() { obFinish(); setTimeout(function() { showView('rate', document.querySelector('.nav-item[onclick*="rate"]')); }, 300); } function obFinishScan() { obFinish(); setTimeout(function() { showView('deals', document.querySelector('.nav-item[onclick*="deals"]')); }, 300); } function obFinish() { document.getElementById('onboardingOverlay').style.display = 'none'; localStorage.setItem('nildash-onboarded-' + currentUser.id, '1'); await loadAthletes(); await loadKPIs(); await loadPipeline(); showView('rate', document.querySelector('.nav-item')); showToast('Welcome to NILDash! 🎉'); } function checkOnboarding() { const key = 'nildash-onboarded-' + currentUser.id; if (!localStorage.getItem(key) && athletes.length === 0) { document.getElementById('ob-name').textContent = currentUser.name.split(' ')[0]; document.getElementById('onboardingOverlay').style.display = 'flex'; obStep(1); } } function applyThemeUI() { const isLight = document.documentElement.getAttribute('data-theme') === 'light'; const icon = document.getElementById('theme-icon'); const label = document.getElementById('theme-label'); if (icon) icon.textContent = isLight ? '🌙' : '☀️'; if (label) label.textContent = isLight ? 'Dark Mode' : 'Light Mode'; } function toggleTheme() { const html = document.documentElement; const isLight = html.getAttribute('data-theme') === 'light'; if (isLight) { html.removeAttribute('data-theme'); document.getElementById('theme-icon').textContent = '☀️'; document.getElementById('theme-label').textContent = 'Light Mode'; localStorage.setItem('nildasTheme', 'dark'); } else { html.setAttribute('data-theme', 'light'); document.getElementById('theme-icon').textContent = '🌙'; document.getElementById('theme-label').textContent = 'Dark Mode'; localStorage.setItem('nildasTheme', 'light'); } } // Apply saved theme immediately (function() { const saved = localStorage.getItem('nildasTheme'); if (saved === 'light') { document.documentElement.setAttribute('data-theme', 'light'); } document.addEventListener('DOMContentLoaded', applyThemeUI); })();