comp-tracker / cloud-sync.js
Honda219's picture
Rename sign-in card to Comp Tracker
6df96f0 verified
Raw History Blame Contribute Delete
6.39 kB
import{createClient}from'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm';
const cfg=window.COMP_TRACKER_CONFIG,db=createClient(cfg.supabaseUrl,cfg.supabasePublishableKey,{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:true}}),CK='compensationClients',SK='sortState',PK='compensationCloudPending';let user,channel,timer,remoteWrite=false,lastIds=new Set();
const nativeSet=localStorage.setItem.bind(localStorage);localStorage.setItem=function(k,v){nativeSet(k,v);if(remoteWrite||!user)return;if(k===CK)queueSave();if(k===SK)setTimeout(saveSettings,400)};
document.body.insertAdjacentHTML('beforeend',`<section id="cloudAuthGate"><form id="cloudLogin" class="cloud-card"><h2>Comp Tracker</h2><p>Sign in to your private synchronized records.</p><label>Email</label><input id="cloudEmail" type="email" autocomplete="username" required><label>Password</label><input id="cloudPassword" type="password" autocomplete="current-password" required><p id="cloudError" class="cloud-error" role="alert"></p><button>Sign in</button></form></section><aside id="cloudBar" class="cloud-offline" aria-live="polite"><span class="cloud-dot"></span><span id="cloudStatus">Offline</span><button id="cloudSignOut">Sign out</button></aside>`);
const gate=document.querySelector('#cloudAuthGate'),statusEl=document.querySelector('#cloudStatus'),bar=document.querySelector('#cloudBar');function status(k,t){bar.className='cloud-'+k;statusEl.textContent=t}function fail(e){console.error(e);status('failed','Failed — will retry')}
function norm(c,i=0){return{id:c.id||crypto.randomUUID(),clientName:String(c.clientName||'').trim(),loanAmount:Number(c.loanAmount),pointsCharged:Number(c.pointsCharged),lenderPrice:Number(c.lenderPrice),compTier:c.compTier,closingDate:c.closingDate,excludePoints:Boolean(c.excludePoints),sortOrder:Number.isInteger(c.sortOrder)?c.sortOrder:i}}
function local(){try{return(JSON.parse(localStorage.getItem(CK))||[]).map(norm)}catch{return[]}}function row(c,i){c=norm(c,i);return{id:c.id,owner_id:user.id,client_name:c.clientName,loan_amount:c.loanAmount,points_charged:c.pointsCharged,lender_price:c.lenderPrice,comp_tier:c.compTier,closing_date:c.closingDate,exclude_points:c.excludePoints,sort_order:c.sortOrder}}function client(r){return{id:r.id,clientName:r.client_name,loanAmount:Number(r.loan_amount),pointsCharged:Number(r.points_charged),lenderPrice:Number(r.lender_price),compTier:r.comp_tier,closingDate:r.closing_date,excludePoints:r.exclude_points,sortOrder:r.sort_order}}
function setLocal(items,reload=false){remoteWrite=true;nativeSet(CK,JSON.stringify(items));remoteWrite=false;if(reload)location.reload()}function queueSave(){nativeSet(PK,'true');clearTimeout(timer);if(!navigator.onLine)return status('offline','Offline — pending');status('saving','Saving…');timer=setTimeout(()=>save(local()).catch(fail),650)}
async function save(items){if(!navigator.onLine)return status('offline','Offline — pending');status('saving','Saving…');items=items.map(norm);setLocal(items);if(!lastIds.size)lastIds=new Set(JSON.parse(sessionStorage.getItem('compLastIds')||'[]'));const ids=new Set(items.map(x=>x.id)),removed=[...lastIds].filter(id=>!ids.has(id));if(items.length){const{error}=await db.from('compensation_clients').upsert(items.map(row));if(error)throw error}if(removed.length){const{error}=await db.from('compensation_clients').delete().in('id',removed);if(error)throw error}lastIds=ids;sessionStorage.setItem('compLastIds',JSON.stringify([...ids]));localStorage.removeItem(PK);status('saved',`Saved ${new Date().toLocaleTimeString([],{hour:'numeric',minute:'2-digit'})}`)}
async function load(){if(!navigator.onLine)return status('offline','Offline');status('saving','Loading…');const{data,error}=await db.from('compensation_clients').select('*').order('sort_order').order('created_at');if(error)throw error;const cloud=(data||[]).map(client),here=local();lastIds=new Set(cloud.map(x=>x.id));sessionStorage.setItem('compLastIds',JSON.stringify([...lastIds]));if(!cloud.length&&here.length){if(confirm(`Upload ${here.length} existing record(s) to your private Supabase account?`))return save(here)}else if(cloud.length&&!localStorage.getItem(PK)&&JSON.stringify(cloud)!==JSON.stringify(here))return setLocal(cloud,true);status('saved','Saved')}
async function saveSettings(){if(!navigator.onLine||!user)return;const s=JSON.parse(localStorage.getItem(SK)||'{}'),{error}=await db.from('compensation_settings').upsert({owner_id:user.id,sort_column:s.column||null,sort_direction:s.direction==='desc'?'desc':'asc'});if(error)fail(error)}async function loadSettings(){const{data,error}=await db.from('compensation_settings').select('*').maybeSingle();if(error)throw error;if(data?.sort_column)nativeSet(SK,JSON.stringify({column:data.sort_column,direction:data.sort_direction}))}
function subscribe(){if(channel)db.removeChannel(channel);channel=db.channel('compensation:'+user.id).on('postgres_changes',{event:'*',schema:'public',table:'compensation_clients',filter:`owner_id=eq.${user.id}`},()=>{if(!localStorage.getItem(PK))setTimeout(()=>load().catch(fail),400)}).subscribe()}
async function enter(u){user=u;gate.classList.add('hidden');try{await loadSettings();await load();subscribe()}catch(e){fail(e)}}function leave(){user=null;if(channel)db.removeChannel(channel);gate.classList.remove('hidden');status('offline','Signed out')}
document.querySelector('#cloudLogin').addEventListener('submit',async e=>{e.preventDefault();const b=e.currentTarget.querySelector('button'),out=document.querySelector('#cloudError');out.textContent='';b.disabled=true;b.textContent='Signing in…';const{error}=await db.auth.signInWithPassword({email:document.querySelector('#cloudEmail').value.trim(),password:document.querySelector('#cloudPassword').value});if(error)out.textContent=error.message;b.disabled=false;b.textContent='Sign in'});document.querySelector('#cloudSignOut').onclick=()=>db.auth.signOut();
addEventListener('offline',()=>user&&status('offline','Offline'));addEventListener('online',()=>user&&(localStorage.getItem(PK)?save(local()):load()).catch(fail));
db.auth.onAuthStateChange((_e,s)=>{if(s?.user&&user?.id!==s.user.id)enter(s.user);else if(!s?.user&&user)leave()});
const{data:{session}}=await db.auth.getSession();if(session?.user&&user?.id!==session.user.id)await enter(session.user);else if(!session?.user)leave();