Pending
—
Awaiting decision
⏳
Avg Score
—
AI quality score
⭐
Pending Approvals
—
Awaiting review
✅
| Client | Project | Industry | Template | Score | Outcome | Approval | Date |
|---|
Template
📋
Standard
Scope, timeline & pricing
⚙️
Technical
Architecture & stack
✨
Creative
Narrative & vision
Preview
Fill in the form and generate
Outcome
| Name | Email | Role | Status | Last Login | Created | Actions |
📄
Click or drag to upload
.docx files only
| Organisation | Slug | Plan | Users | Proposals | Status | Actions |
🔒
Password change requiredYou must change your password before using the app.
Preview
Signature preview appears here...
Quick templates:
Customize the emails sent for each event
| Status | To | Subject | Sent By | Date | Error |
// ═══════════════════════════════════════════════════════════════
// PART 1 NEW FEATURES
// ═══════════════════════════════════════════════════════════════
// ── Nav extension ─────────────────────────────────────────────
// nav() extended in original function below
// ── Must change password check ────────────────────────────────
function checkMustChangePwd() {
const raw = localStorage.getItem('cu');
if (!raw) return;
const u = JSON.parse(raw);
if (u.must_change_password) {
document.getElementById('modal-pwd').style.display = 'flex';
const b = document.getElementById('pwd-change-banner');
if (b) b.style.display = 'flex';
}
}
// ── Change password ────────────────────────────────────────────
async function doChangePassword() {
const cur = document.getElementById('pwd-current').value;
const nw = document.getElementById('pwd-new').value;
const cnf = document.getElementById('pwd-confirm').value;
if (!cur||!nw) { toast('All fields required','error'); return; }
if (nw !== cnf) { toast('Passwords do not match','error'); return; }
if (nw.length < 8) { toast('Min 8 characters','error'); return; }
try {
await put('/api/auth/change-password', {current_password:cur, new_password:nw});
toast('Password changed!','success');
closeM('modal-pwd');
const b = document.getElementById('pwd-change-banner');
if (b) b.style.display = 'none';
const cu = JSON.parse(localStorage.getItem('cu')||'{}');
cu.must_change_password = false;
localStorage.setItem('cu', JSON.stringify(cu));
} catch(e) { toast(e.message,'error'); }
}
// ── Profile ────────────────────────────────────────────────────
async function loadProfile() {
try {
const d = await get('/api/auth/me');
document.getElementById('prof-av').textContent = (d.full_name||'?')[0].toUpperCase();
document.getElementById('prof-name-disp').textContent = d.full_name||'';
document.getElementById('prof-email-disp').textContent = d.email||'';
document.getElementById('prof-role-badge').textContent = d.role||'';
document.getElementById('prof-fullname').value = d.full_name||'';
document.getElementById('prof-phone').value = d.phone||'';
document.getElementById('prof-title').value = d.job_title||'';
document.getElementById('prof-dept').value = d.department||'';
document.getElementById('prof-linkedin').value = d.linkedin_url||'';
document.getElementById('prof-bio').value = d.bio||'';
if (d.must_change_password) {
const b = document.getElementById('pwd-change-banner');
if (b) b.style.display = 'flex';
}
const sig = await get('/api/users/me/signature').catch(()=>({signature_html:''}));
document.getElementById('sig-html').value = sig.signature_html||'';
previewSig();
} catch(e) { toast('Profile load failed','error'); }
}
async function saveProfile() {
try {
await put('/api/users/'+currentUser.id, {
full_name: document.getElementById('prof-fullname').value,
phone: document.getElementById('prof-phone').value,
job_title: document.getElementById('prof-title').value,
department: document.getElementById('prof-dept').value,
linkedin_url: document.getElementById('prof-linkedin').value,
bio: document.getElementById('prof-bio').value,
});
const sigHtml = document.getElementById('sig-html').value;
await put('/api/users/me/signature', {signature_html:sigHtml, signature_text:''});
const cu = JSON.parse(localStorage.getItem('cu')||'{}');
cu.full_name = document.getElementById('prof-fullname').value;
localStorage.setItem('cu', JSON.stringify(cu));
document.getElementById('user-name').textContent = cu.full_name;
toast('Profile saved','success');
} catch(e) { toast(e.message,'error'); }
}
function previewSig() {
const html = document.getElementById('sig-html').value;
const el = document.getElementById('sig-preview');
el.innerHTML = html.trim()
? html
: 'Preview appears here...';
}
function insertSigTemplate(type) {
const u = currentUser||{};
const t = {
basic: `${u.full_name||'Your Name'}
${u.role||'Your Title'}
📞 Your Phone • ✉️ ${u.email||'your@email.com'}
`,
styled: `${u.full_name||'Your Name'} ${u.role||'Title'} | 📞 Phone ✉️ ${u.email||'email'} 🌐 www.yourcompany.com |
`
};
document.getElementById('sig-html').value = t[type]||'';
previewSig();
}
// ── SMTP ───────────────────────────────────────────────────────
async function loadSmtp() {
try {
const d = await get('/api/settings/smtp');
if (d && d.host) {
document.getElementById('smtp-host').value = d.host||'';
document.getElementById('smtp-port').value = d.port||587;
document.getElementById('smtp-user').value = d.username||'';
document.getElementById('smtp-fname').value = d.from_name||'';
document.getElementById('smtp-femail').value = d.from_email||'';
document.getElementById('smtp-tls').checked = d.use_tls !== 0;
}
} catch(e) {}
}
async function saveSmtp() {
const host = document.getElementById('smtp-host').value;
const femail = document.getElementById('smtp-femail').value;
const fname = document.getElementById('smtp-fname').value;
if (!host||!femail||!fname) { toast('Host, From Name and From Email required','error'); return; }
try {
await post('/api/settings/smtp', {
host, port: parseInt(document.getElementById('smtp-port').value)||587,
username: document.getElementById('smtp-user').value,
password: document.getElementById('smtp-pass').value,
use_tls: document.getElementById('smtp-tls').checked,
from_name: fname, from_email: femail,
});
toast('SMTP saved','success');
document.getElementById('smtp-pass').value = '';
} catch(e) { toast(e.message,'error'); }
}
async function testSmtp() {
try {
const r = await post('/api/settings/smtp/test', {});
toast(r.message,'success');
} catch(e) { toast(e.message,'error'); }
}
// ── Email Templates ────────────────────────────────────────────
let editingTplId = null;
const tplVarMap = {
welcome: ['full_name','email','temp_password','role','tenant_name','login_url'],
proposal_sent: ['client_name','project_title','sender_name','sender_title','tenant_name','custom_message','signature'],
approval_requested: ['approver_name','project_title','client_name','submitted_by','message','review_url'],
approve: ['creator_name','project_title','client_name','approver_name','comment','proposal_url'],
reject: ['creator_name','project_title','client_name','approver_name','comment','proposal_url'],
custom: ['client_name','project_title','sender_name','tenant_name','custom_message','signature'],
};
async function loadEmailTemplates() {
try {
const d = await get('/api/settings/email-templates');
const el = document.getElementById('email-tpl-list');
if (!d.templates.length) {
el.innerHTML = '📝
No templates yet
Click "+ New Template" to create one.
';
return;
}
el.innerHTML = d.templates.map(t => `${t.name}${t.is_default?'Default':''}
${t.template_type} · ${t.subject}
`).join('');
} catch(e) {}
}
function openEmailTplEditor(id) {
editingTplId = id;
if (!id) {
document.getElementById('etpl-editor-title').textContent = 'New Template';
document.getElementById('etpl-name').value = '';
document.getElementById('etpl-type').value = 'proposal_sent';
document.getElementById('etpl-subject').value = '';
document.getElementById('etpl-body').value = '';
document.getElementById('etpl-default').checked = false;
} else {
get('/api/settings/email-templates').then(d => {
const t = d.templates.find(x => x.id===id);
if (!t) return;
document.getElementById('etpl-editor-title').textContent = 'Edit Template';
document.getElementById('etpl-name').value = t.name;
document.getElementById('etpl-type').value = t.template_type;
document.getElementById('etpl-subject').value = t.subject;
document.getElementById('etpl-body').value = t.body_html;
document.getElementById('etpl-default').checked = t.is_default;
updateTplVarChips();
});
}
updateTplVarChips();
}
function updateTplVarChips() {
const type = document.getElementById('etpl-type').value;
const vars = tplVarMap[type]||[];
document.getElementById('etpl-vars').innerHTML = vars.map(v =>
`{{${v}}}`
).join('');
}
function insertVar(fieldId, text) {
const el = document.getElementById(fieldId);
if (!el) return;
const s = el.selectionStart;
el.value = el.value.substring(0,s) + text + el.value.substring(el.selectionEnd);
el.selectionStart = el.selectionEnd = s + text.length;
el.focus();
}
async function saveEmailTpl() {
const name=document.getElementById('etpl-name').value, type=document.getElementById('etpl-type').value,
subj=document.getElementById('etpl-subject').value, body=document.getElementById('etpl-body').value;
if (!name||!subj||!body) { toast('Name, subject and body required','error'); return; }
const payload = {name, template_type:type, subject:subj, body_html:body,
is_default: document.getElementById('etpl-default').checked, variables: tplVarMap[type]||[]};
try {
if (editingTplId) { await put('/api/settings/email-templates/'+editingTplId, payload); toast('Updated','success'); }
else { await post('/api/settings/email-templates', payload); toast('Created','success'); }
loadEmailTemplates(); resetEmailTplEditor();
} catch(e) { toast(e.message,'error'); }
}
function resetEmailTplEditor() {
editingTplId = null;
document.getElementById('etpl-editor-title').textContent = 'New Template';
['etpl-name','etpl-subject','etpl-body'].forEach(id => { const el=document.getElementById(id); if(el) el.value=''; });
const def=document.getElementById('etpl-default'); if(def) def.checked=false;
}
async function deleteEmailTpl(id) {
if (!confirm('Delete this template?')) return;
try { await del('/api/settings/email-templates/'+id); toast('Deleted','info'); loadEmailTemplates(); }
catch(e) { toast(e.message,'error'); }
}
// ── Email Logs ─────────────────────────────────────────────────
async function loadEmailLogs() {
try {
const d = await get('/api/email/logs');
const tb = document.getElementById('email-log-tbody');
if (!tb) return;
if (!d.logs.length) { tb.innerHTML='| No emails sent yet |
'; return; }
tb.innerHTML = d.logs.map(l => `| ${l.status} | ${l.to_email||'—'} | ${l.subject||'—'} | ${l.sender_name||'System'} | ${fmtD(l.sent_at)} | ${l.error_message||''} |
`).join('');
} catch(e) {}
}
// ── Settings tab switcher ──────────────────────────────────────
function switchStab(btn, panel) {
document.querySelectorAll('.stab').forEach(b=>b.classList.remove('active'));
document.querySelectorAll('.stab-panel').forEach(p=>p.classList.remove('active'));
btn.classList.add('active');
const el = document.getElementById('stab-'+panel);
if (el) el.classList.add('active');
if (panel==='email-logs') loadEmailLogs();
if (panel==='email-tpls') loadEmailTemplates();
}
// ── Send Email ─────────────────────────────────────────────────
let sendEmailPropId = null;
async function openSendEmailModal(propId) {
sendEmailPropId = propId;
try {
const p = await get('/api/proposals/'+propId);
document.getElementById('se-to').value = p.client_email||'';
document.getElementById('se-cc').value = '';
document.getElementById('se-custom').value = '';
document.getElementById('se-subject').value = `${p.project_title} — Proposal from ${currentUser.tenant_name||''}`;
document.getElementById('se-body').value = `Dear {{client_name}},
\nPlease find our proposal for {{project_title}} attached.
\n{{custom_message}}
\nBest regards,
{{sender_name}}
{{sender_title}}
\n{{signature}}
`;
document.getElementById('se-pdf').checked = true;
document.getElementById('se-docx').checked = false;
document.getElementById('extra-files-list').innerHTML = '';
// Load templates
const tpls = await get('/api/settings/email-templates');
const sel = document.getElementById('se-template');
sel.innerHTML = '';
tpls.templates.filter(t=>t.template_type==='proposal_sent'||t.template_type==='custom')
.forEach(t=>{ const o=document.createElement('option'); o.value=t.id; o.textContent=t.name; sel.appendChild(o); });
// Var chips
const chips = ['client_name','project_title','sender_name','sender_title','tenant_name','custom_message','signature'];
document.getElementById('se-var-chips').innerHTML = chips.map(v=>`{{${v}}}`).join('');
} catch(e) {}
document.getElementById('modal-send-email').style.display = 'flex';
}
async function loadEmailTplIntoComposer() {
const id = document.getElementById('se-template').value;
if (!id) return;
try {
const d = await get('/api/settings/email-templates');
const t = d.templates.find(x=>x.id===id);
if (t) {
document.getElementById('se-subject').value = t.subject;
document.getElementById('se-body').value = t.body_html;
}
} catch(e) {}
}
function showExtraFiles() {
const files = document.getElementById('se-extra-files').files;
document.getElementById('extra-files-list').innerHTML =
[...files].map(f=>`📎 ${f.name} (${(f.size/1024).toFixed(1)} KB)
`).join('');
}
async function doSendEmail() {
const to=document.getElementById('se-to').value, subj=document.getElementById('se-subject').value, body=document.getElementById('se-body').value;
if (!to||!subj||!body) { toast('To, Subject and Body required','error'); return; }
const btn=document.getElementById('send-email-btn');
btn.disabled=true; btn.textContent='Sending...';
const fd = new FormData();
fd.append('to_email',to); fd.append('cc_email',document.getElementById('se-cc').value);
fd.append('subject',subj); fd.append('body_html',body);
fd.append('custom_message',document.getElementById('se-custom').value);
fd.append('attach_pdf', document.getElementById('se-pdf').checked?'true':'false');
fd.append('attach_docx',document.getElementById('se-docx').checked?'true':'false');
const extra=document.getElementById('se-extra-files').files;
for (const f of extra) fd.append('extra_files',f);
try {
const r = await fetch(API+'/api/proposals/'+sendEmailPropId+'/send-email',
{method:'POST',headers:{Authorization:`Bearer ${authToken}`},body:fd});
if (!r.ok) { const e=await r.json(); throw new Error(e.detail||'Failed'); }
toast('Email sent!','success'); closeM('modal-send-email');
} catch(e) { toast(e.message,'error'); }
finally { btn.disabled=false; btn.textContent='Send Email'; }
}
// ── Versions ───────────────────────────────────────────────────
let versionsPropId = null;
async function openVersionsModal(propId) {
versionsPropId = propId;
document.getElementById('modal-versions').style.display = 'flex';
const el = document.getElementById('version-list');
el.innerHTML = '';
try {
const d = await get('/api/proposals/'+propId+'/versions');
if (!d.versions.length) { el.innerHTML='No versions yet.
'; return; }
const cur = d.versions[0].version_number;
el.innerHTML = d.versions.map(v=>`${v.version_number}
${v.change_summary||'Version '+v.version_number}${v.version_number===cur?'Current':''}
${v.changed_by_name||'—'} · ${fmtD(v.created_at)} · ${(v.word_count||0).toLocaleString()} words
${v.version_number!==cur?``:''}
`).join('');
} catch(e) { toast('Failed to load versions','error'); }
}
async function viewVersion(propId, vnum) {
try {
const v = await get('/api/proposals/'+propId+'/versions/'+vnum);
const w = window.open('','_blank');
const esc = JSON.stringify(v.content||'');
const sc='script';
const html=['Version '+vnum+'',
'<'+sc+' src="https://cdn.jsdelivr.net/npm/marked/marked.min.js">'+sc+'>',
'',
'Version '+vnum+' · '+(v.change_summary||'')+'
',
'',
'<'+sc+'>document.getElementById("c").innerHTML=marked.parse('+esc+');'+sc+'>',
''].join('');
w.document.write(html);
w.document.close();
} catch(e) { toast('Cannot open version','error'); }
}
async function restoreVersion(propId, vnum) {
if (!confirm('Restore version '+vnum+'? Current content saved first.')) return;
try {
const r = await post('/api/proposals/'+propId+'/versions/'+vnum+'/restore', {});
toast('Restored as v'+r.version_number,'success');
closeM('modal-versions');
openDetail(propId);
} catch(e) { toast(e.message,'error'); }
}
// ── Patch renderDetail to add Send Email + Versions buttons ────
// renderDetail() extended in original below
// showApp patched in original below
// ═══════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════
const API = '';
let authToken = null, currentUser = null;
let selTemplate = 'standard', selToneV = 'professional';
let streamContent = '', isStreaming = false, curPropId = null;
let detailData = null, detailId = null;
let charts = {}, searchTimer = null;
let editUserId = null, pendingApprovalAction = null, pendingApprovalId = null;
// ═══════════════════════════════════════════════════════════════
// THEME
// ═══════════════════════════════════════════════════════════════
function toggleTheme(){
const d = document.documentElement;
d.dataset.theme = d.dataset.theme==='dark'?'light':'dark';
localStorage.setItem('theme',d.dataset.theme);
setTimeout(()=>loadDashboard(),50);
}
(()=>{const s=localStorage.getItem('theme');if(s)document.documentElement.dataset.theme=s;})();
// ═══════════════════════════════════════════════════════════════
// AUTH
// ═══════════════════════════════════════════════════════════════
async function doLogin(){
const email=v('l-email'), pass=v('l-pass'), slug=v('l-slug');
if(!email||!pass){showErr('Email and password required');return;}
const btn=document.getElementById('login-btn');
btn.disabled=true; btn.textContent='Signing in…';
const err=document.getElementById('login-error'); err.style.display='none';
try{
const r=await post('/api/auth/login',{email,password:pass,tenant_slug:slug||null});
authToken=r.access_token; currentUser=r;
localStorage.setItem('pat',authToken);
localStorage.setItem('cu',JSON.stringify(r));
showApp();
}catch(e){showErr(e.message||'Login failed');}
finally{btn.disabled=false;btn.textContent='Sign In →';}
}
function showErr(m){const e=document.getElementById('login-error');e.textContent=m;e.style.display='block';}
function doLogout(){authToken=null;currentUser=null;localStorage.removeItem('pat');localStorage.removeItem('cu');document.getElementById('app-shell').style.display='none';document.getElementById('login-page').style.display='flex';}
function showApp(){
document.getElementById('login-page').style.display='none';
const shell=document.getElementById('app-shell'); shell.style.display='grid';
const u=currentUser;
document.getElementById('tenant-badge').textContent=u.tenant_name;
document.getElementById('user-name').textContent=u.full_name;
document.getElementById('user-role').textContent=u.role;
document.getElementById('user-av').textContent=(u.full_name||'U')[0].toUpperCase();
const isAdmin=u.role==='admin'||u.role==='super_admin';
const isSA=u.role==='super_admin';
document.querySelectorAll('.admin-section').forEach(e=>e.style.display=isAdmin?'':'none');
document.querySelectorAll('.super-section').forEach(e=>e.style.display=isSA?'':'none');
checkHealth(); loadDocxTemplatesDropdown();
nav('dashboard');
setTimeout(checkMustChangePwd, 600);
}
(()=>{
const t=localStorage.getItem('pat'),u=localStorage.getItem('cu');
if(t&&u){authToken=t;currentUser=JSON.parse(u);showApp();}
})();
// ═══════════════════════════════════════════════════════════════
// API HELPERS
// ═══════════════════════════════════════════════════════════════
function hdrs(extra={}){return{...extra,'Content-Type':'application/json',...(authToken?{Authorization:`Bearer ${authToken}`}:{})}}
async function get(url){const r=await fetch(API+url,{headers:hdrs()});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||`HTTP ${r.status}`)}return r.json()}
async function post(url,body){const r=await fetch(API+url,{method:'POST',headers:hdrs(),body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||`HTTP ${r.status}`)}return r.json()}
async function put(url,body){const r=await fetch(API+url,{method:'PUT',headers:hdrs(),body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||`HTTP ${r.status}`)}return r.json()}
async function del(url){const r=await fetch(API+url,{method:'DELETE',headers:hdrs()});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||`HTTP ${r.status}`)}return r.json()}
function v(id){return document.getElementById(id)?.value||''}
// ═══════════════════════════════════════════════════════════════
// NAVIGATION
// ═══════════════════════════════════════════════════════════════
function nav(view){
document.querySelectorAll('.view').forEach(e=>e.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(e=>e.classList.remove('active'));
const el=document.getElementById('view-'+view); if(el)el.classList.add('active');
const ni=document.querySelector(`[data-view="${view}"]`); if(ni)ni.classList.add('active');
document.getElementById('main-content').scrollTo(0,0);
if(view==='dashboard')loadDashboard();
if(view==='history')loadHistory();
if(view==='approvals')loadApprovals('queue');
if(view==='users')loadUsers();
if(view==='templates')loadTemplates();
if(view==='license')loadLicense();
if(view==='tenants')loadTenants();
if(view==='profile')loadProfile();
if(view==='settings'){loadSmtp();loadEmailTemplates();loadEmailLogs();}
}
// ═══════════════════════════════════════════════════════════════
// HEALTH
// ═══════════════════════════════════════════════════════════════
async function checkHealth(){
try{const d=await get('/health');
const dot=document.getElementById('az-dot'),lbl=document.getElementById('az-label');
if(d.azure_ready){dot.className='sdot ok';lbl.textContent='Azure Connected';}
else{dot.className='sdot err';lbl.textContent='Azure Not Configured';}
}catch{document.getElementById('az-dot').className='sdot err';document.getElementById('az-label').textContent='Backend Offline';}
}
// ═══════════════════════════════════════════════════════════════
// DASHBOARD
// ═══════════════════════════════════════════════════════════════
async function loadDashboard(){
try{
const d=await get('/api/dashboard/stats');
const won=d.won||0,total=d.total_proposals||0,decided=won+(d.lost||0);
si('k-total',total); si('k-won',won); si('k-lost',d.lost||0);
si('k-pend',d.pending||0); si('k-wr',(d.win_rate||0)+'%');
si('k-rev','$'+fmtN(d.total_revenue||0));
const sc=document.getElementById('k-score'); sc.textContent=d.avg_score||'—';
if(d.avg_score>=7.5)sc.className='kpi-v gn';else if(d.avg_score>=5)sc.className='kpi-v am';
si('k-approv',d.pending_approvals||0);
si('k-won-s',`${total>0?Math.round(won/total*100):0}% of total`);
si('k-lost-s',`${total>0?Math.round((d.lost||0)/total*100):0}% of total`);
document.getElementById('badge-total').textContent=total;
const ba=document.getElementById('badge-approvals');
if(d.pending_approvals>0){ba.textContent=d.pending_approvals;ba.style.display='flex';}else ba.style.display='none';
buildCharts(d); buildTopClients(d.top_clients||[]); buildDashQueue(d.approval_queue||[]);
buildRecentRows(d.recent_activity||[]);
}catch(e){console.error('Dashboard:',e);}
}
function si(id,v){const e=document.getElementById(id);if(e)e.textContent=v;}
function fmtN(n){if(!n)return'0';if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return Math.round(n).toLocaleString();}
function fmtD(iso){if(!iso)return'—';return new Date(iso).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});}
function cssV(n){return getComputedStyle(document.documentElement).getPropertyValue(n).trim();}
function sPill(v){if(!v)return`—`;const c=v>=7.5?'hi':v>=5?'md':'lo';return`${v.toFixed(1)}`;}
function buildCharts(d){
Object.values(charts).forEach(c=>c.destroy()); charts={};
Chart.defaults.color=cssV('--tx2'); Chart.defaults.borderColor=cssV('--bd'); Chart.defaults.font.family="'DM Sans',sans-serif";
const ac=cssV('--ac'),gn=cssV('--gn'),rd=cssV('--rd'),am=cssV('--am'),bl=cssV('--bl'),bd=cssV('--bd');
const months=d.proposals_by_month||[];
charts.m=new Chart(document.getElementById('ch-monthly'),{type:'bar',data:{labels:months.map(m=>m.month),datasets:[{label:'Won',data:months.map(m=>m.won),backgroundColor:gn+'99',borderColor:gn,borderWidth:1,borderRadius:4},{label:'Lost',data:months.map(m=>m.lost),backgroundColor:rd+'99',borderColor:rd,borderWidth:1,borderRadius:4},{label:'Pending',data:months.map(m=>m.total-m.won-m.lost),backgroundColor:am+'99',borderColor:am,borderWidth:1,borderRadius:4}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'top',labels:{boxWidth:10,font:{size:11}}}},scales:{x:{stacked:true,grid:{display:false}},y:{stacked:true,grid:{color:bd},ticks:{stepSize:1}}}}});
charts.p=new Chart(document.getElementById('ch-pie'),{type:'doughnut',data:{labels:['Won','Lost','Pending'],datasets:[{data:[d.won,d.lost,d.pending],backgroundColor:[gn,rd,am],borderColor:cssV('--sf'),borderWidth:3,hoverOffset:5}]},options:{responsive:true,maintainAspectRatio:false,cutout:'68%',plugins:{legend:{position:'bottom',labels:{boxWidth:10,font:{size:11},padding:10}}}}});
const ind=d.proposals_by_industry||[];
charts.i=new Chart(document.getElementById('ch-ind'),{type:'bar',data:{labels:ind.map(i=>i.industry.length>14?i.industry.slice(0,13)+'…':i.industry),datasets:[{label:'Total',data:ind.map(i=>i.total),backgroundColor:ac+'88',borderColor:ac,borderWidth:1,borderRadius:4},{label:'Won',data:ind.map(i=>i.won),backgroundColor:gn+'99',borderColor:gn,borderWidth:1,borderRadius:4}]},options:{responsive:true,maintainAspectRatio:false,indexAxis:'y',plugins:{legend:{position:'top',labels:{boxWidth:10,font:{size:11}}}},scales:{x:{grid:{color:bd}},y:{grid:{display:false}}}}});
const tpls=d.proposals_by_template||[];const tclrs=[ac,gn,bl,am];
charts.t=new Chart(document.getElementById('ch-tpl'),{type:'polarArea',data:{labels:tpls.map(t=>t.template_type.charAt(0).toUpperCase()+t.template_type.slice(1)),datasets:[{data:tpls.map(t=>t.total),backgroundColor:tclrs.map(c=>c+'bb'),borderColor:tclrs,borderWidth:2}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{boxWidth:10,font:{size:11},padding:8}}},scales:{r:{grid:{color:bd},ticks:{display:false}}}}});
}
function buildTopClients(cl){
const el=document.getElementById('top-clients-body');
if(!cl.length){el.innerHTML='';return;}
el.innerHTML=cl.map(c=>`${c.client_name}
${c.proposals} proposal${c.proposals>1?'s':''} · ${c.won} won
${c.revenue>0?'$'+fmtN(c.revenue):'—'}
`).join('');
}
function buildDashQueue(q){
const el=document.getElementById('dash-approval-queue');
if(!q.length){el.innerHTML='No proposals awaiting approval
';return;}
el.innerHTML=q.map(p=>`${p.project_title}
${p.client_name} · ${p.creator_name||'—'}
${p.approval_status.replace('_',' ')}
`).join('');
}
function buildRecentRows(rows){
const tb=document.getElementById('dash-recent');
if(!rows.length){tb.innerHTML='| No proposals yet |
';return;}
tb.innerHTML=rows.map(r=>`| ${r.client_name} | ${r.project_title} | ${r.industry} | ${r.template_type} | ${sPill(r.score_value)} | ${r.outcome} | ${(r.approval_status||'draft').replace('_',' ')} | ${fmtD(r.created_at)} |
`).join('');
}
// ═══════════════════════════════════════════════════════════════
// GENERATOR
// ═══════════════════════════════════════════════════════════════
function selTpl(el){document.querySelectorAll('.tpl-card').forEach(c=>c.classList.remove('active'));el.classList.add('active');selTemplate=el.dataset.id;}
function selTone(el){document.querySelectorAll('.tone-btn').forEach(b=>b.classList.remove('active'));el.classList.add('active');selToneV=el.dataset.tone;}
async function loadDocxTemplatesDropdown(){
try{const d=await get('/api/templates/docx');const sel=document.getElementById('f-docx-tpl');
d.templates.forEach(t=>{const o=document.createElement('option');o.value=t.id;o.textContent=t.name+(t.is_default?' (default)':'');sel.appendChild(o);});
}catch{}
}
async function doGenerate(){
const client=v('f-client'),title=v('f-title'),industry=v('f-industry'),scope=v('f-scope');
if(!client||!title||!industry||!scope){toast('Fill in all required fields','error');return;}
if(isStreaming)return;
const custom=v('f-custom').trim().split(',').map(s=>s.trim()).filter(Boolean);
const payload={client_name:client,project_title:title,industry,project_scope:scope,
budget_range:v('f-budget')||null,timeline:v('f-timeline')||null,
company_info:v('f-company')||null,client_email:v('f-email')||null,
template_type:selTemplate,tone:selToneV,
docx_template_id:v('f-docx-tpl')||null,
custom_sections:custom.length?custom:null};
isStreaming=true; streamContent=''; curPropId=null;
const btn=document.getElementById('btn-gen'); btn.disabled=true;
document.getElementById('btn-gen').innerHTML=' Generating…';
document.getElementById('prev-acts').style.display='none';
document.getElementById('sec-nav-bar').style.display='none';
document.getElementById('score-panel').style.display='none';
document.getElementById('prev-meta').textContent='Streaming…';
document.getElementById('prev-scroll').innerHTML='';
let rendered=false; const mdEl=document.createElement('div'); mdEl.className='prop-md stream-cursor';
try{
const resp=await fetch(API+'/api/generate-proposal',{method:'POST',headers:hdrs(),body:JSON.stringify(payload)});
if(!resp.ok){const e=await resp.json().catch(()=>({}));throw new Error(e.detail||`HTTP ${resp.status}`);}
const reader=resp.body.getReader(); const dec=new TextDecoder(); let buf='';
while(true){
const{done,value}=await reader.read(); if(done)break;
buf+=dec.decode(value,{stream:true}); const lines=buf.split('\n'); buf=lines.pop();
for(const line of lines){
if(!line.startsWith('data: '))continue;
try{
const ev=JSON.parse(line.slice(6));
if(ev.error)throw new Error(ev.error);
if(ev.chunk){streamContent+=ev.chunk;if(!rendered){document.getElementById('prev-scroll').innerHTML='';document.getElementById('prev-scroll').appendChild(mdEl);rendered=true;}mdEl.innerHTML=marked.parse(streamContent);const ps=document.getElementById('prev-scroll');ps.scrollTop=ps.scrollHeight;}
if(ev.done){
curPropId=ev.proposal_id; mdEl.classList.remove('stream-cursor');
const secs=ev.sections||[];
if(secs.length){const nb=document.getElementById('sec-nav-bar');nb.style.display='flex';nb.innerHTML=secs.map(s=>`${s}`).join('');}
document.getElementById('prev-meta').textContent=`${(ev.word_count||0).toLocaleString()} words · ${(ev.sections||[]).length} sections · ${ev.proposal_id}`;
document.getElementById('prev-acts').style.display='flex';
// Show approval submit btn if feature enabled
if(currentUser?.features?.approval_workflow){document.getElementById('btn-submit-appr').style.display='flex';}
if(!currentUser?.features?.docx){document.getElementById('btn-docx').style.display='none';}
toast('Proposal generated!','success'); loadBadge();
}
}catch(e){if(e.message!=='Unexpected end of JSON input')throw e;}
}
}
}catch(e){document.getElementById('prev-scroll').innerHTML=`✕
Generation Failed
${e.message}
`;toast('Error: '+e.message,'error');}
finally{isStreaming=false;btn.disabled=false;btn.innerHTML='✦ Generate Proposal';}
}
function scrToSec(name){const els=[...document.querySelectorAll('.prop-md h2')];const f=els.find(h=>h.textContent.includes(name));if(f)f.scrollIntoView({behavior:'smooth',block:'start'});}
async function cpMd(){if(!streamContent)return;await navigator.clipboard.writeText(streamContent);toast('Copied!','success');}
function dlMd(){if(!streamContent)return;const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([streamContent],{type:'text/markdown'}));a.download=`proposal-${curPropId||'draft'}.md`;a.click();toast('Downloaded','success');}
function dlDocx(){if(!curPropId)return;window.open(API+`/api/proposals/${curPropId}/export/docx`,'_blank');toast('DOCX download started','info');}
async function doScore(){
if(!curPropId){toast('Generate first','error');return;}
toast('Scoring…','info');
try{const s=await post(`/api/proposals/${curPropId}/score`,{});renderScore(s,'score-content');document.getElementById('score-panel').style.display='block';toast('Score ready!','success');}
catch(e){toast('Scoring failed: '+e.message,'error');}
}
function renderScore(s,containerId){
const ov=s.overall||0;const col=ov>=7.5?'var(--gn)':ov>=5?'var(--am)':'var(--rd)';
const bars=['clarity','completeness','roi_focus','specificity','persuasiveness'];
const labs={clarity:'Clarity',completeness:'Completeness',roi_focus:'ROI Focus',specificity:'Specificity',persuasiveness:'Persuasiveness'};
document.getElementById(containerId).innerHTML=`
${ov.toFixed(1)}
/ 10 Overall
"${s.summary||''}"
${bars.map(k=>`
${labs[k]}${(s[k]||0).toFixed(1)} `).join('')}
${s.suggestions?.length?`Suggestions
${s.suggestions.map(sg=>`
→${sg}
`).join('')}
`:''}`;
}
async function doShare(){
if(!curPropId){toast('Generate first','error');return;}
try{const r=await post(`/api/proposals/${curPropId}/share`,{});const url=`${location.origin}/?share=${r.share_token}`;await navigator.clipboard.writeText(url);toast('Share link copied!','success');}
catch(e){toast(e.message,'error');}
}
function doPrint(){if(!streamContent)return;const w=window.open('','_blank');w.document.write(`Proposal${marked.parse(streamContent)}
📧 Send Proposal by Email
✕
Variables:
📎 Click to attach more files
`);w.document.close();w.print();}
function doSubmitApproval(){if(!curPropId){toast('Generate first','error');return;}openSubmitModal(curPropId);}
async function openSubmitModal(pid){
pendingApprovalId=pid;
try{const d=await get('/api/users');const sel=document.getElementById('submit-approver');sel.innerHTML='';
(d.users||[]).filter(u=>['approver','admin'].includes(u.role)&&u.is_active).forEach(u=>{const o=document.createElement('option');o.value=u.id;o.textContent=`${u.full_name} (${u.role})`;sel.appendChild(o);});
}catch{}
document.getElementById('modal-submit').style.display='flex';
}
async function confirmSubmit(){
const aid=v('submit-approver'),msg=v('submit-msg');
try{await post(`/api/proposals/${pendingApprovalId}/submit`,{assigned_approver_id:aid||null,message:msg||null});toast('Submitted for approval!','success');closeM('modal-submit');}
catch(e){toast(e.message,'error');}
}
// ═══════════════════════════════════════════════════════════════
// HISTORY
// ═══════════════════════════════════════════════════════════════
async function loadHistory(){
const s=v('hist-search'),o=v('hist-outcome'),a=v('hist-appr');
const q=new URLSearchParams(); if(s)q.set('search',s); if(o)q.set('outcome',o); if(a)q.set('approval_status',a); q.set('limit','100');
const el=document.getElementById('hist-cards'); el.innerHTML='';
try{const d=await get('/api/proposals?'+q);
document.getElementById('hist-count').textContent=`${d.total} proposal${d.total!==1?'s':''} total`;
if(!d.proposals.length){el.innerHTML=`📁
No proposals
Generate your first proposal or adjust filters.
`;return;}
el.innerHTML=d.proposals.map(p=>`${p.project_title}
📌 ${p.client_name}
${p.industry}📅 ${fmtD(p.created_at)}📝 ${(p.word_count||0).toLocaleString()}w${p.template_type}
${p.outcome}${(p.approval_status||'draft').replace('_',' ')}${sPill(p.score_value)}${p.revenue_value?`$${fmtN(p.revenue_value)}`:''}
`).join('');
}catch(e){el.innerHTML=``;}
}
function debSearch(){clearTimeout(searchTimer);searchTimer=setTimeout(loadHistory,350);}
async function loadBadge(){try{const d=await get('/api/proposals?limit=1');document.getElementById('badge-total').textContent=d.total;}catch{}}
// ═══════════════════════════════════════════════════════════════
// DETAIL
// ═══════════════════════════════════════════════════════════════
async function openDetail(id){
detailId=id; nav('detail');
document.getElementById('det-content').innerHTML='';
try{const d=await get(`/api/proposals/${id}`); detailData=d; renderDetail(d);}
catch(e){toast('Failed to load: '+e.message,'error');}
}
function renderDetail(d){
document.getElementById('det-title').textContent=d.project_title;
document.getElementById('det-sub').textContent=`${d.client_name} · ${d.industry} · ${fmtD(d.created_at)}`;
document.getElementById('det-content').innerHTML=`${marked.parse(d.content||'_No content_')}
`;
// Inject Send Email + Versions buttons
setTimeout(()=>{
const acts=document.querySelector('#view-detail .page-header > div:last-child');
if(acts){
['_btn_email','_btn_ver'].forEach(id=>{const old=document.getElementById(id);if(old)old.remove();});
const be=document.createElement('button');
be.id='_btn_email';be.className='btn btn-p btn-sm';
be.innerHTML='📧 Email';
be.onclick=()=>openSendEmailModal(d.id);
acts.insertBefore(be,acts.firstChild);
const bv=document.createElement('button');
bv.id='_btn_ver';bv.className='btn btn-g btn-sm';
bv.innerHTML='📂 Versions';
bv.onclick=()=>openVersionsModal(d.id);
acts.insertBefore(bv,acts.firstChild);
}
},50);
// Meta card
document.getElementById('det-meta-card').innerHTML=`Details
${ir('ID',d.id.slice(0,16)+'…')}${ir('Template',d.template_type)}${ir('Tone',d.tone)}
${ir('Industry',d.industry)}${ir('Budget',d.budget_range||'—')}${ir('Timeline',d.timeline||'—')}
${ir('Words',(d.word_count||0).toLocaleString())}${ir('Created by',d.created_by_name||'—')}
${ir('Created',fmtD(d.created_at))}`;
// Outcome
document.querySelectorAll('.outcome-btn').forEach(b=>{b.className='outcome-btn';if(b.dataset.o===d.outcome)b.classList.add('a'+d.outcome[0]);});
document.getElementById('det-rev').value=d.revenue_value||'';
document.getElementById('det-notes').value=d.notes||'';
// Approval status bar
buildApprovalBar(d);
buildApprovalActions(d);
buildApprovalEvents(d.approval_events||[]);
// Score
if(d.score_breakdown&&typeof d.score_breakdown==='object'){
const sc=document.getElementById('det-score-area');
sc.innerHTML=``;
renderScore(d.score_breakdown,'det-sc-inner');
}
// Share
if(d.share_token){document.getElementById('det-share-card').style.display='block';document.getElementById('det-share-link').textContent=`${location.origin}/?share=${d.share_token}`;}
}
function ir(k,v){return`${k}${v}
`;}
function buildApprovalBar(d){
const STEPS=['draft','submitted','in_review','approved','published'];
const el=document.getElementById('det-approval-bar');
const cur=d.approval_status;
const rejected=cur==='rejected';
el.innerHTML=`
${STEPS.map((s,i)=>{let cls='asdot';if(rejected&&i>0)cls+=' rejected';else if(STEPS.indexOf(cur)>i)cls+=' done';else if(s===cur)cls+=' active';return `
${i
›':''}`}).join('')}
${rejected?'⛔ Rejected':''}
`;
}
function buildApprovalActions(d){
const el=document.getElementById('det-appr-actions');
const isApprover=currentUser&&['approver','admin','super_admin'].includes(currentUser.role);
const isEditor=currentUser&&['editor','approver','admin','super_admin'].includes(currentUser.role);
const hasFeature=currentUser?.features?.approval_workflow;
if(!hasFeature){el.innerHTML='Upgrade to Professional or Enterprise plan to use approval workflows.
';return;}
let btns='';
if(['draft','rejected'].includes(d.approval_status)&&isEditor)
btns+=``;
if(d.approval_status==='submitted'&&isApprover)
btns+=``;
if(d.approval_status==='in_review'&&isApprover){
btns+=``;
btns+=``;
btns+=``;
}
if(d.approval_status==='approved'&&isApprover)
btns+=``;
el.innerHTML=btns||`Status: ${d.approval_status}
`;
}
function buildApprovalEvents(events){
const el=document.getElementById('det-events');
if(!events.length){el.innerHTML='No approval activity yet
';return;}
el.innerHTML=events.map(e=>`${e.action.replace(/_/g,' ')}
by ${e.actor_name||'System'} · ${fmtD(e.created_at)}
${e.comment?``:''}
`).join('');
}
function apprAction(pid,action){
pendingApprovalId=pid; pendingApprovalAction=action;
const titles={approve:'Approve Proposal',reject:'Reject Proposal',request_revision:'Request Revision',start_review:'Start Review',publish:'Publish Proposal'};
document.getElementById('act-title').textContent=titles[action]||action;
document.getElementById('act-comment').value='';
const btn=document.getElementById('act-confirm');
btn.onclick=async()=>{
try{await post(`/api/proposals/${pendingApprovalId}/approve-action`,{action:pendingApprovalAction,comment:v('act-comment')||null});
toast(`Action '${pendingApprovalAction}' applied`,'success'); closeM('modal-action');
openDetail(pendingApprovalId);
}catch(e){toast(e.message,'error');}
};
document.getElementById('modal-action').style.display='flex';
}
async function setOutcome(o){
if(!detailId)return;
document.querySelectorAll('.outcome-btn').forEach(b=>{b.className='outcome-btn';if(b.dataset.o===o)b.classList.add('a'+o[0]);});
await saveOutcome(o);
}
async function saveOutcome(outcome){
if(!detailId)return;
const o=outcome||detailData?.outcome||'pending';
const rev=parseFloat(v('det-rev'))||null, notes=v('det-notes');
try{await put(`/api/proposals/${detailId}/outcome`,{outcome:o,revenue_value:rev,notes});if(outcome)toast('Outcome updated','success');}catch{}
}
async function detDocx(){if(!detailId)return;window.open(API+`/api/proposals/${detailId}/export/docx`,'_blank');toast('DOCX download started','info');}
async function detScore(){
if(!detailId)return; toast('Scoring…','info');
try{const s=await post(`/api/proposals/${detailId}/score`,{});const sa=document.getElementById('det-score-area');sa.innerHTML=``;renderScore(s,'det-sc2');toast('Score ready!','success');}
catch(e){toast(e.message,'error');}
}
async function detShare(){
if(!detailId)return;
try{const r=await post(`/api/proposals/${detailId}/share`,{});const url=`${location.origin}/?share=${r.share_token}`;await navigator.clipboard.writeText(url);document.getElementById('det-share-card').style.display='block';document.getElementById('det-share-link').textContent=url;toast('Share link copied!','success');}
catch(e){toast(e.message,'error');}
}
function cpShareLink(){const t=document.getElementById('det-share-link').textContent;navigator.clipboard.writeText(t);toast('Copied!','success');}
function detPrint(){if(!detailData?.content)return;const w=window.open('','_blank');w.document.write(`${detailData.project_title}${marked.parse(detailData.content)}`);w.document.close();w.print();}
async function detDelete(){if(!detailId||!confirm('Delete this proposal?'))return;try{await del(`/api/proposals/${detailId}`);toast('Deleted','info');nav('history');}catch(e){toast(e.message,'error');}}
// ═══════════════════════════════════════════════════════════════
// APPROVALS VIEW
// ═══════════════════════════════════════════════════════════════
let apprTab='queue';
async function loadApprovals(tab){
apprTab=tab||'queue';
const el=document.getElementById('approval-list');
el.innerHTML='';
document.getElementById('tab-queue').className=apprTab==='queue'?'btn btn-p btn-sm':'btn btn-g btn-sm';
document.getElementById('tab-ahist').className=apprTab==='history'?'btn btn-p btn-sm':'btn btn-g btn-sm';
try{
const endpoint=apprTab==='queue'?'/api/approvals/queue':'/api/approvals/history';
const d=await get(endpoint); const items=d.queue||d.history||[];
if(!items.length){el.innerHTML=`✅
${apprTab==='queue'?'Queue is clear':'No history yet'}
${apprTab==='queue'?'No proposals awaiting review.':'Approved and rejected proposals will appear here.'}
`;return;}
el.innerHTML=items.map(p=>`${p.project_title}
${p.client_name} · ${p.creator_name||'—'}
${(p.approval_status||'').replace('_',' ')}${p.industry}📅 ${fmtD(p.submitted_at||p.created_at)}
${apprTab==='queue'?``:''}
`).join('');
}catch(e){el.innerHTML=``;}
}
// ═══════════════════════════════════════════════════════════════
// USERS
// ═══════════════════════════════════════════════════════════════
async function loadUsers(){
try{const d=await get('/api/users');
document.getElementById('users-sub').textContent=`${d.users.length} member${d.users.length!==1?'s':''} · ${d.license.max_users===-1?'Unlimited':d.license.max_users} seats`;
const tb=document.getElementById('users-tbody');
tb.innerHTML=d.users.map(u=>`| ${u.full_name} | ${u.email} | ${u.role} | ${u.is_active?'Active':'Inactive'} | ${fmtD(u.last_login)} | ${fmtD(u.created_at)} | |
`).join('');
}catch(e){toast('Failed to load users: '+e.message,'error');}
}
function openAddUser(){editUserId=null;document.getElementById('mu-title').textContent='Add User';document.getElementById('mu-name').value='';document.getElementById('mu-email').value='';document.getElementById('mu-pass').value='';document.getElementById('mu-role').value='editor';document.getElementById('modal-user').style.display='flex';}
function openEditUser(id,name,email,role){editUserId=id;document.getElementById('mu-title').textContent='Edit User';document.getElementById('mu-name').value=name;document.getElementById('mu-email').value=email;document.getElementById('mu-pass').value='';document.getElementById('mu-role').value=role;document.getElementById('modal-user').style.display='flex';}
async function saveUser(){
const name=v('mu-name'),email=v('mu-email'),pass=v('mu-pass'),role=v('mu-role');
if(!name||!email){toast('Name and email required','error');return;}
try{
if(editUserId){await put(`/api/users/${editUserId}`,{full_name:name,role,password:pass||undefined});}
else{if(!pass){toast('Password required for new user','error');return;}await post('/api/users',{email,full_name:name,password:pass,role});}
toast(editUserId?'User updated':'User created','success');closeM('modal-user');loadUsers();
}catch(e){toast(e.message,'error');}
}
// ═══════════════════════════════════════════════════════════════
// DOCX TEMPLATES
// ═══════════════════════════════════════════════════════════════
async function loadTemplates(){
try{const d=await get('/api/templates/docx');
const ph=document.getElementById('ph-list');
if(ph)ph.innerHTML=d.placeholders.map(p=>`${p}`).join('
');
const el=document.getElementById('tpl-list');
if(!d.templates.length){el.innerHTML='📄
No templates yet
Upload a branded .docx template with placeholders like {{CLIENT_NAME}} and {{PROPOSAL_CONTENT}}.
';return;}
el.innerHTML=d.templates.map(t=>`${t.name}${t.is_default?'Default':''}
${t.description||'No description'}
Uploaded: ${fmtD(t.created_at)}
${!t.is_default?``:'✓ Default'}
`).join('');
}catch(e){toast('Load error: '+e.message,'error');}
}
function fileChosen(input){const f=input.files[0];if(f)document.getElementById('file-chosen').textContent='📄 '+f.name;}
function setupFileDrop(){const drop=document.getElementById('file-drop');if(!drop)return;drop.addEventListener('dragover',e=>{e.preventDefault();drop.classList.add('drag');});drop.addEventListener('dragleave',()=>drop.classList.remove('drag'));drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('drag');const f=e.dataTransfer.files[0];if(f&&f.name.endsWith('.docx')){document.getElementById('tpl-file').files=e.dataTransfer.files;document.getElementById('file-chosen').textContent='📄 '+f.name;}else toast('Only .docx files accepted','error');});}
async function uploadTemplate(){
const name=v('tpl-name'),file=document.getElementById('tpl-file').files[0];
if(!name){toast('Template name required','error');return;}
if(!file){toast('Select a .docx file','error');return;}
const fd=new FormData(); fd.append('name',name); fd.append('description',v('tpl-desc')); fd.append('is_default',document.getElementById('tpl-default').checked?'true':'false'); fd.append('file',file);
try{const r=await fetch(API+'/api/templates/docx',{method:'POST',headers:{Authorization:`Bearer ${authToken}`},body:fd});if(!r.ok){const e=await r.json();throw new Error(e.detail);}toast('Template uploaded!','success');loadTemplates();loadDocxTemplatesDropdown();}
catch(e){toast(e.message,'error');}
}
async function setDefaultTpl(id){try{await put(`/api/templates/docx/${id}/default`,{});toast('Default updated','success');loadTemplates();}catch(e){toast(e.message,'error');}}
async function deleteTpl(id){if(!confirm('Delete this template?'))return;try{await del(`/api/templates/docx/${id}`);toast('Deleted','info');loadTemplates();}catch(e){toast(e.message,'error');}}
// ═══════════════════════════════════════════════════════════════
// LICENSE
// ═══════════════════════════════════════════════════════════════
async function loadLicense(){
const el=document.getElementById('license-body');
try{const d=await get('/api/license'); const cur=d.current,us=d.usage,plans=d.all_plans||[];
const feats={scoring:'AI Scoring',docx:'DOCX Export',custom_template:'Custom Templates',approval_workflow:'Approval Workflow',rag:'RAG Context',share_links:'Share Links',api_access:'API Access'};
el.innerHTML=`
Current Plan
${cur.name||'Starter'}
Active license
Users
${us.users} / ${cur.max_users===-1?'∞':cur.max_users}
Seats used
This Month
${us.proposals_this_month} / ${cur.max_proposals_month===-1?'∞':cur.max_proposals_month}
Proposals generated
${Object.entries(feats).map(([k,label])=>`
${cur['feature_'+k]?'✓':'✕'} ${label}
`).join('')}
Available Plans
${plans.map(p=>`
${p.name}
$${p.price_monthly}/mo
${p.max_users===-1?'Unlimited':p.max_users} users · ${p.max_proposals_month===-1?'Unlimited':p.max_proposals_month} proposals/mo
${Object.entries(feats).map(([k,label])=>`
${p['feature_'+k]?'✓':'✕'} ${label}
`).join('')}
${(cur.name||'Starter')===p.name?`
Current Plan
`:`
`}
`).join('')}
`;
}catch(e){el.innerHTML=``;}
}
// ═══════════════════════════════════════════════════════════════
// TENANTS (super_admin)
// ═══════════════════════════════════════════════════════════════
async function loadTenants(){
try{const d=await get('/api/tenants');
document.getElementById('tenants-tbody').innerHTML=d.tenants.map(t=>`| ${t.name} | ${t.slug} | ${t.license?.name||'Starter'} | ${t.user_count} | ${t.proposal_count} | ${t.is_active?'Active':'Suspended'} | |
`).join('');
}catch(e){toast('Load error','error');}
}
function openAddTenant(){document.getElementById('modal-tenant').style.display='flex';}
async function saveTenant(){
const body={name:v('t-name'),slug:v('t-slug'),plan_id:v('t-plan')||'plan_starter',primary_color:v('t-color')||'#c9a455',admin_email:v('t-email'),admin_name:v('t-admin-name'),admin_password:v('t-pass')};
if(!body.name||!body.slug||!body.admin_email||!body.admin_password){toast('Fill all required fields','error');return;}
try{await post('/api/tenants',body);toast('Tenant created!','success');closeM('modal-tenant');loadTenants();}catch(e){toast(e.message,'error');}
}
async function toggleTenant(id){try{await put(`/api/tenants/${id}/toggle`,{});toast('Updated','success');loadTenants();}catch(e){toast(e.message,'error');}}
async function assignLicense(id){const plan=prompt('Plan ID (plan_starter / plan_professional / plan_enterprise):');if(!plan)return;try{await put(`/api/tenants/${id}/license`,{plan_id:plan});toast('License updated','success');loadTenants();}catch(e){toast(e.message,'error');}}
// ═══════════════════════════════════════════════════════════════
// SHARE PAGE
// ═══════════════════════════════════════════════════════════════
async function checkShare(){
const token=new URLSearchParams(location.search).get('share');
if(!token)return;
document.getElementById('login-page').style.display='none';
const div=document.createElement('div');div.style.cssText='max-width:820px;margin:0 auto;padding:40px 24px';
document.body.appendChild(div);
try{const d=await fetch(API+`/api/share/${token}`).then(r=>{if(!r.ok)throw new Error('Invalid link');return r.json();});
div.innerHTML=`ProposalAI · Shared Proposal
${d.project_title||''}
Prepared for ${d.client_name||''} · ${fmtD(d.created_at)}
${d.approval_status==='approved'||d.approval_status==='published'?'
✓ Approved':''}
${marked.parse(d.content||'')}
`;
}catch(e){div.innerHTML=`🔗
Link Not Found
${e.message}
`;}
}
// ═══════════════════════════════════════════════════════════════
// TOAST & MODAL HELPERS
// ═══════════════════════════════════════════════════════════════
function toast(msg,type='success'){const icons={success:'✓',error:'✕',info:'ℹ'};const el=document.createElement('div');el.className=`toast ${type}`;el.innerHTML=`${icons[type]||''} ${msg}`;document.getElementById('toast-wrap').appendChild(el);setTimeout(()=>el.remove(),3500);}
function closeM(id){document.getElementById(id).style.display='none';}
// ═══════════════════════════════════════════════════════════════
// INIT
// ═══════════�