Firm Admin Panel
Firm Dashboard
Platform oversight · Client & professional management · Billing & compliance
Forgot password?
MyCo Admin · Role-based access · Activity logged
// ══════════════════════════════════════════════════════════════ // AGREEMENTS MODULE // ══════════════════════════════════════════════════════════════ var _agrClients = []; var _loeScopeCount = 0; // ── Load agreements list ──────────────────────────────────────── async function sendAgreementEmail(payload) { try { var sb = getSB(); if(!sb) return {ok:false, err:'No Supabase client'}; var sess = await sb.auth.getSession(); var token = sess.data && sess.data.session ? sess.data.session.access_token : ''; var r = await fetch(_EDGE_URL + '/send-agreement-notification', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imttb3Jja3lnbmlka2tkZm50YndoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU2OTMyNDEsImV4cCI6MjA5MTI2OTI0MX0.ahOhwRUSL9ngqTAjMXYuGPTRerLVyPSXWIlPRUrMJSs' }, body: JSON.stringify(payload) }); if(!r.ok) { var e=await r.text(); return {ok:false, err:e}; } return {ok:true}; } catch(e) { return {ok:false, err:String(e)}; } } async function loadAgreements() { var sb = getSB(); if(!sb) return; var tbody = document.getElementById('agr-tbody'); if(tbody) tbody.innerHTML = 'Loading...'; var typeFilter = document.getElementById('agr-filter-type'); var statusFilter = document.getElementById('agr-filter-status'); try { var q = sb.from('agreements').select('*').order('created_at', {ascending:false}); if(typeFilter && typeFilter.value) q = q.eq('type', typeFilter.value); if(statusFilter && statusFilter.value) q = q.eq('status', statusFilter.value); var r = await q; if(r.error) { if(r.error.message && r.error.message.includes('does not exist')) { var notice = document.getElementById('agr-setup-notice'); if(notice) notice.style.display = 'block'; if(tbody) tbody.innerHTML = 'Run the SQL setup above to enable agreements.'; } else { if(tbody) tbody.innerHTML = 'Error: '+esc(r.error.message)+''; } return; } var rows = r.data || []; // KPI counts var masters = rows.filter(function(a){return a.type==='master';}); var loes = rows.filter(function(a){return a.type==='loe';}); var signed = rows.filter(function(a){return a.status==='accepted';}); var pending = rows.filter(function(a){return a.status==='sent';}); var el = function(id, val) { var e=document.getElementById(id); if(e) e.textContent=val; }; el('agr-kc-master', masters.length); el('agr-kc-master-sub', signed.filter(function(a){return a.type==='master';}).length + ' signed'); el('agr-kc-signed', signed.length); el('agr-kc-loe', loes.length); el('agr-kc-loe-sub', signed.filter(function(a){return a.type==='loe';}).length + ' accepted'); el('agr-kc-pending', pending.length); var nb = document.getElementById('nb-agreements'); if(nb) { nb.textContent=pending.length; nb.style.display=pending.length>0?'':'none'; } if(!rows.length) { if(tbody) tbody.innerHTML = 'No agreements yet. Generate a Master Agreement or issue a Letter of Engagement to get started.'; return; } if(tbody) { tbody.innerHTML = ''; rows.forEach(function(a) { var statusHtml = { draft: 'Draft', sent: '⏳ Awaiting', accepted: '✓ Accepted', expired: 'Expired' }[a.status] || a.status; var typeHtml = a.type === 'master' ? 'Master' : 'LOE'; var d = a.agreement_data || {}; var clientName = esc(d.client_name || '—'); var createdDate = a.created_at ? new Date(a.created_at).toLocaleDateString('en-SG') : '—'; var acceptedDate = a.accepted_at ? new Date(a.accepted_at).toLocaleDateString('en-SG') + '
'+esc(a.accepted_by_name||'')+'' : '—'; var tr = document.createElement('tr'); tr.innerHTML = ''+esc(a.ref_number||a.id.slice(0,8))+''+ ''+clientName+''+ ''+typeHtml+''+ ''+statusHtml+''+ ''+createdDate+''+ ''+acceptedDate+''+ '
'; var actions = tr.querySelector('.tbl-actions'); // Copy link button (if sent or accepted) if(a.status === 'sent' || a.status === 'accepted') { var lb = document.createElement('a'); lb.className = 'btn btn-ghost btn-sm'; lb.textContent = 'Copy link'; (function(token){ lb.addEventListener('click', function(){ var link = window.location.origin + '/MyCo_Agreement_Accept.html?token=' + token; navigator.clipboard.writeText(link).then(function(){ showToast('Link copied to clipboard'); }); }); })(a.acceptance_token); actions.appendChild(lb); } // Mark expired button (if sent) if(a.status === 'sent') { var eb = document.createElement('a'); eb.className = 'btn btn-ghost btn-sm'; eb.textContent = 'Expire'; (function(id){ eb.addEventListener('click', function(){ if(!confirm('Mark this agreement as expired?')) return; getSB().from('agreements').update({status:'expired'}).eq('id',id).then(function(){ loadAgreements(); showToast('Agreement expired'); }); }); })(a.id); actions.appendChild(eb); } tbody.appendChild(tr); }); } } catch(e) { if(tbody) tbody.innerHTML = 'Error loading agreements.'; } } // ── Load clients into dropdowns ───────────────────────────────── async function loadAgrClients() { var sb = getSB(); if(!sb) return; try { var r = await sb.rpc('get_all_clients_admin'); _agrClients = (r.data || []).filter(function(c){return c.status !== 'offboarded';}); var selIds = ['ma-client-select', 'loe-client-select']; selIds.forEach(function(sid) { var sel = document.getElementById(sid); if(!sel) return; var cur = sel.value; sel.innerHTML = ''; _agrClients.forEach(function(c) { var opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.company_name; sel.appendChild(opt); }); if(cur) sel.value = cur; }); } catch(e) {} } // ── Generate reference number ─────────────────────────────────── async function generateRef(type) { var sb = getSB(); if(!sb) return type.toUpperCase()+'-'+Date.now().toString().slice(-6); try { var r = await sb.from('agreements').select('ref_number').eq('type',type).order('created_at',{ascending:false}).limit(1); var rows = r.data || []; var prefix = type === 'master' ? 'MA' : 'LOE'; var year = new Date().getFullYear(); var seq = 1; if(rows.length && rows[0].ref_number) { var parts = rows[0].ref_number.split('-'); if(parts.length >= 3) seq = parseInt(parts[parts.length-1]||0)+1; } return prefix+'-'+year+'-'+String(seq).padStart(3,'0'); } catch(e) { return (type==='master'?'MA':'LOE')+'-'+new Date().getFullYear()+'-001'; } } // ── Copy acceptance link ──────────────────────────────────────── function copyAgrLink(elId) { var el = document.getElementById(elId); if(!el) return; navigator.clipboard.writeText(el.textContent).then(function(){ showToast('Link copied to clipboard'); }); } // ══ MASTER AGREEMENT ══════════════════════════════════════════ function openMasterModal() { document.getElementById('master-agr-modal').style.display='flex'; document.getElementById('master-step-1').style.display='block'; document.getElementById('master-step-2').style.display='none'; var mes=document.getElementById('ma-email-status');if(mes){mes.textContent='';mes.style.color=''} var btn = document.getElementById('ma-submit-btn'); if(btn){btn.style.display='';btn.disabled=false;btn.textContent='Generate & Get Link →';} document.getElementById('ma-effective-date').value = new Date().toISOString().split('T')[0]; var msg = document.getElementById('ma-msg'); if(msg) msg.style.display='none'; loadAgrClients(); } function closeMasterModal() { document.getElementById('master-agr-modal').style.display='none'; document.getElementById('ma-client-select').value=''; document.getElementById('ma-client-name').value=''; document.getElementById('ma-client-uen').value=''; document.getElementById('ma-client-address').value=''; document.getElementById('ma-addressee').value=''; document.getElementById('ma-prev-name').textContent='[ Select client above ]'; document.getElementById('ma-prev-uen').textContent='—'; document.getElementById('ma-prev-date').textContent='—'; } function onMasterClientSelect() { var sel = document.getElementById('ma-client-select'); var client = _agrClients.find(function(c){return c.id===sel.value;}); if(!client) return; document.getElementById('ma-client-name').value = client.company_name || ''; document.getElementById('ma-client-uen').value = client.uen_number || ''; document.getElementById('ma-client-address').value = client.registered_address || ''; document.getElementById('ma-addressee').value = client.contact_person_name || ''; document.getElementById('ma-prev-name').textContent = client.company_name || '—'; document.getElementById('ma-prev-uen').textContent = client.uen_number || '—'; var d = document.getElementById('ma-effective-date').value; document.getElementById('ma-prev-date').textContent = d ? new Date(d).toLocaleDateString('en-SG',{day:'numeric',month:'long',year:'numeric'}) : '—'; } async function submitMasterAgreement() { var sb = getSB(); if(!sb) return; var clientId = document.getElementById('ma-client-select').value; var clientName = document.getElementById('ma-client-name').value.trim(); var clientUen = document.getElementById('ma-client-uen').value.trim(); var clientAddr = document.getElementById('ma-client-address').value.trim(); var effectiveDate = document.getElementById('ma-effective-date').value; var addressee = document.getElementById('ma-addressee').value.trim(); var msg = document.getElementById('ma-msg'); if(!clientId || !clientName || !effectiveDate) { msg.textContent = 'Please select a client and set the effective date.'; msg.style.cssText = 'display:block;background:var(--red-bg);color:var(--red);padding:10px 14px;border-radius:8px;font-size:13px'; return; } var btn = document.getElementById('ma-submit-btn'); btn.textContent = 'Generating...'; btn.disabled = true; var ref = await generateRef('master'); var token = crypto.randomUUID ? crypto.randomUUID() : (Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)); var r = await sb.from('agreements').insert({ client_id: clientId, type: 'master', status: 'sent', ref_number: ref, acceptance_token: token, sent_at: new Date().toISOString(), agreement_data: { client_name: clientName, client_uen: clientUen, client_address: clientAddr, effective_date: effectiveDate, addressee: addressee } }).select().single(); if(r.error) { msg.textContent = 'Error: '+r.error.message; msg.style.cssText = 'display:block;background:var(--red-bg);color:var(--red);padding:10px 14px;border-radius:8px;font-size:13px'; btn.textContent='Generate & Get Link →'; btn.disabled=false; return; } var link = window.location.origin + '/MyCo_Agreement_Accept.html?token=' + token; document.getElementById('ma-accept-link').textContent = link; document.getElementById('master-step-1').style.display='none'; document.getElementById('master-step-2').style.display='block'; btn.style.display = 'none'; loadAgreements(); // Send email automatically var d = r.data; var agrData = d.agreement_data || {}; var emailStatus = document.getElementById('ma-email-status'); if(emailStatus) emailStatus.textContent = '⏳ Sending email to client…'; var emailResult = await sendAgreementEmail({ client_email: _agrClients.find(function(c){return c.id===clientId;}) ? (_agrClients.find(function(c){return c.id===clientId;}).contact_email || '') : '', client_name: clientName, addressee: addressee, agreement_type: 'master', ref_number: ref, acceptance_token: token, effective_date: effectiveDate }); if(emailStatus) { emailStatus.textContent = emailResult.ok ? '✓ Email sent to client successfully' : '⚠ Agreement created but email failed — copy link and send manually. Error: '+emailResult.err; emailStatus.style.color = emailResult.ok ? 'var(--green)' : 'var(--amber)'; } showToast(emailResult.ok ? 'Agreement generated & email sent — '+ref : 'Agreement generated — '+ref); } // ══ LOE ═══════════════════════════════════════════════════════ async function openLOEModal() { document.getElementById('loe-modal').style.display='flex'; document.getElementById('loe-step-1').style.display='block'; document.getElementById('loe-step-2').style.display='none'; var les=document.getElementById('loe-email-status');if(les){les.textContent='';les.style.color=''} var btn = document.getElementById('loe-submit-btn'); if(btn){btn.style.display='';btn.disabled=false;btn.textContent='Generate & Get Link →';} document.getElementById('loe-date').value = new Date().toISOString().split('T')[0]; var msg = document.getElementById('loe-msg'); if(msg) msg.style.display='none'; // Reset scope rows _loeScopeCount=0; var sr=document.getElementById('loe-scope-rows'); if(sr) sr.innerHTML=''; document.getElementById('loe-total-display').textContent='0'; // Generate ref var ref = await generateRef('loe'); document.getElementById('loe-ref').value = ref; loadAgrClients(); // Add 2 default scope rows addScopeRow(); addScopeRow(); } function closeLOEModal() { document.getElementById('loe-modal').style.display='none'; } function onLOEClientSelect() { var sel = document.getElementById('loe-client-select'); var client = _agrClients.find(function(c){return c.id===sel.value;}); if(!client) return; document.getElementById('loe-addressee').value = client.contact_person_name || ''; } function addScopeRow() { _loeScopeCount++; var n = _loeScopeCount; var container = document.getElementById('loe-scope-rows'); var row = document.createElement('div'); row.className = 'loe-scope-row'; row.id = 'loe-scope-row-'+n; row.innerHTML = ''+ ''+ ''; container.appendChild(row); } function removeScopeRow(n) { var row = document.getElementById('loe-scope-row-'+n); if(row) row.remove(); calcLOETotal(); } function calcLOETotal() { var total = 0; for(var i=1; i<=_loeScopeCount; i++) { var feeEl = document.getElementById('loe-scope-fee-'+i); if(feeEl && feeEl.value) total += parseFloat(feeEl.value)||0; } var td = document.getElementById('loe-total-display'); if(td) td.textContent = total.toLocaleString('en-SG', {minimumFractionDigits:2, maximumFractionDigits:2}); } async function submitLOE() { var sb = getSB(); if(!sb) return; var clientId = document.getElementById('loe-client-select').value; var serviceCat = document.getElementById('loe-service-cat').value; var msg = document.getElementById('loe-msg'); if(!clientId || !serviceCat) { msg.textContent = 'Please select a client and service category.'; msg.style.cssText = 'display:block;background:var(--red-bg);color:var(--red);padding:10px 14px;border-radius:8px;font-size:13px'; return; } // Collect scope items var scopeItems = []; var totalFee = 0; for(var i=1; i<=_loeScopeCount; i++) { var descEl = document.getElementById('loe-scope-desc-'+i); var feeEl = document.getElementById('loe-scope-fee-'+i); if(descEl && descEl.closest('#loe-scope-rows') && descEl.value.trim()) { var fee = parseFloat(feeEl&&feeEl.value)||0; scopeItems.push({description: descEl.value.trim(), fee: fee}); totalFee += fee; } } if(!scopeItems.length) { msg.textContent = 'Please add at least one scope item.'; msg.style.cssText = 'display:block;background:var(--red-bg);color:var(--red);padding:10px 14px;border-radius:8px;font-size:13px'; return; } var btn = document.getElementById('loe-submit-btn'); btn.textContent='Generating...'; btn.disabled=true; var client = _agrClients.find(function(c){return c.id===clientId;}); var ref = document.getElementById('loe-ref').value; var token = crypto.randomUUID ? crypto.randomUUID() : (Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)); var oosRaw = document.getElementById('loe-oos').value.trim(); var oos = oosRaw ? oosRaw.split(/[;\n]/).map(function(s){return s.trim();}).filter(Boolean) : []; var r = await sb.from('agreements').insert({ client_id: clientId, type: 'loe', status: 'sent', ref_number: ref, acceptance_token: token, sent_at: new Date().toISOString(), agreement_data: { client_name: client ? client.company_name : '', client_uen: client ? client.uen_number : '', client_address: client ? (client.registered_address||'') : '', addressee: document.getElementById('loe-addressee').value.trim(), service_category: serviceCat, period: document.getElementById('loe-period').value.trim(), billing_frequency: document.getElementById('loe-billing').value, effective_date: document.getElementById('loe-date').value, scope_items: scopeItems, total_fee: totalFee, dates: { info_due: document.getElementById('loe-info-due').value, draft_due: document.getElementById('loe-draft-due').value, approval_due: document.getElementById('loe-approval-due').value, statutory: document.getElementById('loe-statutory').value }, out_of_scope: oos } }).select().single(); if(r.error) { msg.textContent = 'Error: '+r.error.message; msg.style.cssText = 'display:block;background:var(--red-bg);color:var(--red);padding:10px 14px;border-radius:8px;font-size:13px'; btn.textContent='Generate & Get Link →'; btn.disabled=false; return; } var link = window.location.origin + '/MyCo_Agreement_Accept.html?token=' + token; document.getElementById('loe-accept-link').textContent = link; document.getElementById('loe-step-1').style.display='none'; document.getElementById('loe-step-2').style.display='block'; btn.style.display='none'; loadAgreements(); // Send email automatically var loeEmailStatus = document.getElementById('loe-email-status'); if(loeEmailStatus) loeEmailStatus.textContent = '⏳ Sending email to client…'; var loeClient = _agrClients.find(function(c){return c.id===clientId;}); var loeEmailResult = await sendAgreementEmail({ client_email: loeClient ? (loeClient.contact_email || '') : '', client_name: client ? client.company_name : '', addressee: document.getElementById('loe-addressee').value.trim(), agreement_type: 'loe', ref_number: ref, acceptance_token: token, service_category: serviceCat, total_fee: totalFee, effective_date: document.getElementById('loe-date').value }); if(loeEmailStatus) { loeEmailStatus.textContent = loeEmailResult.ok ? '✓ Email sent to client successfully' : '⚠ LOE created but email failed — copy link and send manually. Error: '+loeEmailResult.err; loeEmailStatus.style.color = loeEmailResult.ok ? 'var(--green)' : 'var(--amber)'; } showToast(loeEmailResult.ok ? 'LOE generated & email sent — '+ref : 'LOE generated — '+ref); } // Hook loadAgreements into goTo (function(){ var orig = window.goTo || function(){}; window.goTo = function(id, el) { orig.call(this, id, el); if(id === 'agreements') { loadAgreements(); loadAgrClients(); } }; // Also patch the existing goTo var sb = getSB(); })();
Generate Master Agreement
Pre-filled from client record · Admin reviews before sending
Agreement preview

CLIENT ENGAGEMENT AGREEMENT

Between: MyCompany (Singapore) Pte Ltd (UEN: 202023844E)
— and —
Client: [ Select client above ]
UEN:
Effective:
Covers: Scope of services · Fees & payment terms · Confidentiality · IP · Limitation of liability · Non-circumvention · Termination · Governing law (Singapore)
New Letter of Engagement
Define scope, fees, and dates · Client accepts online
Scope of work
Total fee: SGD $0
Key dates
Out of scope (optional)
Separate items with semicolons or new lines
Add CPD Resource
Published immediately to the Professional Portal
Send Quarterly CPD Digest
Sends to all active professionals — previewing resources added in the last 90 days
Resources included in this digest
Loading preview…
This email will be sent to all active professionals. Each professional receives a branded digest showing only resources added or updated in the past 90 days. If there are no new resources, the button will be disabled.