Samuha Bachat

const BSD={2000:[30,32,31,32,31,30,30,30,29,30,29,31],2001:[31,31,32,31,31,31,30,29,30,29,30,30],2002:[31,31,32,32,31,30,30,29,30,29,30,30],2003:[31,32,31,32,31,30,30,30,29,29,30,31],2004:[30,32,31,32,31,30,30,30,29,30,29,31],2005:[31,31,32,31,31,31,30,29,30,29,30,30],2006:[31,31,32,32,31,30,30,29,30,29,30,30],2007:[31,32,31,32,31,30,30,30,29,29,30,31],2008:[31,31,31,32,31,31,29,30,30,29,29,31],2009:[31,31,32,31,31,31,30,29,30,29,30,30],2010:[31,31,32,32,31,30,30,29,30,29,30,30],2011:[31,32,31,32,31,30,30,30,29,29,30,31],2012:[31,31,31,32,31,31,29,30,30,29,30,30],2013:[31,31,32,31,31,31,30,29,30,29,30,30],2014:[31,31,32,32,31,30,30,29,30,29,30,30],2015:[31,32,31,32,31,30,30,30,29,29,30,31],2016:[31,31,31,32,31,31,29,30,30,29,30,30],2017:[31,31,32,31,31,31,30,29,30,29,30,30],2018:[31,32,31,32,31,30,30,29,30,29,30,30],2019:[31,32,31,32,31,30,30,30,29,30,29,31],2020:[31,31,31,32,31,31,30,29,30,29,30,30],2021:[31,31,32,31,31,31,30,29,30,29,30,30],2022:[31,32,31,32,31,30,30,30,29,29,30,30],2023:[31,32,31,32,31,30,30,30,29,30,29,31],2024:[31,31,31,32,31,31,30,29,30,29,30,30],2025:[31,31,32,31,31,31,30,29,30,29,30,30],2026:[31,32,31,32,31,30,30,30,29,29,30,31],2027:[30,32,31,32,31,30,30,30,29,30,29,31],2028:[31,31,32,31,31,31,30,29,30,29,30,30],2029:[31,31,32,31,32,30,30,29,30,29,30,30],2030:[31,32,31,32,31,30,30,30,29,29,30,31],2031:[30,32,31,32,31,30,30,30,29,30,29,31],2032:[31,31,32,31,31,31,30,29,30,29,30,30],2033:[31,31,32,32,31,30,30,29,30,29,30,30],2034:[31,32,31,32,31,30,30,30,29,29,30,31],2035:[30,32,31,32,31,31,29,30,30,29,29,31],2036:[31,31,32,31,31,31,30,29,30,29,30,30],2037:[31,31,32,32,31,30,30,29,30,29,30,30],2038:[31,32,31,32,31,30,30,30,29,29,30,31],2039:[31,31,31,32,31,31,29,30,30,29,30,30],2040:[31,31,32,31,31,31,30,29,30,29,30,30],2041:[31,31,32,32,31,30,30,29,30,29,30,30],2042:[31,32,31,32,31,30,30,30,29,29,30,31],2043:[31,31,31,32,31,31,29,30,30,29,30,30],2044:[31,31,32,31,31,31,30,29,30,29,30,30],2045:[31,32,31,32,31,30,30,29,30,29,30,30],2046:[31,32,31,32,31,30,30,30,29,29,30,31],2047:[31,31,31,32,31,31,30,29,30,29,30,30],2048:[31,31,32,31,31,31,30,29,30,29,30,30],2049:[31,32,31,32,31,30,30,30,29,29,30,30],2050:[31,32,31,32,31,30,30,30,29,30,29,31],2051:[31,31,31,32,31,31,30,29,30,29,30,30],2052:[31,31,32,31,31,31,30,29,30,29,30,30],2053:[31,32,31,32,31,30,30,30,29,29,30,30],2054:[31,32,31,32,31,30,30,30,29,30,29,31],2055:[31,31,32,31,31,31,30,29,30,29,30,30],2056:[31,31,32,31,32,30,30,29,30,29,30,30],2057:[31,32,31,32,31,30,30,30,29,29,30,31],2058:[30,32,31,32,31,30,30,30,29,30,29,31],2059:[31,31,32,31,31,31,30,29,30,29,30,30],2060:[31,31,32,32,31,30,30,29,30,29,30,30],2061:[31,32,31,32,31,30,30,30,29,29,30,31],2062:[30,32,31,32,31,31,29,30,29,30,29,31],2063:[31,31,32,31,31,31,30,29,30,29,30,30],2064:[31,31,32,32,31,30,30,29,30,29,30,30],2065:[31,32,31,32,31,30,30,30,29,29,30,31],2066:[31,31,31,32,31,31,29,30,30,29,29,31],2067:[31,31,32,31,31,31,30,29,30,29,30,30],2068:[31,31,32,32,31,30,30,29,30,29,30,30],2069:[31,32,31,32,31,30,30,30,29,29,30,31],2070:[31,31,31,32,31,31,29,30,30,29,30,30],2071:[31,31,32,31,31,31,30,29,30,29,30,30],2072:[31,32,31,32,31,30,30,29,30,29,30,30],2073:[31,32,31,32,31,30,30,30,29,29,30,31],2074:[31,31,31,32,31,31,30,29,30,29,30,30],2075:[31,31,32,31,31,31,30,29,30,29,30,30],2076:[31,32,31,32,31,30,30,30,29,29,30,30],2077:[31,32,31,32,31,30,30,30,29,30,29,31],2078:[31,31,31,32,31,31,30,29,30,29,30,30],2079:[31,31,32,31,31,31,30,29,30,29,30,30],2080:[31,32,31,32,31,30,30,30,29,29,30,30],2081:[31,32,31,32,31,30,30,30,29,30,29,31],2082:[31,31,32,31,31,31,30,29,30,29,30,30],2083:[31,31,32,31,31,31,30,29,30,29,30,30],2084:[31,31,32,31,31,30,30,30,29,30,30,30],2085:[31,32,31,32,30,31,30,30,29,30,30,30],2086:[30,32,31,32,31,30,30,30,29,30,30,30],2087:[31,31,32,31,31,31,30,30,29,30,30,30],2088:[30,31,32,32,30,31,30,30,29,30,30,30],2089:[30,32,31,32,31,30,30,30,29,30,30,30],2090:[30,31,32,31,31,30,30,30,29,30,30,30],2091:[31,31,32,31,31,31,30,30,29,30,30,30],2092:[30,31,32,32,31,30,30,30,29,30,30,30],2093:[30,32,31,32,31,30,30,30,29,30,30,30],2094:[31,31,32,31,31,30,30,30,29,30,30,30],2095:[31,31,32,31,31,31,30,29,30,30,30,30],2096:[30,31,32,32,31,30,30,29,30,29,30,30],2097:[31,32,31,32,31,30,30,30,29,30,30,30],2098:[31,31,32,31,31,31,29,30,29,30,29,31],2099:[31,32,31,32,31,30,30,30,29,29,30,31]}; const BSM=['Baishakh','Jestha','Asar','Shrawan','Bhadra','Ashwin','Kartik','Mangsir','Poush','Magh','Falgun','Chaitra']; const BSMN=['बैशाख','जेठ','असार','साउन','भदौ','असोज','कात्तिक','मंसिर','पुष','माघ','फागुन','चैत']; const BSW=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const BSWN=['आइत','सोम','मंगल','बुध','बिहि','शुक्र','शनि']; const BSEPOCH=Date.UTC(1943,3,14); function adUtc(s){const [y,m,d]=String(s).slice(0,10).split('-').map(Number);return Date.UTC(y,m-1,d)} function adStr(t){const d=new Date(t);return d.getUTCFullYear()+'-'+String(d.getUTCMonth()+1).padStart(2,'0')+'-'+String(d.getUTCDate()).padStart(2,'0')} function adToBs(s){let n=Math.floor((adUtc(s)-BSEPOCH)/86400000),y=2000;if(n<0)return null;while(y<=2099){const yl=BSD[y].reduce((a,b)=>a+b,0);if(n=BSD[y][m-1]){n-=BSD[y][m-1];m++}return{y,m,d:n+1}} function bsToAd(y,m,d){if(!BSD[y]||m<1||m>12||d<1||d>BSD[y][m-1])return null;let n=0;for(let yy=2000;yya+b,0);for(let i=0;i=1&&m<=12?(LANG==='ne'?BSMN[m-1]+' '+nd(y):BSM[m-1]+' '+y):String(v)} function bsMonthFromAd(s){const b=adToBs(String(s).slice(0,10));if(b)return b.y+'-'+String(b.m).padStart(2,'0');const z=String(s).match(/^(\d{4})-(\d{2})$/);if(z){const a=adToBs(z[1]+'-'+z[2]+'-01');return a?a.y+'-'+String(a.m).padStart(2,'0'):s}return String(s)} function bsMonthToAd(v){const [y,m]=String(v).split('-').map(Number);const a=bsToAd(y,m,1);return a?a.slice(0,7):''} function bsNow(){return adToBs(today())} let bspick=null; function closeBsp(){ if(bspick){ if(bspick.outside) document.removeEventListener('pointerdown',bspick.outside,true); bspick.pop.remove(); bspick=null; } } function monthPickerBs(raw){const p=String(raw).split('-').map(Number);if(BSD[p[0]]&&p[1]>=1&&p[1]<=12)return{y:p[0],m:p[1],d:1};const z=bsMonthFromAd(raw).split('-').map(Number);return{y:z[0],m:z[1],d:1}} function bsOpen(id,kind,anchor){ closeBsp(); const el=document.getElementById(id); if(!el)return; const raw=el.value||anchor||today(); const b=kind==='month'?monthPickerBs(raw):(adToBs(raw)||bsNow()); if(!b)return; const pop=document.createElement('div'); pop.className='bspop'; pop.addEventListener('pointerdown',e=>e.stopPropagation()); pop.addEventListener('click',e=>{ e.preventDefault(); e.stopPropagation(); const btn=e.target.closest('button[data-bs-action]'); if(!btn)return; const action=btn.dataset.bsAction; if(action==='prev') return bspick.kind==='month'?bsMoveYear(-1):bsMove(-1); if(action==='next') return bspick.kind==='month'?bsMoveYear(1):bsMove(1); if(action==='today') return bsToday(); if(action==='close') return closeBsp(); if(action==='date') return bsChooseDate(btn.dataset.value); if(action==='month') return bsChooseMonth(+btn.dataset.year,+btn.dataset.month); }); document.body.appendChild(pop); const outside=e=>{if(!bspick)return;if(!pop.contains(e.target)&&e.target!==el&&e.target!==document.getElementById(id+'_view'))closeBsp()}; bspick={pop,id,kind,y:b.y,m:b.m,outside}; document.addEventListener('pointerdown',outside,true); bsRedraw(); requestAnimationFrame(()=>{ if(!bspick||bspick.pop!==pop)return; const targetEl = document.getElementById(id+'_view') || el; const r = targetEl.getBoundingClientRect(); pop.style.left=Math.min(Math.max(8,r.left),Math.max(8,innerWidth-pop.offsetWidth-8))+'px'; pop.style.top=Math.min(r.bottom+6,Math.max(8,innerHeight-pop.offsetHeight-8))+'px'; }); } function bsMoveYear(n){ if(!bspick||bspick.kind!=='month')return; const y=bspick.y+n; if(BSD[y]){bspick.y=y;bsRedraw()} } function bsMove(n){ if(!bspick)return; let m=bspick.m+n,y=bspick.y; if(m<1){m=12;y--} if(m>12){m=1;y++} if(BSD[y]){bspick.y=y;bspick.m=m;bsRedraw()} } function bsRedraw(){ if(!bspick)return; const {id,kind,y,m,pop}=bspick,el=document.getElementById(id); if(!el)return; if(kind==='month'){ pop.innerHTML=`
${y}
${BSM.map((n,i)=>``).join('')}
BS month
`; return; } const first=bsToAd(y,m,1),wd=new Date(adUtc(first)).getUTCDay(),days=BSD[y]?.[m-1]||30; let cells=BSW.map((x,i)=>`
${LANG=='ne'?BSWN[i]:x}
`).join(''); for(let i=0;i'; const cur=adToBs(el.value); for(let d=1;d<=days;d++){ const a=bsToAd(y,m,d),dis=(el.dataset.min&&ael.dataset.max),sel=cur&&cur.y==y&&cur.m==m&&cur.d==d; cells+=``; } pop.innerHTML=`
${LANG=='ne'?BSMN[m-1]:BSM[m-1]} ${y}
${cells}
`; } function bsChooseDate(a){ if(!bspick)return; const el=document.getElementById(bspick.id); if(!el)return; el.value=a; const v=document.getElementById(bspick.id+'_view'); if(v)v.value=bsLabel(a); if(bspick.id==='b_d')bd=a; if(bspick.id==='mb_d')mbd=a; if(typeof window[bspick.id+'_changed']==='function')window[bspick.id+'_changed'](a); closeBsp(); el.dispatchEvent(new Event('change',{bubbles:true})); } function bsChooseMonth(y,m){ if(!bspick||!BSD[y]||m<1||m>12)return; const id=bspick.id,el=document.getElementById(id),a=bsToAd(y,m,1); if(!el||!a)return; const av=y+'-'+String(m).padStart(2,'0'); el.value=av; const v=document.getElementById(id+'_view'); if(v)v.value=(LANG==='ne'?BSMN[m-1]:BSM[m-1])+' '+y; if(id==='b_m')bm=av; if(id==='rm')rm=av; if(typeof window[id+'_changed']==='function')window[id+'_changed'](av); closeBsp(); el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); if(['b_m','rm','rf'].includes(id))render(); } function bsToday(){if(!bspick)return;const t=today(),b=adToBs(t);if(!b)return;bspick.y=b.y;bspick.m=b.m;bsRedraw()} function bsCtl(id,val,kind,attrs=''){ const display=kind==='month'?bsMonthLabel(val):bsLabel(val); const min=(attrs.match(/min="([^"]+)"/)||[])[1]||''; const max=(attrs.match(/max="([^"]+)"/)||[])[1]||''; return `
` } const SMALL=100; // extra principal up to this amount is recorded as interest income const FIRST=1; // interest starts one month after the loan is issued (loan in October: first interest due in November) const fresh=()=>({set:{dep:500,rate:12},members:[],deposits:[],loans:[],reps:[],pay:[]}); let q='',pg=0,sortBy='name',LF=()=>''; let menu=false,db=fresh(),firm=null,tab='dash',authTab='in',ed=null,bulk=false,selL=null; let LANG=localStorage.getItem('sb-lang')||'ne'; function setLang(){LANG=LANG==='ne'?'en':'ne';try{localStorage.setItem('sb-lang',LANG)}catch(e){};render();apply();} /* ===== Nepali language engine ===== 1) UI_NE: exact phrases 2) NE_PAT: phrases with changing parts (dates, amounts, names) 3) NE_MSG / NE_MSGP: pop-up messages 4) token fallback: BS month names, Rs, units, Nepali digits Switching to English simply re-renders from the English source, so nothing is lost. Set NE_DIGITS=false to keep 0-9 digits in Nepali mode. */ const NE_DIGITS=true; const UI_NE={"Samuha Bachat":"समूह बचत","Monthly savings, loans and repayments for your group":"तपाईंको समूहको मासिक बचत, ऋण र भुक्तानी","Monthly deposits, loans and repayments for your group":"तपाईंको समूहको मासिक बचत, ऋण र भुक्तानी","Accounting Cloud":"लेखा क्लाउड","Loading…":"खुल्दै छ…","Email":"इमेल","Password":"पासवर्ड","Log in":"लगइन","Log out":"लगआउट","Remember me":"मलाई सम्झनुहोस्","Forgot password?":"पासवर्ड बिर्सनुभयो?","English":"English","नेपाली":"नेपाली","Show password":"पासवर्ड देखाउनुहोस्","Hide password":"पासवर्ड लुकाउनुहोस्","Create account":"खाता बनाउनुहोस्","Create Samuha":"समूह बनाउनुहोस्","Create a new group":"नयाँ समूह बनाउनुहोस्","Create your first group":"आफ्नो पहिलो समूह बनाउनुहोस्","Back":"पछाडि","Reload":"पुनः लोड गर्नुहोस्","Only authorized app operators should use this app. Ordinary financial members do not need app accounts.":"यो एप अधिकृत सञ्चालकहरूले मात्र प्रयोग गर्नुपर्छ। साधारण सदस्यहरूलाई एपको खाता आवश्यक पर्दैन।","Financial records remain in the Supabase cloud. This app requires an internet connection to save transactions.":"आर्थिक अभिलेख Supabase क्लाउडमा सुरक्षित रहन्छ। कारोबार सुरक्षित गर्न इन्टरनेट आवश्यक छ।","Choose a new password to finish account setup or recover your existing account. Your group access will not be changed.":"खाता सेटअप पूरा गर्न वा खाता पुनःप्राप्त गर्न नयाँ पासवर्ड छान्नुहोस्। तपाईंको समूह पहुँच परिवर्तन हुँदैन।","New password":"नयाँ पासवर्ड","Confirm new password":"नयाँ पासवर्ड पुष्टि गर्नुहोस्","Set password":"पासवर्ड सेट गर्नुहोस्","Repeat password":"पासवर्ड फेरि लेख्नुहोस्","Current password":"हालको पासवर्ड","Change password":"पासवर्ड परिवर्तन गर्नुहोस्","Choose your own password":"आफ्नै पासवर्ड छान्नुहोस्","Your account was created with a temporary password. Choose a new password of at least 8 characters to continue.":"तपाईंको खाता अस्थायी पासवर्डसहित बनाइएको हो। जारी राख्न कम्तीमा ८ अक्षरको नयाँ पासवर्ड छान्नुहोस्।","Save password":"पासवर्ड सुरक्षित गर्नुहोस्","This will work only if the secure Supabase setup permission has been enabled for this authorized first Admin.":"यो अधिकृत पहिलो प्रशासकका लागि सुरक्षित Supabase अनुमति सक्रिय गरिएको छ भने मात्र काम गर्छ।","No group access is assigned to this account.":"यस खातालाई कुनै समूहको पहुँच दिइएको छैन।","If this is the first Admin account, you can try firm setup below. Existing groups will appear only when this account has membership access.":"यो पहिलो प्रशासक खाता हो भने तल समूह सेटअप प्रयास गर्न सक्नुहुन्छ। खातालाई सदस्यता पहुँच भएका समूह मात्र देखिन्छन्।","Refresh group access":"समूह पहुँच पुनः जाँच्नुहोस्","Cloud setup required":"क्लाउड सेटअप आवश्यक छ","Connect to cloud":"क्लाउडमा जडान गर्नुहोस्","Supabase Project URL":"Supabase परियोजना URL","Supabase Publishable / Anon Key":"Supabase Publishable / Anon Key","Paste your publishable key":"आफ्नो publishable key यहाँ राख्नुहोस्","Could not load the cloud library":"क्लाउड लाइब्रेरी लोड गर्न सकिएन","Check your internet connection, then reload the app.":"इन्टरनेट जडान जाँचेर एप पुनः लोड गर्नुहोस्।","Group name":"समूहको नाम","Name":"नाम","Phone":"फोन","Dashboard":"ड्यासबोर्ड","Members":"सदस्यहरू","Member":"सदस्य","Financial Transactions":"आर्थिक कारोबार","Approvals":"स्वीकृति","Reports":"प्रतिवेदन","Administration":"सेटिङ","Setting":"सेटिङ","Group Management":"समूह व्यवस्थापन","Group Information":"समूह जानकारी","Group Rules":"समूहका नियम","Group Dissolve":"समूह विघटन","My Profile":"मेरो प्रोफाइल","Update Details":"विवरण अपडेट","Change Password":"पासवर्ड परिवर्तन","Current Group":"हालको समूह","Switch Group":"समूह परिवर्तन","Create Group":"समूह बनाउनुहोस्","Main menu":"मुख्य मेनु","Closed":"बन्द","Register New Member":"नयाँ सदस्य दर्ता","Member Exit":"सदस्य बहिर्गमन","Deposit Register":"बचत जम्मा दर्ता","Loan Register":"ऋण दर्ता","Loan Repayment":"ऋण भुक्तानी","Loan":"ऋण","Group Dissolve":"समूह विघटन","Transaction Correction":"कारोबार सच्याउने","Group Rule":"समूहको नियम","Deposit Report":"बचत जम्मा प्रतिवेदन","Loan Reports":"ऋण प्रतिवेदन","Cash Book":"नगद किताब","Member Earnings":"सदस्य आम्दानी","Group Profile":"समूह प्रोफाइल","Group Management":"समूह व्यवस्थापन","Group Rules":"समूहका नियम","User Management":"प्रयोगकर्ता व्यवस्थापन","System Maintenance":"प्रणाली मर्मत","Group Closure":"समूह बन्द / विघटन","Group Dissolve":"समूह विघटन","Audit & Activity History":"लेखापरीक्षण तथा गतिविधि इतिहास","Member Reports":"सदस्य प्रतिवेदन","Financial Overview":"वित्तीय सारांश","Cash & Financial Transactions":"नगद तथा आर्थिक कारोबार","Monthly Performance":"मासिक कार्यसम्पादन","Income & Expenses":"आम्दानी तथा खर्च","Soon":"छिट्टै","Open menu":"मेनु खोल्नुहोस्","Close menu":"मेनु बन्द गर्नुहोस्","Report categories":"प्रतिवेदनका वर्गहरू","Related reports":"सम्बन्धित प्रतिवेदन","Related Reports":"सम्बन्धित प्रतिवेदन","Member Portfolio":"सदस्य सारांश","Member Statement":"सदस्य विवरणपत्र","Member Exit History":"सदस्य बहिर्गमन इतिहास","Loan Recovery & Overdue":"ऋण असुली तथा म्याद नाघेको","Loan Recovery":"ऋण असुली","Deposit Pending Details":"बाँकी बचतको विवरण","Admin":"प्रशासक","Owner (admin)":"मालिक (प्रशासक)","Save":"सुरक्षित गर्नुहोस्","Update":"अपडेट गर्नुहोस्","Cancel":"रद्द गर्नुहोस्","Edit":"सम्पादन","Delete":"मेटाउनुहोस्","Remove":"हटाउनुहोस्","Search":"खोज्नुहोस्","Search member":"सदस्य खोज्नुहोस्","Search by name":"नामबाट खोज्नुहोस्","Type a name":"नाम लेख्नुहोस्","Type a name to find a member":"सदस्य भेट्टाउन नाम लेख्नुहोस्","Single entry":"एकल प्रविष्टि","Bulk entry":"सामूहिक प्रविष्टि","Single Entry":"एकल प्रविष्टि","Bulk Entry":"सामूहिक प्रविष्टि","Active":"सक्रिय","Pending":"बाँकी","Settled":"फर्छ्यौट भएको","Closed on":"बन्द गरिएको मिति","Total":"जम्मा","Date":"मिति","Month":"महिना","Amount":"रकम","Fine":"जरिवाना","Rate":"दर","Principal":"सावाँ","Interest":"ब्याज","Joined":"सामेल मिति","Savings":"बचत","Status":"स्थिति","Description":"विवरण","Value":"मान","Position":"पद","Powers":"अधिकारहरू","Your powers":"तपाईंका अधिकारहरू","Access":"पहुँच","Person":"व्यक्ति","Who":"कसले","When":"कहिले","What changed":"के परिवर्तन भयो","S.N.":"क्र.सं.","All members":"सबै सदस्य","Everything":"सबै","None":"छैन","Not set":"तोकिएको छैन","Nothing recorded yet.":"अहिलेसम्म केही दर्ता गरिएको छैन।","Select all pending":"सबै बाँकी छान्नुहोस्","Clear selection":"छनोट हटाउनुहोस्","Record all":"सबै दर्ता गर्नुहोस्","Choose date":"मिति छान्नुहोस्","Choose month":"महिना छान्नुहोस्","Calculated":"गणना गरिएको","Months":"महिनाहरू","Group":"समूह","Money":"रकम","Indicator":"सूचक","Particulars":"विवरण","Reference":"सन्दर्भ","Receipts":"प्राप्ति","Payments":"भुक्तानी","Treatment":"व्यवहार","Entries":"प्रविष्टिहरू","Deduction":"कट्टी","Net":"खुद","Income":"आम्दानी","Fines":"जरिवाना","Active members":"सक्रिय सदस्य","Active Members":"सक्रिय सदस्य","Total savings":"कुल बचत","Total Savings":"कुल बचत","Loans outstanding":"बाँकी ऋण","Loans Outstanding":"बाँकी ऋण","Interest due":"तिर्न बाँकी ब्याज","Interest Due":"तिर्न बाँकी ब्याज","Interest and fines received":"प्राप्त ब्याज तथा जरिवाना","Interest And Fines Received":"प्राप्त ब्याज तथा जरिवाना","Cash in hand":"हातमा नगद","Cash In Hand":"हातमा नगद","Active borrowers":"ऋण लिइरहेका सदस्य","Active loans":"चालु ऋण","Members with pending deposits":"बचत बाँकी भएका सदस्य","Members with a loan running":"ऋण चलिरहेका सदस्य","Members who left":"छोडेका सदस्य","Pending months":"बाँकी महिना","Amount owed":"तिर्नुपर्ने रकम","Deposit status":"बचत स्थिति","Deposited this month":"यो महिना जम्मा गरेको","Group financial snapshot":"समूहको वित्तीय अवस्थाको सारांश","Group details":"समूहको विवरण","Pending members are shown first and the rest are alphabetically sorted. Search by name to work with a smaller list.":"बाँकी रहेका सदस्य पहिले देखिन्छन्, अरू नामको क्रमअनुसार छन्। सानो सूचीका लागि नामले खोज्नुहोस्।","Pending months are not counted as savings. If they have paid, record them on the Dashboard first.":"बाँकी महिनाहरू बचतमा गणना हुँदैनन्। सदस्यले तिरिसकेको भए पहिले ड्यासबोर्डमा दर्ता गर्नुहोस्।","Save member":"सदस्य सुरक्षित गर्नुहोस्","Update member":"सदस्य अपडेट गर्नुहोस्","Save members":"सदस्यहरू सुरक्षित गर्नुहोस्","Joined (applies to all)":"सामेल मिति (सबैलाई लागू)","Members, one per line: Name, Phone":"सदस्यहरू, प्रत्येक लाइनमा एक: नाम, फोन","Phone is optional. You can paste two columns (name and phone) straight from a spreadsheet. Names already in the group are skipped.":"फोन ऐच्छिक हो। स्प्रेडसिटबाट दुई स्तम्भ (नाम र फोन) सिधै टाँस्न सकिन्छ। समूहमा पहिल्यै भएका नाम छोडिन्छन्।","Sita Sharma, 9841000000 Ram Thapa, 9851000000 Gita Rai":"सीता शर्मा, 9841000000 राम थापा, 9851000000 गीता राई","Select the position from the dropdown to automatically configure user permissions. No email is sent: you give them a temporary password and they choose their own at first login.":"प्रयोगकर्ताको अधिकार स्वतः मिलाउन ड्रपडाउनबाट पद छान्नुहोस्। इमेल पठाइँदैन: तपाईंले अस्थायी पासवर्ड दिनुहुन्छ र उनीहरूले पहिलो लगइनमा आफ्नै पासवर्ड छान्छन्।","Select position / भूमिका छान्नुहोस्":"पद छान्नुहोस्","Save deposit":"बचत सुरक्षित गर्नुहोस्","Save loan":"ऋण सुरक्षित गर्नुहोस्","Save repayment":"भुक्तानी सुरक्षित गर्नुहोस्","Save selected deposits":"छानिएका बचत सुरक्षित गर्नुहोस्","Amount (Rs)":"रकम (रू)","Fine (Rs)":"जरिवाना (रू)","Loan (Rs)":"ऋण (रू)","Principal (Rs)":"सावाँ (रू)","Interest (Rs)":"ब्याज (रू)","Interest % / year":"ब्याज % / वर्ष","Monthly deposit":"मासिक बचत","Monthly deposit (Rs)":"मासिक बचत (रू)","Loan date":"ऋण मिति","Loan amount":"ऋण रकम","Loan balance":"ऋण बाँकी","Principal left":"बाँकी सावाँ","Original principal":"सुरुको सावाँ","Last repayment":"अन्तिम भुक्तानी","Interest charged":"लगाइएको ब्याज","Interest paid":"तिरेको ब्याज","Loan interest":"ऋणको ब्याज","Loan principal":"ऋणको सावाँ","Loan limit":"ऋण सीमा","Loan limit (times savings, 0 = no limit)":"ऋण सीमा (बचतको गुणा, ० = सीमा छैन)","Loan interest % / year":"ऋणको ब्याज % / वर्ष","Loan repayment":"ऋण भुक्तानी","Loan repayment received":"प्राप्त ऋण भुक्तानी","Loans disbursed":"वितरित ऋण","Loans given":"दिइएको ऋण","Loan disbursed":"वितरित ऋण","Loan principal disbursed":"वितरित ऋणको सावाँ","Loans still running":"चालु ऋण","Principal outstanding":"बाँकी सावाँ","Principal repaid":"तिरिएको सावाँ","Principal recovered":"असुल भएको सावाँ","Outstanding principal":"बाँकी सावाँ","Interest collected":"असुल भएको ब्याज","Interest received":"प्राप्त ब्याज","Interest receivable":"प्राप्त गर्नुपर्ने ब्याज","Interest paid this month":"यो महिना तिरेको ब्याज","Interest received on repayments":"भुक्तानीमा प्राप्त ब्याज","Interest still due":"अझै तिर्न बाँकी ब्याज","Fines collected":"असुल भएको जरिवाना","Fines received":"प्राप्त जरिवाना","Deposits collected":"संकलित बचत","Deposits recorded":"दर्ता गरिएको बचत","Total deposits":"कुल बचत जम्मा","Savings received":"प्राप्त बचत","Savings held":"रहेको बचत","Savings balance":"बचत मौज्दात","Savings paid":"फर्काइएको बचत","Savings refunded":"फिर्ता गरिएको बचत","Savings (principal) paid back, Rs":"फिर्ता गरिएको बचत (सावाँ), रू","Interest paid, Rs":"तिरिएको ब्याज, रू","Interest paid in exit settlements":"बहिर्गमन हिसाबमा तिरिएको ब्याज","Principal paid in exit settlements":"बहिर्गमन हिसाबमा तिरिएको सावाँ","Interest paid on exit":"बहिर्गमनमा तिरिएको ब्याज","Principal recovery; not income":"सावाँ असुली; आम्दानी होइन","Cash outflow; not an expense":"नगद बाहिरिएको; खर्च होइन","Settlement movement; not an expense":"हिसाब मिलान सम्बन्धी चाल; खर्च होइन","Penalty/fine receipts":"जरिवाना प्राप्ति","Rounding kept as income":"पूर्णाङ्क मिलान आम्दानीमा राखिएको","Net recorded income":"दर्ता भएको खुद आम्दानी","Net recorded interest and fine income":"दर्ता भएको खुद ब्याज तथा जरिवाना आम्दानी","Cash at month end":"महिनाको अन्त्यमा नगद","Cash available":"उपलब्ध नगद","Calculated cash balance":"गणना गरिएको नगद मौज्दात","Running balance":"चलिरहेको मौज्दात","Calculated from the app's recorded savings, fines, repayments, loan disbursements, and exit settlements. Reconcile against actual cash/bank records before treating this as a verified cash balance.":"एपमा दर्ता भएका बचत, जरिवाना, भुक्तानी, ऋण वितरण र बहिर्गमन हिसाबबाट गणना गरिएको। यसलाई प्रमाणित नगद मौज्दात मान्नुअघि वास्तविक नगद/बैंक अभिलेखसँग मिलान गर्नुहोस्।","Deposit Pending":"बाँकी बचत","Member balances and current position":"सदस्यको मौज्दात र हालको अवस्था","Savings, loans and repayments for one member":"एक सदस्यको बचत, ऋण र भुक्तानी","Previously recorded member departures":"पहिले दर्ता गरिएका सदस्य बहिर्गमन","Recorded receipts and payments":"दर्ता गरिएका प्राप्ति र भुक्तानी","Savings deposits and fines":"बचत जम्मा तथा जरिवाना","Loans issued, principal and interest":"दिइएका ऋण, सावाँ र ब्याज","Outstanding balances and recovery":"बाँकी रकम र असुली","Performance by month":"महिनागत कार्यसम्पादन","Recorded member earnings":"दर्ता भएको सदस्य आम्दानी","Summary of recorded income and expenses":"दर्ता भएको आम्दानी र खर्चको सारांश","Print / Save as PDF":"प्रिन्ट / PDF मा सुरक्षित गर्नुहोस्","Deposits":"बचत जम्मा","Loans":"ऋण","Repayments":"भुक्तानी","Loan Applications / Register":"ऋण दर्ता","Total (all members)":"जम्मा (सबै सदस्य)","Loan deducted":"कट्टा गरिएको ऋण","Loan deductions":"ऋण कट्टी","Interest share":"ब्याजको हिस्सा","Interest Share":"ब्याजको हिस्सा","Fine share":"जरिवानाको हिस्सा","Share of interest and fines":"ब्याज तथा जरिवानाको हिस्सा","Total earning":"कुल आम्दानी","Final amount":"अन्तिम रकम","Final settlement":"अन्तिम हिसाब","Settlement":"अन्तिम हिसाब","Total earning = savings + share of the interest and fines received so far, split in proportion to savings. Interest still due is not counted until it is paid.":"कुल आम्दानी = बचत + हालसम्म प्राप्त ब्याज तथा जरिवानाको हिस्सा, बचतको अनुपातमा बाँडिएको। तिर्न बाँकी ब्याज तिरेपछि मात्र गणना हुन्छ।","This is a management summary of amounts represented in the current app data, not a formal profit-and-loss statement. Operating expenses and a full double-entry chart of accounts are not currently recorded in this data model.":"यो एपमा रहेको तथ्याङ्कबाट बनेको व्यवस्थापकीय सारांश हो, औपचारिक नाफा-नोक्सान विवरण होइन। सञ्चालन खर्च र पूर्ण दोहोरो लेखा प्रणाली अहिलेको तथ्याङ्क संरचनामा दर्ता गरिएको छैन।","This report lists all past member exit settlements. Gross Settlement includes the refunded savings balance and the departing member's share of interest and fines. Loan Deductions shows the principal and interest cleared from the member's outstanding loans on their leaving date to determine the net amount paid.":"यो प्रतिवेदनमा विगतका सबै सदस्य बहिर्गमन हिसाब देखाइन्छ। कुल भुक्तानीमा फिर्ता हुने बचत तथा छोड्ने सदस्यको ब्याज र जरिवानाको हिस्सा समावेश हुन्छ। ऋण कट्टीले छोडेको मितिमा सदस्यको बाँकी ऋणको सावाँ र ब्याज देखाउँछ, जसबाट खुद भुक्तानी निकालिन्छ।","Gross Settlement":"कुल भुक्तानी","Loan Deductions":"ऋण कट्टी","Net Settlement":"खुद हिसाब","Exit Date":"बहिर्गमन मिति","Savings Refunded":"फिर्ता गरिएको बचत","Exited members":"बहिर्गमन भएका सदस्य","Earnings share paid":"तिरिएको आम्दानी हिस्सा","Total loan deductions":"कुल ऋण कट्टी","Net cash paid out":"खुद नगद भुक्तानी","Member earnings as of":"सदस्य आम्दानी","Member leaves the group":"सदस्यले समूह छोड्दै","Leaving date":"छोड्ने मिति","Left on":"छोडेको मिति","Record exit":"बहिर्गमन दर्ता गर्नुहोस्","Loan interest to deduct":"कट्टा गर्नुपर्ने ऋण ब्याज","Loan principal to deduct":"कट्टा गर्नुपर्ने ऋण सावाँ","These amounts are only suggestions. Change the principal or interest to whatever the group agrees to pay. Any loan and interest still due is deducted and recorded as a repayment. Money not paid out stays with the group and is shared as income by the remaining members. The member stops owing monthly deposits.":"यी रकम सुझाव मात्र हुन्। समूहले तिर्ने सहमति गरेअनुसार सावाँ वा ब्याज परिवर्तन गर्न सकिन्छ। तिर्न बाँकी ऋण र ब्याज कट्टा भई भुक्तानीका रूपमा दर्ता हुन्छ। नतिरिएको रकम समूहमै रहन्छ र बाँकी सदस्यहरूले आम्दानीका रूपमा बाँड्छन्। सदस्यले मासिक बचत तिर्नुपर्दैन।","Reopen group":"समूह पुनः खोल्नुहोस्","Close group":"समूह बन्द गर्नुहोस्","Final settlement (preview)":"अन्तिम हिसाब (पूर्वावलोकन)","To collect from members":"सदस्यहरूबाट असुल गर्नुपर्ने","To pay out to members":"सदस्यहरूलाई भुक्तानी गर्नुपर्ने","Type the group name to confirm":"पुष्टि गर्न समूहको नाम लेख्नुहोस्","Delete group permanently":"समूह स्थायी रूपमा मेटाउनुहोस्","Delete this group permanently":"यो समूह स्थायी रूपमा मेटाउनुहोस्","Data deletion":"तथ्याङ्क मेटाइ","Not offered as a routine maintenance action":"नियमित मर्मतको रूपमा उपलब्ध छैन","Permanently deletes this group and all its records from the cloud, for every user. Download a backup first. This cannot be undone.":"यसले यो समूह र यसका सबै अभिलेख क्लाउडबाट सबै प्रयोगकर्ताका लागि स्थायी रूपमा मेटाउँछ। पहिले ब्याकअप डाउनलोड गर्नुहोस्। यो फिर्ता हुँदैन।","Dissolving closes the books. Interest and pending deposits are frozen as of today, every member's final amount is fixed, and all records become read-only. A backup file is downloaded automatically. You can reopen the group afterwards if this was a mistake.":"विघटन गर्दा हिसाब बन्द हुन्छ। ब्याज र बाँकी बचत आजकै मितिमा स्थिर हुन्छन्, प्रत्येक सदस्यको अन्तिम रकम निश्चित हुन्छ र सबै अभिलेख हेर्न मात्र मिल्छ। ब्याकअप फाइल स्वतः डाउनलोड हुन्छ। गल्ती भएमा पछि समूह पुनः खोल्न सकिन्छ।","Dissolving the group, restoring backups, deleting the group and managing users stay strictly with you, the Admin (Owner).":"समूह विघटन, ब्याकअप पुनर्स्थापना, समूह मेटाउने र प्रयोगकर्ता व्यवस्थापन तपाईं (प्रशासक/मालिक) कै जिम्मामा मात्र रहन्छ।","Records are read-only. Tick each member once their final amount has been paid out, or collected from them if they owe the group.":"अभिलेख हेर्न मात्र मिल्छ। प्रत्येक सदस्यको अन्तिम रकम भुक्तानी भएपछि (वा समूहलाई तिर्नुपर्ने भए असुल भएपछि) चिह्न लगाउनुहोस्।","Final amount = savings + share of interest and fines - loan principal - interest still due. Interest still due is taken from the borrower and shared among all members in proportion to savings, like other interest. \"Owes\" means the loan is bigger than the member's payout, so that member must pay the group. Amounts are rounded to whole rupees.":"अन्तिम रकम = बचत + ब्याज तथा जरिवानाको हिस्सा − ऋणको सावाँ − तिर्न बाँकी ब्याज। तिर्न बाँकी ब्याज ऋणीबाट असुल गरी अरू ब्याजसरह बचतको अनुपातमा सबै सदस्यबीच बाँडिन्छ। \"तिर्नुपर्ने\" भनेको ऋण सदस्यको भुक्तानी रकमभन्दा ठूलो भएको हो, त्यसैले ती सदस्यले समूहलाई तिर्नुपर्छ। रकम पूर्ण रुपैयाँमा मिलाइएको हुन्छ।","The estimated close date is only a planning date. The firm is not closed automatically. Use":"अनुमानित बन्द मिति योजना बनाउने मिति मात्र हो। समूह स्वतः बन्द हुँदैन।","Tip: use":"सुझाव:","instead of Remove when someone leaves. Their name stays in the activity log and they can be enabled again later.":"कोही छोडेमा \"हटाउनुहोस्\" को सट्टा प्रयोग गर्नुहोस्। उनको नाम गतिविधि इतिहासमा रहन्छ र पछि फेरि सक्रिय गर्न सकिन्छ।","when the firm has actually finished.":"समूहको काम वास्तवमै सकिएपछि मात्र।","This group was closed on":"यो समूह बन्द गरिएको मिति","tab.":"ट्याब।",". Its records are read-only. You can reopen it from the Dissolve/Settlement section.":"। यसका अभिलेख हेर्न मात्र मिल्छ। तपाईं विघटन/हिसाब खण्डबाट पुनः खोल्न सक्नुहुन्छ।","Group information":"समूहको जानकारी","Group rules":"समूहका नियम","Security":"सुरक्षा","Backup & Restore":"ब्याकअप तथा पुनर्स्थापना","Backup & restore":"ब्याकअप तथा पुनर्स्थापना","Save group information":"समूहको जानकारी सुरक्षित गर्नुहोस्","Group start date":"समूह सुरु मिति","Estimated close date":"अनुमानित बन्द मिति","Estimated group close date":"अनुमानित समूह बन्द मिति","Firm description":"समूहको विवरण","System created on":"प्रणालीमा बनाइएको मिति","Running for":"सञ्चालन अवधि","Last backup":"अन्तिम ब्याकअप","Not downloaded yet":"अहिलेसम्म डाउनलोड गरिएको छैन","Group ID":"समूह आईडी","Download backup":"ब्याकअप डाउनलोड गर्नुहोस्","Restore from backup":"ब्याकअपबाट पुनर्स्थापना गर्नुहोस्","Group start date is the date the group actually began operating. It may be earlier than the date this software was set up, so historical members, deposits, loans and repayments can be entered from that date onward.":"समूह सुरु मिति भनेको समूहले वास्तवमै काम सुरु गरेको मिति हो। यो सफ्टवेयर सेटअप गरेको मितिभन्दा अघि हुन सक्छ, त्यसैले पुराना सदस्य, बचत, ऋण र भुक्तानी त्यस मितिदेखि प्रविष्ट गर्न सकिन्छ।","Monthly deposit, interest rate and loan limit can be changed in Settings.":"मासिक बचत, ब्याज दर र ऋण सीमा सेटिङमा परिवर्तन गर्न सकिन्छ।","Data is saved in the central cloud database. Download a backup after important meetings as an additional safety copy. Passwords are not included in backups.":"तथ्याङ्क केन्द्रीय क्लाउड डाटाबेसमा सुरक्षित हुन्छ। महत्त्वपूर्ण बैठकपछि थप सुरक्षाका लागि ब्याकअप डाउनलोड गर्नुहोस्। ब्याकअपमा पासवर्ड समावेश हुँदैन।","Download a backup before important changes. Restoring replaces this group's current cloud data, so verify the file and confirm carefully. This page does not change the live database schema.":"महत्त्वपूर्ण परिवर्तनअघि ब्याकअप डाउनलोड गर्नुहोस्। पुनर्स्थापनाले यस समूहको हालको क्लाउड तथ्याङ्क बदल्छ, त्यसैले फाइल जाँचेर होसियारीसाथ पुष्टि गर्नुहोस्। यो पृष्ठले लाइभ डाटाबेस संरचना परिवर्तन गर्दैन।","Cloud storage":"क्लाउड भण्डारण","Connection":"जडान","Supabase":"Supabase","No invite code system":"निमन्त्रणा कोड प्रणाली छैन","Online connection required to save changes":"परिवर्तन सुरक्षित गर्न अनलाइन जडान आवश्यक छ","Users are created by the group Owner under Users & Roles. Each person can do only what the Owner has allowed for their position.":"प्रयोगकर्ता समूहका मालिकले \"प्रयोगकर्ता तथा भूमिका\" अन्तर्गत बनाउँछन्। प्रत्येक व्यक्तिले आफ्नो पदका लागि मालिकले दिएको अधिकारभित्र मात्र काम गर्न सक्छन्।","Create a user":"प्रयोगकर्ता बनाउनुहोस्","Create user":"प्रयोगकर्ता बनाउनुहोस्","Full name":"पूरा नाम","Email (their login)":"इमेल (उनीहरूको लगइन)","Reset password":"पासवर्ड रिसेट","Temporary password":"अस्थायी पासवर्ड","Copy password":"पासवर्ड प्रतिलिपि गर्नुहोस्","Done, hide it":"भयो, लुकाउनुहोस्","Disable":"निष्क्रिय गर्नुहोस्","Enable":"सक्रिय गर्नुहोस्","No users yet.":"अहिलेसम्म कुनै प्रयोगकर्ता छैनन्।","Change powers":"अधिकार परिवर्तन गर्नुहोस्","Quick Preset:":"द्रुत चयन:","Add / edit members":"सदस्य थप्ने / सम्पादन गर्ने","Enter deposits":"बचत प्रविष्ट गर्ने","Give loans":"ऋण दिने","Record repayments":"ऋण भुक्तानी दर्ता गर्ने","Record member exits":"सदस्य बहिर्गमन दर्ता गर्ने","Edit or delete past records":"पुराना अभिलेख सम्पादन वा मेटाउने","Change group rules & details":"समूहका नियम तथा विवरण परिवर्तन गर्ने","Manager (Full Financial)":"व्यवस्थापक (पूर्ण आर्थिक)","Accountant (Entries & Exits)":"लेखापाल (प्रविष्टि तथा बहिर्गमन)","Member (Reports Only)":"सदस्य (प्रतिवेदन मात्र)","Admin (Owner · Full Access)":"प्रशासक (मालिक · पूर्ण अधिकार)","Member · Reports Only":"सदस्य · प्रतिवेदन मात्र","Reports Only":"प्रतिवेदन मात्र","Full Access":"पूर्ण अधिकार","Manager":"व्यवस्थापक","Accountant":"लेखापाल","Owner":"मालिक","Member (custom powers)":"सदस्य (अनुकूल अधिकार)","Who changed what, newest first. Recorded by the server, so it cannot be edited from the app. The last 400 days are kept.":"को ले के परिवर्तन गर्‍यो, नयाँ पहिले। सर्भरले दर्ता गर्ने भएकाले एपबाट सम्पादन गर्न मिल्दैन। पछिल्लो ४०० दिनको अभिलेख राखिन्छ।","Exit settlements":"बहिर्गमन हिसाब","Loan Approval":"ऋण स्वीकृति","Member Exit Approval":"सदस्य बहिर्गमन स्वीकृति","Group Dissolve Approval":"समूह विघटन स्वीकृति","Transaction Correction Approval":"कारोबार सच्याउने स्वीकृति","Group Rule Approval":"समूह नियम स्वीकृति","The secure Maker–Checker approval workflow is not yet connected in this build. Do not treat a request as approved from this screen.":"सुरक्षित तयार-गर्ने/जाँच-गर्ने (Maker–Checker) स्वीकृति प्रक्रिया यो संस्करणमा जोडिएको छैन। यो स्क्रिनबाट कुनै अनुरोध स्वीकृत भएको नमान्नुहोस्।","The secure Maker–Checker approval workflow is not yet connected in this build. Do not treat a settlement as approved from this screen.":"सुरक्षित तयार-गर्ने/जाँच-गर्ने (Maker–Checker) स्वीकृति प्रक्रिया यो संस्करणमा जोडिएको छैन। यो स्क्रिनबाट कुनै हिसाब स्वीकृत भएको नमान्नुहोस्।","Group dissolution requires a server-enforced approval and safe closure workflow. This screen is informational until that workflow is implemented.":"समूह विघटनका लागि सर्भरले लागू गर्ने स्वीकृति र सुरक्षित बन्द गर्ने प्रक्रिया आवश्यक छ। त्यो प्रक्रिया लागू नभएसम्म यो स्क्रिन जानकारीका लागि मात्र हो।","Corrections must preserve original transaction history. The secure approval workflow is not yet connected in this build.":"सच्याइँले मूल कारोबार इतिहास सुरक्षित राख्नुपर्छ। सुरक्षित स्वीकृति प्रक्रिया यो संस्करणमा जोडिएको छैन।","Rule changes must be approved and have prospective effective dates. The secure approval workflow is not yet connected in this build.":"नियम परिवर्तन स्वीकृत हुनुपर्छ र भविष्यको मितिदेखि लागू हुनुपर्छ। सुरक्षित स्वीकृति प्रक्रिया यो संस्करणमा जोडिएको छैन।","This menu item has its own destination and is not silently redirected to an unrelated feature. The missing workflow must be implemented and tested before it can be used for financial operations.":"यो मेनुको आफ्नै गन्तव्य छ र अर्कै सुविधामा चुपचाप पठाइँदैन। आर्थिक कामका लागि प्रयोग गर्नुअघि यो प्रक्रिया लागू गरी परीक्षण गर्नुपर्छ।","Group closed on":"समूह बन्द गरिएको मिति","Group Info":"समूह जानकारी","Group Management":"समूह व्यवस्थापन","Group closure":"समूह बन्द / विघटन","Group profile":"समूह प्रोफाइल","Your role":"तपाईंको भूमिका","Assign role":"भूमिका तोक्नुहोस्","Role":"भूमिका","Add manager":"व्यवस्थापक थप्नुहोस्","Today":"आज","Close":"बन्द गर्नुहोस्","BS month":"बि.सं. महिना","Previous":"अघिल्लो","Next":"अर्को"}; const NE_PAT=[["^As of (.+)$","{1} सम्मको अवस्था"],["^Month: (.+)$","महिना: {1}"],["^Closed on (.+)$","{1} मा बन्द गरिएको"],["^Generated on (.+)$","{1} मा तयार गरिएको"],["^All recorded cash movements through (.+)$","{1} सम्मका सबै दर्ता भएका नगद कारोबार"],["^All recorded member departures and settlements through (.+)$","{1} सम्मका सबै दर्ता भएका सदस्य बहिर्गमन तथा हिसाब"],["^Cumulative amounts through (.+)$","{1} सम्मको सञ्चित रकम"],["^Outstanding loans as of (.+)$","{1} सम्मका बाँकी ऋण"],["^Member earnings as of (.+)$","{1} सम्मको सदस्य आम्दानी"],["^Final settlement \\(preview as of (.+)\\)$","अन्तिम हिसाब ({1} सम्मको पूर्वावलोकन)"],["^Group closed on (.+)$","समूह {1} मा बन्द गरिएको"],["^This group was closed on (.+?)\\. Records are read-only\\. See the$","यो समूह {1} मा बन्द गरिएको छ। अभिलेख हेर्न मात्र मिल्छ। हेर्नुहोस्"],["^Loan #(\\d+)$","ऋण #{1}"],["^(\\d+(?:\\.\\d+)?)% per year$","वार्षिक {1}%"],["^(\\d+(?:\\.\\d+)?) times savings$","बचतको {1} गुणा"],["^Owes (Rs [\\d,\\-]+)$","तिर्नुपर्ने {1}"],["^Member pays the group: (Rs [\\d,\\-]+)$","सदस्यले समूहलाई तिर्नुपर्ने: {1}"],["^Group pays the member: (Rs [\\d,\\-]+)$","समूहले सदस्यलाई दिनुपर्ने: {1}"],["^(\\d+) of (\\d+)$","{2} मध्ये {1}"],["^(\\d+) member\\(s\\) still have pending deposits \\((.+)\\)\\.$","{1} जना सदस्यको बचत अझै बाँकी छ ({2})।"],["^(.+) \\(left\\)$","{1} (छोडेका)"],["^Left (.+)$","{1} मा छोडेको"],["^(\\d+) members?$","{1} जना सदस्य"],["^(\\d+) month\\(s\\)(, Rs [\\d,\\-]+)?$","{1} महिना{2}"],["^Showing (\\d+)\\u2013(\\d+) of (\\d+)$","{3} मध्ये {1}–{2} देखाइँदै"],["^Page (\\d+) of (\\d+)$","पृष्ठ {1} / {2}"],["^Phone: (.*)$","फोन: {1}"],["^Joined: (.+)$","सामेल मिति: {1}"],["^Pending deposits: (.+)$","बाँकी बचत: {1}"],["^As of: (.+)$","आजसम्म: {1}"],["^Dashboard \\u00b7 (.+)$","ड्यासबोर्ड · {1}"],["^Send to (.+)$","{1} लाई पठाउनुहोस्"]].map(([a,t])=>[new RegExp(a),t]); const NE_MSG={"This group has been closed, so its records are read-only.":"यो समूह बन्द गरिएको छ, त्यसैले अभिलेख हेर्न मात्र मिल्छ।","You do not have permission to do this. Ask the Owner to give you this power.":"तपाईंलाई यो काम गर्ने अनुमति छैन। मालिकसँग यो अधिकार माग्नुहोस्।","The deposit month cannot be before the month the group was created.":"बचत महिना समूह बनेको महिनाभन्दा अगाडिको हुन मिल्दैन।","A deposit cannot be made for a future month.":"भविष्यको महिनाका लागि बचत जम्मा गर्न मिल्दैन।","Nothing to save. Tick at least one member with an amount above zero.":"सुरक्षित गर्न केही छैन। शून्यभन्दा बढी रकम भएको कम्तीमा एक सदस्य छान्नुहोस्।","Choose the joining date.":"सामेल हुने मिति छान्नुहोस्।","The joining date cannot be in the future.":"सामेल मिति भविष्यको हुन मिल्दैन।","No members to save.":"सुरक्षित गर्ने सदस्य छैनन्।","Enter one member per line.":"प्रत्येक लाइनमा एक सदस्य लेख्नुहोस्।","You have changes that are not saved. Switch anyway?":"तपाईंका परिवर्तनहरू सुरक्षित गरिएका छैनन्। जे भए पनि बदल्ने?","The loan could not be cleared on this date. Choose a later leaving date.":"यो मितिमा ऋण चुक्ता गर्न सकिएन। पछिको छोड्ने मिति छान्नुहोस्।","This exit cannot be undone because the group cash would become negative.":"यो बहिर्गमन उल्टाउन मिल्दैन किनभने समूहको नगद ऋणात्मक हुन्छ।","Only the Admin can dissolve this group.":"यो समूह प्रशासकले मात्र विघटन गर्न सक्छ।","Add members before dissolving the group.":"समूह विघटन गर्नुअघि सदस्य थप्नुहोस्।","Type the group name exactly to confirm.":"पुष्टि गर्न समूहको नाम ठ्याक्कै लेख्नुहोस्।","Only the Admin can reopen this group.":"यो समूह प्रशासकले मात्र पुनः खोल्न सक्छ।","Reopen this group? The settlement and its settled ticks will be discarded, and records become editable again.":"यो समूह पुनः खोल्ने? हिसाब र फर्छ्यौटका चिह्नहरू हटाइनेछन् र अभिलेख फेरि सम्पादन गर्न मिल्नेछ।","Enter the member name.":"सदस्यको नाम लेख्नुहोस्।","A member with this name already exists.":"यो नामको सदस्य पहिल्यै छ।","Amounts cannot be negative.":"रकम ऋणात्मक हुन सक्दैन।","This change would make the group cash negative, because loans have already been given from it.":"यो परिवर्तनले समूहको नगद ऋणात्मक बनाउँछ, किनभने यसबाट ऋण पहिल्यै दिइसकिएको छ।","There is no active loan to repay.":"भुक्तानी गर्नुपर्ने चालु ऋण छैन।","Add a member first.":"पहिले सदस्य थप्नुहोस्।","Enter an amount above zero.":"शून्यभन्दा बढी रकम लेख्नुहोस्।","Choose the date and the month.":"मिति र महिना छान्नुहोस्।","Enter the principal or interest paid.":"तिरेको सावाँ वा ब्याज लेख्नुहोस्।","Principal is more than the loan balance as of this repayment date.":"भुक्तानी मितिसम्मको ऋण बाँकीभन्दा सावाँ बढी छ।","Interest paid is more than the interest due as of this repayment date.":"भुक्तानी मितिसम्म तिर्न बाँकी ब्याजभन्दा तिरेको ब्याज बढी छ।","No loan was given to this member on or before this date.":"यो मितिमा वा त्यसअघि यस सदस्यलाई कुनै ऋण दिइएको छैन।","Principal is more than the loan balance of this member.":"यस सदस्यको ऋण बाँकीभन्दा सावाँ बढी छ।","The principal is more than the loan balance of this member.":"यस सदस्यको ऋण बाँकीभन्दा सावाँ बढी छ।","The loan cannot be less than the principal already repaid.":"ऋण रकम पहिले नै तिरिसकेको सावाँभन्दा कम हुन सक्दैन।","Choose the date.":"मिति छान्नुहोस्।","The loan date cannot be later than its existing repayments.":"ऋण मिति यसका विद्यमान भुक्तानीहरूभन्दा पछिको हुन सक्दैन।","This deposit cannot be deleted because loans have already been given from the group cash.":"यो बचत मेटाउन मिल्दैन किनभने समूहको नगदबाट ऋण पहिल्यै दिइसकिएको छ।","This loan cannot be deleted because the group cash would become negative.":"यो ऋण मेटाउन मिल्दैन किनभने समूहको नगद ऋणात्मक हुन्छ।","This repayment cannot be deleted because the group cash would become negative.":"यो भुक्तानी मेटाउन मिल्दैन किनभने समूहको नगद ऋणात्मक हुन्छ।","This member has deposits, loans, or exit settlements recorded, so they cannot be deleted. Delete those entries first.":"यस सदस्यको बचत, ऋण वा बहिर्गमन हिसाब दर्ता भएकाले मेटाउन मिल्दैन। पहिले ती प्रविष्टि मेटाउनुहोस्।","Enter a valid Supabase project URL and publishable/anon key.":"मान्य Supabase परियोजना URL र publishable/anon key लेख्नुहोस्।","Cloud client is not ready. Reload the page and try again.":"क्लाउड क्लाइन्ट तयार छैन। पृष्ठ पुनः लोड गरेर फेरि प्रयास गर्नुहोस्।","Enter the group name.":"समूहको नाम लेख्नुहोस्।","Please log in again before creating a group.":"समूह बनाउनुअघि कृपया फेरि लगइन गर्नुहोस्।","Only an Admin or Super Admin can create users.":"प्रशासक वा सुपर प्रशासकले मात्र प्रयोगकर्ता बनाउन सक्छन्।","Enter the person's name.":"व्यक्तिको नाम लेख्नुहोस्।","Enter a valid email address.":"मान्य इमेल ठेगाना लेख्नुहोस्।","Please select a position (Manager, Accountant, or Member).":"कृपया पद छान्नुहोस् (व्यवस्थापक, लेखापाल वा सदस्य)।","No powers are ticked, so this person can only view. Continue?":"कुनै अधिकार छानिएको छैन, त्यसैले यो व्यक्तिले हेर्न मात्र सक्छ। जारी राख्ने?","The user could not be created.":"प्रयोगकर्ता बनाउन सकिएन।","Only the platform Super Admin can grant the Admin role.":"प्रशासक भूमिका प्लेटफर्मका सुपर प्रशासकले मात्र दिन सक्छन्।","Enter the name and position.":"नाम र पद लेख्नुहोस्।","Remove this person from the group completely? Disabling is usually better.":"यो व्यक्तिलाई समूहबाट पूरै हटाउने? सामान्यतया निष्क्रिय पार्नु राम्रो हुन्छ।","Use a password of at least 8 characters.":"कम्तीमा ८ अक्षरको पासवर्ड प्रयोग गर्नुहोस्।","The two passwords do not match.":"दुवै पासवर्ड मिलेनन्।","Enter your account email first.":"पहिले आफ्नो खाताको इमेल लेख्नुहोस्।","If this email belongs to an account, Supabase will send a secure password-reset link. Check Inbox and Spam.":"यो इमेल कुनै खाताको हो भने Supabase ले पासवर्ड रिसेट गर्ने सुरक्षित लिङ्क पठाउँछ। इनबक्स र स्प्याम जाँच्नुहोस्।","Cloud is not connected.":"क्लाउड जोडिएको छैन।","Password updated. Please log in with your new password.":"पासवर्ड अपडेट भयो। कृपया नयाँ पासवर्डले लगइन गर्नुहोस्।","Another user saved this group before you. Your unsaved change was not overwritten; the latest cloud data has been loaded. Please enter your change again.":"तपाईंभन्दा अघि अर्को प्रयोगकर्ताले यो समूह सुरक्षित गरेको छ। तपाईंको सुरक्षित नगरिएको परिवर्तन माथि लेखिएको छैन; क्लाउडको पछिल्लो तथ्याङ्क लोड गरिएको छ। कृपया आफ्नो परिवर्तन फेरि प्रविष्ट गर्नुहोस्।","Enter email and password.":"इमेल र पासवर्ड लेख्नुहोस्।","You have changes that are not saved. Log out anyway?":"तपाईंका परिवर्तनहरू सुरक्षित गरिएका छैनन्। जे भए पनि लगआउट गर्ने?","Only the Admin can restore a backup.":"ब्याकअप प्रशासकले मात्र पुनर्स्थापना गर्न सक्छ।","This file is not a valid backup.":"यो फाइल मान्य ब्याकअप होइन।","Replace this group's cloud data with the backup? This cannot be undone.":"यो समूहको क्लाउड तथ्याङ्क ब्याकअपले बदल्ने? यो फिर्ता हुँदैन।","This file is not a valid backup or could not be read.":"यो फाइल मान्य ब्याकअप होइन वा पढ्न सकिएन।","You do not have permission to change group rules.":"तपाईंलाई समूहका नियम परिवर्तन गर्ने अनुमति छैन।","You do not have permission to change group details.":"तपाईंलाई समूहको विवरण परिवर्तन गर्ने अनुमति छैन।","Group start date cannot be in the future.":"समूह सुरु मिति भविष्यको हुन सक्दैन।","Enter the new password.":"नयाँ पासवर्ड लेख्नुहोस्।","The two new passwords do not match.":"दुवै नयाँ पासवर्ड मिलेनन्।","Password changed successfully.":"पासवर्ड सफलतापूर्वक परिवर्तन भयो।","Permanent group deletion is disabled in this V2 test build. Use Administration → Group Closure to test closure workflows; do not call the legacy sb_delete_group function with a V2 firm ID.":"यो V2 परीक्षण संस्करणमा समूह स्थायी रूपमा मेट्ने सुविधा बन्द छ। बन्द गर्ने प्रक्रिया परीक्षण गर्न प्रशासन → समूह बन्द प्रयोग गर्नुहोस्।","Not enough cash in hand to pay this member.":"यो सदस्यलाई भुक्तानी गर्न हातमा पर्याप्त नगद छैन।","Not saved · Retry":"सुरक्षित भएन · फेरि प्रयास गर्नुहोस्","Saving…":"सुरक्षित गर्दै…","Saved":"सुरक्षित भयो","Skipped:":"छोडिएका:","Your change is kept on screen. Tap \"Not saved · Retry\" to try again.":"तपाईंको परिवर्तन स्क्रिनमै राखिएको छ। फेरि प्रयास गर्न \"सुरक्षित भएन · फेरि प्रयास\" थिच्नुहोस्।"}; const NE_MSGP=[["^Insufficient Cash Balance\\. The available group cash is insufficient to complete this transaction\\. Cash shortfall: (Rs [\\d,\\-]+)\\. This transaction cannot be recorded\\.$","नगद मौज्दात अपर्याप्त। यो कारोबार पूरा गर्न समूहको उपलब्ध नगद मौज्दात पर्याप्त छैन। नगद कमी: {1}। यो कारोबार दर्ता गर्न सकिँदैन।"],["^Record (\\d+) deposit\\(s\\) of (Rs [\\d,\\-]+) each for (.+), dated today\\?$","{3} का लागि प्रत्येक {2} का {1} बचत आजकै मितिमा दर्ता गर्ने?"],["^A deposit cannot be dated before the firm start date \\((.+)\\)\\.$","बचतको मिति समूह सुरु मिति ({1}) भन्दा अगाडिको हुन सक्दैन।"],["^A deposit cannot be dated in the future\\. Today is (.+)\\.$","बचतको मिति भविष्यको हुन सक्दैन। आज {1} हो।"],["^The joining date cannot be before the firm start date \\((.+)\\)\\.$","सामेल मिति समूह सुरु मिति ({1}) भन्दा अगाडिको हुन सक्दैन।"],["^(\\d+) member\\(s\\) added\\.(.*)$","{1} जना सदस्य थपियो।{2}"],["^Choose a leaving date between the firm start date \\((.+)\\) and today\\.$","समूह सुरु मिति ({1}) र आजको बीचको छोड्ने मिति छान्नुहोस्।"],["^Not enough cash in hand to pay this member\\. Available: (Rs [\\d,\\-]+)$","यो सदस्यलाई भुक्तानी गर्न हातमा पर्याप्त नगद छैन। उपलब्ध: {1}"],["^Record the exit of (.+)\\? (.+)\\.$","{1} को बहिर्गमन दर्ता गर्ने? {2}।"],["^Undo the exit of (.+)\\? The payout and loan deduction recorded with it are removed and the member becomes active again\\.$","{1} को बहिर्गमन उल्टाउने? यसका साथ दर्ता भएको भुक्तानी र ऋण कट्टी हटाइन्छ र सदस्य फेरि सक्रिय हुन्छन्।"],["^Dissolve \"(.+)\"\\? Records become read-only and final amounts are fixed\\. A backup file will be downloaded first\\.$","\"{1}\" विघटन गर्ने? अभिलेख हेर्न मात्र मिल्नेछ र अन्तिम रकम निश्चित हुनेछ। पहिले ब्याकअप फाइल डाउनलोड हुनेछ।"],["^The joining date cannot be before the firm start date \\((.+)\\)\\.$","सामेल मिति समूह सुरु मिति ({1}) भन्दा अगाडिको हुन सक्दैन।"],["^The deposit month cannot be before the member joined \\((.+)\\)\\.$","बचत महिना सदस्य सामेल भएको मिति ({1}) भन्दा अगाडिको हुन सक्दैन।"],["^This member already has a deposit for (.+)\\. Edit that deposit instead\\.$","यस सदस्यको {1} को बचत पहिल्यै दर्ता छ। बरु त्यही बचत सम्पादन गर्नुहोस्।"],["^Not enough cash in hand\\. Available: (Rs [\\d,\\-]+)$","हातमा पर्याप्त नगद छैन। उपलब्ध: {1}"],["^This is above the loan limit of (\\d+(?:\\.\\d+)?) times savings\\. This member can borrow up to (Rs [\\d,\\-]+) more\\.$","यो बचतको {1} गुणाको ऋण सीमाभन्दा बढी हो। यस सदस्यले थप {2} सम्म ऋण लिन सक्छन्।"],["^The date cannot be before the firm start date \\((.+)\\)\\.$","मिति समूह सुरु मिति ({1}) भन्दा अगाडिको हुन सक्दैन।"],["^The date cannot be in the future\\. Today is (.+)\\.$","मिति भविष्यको हुन सक्दैन। आज {1} हो।"],["^A repayment cannot be dated before the loan was given \\((.+)\\)\\.$","ऋण दिइएको मिति ({1}) भन्दा अगाडिको मितिमा भुक्तानी दर्ता गर्न मिल्दैन।"],["^Rs ([\\d,\\-]+) extra principal was recorded as interest income of the group\\.$","थप सावाँ रू {1} समूहको ब्याज आम्दानीका रूपमा दर्ता गरियो।"],["^Delete the deposit of (Rs [\\d,\\-]+) by (.+) for (.+)\\?$","{2} को {3} का लागि {1} को बचत मेटाउने?"],["^Delete the repayment of (Rs [\\d,\\-]+) dated (.+)\\?$","{2} मितिको {1} को भुक्तानी मेटाउने?"],["^Delete the member (.+)\\?$","सदस्य {1} लाई मेटाउने?"],["^Could not load your groups: (.+)$","तपाईंका समूह लोड गर्न सकिएन: {1}"],["^Could not finish creating the firm: (.+)$","समूह बनाउने काम पूरा गर्न सकिएन: {1}"],["^Make a new temporary password for (.+)\\? Their old password will stop working\\.$","{1} का लागि नयाँ अस्थायी पासवर्ड बनाउने? उनको पुरानो पासवर्ड काम गर्दैन।"],["^Disable (.+)\\? They will not be able to open or change this group until you enable them again\\.$","{1} लाई निष्क्रिय गर्ने? तपाईंले फेरि सक्रिय नगरेसम्म उनले यो समूह खोल्न वा परिवर्तन गर्न सक्दैनन्।"],["^Password recovery could not be requested: (.+)$","पासवर्ड पुनःप्राप्तिको अनुरोध गर्न सकिएन: {1}"],["^Password could not be updated: (.+)$","पासवर्ड अपडेट गर्न सकिएन: {1}"],["^Cloud save failed: (.+)$","क्लाउडमा सुरक्षित गर्न असफल भयो: {1}"],["^Delete the loan of (Rs [\\d,\\-]+) for (.+) and its (\\d+) repayment\\(s\\)\\?$","{2} को {1} को ऋण र यसका {3} भुक्तानी मेटाउने?"],["^Delete the loan of (Rs [\\d,\\-]+) for (.+)\\?$","{2} को {1} को ऋण मेटाउने?"],["^Line (\\d+): name is missing$","लाइन {1}: नाम छुटेको छ"],["^Line (\\d+): (.+) \\(already in the group or repeated\\)$","लाइन {1}: {2} (समूहमा पहिल्यै छ वा दोहोरिएको)"]].map(([a,t])=>[new RegExp(a),t]); /* ===== Nepali improvements: corrected terms, new screens, role/permission texts ===== */ Object.assign(UI_NE,{"Administration": "प्रशासन", "Edit": "सम्पादन गर्नुहोस्", "Log in": "लगइन गर्नुहोस्", "Log out": "लगआउट गर्नुहोस्", "Reset password": "पासवर्ड रिसेट गर्नुहोस्", "Super Admin": "सुपर प्रशासक", "Viewer": "हेर्ने प्रयोगकर्ता", "Admin · Full Access": "प्रशासक · पूर्ण अधिकार", "Admin (Full Access)": "प्रशासक (पूर्ण अधिकार)", "Member · Custom permissions": "सदस्य · अनुकूल अधिकार", "Member · Reports only": "सदस्य · प्रतिवेदन मात्र", "Login Id": "लगइन आईडी", "Enter Login Id": "लगइन आईडी लेख्नुहोस्", "User Name": "प्रयोगकर्ता नाम", "Unique username": "अद्वितीय प्रयोगकर्ता नाम", "Account created": "खाता बनेको मिति", "Account menu": "खाता मेनु", "Actions": "कार्यहरू", "Add Existing User": "पहिलेको प्रयोगकर्ता थप्नुहोस्", "Add Existing User to This Group": "पहिलेदेखि खाता भएको प्रयोगकर्तालाई यो समूहमा थप्नुहोस्", "Address": "ठेगाना", "Group Address": "समूहको ठेगाना", "Group address": "समूहको ठेगाना", "Authorized Person": "अधिकृत व्यक्ति", "Authorized person": "अधिकृत व्यक्ति", "Authorized person's full name": "अधिकृत व्यक्तिको पूरा नाम", "Cash book and deposit records": "नगद किताब तथा बचत अभिलेख", "Change monthly deposit, interest rate, loan limit and group details": "मासिक बचत, ब्याज दर, ऋण सीमा र समूहको विवरण परिवर्तन गर्न", "Changing your email may require verification from your new email address.": "इमेल परिवर्तन गर्दा नयाँ इमेलबाट प्रमाणीकरण आवश्यक पर्न सक्छ।", "Choose a group to open.": "खोल्नुपर्ने समूह छान्नुहोस्।", "Choose a temporary password of at least 8 characters and give it securely to the user. They will be asked to set their own password at first login.": "कम्तीमा ८ अक्षरको अस्थायी पासवर्ड छानेर प्रयोगकर्तालाई सुरक्षित तरिकाले दिनुहोस्। पहिलो लगइनमा उनीहरूले आफ्नै पासवर्ड राख्नुपर्नेछ।", "Clear all": "सबै हटाउनुहोस्", "Tick all": "सबै छान्नुहोस्", "Use role preset": "भूमिकाअनुसार अधिकार मिलाउनुहोस्", "Created on": "बनाइएको मिति", "Created today": "आज बनाइएको", "Current / temporary password": "हालको / अस्थायी पासवर्ड", "Current Group Profile": "हालको समूह प्रोफाइल", "Current account": "हालको खाता", "Delete permanently": "स्थायी रूपमा मेटाउनुहोस्", "Disabled": "निष्क्रिय", "Edit or delete past deposits, loans and repayments": "पुराना बचत, ऋण र भुक्तानी सम्पादन वा मेटाउने", "Edit role": "भूमिका सम्पादन गर्नुहोस्", "Edit user access": "प्रयोगकर्ताको पहुँच सम्पादन गर्नुहोस्", "Enter monthly savings, single or bulk": "मासिक बचत एकल वा सामूहिक रूपमा प्रविष्ट गर्न", "Enter temporary password": "अस्थायी पासवर्ड लेख्नुहोस्", "Enter your account password to confirm": "पुष्टि गर्न आफ्नो खाताको पासवर्ड लेख्नुहोस्", "Your login password": "तपाईंको लगइन पासवर्ड", "Existing account email": "पहिलेको खाताको इमेल", "Exit settlement": "बहिर्गमन हिसाब", "Give new loans and edit loan details": "नयाँ ऋण दिन र ऋणको विवरण सम्पादन गर्न", "Group Information & Rules": "समूह जानकारी तथा नियम", "Group description": "समूहको विवरण", "Group settlement": "समूह हिसाब", "Last login": "अन्तिम लगइन", "Left": "छोडेको", "Loan interest rate": "ऋणको ब्याज दर", "Loan records": "ऋणका अभिलेख", "Loans, balances and recovery": "ऋण, बाँकी रकम र असुली", "Logging in…": "लगइन हुँदै…", "Manage users": "प्रयोगकर्ता व्यवस्थापन", "Member Information": "सदस्य जानकारी", "Money entries": "रकम सम्बन्धी प्रविष्टि", "People": "सदस्य सम्बन्धी", "Records & rules": "अभिलेख तथा नियम", "Monthly trends and member earnings": "मासिक प्रवृत्ति तथा सदस्य आम्दानी", "Never": "कहिल्यै गरेको छैन", "No email available": "इमेल उपलब्ध छैन", "No group selected": "कुनै समूह छानिएको छैन", "No limit": "सीमा छैन", "No powers: view only": "कुनै अधिकार छैन: हेर्न मात्र", "Not joined": "सामेल भएको छैन", "Not provided": "उल्लेख गरिएको छैन", "Not recorded": "दर्ता गरिएको छैन", "Owes": "तिर्नुपर्ने", "Paid": "तिरेको", "Permissions": "अधिकारहरू", "Permissions for this group": "यो समूहका लागि अधिकारहरू", "Phone Number": "फोन नम्बर", "Phone number": "फोन नम्बर", "Portfolio, statements and exit history": "सदस्य सारांश, विवरणपत्र र बहिर्गमन इतिहास", "Principal at start of month": "महिनाको सुरुमा सावाँ", "Ran for": "सञ्चालन भएको अवधि", "Read-only overview of this group’s information, rules, members and financial position.": "यो समूहको जानकारी, नियम, सदस्य र आर्थिक अवस्थाको हेर्न मात्र मिल्ने सारांश।", "Record a member leaving and settle their payout": "सदस्य छोडेको दर्ता गरी भुक्तानी मिलाउन", "Record principal and interest repayments": "सावाँ र ब्याजको भुक्तानी दर्ता गर्न", "Recorded income and expense summary": "दर्ता भएको आम्दानी र खर्चको सारांश", "Register new members and edit their details": "नयाँ सदस्य दर्ता गर्न र विवरण सम्पादन गर्न", "Save Group Management": "समूह व्यवस्थापन सुरक्षित गर्नुहोस्", "Save role and permissions": "भूमिका र अधिकार सुरक्षित गर्नुहोस्", "Select role": "भूमिका छान्नुहोस्", "Select a role.": "भूमिका छान्नुहोस्।", "Please select a role.": "कृपया भूमिका छान्नुहोस्।", "Selecting Manager or Accountant loads a preset. You can then fine-tune individual permissions.": "व्यवस्थापक वा लेखापाल छान्दा अधिकारको पूर्वनिर्धारित सूची आउँछ। त्यसपछि प्रत्येक अधिकार आवश्यकताअनुसार मिलाउन सकिन्छ।", "Sensitive": "संवेदनशील", "Total deposits recorded": "दर्ता गरिएको कुल बचत", "Total member records": "कुल सदस्य अभिलेख", "Undo": "उल्टाउनुहोस्", "Unnamed user": "नाम नतोकिएको प्रयोगकर्ता", "View only": "हेर्न मात्र", "Open report": "प्रतिवेदन खोल्नुहोस्", "Update the person’s display name, role preset, and individual permissions for this group.": "यो समूहका लागि व्यक्तिको नाम, भूमिका र प्रत्येक अधिकार अद्यावधिक गर्नुहोस्।", "User management, the audit log, backup/restore and closing the group are always Admin-only.": "प्रयोगकर्ता व्यवस्थापन, लेखापरीक्षण इतिहास, ब्याकअप/पुनर्स्थापना र समूह बन्द गर्ने काम सधैं प्रशासकका लागि मात्र हुन्।", "Admin and Super Admin roles cannot be granted here by a regular Admin.": "साधारण प्रशासकले यहाँबाट प्रशासक वा सुपर प्रशासकको भूमिका दिन सक्दैन।", "Enter the temporary password you used to sign in, then choose a new password of at least 8 characters.": "लगइन गर्दा प्रयोग गरेको अस्थायी पासवर्ड लेख्नुहोस्, त्यसपछि कम्तीमा ८ अक्षरको नयाँ पासवर्ड छान्नुहोस्।", "Current / Temporary Password": "हालको / अस्थायी पासवर्ड", "You can view this list but you do not have the power to add entries here. Ask the Owner if you need it.": "तपाईं यो सूची हेर्न सक्नुहुन्छ तर यहाँ प्रविष्टि थप्ने अधिकार छैन। आवश्यक परे मालिकसँग माग्नुहोस्।", "You can view exits but you do not have the power to record them. Ask the Owner if you need it.": "तपाईं बहिर्गमन हेर्न सक्नुहुन्छ तर दर्ता गर्ने अधिकार छैन। आवश्यक परे मालिकसँग माग्नुहोस्।", "You can view group details and rules. Ask the Admin to make changes.": "तपाईं समूहको विवरण र नियम हेर्न सक्नुहुन्छ। परिवर्तन गर्न प्रशासकसँग अनुरोध गर्नुहोस्।", "Only the Admin can change the group name or start date.": "समूहको नाम वा सुरु मिति प्रशासकले मात्र परिवर्तन गर्न सक्छ।", "Only the Admin can dissolve or reopen this group.": "यो समूह प्रशासकले मात्र विघटन वा पुनः खोल्न सक्छ।", "Only the Admin can view the activity log.": "गतिविधि इतिहास प्रशासकले मात्र हेर्न सक्छ।", "Only an Admin or Super Admin can manage users and roles.": "प्रयोगकर्ता र भूमिका प्रशासक वा सुपर प्रशासकले मात्र व्यवस्थापन गर्न सक्छ।", "This page displays current group details for reading. To change group settings or rules, use the appropriate Administration options if your role allows it.": "यो पृष्ठले समूहको हालको विवरण हेर्न मात्र देखाउँछ। समूहको सेटिङ वा नियम परिवर्तन गर्न तपाईंको भूमिकाले मिल्छ भने प्रशासनका सम्बन्धित विकल्प प्रयोग गर्नुहोस्।", "This password is shown only once. The person must choose their own password the first time they log in.": "यो पासवर्ड एक पटक मात्र देखाइन्छ। पहिलो पटक लगइन गर्दा सम्बन्धित व्यक्तिले आफ्नै पासवर्ड छान्नुपर्छ।", "Your accounting records are stored in Supabase. The browser only keeps the login session, cloud configuration and a small cache.": "तपाईंको लेखा अभिलेख Supabase मा सुरक्षित हुन्छ। ब्राउजरले लगइन सत्र, क्लाउड सेटिङ र सानो क्यास मात्र राख्छ।", "Connect this app to your Supabase project. Never use a service_role key in this HTML.": "यो एपलाई आफ्नो Supabase परियोजनासँग जोड्नुहोस्। यो HTML मा कहिल्यै service_role key प्रयोग नगर्नुहोस्।", "There are no active members.": "सक्रिय सदस्य छैनन्।", "You only have access to this group.": "तपाईंसँग यही समूहको मात्र पहुँच छ।", "Group closure status changed": "समूह बन्द हुने स्थिति परिवर्तन भयो", "Group details changed": "समूहको विवरण परिवर्तन भयो", "Group rules changed": "समूहका नियम परिवर्तन भयो", "Member exit status changed": "सदस्य बहिर्गमन स्थिति परिवर्तन भयो", "Unknown member": "अज्ञात सदस्य", "(removed member)": "(हटाइएको सदस्य)", "(removed loan)": "(हटाइएको ऋण)", "Saved": "सुरक्षित भयो", "Signed in": "लगइन गरिएको", "e.g. Sanibar Samuha Bachat": "जस्तै: सानीबार समूह बचत", "Existing account added to this group. They should sign in with their existing email and password.": "पहिलेदेखिको खाता यो समूहमा थपियो। उनीहरूले आफ्नै इमेल र पासवर्डले लगइन गर्नुपर्छ।"}); Object.assign(UI_NE,{"Exit Settlement Before Fine":"जरिवाना अघिको बहिर्गमन हिसाब","Exit Fine, Rs":"बहिर्गमन जरिवाना, रू","Final Settlement":"अन्तिम हिसाब","Reason":"कारण","Allocated earnings":"बाँडिएको आम्दानी","Unpaid fines":"तिर्न बाँकी जरिवाना","Savings":"बचत","Loan principal":"ऋणको सावाँ","Loan interest":"ऋणको ब्याज","Pending":"बाँकी","Partially Settled":"आंशिक हिसाब मिलान","Fully Settled":"पूरा हिसाब मिलान","Record Exit":"बहिर्गमन दर्ता गर्नुहोस्","Member Exit":"सदस्य बहिर्गमन","Member":"सदस्य","Leaving date":"बहिर्गमन मिति","There are no active members.":"सक्रिय सदस्य छैनन्।"}); Object.assign(NE_MSG,{"Only an Admin or Super Admin can add users.": "प्रशासक वा सुपर प्रशासकले मात्र प्रयोगकर्ता थप्न सक्छन्।", "Enter the person's full name.": "व्यक्तिको पूरा नाम लेख्नुहोस्।", "Enter the user address.": "प्रयोगकर्ताको ठेगाना लेख्नुहोस्।", "Enter a username without spaces.": "खाली ठाउँ नभएको प्रयोगकर्ता नाम लेख्नुहोस्।", "Enter a valid email address or leave it blank.": "मान्य इमेल ठेगाना लेख्नुहोस् वा खाली छोड्नुहोस्।", "Only the platform Super Admin can create an Admin.": "प्रशासक प्लेटफर्मका सुपर प्रशासकले मात्र बनाउन सक्छन्।", "Only the platform Super Admin can assign Admin.": "प्रशासकको भूमिका प्लेटफर्मका सुपर प्रशासकले मात्र दिन सक्छन्।", "There can be only one Super Admin.": "सुपर प्रशासक एक जना मात्र हुन सक्छ।", "Enter a temporary password of at least 8 characters.": "कम्तीमा ८ अक्षरको अस्थायी पासवर्ड लेख्नुहोस्।", "Enter the existing account email.": "पहिलेको खाताको इमेल लेख्नुहोस्।", "Select a role.": "भूमिका छान्नुहोस्।", "Please select a role.": "कृपया भूमिका छान्नुहोस्।", "For safety, this action cannot delete an Admin, Super Admin, or your own account.": "सुरक्षाका लागि यो कार्यले प्रशासक, सुपर प्रशासक वा तपाईंको आफ्नै खाता मेटाउन सक्दैन।", "Enter your account password to confirm.": "पुष्टि गर्न आफ्नो खाताको पासवर्ड लेख्नुहोस्।", "Password confirmation failed. Please check your password and try again.": "पासवर्ड पुष्टि भएन। कृपया पासवर्ड जाँचेर फेरि प्रयास गर्नुहोस्।", "Enter your current / temporary password.": "हालको / अस्थायी पासवर्ड लेख्नुहोस्।", "Choose a new password different from the temporary password.": "अस्थायी पासवर्डभन्दा फरक नयाँ पासवर्ड छान्नुहोस्।", "Enter your current password.": "हालको पासवर्ड लेख्नुहोस्।", "Current password is incorrect.": "हालको पासवर्ड मिलेन।", "Enter your full name.": "आफ्नो पूरा नाम लेख्नुहोस्।", "Profile details updated.": "प्रोफाइल विवरण अपडेट भयो।", "Profile details saved. Check your new email to verify the address.": "प्रोफाइल विवरण सुरक्षित भयो। ठेगाना प्रमाणित गर्न आफ्नो नयाँ इमेल हेर्नुहोस्।", "Enter Login Id and password.": "लगइन आईडी र पासवर्ड लेख्नुहोस्।", "Enter Group Name, Group Address, and Authorized Person.": "समूहको नाम, ठेगाना र अधिकृत व्यक्ति लेख्नुहोस्।", "This loan has a repayment recorded together with a member exit. Undo the exit first.": "यो ऋणको एउटा भुक्तानी सदस्य बहिर्गमनसँगै दर्ता भएको छ। पहिले बहिर्गमन उल्टाउनुहोस्।", "This repayment was recorded together with a member exit. Undo the exit on the Member Exit page instead.": "यो भुक्तानी सदस्य बहिर्गमनसँगै दर्ता भएको हो। बरु सदस्य बहिर्गमन पृष्ठमा गएर बहिर्गमन उल्टाउनुहोस्।", "Password recovery needs your email address. Type your email (not your username) in the login box, or ask the Admin to reset your password.": "पासवर्ड पुनःप्राप्तिका लागि इमेल ठेगाना चाहिन्छ। लगइन बाकसमा प्रयोगकर्ता नामको सट्टा इमेल लेख्नुहोस्, वा प्रशासकसँग पासवर्ड रिसेट गर्न भन्नुहोस्।", "Could not reach the user service.": "प्रयोगकर्ता सेवासँग सम्पर्क हुन सकेन।", "Could not reset the password.": "पासवर्ड रिसेट गर्न सकिएन।", "Could not add the existing account.": "पहिलेको खाता थप्न सकिएन।", "Close and settle the group before archiving.": "स्थायी रूपमा मेट्नुअघि समूह बन्द गरी हिसाब मिलाउनुहोस्।", "Only the group Admin can archive this group.": "यो समूह समूहका प्रशासक वा सुपर प्रशासकले मात्र स्थायी रूपमा मेट्न सक्छन्।", "Your signed-in account email is unavailable. Please log in again.": "तपाईंको लगइन खाताको इमेल उपलब्ध छैन। कृपया फेरि लगइन गर्नुहोस्।", "Username login is not configured on the server yet. Update the manage-user Edge Function to support resolve_username.": "सर्भरमा प्रयोगकर्ता नामबाट लगइन अझै सेट गरिएको छैन। manage-user Edge Function मा resolve_username समर्थन थप्नुहोस्।", "Existing account added to this group. They should sign in with their existing email and password.": "पहिलेदेखिको खाता यो समूहमा थपियो। उनीहरूले आफ्नै इमेल र पासवर्डले लगइन गर्नुपर्छ।", "User membership permanently removed from this group. Their access to other groups, if any, is unchanged.": "प्रयोगकर्ताको सदस्यता यो समूहबाट स्थायी रूपमा हटाइयो। अन्य समूहमा भएको पहुँच (भए) परिवर्तन भएको छैन।"}); Object.assign(NE_MSG,{ "A loan cannot be dated before this member joined the group (":"ऋण मिति सदस्य समूहमा सामेल भएको मितिभन्दा अघिको हुन मिल्दैन (", "The member joining date is missing. Correct the member record first.":"सदस्यको सामेल मिति उपलब्ध छैन। पहिले सदस्यको विवरण सच्याउनुहोस्।", "Loan date must be on or after the member's joining date.":"ऋण मिति सदस्य सामेल भएको मिति वा त्यसपछिको हुनुपर्छ।", "Existing loan record has a date before the member's joining date. Correct that loan before saving other changes.":"पुरानो ऋण अभिलेख सदस्य सामेल भएको मितिभन्दा अघिको छ। अन्य परिवर्तन सुरक्षित गर्नुअघि त्यो ऋण सच्याउनुहोस्।" }); NE_PAT.push([new RegExp("^Matches the (.+) preset$"),"{1} को पूर्वनिर्धारित अधिकारसँग मिल्छ"]); NE_PAT.push([new RegExp("^Custom powers \\((\\d+) of (\\d+)\\)$"),"अनुकूल अधिकार ({2} मध्ये {1})"]); NE_PAT.push([new RegExp("^Not joined yet in (.+)$"),"{1} मा अझै सामेल भएको छैन"]); NE_PAT.push([new RegExp("^Already paid for (.+)$"),"{1} को बचत तिरिसकेको"]); NE_PAT.push([new RegExp("^Member exited: (.+)$"),"सदस्य बहिर्गमन भयो: {1}"]); NE_PAT.push([new RegExp("^(Members|Deposits|Loans|Repayments|Exit settlements): (\\d+) added(.*)$"),"{1}: {2} थपियो{3}"]); NE_PAT.push([new RegExp("^(Members|Deposits|Loans|Repayments|Exit settlements): (\\d+) removed or changed(.*)$"),"{1}: {2} हटाइयो वा परिवर्तन भयो{3}"]); NE_PAT.push([new RegExp("^Monthly interest: (.+)$"),"मासिक ब्याज: {1}"]); NE_PAT.push([new RegExp("^Give these login details to (.+)$"),"यी लगइन विवरण {1} लाई दिनुहोस्"]); NE_PAT.push([new RegExp("^Group was not deleted\\. (.*)$"),"समूह मेटाइएन। {1}"]); NE_PAT.push([new RegExp("^User was not deleted\\. (.*)$"),"प्रयोगकर्ता मेटाइएन। {1}"]); NE_PAT.push([new RegExp("^Could not add existing user\\. (.*)$"),"पहिलेको प्रयोगकर्ता थप्न सकिएन। {1}"]); NE_PAT.push([new RegExp("^Password could not be changed: (.+)$"),"पासवर्ड परिवर्तन गर्न सकिएन: {1}"]); NE_PAT.push([new RegExp("^PERMANENTLY DELETE (.+) from this group\\?(.*)$"),"{1} लाई यो समूहबाट स्थायी रूपमा मेटाउने?"]); const NE_MON={Baishakh:'बैशाख',Jestha:'जेठ',Asar:'असार',Shrawan:'साउन',Bhadra:'भदौ',Ashwin:'असोज',Kartik:'कात्तिक',Mangsir:'मंसिर',Poush:'पुष',Magh:'माघ',Falgun:'फागुन',Chaitra:'चैत'}; const neHas=(o,k)=>Object.prototype.hasOwnProperty.call(o,k); const neDig=s=>NE_DIGITS?String(s).replace(/\d/g,d=>'०१२३४५६७८९'[d]):String(s); function nd(s){return LANG==='ne'?neDig(s):String(s)} function neTok(s){ return String(s) .replace(/\b(Baishakh|Jestha|Asar|Shrawan|Bhadra|Ashwin|Kartik|Mangsir|Poush|Magh|Falgun|Chaitra)\b/g,m=>NE_MON[m]) .replace(/\b(\d{4}-\d{2}-\d{2})\b/g,m=>adToBs(m)?bsLabel(m):m) .replace(/\bRs\.?\s?(?=[\d-])/g,'रू ') .replace(/(\d)\s*years?\b/g,'$1 वर्ष').replace(/(\d)\s*months?\b/g,'$1 महिना').replace(/(\d)\s*days?\b/g,'$1 दिन') .replace(/month\(s\)/g,'महिना').replace(/member\(s\)/g,'सदस्य').replace(/deposit\(s\)/g,'बचत').replace(/repayment\(s\)/g,'भुक्तानी') .replace(/\d[\d,.]*/g,(t,o,str)=>(/^\d{7,}$/.test(t)||(o>0&&/[A-Za-z_]/.test(str[o-1])))?t:neDig(t)); } const _neLow=new WeakMap(); function neLookup(dict,t){let m=_neLow.get(dict);if(!m){m=new Map();for(const k in dict)m.set(k.toLowerCase(),dict[k]);_neLow.set(dict,m)}return m.get(String(t).toLowerCase())} function neCore(t,pats,dict){ dict=dict||UI_NE;pats=pats||NE_PAT; if(neHas(dict,t))return dict[t]; {const lw=neLookup(dict,t);if(lw!==undefined)return lw} if(t.slice(-1)===':'){const b=neLookup(dict,t.slice(0,-1));if(b!==undefined)return b+':'} const i=t.indexOf(' · '); if(i>0){ const parts=t.split(' · '),first=parts[0]; if(parts.length===2&&neHas(dict,first)&&/[\u0900-\u097F]/.test(parts[1])&&!/[A-Za-z]/.test(parts[1]))return dict[first]; return parts.map(p=>neCore(p.trim(),pats,dict)).join(' · '); } for(const [re,tpl] of pats){ const m=t.match(re); if(m)return tpl.replace(/\{(\d)\}/g,(_,n)=>m[n]===undefined?'':neCore(String(m[n]),pats,dict)); } return neTok(t); } function neX(raw){ const v=String(raw),t=v.trim(); if(!t||!/[A-Za-z0-9]/.test(t))return v; return v.slice(0,v.indexOf(t))+neCore(t)+v.slice(v.indexOf(t)+t.length); } function neMsg(m){ return String(m).split('\n').map(line=>{ const t=line.trim();if(!t||!/[A-Za-z0-9]/.test(t))return line; const lead=line.slice(0,line.indexOf(t)),trail=line.slice(line.indexOf(t)+t.length); if(neHas(NE_MSG,t))return lead+NE_MSG[t]+trail; for(const [re,tpl] of NE_MSGP){const x=t.match(re);if(x)return lead+tpl.replace(/\{(\d)\}/g,(_,n)=>x[n]===undefined?'':neCore(String(x[n])))+trail} const all=neCore(t); return lead+all+trail; }).join('\n'); } (function(){const C=window.confirm.bind(window);let messageOpen=false; function ensureMessageDialog(){let el=document.getElementById('appMessageDialog');if(el)return el;el=document.createElement('div');el.id='appMessageDialog';el.className='app-message-backdrop';el.setAttribute('role','presentation');el.innerHTML='

Notice

';document.body.appendChild(el);const close=()=>{el.classList.remove('open');messageOpen=false};el.querySelector('#appMessageOk').addEventListener('click',close);el.addEventListener('click',e=>{if(e.target===el)close()});document.addEventListener('keydown',e=>{if(messageOpen&&e.key==='Escape'){e.preventDefault();close()}});return el} window.alert=m=>{const text=LANG==='ne'?neMsg(String(m)):String(m);const el=ensureMessageDialog();el.querySelector('#appMessageTitle').textContent=LANG==='ne'?'सूचना':'Notice';el.querySelector('#appMessageText').textContent=text;el.querySelector('#appMessageOk').textContent=LANG==='ne'?'ठीक छ':'OK';el.classList.add('open');messageOpen=true;requestAnimationFrame(()=>el.querySelector('#appMessageOk').focus())}; window.confirm=m=>C(LANG==='ne'?neMsg(m):m)})(); function neNode(n){const v=n.nodeValue;if(!v||!/[A-Za-z0-9]/.test(v))return;const o=neX(v);if(o!==v)n.nodeValue=o} function neAttrs(e){if(!e||!e.getAttribute)return;for(const a of ['placeholder','title','aria-label']){const v=e.getAttribute(a);if(v){const o=neX(v);if(o!==v)e.setAttribute(a,o)}}} function neWalk(root){ if(!root)return; if(root.nodeType===3){neNode(root);return} if(root.nodeType!==1)return; const w=document.createTreeWalker(root,NodeFilter.SHOW_TEXT,{acceptNode:n=>{const p=n.parentElement;return p&&/^(SCRIPT|STYLE|TEXTAREA)$/.test(p.tagName)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}}); let n;while(n=w.nextNode())neNode(n); neAttrs(root);root.querySelectorAll('[placeholder],[title],[aria-label]').forEach(neAttrs); } const _neQ=new Set();let _neT=0; /* English mode: Title Case for buttons, labels, headings and table headers (never lowercases anything, never touches user data) */ const TC_SMALL=new Set(['a','an','and','as','at','but','by','for','from','in','into','of','on','or','the','to','vs','with','per']); const TC_SEL='button,th,label,h1,h2,h3,h4,legend,.nav-label,.stats small,.pgt'; const TC_SKIP='#avb,.sgi,.sam-card,.bspop,select,option,textarea,input,.note,[data-keep]'; function titleCase(str){let cap=true;return str.split(/(\s+)/).map(tok=>{if(!tok.trim())return tok;const m=tok.match(/^([^A-Za-z]*)([A-Za-z][A-Za-z'’-]*)(.*)$/);let out=tok; if(m&&!/[0-9@_.\\/]/.test(m[2]+m[3].replace(/[?:,)]+$/,''))&&m[2]===m[2].toLowerCase()&&(cap||!TC_SMALL.has(m[2]))&&!/^(e\.g|i\.e)/.test(tok))out=m[1]+m[2][0].toUpperCase()+m[2].slice(1)+m[3]; cap=/[:·]$/.test(tok)||tok==='/'||tok==='·';return out}).join('')} function tcNode(n){const v=n.nodeValue;if(!v||!/[a-z]/.test(v))return;const p=n.parentElement;if(!p||!p.closest(TC_SEL)||p.closest(TC_SKIP))return;if(p.closest('small')&&!p.closest('.stats'))return;const o=titleCase(v);if(o!==v)n.nodeValue=o} function tcWalk(root){if(!root)return;if(root.nodeType===3){tcNode(root);return}if(root.nodeType!==1)return;const w=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);let n;while(n=w.nextNode())tcNode(n)} new MutationObserver(ms=>{ for(const m of ms)m.addedNodes.forEach(n=>_neQ.add(n)); if(!_neT)_neT=requestAnimationFrame(()=>{_neT=0;const q=[..._neQ];_neQ.clear();const fn=LANG==='ne'?neWalk:tcWalk;q.forEach(n=>{if(n.isConnected)fn(n)})}); }).observe(document.body,{childList:true,subtree:true}); function apply(){ try{document.documentElement.lang=LANG==='ne'?'ne':'en';document.body.classList.toggle('auth-mode',!firm); if(LANG!=='ne'){tcWalk(document.body);return} neWalk(document.body); }catch(e){} } const $=s=>document.querySelector(s); let _navWasOpen=false; function applyMenu(){const o=menu&&!!(firm||cloudPlatformAdmin);if(o&&!_navWasOpen&&innerWidth<1100)setTimeout(()=>{const f=document.querySelector('#nav .nav-section,#nav .nav-link');if(f)f.focus()},60);_navWasOpen=o;$('#nav').classList.toggle('open',o);$('#ov').classList.toggle('open',o);$('#mnu').setAttribute('aria-expanded',o);$('#mnu').setAttribute('aria-label',o?'Close menu':'Open menu')} function toggleMenu(v){menu=v===undefined?!menu:v;applyMenu()} document.addEventListener('keydown',e=>{if(e.key=='Escape'&&menu)toggleMenu(false)}); const SS={get:k=>{try{return sessionStorage.getItem(k)}catch(e){return null}},set:(k,v)=>{try{v==null?sessionStorage.removeItem(k):sessionStorage.setItem(k,v)}catch(e){}}}; function resetUi(){depQ='';rmem='';const cm=bsMonthFromAd(today());rf='';rto='';rm=cm;selL=null;bulk=false;mb=false;q='';pg=0} const today=()=>new Date(Date.now()-new Date().getTimezoneOffset()*6e4).toISOString().slice(0,10); const asof=()=>db.closed?db.closed.date:today(); const PERMS=[['members','Add / edit members'],['deposits','Enter deposits'],['loans','Give loans'],['repayments','Record repayments'],['exits','Record member exits'],['edit_records','Edit or delete past records'],['settings','Change group rules & details']]; const NEED={members:'members',deposits:'deposits',loans:'loans',reps:'repayments'}; /* Legacy editors (permissions = NULL) never had the settings power on the server (see sb_has_perm), so the browser must not offer it either. */ const DEF_EDITOR=['members','deposits','loans','repayments','exits','edit_records']; const POSITIONS=[ ['Manager',['members','deposits','loans','repayments','exits','edit_records','settings']], ['Accountant',['members','deposits','loans','repayments','exits']] ]; function can(k){if(!k)return true;if(offlineReadOnly){if(cloudExactRole==='admin'||cloudExactRole==='super_admin')return true;if(cloudPerms)return cloudPerms[k]===true;return ['manager','accountant'].includes(cloudExactRole)&&DEF_EDITOR.includes(k)}if(cloudRole==='owner')return true;if(cloudRole==='viewer')return false;if(cloudExactRole==='manager')return cloudPerms?cloudPerms[k]===true:DEF_EDITOR.includes(k);if(cloudExactRole==='accountant')return cloudPerms?cloudPerms[k]===true:DEF_EDITOR.includes(k);if(cloudExactRole==='viewer')return !!(cloudPerms&&cloudPerms[k]===true);return false} let offlineReadOnly=false,offlineSnapshotAt=0; const OFFLINE_DB_NAME='accounting-cloud-offline'; const OFFLINE_CACHE_MAX_AGE=72*60*60*1000; function offlineDbOpen(){ return new Promise((resolve,reject)=>{ if(!window.indexedDB||!window.crypto?.subtle)return reject(new Error('Secure browser storage is unavailable')); const req=indexedDB.open(OFFLINE_DB_NAME,3); req.onupgradeneeded=()=>{const d=req.result;if(!d.objectStoreNames.contains('firmSnapshots'))d.createObjectStore('firmSnapshots',{keyPath:'key'});if(!d.objectStoreNames.contains('keys'))d.createObjectStore('keys',{keyPath:'userId'});if(!d.objectStoreNames.contains('offlineDrafts'))d.createObjectStore('offlineDrafts',{keyPath:'key'});}; req.onsuccess=()=>resolve(req.result); req.onerror=()=>reject(req.error||new Error('Could not open local offline storage')); }); } async function offlineCryptoKey(localDb,userId,create=true){ const existing=await new Promise((resolve,reject)=>{ const tx=localDb.transaction('keys','readonly'),req=tx.objectStore('keys').get(String(userId)); req.onsuccess=()=>resolve(req.result||null);req.onerror=()=>reject(req.error||new Error('Could not read the local encryption key')); }); if(existing?.cryptoKey)return existing.cryptoKey; if(!create)return null; const key=await crypto.subtle.generateKey({name:'AES-GCM',length:256},false,['encrypt','decrypt']); await new Promise((resolve,reject)=>{ const tx=localDb.transaction('keys','readwrite');tx.objectStore('keys').put({userId:String(userId),cryptoKey:key}); tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error||new Error('Could not save the local encryption key')); tx.onabort=()=>reject(tx.error||new Error('Local encryption key save was cancelled')); }); return key; } async function offlineCacheFirm(){ if(!firm?.id||!cloudUser?.id||!window.indexedDB||!window.crypto?.subtle)return false; let localDb; try{ localDb=await offlineDbOpen(); const userId=String(cloudUser.id),key=await offlineCryptoKey(localDb,userId,true); const snapshot={userId,firmId:String(firm.id),cachedAt:Date.now(),revision:Number(cloudRevision)||0,firm:JSON.parse(JSON.stringify(firm)),db:JSON.parse(JSON.stringify(db)),displayName:cloudName||'',cloudRole,cloudExactRole,cloudPerms:cloudPerms?JSON.parse(JSON.stringify(cloudPerms)):null,cloudJob}; const iv=crypto.getRandomValues(new Uint8Array(12)); const plain=new TextEncoder().encode(JSON.stringify(snapshot)); const cipher=await crypto.subtle.encrypt({name:'AES-GCM',iv},key,plain); const record={key:userId+':'+String(firm.id),userId,firmId:String(firm.id),cachedAt:snapshot.cachedAt,iv,cipher}; await new Promise((resolve,reject)=>{ const tx=localDb.transaction('firmSnapshots','readwrite');tx.objectStore('firmSnapshots').put(record); tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error||new Error('Could not save the encrypted offline snapshot')); tx.onabort=()=>reject(tx.error||new Error('Offline snapshot save was cancelled')); }); return true; }catch(e){console.warn('Encrypted offline snapshot could not be saved',e);return false} finally{if(localDb)localDb.close()} } async function offlineReadFirm(userId,firmId){ let localDb; try{ localDb=await offlineDbOpen(); const uid=String(userId),record=await new Promise((resolve,reject)=>{ const tx=localDb.transaction('firmSnapshots','readonly'),req=tx.objectStore('firmSnapshots').get(uid+':'+String(firmId)); req.onsuccess=()=>resolve(req.result||null);req.onerror=()=>reject(req.error||new Error('Could not read the offline snapshot')); }); if(!record)return null; const key=await offlineCryptoKey(localDb,uid,false);if(!key)return null; const plain=await crypto.subtle.decrypt({name:'AES-GCM',iv:record.iv},key,record.cipher); return JSON.parse(new TextDecoder().decode(plain)); }catch(e){console.warn('Encrypted offline snapshot could not be read',e);return null} finally{if(localDb)localDb.close()} } async function offlineClearUser(userId){ if(!userId||!window.indexedDB)return; let localDb; try{ localDb=await offlineDbOpen(); await new Promise((resolve,reject)=>{ const tx=localDb.transaction(['firmSnapshots','keys','offlineDrafts'],'readwrite'),store=tx.objectStore('firmSnapshots'),draftStore=tx.objectStore('offlineDrafts'),req=store.getAll(),draftReq=draftStore.getAll(); req.onsuccess=()=>{(req.result||[]).filter(x=>x.userId===String(userId)).forEach(x=>store.delete(x.key));}; req.onerror=()=>reject(req.error||new Error('Could not inspect offline snapshots')); draftReq.onsuccess=()=>{(draftReq.result||[]).filter(x=>x.userId===String(userId)).forEach(x=>draftStore.delete(x.key));}; draftReq.onerror=()=>reject(draftReq.error||new Error('Could not inspect offline drafts')); tx.objectStore('keys').delete(String(userId)); tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error||new Error('Could not clear offline snapshots')); tx.onabort=()=>reject(tx.error||new Error('Offline cache clearing was cancelled')); }); }catch(e){console.warn('Offline cache could not be cleared',e)} finally{if(localDb)localDb.close()} } /* Encrypted local draft queue. Drafts are device-local until a user explicitly reviews and submits them online. */ let pwaDraftType='deposit',pwaDraftBusy=false,pwaDraftSyncing=false; function pwaClientId(){try{let id=localStorage.getItem('ac-offline-client-id');if(!id){id=crypto.randomUUID();localStorage.setItem('ac-offline-client-id',id)}return id}catch(e){return '00000000-0000-4000-8000-000000000001'}} async function pwaApplyPendingMembers(){if(!cloudUser?.id||!firm?.id)return;const drafts=await offlineDraftList(cloudUser.id,firm.id);let changed=false;for(const d of drafts.filter(x=>x.type==='member')){const r=d.record||{};if(r.id&&!db.members.some(m=>String(m.id)===String(r.id))){db.members.push(Object.assign({},r,{offlinePending:true,offlineDraftId:String(d.id)}));changed=true}}if(changed)ixReset()} async function offlineDraftPut(draft){ if(!cloudUser?.id||!firm?.id)throw new Error('Sign in and open a firm before saving a draft.'); let localDb; try{ localDb=await offlineDbOpen(); const userId=String(cloudUser.id),firmId=String(firm.id),key=await offlineCryptoKey(localDb,userId,true); const safe=Object.assign({},draft,{userId,firmId,updatedAt:Date.now()}); const iv=crypto.getRandomValues(new Uint8Array(12)),plain=new TextEncoder().encode(JSON.stringify(safe)); const cipher=await crypto.subtle.encrypt({name:'AES-GCM',iv},key,plain); const record={key:userId+':'+firmId+':'+String(draft.id),userId,firmId,draftId:String(draft.id),createdAt:Number(draft.createdAt)||Date.now(),iv,cipher}; await new Promise((resolve,reject)=>{const tx=localDb.transaction('offlineDrafts','readwrite');tx.objectStore('offlineDrafts').put(record);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error||new Error('Could not save the encrypted draft'));tx.onabort=()=>reject(tx.error||new Error('Draft save was cancelled'));}); return true; }finally{if(localDb)localDb.close()} } async function offlineDraftList(userId,firmId){ if(!userId||!window.indexedDB||!window.crypto?.subtle)return []; let localDb; try{ localDb=await offlineDbOpen(); const records=await new Promise((resolve,reject)=>{const tx=localDb.transaction('offlineDrafts','readonly'),req=tx.objectStore('offlineDrafts').getAll();req.onsuccess=()=>resolve(req.result||[]);req.onerror=()=>reject(req.error||new Error('Could not read offline drafts'));}); const uid=String(userId),key=await offlineCryptoKey(localDb,uid,false);if(!key)return []; const out=[]; for(const record of records.filter(x=>x.userId===uid&&(!firmId||x.firmId===String(firmId)))){ try{const plain=await crypto.subtle.decrypt({name:'AES-GCM',iv:record.iv},key,record.cipher),draft=JSON.parse(new TextDecoder().decode(plain));if(draft.userId===uid&&draft.firmId===record.firmId)out.push(draft)}catch(e){console.warn('An encrypted offline draft could not be opened',e)} } return out.sort((a,b)=>(a.createdAt||0)-(b.createdAt||0)); }catch(e){console.warn('Offline drafts could not be listed',e);return []} finally{if(localDb)localDb.close()} } async function offlineDraftDelete(draftId,userId=cloudUser?.id,firmId=firm?.id){ if(!draftId||!userId||!firmId)return false; let localDb; try{localDb=await offlineDbOpen();await new Promise((resolve,reject)=>{const tx=localDb.transaction('offlineDrafts','readwrite');tx.objectStore('offlineDrafts').delete(String(userId)+':'+String(firmId)+':'+String(draftId));tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error||new Error('Could not remove the offline draft'));tx.onabort=()=>reject(tx.error||new Error('Draft removal was cancelled'));});return true} catch(e){console.warn('Offline draft could not be removed',e);return false} finally{if(localDb)localDb.close()} } function pwaDraftOpen(){if(!firm||!cloudUser)return alert('Log in and open a firm before preparing offline drafts.');if(db.closed)return alert('This firm is closed. New accounting drafts are not allowed.');const layer=document.getElementById('pwaDraftLayer');if(!layer)return;layer.hidden=false;pwaDraftType='deposit';pwaDraftRenderForm();pwaDraftRefreshList();} function pwaDraftClose(){const layer=document.getElementById('pwaDraftLayer');if(layer)layer.hidden=true} function pwaDraftSetType(v){pwaDraftType=v==='repayment'?'repayment':'deposit';pwaDraftRenderForm()} function pwaDraftRenderForm(){ const host=document.getElementById('pwaDraftEditor');if(!host)return; const members=(db.members||[]).filter(m=>!m.left).slice().sort((a,b)=>String(a.name||'').localeCompare(String(b.name||''))); const loanMembers=members.filter(m=>(db.loans||[]).some(l=>String(l.mid)===String(m.id)&&l.date<=today()&&bal(l)>0)); const options=list=>list.map(m=>'').join(''); const common=''; const fields=pwaDraftType==='deposit' ?common+'' :common+''; host.innerHTML='
'+fields+'

This stays local and does not change official balances. After reconnecting, Accounting Cloud checks it against the latest records before synchronization.

'; } async function pwaDraftSave(){ if(!cloudUser?.id||!firm?.id)return alert('Log in and open a firm first.'); if(db.closed)return alert('This firm is closed. New accounting drafts are not allowed.'); const mid=String(document.getElementById('pwaDraftMember')?.value||''),date=document.getElementById('pwaDraftDate')?.value||''; if(!mid)return alert('Choose a member.'); if(!date||datetoday())return alert('Choose a valid transaction date within the firm operating period.'); const member=db.members.find(m=>String(m.id)===mid&&!m.left);if(!member)return alert('This member is not active in the cached firm records.'); let record={mid,date}; if(pwaDraftType==='deposit'){ const month=document.getElementById('pwaDraftMonth')?.value||'',amt=Number(document.getElementById('pwaDraftAmount')?.value),fine=Number(document.getElementById('pwaDraftFine')?.value||0); if(datebsMonthFromAd(today())||(minMonth&&monthString(d.mid)===mid&&String(d.month).slice(0,7)===month))return alert('This member already has a deposit for that month in the cached records.'); record=Object.assign(record,{month,amt,fine}); }else{ const prin=Number(document.getElementById('pwaDraftPrincipal')?.value||0),interest=Number(document.getElementById('pwaDraftInterest')?.value||0); if(!Number.isFinite(prin)||!Number.isFinite(interest)||prin<0||interest<0||prin+interest<=0)return alert('Enter a positive repayment amount.'); const loans=db.loans.filter(l=>String(l.mid)===mid&&l.date<=date),maxP=sum(loans,l=>balAt(l,date)),maxI=sum(loans,l=>intDue(l,date)); if(!loans.length)return alert('This member has no loan on or before the selected date.'); if(prin>maxP+SMALL||interest>maxI+SMALL+0.49)return alert('The repayment exceeds the cached principal or interest due. Reconnect and refresh before preparing it.'); record=Object.assign(record,{prin,int:interest}); } const draft={id:crypto.randomUUID(),type:pwaDraftType,record,createdAt:Date.now(),firmName:firm.name}; try{await offlineDraftPut(draft);pwaToast('Draft saved securely on this device.');await pwaDraftRefreshList();await pwaDraftRefreshCount()} catch(e){alert('Draft could not be saved: '+(e?.message||e))} } function pwaDraftAlreadyPosted(draft){ const id=String(draft.id); if(draft.type==='member')return (db.members||[]).some(x=>String(x.id)===String(draft.record?.id)&&x.offlineDraftId===id&&!x.offlinePending); if(draft.type==='deposit')return (db.deposits||[]).some(x=>x.offlineDraftId===id); if(draft.type==='repayment')return (db.reps||[]).some(x=>x.offlineDraftId===id); return false; } async function pwaDraftRefreshList(){ const host=document.getElementById('pwaDraftList');if(!host||!cloudUser?.id||!firm?.id)return; const drafts=await offlineDraftList(cloudUser.id,firm.id); if(!drafts.length){host.innerHTML='

No offline drafts for this firm.

';return} host.innerHTML=drafts.map(d=>{ const r=d.record||{},m=(db.members||[]).find(x=>String(x.id)===String(r.mid)),posted=pwaDraftAlreadyPosted(d); const title=({member:'Add member',deposit:'Deposit',repayment:'Loan repayment',loan:'Loan request'})[d.type]||'Offline entry'; const amount=d.type==='member'?(r.phone||'No phone'):d.type==='loan'?'Loan '+rs(r.amt)+' at '+(r.rate||0)+'%':d.type==='deposit'?'Deposit '+rs(r.amt)+' + fine '+rs(r.fine):'Principal '+rs(r.prin)+' + interest '+rs(r.int); const desc=(d.type==='member'?(r.name||'New member'):(m?m.name:'Member '+r.mid))+' · '+(r.date||r.join||'')+' · '+amount; const status=posted?'Already posted · safe to remove':d.syncError?'Needs review · '+d.syncError:(navigator.onLine===false?'Saved on this device · waiting for internet':'Saved on this device · waiting to sync'); return '
'+title+''+esc(desc)+''+esc(status)+'
'+(posted?'':'')+'
'; }).join(''); } async function pwaDraftRefreshCount(){ const b=document.getElementById('pwaDraftBtn'),n=document.getElementById('pwaDraftCount'); if(!b)return;b.hidden=!(firm&&cloudUser);if(!firm||!cloudUser){if(n)n.textContent='0';return} const drafts=await offlineDraftList(cloudUser.id,firm.id);if(n)n.textContent=String(drafts.length); } async function pwaDraftRemove(id){if(!confirm('Delete this local draft from this device?'))return;await offlineDraftDelete(id);await pwaDraftRefreshList();await pwaDraftRefreshCount()} async function pwaDraftSubmit(id,automatic=false){ if(pwaDraftBusy)return false; const fail=message=>{if(!automatic)alert(message);return false}; if(navigator.onLine===false)return fail('Connect to the internet before submitting this draft.'); if(!cloudUser?.id||!firm?.id||!cloud)return fail('Your cloud session is unavailable. Log in again.'); if(offlineReadOnly||cloudUnsaved||cloudSaving)return fail('Refresh the live firm and resolve unsaved changes before submitting a draft.'); pwaDraftBusy=true;let draft=null;const gid=String(firm.id); try{ const drafts=await offlineDraftList(cloudUser.id,gid);draft=drafts.find(x=>String(x.id)===String(id));if(!draft)return false; if(!await cloudOpenGroup(gid,true,true))return fail('Could not refresh the latest firm records. The draft remains on this device.'); if(db.closed)return fail('This firm is closed. The draft remains on this device.'); if(pwaDraftAlreadyPosted(draft)){await offlineDraftDelete(draft.id);await pwaDraftRefreshList();await pwaDraftRefreshCount();return true} const r=draft.record||{},mid=String(r.mid||''),member=db.members.find(m=>String(m.id)===mid&&!m.left); if(draft.type==='member'){ if(!can('members'))return fail('Your current permissions do not allow member registration. The draft was kept.'); if(!r.id||!String(r.name||'').trim()||!r.join||r.jointoday())return fail('Member details or joining date are invalid. Review this draft.'); if(db.members.some(m=>!m.offlinePending&&String(m.name||'').trim().toLowerCase()===String(r.name).trim().toLowerCase()))return fail('A member with this name already exists in the live records. Review this draft.'); const {error}=await cloud.rpc('sb2_sync_offline_entry',{p_firm_id:gid,p_client_id:pwaClientId(),p_idempotency_key:String(draft.id),p_operation_kind:'member',p_operation_data:r});if(error)throw error; }else if(draft.type==='deposit'){ if(!member||member.offlinePending)return fail('The member must be registered in the cloud before this deposit can synchronize.'); if(!can('deposits'))return fail('Your current permissions do not allow deposits. The draft was kept.'); const minMonth=bsMonthFromAd(member.join||startDate()); if(!r.date||r.datetoday()||r.datebsMonthFromAd(today())||(minMonth&&r.monthString(l.mid)===mid&&l.date<=r.date),maxP=sum(loans,l=>balAt(l,r.date)),maxI=sum(loans,l=>intDue(l,r.date)); if(!loans.length||Number(r.prin)>maxP+SMALL||Number(r.int)>maxI+SMALL+0.49)return fail('The live loan balance or interest due has changed. Review the repayment amount before submitting.'); const {error}=await cloud.rpc('sb2_sync_offline_entry',{p_firm_id:gid,p_client_id:pwaClientId(),p_idempotency_key:String(draft.id),p_operation_kind:'repayment',p_operation_data:r});if(error)throw error; }else if(draft.type==='loan'){ if(!member||member.offlinePending)return fail('The member must be registered in the cloud before this loan request can synchronize.'); if(!can('loans'))return fail('Your current permissions do not allow loan requests. The draft was kept.'); if(!r.date||r.datetoday()||r.dateString(l.mid)===mid),bal);if(+r.amt>cap+0.005)return fail('The current loan limit has changed. This member can borrow up to '+rs(Math.max(0,cap))+' more.');} if(!await requestApproval('loan',{loan:r},String(draft.id)))return false; }else return fail('This draft type is not supported.'); await offlineDraftDelete(draft.id); if(!await cloudOpenGroup(gid,true,true))return fail('The server accepted the entry, but the latest firm records could not be refreshed. Reopen the firm.'); render();await pwaDraftRefreshList();await pwaDraftRefreshCount(); if(!automatic)alert(draft.type==='member'?'Member registered successfully.':draft.type==='deposit'?'Deposit synchronized successfully.':draft.type==='repayment'?'Loan repayment synchronized successfully.':'Loan request submitted for independent approval.'); return true; }catch(e){ console.warn('Offline draft submission failed',e);if(draft){draft.syncError=String(e?.message||e);try{await offlineDraftPut(draft)}catch(_){}} return fail('Synchronization failed: '+(e?.message||e)+'. The draft remains on this device for review.'); }finally{pwaDraftBusy=false} } async function pwaDraftSyncAll(){ if(pwaDraftSyncing||navigator.onLine===false||!cloudUser?.id||!firm?.id||!cloud||offlineReadOnly||cloudUnsaved||cloudSaving||db.closed)return false; pwaDraftSyncing=true; try{const drafts=await offlineDraftList(cloudUser.id,firm.id);for(const d of drafts){const ok=await pwaDraftSubmit(d.id,true);if(!ok)break;}await pwaDraftRefreshList();await pwaDraftRefreshCount();return true} finally{pwaDraftSyncing=false} } async function offlineRestoreFirm(firmId){ if(!cloudUser?.id||!firmId)return false; const snap=await offlineReadFirm(cloudUser.id,firmId); if(!snap||snap.userId!==String(cloudUser.id)||snap.firmId!==String(firmId)||!snap.firm||!snap.db||Date.now()-Number(snap.cachedAt||0)>OFFLINE_CACHE_MAX_AGE)return false; firm=Object.assign({},snap.firm,{id:snap.firm.id||firmId}); db=Object.assign(fresh(),snap.db); db.set=Object.assign(fresh().set,db.set||{}); for(const k of ['members','deposits','loans','reps','pay'])if(!Array.isArray(db[k]))db[k]=[]; cloudRevision=Number(snap.revision)||0; cloudExactRole=snap.cloudExactRole||'viewer';cloudRole=snap.cloudRole||'viewer';cloudPerms=snap.cloudPerms||{};cloudJob=snap.cloudJob||'Offline · draft mode';cloudName=snap.displayName||''; cloudGroupInfo={name:firm.name,role:'viewer'};cloudPlatformAdmin=false; offlineReadOnly=true;offlineSnapshotAt=Number(snap.cachedAt)||Date.now(); cloudUnsaved=false;tab='rpt';rt='overview';navSection='';ed=null;selL=null; await pwaApplyPendingMembers();render(); pwaDraftRefreshCount();pwaDraftRefreshList(); return true; } const ro=k=>(offlineReadOnly||navigator.onLine===false)?(db.closed?(alert('This group has been closed, so its records are read-only.'),true):(['members','deposits','loans','repayments'].includes(k)&&can(k)?false:(alert('This operation is not available offline. Connect to the internet to continue.'),true))):db.closed?(alert('This group has been closed, so its records are read-only.'),true):(k&&!can(k)?(alert('You do not have permission to do this. Ask the Owner to give you this power.'),true):false); let bd=today(),bm=bsMonthFromAd(today()),mb=false,mbd=today(),mbt='',depQ=''; const okD=s=>/^\d{4}-\d{2}-\d{2}$/.test(String(s||''))?String(s):''; const created=()=>okD(firm.created)||today(); const startDate=()=>okD(firm.startDate)||okD(firm.created)||today(); const rs=n=>'Rs '+(Math.round(n)||0).toLocaleString('en-IN'); const df=s=>esc(bsLabel(s));const mf=s=>esc(bsMonthLabel(s)); const sum=(a,f)=>a.reduce((t,x)=>t+(+f(x)||0),0); const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); /* per-render index: makes savings, pending months, loan balances O(1) instead of rescanning every record */ let _ix=null; function ixReset(){_ix=null} function ix(){if(_ix)return _ix;const mem=new Map(),left=new Set(),sav=new Map(),dep=new Set(),loan=new Map(),reps=new Map(); for(const m of db.members){const k=String(m.id);if(!mem.has(k))mem.set(k,m);if(m.left)left.add(k)} for(const l of db.loans){const k=String(l.id);if(!loan.has(k))loan.set(k,l)} for(const d of db.deposits){const k=String(d.mid);sav.set(k,(sav.get(k)||0)+(+d.amt||0));const mo=depMonth(d);if(mo)dep.add(k+'|'+mo)} for(const r of db.reps){const k=String(r.lid);let a=reps.get(k);if(!a){a=[];reps.set(k,a)}a.push(r)} return _ix={mem,left,sav,dep,loan,reps}} const memMap=()=>ix().mem,loanMap=()=>ix().loan,repsOf=id=>ix().reps.get(String(id))||[]; const notSkip=(r,skip)=>skip==null||String(r.id)!==String(skip); const nm=id=>esc((memMap().get(String(id))||{}).name||'?'); const loanOf=id=>loanMap().get(String(id))||{}; const bal=l=>l.amt-sum(repsOf(l.id),r=>r.prin); const saved=id=>{const x=ix();return x.left.has(String(id))?0:(x.sav.get(String(id))||0)}; const sav=()=>sum(db.members,m=>saved(m.id)); /* profit pools shared by active members: interest received minus interest paid to leavers, plus savings not paid back to leavers; fines separate */ function pools(){const ir=sum(db.reps,r=>r.int),fr=sum(db.deposits,d=>d.fine)+sum(db.pay,p=>p.exit_fine||0),ret=sum(db.members.filter(m=>m.left),m=>sum(db.deposits.filter(d=>d.mid==m.id),d=>d.amt))-sum(db.pay,p=>p.prin);let I=ir-sum(db.pay,p=>p.int)+ret,F=fr;if(I<0){F+=I;I=0}return{I,F,den:sav()}} const cash=()=>sum(db.deposits,d=>(+d.amt||0)+(+d.fine||0))+sum(db.reps.filter(r=>!(r.x!=null&&db.pay.some(p=>String(p.id)===String(r.x)&&p.settlement_net!=null))),r=>(+r.prin||0)+(+r.int||0))-sum(db.loans,l=>l.amt)-sum(db.pay,p=>p.settlement_cash!=null?+p.settlement_cash:(p.settlement_net!=null?(p.status==='pending'?0:+p.settlement_net):((+p.prin||0)+(+p.int||0)))); /* Validate the chronological cash position, not today's aggregate cash. Since this app stores dates (not times), entries on the same date are assessed together at that day's closing balance. */ function cashTimelineIssue(data=db){ const events=[]; const add=(rows,amounts,sign,label)=>{for(const x of (Array.isArray(rows)?rows:[])){ const date=String(x.date||'');if(!/^\d{4}-\d{2}-\d{2}$/.test(date))return {date,label,reason:'invalid date'}; const parts=amounts(x).map(v=>Number(v??0));if(parts.some(v=>!Number.isFinite(v)||v<0))return {date,label,reason:'invalid amount'}; events.push({date,delta:sign*parts.reduce((a,b)=>a+b,0),label}); }}; let issue=add(data.deposits,x=>[x.amt,x.fine],1,'deposit');if(issue)return issue; const exitIds=new Set((data.pay||[]).filter(p=>p.settlement_net!=null).map(p=>String(p.id)));issue=add((data.reps||[]).filter(x=>!(x.x!=null&&exitIds.has(String(x.x)))),x=>[x.prin,x.int],1,'repayment');if(issue)return issue; issue=add(data.loans,x=>[x.amt],-1,'loan');if(issue)return issue; for(const p of (Array.isArray(data.pay)?data.pay:[])){if(!/^\d{4}-\d{2}-\d{2}$/.test(String(p.date||'')))return {date:p.date,label:'member exit settlement',reason:'invalid date'};const v=p.settlement_cash!=null?Number(p.settlement_cash):(p.settlement_net!=null?(p.status==='pending'?0:Number(p.settlement_net)):Number(p.prin||0)+Number(p.int||0));if(!Number.isFinite(v))return {date:p.date,label:'member exit payment',reason:'invalid amount'};events.push({date:String(p.date||''),delta:-v,label:'member exit settlement'});} events.sort((a,b)=>a.date.localeCompare(b.date));let balance=0; for(let i=0;iString(d).slice(0,7); const addM=(m,n)=>{let[y,o]=m.split('-').map(Number);o+=n;y+=Math.floor((o-1)/12);o=(o-1)%12+1;return y+'-'+String(o).padStart(2,'0')}; function balAt(l,until){return l.amt-sum(repsOf(l.id).filter(r=>!until||r.date<=until),r=>r.prin)} /* minimum charge: the first interest month is always charged on the full loan amount, even if part or all of it was repaid in the issue month; a loan closed early owes that month at once */ function sched(l,until){const rows=[],m0=addM(ym(l.date),FIRST),e0=ym(until||asof()),end=(e0ym(r.date)r.prin);if(open<=0)break;rows.push([m,open,open*l.rate/1200])}return rows} /* amounts under 50 paisa are ignored, so a loan paid in whole rupees can close */ const intDue=(l,until)=>{const v=sum(sched(l,until),r=>r[2])-sum(repsOf(l.id).filter(r=>!until||r.date<=until),r=>r.int);return v>=0.5?v:0};const intDueEx=(l,until,skip)=>{const v=sum(sched(l,until),r=>r[2])-sum(repsOf(l.id).filter(r=>notSkip(r,skip)&&(!until||r.date<=until)),r=>r.int);return v>=0.5?v:0};const balAtEx=(l,until,skip)=>l.amt-sum(repsOf(l.id).filter(r=>notSkip(r,skip)&&(!until||r.date<=until)),r=>r.prin); let _rn=0;const rid=()=>Date.now()*100+(_rn++%100); function normalizeDb(){ const n=v=>Number.isFinite(+v)?+v:0,t=v=>v==null?'':String(v); db.set=Object.assign({dep:500,rate:12},db.set&&typeof db.set==='object'?db.set:{}); db.set.dep=n(db.set.dep);db.set.rate=n(db.set.rate);if(db.set.lim!=null)db.set.lim=n(db.set.lim); /* ids and id references end up inside onclick="" and id="" attributes, so only letters, digits, _ and - are kept; anything else in saved or restored data is stripped */ const sid=v=>typeof v==='number'?(Number.isFinite(v)?v:0):String(v).replace(/[^\w-]/g,''),si=(o,k)=>{if(o[k]!=null)o[k]=sid(o[k])}; for(const m of db.members){m.name=t(m.name);m.phone=t(m.phone);m.join=t(m.join);si(m,'id')} for(const d of db.deposits){d.amt=n(d.amt);d.fine=n(d.fine);d.date=t(d.date);d.month=t(d.month);si(d,'mid')} for(const l of db.loans){l.amt=n(l.amt);l.rate=n(l.rate);l.date=t(l.date);si(l,'id');si(l,'mid')} for(const r of db.reps){r.prin=n(r.prin);r.int=n(r.int);r.date=t(r.date);si(r,'id');si(r,'lid');si(r,'x')} for(const p of db.pay){p.prin=n(p.prin);p.int=n(p.int);p.date=t(p.date);si(p,'id');si(p,'mid')} } function migrateMonths(){normalizeDb();for(const r of db.reps)if(r.id==null)r.id=rid();const v=+db.bsMonthsVersion||0;if(v<1){for(const d of db.deposits){const mm=String(d.month||'').match(/^(\d{4})-(\d{2})$/);if(mm&&+mm[1]<2070)d.month=bsMonthFromAd(d.month)}db.bsMonthsVersion=2}else if(v<2){db.bsMonthsVersion=2}} const T={ members:{l:'Members',n:'member',f:[['name','Name','text'],['phone','Phone','tel'],['join','Joined','date']], h:['Name','Phone','Joined','Savings','Pending'],r:m=>[esc(m.name)+(m.left?' (left)':(m.offlinePending?' (pending sync)':'')),esc(m.phone),df(m.join),rs(saved(m.id)),m.left?'Left '+df(m.left.date):pend(m).length?rs(pend(m).length*db.set.dep):'None'], a:i=>``+(can('edit_records')?` `:'')}, deposits:{l:'Deposits',n:'deposit',f:[['date','Date','date'],['mid','Member','mem'],['month','Month','month'],['amt','Amount (Rs)','number'],['fine','Fine (Rs)','number']], h:['Date','Member','Month','Amount','Fine'],r:d=>[df(d.date),nm(d.mid),mf(d.month),rs(d.amt),rs(d.fine)], a:i=>` `}, loans:{l:'Loans',n:'loan',f:[['date','Date','date'],['mid','Member','mem'],['amt','Loan (Rs)','number'],['rate','Interest % / year','number']], h:['Date','Member','Loan','Rate','Principal left','Interest due'],r:l=>[df(l.date),nm(l.mid),rs(l.amt),l.rate+'%',rs(bal(l)),rs(intDue(l))], a:i=>` `}, reps:{l:'Repayments',n:'repayment',f:[['date','Date','date'],['lid','Member','loan'],['prin','Principal (Rs)','number'],['int','Interest (Rs)','number']], h:['Date','Member','Principal','Interest'],r:p=>[df(p.date),nm(loanOf(p.lid).mid),rs(p.prin),rs(p.int)], a:i=>` `} }; const dflt=()=>({date:today(),join:today(),month:bsMonthFromAd(today()),amt:db.set.dep,rate:db.set.rate,fine:0,prin:'',int:''}); /* pending rule: each member owes a deposit starting from that member's own joining month */ function depMonth(d){if(!d)return '';const sm=String(d.month||'').match(/^\d{4}-\d{2}$/);if(sm&&BSD[+sm[0].slice(0,4)]&&+sm[0].slice(5,7)>=1&&+sm[0].slice(5,7)<=12)return sm[0];const b=d.date&&adToBs(d.date);return b?b.y+'-'+String(b.m).padStart(2,'0'):''}function depMatchesMonth(d,x){return depMonth(d)==String(x).slice(0,7)} function pend(m){if(!m||m.left)return[];const e=bsMonthFromAd(asof()),g=bsMonthFromAd(startDate()),jm=bsMonthFromAd(m.join||startDate());const c=[g,jm].filter(Boolean).sort()[1],o=[];if(!e||!c||c>e)return o;for(let x=c;x<=e;x=addM(x,1))if(!ix().dep.has(String(m.id)+'|'+x))o.push(x);return o} function payPend(id){if(offlineReadOnly||navigator.onLine===false)return alert('Bulk pending-deposit collection is online-only. Use the single deposit form for offline drafts.');if(ro('deposits'))return;const m=db.members.find(x=>x.id==id);if(!m)return;const p=pend(m);if(!p.length)return;const d=today();if(d<(m.join||startDate()))return alert('A deposit cannot be dated before this member joined ('+bsLabel(m.join)+').');if(!confirm('Record '+p.length+' deposit(s) of '+rs(db.set.dep)+' each for '+m.name+', dated today?'))return;const additions=p.map(month=>({date:d,mid:String(id),month,amt:db.set.dep,fine:0}));const next=Object.assign({},db,{deposits:db.deposits.concat(additions)});if(!requireValidCashTimeline(next))return;db.deposits.push(...additions);save();render()} function editRow(t,i){if(offlineReadOnly||navigator.onLine===false)return alert('Editing existing records is online-only. You can add new offline entries.');if(t==='members'?ro('members'):(ro('edit_records')||ro(NEED[t])))return;if(t==='reps'&&db.reps[i]&&db.reps[i].x!=null)return alert('This repayment was recorded together with a member exit. Undo the exit on the Member Exit page instead.');ed={t,i};bulk=false;mb=false;selL=null;render();scrollTo(0,0)} function tbl(h,rows,foot){const nu=h.map((_,i)=>rows.some(r=>/^()?(Rs |Owes )/.test(String(r[i])))),cl=i=>nu[i]?' class="n"':'';return'
'+h.map((x,i)=>''+x+'').join('')+''+ (rows.length?rows.map(r=>''+r.map((c,i)=>''+c+'').join('')+'').join(''):'')+''+(foot?''+foot.map((c,i)=>''+c+'').join('')+'':'')+'
Nothing recorded yet.
'} function fill(){const e=$('#lst');if(e)e.innerHTML=LF()} const fillD=(()=>{let t;return()=>{clearTimeout(t);t=setTimeout(fill,120)}})(); function paged(h,rows,ps,foot){const ql=q.toLowerCase(),S=q?(rows._s||(rows._s=rows.map(r=>r.join(' ').replace(/<[^>]*>/g,'').toLowerCase()))):null,f=q?rows.filter((r,i)=>S[i].includes(ql)):rows,n=Math.max(1,Math.ceil(f.length/ps));if(pg>=n)pg=n-1; return tbl(h,f.slice(pg*ps,pg*ps+ps),foot)+(n>1?`
Page ${pg+1} of ${n} (${f.length} rows)
`:'')} function memberPicker(id,v,allowed){ const ms=(allowed||db.members.filter(m=>!m.left||String(m.id)==String(v))).slice().sort((a,b)=>a.name.localeCompare(b.name,undefined,{sensitivity:'base'})); const cur=ms.find(m=>String(m.id)==String(v)); const list=id+'_names'; return `
${ms.map(m=>``).join('')}
Type a name to find a member
`; } function pickMember(id,name){ const t=String(name||'').trim().toLowerCase(), el=$('#'+id), src=document.getElementById(id+'_names'); if(!el)return; const ms=[...src.options].map(o=>o.value); const exact=ms.find(x=>x.trim().toLowerCase()==t); if(exact){const m=db.members.find(x=>x.name.trim().toLowerCase()==t);el.value=m?m.id:'';} else el.value=''; if(id=='f_lid')sug(); } function field([k,lab,ty],rec,t){const v=rec?rec[k]:((t=='loans'&&k=='amt')?'':dflt()[k]);let el; if(ty=='mem'){const ms=db.members.filter(m=>!m.left||String(m.id)==String(v));el=memberPicker('f_'+k,v,ms)} else if(ty=='loan'){const vm=rec?loanOf(v).mid:v;const ms=db.members.filter(m=>db.loans.some(l=>String(l.mid)==String(m.id)&&(act(l)||String(l.id)==String(v)))).sort((a,b)=>a.name.localeCompare(b.name,undefined,{sensitivity:'base'}));el=memberPicker('f_'+k,vm,ms)} else if(ty=='date'){const a=['deposits','loans','reps','members'].includes(t)&&(k=='date'||k=='join')?` min="${startDate()}" max="${today()}"`:'';el=bsCtl('f_'+k,v,'date',a)} else if(ty=='month'){const a=t=='deposits'?` max="${bsMonthFromAd(today())}"`:'';el=bsCtl('f_'+k,v,'month',a)} else el=``; return``} function bulkView(){ const month=String(bm).slice(0,7), start=startDate(); const rows=live().filter(x=>bsMonthFromAd(x.join||start)<=month&&!ix().dep.has(String(x.id)+'|'+month)) .filter(x=>!depQ||x.name.toLowerCase().includes(depQ.trim().toLowerCase())) .sort((a,b)=>a.name.localeCompare(b.name,undefined,{sensitivity:'base'})); return `
`+ `
${tbl(['Save','Member','Amount (Rs)','Fine (Rs)','Pending months'],rows.map(x=>[``,esc(x.name),``,``,pend(x).length]))}

Only members who have not paid for the selected month are shown. Members already paid are listed in the deposit history below.

`} function filterBulk(){const q=String(depQ||'').trim().toLowerCase();document.querySelectorAll('#bulk_rows tbody tr').forEach(r=>{const n=(r.cells[1]?.textContent||'').toLowerCase();r.style.display=!q||n.includes(q)?'':'none'})} function selectPending(on){document.querySelectorAll('input[id^="bk_"]').forEach(e=>e.checked=on);} function saveBulk(){if(ro('deposits'))return;const additions=[];const d=$('#b_d').value,c0=startDate(); if(!d||dtoday())return alert('A deposit cannot be dated in the future. Today is '+bsLabel(today())+'.');if(bm>bsMonthFromAd(today()))return alert('A deposit cannot be made for a future month.'); for(const x of live()){const k=$('#bk_'+x.id);if(!k||!k.checked)continue;const tr=k.closest('tr');if(tr&&tr.style.display==='none')continue;if(bsMonthFromAd(x.join||c0)>bm)continue;if(db.deposits.some(z=>String(z.mid)==String(x.id)&&String(z.month).slice(0,7)==String(bm).slice(0,7)))return alert(x.name+' has already paid for '+mf(bm)+'. Refresh the checklist. No deposits were saved.');if(d<(x.join||c0))return alert('The selected deposit date '+bsLabel(d)+' is before '+x.name+' joined on '+bsLabel(x.join)+'. No deposits were saved.');const a=+$('#ba_'+x.id).value||0;if(a<=0)continue;additions.push({date:d,mid:String(x.id),month:bm,amt:a,fine:Math.max(0,+$('#bf_'+x.id).value||0)});} if(!additions.length)return alert('Nothing to save. Tick at least one member with an amount above zero.');const next=Object.assign({},db,{deposits:db.deposits.concat(additions)});if(!requireValidCashTimeline(next))return;db.deposits.push(...additions);save();render()} function memBulkView(){return`

Phone is optional. You can paste two columns (name and phone) straight from a spreadsheet. Names already in the group are skipped.

`} function saveMemBulk(){if(offlineReadOnly||navigator.onLine===false)return alert('Bulk member entry is online-only. Use the single-member form to save offline drafts.');if(ro('members'))return;const d=$('#mb_d').value,c0=startDate(); if(!d)return alert('Choose the joining date.');if(dtoday())return alert('The joining date cannot be in the future.'); const seen=new Set(db.members.map(m=>m.name.trim().toLowerCase())),ok=[],bad=[]; $('#mb_t').value.split(/\r?\n/).forEach((ln,i)=>{ln=ln.trim();if(!ln)return;const p=ln.split(/\t|,/).map(x=>x.trim()),name=p[0].replace(/\s+/g,' '),k=name.toLowerCase(); if(!name)bad.push('Line '+(i+1)+': name is missing');else if(seen.has(k))bad.push('Line '+(i+1)+': '+name+' (already in the group or repeated)');else{seen.add(k);ok.push({name,phone:p[1]||'',join:d})}}); if(!ok.length)return alert('No members to save.'+(bad.length?'\n\n'+bad.join('\n'):'\nEnter one member per line.')); let id=Math.max(Date.now(),...db.members.map(m=>(+m.id||0)+1)); ok.forEach(m=>{m.id=id++;db.members.push(m)}); mbt='';save();render(); alert(ok.length+' member(s) added.'+(bad.length?'\n\nSkipped:\n'+bad.join('\n'):''))} function sheet(t){const c=T[t],e=ed&&ed.t==t?ed:null,rec=e?db[t][e.i]:null,cl=!!db.closed;let h=''; if(!cl&&!offlineReadOnly&&navigator.onLine!==false&&t=='deposits'&&can('deposits'))h+=`
`; if(!cl&&!offlineReadOnly&&navigator.onLine!==false&&t=='members'&&can('members'))h+=`
`; if(cl);else if(!can(NEED[t]))h+='
You can view this list but you do not have the power to add entries here. Ask the Owner if you need it.
';else if(t=='deposits'&&bulk)h+=bulkView(); else if(t=='members'&&mb&&!e)h+=memBulkView(); else h+=`
${c.f.map(x=>field(x,rec,t)).join('')}${e?'':''}
`; const MO=i=>``,canEditMem=(t==='members'&&can('members')),E_=can('edit_records')||canEditMem,A=c.a&&(cl?t=='loans':(E_||t=='loans')),ac=i=>cl?MO(i):(E_?c.a(i):MO(i)),hd=c.h.concat(A?['']:[]),rows=db[t].map((x,i)=>c.r(x).concat(A?[ac(i)]:[])).reverse();LF=()=>paged(hd,rows,20); h+=`
${LF()}
`; if(t=='loans'&&selL){const l=loanOf(selL);if(l.id)h+=`

Monthly interest: ${nm(l.mid)}, ${rs(l.amt)} at ${l.rate}%

`+tbl(['Month (AD calendar)','Principal at start of month','Interest charged'],sched(l).map(r=>[esc(r[0]),rs(r[1]),rs(r[2])]))} return h} let dashMonth=bsMonthFromAd(today()); function dashMonthShift(v,n){let [y,m]=String(v).split('-').map(Number);m+=n;while(m<1){m+=12;y--}while(m>12){m-=12;y++}return y+'-'+String(m).padStart(2,'0')} function dashMonthRows(){const out=[];for(let i=5;i>=0;i--){const mo=dashMonthShift(dashMonth,-i),b=bsMonthBounds(mo),inM=x=>x.date>=b.start&&x.date<=b.end;const dep=db.deposits.filter(inM),rep=db.reps.filter(inM),loan=db.loans.filter(inM);out.push({mo,deposit:sum(dep,x=>x.amt),repay:sum(rep,x=>x.prin),issued:sum(loan,x=>x.amt),interest:sum(rep,x=>x.int),fine:sum(dep,x=>x.fine)})}return out} function dashRecent(){const a=[];db.deposits.forEach(x=>a.push({date:x.date,type:'Deposit',member:nm(x.mid),amount:(+x.amt||0)+(+x.fine||0),detail:'Deposit '+rs(x.amt)+(x.fine?' · Fine '+rs(x.fine):'')}));db.loans.forEach(x=>a.push({date:x.date,type:'Loan issued',member:nm(x.mid),amount:+x.amt||0,detail:'Loan issued'}));db.reps.forEach(x=>a.push({date:x.date,type:'Loan repayment',member:nm(loanOf(x.lid).mid),amount:(+x.prin||0)+(+x.int||0),detail:'Principal '+rs(x.prin)+' · Interest '+rs(x.int)}));db.pay.forEach(x=>a.push({date:x.date,type:'Exit settlement',member:nm(x.mid),amount:+(x.settlement_cash??x.settlement_net??((+x.prin||0)+(+x.int||0))),detail:'Member exit settlement'}));return a.sort((x,y)=>y.date.localeCompare(x.date)).slice(0,10)} function dashBarChart(rows,keys,labels){const max=Math.max(1,...rows.flatMap(r=>keys.map(k=>r[k])));return '
'+rows.map(r=>'
'+mf(r.mo)+'
'+keys.map((k,i)=>'
').join('')+'
'+rs(keys.reduce((t,k)=>t+r[k],0))+'
').join('')+'
'+labels.map((x,i)=>''+x+'').join('')+'
'} function dash(){ const month=dashMonth,mbx=bsMonthBounds(month),prev=dashMonthShift(month,-1),pbx=bsMonthBounds(prev),inRange=(x,b)=>x.date>=b.start&&x.date<=b.end; const outstanding=sum(db.loans,bal),receivable=sum(db.loans,intDue),activeLoans=db.loans.filter(l=>bal(l)>0).length; const pendingMembers=db.members.filter(m=>!m.left&&pend(m).length),recent=dashRecent(),months=dashMonthRows(); const depDue=sum(pendingMembers,m=>pend(m).length*db.set.dep),interestReceived=sum(db.reps.filter(x=>inRange(x,mbx)),x=>x.int),fineReceived=sum(db.deposits.filter(x=>inRange(x,mbx)),x=>x.fine); const prevInt=sum(db.reps.filter(x=>inRange(x,pbx)),x=>x.int),prevFine=sum(db.deposits.filter(x=>inRange(x,pbx)),x=>x.fine); const stat=(label,val,sub='')=>'
'+label+''+val+''+(sub?''+sub+'':'')+'
'; const recentHtml=recent.length?'
'+recent.map(x=>'').join('')+'
DateTransactionMemberAmount
'+df(x.date)+''+x.type+''+x.detail+''+x.member+''+rs(x.amount)+'
':'

Nothing recorded yet.

'; const pendingHtml=pendingMembers.length?'
'+pendingMembers.map(m=>'').join('')+'
MemberPending monthsAmount due
'+esc(m.name)+''+pend(m).length+''+rs(pend(m).length*db.set.dep)+''+(db.closed?'':`` )+'
':'

No pending deposits found.

'; return `

Current Financial Position

${stat('Active Members',live().length)}${stat('Inactive / Non-depositing',pendingMembers.length)}${stat('Total Savings',rs(sav()))}${stat('Outstanding Loan',rs(outstanding))}${stat('Interest Receivable',rs(receivable))}${stat('Active Loans',activeLoans)}${stat('Available Cash',rs(cash()),'Combined recorded cash position')}

Available Cash is calculated from the transaction records currently stored in the app. Separate cash-box, bank and wallet balances are not yet available in this data model.

Pending Matters

Pending member deposits${rs(depDue)}
${pendingHtml}

Approval requests are available in the Approvals section. Loan repayment overdue status is not inferred because due dates are not recorded.

Monthly Performance

${stat('Deposits',rs(months[5].deposit))}${stat('Loans issued',rs(months[5].issued))}${stat('Repayment',rs(months[5].repay))}

Six-month comparison

${dashBarChart(months,['deposit','issued','repay'],['Deposits','Loans issued','Repayment'])}

Repayment shows principal only. Daily details and transaction drill-down can be added when the underlying transaction-detail view is connected to this dashboard.

Income & Expenses

${stat('Interest Income',rs(interestReceived),'Previous month: '+rs(prevInt))}${stat('Fine Income',rs(fineReceived),'Previous month: '+rs(prevFine))}${stat('Other Income','Not recorded')}${stat('Expenses','Not recorded')}${stat('Net Income','Not available','Expense records are not currently supported')}

This build does not yet store general expense transactions or separately classified Other Income. A truthful income-versus-expenses comparison and six-month bar chart require those records; no zero values are fabricated.

Recent Activities

Latest 10 financial transactions, newest first.

${recentHtml}
` } function earnTbl(){ const P=pools(),T0=[0,0,0,0,0,0]; const L=live().map(m=>{const s=saved(m.id),ls=db.loans.filter(l=>l.mid==m.id),a=P.den?s/P.den*P.I:0,f=P.den?s/P.den*P.F:0,v=[s,a,f,s+a+f,sum(ls,bal),sum(ls,intDue)];v.forEach((x,i)=>T0[i]+=x);return{n:m.name,v}}); L.sort(sortBy=='tot'?(x,y)=>y.v[3]-x.v[3]:(x,y)=>x.n.localeCompare(y.n)); return paged(['Member','Savings','Interest share','Fine share','Total earning','Loan balance','Interest due'],L.map(o=>[esc(o.n),...o.v.map((x,i)=>i==3?''+rs(x)+'':rs(x))]),1e9,L.length?['Total (all members)',...T0.map(x=>''+rs(x)+'')]:null)} function earn(){LF=earnTbl; return`

Member earnings as of ${df(asof())}

${PB}
${earnTbl()}

Total earning = savings + share of the interest and fines received so far, split in proportion to savings. Interest still due is not counted until it is paid.

`} let rm=bsMonthFromAd(today()); function dMonth(ad){return bsMonthFromAd(ad)} function bsMonthBounds(v){const [y,m]=String(v).split('-').map(Number);const start=bsToAd(y,m,1);let ny=y,nm=m+1;if(nm>12){nm=1;ny++}const next=BSD[ny]?bsToAd(ny,nm,1):null;const end=next?adStr(adUtc(next)-86400000):today();return{start,end}} function rMonthly(){ const mbx=bsMonthBounds(rm),E=mbx.end,inM=x=>dMonth(x.date)==rm,conM=x=>depMatchesMonth(x,rm),up=x=>x.date<=E,D=db.deposits.filter(conM),C=db.deposits.filter(inM),R=db.reps.filter(inM); const v=[['Deposits collected',sum(D,d=>d.amt)],['Fines collected',sum(D,d=>d.fine)],['Loans given',sum(db.loans.filter(inM),l=>l.amt)],['Principal repaid',sum(R,r=>r.prin)],['Interest received',sum(R,r=>r.int)],['Cash at month end',sum(db.deposits.filter(up),d=>(+d.amt||0)+(+d.fine||0))+sum(db.reps.filter(up),r=>(+r.prin||0)+(+r.int||0))-sum(db.loans.filter(up),l=>l.amt)-sum(db.pay.filter(up),p=>(+p.prin||0)+(+p.int||0))]]; const rows=db.members.map((m,i)=>[i+1,esc(m.name),(ix().dep.has(String(m.id)+'|'+String(rm).slice(0,7))?'Paid':(bsMonthFromAd(m.join||startDate())>rm?'Not joined':(m.left&&bsMonthFromAd(m.left.date)d.mid==m.id),d=>d.amt)),rs(sum(db.deposits.filter(d=>d.mid==m.id&&up(d)),d=>d.amt)),rs(sum(db.loans.filter(l=>l.mid==m.id&&up(l)),l=>l.amt)-sum(db.reps.filter(r=>up(r)&&loanOf(r.lid).mid==m.id),r=>r.prin)),rs(sum(db.loans.filter(l=>String(l.mid)==String(m.id)),l=>intDue(l,E))),rs(sum(R.filter(r=>loanOf(r.lid).mid==m.id),r=>r.int))]); return rhead('Monthly Performance','Month: '+mf(rm))+`
${PB}
`+v.map((x,i)=>`
${x[0]}${rs(x[1])}
`).join('')+'
'+tbl(['S.N.','Member','Deposit status','Deposited this month','Total savings','Loan balance','Interest due','Interest paid this month'],rows)+rfoot()} let rt='monthly',rmem=''; const RT=[['overview','Financial Overview'],['indiv','Member Statement'],['cashbook','Cash Book'],['deposit','Deposit Report'],['loan','Loan Reports'],['recovery','Loan Recovery & Overdue'],['income','Income & Expenses'],['monthly','Monthly Performance'],['member','Member Portfolio'],['earn','Member Earnings'],['exithist','Member Exit History']]; const PB='',byDate=(a,b)=>a.dateb.date?1:0; const mopts=all=>(all?'':'')+db.members.map(m=>``).join(''); const msel=all=>``; const B=v=>''+v+''; const rhead=(t,sub)=>`
${esc(firm.name)}
${t}
${sub}
`; const rsum=a=>'
'+a.map(x=>`
${x[0]}${x[1]}
`).join('')+'
'; const rfoot=()=>`

Generated on ${df(today())} · Samuha Bachat

`; function rOverview(){const dep=sav(),lo=sum(db.loans,bal),ir=sum(db.reps,r=>r.int),fine=sum(db.deposits,d=>d.fine),cashv=cash(),active=live().length,borrowers=new Set(db.loans.filter(l=>bal(l)>0).map(l=>String(l.mid))).size;return rhead('Financial Overview','As of '+df(asof()))+`
${PB}
`+rsum([['Active members',active],['Savings balance',rs(dep)],['Outstanding principal',rs(lo)],['Interest receivable',rs(sum(db.loans,intDue))],['Interest collected',rs(ir)],['Fines collected',rs(fine)],['Active borrowers',borrowers],['Cash available',rs(cashv)]])+tbl(['Indicator','Value'],[['Total deposits',rs(sum(db.deposits,d=>d.amt))],['Loans disbursed',rs(sum(db.loans,l=>l.amt))],['Principal recovered',rs(sum(db.reps,r=>r.prin))],['Interest collected',rs(ir)],['Cash available',rs(cashv)]] )+rfoot()} function rRecovery(){const L=db.loans.filter(l=>bal(l)>0||intDue(l)>0).sort((a,b)=>intDue(b)-intDue(a));return rhead('Loan Recovery','Outstanding loans as of '+df(asof()))+`
${PB}
`+rsum([['Active loans',L.length],['Outstanding principal',rs(sum(L,bal))],['Interest receivable',rs(sum(L,intDue))]])+tbl(['S.N.','Member','Loan date','Original principal','Principal outstanding','Interest receivable','Last repayment'],L.map((l,i)=>{const rr=db.reps.filter(r=>r.lid==l.id).sort(byDate),last=rr.length?rr[rr.length-1].date:'None';return[i+1,nm(l.mid),df(l.date),rs(l.amt),rs(bal(l)),rs(intDue(l)),last==='None'?last:df(last)]}),['',B('Total'),' ',B(rs(sum(L,l=>l.amt))),B(rs(sum(L,bal))),B(rs(sum(L,intDue))),''])+rfoot()} function rCashbook(){ const E=asof(),items=[]; db.deposits.filter(x=>x.date<=E).forEach(x=>items.push({date:x.date,type:'Savings received',name:nm(x.mid),ref:mf(x.month),in:(+x.amt||0)+(+x.fine||0),out:0})); db.reps.filter(x=>x.date<=E).forEach(x=>items.push({date:x.date,type:'Loan repayment received',name:nm(loanOf(x.lid).mid),ref:'Loan repayment',in:(+x.prin||0)+(+x.int||0),out:0})); db.loans.filter(x=>x.date<=E).forEach(x=>items.push({date:x.date,type:'Loan disbursed',name:nm(x.mid),ref:'Loan #'+esc(x.id),in:0,out:+x.amt||0})); db.pay.filter(x=>x.date<=E).forEach(x=>items.push({date:x.date,type:'Member exit settlement',name:nm(x.mid),ref:'Exit settlement',in:0,out:(x.settlement_cash!=null?Math.max(0,+x.settlement_cash):Math.max(0,+x.settlement_net||((+x.prin||0)+(+x.int||0))))})); items.sort((a,b)=>a.date.localeCompare(b.date));let run=0;items.forEach(x=>{run+=x.in-x.out;x.balance=run}); return rhead('Cash Book','All recorded cash movements through '+df(E))+`
${PB}
`+rsum([['Receipts',rs(sum(items,x=>x.in))],['Payments',rs(sum(items,x=>x.out))],['Calculated cash balance',rs(run)]])+tbl(['S.N.','Date','Description','Member','Reference','Receipts','Payments','Running balance'],items.map((x,i)=>[i+1,df(x.date),x.type,x.name,x.ref,rs(x.in),rs(x.out),rs(x.balance)]),['','','','','',B(rs(sum(items,x=>x.in))),B(rs(sum(items,x=>x.out))),B(rs(run))])+`

Calculated from the app's recorded savings, fines, repayments, loan disbursements, and exit settlements. Reconcile against actual cash/bank records before treating this as a verified cash balance.

`+rfoot() } function rIncome(){ const deposits=sum(db.deposits,d=>+d.fine||0),exitFines=sum(db.pay,p=>+p.exit_fine||0),interest=sum(db.reps,r=>+r.int||0),paidInterest=sum(db.pay,p=>+p.int||0),income=deposits+exitFines+interest-paidInterest; const costs=sum(db.loans,l=>+l.amt||0),principal=sum(db.reps,r=>+r.prin||0),exitPrincipal=sum(db.pay,p=>+p.prin||0); return rhead('Income & Expenses','Cumulative amounts through '+df(asof()))+`
${PB}
`+rsum([['Deposit fines collected',rs(deposits)],['Exit fines',rs(exitFines)],['Fines collected',rs(deposits+exitFines)],['Interest received',rs(interest)],['Interest paid on exit',rs(paidInterest)],['Net recorded income',rs(income)]])+tbl(['Particulars','Amount','Treatment'],[['Penalty/fine receipts',rs(deposits),'Income'],['Interest received on repayments',rs(interest),'Income'],['Interest paid in exit settlements',rs(paidInterest),'Deduction'],['Net recorded interest and fine income',B(rs(income)),'Calculated'],['Loan principal disbursed',rs(costs),'Cash outflow; not an expense'],['Principal recovered',rs(principal),'Principal recovery; not income'],['Principal paid in exit settlements',rs(exitPrincipal),'Settlement movement; not an expense']])+`

This is a management summary of amounts represented in the current app data, not a formal profit-and-loss statement. Operating expenses and a full double-entry chart of accounts are not currently recorded in this data model.

`+rfoot() } function rExitHist(){ const E=asof(); const settlements=(db.pay||[]).filter(p=>p.date<=E).slice().sort(byDate).reverse(); const totalPrin=sum(settlements,p=>+p.prin||0); const totalInt=sum(settlements,p=>+p.int||0); const totalGross=totalPrin+totalInt; const totalDed=sum(settlements,p=>sum(db.reps.filter(r=>r.x==p.id),r=>(+r.prin||0)+(+r.int||0))); const totalNet=totalGross-totalDed; const rows=settlements.map((p,i)=>{ const reps=db.reps.filter(r=>r.x==p.id); const pDed=sum(reps,r=>+r.prin||0); const iDed=sum(reps,r=>+r.int||0); const ded=pDed+iDed; const gross=(+p.prin||0)+(+p.int||0); const net=gross-ded; const dedDetails=ded>0?`${rs(ded)} (P: ${rs(pDed)}, I: ${rs(iDed)})`:'None'; return [ i+1, nm(p.mid), df(p.date), rs(p.prin), rs(p.int), rs(gross), dedDetails, B(rs(net)) ]; }); const stats=[ ['Exited members',settlements.length], ['Savings refunded',rs(totalPrin)], ['Earnings share paid',rs(totalInt)], ['Total loan deductions',rs(totalDed)], ['Net cash paid out',rs(totalNet),'c'] ]; const foot=settlements.length?[ '', B('Total'), '', B(rs(totalPrin)), B(rs(totalInt)), B(rs(totalGross)), B(rs(totalDed)), B(rs(totalNet)) ]:null; return rhead('Member Exit History','All recorded member departures and settlements through '+df(E))+ `
${PB}
`+ rsum(stats)+ tbl(['S.N.','Member','Exit Date','Savings Refunded','Interest Share','Gross Settlement','Loan Deductions','Net Settlement'],rows,foot)+ `

This report lists all past member exit settlements. Gross Settlement includes the refunded savings balance and the departing member's share of interest and fines. Loan Deductions shows the principal and interest cleared from the member's outstanding loans on their leaving date to determine the net amount paid.

`+ rfoot(); } const REPORT_GROUPS=[ ['overview','Financial Overview',['overview'],'▦','Group financial snapshot'], ['member','Member Reports',['member','indiv','exithist'],'♙','Portfolio, statements and exit history'], ['cash','Cash & Financial Transactions',['cashbook','deposit'],'₹','Cash book and deposit records'], ['loan','Loan Reports',['loan','recovery'],'↗','Loans, balances and recovery'], ['monthly','Monthly Performance',['monthly','earn'],'◷','Monthly trends and member earnings'], ['income','Income & Expenses',['income'],'+','Recorded income and expense summary'] ]; const NAV_ICON={ 'Dashboard':'', 'Members':'', 'Financial Transactions':'', 'Approvals':'', 'Reports':'', 'Administration':'', lang:'', out:'', close:'' }; const RPT_ICON={ overview:NAV_ICON.Dashboard,member:NAV_ICON.Members,cash:NAV_ICON['Financial Transactions'], loan:'', monthly:'', income:''}; const SUB_ICON={ member:NAV_ICON.Members, indiv:'', exithist:NAV_ICON.out, cashbook:'', deposit:'', loan:RPT_ICON.loan, recovery:'', monthly:RPT_ICON.monthly, earn:'', income:RPT_ICON.income,overview:NAV_ICON.Dashboard}; const SUBM={member:['Member Portfolio','Member balances and current position'],indiv:['Member Statement','Savings, loans and repayments for one member'],exithist:['Member Exit History','Previously recorded member departures'],cashbook:['Cash Book','Recorded receipts and payments'],deposit:['Deposit Report','Savings deposits and fines'],loan:['Loan Reports','Loans issued, principal and interest'],recovery:['Loan Recovery & Overdue','Outstanding balances and recovery'],monthly:['Monthly Performance','Performance by month'],earn:['Member Earnings','Recorded member earnings'],income:['Income & Expenses','Summary of recorded income and expenses'],overview:['Financial Overview','Group financial snapshot']}; function selectReportCat(catKey){ const g=REPORT_GROUPS.find(x=>x[0]===catKey); if(!g)return; if(!g[2].includes(rt))rt=g[2][0]; render(); } function report(){ const labels=Object.fromEntries(RT); const curGroup=REPORT_GROUPS.find(g=>g[2].includes(rt))||REPORT_GROUPS[0]; const cur=SUBM[rt]; const tier1=`

Reports

`+ REPORT_GROUPS.map(([gId,gName])=>``).join('')+`
`; const tier2=(curGroup[2].length>1?`
`+ curGroup[2].map(k=>{const m=SUBM[k]||[labels[k]||k,'Open report'];return ``}).join('')+`
`:'')+ `

${cur?cur[1]:curGroup[4]}

`; const reportMap={overview:rOverview,member:rMember,deposit:rDeposit,loan:rLoan,monthly:rMonthly,earn:earn,recovery:rRecovery,indiv:rIndiv,cashbook:rCashbook,income:rIncome,exithist:rExitHist}; return tier1+tier2+(reportMap[rt]||rOverview)(); } function rMember(){ const rows=db.members.map((m,i)=>{const p=pend(m).length;return[i+1,esc(m.name)+(m.left?' (left)':''),esc(m.phone),df(m.join),rs(saved(m.id)),p?p+' ('+rs(p*db.set.dep)+')':'None',rs(sum(db.loans.filter(l=>l.mid==m.id),bal))]}); return rhead('Member Portfolio','As of '+df(asof()))+`
${PB}
`+rsum([['Active members',live().length],['Total savings',rs(sav())],['Loans outstanding',rs(sum(db.loans,bal))]])+tbl(['S.N.','Name','Phone','Joined','Total savings','Pending months','Loan balance'],rows,['',B('Total'),'','',B(rs(sav())),'',B(rs(sum(db.loans,bal)))])+rfoot()} function rDeposit(){ const f=rm,L=db.deposits.filter(d=>depMonth(d)===f&&(!rmem||d.mid==rmem)).sort(byDate); return rhead('Deposit Report','Month: '+mf(f)+(rmem?' · '+nm(rmem):' · All members'))+`
${msel(1)}${PB}
`+ rsum([['Entries',L.length],['Deposits',rs(sum(L,d=>d.amt))],['Fines',rs(sum(L,d=>d.fine))]])+tbl(['S.N.','Date','Member','Month','Amount','Fine'],L.map((d,i)=>[i+1,df(d.date),nm(d.mid),mf(d.month),rs(d.amt),rs(d.fine)]),['','',B('Total'),'',B(rs(sum(L,d=>d.amt))),B(rs(sum(L,d=>d.fine)))])+rfoot()} function rLoan(){ const L=db.loans.filter(l=>!rmem||l.mid==rmem).sort(byDate), c=L.map(l=>({l,pr:sum(db.reps.filter(r=>r.lid==l.id),r=>r.prin),ip:sum(db.reps.filter(r=>r.lid==l.id),r=>r.int),ch:sum(sched(l),x=>x[2])})),T=f=>B(rs(sum(c,f))); return rhead('Loan Reports','As of '+df(asof())+(rmem?' · '+nm(rmem):' · All members'))+`
${msel(1)}${PB}
`+rsum([['Loans given',rs(sum(L,l=>l.amt))],['Principal left',rs(sum(L,bal))],['Interest due',rs(sum(L,intDue))]])+ tbl(['S.N.','Member','Loan date','Loan amount','Rate','Principal repaid','Principal left','Interest charged','Interest paid','Interest due'],c.map((x,i)=>[i+1,nm(x.l.mid),df(x.l.date),rs(x.l.amt),x.l.rate+'%',rs(x.pr),rs(x.l.amt-x.pr),rs(x.ch),rs(x.ip),rs(intDue(x.l))]), ['',B('Total'),'',T(x=>x.l.amt),'',T(x=>x.pr),T(x=>x.l.amt-x.pr),T(x=>x.ch),T(x=>x.ip),B(rs(sum(L,intDue)))])+rfoot()} function rIndiv(){ const m=db.members.find(x=>x.id==rmem)||db.members[0];if(!m)return'

Add a member first.

'; const P=pools(),s=saved(m.id),a=P.den?s/P.den*P.I:0,f=P.den?s/P.den*P.F:0,ls=db.loans.filter(l=>l.mid==m.id).sort(byDate),p=pend(m), D=db.deposits.filter(d=>d.mid==m.id).sort(byDate),R=db.reps.filter(r=>loanOf(r.lid).mid==m.id).sort(byDate), st=[['Total savings',s],['Interest share',a],['Fine share',f],['Total earning',s+a+f],['Loan balance',sum(ls,bal)],['Interest due',sum(ls,intDue)]]; return rhead('Member Statement',esc(m.name)+' · As of '+df(asof()))+`

Phone: ${esc(m.phone||'-')} · Joined: ${df(m.join)} · Pending deposits: ${p.length?p.length+' month(s), '+rs(p.length*db.set.dep):'None'} · As of: ${df(asof())}

${msel(0)}${PB}
`+ st.map((x,i)=>`
${x[0]}${rs(x[1])}
`).join('')+'

Deposits

'+ tbl(['S.N.','Date','Month','Amount','Fine'],D.map((d,i)=>[i+1,df(d.date),mf(d.month),rs(d.amt),rs(d.fine)]),['',B('Total'),'',B(rs(sum(D,d=>d.amt))),B(rs(sum(D,d=>d.fine)))])+'

Loans

'+ tbl(['S.N.','Loan date','Loan amount','Rate','Principal repaid','Principal left','Interest due'],ls.map((l,i)=>[i+1,df(l.date),rs(l.amt),l.rate+'%',rs(l.amt-bal(l)),rs(bal(l)),rs(intDue(l))]))+'

Repayments

'+ tbl(['S.N.','Date','Loan date','Principal','Interest'],R.map((r,i)=>[i+1,df(r.date),df(loanOf(r.lid).date),rs(r.prin),rs(r.int)]),['',B('Total'),'',B(rs(sum(R,r=>r.prin))),B(rs(sum(R,r=>r.int)))])+rfoot()} function kv(r){return'
'+r.map(x=>'').join('')+'
'+x[0]+''+x[1]+'
'} function roleLabel(){if(cloudExactRole==='super_admin')return 'Super Admin';if(cloudRole==='owner')return 'Admin';const j=cloudJob||(cloudRole==='viewer'?'Viewer':'Manager');return cloudName?cloudName+' · '+j:j} function powerText(){if(cloudRole==='owner')return 'Everything';const l=PERMS.filter(x=>can(x[0])).map(x=>x[1]);return l.length?l.join(', '):'View only'} function groupInfo(){ const x=cloudGroupInfo||{},cl=db.closed,c0=startDate(),e0=asof(); const active=live().length,left=Math.max(0,db.members.length-active); const rules=[ ['Monthly deposit',rs(db.set.dep||0)], ['Loan interest rate',String(db.set.rate||0)+'% per year'], ['Loan limit',db.set.lim?String(db.set.lim)+' × savings':'No limit'], ['Group description',firm.description||'Not provided'], ['Estimated close date',firm.estimatedClose?df(firm.estimatedClose):'Not set'] ]; return '

Current Group Profile

'+ '

Read-only overview of this group’s information, rules, members and financial position.

'+ '

Group Information

'+kv([ ['Group name',esc(firm.name)], ['Group address',esc(firm.address||x.address||'Not recorded')], ['Authorized person',esc(firm.authorizedPerson||firm.authorized_person||x.authorized_person||'Not recorded')], ['Group ID',''+esc(firm.id)+''], ['Created on',df(created())], ['Group start date',df(c0)], ['Status',cl?'Closed on '+esc(df(cl.date)):'Active'], ['Group description',esc(firm.description||'Not provided')] ])+ '

Group Rules

'+kv(rules.map(r=>[r[0],esc(String(r[1]))]))+ '

Member Information

'+kv([ ['Active members',active], ['Members who left',left], ['Total member records',db.members.length], ['Members with pending deposits',db.members.filter(m=>pend(m).length).length] ])+ '

Financial Overview

'+kv([ ['Total savings',rs(sav())], ['Loans outstanding',rs(sum(db.loans,bal))], ['Cash in hand',rs(cash())], ['Total deposits recorded',rs(sum(db.deposits,d=>d.amt||0))], ['Loan records',db.loans.length], ['Last backup',db.lastBackup?df(db.lastBackup):'Not downloaded yet'] ])+ '

This page displays current group details for reading. To change group settings or rules, use the appropriate Administration options if your role allows it.

'; } function profile(){ const cl=db.closed,c0=startDate(),e0=asof(),days=Math.max(0,Math.round((new Date(e0)-new Date(c0))/864e5)), A=new Date(c0),Z=new Date(e0),dm=(Z.getUTCFullYear()-A.getUTCFullYear())*12+Z.getUTCMonth()-A.getUTCMonth()-(Z.getUTCDate()1?' years':' year'),mo&&mo+(mo>1?' months':' month'),(d||(!y&&!mo))&&d+(d==1?' day':' days')].filter(Boolean).join(', '), dep=sum(db.deposits,x=>x.amt),fines=sum(db.deposits,x=>x.fine),ir=sum(db.reps,r=>r.int), open=db.loans.filter(l=>bal(l)>0),borrowers=new Set(open.map(l=>String(l.mid))).size, pm=db.members.filter(m=>pend(m).length).length, st=[['Members',live().length],['Total savings',rs(sav())],['Loans outstanding',rs(sum(db.loans,bal))],['Cash in hand',rs(cash()),'c']]; return'

Group profile

'+st.map(x=>`
${x[0]}${x[1]}
`).join('')+'
'+ '

Group details

'+kv([['Group name',esc(firm.name)],['System created on',df(created())],['Group start date',df(c0)],['Status',cl?'Closed on '+esc(df(cl.date)):'Active'],[cl?'Ran for':'Running for',age],['Last backup',db.lastBackup?esc(df(db.lastBackup)):'Not downloaded yet']])+ '

Members

'+kv([['Active members',live().length],['Members who left',db.members.length-live().length],['Members with pending deposits',pm],['Members with a loan running',borrowers]])+ '

Money

'+kv([['Monthly deposit',rs(db.set.dep)],['Loan interest',db.set.rate+'% per year'],['Loan limit',+db.set.lim?db.set.lim+' times savings':'No limit'],['Deposits recorded',db.deposits.length+' ('+rs(dep)+')'],['Loans given',db.loans.length+' ('+rs(sum(db.loans,l=>l.amt))+')'],['Loans still running',open.length],['Interest received',rs(ir)],['Fines received',rs(fines)],['Interest still due',rs(sum(db.loans,intDue))]])+ `
${PB}

Monthly deposit, interest rate and loan limit can be changed in Settings.

`} const NAV_SECTIONS=[ ['Dashboard',[ ['dash','Dashboard']]], ['Members',[ ['members','Register New Member'],['exit','Member Exit']]], ['Financial Transactions',[ ['deposits','Deposit Register'],['loans','Loan Register'],['reps','Loan Repayment']]], ['Approvals',[ ['approval-loan','Loan'],['approval-exit','Member Exit'],['approval-dissolve','Group Dissolve'], ['approval-correction','Transaction Correction'],['approval-rules','Group Rule']]], ['Reports',[ ['rpt-member','Member Reports'],['rpt-deposit','Deposit Report'],['rpt-loan-register','Loan Reports'], ['cashbook','Cash Book'],['rpt-earn','Member Earnings']]], ['Administration',[ ['set','Group Management'],['users','User Management'],['maintenance','System Maintenance'],['dis','Group Dissolve'],['audit','Audit & Activity History']]] ]; let navSection='Dashboard'; function navSectionForTab(k){ if(k==='calc')return 'Tools'; if(cloudRole==='viewer')return 'Reports'; if(k==='members'||k==='exit')return 'Members'; if(k==='deposits'||k==='loans'||k==='reps'||k==='rpt-recovery'||k==='rpt-loanport')return 'Financial Transactions'; if(k.startsWith('approval-'))return 'Approvals'; if(k==='prof'||k==='set'||k==='users'||k==='audit'||k==='dis'||k==='maintenance')return 'Administration'; if(k.startsWith('rpt-')||k==='cashbook'||k==='rpt'||k==='earn')return 'Reports'; return NAV_SECTIONS.find(([name,items])=>items.some(([id])=>id===k))?.[0]||'Dashboard'; } const NAV_PLACEHOLDERS={ 'approval-loan':['Loan Approval','Review pending loan requests. Maker and checker must be different people.'], 'approval-exit':['Member Exit Approval','Review member-exit requests. Approval does not itself post a settlement.'], 'approval-dissolve':['Group Dissolve Approval','Only the firm Admin can approve group-level requests.'], 'approval-correction':['Transaction Correction Approval','Corrections must preserve the original transaction history.'], 'approval-rules':['Group Rule Approval','Only the firm Admin can approve group-level rule changes.'] }; let approvalCache=null; const approvalKinds={'approval-loan':'loan','approval-exit':'member_exit','approval-dissolve':'group_dissolution','approval-correction':'transaction_correction','approval-rules':'group_rule_change'}; const approvalLabel=k=>({'loan':'Loan','member_exit':'Member Exit','transaction_correction':'Transaction Correction','group_dissolution':'Group Dissolution','group_rule_change':'Group Rule Change'}[k]||k); function approvalView(){ const kind=approvalKinds[navPlaceholder],rows=Array.isArray(approvalCache)?approvalCache.filter(r=>!kind||r.kind===kind):[]; const canVerify=cloudExactRole==='admin'||cloudExactRole==='manager'; const body=approvalCache===null?'

Loading approval requests…

':(approvalCache.error?'

'+esc(approvalCache.error)+'

':(rows.length?rows.map(r=>{ let data='{}';try{data=JSON.stringify(r.request_data||{},null,2)}catch(e){} const maker=r.maker_id===cloudUser?.id; const actions=r.status==='pending'&&canVerify&&!maker?'
':(r.status==='pending'&&maker?'

You created this request. Another authorized person must verify it.

':(r.status==='approved'&&!r.applied_at&&canVerify&&!maker&&r.verifier_id===cloudUser?.id?'':(r.applied_at?'

Posted to accounting on '+esc(dtf(r.applied_at))+'.

':''))); return '
'+esc(approvalLabel(r.kind))+''+esc(r.status)+'

Created '+esc(dtf(r.created_at))+' · '+esc(r.id)+'

'+esc(data)+'
'+(r.decision_note?'

Decision note: '+esc(r.decision_note)+'

':'')+actions+'
'; }).join(''):'

No matching approval requests found.

')); return '

'+esc(NAV_PLACEHOLDERS[navPlaceholder]?.[0]||'Approvals')+'

Decisions are checked and saved by the database. Maker and checker must be different people.

'+body+'
'; } async function requestApproval(kind,requestData,clientRequestId=crypto.randomUUID()){ if(offlineReadOnly){alert('Offline mode cannot submit approval requests. Connect to the internet first.');return false} if(!cloud||!firm||!cloudUser){alert('Sign in to the firm before submitting an approval request.');return false} const {data,error}=await cloud.rpc('sb2_request_approval',{p_firm_id:firm.id,p_kind:kind,p_request_data:requestData,p_client_request_id:clientRequestId}); if(error){alert('Could not submit the approval request: '+error.message);return false} alert((kind==='loan'?'Loan':'Member exit')+' submitted for approval. It will not affect accounting balances until a different Admin or Manager approves and posts it.'); if(tab==='nav-placeholder'&&navPlaceholder.startsWith('approval-'))await loadApprovalRequests(); return true; } async function applyApprovedRequest(id){ if(offlineReadOnly){alert('Approvals can only be posted while online.');return} if(!cloud||!firm)return; if(!confirm('Post this approved request to the accounting records? This updates balances and cash history.'))return; const {data,error}=await cloud.rpc('sb2_apply_approved_request',{p_request_id:id}); if(error){alert('Could not post the approved request: '+error.message);return} const ok=await cloudOpenGroup(firm.id,true,true); if(!ok){alert('The request was posted, but the latest accounting data could not be refreshed. Reopen this firm before making another entry.');return} await loadApprovalRequests();alert('Approved transaction posted to accounting successfully.'); } async function loadApprovalRequests(){ if(!cloud||!firm)return; const {data,error}=await cloud.rpc('sb2_list_approval_requests',{p_firm_id:firm.id,p_limit:100}); approvalCache=error?{error:error.message}:(data||[]); const host=$('#approvalRows');if(host)host.innerHTML=approvalView().split('
')[1].replace(/<\/div>$/,''); } async function decideApproval(id,decision){ if(offlineReadOnly){alert('Approval decisions require an internet connection.');return} if(!confirm((decision==='approved'?'Approve':'Reject')+' this request?'))return; const note=prompt('Optional decision note:')||null; const {error}=await cloud.rpc('sb2_decide_approval',{p_request_id:id,p_decision:decision,p_note:note}); if(error){alert(error.message||'The decision could not be saved.');return} await loadApprovalRequests();alert('Decision recorded.'); } const MEMBER_NAV_ITEMS=[ ['rpt-overview','Financial Overview'], ['rpt-member','Member Reports'], ['rpt-cash','Cash & Financial Transactions'], ['rpt-loan','Loan Reports'], ['rpt-monthly','Monthly Performance'], ['rpt-income','Income & Expenses'], ['ginfo','Group Profile'] ]; function navGo(route,label){ if(route==='pa-return'&&cloudPlatformAdmin){if(cloudUnsaved&&!confirm('You have changes that are not saved. Return to the Super Admin dashboard anyway?'))return;cloudUnsaved=false;try{localStorage.removeItem('sb-last-group');if(cloudUser?.id)localStorage.removeItem('sb-last-group:'+cloudUser.id)}catch(e){}firm=null;db=fresh();tab='dash';platformTab='dashboard';menu=false;SS.set('sb-sess');sst('');render();cloudRefreshPlatformOverview();return;} if(cloudPlatformAdmin&&!firm){const routes={'pa-dashboard':'dashboard','pa-admin':'admin','pa-firms':'firms','pa-login':'login','pa-admins':'admins','pa-firm-management':'firm-management','pa-stats':'stats','pa-activity':'activity','pa-settings':'settings'};if(routes[route]){platformTab=routes[route];menu=false;render();return;}} if(route!=='users')lastCreds=null; /* a temporary password is never kept on screen once the Owner leaves the Users page */ if(firm&&cloudRole==='viewer'){ if(route!=='calc'&&route!=='cashbook'&&!route.startsWith('rpt')&&route!=='prof'&&route!=='ginfo'){ route='rpt-overview'; } } // Resolve each sidebar destination explicitly, then render the selected page. if(route==='exit'){tab='exit';navPlaceholder=''} else if(route==='rpt-overview'){tab='rpt';rt='overview';navPlaceholder=''} else if(route==='rpt-member'){tab='rpt';if(!['member','indiv','exithist'].includes(rt))rt='member';navPlaceholder=''} else if(route==='cashbook'){tab='rpt';rt='cashbook';navPlaceholder=''} else if(route==='rpt-cash'){tab='rpt';if(!['cashbook','deposit'].includes(rt))rt='cashbook';navPlaceholder=''} else if(route==='rpt-portfolio'){tab='rpt';rt='member';navPlaceholder=''} else if(route==='rpt-loanport'){tab='rpt';rt='loan';navPlaceholder=''} else if(route==='rpt-loan'){tab='rpt';if(!['loan','recovery'].includes(rt))rt='loan';navPlaceholder=''} else if(route==='rpt-monthly'){tab='rpt';if(!['monthly','earn'].includes(rt))rt='monthly';navPlaceholder=''} else if(route==='rpt-income'){tab='rpt';rt='income';navPlaceholder=''} else if(route==='rpt-earn'){tab='rpt';rt='earn';navPlaceholder=''} else if(route==='rpt-deposit'){tab='rpt';rt='deposit';navPlaceholder=''} else if(route==='rpt-recovery'){tab='rpt';rt='recovery';navPlaceholder=''} else if(route==='rpt-indiv'){tab='rpt';rt='indiv';navPlaceholder=''} else if(route==='rpt-member-register'){tab='rpt';rt='member';navPlaceholder=''} else if(route==='rpt-loan-register'){tab='rpt';rt='loan';navPlaceholder=''} else if(route==='maintenance'){tab='maintenance';navPlaceholder=''} else if(route==='rpt-exithist'){tab='rpt';rt='exithist';navPlaceholder=''} else if(route.startsWith('rpt-')){rt=route.slice(4);tab='rpt';navPlaceholder=''} else if(route==='rpt'){tab='rpt';if(!rt)rt='overview';navPlaceholder=''} else if(Object.prototype.hasOwnProperty.call(NAV_PLACEHOLDERS,route)){tab='nav-placeholder';navPlaceholder=route} else {tab=route;navPlaceholder=''} if(firm&&cloudRole!=='owner'&&(tab==='dis'||tab==='users'||tab==='audit'||tab==='maintenance'))tab='dash'; if(firm&&cloudRole==='viewer'&&(tab!=='rpt'&&tab!=='calc'&&tab!=='prof'&&tab!=='ginfo')){tab='rpt';if(!rt)rt='overview';} navSection=navSectionForTab(route); ed=null;selL=null;q='';depQ='';pg=0;menu=false;render(); } let navPlaceholder=''; function navToggle(n){navSection=navSection===n?'':n;const nv=$('#nav'),t=nv.scrollTop;nv.innerHTML=navHtml();nv.scrollTop=t;apply()} function groupSwitch(){if(!cloudGroups||cloudGroups.length<2)return'';return''} async function cloudSwitch(id){if(!id||!firm||String(id)===String(firm.id))return;if(cloudUnsaved&&!confirm('You have changes that are not saved. Switch anyway?')){$('#nav').innerHTML=navHtml();return}cloudUnsaved=false;sst('');await cloudOpenGroup(id)} function navPlaceholderView(){const x=NAV_PLACEHOLDERS[navPlaceholder];return navPlaceholder.startsWith('approval-')?approvalView():(x?`

${esc(x[0])}

${esc(x[1])}

`:'')} // Delegate route clicks from the whole app: sidebar links and dashboard Quick Actions. document.addEventListener('click',function(event){ const button=event.target.closest('[data-nav-route]'); if(!button||!document.contains(button))return; event.preventDefault(); navGo(button.dataset.navRoute,button.textContent.trim()); }); const navSvg=(k,s)=>``; function navInitials(s){const p=String(s||'').trim().split(/[\s@._-]+/).filter(Boolean);return((p[0]||'?')[0]+((p[1]||'')[0]||'')).toUpperCase()} function navIsActive(k){ if(k==='exit')return tab==='exit'; if(k==='cashbook')return tab==='rpt'&&rt==='cashbook'; if(k==='rpt-overview')return tab==='rpt'&&rt==='overview'; if(k==='rpt-member')return tab==='rpt'&&['member','indiv','exithist'].includes(rt); if(k==='rpt-cash')return tab==='rpt'&&['cashbook','deposit'].includes(rt); if(k==='rpt-loan'||k==='rpt-loan-register')return tab==='rpt'&&['loan','recovery'].includes(rt); if(k==='rpt-monthly')return tab==='rpt'&&['monthly','earn'].includes(rt); if(k==='rpt-income')return tab==='rpt'&&rt==='income'; if(k==='rpt-earn')return tab==='rpt'&&rt==='earn'; if(k==='rpt-portfolio')return tab==='rpt'&&rt==='member'; if(k==='rpt-loanport')return tab==='rpt'&&rt==='loan'; if(k.startsWith('rpt-'))return tab==='rpt'&&rt===k.slice(4); if(k==='rpt')return tab==='rpt'; if(k==='exit-settle'||NAV_PLACEHOLDERS[k])return tab==='nav-placeholder'&&navPlaceholder===k; return k===tab; } function navItem(k,label){ const on=navIsActive(k),soon=!!NAV_PLACEHOLDERS[k]&&!k.startsWith('approval-'); return ``; } function navBrand(sub){ const w=acctWho(); return `
`; } function navFoot(){ const email=(cloudUser&&cloudUser.email)||'',who=cloudName||email||'Signed in',role=cloudExactRole==='super_admin'?'Super Admin':(cloudRole==='owner'?'Admin':(cloudJob||(cloudRole==='viewer'?'Member':'Manager'))); return ``; } function navHtml(){ if(cloudRole==='viewer'){ return navBrand('Member · Reports Only')+''+navFoot(); } const groups=NAV_SECTIONS.map(([name,items],i)=>{ const allowed=items.filter(([k])=>k!=='prof').filter(([k])=>!(k==='dis'||k==='users'||k==='audit'||k==='maintenance')||cloudRole==='owner'); if(!allowed.length)return''; const cur=allowed.some(([k])=>navIsActive(k)); if(allowed.length===1&&allowed[0][1]===name){ const k=allowed[0][0]; return ``; } const expanded=navSection===name; const sub=expanded?`':''; return `${sub}`; }).join(''); return navBrand(db.closed?'Closed':'Main menu')+(cloudPlatformAdmin&&firm?'':'')+''+navFoot(); } let lm=''; const tabName=()=>tab==='calc'?'Calculator':tab==='rpt'?((RT.find(x=>x[0]===rt)||[])[1]||'Reports'):tab==='nav-placeholder'?(NAV_PLACEHOLDERS[navPlaceholder]?.[0]||'Feature'):((NAV_SECTIONS.flatMap(g=>g[1]).find(x=>x[0]==tab)||[])[1]||(db.closed?'Settlement':'Close group')); const live=()=>db.members.filter(m=>!m.left); const curM=()=>live().find(x=>x.id==lm)||live()[0]; function leaveCalc(m){const P=pools(),s=saved(m.id),ls=db.loans.filter(l=>l.mid==m.id);return{s,e:Math.round(P.den?s/P.den*(P.I+P.F):0),lp:sum(ls,bal),li:Math.round(sum(ls,intDue))}} const netTxt=n=>n>=0?'Group pays the member: '+rs(n):'Member pays the group: '+rs(-n); function xnet(){const c=leaveCalc(curM());const fine=Math.max(0,+($('#x_fine')?.value||0));const base=c.s+c.e-c.lp-c.li;const final=base-fine;const a=$('#x_before');if(a)a.textContent=netTxt(base);const b=$('#x_final');if(b)b.textContent=netTxt(final);const f=$('#x_fine_value');if(f)f.textContent=rs(fine)} function exitView(){const cl=!!db.closed,L=live(),m=curM();let h=''; if(!cl&&!can('exits'))h+='
You can view exits but you do not have the power to record them. Ask the Owner if you need it.
';else if(!cl){h+='

Member Exit

'; if(!m)h+='

There are no active members.

'; else{const c=leaveCalc(m),base=c.s+c.e-c.lp-c.li;h+=`
`+ kv([['Savings',rs(c.s)],['Allocated earnings',rs(c.e)],['Loan principal',rs(c.lp)],['Loan interest',rs(c.li)],['Unpaid fines',rs(sum(db.deposits.filter(d=>d.mid==m.id),d=>+d.fine||0)-sum(db.reps.filter(r=>r.mid==m.id),r=>+r.fine_paid||0))],['Exit Settlement Before Fine',netTxt(base)]])+ `
Final Settlement
${netTxt(base)}

The financial summary and Exit Settlement Before Fine are preserved separately. Recording the exit automatically records the final settlement as paid or collected. Loan principal and interest are recorded as a linked repayment.

`}} return h+'

Members who left

'+tbl(['Member','Left on','Savings considered','Interest considered','Loan deducted','Exit fine','Net settlement','Settlement recorded','Status',...(cl||!can('exits')?[]:[''])],db.pay.slice().reverse().map(p=>{const dd=sum(db.reps.filter(r=>r.x==p.id),r=>(+r.prin||0)+(+r.int||0));return[nm(p.mid),df(p.date),rs(p.prin),rs(p.int),rs(dd),rs(p.exit_fine||0),rs(p.settlement_net!=null?p.settlement_net:p.prin+p.int-dd),rs(p.paid_amount||0),esc(p.status||'settled'),...(cl||!can('exits')?[]:[``])]}))} async function leave(){if(ro('exits'))return;const m=curM();if(!m)return;const c=leaveCalc(m),pr=c.s,pi=c.e,fine=Math.max(0,+$('#x_fine').value||0),reason=($('#x_reason').value||'').trim(),d=$('#x_d').value,base=pr+pi-c.lp-c.li,net=base-fine,paidNow=Math.abs(net),status='settled'; if(!d||dtoday())return alert('Choose a leaving date between the firm start date ('+bsLabel(startDate())+') and today.'); if(!Number.isFinite(net))return alert('Enter a valid exit fine.');if(!reason)return alert('Enter the reason for the exit adjustment.'); const id=Date.now(),out=[];if(c.lp>0||c.li>0){const made=fifo(m.id,d,c.lp,c.li);if(!made)return alert('The loan could not be cleared on this date. Choose a later leaving date.');made.forEach(x=>x.x=id);out.push(...made)} const exitPay={id,mid:m.id,date:d,prin:pr,int:pi,calc_savings:c.s,calc_interest:c.e,loan_principal:c.lp,loan_interest:c.li,exit_settlement_before_fine:base,exit_fine:fine,fine_reason:reason,calculated_net:base-fine,settlement_net:net,settlement_direction:net>0?'pay_member':net<0?'collect_member':'zero',remarks:reason,status,paid_amount:paidNow,settlement_cash:Math.sign(net)*paidNow,payment_date:paidNow>0?d:null,recorded_at:new Date().toISOString(),recorded_by:(cloudUser&&cloudUser.id)||'local-session'}; const next=Object.assign({},db,{reps:db.reps.concat(out),pay:db.pay.concat([exitPay])});if(!requireValidCashTimeline(next))return; if(!confirm('Submit the exit of '+m.name+' for independent approval? No settlement or member-status change will be posted until an authorized checker approves and posts it.'))return;if(cloud){await requestApproval('member_exit',{exit:exitPay,repayments:out});return}db.reps.push(...out);db.pay.push(exitPay);m.left={date:d};save();render()} function undoExit(id){if(ro('exits'))return;const p=db.pay.find(x=>x.id==id),m=p&&db.members.find(x=>x.id==p.mid);if(!m)return; const d=(+p.prin||0)+(+p.int||0)-sum(db.reps.filter(r=>r.x==id),r=>(+r.prin||0)+(+r.int||0)); if(!confirm('Undo the exit of '+m.name+'? The payout and loan deduction recorded with it are removed and the member becomes active again.'))return; const next=Object.assign({},db,{reps:db.reps.filter(r=>r.x!=id),pay:db.pay.filter(x=>x.id!=id)});if(!requireValidCashTimeline(next))return; db.reps=next.reps;db.pay=next.pay;delete m.left;save();render()} const bn=()=>db.closed&&tab!='dis'?`
This group was closed on ${df(db.closed.date)}. Records are read-only. See the Settlement tab.
`:''; /* final amount = savings + share of interest and fines - loan principal - interest still due; interest still due is set off against the borrower and shared like other interest, so the amounts always add up to the cash in hand */ function settle(){ const P=pools(),idue=sum(db.loans,intDue); return live().map(m=>{const ls=db.loans.filter(l=>l.mid==m.id),s=saved(m.id),k=P.den?s/P.den:0,lb=sum(ls,bal),di=sum(ls,intDue),a=k*(P.I+idue),f=k*P.F; return{name:m.name,phone:m.phone||'',s,a,f,lb,di,net:Math.round(s+a+f-lb-di)}})} function stlTbl(R,cashNow,cl){ const S=k=>sum(R,r=>r[k]),pay=sum(R,r=>Math.max(0,r.net)),col=sum(R,r=>Math.max(0,-r.net)),tot=R.filter(r=>r.net).length,done=R.filter(r=>r.net&&r.paid).length, st=[['Cash in hand',cashNow],['To pay out to members',pay],['To collect from members',col],['Rounding kept as income',cashNow-(pay-col)]], h=['S.N.','Member','Savings','Interest share','Fine share','Loan principal','Interest due','Final amount']; return'
'+st.map(x=>`
${x[0]}${rs(x[1])}
`).join('')+(cl?`
Settled${done} of ${tot}
`:'')+'
'+ tbl(cl?h.concat(['Settled']):h,R.map((r,i)=>[i+1,esc(r.name),rs(r.s),rs(r.a),rs(r.f),rs(r.lb),rs(r.di),B(r.net>=0?rs(r.net):'Owes '+rs(-r.net))].concat(cl?[r.net?``:'-']:[])), ['',B('Total'),B(rs(S('s'))),B(rs(S('a'))),B(rs(S('f'))),B(rs(S('lb'))),B(rs(S('di'))),B(rs(pay-col))].concat(cl?['']:[]))} function dissolve(){ if(cloudRole!=='owner')return '

Group settlement

Only the Admin can dissolve or reopen this group.
'; if(db.closed)return settled(); const R=settle(),pp=db.members.map(m=>pend(m).length).filter(n=>n), w=pp.length?`
${pp.length} member(s) still have pending deposits (${rs(sum(pp,n=>n)*db.set.dep)}). Pending months are not counted as savings. If they have paid, record them on the Dashboard first.
`:''; return`

Close group

Dissolving closes the books. Interest and pending deposits are frozen as of today, every member's final amount is fixed, and all records become read-only. A backup file is downloaded automatically. You can reopen the group afterwards if this was a mistake.

`+w+ `

Final settlement (preview as of ${df(today())})

`+stlTbl(R,cash(),false)+ `

Final amount = savings + share of interest and fines - loan principal - interest still due. Interest still due is taken from the borrower and shared among all members in proportion to savings, like other interest. "Owes" means the loan is bigger than the member's payout, so that member must pay the group. Amounts are rounded to whole rupees.

`+ `
`} function settled(){const c=db.closed; return`

Group closed on ${df(c.date)}

Records are read-only. Tick each member once their final amount has been paid out, or collected from them if they owe the group.

`+stlTbl(c.rows,c.cash,true)+ `
${PB}
`+ `

Delete this group permanently

Permanently deletes this group and all its records from the cloud, for every user. Download a backup first. This cannot be undone.

`} async function dissolveGroup(){if(cloudRole!=='owner')return alert('Only the Admin can dissolve this group.');if(db.closed)return; const pw=$('#d_password')?.value||'';if(!pw)return alert('Enter your account password to confirm.'); if(!cloudUser?.email)return alert('Your signed-in account email is unavailable. Please log in again.'); if(!confirm('Dissolve "'+firm.name+'"? Records become read-only and final amounts are fixed. A backup file will be downloaded first.'))return; const {error:authError}=await cloud.auth.signInWithPassword({email:cloudUser.email,password:pw});if(authError)return alert('Password confirmation failed. Please check your password and try again.'); exp();db.closed={date:today(),cash:cash(),rows:settle().map(r=>({...r,paid:false}))};await save();tab='dis';render()} function markPaid(i,v){if(!db.closed||!db.closed.rows[i])return;db.closed.rows[i].paid=!!v;save();render()} function reopen(){if(cloudRole!=='owner')return alert('Only the Admin can reopen this group.');if(!db.closed)return;if(!confirm('Reopen this group? The settlement and its settled ticks will be discarded, and records become editable again.'))return;delete db.closed;save();render()} function systemMaintenanceView(){ return '

System Maintenance

'+kv([['Group',esc(firm.name)],['Cloud storage','Supabase'],['Connection','Online connection required to save changes'],['Data deletion','Not offered as a routine maintenance action']])+ '

Backup & Restore

'+ '

Download a backup before important changes. Restoring replaces this group's current cloud data, so verify the file and confirm carefully. This page does not change the live database schema.

'; } function settings(){ const est=firm.estimatedClose||'',status=db.closed?'Closed':'Active'; return `

Group Management

`+ ``+ (est?``:``)+ `
Group name${esc(firm.name)}
System created on${df(created())}
Group start date${df(startDate())}
Status${status}${db.closed?' on '+esc(df(db.closed.date)):''}
Estimated close date${df(est)}
Estimated close dateNot set

Group Information & Rules

`+ `${cloudRole==='owner'?`
`:`

Group name: ${esc(firm.name)}

Group start date: ${esc(df(startDate()))}

Only the Admin can change the group name or start date.

`}
`+ (can('settings')?'':`

You can view group details and rules. Ask the Admin to make changes.

`); } /* Compact firm screens: keep explanatory copy available without letting it dominate the page. */ function compactFirmInfo(){ if(!firm)return; const host=$('#v');if(!host)return; host.querySelectorAll('p.note:not([data-compact-info]),p.sub:not([data-compact-info])').forEach(p=>{ const t=(p.textContent||'').trim(); if(!t)return; // Empty-state messages are useful status, not optional explanation. if(/^(nothing recorded|no pending|no .* found|no .* yet|no .* recorded|loading |you only have access)/i.test(t))return; p.dataset.compactInfo='1'; const d=document.createElement('details');d.className='compact-info'; const summary=document.createElement('summary');summary.textContent='More information'; d.appendChild(summary);d.appendChild(p.cloneNode(true));p.replaceWith(d); }); } function render(){ ixReset(); if(firm&&cloudRole!=='owner'&&(tab==='dis'||tab==='users'||tab==='audit'||tab==='maintenance'))tab='dash'; if(firm&&cloudRole==='viewer'&&(tab!=='rpt'&&tab!=='prof'&&tab!=='ginfo')){tab='rpt';if(!rt)rt='overview';} if(firm){SS.set('sb-tab',tab);if(!navSection)navSection=navSectionForTab(tab);} $('#ttl').textContent=''; $('#groupTitle').textContent=firm?(firm.name||'') : ''; $('#groupTitle').hidden=!firm; const roleBadgeText=offlineReadOnly?'Offline · Draft mode':(cloudExactRole==='super_admin'?'Super Admin':(cloudRole==='owner'?'Admin':(cloudJob||(cloudRole==='viewer'?'Member':'Manager')))); const email=(cloudUser&&cloudUser.email)||''; const username=(cloudName||((email.split('@')[0])||'Signed in')); $('#sub').innerHTML=firm?(''+esc(tabName())+''+esc(username)+''+esc(roleBadgeText)+''+(db.closed?' Closed on '+esc(df(db.closed.date))+'':'')+(offlineReadOnly?' Last synced '+esc(new Date(offlineSnapshotAt).toLocaleString())+'':'')):''; document.title=firm?(offlineReadOnly?'Offline · ':'')+tabName()+' - '+firm.name:(cloudPlatformAdmin?'Accounting Cloud · Super Admin':'Accounting Cloud'); {const nv=$('#nav'),st=nv.scrollTop;nv.innerHTML=firm?navHtml():(cloudPlatformAdmin?platformNavHtml():'');nv.scrollTop=st;pwaUpdateInstallButton()} $('#mnu').hidden=!(firm||cloudPlatformAdmin);document.body.classList.toggle('in',!!firm);document.body.classList.toggle('pa-in',!!cloudPlatformAdmin&&!firm);['dashboard','admin','firms','login','admins','firm-management','stats','activity','settings','upcoming'].forEach(k=>document.body.classList.toggle('pa-'+k+'-page',platformTab===k));if(!firm&&!cloudPlatformAdmin)menu=false;applyMenu();closeAcct(); if(tab=='earn'){tab='rpt';rt='earn'} $('#v').innerHTML=cloudPlatformAdmin&&!firm?platformDashboard():(!firm?auth():bn()+(tab=='dash'?dash():tab=='prof'?profile():tab=='ginfo'?groupInfo():tab=='rpt'?report():tab=='calc'?calculatorView():tab=='set'?settings():tab=='dis'?dissolve():tab=='exit'?exitView():tab=='users'?usersRoles():tab=='audit'?auditView():tab=='maintenance'?systemMaintenanceView():tab=='nav-placeholder'?navPlaceholderView():sheet(tab))); if(firm&&tab=='reps'&&$('#f_lid')){$('#f_lid').onchange=sug;if(!ed)sug()}if(firm&&tab==='users')loadUsersRoles();if(firm&&tab==='audit')loadAudit();if(firm&&tab==='nav-placeholder'&&navPlaceholder.startsWith('approval-'))loadApprovalRequests();sbEnhancePasswordFields();compactFirmInfo();apply()} const act=l=>bal(l)>0||intDue(l)>0; /* FIFO: a repayment goes to the member's oldest loan first, then the next */ function fifo(mid,date,prin,int){const ls=db.loans.filter(l=>l.mid==mid&&l.date<=date&&(balAt(l,date)>0||intDue(l,date)>0)).sort((a,b)=>a.dateb.date?1:a.id-b.id),out=[];let p=prin,i=int; /* interest goes to each loan only up to what it owes; any extra stays on the oldest loan, so one payment stays one entry unless several loans really owe interest */ const ins=ls.map(l=>{const x=Math.min(i,intDue(l,date));i-=x;return x});if(ins.length)ins[0]+=i; ls.forEach((l,k)=>{const ap=Math.min(p,Math.max(0,bal(l))),ai=ins[k];p-=ap;if(ap>0||ai>0)out.push({id:rid(),date,lid:l.id,prin:ap,int:ai})}); return p>0?null:out} function sug(){const m=$('#f_lid').value;$('#f_int').value=Math.round(sum(db.loans.filter(l=>l.mid==m),intDue))} async function add(t){if(ro(NEED[t])||(ed&&ed.t==t&&t!=='members'&&ro('edit_records')))return;const o={},e=ed&&ed.t==t?ed:null,old=e?db[t][e.i]:null; for(const[k,,ty]of T[t].f){const el=$('#f_'+k);o[k]=ty=='number'?(+el.value||0):el.value} if(t=='members'){if(!o.name.trim())return alert('Enter the member name.');if(db.members.some((m,i)=>m.name.trim().toLowerCase()==o.name.trim().toLowerCase()&&!(e&&i==e.i)))return alert('A member with this name already exists.');if(o.jointoday())return alert('The joining date cannot be in the future.');if(e){const joinedMonth=bsMonthFromAd(o.join);const prior=db.deposits.find(d=>String(d.mid)==String(old.id)&&(String(d.date)String(l.mid)==String(old.id)&&String(l.date)typeof v=='number'&&v<0))return alert('Amounts cannot be negative.'); if(t!='members'&&!o[t=='reps'?'lid':'mid'])return alert(t=='reps'?'There is no active loan to repay.':'Add a member first.'); if((t=='deposits'||t=='loans')&&o.amt<=0)return alert('Enter an amount above zero.'); if(t=='deposits'){const c0=startDate();if(!o.date||!o.month)return alert('Choose the date and the month.');if(o.datetoday())return alert('A deposit cannot be dated in the future. Today is '+bsLabel(today())+'.');if(o.month>bsMonthFromAd(today()))return alert('A deposit cannot be made for a future month.');const mmbr=db.members.find(m=>String(m.id)==String(o.mid));if(!mmbr)return alert('Select a valid member.');if(o.dated.mid==o.mid&&d.month==o.month&&!(e&&i==e.i)))return alert('This member already has a deposit for '+bsMonthLabel(o.month)+'. Edit that deposit instead.')} if(t=='reps'){ if(o.prin+o.int<=0)return alert('Enter the principal or interest paid.'); if(e){ const l=loanOf(o.lid),skip=old.id,maxP=balAtEx(l,o.date,skip); if(o.prin>maxP+0.01)return alert('Principal is more than the loan balance as of this repayment date.'); const maxI=intDueEx(l,o.date,skip); if(o.int>maxI+0.49)return alert('Interest paid is more than the interest due as of this repayment date.'); }else{ const memLoans=db.loans.filter(l=>String(l.mid)==String(o.lid)&&l.date<=o.date); if(!memLoans.length)return alert('No loan was given to this member on or before this date.'); const maxP=sum(memLoans,l=>balAt(l,o.date)); if(o.prin>maxP+SMALL)return alert('Principal is more than the loan balance of this member.'); const maxI=sum(memLoans,l=>intDue(l,o.date)); if(o.int>maxI+SMALL+0.49)return alert('Interest paid is more than the interest due as of this repayment date.'); } } if(t=='loans'&&old&&o.amtl.mid==o.mid&&l!==old),bal);if(o.amt>cap)return alert('This is above the loan limit of '+db.set.lim+' times savings. This member can borrow up to '+rs(Math.max(0,cap))+' more.')} if(t=='loans'||t=='reps'){const c0=startDate();if(!o.date)return alert('Choose the date.');if(o.datetoday())return alert('The date cannot be in the future. Today is '+bsLabel(today())+'.')} if(t=='loans'){const member=db.members.find(m=>String(m.id)==String(o.mid));if(!member)return alert('Select a valid member.');if(!member.join)return alert('The member joining date is missing. Correct the member record first.');if(String(o.date).slice(0,10)String(x.mid)==String(o.lid)&&x.date<=o.date).sort((a,b)=>a.dateb.date?1:a.id-b.id)[0];if(!l)return alert('No loan was given to this member on or before this date.');if(o.dater.lid==old.id&&r.datel.mid==o.lid&&l.date<=o.date),bal);if(ex>0&&ex<=SMALL)out=fifo(o.lid,o.date,o.prin-ex,o.int+ex);else ex=0} if(!out)return alert('The principal is more than the loan balance of this member.');if(!out.length)return alert('No loan was given to this member on or before this date.');const next=Object.assign({},db,{reps:db.reps.concat(out)});if(!requireValidCashTimeline(next))return;db.reps.push(...out);save();render();if(ex)alert('Rs '+ex+' extra principal was recorded as interest income of the group.');return} if(t==='loans'&&!e){o.id=Date.now();if(!requireValidCashTimeline(Object.assign({},db,{loans:db.loans.concat([o])})))return;if(cloud){if(!confirm('Submit this loan for independent approval? It will not enter the accounts until an authorized checker approves and posts it.'))return;await requestApproval('loan',{loan:o});return}} const next=Object.assign({},db);if(['deposits','loans','reps'].includes(t)){next[t]=db[t].slice();if(e)next[t][e.i]=Object.assign({},old,o);else{if(t=='loans')o.id=Date.now();next[t].push(o)}if(!requireValidCashTimeline(next))return;} if(e){db[t][e.i]=Object.assign(old,o);ed=null}else{if(t=='members'||t=='loans')o.id=o.id||Date.now();db[t].push(o)} save();render()} function delDep(i){if(ro('edit_records'))return;const d=db.deposits[i],m=db.members.find(x=>x.id==d.mid)||{name:'?'};if(!confirm('Delete the deposit of '+rs(d.amt)+' by '+m.name+' for '+bsMonthLabel(d.month)+'?'))return;const next=Object.assign({},db,{deposits:db.deposits.filter((_,j)=>j!==i)});if(!requireValidCashTimeline(next))return;db.deposits.splice(i,1);ed=null;save();render()} function delLoan(i){if(ro('edit_records'))return;const l=db.loans[i],rp=db.reps.filter(r=>r.lid==l.id),m=db.members.find(x=>x.id==l.mid)||{name:'?'}; if(rp.some(r=>r.x!=null))return alert('This loan has a repayment recorded together with a member exit. Undo the exit first.'); if(!confirm('Delete the loan of '+rs(l.amt)+' for '+m.name+(rp.length?' and its '+rp.length+' repayment(s)':'')+'?'))return; const next=Object.assign({},db,{loans:db.loans.filter((_,j)=>j!==i),reps:db.reps.filter(r=>r.lid!=l.id)});if(!requireValidCashTimeline(next))return; db.reps=next.reps;db.loans.splice(i,1);if(selL==l.id)selL=null;ed=null;save();render()} function delRep(i){if(ro('edit_records'))return;const r=db.reps[i];if(r.x!=null)return alert('This repayment was recorded together with a member exit. Undo the exit on the Member Exit page instead.');if(!confirm('Delete the repayment of '+rs((+r.prin||0)+(+r.int||0))+' dated '+bsLabel(r.date)+'?'))return;const next=Object.assign({},db,{reps:db.reps.filter((_,j)=>j!==i)});if(!requireValidCashTimeline(next))return;db.reps.splice(i,1);ed=null;save();render()} function delMember(i){if(ro('edit_records'))return;const m=db.members[i];if(db.deposits.some(d=>d.mid==m.id)||db.loans.some(l=>l.mid==m.id)||(Array.isArray(db.pay)&&db.pay.some(p=>p.mid==m.id)))return alert('This member has deposits, loans, or exit settlements recorded, so they cannot be deleted. Delete those entries first.');if(!confirm('Delete the member '+m.name+'?'))return;db.members.splice(i,1);if(rmem==m.id)rmem='';ed=null;save();render()} /* ================= CLOUD MULTI-USER ADAPTER ================= Keeps the existing accounting/UI engine, but moves the authoritative group data to Supabase. Local storage is used only for configuration/cache/session. Never place a service_role key in this file. */ const CLOUD_CFG_KEY='sb-cloud-config-v1'; const DEFAULT_CLOUD_URL='https://sjzjslxnmpyshyugbmow.supabase.co'; const DEFAULT_CLOUD_KEY='sb_publishable_U_cykf8FYl6CmBeY6LjvpA_Nh-qqxeZ'; let cloud=null, cloudUser=null, cloudRevision=0, cloudSaving=false, cloudConfiguredFlag=false, cloudRole='', cloudExactRole='', cloudPlatformAdmin=false, cloudGroupInfo=null, cloudPerms=null, cloudJob='', cloudName=''; function cloudCfg(){ try{ const saved=JSON.parse(localStorage.getItem(CLOUD_CFG_KEY)||'null'); if(saved&&saved.url&&saved.key)return saved; }catch(e){} return {url:DEFAULT_CLOUD_URL,key:DEFAULT_CLOUD_KEY}; } function cloudConfigured(){const c=cloudCfg();return !!(c&&c.url&&c.key&&window.supabase)} function initCloud(){if(cloud&&cloudConfiguredFlag)return true;const c=cloudCfg();if(!c||!window.supabase)return false;try{cloud=window.supabase.createClient(c.url,c.key,{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:true}});cloudConfiguredFlag=true;return true}catch(e){cloud=null;cloudConfiguredFlag=false;return false}} function saveCloudConfig(){const url=$('#c_url').value.trim().replace(/\/$/,''),key=$('#c_key').value.trim();if(!/^https:\/\/[^\s]+\.supabase\.co$/i.test(url)||key.length<20)return alert('Enter a valid Supabase project URL and publishable/anon key.');localStorage.setItem(CLOUD_CFG_KEY,JSON.stringify({url,key}));initCloud();render()} async function cloudSession(){if(!cloud&& !initCloud())return null;const {data}=await cloud.auth.getSession();return data&&data.session?data.session.user:null} let cloudGroups=[]; async function cloudListGroups(){const {data,error}=await cloud.rpc('sb2_list_my_firms');if(error)return alert('Could not load your groups: '+error.message);cloudGroups=data||[];if(!cloudGroups.length){tab='dash';firm=null;db=fresh();authTab='in';render();return}await cloudOpenGroup(cloudGroups[0].id)} function cloudShowGroupCreator(){authTab='newgroup';render()} async function createIndependentAdmin(){ if(!cloudPlatformAdmin)return alert('Only the platform Super Admin can create an independent Admin.'); const name=($('#ia_name')?.value||'').trim(),address=($('#ia_address')?.value||'').trim(),phone=($('#ia_phone')?.value||'').trim(),username=($('#ia_username')?.value||'').trim(),email=($('#ia_email')?.value||'').trim().toLowerCase(),temp_password=$('#ia_password')?.value||''; if(!name)return alert('Enter the Admin full name.'); if(!address)return alert('Enter the Admin address.'); if(address.length>500||phone.length>40)return alert('Address or phone number is too long.'); if(!/^[A-Za-z][A-Za-z0-9._-]{2,31}$/.test(username))return alert('Username must be 3–32 characters, start with a letter, and contain only letters, numbers, dots, underscores, or hyphens.'); if(email&&!/^\S+@\S+\.\S+$/.test(email))return alert('Enter a valid email address or leave it blank.'); if(temp_password.length<8)return alert('Temporary password must contain at least 8 characters.'); const btn=$('#ia_create');if(btn){btn.disabled=true;btn.textContent='Creating…'} try{ const {data:out,error}=await cloud.functions.invoke('manage-user',{body:{action:'create_independent_admin',name,address,phone,email:email||null,username,temp_password}}); if(error)throw new Error(await fnMsg(error,'Could not reach the user service.')); if(!out?.success)throw new Error(out?.error||'The Admin account could not be created.'); lastCreds={name,email:email||username,pw:out.temp_password};render(); alert('Independent Admin account created. Share the username and temporary password securely. The Admin can create a firm later.'); }catch(e){alert(e.message||'The Admin account could not be created.')} finally{const b=$('#ia_create');if(b){b.disabled=false;b.textContent='Create independent Admin'}} } async function cloudCreateGroup(){ if(!initCloud())return alert('Cloud client is not ready. Reload the page and try again.'); const name=($('#cg_name')?.value||'').trim(),start=($('#cg_start')?.value||''),dep=$('#cg_dep')?.value,rate=$('#cg_rate')?.value,address=($('#cg_address')?.value||'').trim(),authorized_person=($('#cg_authorized')?.value||'').trim(),description=($('#cg_desc')?.value||'').trim(),closeDate=$('#cg_close')?.value||'',limit=$('#cg_lim')?.value||'0'; if(!name||!start||dep===''||rate==='')return alert('Group name, group start date, monthly deposit, and loan interest are required.'); if(start>today())return alert('Group start date cannot be in the future.');if(closeDate&&closeDateObject.fromEntries(PERMS.map(([k])=>[k,l.includes(k)])); function permsOf(r){if(r.role==='admin'||r.role==='super_admin'||r.role==='owner')return PERMS.map(x=>x[0]);const p=r.permissions;return p?PERMS.map(x=>x[0]).filter(k=>p[k]===true):(r.role==='viewer'?[]:DEF_EDITOR)} const PERM_GROUPS=[['People',['members','exits']],['Money entries',['deposits','loans','repayments']],['Records & rules',['edit_records','settings']]]; const PERM_DESC={members:'Register new members and edit their details',exits:'Record a member leaving and settle their payout',deposits:'Enter monthly savings, single or bulk',loans:'Give new loans and edit loan details',repayments:'Record principal and interest repayments',edit_records:'Edit or delete past deposits, loans and repayments',settings:'Change monthly deposit, interest rate, loan limit and group details'}; const PERM_RISK=['edit_records','settings']; function permMatch(list){const a=[...list].sort().join(',');const q=POSITIONS.find(x=>[...x[1]].sort().join(',')===a);return q?q[0]:null} function permStatus(pre){const on=PERMS.map(x=>x[0]).filter(k=>$('#'+pre+k)?.checked),el=$('#'+pre+'pst');if(!el)return;const m=permMatch(on);el.textContent=m?'Matches the '+m+' preset':(on.length?'Custom powers ('+on.length+' of '+PERMS.length+')':'No powers: view only');el.className='pst'+(m?'':' cus')} function permAll(pre,on){PERMS.forEach(([k])=>{const c=$('#'+pre+k);if(c)c.checked=on});permStatus(pre)} function permBoxes(pre,sel){ const lab=Object.fromEntries(PERMS),m=permMatch(sel); const groups=PERM_GROUPS.map(([g,ks])=>'
'+g+'
'+ks.map(k=>'').join('')+'
').join(''); return '
'+(m?'Matches the '+m+' preset':(sel.length?'Custom powers ('+sel.length+' of '+PERMS.length+')':'No powers: view only'))+'
'+groups+'

User management, the audit log, backup/restore and closing the group are always Admin-only.

'} const readPerms=pre=>Object.fromEntries(PERMS.map(([k])=>[k,!!$('#'+pre+k)?.checked])); function posChange(pre){ const sel=$('#'+pre+'pos'),v=(sel?.value||'').trim().toLowerCase(),q=POSITIONS.find(x=>x[0].toLowerCase()===v); if(q)PERMS.forEach(([k])=>{const c=$('#'+pre+k);if(c)c.checked=q[1].includes(k)});permStatus(pre); } const posSelect=(pre,sel)=>{ const known=['Admin','Manager','Accountant']; const isCustom=sel&&!known.includes(sel); const sa=cloudExactRole==='super_admin'; return ``; }; let userEdit=null,lastCreds=null,userMode='manage'; const dtf=v=>{try{return v?new Date(v).toLocaleDateString():'Never'}catch(e){return ''}}; async function fnMsg(err,fallback){try{const j=await err?.context?.json?.();if(j&&j.error)return j.error}catch(e){}return fallback||err?.message||'Something went wrong.'} function usersRoles(){ if(cloudRole!=='owner')return '

User Management

Only an Admin or Super Admin can manage users and roles.
'; const rows=cloudUsersCache,eu=userEdit&&rows.find(r=>String(r.user_id)===String(userEdit)); const creds=lastCreds?``:''; const toggle=`
`; const create=`

Create User

Create a login for a person who needs access to this group. Phone number and email are optional and may help with account recovery when recovery support is configured.

* Required fields

Choose a temporary password of at least 8 characters and give it securely to the user. They will be asked to set their own password at first login.

Permissions
${permBoxes('ru_',[])}

Admin and Super Admin roles cannot be granted here by a regular Admin.

`; const edit=eu?`

Edit user access

Update the person’s display name, role preset, and individual permissions for this group.

Current account${esc(eu.email||'No email available')}
${eu.active===false?'Disabled':'Active'} · Last login: ${esc(dtf(eu.last_sign_in_at))}
Permissions for this group

Selecting Manager or Accountant loads a preset. You can then fine-tune individual permissions.

${permBoxes('eu_',permsOf(eu))}
`:''; const chips=r=>{if(r.role==='owner'||r.role==='admin')return 'Admin · Full Access';if(r.role==='viewer'&&permsOf(r).length===0)return 'Reports Only';const l=PERMS.filter(x=>permsOf(r).includes(x[0])).map(x=>x[1]);return l.length?l.map(x=>''+esc(x)+'').join(''):'Reports Only'}; const body=rows.length?rows.map(r=>{const own=r.role==='owner'||r.role==='admin'||r.role==='super_admin',on=r.active!==false,id=esc(r.user_id||''); const posDisplay=own?'Admin (Full Access)':(r.job_title==='Manager'?'Manager':(r.job_title==='Accountant'?'Accountant':(r.job_title==='Member'?(permsOf(r).length?'Member · Custom permissions':'Member · Reports only'):(r.role==='viewer'?'Member · Reports only':(r.job_title||r.role||'User'))))); return ''+esc(r.display_name||'Unnamed user')+'
'+esc(r.email||'')+''+esc(posDisplay)+''+chips(r)+''+(on?'Active':'Disabled')+'
Last login: '+esc(dtf(r.last_sign_in_at))+'
'+(own?'':` `)+'
'}).join(''):'No users found for this group. If you expected users here, reload the list and check the database function.'; const addExisting=`

Add Existing User to This Group

Use the same login email. This adds group access to an existing account; it must not create a duplicate login.

Permissions
${permBoxes('rx_',[])}
`; const content=userMode==='create'?create+addExisting:`${edit}
${body}
PersonRolePermissionsStatusActions

Delete permanently removes the selected person’s membership from this group only. Their login account and other group memberships are not deleted.

`; return '

User Management

'+creds+toggle+content; } async function loadUsersRoles(){ if(cloudRole!=='owner'||!cloud)return; const {data,error}=await cloud.rpc('sb2_list_firm_members',{p_firm_id:firm.id}); if(error){cloudUsersCache=[];return}cloudUsersCache=(data||[]).map(r=>({...r,job_title:r.role==='manager'?'Manager':(r.role==='accountant'?'Accountant':(r.role==='viewer'?'Member':'')),display_name:r.display_name||'',active:r.active!==false})); if(tab==='users'&&firm&&userMode!=='create'&&!userEdit){const y=window.scrollY;$('#v').innerHTML=bn()+usersRoles();sbEnhancePasswordFields();apply();scrollTo(0,y)} } async function createUserAccount(){ const btn=document.querySelector('.role-admin .p'); try{ if(cloudRole!=='owner')return alert('Only an Admin or Super Admin can create users.'); const name=$('#ru_name').value.trim(),address=$('#ru_address').value.trim(),phone=$('#ru_phone').value.trim(),email=$('#ru_email').value.trim().toLowerCase(),username=$('#ru_username').value.trim(),job=$('#ru_pos').value.trim(),temp_password=$('#ru_temp_password').value; if(!name)return alert('Enter the person\'s full name.'); if(!address)return alert('Enter the user address.'); if(!username||/\s/.test(username))return alert('Enter a username without spaces.'); if(email&&!/^\S+@\S+\.\S+$/.test(email))return alert('Enter a valid email address or leave it blank.'); if(!job)return alert('Please select a role.'); if(job==='Super Admin')return alert('There can be only one Super Admin.');if(job==='Admin'&&cloudExactRole!=='super_admin')return alert('Only the platform Super Admin can create an Admin.'); if(!temp_password||temp_password.length<8)return alert('Enter a temporary password of at least 8 characters.'); const permissions=readPerms('ru_'); if(!Object.values(permissions).some(Boolean)&&!confirm('No powers are ticked, so this person can only view. Continue?'))return; if(btn){btn.disabled=true;btn.textContent='Creating…'} const {data:out,error:fe}=await cloud.functions.invoke('manage-user',{body:{action:'create',group_id:firm.id,name,address,phone,email:email||null,username,temp_password,job_title:job,role:(job==='Admin'?'admin':/manager/i.test(job)?'manager':'accountant'),permissions}}); if(fe)throw new Error(await fnMsg(fe,'Could not reach the user service.')); if(!out?.success)throw new Error(out?.error||'The user could not be created.'); lastCreds={name,email:email||username,pw:out.temp_password}; userMode='manage';await loadUsersRoles();render();scrollTo(0,0); }catch(e){alert(e.message||'The user could not be created.')} finally{if(btn){btn.disabled=false;btn.textContent='Create user'}} } async function addExistingUserToGroup(){ if(cloudRole!=='owner')return alert('Only an Admin or Super Admin can add users.'); const email=($('#ru_existing_email')?.value||'').trim().toLowerCase(),job=$('#rx_pos')?.value||''; if(!/^\S+@\S+\.\S+$/.test(email))return alert('Enter the existing account email.'); if(!job)return alert('Select a role.'); if(job==='Super Admin')return alert('There can be only one Super Admin.');if(job==='Admin'&&cloudExactRole!=='super_admin')return alert('Only the platform Super Admin can assign Admin.'); const permissions=readPerms('rx_'); try{ const {data:out,error}=await cloud.functions.invoke('manage-user',{body:{action:'add_existing',group_id:firm.id,email,job_title:job,role:job==='Admin'?'admin':job==='Manager'?'manager':'accountant',permissions}}); if(error)throw new Error(await fnMsg(error,'Could not reach the user service.')); if(!out?.success)throw new Error(out?.error||'Could not add the existing account.'); alert('Existing account added to this group. They should sign in with their existing email and password.'); await loadUsersRoles();render(); }catch(e){alert('Could not add existing user. '+(e.message||'The manage-user service must support the add_existing action.'))} } async function resetUserPassword(uid){ const r=cloudUsersCache.find(x=>String(x.user_id)===String(uid));if(!r)return; if(!confirm('Make a new temporary password for '+(r.display_name||r.email)+'? Their old password will stop working.'))return; try{const {data:out,error:fe}=await cloud.functions.invoke('manage-user',{body:{action:'reset',group_id:firm.id,user_id:uid}}); if(fe)throw new Error(await fnMsg(fe));if(!out?.success)throw new Error(out?.error||'Could not reset the password.'); lastCreds={name:r.display_name,email:r.email,pw:out.temp_password};render();scrollTo(0,0)}catch(e){alert(e.message)} } async function grantAccess(r,over){ const perms=over.perms||mkPerms(permsOf(r)); const job=String(over.job!==undefined?over.job:(r.job_title||r.role||'viewer')).toLowerCase(); const role=job==='admin'?'admin':job.includes('manager')?'manager':'accountant'; if((role==='admin'||role==='super_admin')&&cloudExactRole!=='super_admin') { alert('Only the platform Super Admin can grant the Admin role.'); return false; } const {error}=await cloud.rpc('sb2_set_firm_member',{p_firm_id:firm.id,p_user_id:r.user_id,p_role:role,p_active:over.active!==undefined?over.active:r.active!==false,p_display_name:over.name!==undefined?over.name:(r.display_name||''),p_permissions:perms}); if(error){alert(error.message);return false}return true} async function saveUserEdit(){ const r=cloudUsersCache.find(x=>String(x.user_id)===String(userEdit));if(!r)return; const job=$('#eu_pos').value.trim(),name=$('#eu_name').value.trim();if(!name||!job)return alert('Enter the name and position.'); if(await grantAccess(r,{perms:readPerms('eu_'),job,name})){userEdit=null;await loadUsersRoles();render()} } async function toggleUser(uid,on){ const r=cloudUsersCache.find(x=>String(x.user_id)===String(uid));if(!r)return; if(!on&&!confirm('Disable '+(r.display_name||r.email)+'? They will not be able to open or change this group until you enable them again.'))return; if(await grantAccess(r,{active:on})){await loadUsersRoles();render()} } async function removeUser(uid){ if(!uid||cloudRole!=='owner')return; const r=cloudUsersCache.find(x=>String(x.user_id)===String(uid));if(!r)return; if(r.role==='admin'||r.role==='super_admin'||r.user_id===cloudUser?.id)return alert('For safety, this action cannot delete an Admin, Super Admin, or your own account.'); const label=r.display_name||r.email||uid; if(!confirm('PERMANENTLY DELETE '+label+' from this group?\n\nTheir membership and group access will be removed, not merely disabled. If they belong to other groups, those memberships must remain intact. This cannot be undone.'))return; try{ const {data:out,error}=await cloud.rpc('sb2_delete_firm_member',{p_firm_id:firm.id,p_user_id:uid}); if(error)throw error; if(out===false||(out&&out.success===false))throw new Error(out?.error||'Server did not confirm removal.'); userEdit=null;await loadUsersRoles();render();alert('User membership permanently removed from this group. Their access to other groups, if any, is unchanged.'); }catch(e){alert('User was not deleted. '+(e.message||'Install the secure sb2_delete_firm_member SQL function first.'))} } let cloudAuditCache=null; async function loadAudit(){ if(cloudRole!=='owner'||!cloud)return; const {data,error}=await cloud.rpc('sb2_list_audit',{p_firm_id:firm.id,p_limit:200}); cloudAuditCache=error?{err:error.message}:(data||[]); if(tab==='audit'&&firm){$('#v').innerHTML=bn()+auditView();apply()} } const AUD={members:'Members',deposits:'Deposits',loans:'Loans',reps:'Repayments',pay:'Exit settlements'}; const audNm=id=>{const m=id==null?null:memMap().get(String(id));return m?esc(m.name):'(removed member)'}; function auditItem(c,x){x=x||{};if(c==='members')return esc(x.name||'');if(c==='deposits')return audNm(x.mid)+' '+rs(x.amt)+' '+mf(x.month||'');if(c==='loans')return audNm(x.mid)+' '+rs(x.amt);if(c==='reps'){const l=loanMap().get(String(x.lid));return (l?audNm(l.mid):'(removed loan)')+' '+rs((+x.prin||0)+(+x.int||0))}return audNm(x.mid)+' '+rs((+x.prin||0)+(+x.int||0))} function auditText(sm){ sm=sm||{};const o=[]; for(const c of Object.keys(AUD)){ const v=sm[c];if(!v)continue; let addedItems=Array.isArray(v.added_items)?v.added_items.slice():[]; let removedItems=Array.isArray(v.removed_items)?v.removed_items.slice():[]; let added=Number(v.added)||0, removed=Number(v.removed)||0; // A member record appearing in both sides with the same ID, and only gaining a left date, // is an exit-status update rather than a true delete-and-add operation. if(c==='members'){ const exited=[]; for(let i=addedItems.length-1;i>=0;i--){ const a=addedItems[i];if(!a||!a.id||!(a.left&&a.left.date))continue; const j=removedItems.findIndex(r=>r&&String(r.id)===String(a.id)&&!(r.left&&r.left.date)); if(j>=0){exited.push(a);addedItems.splice(i,1);removedItems.splice(j,1);added=Math.max(0,added-1);removed=Math.max(0,removed-1)} } exited.forEach(x=>o.push('Member exited: '+esc(x.name||'Unknown member')+(x.left&&x.left.date?' ('+df(x.left.date)+')':''))); } const it=a=>(a&&a.length)?' ('+a.slice(0,5).map(x=>auditItem(c,x)).join('; ')+(a.length>5?'; …':'')+')':''; if(added)o.push(AUD[c]+': '+added+' added'+it(addedItems)); if(removed)o.push(AUD[c]+': '+removed+' removed or changed'+it(removedItems)); } if(sm.member_exit_changes&&!o.some(x=>x.startsWith('Member exited:')))o.push('Member exit status changed'); if(sm.rules)o.push('Group rules changed');if(sm.closed)o.push('Group closure status changed');if(sm.firm_info)o.push('Group details changed'); return o.length?o.join('
'):'Saved' } function auditRoleLabel(role){ const labels={super_admin:'Super Admin',admin:'Admin',manager:'Manager',accountant:'Accountant',viewer:'Viewer',owner:'Admin'}; const key=String(role||'').trim().toLowerCase(); return labels[key]||''; } function auditView(){ if(cloudRole!=='owner')return '

Audit & Activity History

Only the Admin can view the activity log.
'; const a=cloudAuditCache;if(a===null)return '

Audit & Activity History

Loading…

'; if(a.err)return '

Audit & Activity History

'+esc(a.err)+'

If the error mentions a missing function, run ROLES-1-MIGRATION.sql in Supabase.

'; return '

Audit & Activity History

Who changed what, newest first. Recorded by the server, so it cannot be edited from the app. The last 400 days are kept.

'+tbl(['When','Who','What changed'],a.map(r=>{const role=auditRoleLabel(r.summary&&r.summary.actor_role);return [esc(new Date(r.logged_at).toLocaleString()),esc(r.actor_name||r.actor_email||'Unknown')+(r.actor_name&&r.actor_email?'
'+esc(r.actor_email)+'':'')+(role?'
'+esc(role)+'':''),auditText(r.summary)]}))} function mustChangePw(){return !!(cloudUser&&cloudUser.user_metadata&&cloudUser.user_metadata.must_change_password)} function forcePwView(){return `
${LANG==='ne'?'समूह बचत':'Samuha Bachat'}

Choose your own password

Enter the temporary password you used to sign in, then choose a new password of at least 8 characters.

`} async function forcePwSave(){const cur=$('#fp_current').value,p=$('#fp1').value,p2=$('#fp2').value;if(!cur)return alert('Enter your current / temporary password.');if(p.length<8)return alert('Use a password of at least 8 characters.');if(p!==p2)return alert('The two passwords do not match.');if(p===cur)return alert('Choose a new password different from the temporary password.'); if(!cloudUser||!cloudUser.email)return alert('Cloud is not connected.');const chk=await cloud.auth.signInWithPassword({email:cloudUser.email,password:cur});if(chk.error)return alert('Current password is incorrect.'); const {data,error}=await cloud.auth.updateUser({password:p,current_password:cur,data:{must_change_password:false}});if(error)return alert('Password could not be changed: '+error.message); cloudUser=data.user||cloudUser;if(cloudUser.user_metadata)cloudUser.user_metadata.must_change_password=false;await cloudEnter()} async function cloudForgotPassword(){ if(!initCloud())return render(); const e=($('#a_e')?.value||'').trim().toLowerCase(); if(!e)return alert('Enter your account email first.'); if(!e.includes('@'))return alert('Password recovery needs your email address. Type your email (not your username) in the login box, or ask the Admin to reset your password.'); const redirectTo=location.origin+location.pathname+'?reset-password=1'; const {error}=await cloud.auth.resetPasswordForEmail(e,{redirectTo}); if(error)return alert('Password recovery could not be requested: '+error.message); alert('If this email belongs to an account, Supabase will send a secure password-reset link. Check Inbox and Spam.'); } async function finishPasswordRecovery(){ if(!cloud)return alert('Cloud is not connected.'); const p=$('#rp_new')?.value||'',p2=$('#rp_confirm')?.value||''; if(p.length<8)return alert('Use a password of at least 8 characters.'); if(p!==p2)return alert('The two passwords do not match.'); const {error}=await cloud.auth.updateUser({password:p}); if(error)return alert('Password could not be updated: '+error.message); history.replaceState({},document.title,location.pathname); alert('Password updated. Please log in with your new password.'); await cloud.auth.signOut();cloudUser=null;cloudGroups=[];firm=null;render(); } function cloudAuthState(){return !!cloudUser} function remGet(){try{return{on:localStorage.getItem('sb-rem')!=='0',email:localStorage.getItem('sb-rem-email')||''}}catch(e){return{on:true,email:''}}} function remSave(on,email){try{localStorage.setItem('sb-rem',on?'1':'0');if(on)localStorage.setItem('sb-rem-email',email);else localStorage.removeItem('sb-rem-email');if(!on)sessionStorage.setItem('sb-tab-alive','1')}catch(e){}} function remExpired(){try{return localStorage.getItem('sb-rem')==='0'&&!sessionStorage.getItem('sb-tab-alive')}catch(e){return false}} function authFocus(){const a=document.getElementById('a_e'),pw=document.getElementById('a_p');if(a&&pw&&a.value&&!(document.activeElement&&document.activeElement.tagName==='INPUT'))pw.focus()} function togglePassword(id,btn){const el=document.getElementById(id);if(!el)return;const show=el.type==='password';el.type=show?'text':'password';btn.innerHTML=sbEyeSvg(show);const en=show?'Hide password':'Show password';const ne=show?'पासवर्ड लुकाउनुहोस्':'पासवर्ड देखाउनुहोस्';btn.setAttribute('aria-label',LANG==='ne'?ne:en);btn.title=LANG==='ne'?ne:en;} let booting=true,cloudUnsaved=false,cloudWaiters=[],sstT=null; function sst(st){const b=$('#sst');if(!b)return;clearTimeout(sstT);const ne=LANG==='ne'; if(st==='saving'){b.hidden=false;b.disabled=true;b.textContent=ne?'सुरक्षित गर्दै…':'Saving…'} else if(st==='err'){b.hidden=false;b.disabled=false;b.textContent=ne?'सुरक्षित भएन · पुनः प्रयास':'Not saved · Retry'} else if(st==='ok'){b.hidden=false;b.disabled=true;b.textContent=ne?'सुरक्षित भयो':'Saved';sstT=setTimeout(()=>{b.hidden=true},1600)} else b.hidden=true} /* Saves are queued and coalesced: a save requested while another is in flight is never dropped, it goes next with the latest data. */ function cloudSave(){if(offlineReadOnly||navigator.onLine===false)return Promise.resolve(false);return new Promise(res=>{if(!firm||!cloud)return res(false);ixReset();cloudWaiters.push(res);if(!cloudSaving)cloudPump()})} async function cloudPump(){cloudSaving=true;sst('saving'); try{while(cloudWaiters.length){const ws=cloudWaiters;cloudWaiters=[];let r='err';try{r=await cloudSaveOnce()}catch(e){r='err'} if(r==='conflict'){const rest=cloudWaiters;cloudWaiters=[];ws.concat(rest).forEach(w=>w(false));cloudUnsaved=false;sst('');break} ws.forEach(w=>w(r==='ok')); if(r==='ok'){cloudUnsaved=false;if(!cloudWaiters.length)sst('ok')} else{cloudUnsaved=true;const rest=cloudWaiters;cloudWaiters=[];rest.forEach(w=>w(false));sst('err');break}}} finally{cloudSaving=false}} function validateLoanJoinDates(data=db){ const members=new Map((data.members||[]).map(m=>[String(m.id),m])); for(const loan of (data.loans||[])){ const member=members.get(String(loan.mid)); if(!member||!member.join){alert('The member joining date is missing. Correct the member record first.');return false;} const loanDate=String(loan.date||'').slice(0,10),joinDate=String(member.join||'').slice(0,10); if(!loanDate||!joinDate||loanDate{if(!quiet)alert(m);return false}; if(!id)return fail('The firm record did not include an ID. Check sb2_list_my_firms / sb2_load_firm return values.'); let res;try{res=await cloud.rpc('sb2_load_firm',{p_firm_id:id})}catch(e){return fail('Could not load group: '+(e?.message||e))} const {data,error}=res;if(error)return fail('Could not load group: '+error.message); const row=Array.isArray(data)?data[0]:data;if(!row)return fail('Supabase returned no firm row for '+id+'.'); let payload=row.payload; if(typeof payload==='string'){try{payload=JSON.parse(payload)}catch(e){return fail('Firm payload is not valid JSON. Existing records were not changed.')}} if(!payload||typeof payload!=='object')return fail('Firm payload is missing or invalid. Existing records were not changed.'); const firmData=(payload.firm&&typeof payload.firm==='object')?payload.firm:{id,name:row.name||'Samuha Bachat',created:today(),startDate:today()}; const dbData=(payload.db&&typeof payload.db==='object')?payload.db:payload; offlineReadOnly=false;offlineSnapshotAt=0;cloudRevision=Number(row.revision)||0;cloudExactRole=row.role||'';cloudRole=(row.role==='admin'||row.role==='super_admin')?'owner':(row.role==='viewer'&&!(row.permissions&&Object.values(row.permissions).some(v=>v===true))?'viewer':'editor');cloudPerms=row.permissions||null;cloudJob=row.role==='manager'?'Manager':(row.role==='accountant'?'Accountant':(row.role==='viewer'?'Viewer':''));cloudName=row.display_name||'';firm=Object.assign({},firmData,{id,name:row.name||firmData.name||'Samuha Bachat'}); db=Object.assign(fresh(),dbData);db.set=Object.assign(fresh().set,db.set||{}); for(const k of['members','deposits','loans','reps','pay'])if(!Array.isArray(db[k]))db[k]=[]; migrateMonths();ixReset();cloudGroupInfo={name:firm.name,role:cloudRole};ed=null;selL=null; if(!keep){tab=(cloudRole==='viewer'?'rpt':'dash');if(cloudRole==='viewer')rt='overview';resetUi()} SS.set('sb-sess',id);try{localStorage.setItem('sb-last-group',id);if(cloudUser?.id)localStorage.setItem('sb-last-group:'+cloudUser.id,id)}catch(e){} await offlineCacheFirm();await pwaApplyPendingMembers();render();pwaDraftRefreshCount();pwaDraftRefreshList();return true} async function cloudRefreshGroups(){try{const {data}=await cloud.rpc('sb2_list_my_firms');cloudGroups=data||[];if(firm){const nv=$('#nav'),t=nv.scrollTop;nv.innerHTML=navHtml();nv.scrollTop=t;apply()}}catch(e){}} /* fast path: open the last used group directly (one request) and list groups in the background */ async function cloudEnter(){ offlineReadOnly=false;offlineSnapshotAt=0; let last='';try{last=(cloudUser?.id&&localStorage.getItem('sb-last-group:'+cloudUser.id))||localStorage.getItem('sb-last-group')||''}catch(e){}if(!last)last=SS.get('sb-sess')||''; if(navigator.onLine===false){if(last&&await offlineRestoreFirm(last))return;firm=null;db=fresh();cloudPlatformAdmin=false;render();alert('No recent offline copy is available for this account. Connect to the internet to open Accounting Cloud.');return} try{const {data,error}=await cloud.rpc('sb2_is_platform_admin',{p_user_id:cloudUser?.id});cloudPlatformAdmin=!error&&data===true}catch(e){cloudPlatformAdmin=false} if(cloudPlatformAdmin){ // Restore the Super Admin's previously opened firm after refresh. if(last&&await cloudOpenGroup(last,true)){cloudRefreshGroups();await pwaDraftSyncAll();return;} firm=null;db=fresh();tab='dash';authTab='in';SS.set('sb-sess');cloudGroups=[];render(); cloudRefreshPlatformGroups(); return; } if(last&&await cloudOpenGroup(last,true)){cloudRefreshGroups();await pwaDraftSyncAll();return} if(navigator.onLine===false&&last&&await offlineRestoreFirm(last))return; await cloudListGroups()} async function cloudLogin(){if(!initCloud())return render();const e=($('#a_e').value||'').trim(),p=$('#a_p').value,rem=!!($('#a_rem')&&$('#a_rem').checked);if(!e||!p)return alert('Enter Login Id and password.'); const b=document.querySelector('.auth-main-btn');if(b){b.disabled=true;b.textContent='Logging in…'} try{let data;if(e.includes('@')){const result=await cloud.auth.signInWithPassword({email:e,password:p});if(result.error)return alert(result.error.message);data=result.data;}else{const {data:loginResult,error:loginError}=await cloud.functions.invoke('sb2-login',{body:{username:e,password:p}});if(loginError||!loginResult?.success||!loginResult?.session)return alert(loginResult?.error||'Invalid username or password.');const {data:sessionData,error:sessionError}=await cloud.auth.setSession({access_token:loginResult.session.access_token,refresh_token:loginResult.session.refresh_token});if(sessionError)return alert(sessionError.message);data=sessionData;}remSave(rem,e);cloudUser=data.user;if(mustChangePw()){render();return}await cloudEnter()} finally{const b2=document.querySelector('.auth-main-btn');if(b2){b2.disabled=false;b2.textContent='Log in';apply()}}} let cloudLogoutBusy=false; async function cloudLogout(){ if(cloudLogoutBusy)return; if(cloudUnsaved&&!confirm('You have changes that are not saved. Log out anyway?'))return; const offlineUid=cloudUser?.id; let pendingDrafts=[]; try{if(offlineUid)pendingDrafts=await offlineDraftList(offlineUid)||[]} catch(e){console.warn('Could not inspect offline drafts before logout',e);if(!confirm('Offline drafts could not be checked. Log out anyway?'))return} if(pendingDrafts.length&&!confirm('There are '+pendingDrafts.length+' offline draft(s) on this device. Logging out will clear this account’s encrypted local drafts. Continue?'))return; cloudLogoutBusy=true; const buttons=Array.from(document.querySelectorAll('#nav .nav-acts .lo')); buttons.forEach(b=>{b.disabled=true;b.dataset.logoutLabel=b.textContent;b.textContent='Logging out…'}); try{ if(!cloud)throw new Error('The secure cloud connection is not ready. Please refresh and try again.'); const {error}=await cloud.auth.signOut({scope:'local'}); if(error)throw error; if(offlineUid){try{await offlineClearUser(offlineUid)}catch(e){console.warn('Offline draft cleanup failed after successful sign-out',e)}} cloudUser=null;cloudPlatformAdmin=false;cloudExactRole='';cloudRole='';cloudGroupInfo=null;cloudPerms=null;cloudJob='';cloudName='';offlineReadOnly=false;offlineSnapshotAt=0;cloudGroups=[];firm=null;db=fresh();menu=false;cloudUnsaved=false;cloudWaiters=[];sst(''); try{localStorage.removeItem('sb-last-group');if(offlineUid)localStorage.removeItem('sb-last-group:'+offlineUid);Object.keys(localStorage).filter(k=>k.startsWith('sb-cache-')).forEach(k=>localStorage.removeItem(k))}catch(e){} SS.set('sb-sess');SS.set('sb-tab');ixReset();render();pwaDraftRefreshCount(); }catch(e){ const message=(e&&e.message)||String(e); alert('Logout failed: '+message+' Your current session has not been cleared. Please try again.'); }finally{ cloudLogoutBusy=false; document.querySelectorAll('#nav .nav-acts .lo').forEach(b=>{b.disabled=false;b.textContent=b.dataset.logoutLabel||'Log out';delete b.dataset.logoutLabel}); } } function logout(){return cloudLogout()} async function cloudBoot(){ try{if(!initCloud())return;cloudUser=await cloudSession(); if(new URLSearchParams(location.search).get('reset-password')==='1'){firm=null;db=fresh();return} if(cloudUser&&remExpired()){const expiredUid=cloudUser.id;try{await cloud.auth.signOut({scope:'local'})}catch(e){}await offlineClearUser(expiredUid);cloudUser=null} if(cloudUser&&!mustChangePw())await cloudEnter()} catch(e){console.warn('Boot failed',e)} finally{booting=false;if(!firm)render()}} function exp(){const payload={version:2,exportedAt:new Date().toISOString(),firm,db};const u=URL.createObjectURL(new Blob([JSON.stringify(payload,null,2)],{type:'application/json'})),a=document.createElement('a');a.href=u;a.download='samuha-bachat-'+(firm?.name||'backup').replace(/[\\/:*?"<>|\s]+/g,'-')+'-'+today()+'.json';document.body.appendChild(a);a.click();a.remove();if(firm){db.lastBackup=today();cloudSave()}setTimeout(()=>URL.revokeObjectURL(u),1000)} const RE_D=/^\d{4}-\d{2}-\d{2}$/; function backupOk(d){try{if(!d||![d.members,d.deposits,d.loans,d.reps,d.pay].every(Array.isArray))return false;const mids=new Set(),lids=new Set(); for(const m of d.members){if(m.id==null||!m.name||!RE_D.test(String(m.join))||mids.has(String(m.id)))return false;mids.add(String(m.id))} for(const x of d.deposits)if(x.mid==null||!mids.has(String(x.mid))||!RE_D.test(String(x.date))||!/^\d{4}-\d{2}$/.test(String(x.month))||!Number.isFinite(+x.amt)||!Number.isFinite(+x.fine))return false; for(const l of d.loans){if(l.id==null||!mids.has(String(l.mid))||!RE_D.test(String(l.date))||!Number.isFinite(+l.amt)||!Number.isFinite(+l.rate)||lids.has(String(l.id)))return false;lids.add(String(l.id))} for(const r of d.reps)if(!lids.has(String(r.lid))||!RE_D.test(String(r.date))||!Number.isFinite(+r.prin)||!Number.isFinite(+r.int))return false; for(const p of d.pay)if(!mids.has(String(p.mid))||!RE_D.test(String(p.date))||!Number.isFinite(+p.prin)||!Number.isFinite(+p.int))return false; return true}catch(e){return false}} function imp(i){const f=i.files[0];if(!f)return;if(cloudRole!=='owner'){i.value='';return alert('Only the Admin can restore a backup.')} const r=new FileReader();r.onload=async()=>{try{const d=JSON.parse(r.result);const p=d&&d.db?d:{db:d}; if(!backupOk(p.db))return alert('This file is not a valid backup.'); if(!confirm('Replace this group\'s cloud data with the backup? This cannot be undone.'))return; const prevDb=db,prevFirm=Object.assign({},firm),gid=firm.id; db=Object.assign(fresh(),p.db);db.set=Object.assign(fresh().set,db.set); if(p.firm&&typeof p.firm==='object'){const {id,...rest}=p.firm;Object.assign(firm,rest);firm.id=gid} migrateMonths(); const ok=await cloudSave(); if(!ok&&!(await cloudOpenGroup(gid,true,true))){db=prevDb;firm=prevFirm;cloudUnsaved=false;sst('')} render()}catch(e){alert('This file is not a valid backup or could not be read.')}finally{i.value=''}};r.readAsText(f)} async function setR(){if(offlineReadOnly||navigator.onLine===false)return alert('Group rules can only be changed online.');if(!can('settings')){alert('You do not have permission to change group rules.');render();return}if(db.closed)return;db.set.dep=Math.max(0,+$('#s_dep').value||0);db.set.rate=Math.max(0,+$('#s_rate').value||0);db.set.lim=Math.max(0,+$('#s_lim').value||0);await cloudSave();render()} async function saveFirmInfo(){if(offlineReadOnly||navigator.onLine===false)return alert('Group details can only be changed online.');if(db.closed)return;if(!can('settings'))return alert('You do not have permission to change group details.');const before=Object.assign({},firm),isOwner=cloudRole==='owner';const n=isOwner?($('#f_name')?.value||firm.name).trim():firm.name;const st=isOwner?(($('#f_start')?.value)||firm.startDate):firm.startDate;if(isOwner&&!n)return alert('Enter the group name.');if(isOwner&&(!st||st>today()))return alert('Group start date cannot be in the future.');if(isOwner){firm.name=n;firm.startDate=st}firm.estimatedClose=$('#f_close')?.value||'';firm.description=$('#f_desc')?.value.trim()||'';const gid=firm.id,ok=await cloudSave();if(!ok){firm=before;await cloudOpenGroup(gid,true,true);render();return}render()} async function changePassword(){const p=$('#pw_new').value,p2=$('#pw_new2').value;if(!p)return alert('Enter the new password.');if(p.length<8)return alert('Use a password of at least 8 characters.');if(p!==p2)return alert('The two new passwords do not match.');if(!cloud)return alert('Cloud is not connected.');const {error}=await cloud.auth.updateUser({password:p});if(error)return alert(error.message);$('#pw_new').value='';$('#pw_new2').value='';alert('Password changed successfully.')} async function removeGroup(){ if(cloudRole!=='owner'||!firm||!cloud)return alert('Only the group Admin or Super Admin can permanently delete this group.'); if(!db.closed)return alert('Close and settle the group before permanent deletion.'); const password=$('#r_password')?.value||''; if(!password)return alert('Enter your account password to confirm permanent deletion.'); if(!cloudUser?.email)return alert('Your signed-in account email is unavailable. Please log in again.'); if(!confirm('Permanently delete this group and all its cloud records, including audit history? This cannot be undone. Download a backup first.'))return; const {error:authError}=await cloud.auth.signInWithPassword({email:cloudUser.email,password}); if(authError)return alert('Password confirmation failed. Please check your password and try again.'); const gid=firm.id,gname=firm.name; try{ const {data,error}=await cloud.rpc('sb2_delete_firm',{p_firm_id:gid}); if(error)throw error; if(data===false||(data&&data.success===false))throw new Error(data?.error||'Server did not confirm deletion.'); try{localStorage.removeItem('sb-last-group');if(cloudUser?.id)localStorage.removeItem('sb-last-group:'+cloudUser.id)}catch(e){}SS.set('sb-sess');SS.set('sb-tab'); firm=null;db=fresh();cloudGroups=(cloudGroups||[]).filter(x=>x.id!==gid);tab='dash'; alert('Group '+gname+' and all associated cloud records were permanently deleted.'); if(cloudPlatformAdmin){render();cloudRefreshPlatformOverview();cloudRefreshPlatformGroups();}else await cloudListGroups(); }catch(e){alert('Group was not permanently deleted. '+(e?.message||'The secure sb2_delete_firm database function may not be installed yet.'))} } window.addEventListener('beforeunload',e=>{if(cloudSaving||cloudUnsaved){e.preventDefault();e.returnValue=''}}); window.addEventListener('online',async()=>{pwaNetworkStatus();if(!firm?.id||!cloudUser?.id)return;const id=firm.id;if(offlineReadOnly){if(!await cloudOpenGroup(id,true,true))return}else if(cloudUnsaved){if(!await cloudSave())return}else if(!await cloudOpenGroup(id,true,true))return;pwaNetworkStatus();await pwaDraftRefreshList();await pwaDraftRefreshCount();await pwaDraftSyncAll()}); window.addEventListener('offline',pwaNetworkStatus); /* cloud login / group UI */ /* ================= PLATFORM SUPER ADMIN DASHBOARD ================= */ let platformTab='dashboard',platformOverview=null,platformOverviewLoading=false,platformOverviewError=''; async function cloudRefreshPlatformOverview(){ if(!cloudPlatformAdmin||!cloudUser?.id||platformOverviewLoading)return; platformOverviewLoading=true;platformOverviewError=''; try{const {data,error}=await cloud.functions.invoke('manage-user',{body:{action:'platform_overview'}});if(error)throw new Error(await fnMsg(error,'Could not load platform records.'));if(!data?.success)throw new Error(data?.error||'Could not load platform records.');platformOverview=data;} catch(e){platformOverviewError=e?.message||'Could not load platform records.';console.error('Platform overview failed:',e)} finally{platformOverviewLoading=false;if(cloudPlatformAdmin&&!firm)render()} } function platformDate(v){try{return v?new Date(v).toLocaleString():'—'}catch(e){return '—'}} function platformFirmRows(){ const all=platformOverview?.firms||[];if(!platformOverview)return '

'+(platformOverviewLoading?'Loading platform firms…':esc(platformOverviewError||'Platform data has not loaded.'))+'

';if(!all.length)return '

No firms are registered yet.

'; return '
'+all.map(g=>{const access=(cloudGroups||[]).some(x=>x.id===g.id);return ''}).join('')+'
FirmStatusCreatedRevisionAccess
'+esc(g.name)+'
'+esc(g.id)+'
'+esc(g.status)+''+esc(platformDate(g.created_at))+''+esc(g.revision)+''+(access?'':'No membership')+'
'; } async function platformResetAdminPassword(userId){ const a=(platformOverview?.admins||[]).find(x=>x.user_id===userId);if(!a||userId===cloudUser?.id)return; if(!confirm('Create a new temporary password for '+a.name+'? Their current password will stop working.'))return; try{const {data,error}=await cloud.functions.invoke('manage-user',{body:{action:'reset',user_id:userId}});if(error)throw new Error(await fnMsg(error,'Could not reset this password.'));if(!data?.success)throw new Error(data?.error||'Could not reset this password.');lastCreds={name:a.name,email:a.contact_email||a.username||data.user?.email||'Admin account',pw:data.temp_password};render()}catch(e){alert(e?.message||'Could not reset this password.')} } document.addEventListener('click',function(event){const b=event.target.closest('[data-platform-reset]');if(b)platformResetAdminPassword(b.dataset.platformReset)}); function platformAdminRows(){ const all=platformOverview?.admins||[];if(!platformOverview)return '

'+(platformOverviewLoading?'Loading administrator directory…':esc(platformOverviewError||'Platform data has not loaded.'))+'

';if(!all.length)return '

No administrator accounts found.

'; return '
'+all.map(a=>'').join('')+'
AdministratorUsernameAccount typeFirm assignmentsActions
'+esc(a.name)+'
'+esc(a.contact_email||'')+'
'+esc(a.username||'—')+''+esc(a.account_type)+''+esc((a.firm_assignments||[]).map(x=>x.firm_name+(x.active?'':' (inactive)')).join(', ')||'Independent')+'
'+(lastCreds?'':''); } function platformActivityRows(){ const all=platformOverview?.activity||[];if(!platformOverview)return '

'+(platformOverviewLoading?'Loading recent activity…':esc(platformOverviewError||'Platform data has not loaded.'))+'

';if(!all.length)return '

No audit events have been recorded yet.

'; return '
'+all.map(a=>'').join('')+'
TimeFirmActorEventTarget
'+esc(platformDate(a.occurred_at))+''+esc(a.firm_name)+''+esc(a.actor_name)+''+esc(a.event_type)+''+esc((a.target_type||'')+(a.target_id?' · '+a.target_id:''))+'
'; } function platformDashboard(){ const who=esc(cloudName||cloudUser?.email||'Platform administrator'); const firms=Array.isArray(cloudGroups)?cloudGroups:[]; const totals=platformOverview?.stats||null; const firmList=firms.length?'
'+firms.map(g=>'').join('')+'
':'

No firm memberships are assigned to your Super Admin account.

'; return `
ACCOUNTING CLOUD · PLATFORM

Welcome, Super Admin!

Manage the Accounting Cloud platform — create admins, set up firms, and access your organizations.

▦
Registered firms${totals?totals.total_firms:"—"}${totals?totals.active_firms+" active":"Loading platform totals"}
♙
Administrators${totals?totals.total_admins:"—"}Independent and firm Admins
✓
Platform users${totals?totals.total_users:"—"}Unique registered accounts
★
Recent audit events${totals?totals.total_activity:"—"}${who}

Platform Overview

Your platform access at a glance

Account active
▥
${firms.length}Firm workspaces available
✓
ProtectedPrivileged actions verified

Quick Actions

Common platform tasks

Platform management

Core administration actions

+

Create independent Admin

Create an Admin account without assigning it to a firm. The Admin can manage a firm after a formal assignment.

* Required fields

Admin access is assigned automatically. The Admin will create or receive a firm through the platform's firm-management process.

${lastCreds?``:''}

Firm management

Create a firm or open a firm you already administer.

+

Create a new firm

The Super Admin who creates the firm automatically becomes its single Admin.

* Required fields

Your firm workspaces

${firmList}

Log in to Firm

Open a firm workspace assigned to your account.

${firmList}

Stays

Reserved platform area for future stay and subscription management.

No stay records or booking workflows are connected yet.

Admin Management

Platform-wide administrator directory. Authentication credentials are never displayed here.

${platformAdminRows()}

Firm Management

Platform-wide firm directory with status and workspace access.

${platformFirmRows()}

Platform Stats

Live totals returned by the authorized platform overview.

Total firms${totals?totals.total_firms:"—"}
Active firms${totals?totals.active_firms:"—"}
Closed firms${totals?totals.closed_firms:"—"}
Administrators${totals?totals.total_admins:"—"}
Unique users${totals?totals.total_users:"—"}
Recent audit records${totals?totals.total_activity:"—"}

Platform Activity Log

Latest audit events across firms.

${platformActivityRows()}

Platform Settings

Account and platform configuration.

⚙

Super Admin account

Signed in as ${who}. Use the account menu for profile details and password changes.

Global platform settings are not available yet.
Platform-wide branding, defaults and security policies should be added with server-side authorization and audit logging.
`; } function platformNavHtml(){ const items=[['pa-dashboard','Dashboard'],['pa-admin','Create Admin'],['pa-firms','Create Firm'],['pa-admins','Admin Management'],['pa-firm-management','Firm Management'],['pa-stats','Platform Stats'],['pa-activity','Activity Log'],['pa-settings','Settings'],['pa-upcoming','Stays']]; return navBrand('Accounting Cloud')+''+navFoot(); } async function cloudRefreshPlatformGroups(){ try{const {data,error}=await cloud.rpc('sb2_list_my_firms');if(error)throw error;cloudGroups=data||[];render();cloudRefreshPlatformOverview()} catch(e){alert('Could not refresh firm access: '+(e.message||e))} } function auth(){const h=_auth();setTimeout(authFocus,60);return h} function _auth(){ const brand=`
${LANG==='ne'?'समूह बचत':'Samuha Bachat'}
`; if(booting&&new URLSearchParams(location.search).get('reset-password')!=='1')return `
${brand}

Loading…

`; if(!window.supabase)return `
${brand}
Could not load the cloud library
Check your internet connection, then reload the app.
`; if(!cloudConfigured())return `
${LANG==='ne'?'समूह बचत':'Samuha Bachat'}
${LANG==='ne'?'तपाईंको समूहको मासिक बचत, ऋण र भुक्तानी':'Monthly savings, loans and repayments for your group'}

Accounting Cloud

Cloud setup required
Connect this app to your Supabase project. Never use a service_role key in this HTML.

Your accounting records are stored in Supabase. The browser only keeps the login session, cloud configuration and a small cache.

`; const recovery = new URLSearchParams(location.search).get('reset-password') === '1'; if(!recovery&&mustChangePw())return forcePwView(); const noFirm = !!cloudUser && !firm; return `
${LANG==='ne'?'समूह बचत':'Samuha Bachat'}
${LANG==='ne'?'तपाईंको समूहको मासिक बचत, ऋण र भुक्तानी':'Monthly savings, loans and repayments for your group'}

Accounting Cloud

`+ (recovery?`
Choose a new password to finish account setup or recover your existing account. Your group access will not be changed.
`: noFirm?(cloudPlatformAdmin?platformDashboard():authTab==='newgroup'?`
Create your first group
This will work only if the secure Supabase setup permission has been enabled for this authorized first Admin.

* Required fields

`:`
No group access is assigned to this account.
If this is the first Admin account, you can try firm setup below. Existing groups will appear only when this account has membership access.
`): `

Only authorized app operators should use this app. Ordinary financial members do not need app accounts.

`)+ `

Financial records remain in the Supabase cloud. This app requires an internet connection to save transactions.

` } /* Use the existing renderer and accounting functions. Only persistence/auth are replaced. */ (function(){cloudBoot()})(); function go(){return true} render();apply();go(); if('serviceWorker' in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('./sw.js').catch(e=>console.warn('PWA service worker unavailable',e)));} /* Password visibility: apply an eye toggle to every password field, including dynamically rendered forms. */ function sbEyeSvg(open){return open?``:``} function sbEnhancePasswordFields(root=document){root.querySelectorAll('.pw-wrap input[type="password"]').forEach(input=>{const btn=input.parentElement.querySelector('.pw-toggle');if(btn&&!btn.dataset.sbEye){btn.dataset.sbEye='1';btn.innerHTML=sbEyeSvg(false);btn.setAttribute('aria-label','Show password');btn.title='Show password';}});root.querySelectorAll('input[type="password"]').forEach(input=>{if(input.dataset.sbEye==='1'||input.closest('.pw-wrap'))return;input.dataset.sbEye='1';const wrap=document.createElement('span');wrap.className='pw-wrap';input.parentNode.insertBefore(wrap,input);wrap.appendChild(input);const btn=document.createElement('button');btn.type='button';btn.className='pw-toggle';btn.setAttribute('aria-label','Show password');btn.title='Show password';btn.innerHTML=sbEyeSvg(false);btn.addEventListener('click',()=>{const visible=input.type==='password';input.type=visible?'text':'password';btn.innerHTML=sbEyeSvg(visible);btn.setAttribute('aria-label',visible?'Hide password':'Show password');btn.title=visible?'Hide password':'Show password';});wrap.appendChild(btn);});} if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>sbEnhancePasswordFields());else sbEnhancePasswordFields(); /* ===== Account (profile circle in sidebar) ===== */ (function(){const add={"My profile":"मेरो प्रोफाइल","Change password":"पासवर्ड परिवर्तन","Manage users":"प्रयोगकर्ता व्यवस्थापन","Role":"भूमिका","Group":"समूह","Last login":"अन्तिम लगइन","Account created":"खाता बनेको मिति","Current password":"हालको पासवर्ड","New password":"नयाँ पासवर्ड","Confirm new password":"नयाँ पासवर्ड पुष्टि गर्नुहोस्","Show passwords":"पासवर्ड देखाउनुहोस्","Cancel":"रद्द गर्नुहोस्","Close":"बन्द गर्नुहोस्","Update password":"पासवर्ड अपडेट गर्नुहोस्","Use at least 8 characters.":"कम्तीमा ८ अक्षर प्रयोग गर्नुहोस्।","Password changed successfully.":"पासवर्ड सफलतापूर्वक परिवर्तन भयो।","Enter your current password.":"हालको पासवर्ड राख्नुहोस्।","The two new passwords do not match.":"दुई नयाँ पासवर्ड मिलेन।","Current password is incorrect.":"हालको पासवर्ड मिलेन।","New password must be different from the current one.":"नयाँ पासवर्ड हालको भन्दा फरक हुनुपर्छ।","Updating…":"अपडेट हुँदै…","Switch group":"समूह परिवर्तन","Choose a group to open.":"खोल्नुपर्ने समूह छान्नुहोस्।","Current":"हालको","Create new group":"नयाँ समूह बनाउनुहोस्","Create group":"समूह बनाउनुहोस्","Group name":"समूह नाम","Group profile":"समूह प्रोफाइल","Group start date":"समूह सुरु मिति","Status":"स्थिति","Active":"सक्रिय","Closed":"बन्द","Enter the group name.":"समूहको नाम राख्नुहोस्।","You only have access to this group.":"तपाईंसँग यही समूहको मात्र पहुँच छ।","Opening…":"खुल्दै छ…","Creating…":"बनाउँदै…","Admin":"प्रशासक","Manager":"प्रबन्धक","Accountant":"लेखापाल","Member":"सदस्य"};for(const k in add)if(!(k in UI_NE))UI_NE[k]=add[k]})(); const acT=t=>{if(LANG!=='ne')return t;const v=neLookup(UI_NE,t);return v!==undefined?v:t}; function acctWho(){const email=(cloudUser&&cloudUser.email)||'';const role=cloudPlatformAdmin&&!firm?'Super Admin':(cloudExactRole==='super_admin'?'Super Admin':(cloudRole==='owner'?'Admin':(cloudJob||(cloudRole==='viewer'?'Member':'Manager'))));return{email,name:cloudName||email||'Signed in',role}} const acIc=p=>``; function ensureAcm(){let m=document.getElementById('acm');if(!m){m=document.createElement('div');m.id='acm';m.setAttribute('role','menu');document.body.appendChild(m)}return m} function toggleAcct(e){ if(e)e.stopPropagation(); const m=ensureAcm(),b=$('#avb');if(!b)return; if(m.classList.contains('open')){closeAcct();return} const w=acctWho(),ne=LANG==='ne',many=(cloudGroups&&cloudGroups.length>1); m.innerHTML=`
${esc(w.name)}${w.email&&w.email!==w.name?`${esc(w.email)}`:''}${esc(w.role)}
`+ ``+ ``; const r=b.getBoundingClientRect(),wd=Math.min(300,innerWidth-24); m.style.width=wd+'px';m.style.left=Math.max(12,Math.min(r.left,innerWidth-wd-12))+'px';m.style.top=(r.bottom+8)+'px'; m.classList.add('open');b.setAttribute('aria-expanded','true');apply(); setTimeout(()=>{const f=m.querySelector('.mi');if(f)f.focus()},30); } function closeAcct(){const m=document.getElementById('acm'),b=$('#avb');if(m)m.classList.remove('open');if(b)b.setAttribute('aria-expanded','false')} function acctClose(){closeAcct();if(innerWidth<1100&&menu)toggleMenu(false)} function acctGo(t){acctClose();closeDlg();tab=t;navSection=navSectionForTab(t);render();scrollTo(0,0)} document.addEventListener('click',e=>{if(!e.target.closest('#acm,#avb'))closeAcct()}); window.addEventListener('resize',closeAcct); document.addEventListener('keydown',e=>{if(e.key==='Escape'){const d=$('#dlg');if(d&&!d.hidden)closeDlg();else closeAcct()}}); function openDlg(html){let d=$('#dlg');if(!d){d=document.createElement('div');d.id='dlg';d.setAttribute('role','dialog');d.setAttribute('aria-modal','true');d.addEventListener('mousedown',e=>{if(e.target===d)closeDlg()});document.body.appendChild(d)}d.innerHTML='
'+html+'
';d.hidden=false;apply();setTimeout(()=>{const f=d.querySelector('input:not([hidden]),button');if(f)f.focus()},40)} function closeDlg(){const d=$('#dlg');if(d){d.hidden=true;d.innerHTML=''}} /* Firm-level calculator: keypad-only controls with delegated tap handling. */ let calcValue='0',calcStored=null,calcOperator=null,calcFresh=false; function calculatorView(){ const keys=[['C','clear'],['±','sign'],['%','percent'],['÷','divide'],['7','digit'],['8','digit'],['9','digit'],['×','multiply'],['4','digit'],['5','digit'],['6','digit'],['−','subtract'],['1','digit'],['2','digit'],['3','digit'],['+','add'],['0','digit'],['.','decimal'],['⌫','back'],['=','equals']]; return '

Calculator

Quick calculations · no keyboard required

'+esc(calcDisplayText())+'
'+keys.map(([label,action])=>'').join('')+'
'; } function calcPress(action,label){ const n=Number(calcValue); const isOperator=['add','subtract','multiply','divide'].includes(action); if(action==='clear'){calcValue='0';calcStored=null;calcOperator=null;calcFresh=false} else if(action==='digit'){ if(calcFresh||calcValue==='0'||calcValue==='Error'){calcValue=label;calcFresh=false} else if(calcValue.replace('-','').replace('.','').length<15)calcValue+=label; } else if(action==='decimal'){ if(calcFresh||calcValue==='Error'){calcValue='0.';calcFresh=false} else if(!calcValue.includes('.'))calcValue+='.'; } else if(action==='sign'){ if(Number.isFinite(n)&&n!==0)calcValue=String(-n); } else if(action==='percent'){ if(Number.isFinite(n))calcValue=String(n/100); } else if(action==='back'){ if(calcFresh||calcValue==='Error')calcValue='0'; else {calcValue=calcValue.length>1?calcValue.slice(0,-1):'0';if(calcValue==='-')calcValue='0'} } else if(isOperator){ if(calcOperator!==null&&calcStored!==null&&!calcFresh){ const result=calcCompute(calcStored,n,calcOperator); calcValue=calcFormat(result); if(calcValue==='Error'){calcStored=null;calcOperator=null;calcFresh=true;calcUpdateDisplay();return} } calcStored=Number(calcValue);calcOperator=action;calcFresh=true; } else if(action==='equals'&&calcOperator!==null&&calcStored!==null&&!calcFresh){ calcValue=calcFormat(calcCompute(calcStored,n,calcOperator)); calcStored=null;calcOperator=null;calcFresh=true; } calcUpdateDisplay(); } function calcCompute(a,b,op){ if(op==='add')return a+b; if(op==='subtract')return a-b; if(op==='multiply')return a*b; if(op==='divide')return b===0?NaN:a/b; return b; } function calcFormat(value){ return Number.isFinite(value)?String(Number(value.toPrecision(12))):'Error'; } function calcDisplayText(){ if(calcOperator!==null&&calcStored!==null){ const symbols={add:'+',subtract:'−',multiply:'×',divide:'÷'}; return String(calcStored)+' '+symbols[calcOperator]+' '+(calcFresh?'':calcValue); } return calcValue; } function calcUpdateDisplay(){ const d=document.querySelector('.calc-display');if(d)d.textContent=calcDisplayText(); } function openProfileHub(){ acctClose();const w=acctWho(),u=cloudUser||{}; openDlg(`

My Profile

${esc(w.name)}
${esc(w.email)}
`+ `
Changing your email may require verification from your new email address.
`+ `
`); } async function saveProfileDetails(){ const name=$('#profile_name')?.value.trim()||'',address=$('#profile_address')?.value.trim()||'',phone=$('#profile_phone')?.value.trim()||'',email=$('#profile_email')?.value.trim()||'',err=$('#profile_err');if(!err)return; if(!name){err.textContent='Enter your full name.';return} if(email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){err.textContent='Enter a valid email address or leave it blank.';return} try{if(!cloud)throw new Error('Cloud is not connected.');const metadata={...(cloudUser?.user_metadata||{}),full_name:name,name,address,phone};const changes={data:metadata};const emailChanged=!!email&&email!==cloudUser?.email;if(emailChanged)changes.email=email;const {data,error}=await cloud.auth.updateUser(changes);if(error)throw error;cloudUser=data.user||cloudUser;cloudName=name;closeDlg();render();alert(emailChanged?'Profile details saved. Check your new email to verify the address.':'Profile details updated.')}catch(e){err.textContent=e.message||String(e)} } function openGroupHub(){ acctClose();const cur=firm?firm.name:'No group selected'; openDlg(`

Group Profile

Current Group${esc(cur)}
Status${firm?(db.closed?'Closed':'Active'):'—'}
`+ `
`); } function openProfileDlg(){ acctClose();const w=acctWho(),u=cloudUser||{},gp=cloudRole==='viewer'?'ginfo':'prof'; openDlg(`
${esc(w.name)}
${esc(w.email)}
`+ `
Role${esc(w.role)}
Last login${esc(dtf(u.last_sign_in_at))}
Account created${esc(dtf(u.created_at))}
`+ (firm?`

Group

Group name${esc(firm.name)}
Group start date${esc(df(startDate()))}
Status${db.closed?'Closed':'Active'}
`:'')+ `
${firm?``:''}
`); } function openPwDlg(){ acctClose(); openDlg(`

Change password

Use at least 8 characters.

`+ `
`+ ``+ ``+ ``+ ``+ ``+ `
`); } async function submitPwDlg(){ const cur=$('#dp_cur').value,p=$('#dp_new').value,p2=$('#dp_new2').value,err=$('#dp_err'),go=$('#dp_go'); const bad=m=>{err.textContent=acT(m)}; err.textContent=''; if(!cur)return bad('Enter your current password.'); if(p.length<8)return bad('Use at least 8 characters.'); if(p!==p2)return bad('The two new passwords do not match.'); if(p===cur)return bad('New password must be different from the current one.'); if(!cloud||!cloudUser)return bad('Cloud is not connected.'); go.disabled=true;go.textContent=acT('Updating…'); try{ const chk=await cloud.auth.signInWithPassword({email:cloudUser.email,password:cur}); if(chk.error){bad('Current password is incorrect.');return} const {data,error}=await cloud.auth.updateUser({password:p,current_password:cur}); if(error){err.textContent=error.message;return} if(data&&data.user)cloudUser=data.user; closeDlg();alert(acT('Password changed successfully.')); }catch(e){err.textContent=e.message||String(e)} finally{go.disabled=false;go.textContent=acT('Update password')} } /* ----- Switch group / create group ----- */ const acRole=r=>({admin:'Admin',super_admin:'Admin',manager:'Manager',accountant:'Accountant',viewer:'Member'}[r]||''); function fillSwitchList(){ const box=$('#sg_list');if(!box)return; const gs=(cloudGroups&&cloudGroups.length)?cloudGroups:(firm?[{id:firm.id,name:firm.name}]:[]); box.innerHTML=gs.map(g=>{const cur=firm&&String(g.id)===String(firm.id),rl=acRole(g.role); return ``}).join('')+ (gs.length<2&&cloudRole!=='owner'?'

You only have access to this group.

':''); apply(); } function openSwitchDlg(){ acctClose(); openDlg(`

Switch group

Choose a group to open.

`+ (cloudRole==='owner'?``:'')+ `
`); fillSwitchList(); (async()=>{try{if(!cloud)return;const {data}=await cloud.rpc('sb2_list_my_firms');if(Array.isArray(data)&&data.length){cloudGroups=data;fillSwitchList()}}catch(e){}})(); } async function switchGroupDlg(id){ if(!id||!firm)return; if(String(id)===String(firm.id)){closeDlg();return} const box=$('#sg_list');if(box)box.querySelectorAll('button').forEach(b=>b.disabled=true); try{await cloudSwitch(id)}catch(e){alert(e.message||String(e))} if(firm&&String(firm.id)===String(id))closeDlg();else fillSwitchList(); } async function createGroupDlg(){ const inp=$('#sg_name'),err=$('#sg_err'),btn=$('#sg_go');if(!inp)return; const name=(inp.value||'').trim(),start=$('#sg_start')?.value||'',dep=$('#sg_dep')?.value,rate=$('#sg_rate')?.value,address=($('#sg_address')?.value||'').trim(),authorized_person=($('#sg_authorized')?.value||'').trim(),description=($('#sg_desc')?.value||'').trim(),closeDate=$('#sg_close')?.value||'',limit=$('#sg_lim')?.value||'0';err.textContent=''; if(!name||!start||dep===''||rate===''){err.textContent='Group name, group start date, monthly deposit, and loan interest are required.';return} if(start>today()||(closeDate&&closeDate'+label+''; } async function pwaInstall(){ if(pwaIsInstalled()){pwaUpdateInstallButton();return} if(!pwaInstallPrompt){ const ios=/iphone|ipad|ipod/i.test(navigator.userAgent); alert(ios?'To install Accounting Cloud: tap Share in Safari, then choose “Add to Home Screen”.':/android/i.test(navigator.userAgent)?'To install Accounting Cloud: open the browser menu (⋮), then choose “Install app” or “Add to Home screen”.':'To install Accounting Cloud: use the Install icon in your browser address bar, or open the browser menu and choose “Install app”.'); return; } const promptEvent=pwaInstallPrompt;pwaInstallPrompt=null; try{await promptEvent.prompt();const choice=await promptEvent.userChoice;if(choice&&choice.outcome==='accepted'){try{localStorage.setItem('accountingCloudInstalled','1')}catch(e){}pwaToast('Accounting Cloud installation started.');}} catch(e){console.warn('PWA installation prompt failed',e)} pwaUpdateInstallButton(); } function pwaToast(message){const el=document.getElementById('pwaNetworkStatus');if(!el)return;el.classList.remove('offline');el.textContent=message;el.hidden=false;window.setTimeout(()=>{if(navigator.onLine!==false&&!offlineReadOnly)el.hidden=true;},3500)} function pwaNetworkStatus(){ const el=document.getElementById('pwaNetworkStatus');if(!el)return; if(navigator.onLine===false){el.classList.add('offline');el.textContent=offlineReadOnly?'Offline · add members, deposits, loans and repayments as drafts':'Offline · eligible entries are saved on this device';el.hidden=false;return} el.classList.remove('offline'); if(offlineReadOnly){el.textContent='Internet available · reconnecting to live firm records…';el.hidden=false;return} if(cloudUnsaved){el.textContent='Some changes are not saved to the cloud · retry saving';el.hidden=false;return} el.hidden=true; } window.addEventListener('beforeinstallprompt',event=>{event.preventDefault();pwaInstallPrompt=event;pwaUpdateInstallButton();}); window.addEventListener('appinstalled',()=>{pwaInstallPrompt=null;try{localStorage.setItem('accountingCloudInstalled','1')}catch(e){}pwaUpdateInstallButton();pwaToast('Accounting Cloud is installed on this device.');}); window.addEventListener('online',pwaNetworkStatus); window.addEventListener('offline',pwaNetworkStatus); window.addEventListener('load',()=>{pwaUpdateInstallButton();pwaNetworkStatus();}); if(window.matchMedia){const standaloneQuery=window.matchMedia('(display-mode: standalone)');if(standaloneQuery.addEventListener)standaloneQuery.addEventListener('change',pwaUpdateInstallButton);} window.addEventListener('load',()=>{pwaDraftRefreshCount();});