+ New folder
⬆ Upload files
📂 Upload folder
📊 Application Stages Sheet
🧾 New invoice
✉ New letter
📝 New LH Doc
🧾 IELTS Invoice
♻ Restore Empty bin
Copy
Cut
Paste
⬇ Download
Rename
Delete
Name Date Size Type
☰ List
Invoice system ✕ Close ';
$('#ed').appendChild(f);f.style.cssText='flex:1;width:100%;border:0;background:#e9edf2';
$('#invClose').onclick=()=>{$('#ed').classList.remove('show');$('#ed').innerHTML='';ed=null};
ed=null;$('#ed').classList.add('show')}
/* ===== Application Stages Sheet (embedded spreadsheet system) ===== */
function openSheets(){
const f=document.createElement('iframe');f.id='sheetsFrame';f.title='Application Stages Sheet';f.allowFullscreen=true;f.allow='clipboard-read; clipboard-write; fullscreen';
f.srcdoc=document.getElementById('sheetsTpl').textContent.replace(/<\\\/script/g,'📊 Application Stages Sheet ✕ Close ';
$('#ed').appendChild(f);f.style.cssText='flex:1;width:100%;border:0;background:#fff';
$('#sheetsClose').onclick=()=>{$('#ed').classList.remove('show');$('#ed').innerHTML='';ed=null};
ed=null;$('#ed').classList.add('show')}
/* ===== LH Doc system (embedded letterhead editor) ===== */
function openLH(){
const f=document.createElement('iframe');f.id='lhFrame';f.title='LH Doc system';
f.srcdoc=document.getElementById('lhTpl').textContent.replace(/<\\\/script/g,'LH Doc system ✕ Close ';
$('#ed').appendChild(f);f.style.cssText='flex:1;width:100%;border:0;background:#d7dbe8';
$('#lhClose').onclick=()=>{$('#ed').classList.remove('show');$('#ed').innerHTML='';ed=null};
ed=null;$('#ed').classList.add('show')}
/* ===== IELTS Invoice system (embedded) ===== */
function openIELTS(){
const f=document.createElement('iframe');f.id='ieltsFrame';f.title='IELTS Invoice';
f.srcdoc=document.getElementById('ieltsTpl').textContent.replace(/<\\\/script/g,'IELTS Invoice ✕ Close ';
$('#ed').appendChild(f);f.style.cssText='flex:1;width:100%;border:0;background:#e9edf2';
$('#ieltsClose').onclick=()=>{$('#ed').classList.remove('show');$('#ed').innerHTML='';ed=null};
ed=null;$('#ed').classList.add('show')}
/* invoices saved before the switch: read-only view + print */
function openLegacyInvoice(node){
ed={type:'invoice',node,data:node.data,parent:node.parent,dirty:false};
$('#ed').innerHTML='Saved invoice (old format) – '+esc(node.name)+' 🖨 Print / PDF ✕ Close
'+invoicePaper(node.data)+'
';
$('#lgPrint').onclick=()=>printHTML(invoicePaper(node.data));
$('#lgClose').onclick=()=>{$('#ed').classList.remove('show');$('#ed').innerHTML='';ed=null};
showEd()}
function openLetter(node){const data=node?JSON.parse(JSON.stringify(node.data)):{ref:'',date:new Date().toLocaleDateString('en-GB',{day:'2-digit',month:'long',year:'numeric'}),to:'Mr. / Ms. ________ Company name Address',subject:'Subject: ________',body:'Dear Sir / Madam,
Type your letter here…
Yours faithfully,
',sign:' ______________________Name Designation '+esc(S.company)};
ed={type:'letter',node,data,parent:node?node.parent:(view==='files'?cwd:'root'),dirty:false};
const cmd=(c,l,t='')=>`${l} `;
$('#ed').innerHTML=edTop(node?'Edit letter':'New letter on letterhead')+`${cmd('bold','B ')}${cmd('italic','I ')}${cmd('underline','U ')}${cmd('insertUnorderedList','• List')}${cmd('insertOrderedList','1. List')}${cmd('justifyLeft','⇤')}${cmd('justifyCenter','↔')}${cmd('justifyRight','⇥')}${cmd('undo','↶')}${cmd('redo','↷')}Click anywhere on the page to type. Yellow areas are editable.
${letterPaper(data,true)}
`;
$('#ed').querySelector('.edbody')?.remove();bindTop();showEd();
$('#ed').querySelector('.rt').addEventListener('click',e=>{const b=e.target.closest('[data-cmd]');if(b){document.execCommand(b.dataset.cmd);ed.dirty=true;syncLetter()}});
$('#prev').addEventListener('input',()=>{ed.dirty=true;syncLetter()})}
function syncLetter(){document.querySelectorAll('#prev [data-f]').forEach(el=>ed.data[el.dataset.f]=el.innerHTML)}
/* ================= start ================= */
(async function init(){
try{mode=localStorage.getItem('ach_mode')||'grid'}catch(e){}
if(!window.indexedDB){document.body.innerHTML='This browser does not support local storage (IndexedDB). Please use Chrome, Edge, Firefox or Safari.
';return}
db=await openDB();
(await rq(st('nodes').getAll())).forEach(n=>N.set(n.id,n));
const saved=await kvGet('S');S={...DEF,...(saved||{})};
if(!saved){await kvSet('S',S);for(const f of['Invoices','Receipts','Letters','Contracts','HR & Staff','Bank & Accounts','Customers & Suppliers'])await mk('root',f)}
try{navigator.storage&&navigator.storage.persist&&navigator.storage.persist()}catch(e){}
render();
})();
/* ===== Office Resources module (portals vault, contacts, notes, folders) ===== */
const RV={'r-dash':'Resources Dashboard','r-portals':'Portal Login Credentials','r-contacts':'Contact Directory','r-notes':'Office Notes & Documents'};
const isR=()=>view.startsWith('r-');
const ICONS={university:'🎓',documents:'📄',website:'🌐',password:'🔑',business:'💼',contacts:'👥',finance:'💰',travel:'✈️',general:'📁'};
const COLORS=['#14399a','#0e9f6e','#d97706','#dc2626','#7c3aed','#0891b2','#db2777','#475569'];
let R={portals:[],contacts:[],notes:[],folders:[],meta:null,audit:[]},rKey=null,rLock=null,rFilter='all',rTab='';
const rSave=()=>kvSet('R',R);
const aud=(a,w)=>{R.audit.unshift({t:Date.now(),a,w});R.audit.length=Math.min(R.audit.length,500);rSave()};
const b64=u=>btoa(String.fromCharCode(...u)),unb=s=>Uint8Array.from(atob(s),c=>c.charCodeAt(0));
async function derive(pw,salt){const k=await crypto.subtle.importKey('raw',new TextEncoder().encode(pw),'PBKDF2',false,['deriveKey']);return crypto.subtle.deriveKey({name:'PBKDF2',salt,iterations:250000,hash:'SHA-256'},k,{name:'AES-GCM',length:256},false,['encrypt','decrypt'])}
async function enc(t){const iv=crypto.getRandomValues(new Uint8Array(12));return b64(iv)+'.'+b64(new Uint8Array(await crypto.subtle.encrypt({name:'AES-GCM',iv},rKey,new TextEncoder().encode(t))))}
async function dec(s){const [i,c]=s.split('.');return new TextDecoder().decode(await crypto.subtle.decrypt({name:'AES-GCM',iv:unb(i)},rKey,unb(c)))}
function touchLock(){clearTimeout(rLock);if(rKey)rLock=setTimeout(()=>{rKey=null;toast('Vault locked after 5 minutes of inactivity');if(isR())renderRes()},300000)}
['click','keydown'].forEach(e=>document.addEventListener(e,touchLock));
function dialog(html){const d=$('#rdlg');d.innerHTML=html;if(!d.open)d.showModal();return d}
function pwPrompt(title,confirm){return new Promise(res=>{const d=dialog(``);
d.onclose=()=>res(d.returnValue==='ok'?[$('#rp1').value,confirm?$('#rp2').value:null]:null);setTimeout(()=>$('#rp1').focus(),50)})}
async function unlock(){if(rKey){touchLock();return true}
if(!R.meta){const r=await pwPrompt('Create vault master password (min 8 chars). It cannot be recovered – keep it safe.',true);if(!r)return false;if(r[0].length<8||r[0]!==r[1]){toast('Passwords must match and be 8+ characters');return false}
const salt=crypto.getRandomValues(new Uint8Array(16));rKey=await derive(r[0],salt);R.meta={salt:b64(salt),chk:await enc('ok')};await rSave();touchLock();if(isR())renderRes();return true}
const r=await pwPrompt('Unlock credential vault');if(!r)return false;
try{const k=await derive(r[0],unb(R.meta.salt));rKey=k;if(await dec(R.meta.chk)!=='ok')throw 0;touchLock();if(isR())renderRes();return true}catch(e){rKey=null;toast('Wrong master password');return false}}
const fById=id=>R.folders.find(f=>f.id===id);
const fPath=id=>{const a=[];let f=fById(id);while(f){a.unshift(f.name);f=fById(f.parent)}return a.join(' › ')};
const fKids=id=>R.folders.filter(f=>(f.parent||'')===id);
const fSub=id=>[id,...fKids(id).flatMap(k=>fSub(k.id))];
const fOpts=(sel)=>'— No folder — '+(function w(p,d){return fKids(p).map(f=>`${'\u00a0\u00a0'.repeat(d)}${ICONS[f.icon]||'📁'} ${esc(f.name)} `+w(f.id,d+1)).join('')})('',0);
const fBadge=id=>{const f=fById(id);return f?`${ICONS[f.icon]||'📁'} ${esc(f.name)} `:''};
const tagsOf=s=>String(s||'').split(',').map(x=>x.trim()).filter(Boolean);
const hay=(o,ks)=>ks.map(k=>o[k]||'').join(' ').toLowerCase();
const KS={portals:['name','url','user','email','company','tags','notes'],contacts:['name','org','pos','email','wa','tel','country','web','tags','notes'],notes:['title','desc','body','tags']};
function match(type,o,s){if(rFilter!=='all'&&!fSub(rFilter).includes(o.folder))return false;if(!s)return true;const h=hay(o,KS[type]);return s.toLowerCase().split(/\s+/).every(w=>h.includes(w))}
function cardP(p){return `🔑 ${esc(p.name)} ${fBadge(p.folder)}
👤 ${esc(p.user||'—')} ✉ ${esc(p.email||'—')}
🔒 ••••••••
${p.tags?`
${tagsOf(p.tags).map(t=>`#${esc(t)} `).join('')}
`:''}${p.notes?`
${esc(p.notes)}
`:''}
Open Copy user Copy email Copy password Reveal Edit Delete
`}
function cardC(c){const wa=(c.wa||'').replace(/\D/g,'');return `👥 ${esc(c.name)} ${fBadge(c.folder)}
${esc([c.pos,c.org,c.country].filter(Boolean).join(' · '))}
✉ ${esc(c.email||'—')} 💬 ${esc(c.wa||'—')} ☎ ${esc(c.tel||'—')}
${c.tags?`
${tagsOf(c.tags).map(t=>`#${esc(t)} `).join('')}
`:''}
${c.email?`
Copy email Email `:''}${wa?`
Copy WhatsApp WhatsApp `:''}${c.notes?`
Notes `:''}
Edit Delete `}
function cardN(n){return `📄 ${esc(n.title)} ${fBadge(n.folder)}
${esc(n.desc||'')}
${esc((n.body||'').slice(0,260))}
${n.tags?`
${tagsOf(n.tags).map(t=>`#${esc(t)} `).join('')}
`:''}
View Edit Delete
`}
const grp=(t,l,f)=>l.length?`${t} (${l.length}) ${l.map(f).join('')}
`:'';
function results(){const s=($('#rs')||{}).value||'',v=view;let h='';
if(v==='r-portals')h=grp('Portals',R.portals.filter(o=>match('portals',o,s)),cardP);
else if(v==='r-contacts')h=grp('Contacts',R.contacts.filter(o=>match('contacts',o,s)),cardC);
else if(v==='r-notes')h=grp('Notes & documents',R.notes.filter(o=>match('notes',o,s)),cardN);
else if(s)h=grp('Portal credentials',R.portals.filter(o=>match('portals',o,s)),cardP)+grp('Contacts',R.contacts.filter(o=>match('contacts',o,s)),cardC)+grp('Office notes & documents',R.notes.filter(o=>match('notes',o,s)),cardN);
else{const all=[...R.portals.map(o=>({o,f:cardP})),...R.contacts.map(o=>({o,f:cardC})),...R.notes.map(o=>({o,f:cardN}))];
const rec=[...all].sort((a,b)=>b.o.created-a.o.created).slice(0,6),acc=all.filter(x=>x.o.accessed).sort((a,b)=>b.o.accessed-a.o.accessed).slice(0,6);
const fav=[...R.portals].filter(p=>p.opens).sort((a,b)=>b.opens-a.opens).slice(0,4);
h=grp('Quick access – frequently used portals',fav,cardP)+grp('Recently added',rec.map(x=>x.o),o=>(o.title!==undefined?cardN:o.pw!==undefined?cardP:cardC)(o))+grp('Recently accessed',acc.map(x=>x.o),o=>(o.title!==undefined?cardN:o.pw!==undefined?cardP:cardC)(o))}
$('#rres').innerHTML=h||'Nothing found. Use the buttons above to add or import records.
'}
function renderRes(){const v=view,a=$('#area');a.className='res';
const add=v==='r-portals'?'+ Add New Portal ⬆ Import PDF / text ':v==='r-contacts'?'+ Add New Contact ⬆ Import PDF / text ':v==='r-notes'?'+ New document ⬆ Upload text / PDF ':'';
const stats=v==='r-dash'?`${[['Saved portals',R.portals.length,'🔑'],['Contacts',R.contacts.length,'👥'],['Notes & documents',R.notes.length,'📄'],['Folders',R.folders.length,'📁']].map(s=>`
${s[2]} ${s[1]} ${s[0]}
`).join('')}
`:'';
a.innerHTML=`
${RV[v]} ${add}📁 Folders ${rKey?'🔓 Lock vault':'🔒 Unlock vault'} Audit log
${'All folders '+fOpts(rFilter).replace('— No folder — ','')}
${stats}
`;
$('#rf').value=rFilter;$('#rs').oninput=results;$('#rf').onchange=e=>{rFilter=e.target.value;results()};results()}
const _ri=renderItems;renderItems=function(){isR()?renderRes():_ri()};
const _rc=renderCrumbs;renderCrumbs=function(){isR()?$('#crumbs').innerHTML='🗂 '+RV[view]:_rc()};
const _rn=render;render=function(){_rn();$('#bar').style.display=isR()?'none':'';$('.status').style.display=isR()?'none':'';if(isR())$('#sideFoot').textContent=rKey?'Vault unlocked':'Vault locked'};
const FORMS={portals:[['name','Portal name *'],['url','Website URL'],['user','Login username'],['email','Login email'],['pw','Password','password'],['company','Company / organization'],['tags','Tags (comma separated)'],['notes','Additional notes','area']],
contacts:[['name','Full name *'],['org','Company / university / organization'],['pos','Job position'],['email','Email'],['wa','WhatsApp number'],['tel','Telephone'],['country','Country'],['web','Website'],['tags','Tags (comma separated)'],['notes','Additional notes','area']],
notes:[['title','Title *'],['desc','Description'],['tags','Tags (comma separated)'],['body','Content','big']]};
async function editRec(t,id){let o=id?{...R[t].find(x=>x.id===id)}:{};if(t==='portals'){if(!await unlock())return;o.pw=id&&o.pw?await dec(o.pw):''}
const fs=FORMS[t].map(([k,l,ty])=>`${l}${ty==='area'?``:ty==='big'?``:` `} `).join('')+`Folder${fOpts(o.folder)} `;
const d=dialog(``);
if(t==='portals')d.querySelector('[data-f=pw]').type='password';
d.onclose=async()=>{if(d.returnValue!=='ok')return;const n={...o};d.querySelectorAll('[data-f]').forEach(i=>n[i.dataset.f]=i.value.trim());
if(t==='contacts'&&(!n.name||!(n.email||n.wa||n.tel))){toast('Name and at least one contact method are required');return}
if((t==='portals'&&!n.name)||(t==='notes'&&!n.title)){toast('Name / title is required');return}
if(t==='portals')n.pw=n.pw?await enc(n.pw):'';
if(id){R[t][R[t].findIndex(x=>x.id===id)]=n;if(t==='portals')aud('edit',n.name)}else{n.id=uid();n.created=Date.now();R[t].unshift(n)}await rSave();toast('Saved');renderRes()}}
async function pwOf(id){if(!await unlock())return null;const p=R.portals.find(x=>x.id===id);return p.pw?dec(p.pw):''}
async function copyTxt(txt,label){try{await navigator.clipboard.writeText(txt);toast(label+' copied')}catch(e){toast('Copy blocked by the browser')}}
function viewNote(id){const o=[...R.notes,...R.contacts].find(x=>x.id===id);o.accessed=Date.now();rSave();dialog(``)}
document.addEventListener('click',async e=>{const el=e.target.closest('[data-ra]');if(!el)return;e.preventDefault();const {ra,id,t}=el.dataset,p=id&&R.portals.find(x=>x.id===id),c=id&&R.contacts.find(x=>x.id===id);
if(ra==='add')editRec(t);else if(ra==='edit')editRec(t,id);
else if(ra==='del'){const o=R[t].find(x=>x.id===id);if(await sure('Delete "'+(o.name||o.title)+'" permanently?')){R[t]=R[t].filter(x=>x.id!==id);if(t==='portals')aud('delete',o.name);await rSave();renderRes()}}
else if(ra==='open'){p.opens=(p.opens||0)+1;p.accessed=Date.now();rSave();if(p.url)window.open(/^https?:/i.test(p.url)?p.url:'https://'+p.url,'_blank','noopener');else toast('No URL saved')}
else if(ra==='cu'){p.accessed=Date.now();copyTxt(p.user,'Username')}else if(ra==='ce'){copyTxt(p.email,'Email')}
else if(ra==='cp'){const w=await pwOf(id);if(w===null)return;aud('copy password',p.name);p.accessed=Date.now();await copyTxt(w,'Password (clears in 30s)');setTimeout(()=>navigator.clipboard.writeText('').catch(()=>{}),30000)}
else if(ra==='show'){const w=await pwOf(id);if(w===null)return;aud('reveal password',p.name);const s=$('#pw_'+id);s.textContent=w;setTimeout(()=>{s.textContent='••••••••'},15000)}
else if(ra==='cm')copyTxt(c.email,'Email');else if(ra==='cw')copyTxt(c.wa,'WhatsApp number');else if(ra==='vn')viewNote(id);
else if(ra==='lock'){rKey=null;renderRes();toast('Vault locked')}else if(ra==='unlockb')unlock();
else if(ra==='audit')dialog(``);
else if(ra==='folders')folderMgr();else if(ra==='imp'){rTab=t;$('#rfile').click()}
else if(ra==='fnew')fEdit('',el.dataset.p||'');else if(ra==='fedit')fEdit(id);else if(ra==='fdel')fDel(id)});
function folderMgr(){const row=(p,d)=>fKids(p).map(f=>`${ICONS[f.icon]||'📁'} ${esc(f.name)} + Sub Edit Delete
`+row(f.id,d+1)).join('');
dialog(``)}
function fEdit(id,parent){const f=id?{...fById(id)}:{name:'',icon:'general',color:COLORS[0],parent};
const d=dialog(``);
d.querySelectorAll('#fi .ico').forEach(b=>b.onclick=()=>{f.icon=b.dataset.i;d.querySelectorAll('#fi .ico').forEach(x=>x.classList.toggle('on',x===b))});d.querySelectorAll('#fc .col').forEach(b=>b.onclick=()=>{f.color=b.dataset.c;d.querySelectorAll('#fc .col').forEach(x=>x.classList.toggle('on',x===b))});
d.onclose=async()=>{if(d.returnValue!=='ok')return;f.name=$('#fn').value.trim();if(!f.name){toast('Folder name required');return}if(id)R.folders[R.folders.findIndex(x=>x.id===id)]=f;else{f.id=uid();R.folders.push(f)}await rSave();renderRes();setTimeout(folderMgr,50)}}
async function fDel(id){if(!await sure('Delete this folder and its subfolders? Records inside are kept but become unfiled.'))return;const ids=fSub(id);R.folders=R.folders.filter(f=>!ids.includes(f.id));['portals','contacts','notes'].forEach(t=>R[t].forEach(o=>{if(ids.includes(o.folder))o.folder=''}));await rSave();renderRes();setTimeout(folderMgr,50)}
/* ---- import / extraction ---- */
const loadJS=u=>new Promise((ok,no)=>{if(document.querySelector(`script[src="${u}"]`))return ok();const s=document.createElement('script');s.src=u;s.onload=ok;s.onerror=()=>no(new Error('Could not load '+u+' (internet needed for PDF/OCR)'));document.head.appendChild(s)});
async function pdfText(file){await loadJS('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js');pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const pdf=await pdfjsLib.getDocument({data:await file.arrayBuffer()}).promise;let out='';
for(let i=1;i<=pdf.numPages;i++){toast(`Reading page ${i}/${pdf.numPages}…`);const pg=await pdf.getPage(i),tc=await pg.getTextContent(),rows={};
tc.items.forEach(it=>{const y=Math.round(it.transform[5]/3);(rows[y]=rows[y]||[]).push(it)});
let tx=Object.keys(rows).sort((a,b)=>b-a).map(y=>rows[y].sort((a,b)=>a.transform[4]-b.transform[4]).map(i=>i.str).join(' ')).join('\n');
if(tx.replace(/\s/g,'').length<20){toast(`Page ${i} looks scanned – running OCR…`);await loadJS('https://cdnjs.cloudflare.com/ajax/libs/tesseract.js/5.1.1/tesseract.min.js');const vp=pg.getViewport({scale:2.5}),cv=document.createElement('canvas');cv.width=vp.width;cv.height=vp.height;await pg.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise;tx=(await Tesseract.recognize(cv,'eng')).data.text}
out+=tx+'\n\n'}return out}
function parseCreds(t){const L=t.split(/\r?\n/).map(s=>s.trim()),out=[];let cur=null;const fresh=()=>({name:'',url:'',user:'',email:'',pw:'',company:'',notes:''}),has=c=>c&&(c.user||c.email||c.pw),push=()=>{if(has(cur))out.push(cur);cur=null},C=()=>cur||(cur=fresh());
const lab=(l,re)=>{const m=l.match(re);return m?m[1].trim():null};
L.forEach((l,i)=>{if(!l){if(cur&&cur.pw)push();return}let v;
if(v=lab(l,/^(?:portal|site|platform|university|institution|name)(?:\s*name)?\s*[:\-–]\s*(.+)/i)){if(has(cur))push();C().name=v;return}
if(v=lab(l,/^(?:url|link|web\s*address|website)\s*[:\-–]?\s*(\S+)/i)){if(cur&&cur.url&&has(cur))push();C().url=v;return}
if(v=lab(l,/^(?:user\s*name|username|user\s*id|login(?:\s*id)?|user)\s*[:\-–=]\s*(.+)/i)){C().user=v;if(v.includes('@'))cur.email=cur.email||v;return}
if(v=lab(l,/^e-?mail(?:\s*(?:address|id))?\s*[:\-–=]\s*(\S+)/i)){C().email=v;return}
if(v=lab(l,/^(?:pass\s*word|password|passcode|pwd|pass)\s*[:\-–=]\s*(.+)/i)){C().pw=v;return}
const um=l.match(/https?:\/\/\S+|www\.\S+/i);if(um){if(cur&&cur.url&&has(cur))push();C().url=um[0];return}
const nx=(L.slice(i+1).find(x=>x)||'');if(!/[:@]/.test(l)&&l.length<60&&/^[\w\s.&()\-\/]+$/.test(l)&&/[:\-–=]/.test(nx)){if(has(cur))push();C().name=l;return}
if(cur)cur.notes+=(cur.notes?' ':'')+l});push();
out.forEach(r=>{if(!r.name&&r.url)r.name=r.url.replace(/^https?:\/\/(www\.)?/i,'').split(/[\/?]/)[0];if(!r.name)r.name='Unnamed portal'});return out}
function parseContacts(t){const blocks=t.split(/\n\s*\n/).map(b=>b.split(/\n/).map(s=>s.trim()).filter(Boolean)).filter(b=>b.length),out=[];
blocks.forEach(b=>{const txt=b.join('\n'),em=(txt.match(/[\w.+-]+@[\w-]+(?:\.[\w-]+)+/)||[''])[0];if(!em&&!/\d{7}/.test(txt.replace(/\D/g,'').length>6?txt:''))return;
const c={name:'',org:'',pos:'',email:em,wa:'',tel:'',country:'',web:''};
b.forEach(l=>{const ph=l.match(/\+?\d[\d\s().-]{7,}\d/);let v;
if(ph){if(/whats\s*app|\bwa\b/i.test(l))c.wa=c.wa||ph[0].trim();else c.tel=c.tel||ph[0].trim()}
else if(v=l.match(/^(?:name|contact|full name)\s*[:\-–]\s*(.+)/i))c.name=v[1];
else if(v=l.match(/^(?:company|university|organi[sz]ation|institution|college)\s*[:\-–]\s*(.+)/i))c.org=v[1];
else if(v=l.match(/^(?:position|title|job)\s*[:\-–]\s*(.+)/i))c.pos=v[1];
else if(v=l.match(/^country\s*[:\-–]\s*(.+)/i))c.country=v[1];
else if(!c.org&&/university|college|institute|school|ltd|inc|gmbh|academy/i.test(l)&&!l.includes('@'))c.org=l;
else if(!c.name&&!l.includes('@')&&!/https?:|www\./i.test(l)&&l.length<50)c.name=l.replace(/^(?:mr|ms|mrs|dr|prof)\.?\s+/i,m=>m)});
if(!c.name&&em)c.name=em.split('@')[0];out.push(c)});return out}
function isDup(t,r){const l=s=>(s||'').toLowerCase();return t==='portals'?R.portals.some(p=>l(p.name)===l(r.name)&&(l(p.user)===l(r.user)||l(p.email)===l(r.email))):t==='contacts'?R.contacts.some(c=>(r.email&&l(c.email)===l(r.email))||(l(c.name)===l(r.name)&&l(c.org)===l(r.org))):R.notes.some(n=>l(n.title)===l(r.title))}
async function reviewImport(t,rows,src){if(t==='portals'&&!await unlock())return;
const ks=t==='portals'?[['name','Portal'],['url','URL'],['user','Username'],['email','Email'],['pw','Password']]:[['name','Name'],['org','Organization'],['email','Email'],['wa','WhatsApp'],['tel','Phone']];
rows.forEach(r=>r.dup=isDup(t,r));
const d=dialog(``);
d.onclose=async()=>{if(d.returnValue!=='ok')return;let n=0;for(let i=0;ir[x.dataset.k]=x.value.trim());
r.id=uid();r.created=Date.now()+i;r.tags='';r.notes=r.notes||'';r.folder=r.folder||'';if(t==='portals')r.pw=r.pw?await enc(r.pw):'';R[t].unshift(r);n++}await rSave();toast(n+' record(s) imported');renderRes()}}
async function handleFile(f){const t=rTab;try{let txt;const isPdf=/\.pdf$/i.test(f.name)||f.type==='application/pdf';txt=isPdf?await pdfText(f):await f.text();
if(t==='notes'){const n={id:uid(),title:f.name.replace(/\.[^.]+$/,''),desc:'Uploaded '+f.name,body:txt.trim(),folder:'',tags:'',created:Date.now()};if(isDup('notes',n)&&!await sure('A document with this title exists. Add anyway?'))return;R.notes.unshift(n);await rSave();toast('Document added');renderRes();return}
const rows=t==='portals'?parseCreds(txt):parseContacts(txt);if(!rows.length){toast('No '+(t==='portals'?'credentials':'contacts')+' recognised in that file');return}reviewImport(t,rows,f.name)}catch(e){toast('Import failed: '+e.message)}}
(function(){const st=document.createElement('style');st.textContent=`#area.res{display:block;padding:16px;overflow:auto}.rt2{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}.rt2 h2{margin:0;color:var(--blue);font-size:20px}.rmenu{display:none}@media(max-width:760px){.rmenu{display:inline-block}}
.rst{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:8px}.rst div{background:#fff;border:1px solid var(--line);border-radius:14px;padding:14px;box-shadow:0 2px 8px #1b223608}.rst span{font-size:22px}.rst b{display:block;font-size:26px;color:var(--blue)}.rst small{color:var(--mut)}
.rg{margin:18px 0 8px;color:var(--mut);text-transform:uppercase;font-size:12px;letter-spacing:.06em}.rgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}.rc{background:#fff;border:1px solid var(--line);border-radius:14px;padding:14px;box-shadow:0 2px 8px #1b223608;display:flex;flex-direction:column;gap:6px}.rc:hover{box-shadow:0 6px 18px #1b223622}.rh{display:flex;justify-content:space-between;gap:6px;align-items:center}.rm{color:var(--mut);font-size:13px;word-break:break-word}.rb{display:flex;gap:5px;flex-wrap:wrap;margin-top:4px}.rb .b{padding:4px 8px;font-size:12px;text-decoration:none}.rtag{display:inline-block;background:#eef2fb;color:var(--blue);border-radius:99px;padding:1px 8px;font-size:11px;margin:2px 3px 0 0}
.rfg{display:grid;grid-template-columns:1fr 1fr;gap:10px;min-width:min(640px,80vw)}.rfg label{display:flex;flex-direction:column;gap:3px;font-size:12px;color:var(--mut)}.rfg .wide{grid-column:1/-1}.rfg input,.rfg select,.rfg textarea{padding:7px;border:1px solid var(--line);border-radius:8px}@media(max-width:700px){.rfg{grid-template-columns:1fr}}
.frow{display:flex;gap:8px;align-items:center;padding:6px 0;border-bottom:1px solid var(--line)}.ico{font-size:22px;border:2px solid transparent;background:#f3f5fa;border-radius:10px;padding:4px 8px;margin:2px;cursor:pointer}.ico.on{border-color:var(--blue)}.col{width:28px;height:28px;border-radius:50%;border:3px solid #fff;box-shadow:0 0 0 1px var(--line);margin:3px;cursor:pointer}.col.on{box-shadow:0 0 0 3px var(--blue)}.rtb{border-collapse:collapse;font-size:12px}.rtb td,.rtb th{padding:3px;text-align:left}.rtb input:not([type=checkbox]){width:140px;padding:4px;border:1px solid var(--line);border-radius:6px}`;document.head.appendChild(st);
const nav=document.querySelector('.nav'),tr=nav.querySelector('[data-v=trash]');[['r-dash','🏠 Resources Dashboard'],['r-portals','🔑 Portal Logins'],['r-contacts','👥 Contact Directory'],['r-notes','📄 Notes & Documents']].reverse().forEach(([v,l])=>{const b=document.createElement('button');b.dataset.v=v;b.textContent=l;nav.insertBefore(b,nav.firstChild.nextSibling?nav.querySelector('[data-v=files]'):null)});
const nv=nav.querySelectorAll('[data-v^=r-]');nv[0].parentNode.insertBefore(Object.assign(document.createElement('hr'),{style:'width:100%;border:0;border-top:1px solid var(--line);margin:4px 0'}),nav.querySelector('[data-v=files]'));
const dl=document.createElement('dialog');dl.id='rdlg';document.body.appendChild(dl);const fi=Object.assign(document.createElement('input'),{type:'file',id:'rfile',hidden:true,accept:'.pdf,.txt,.csv,.md,text/plain,application/pdf'});fi.onchange=()=>{const f=fi.files[0];fi.value='';if(f)handleFile(f)};document.body.appendChild(fi);
const iv=setInterval(async()=>{if(!db)return;clearInterval(iv);const s=await kvGet('R');if(s)R={...R,...s};if(isR())renderRes()},100)})();
/* ===== Navigation history, folder icons, bold names, sidebar labels ===== */
(function(){
const SV={business:' ',
archive:' '};
const CHOICES=[['standard','Standard Folder','General office folders'],['business','Business Folder','Company documents & operations'],['archive','Archive Folder','Completed projects & archives']];
const fico=k=>SV[k]||'📁';
/* ---- folder icons ---- */
const _icon=icon;icon=function(n){return n.type==='folder'&&SV[n.fi]?SV[n.fi]:_icon(n)};
const _rt=renderTree;renderTree=function(){_rt();document.querySelectorAll('#tree .tn[data-go]').forEach(el=>{const n=N.get(el.dataset.go);if(!n)return;if(SV[n.fi])el.innerHTML=el.innerHTML.replace('📁',''+SV[n.fi]+' ');if(n.bold)el.style.fontWeight='700'})};
function changeIcon(){const ns=selNodes().filter(n=>n.type==='folder');if(!ns.length)return;const cur=ns.every(n=>(n.fi||'standard')===(ns[0].fi||'standard'))?(ns[0].fi||'standard'):'';
const d=dialog(``);
d.querySelectorAll('.icc').forEach(b=>b.onclick=async()=>{for(const n of ns){if(b.dataset.fi==='standard')delete n.fi;else n.fi=b.dataset.fi;await put(n)}d.close();render();toast('Folder icon changed')})}
/* ---- bold names ---- */
const _ih=itemHTML;itemHTML=function(n){const h=_ih(n);return n.bold?h.replace('n.bold);for(const n of ns){if(on)n.bold=true;else delete n.bold;await put(n)}render();toast(on?'Name set to bold':'Bold removed')}
/* ---- context menu ---- */
const _sc=showCtx;showCtx=function(e,id){_sc(e,id);if(view==='trash'||!sel.size)return;const c=$('#ctx'),ns=selNodes();let h='
';
if(ns.length&&ns.every(n=>n.type==='folder'))h+='
🎨 Change Icon…
';
h+=`
${ns.every(n=>n.bold)?'Remove bold from name':'B Bold name'}
`;c.insertAdjacentHTML('beforeend',h);c.style.top=Math.min(e.clientY,innerHeight-c.offsetHeight-10)+'px'};
const _act=act;act=async function(a){if(a==='changeIcon'){$('#ctx').style.display='none';return changeIcon()}if(a==='toggleBold'){$('#ctx').style.display='none';return toggleBold()}return _act(a)};
/* ---- navigation history (Back / Forward / Up / Home) ---- */
let H=[{v:view,c:cwd}],i=0,busy=false;const cur=()=>({v:view,c:cwd}),same=(a,b)=>a.v===b.v&&(a.v!=='files'||a.c===b.c);
const _go=go,_sv=setView;
function rec(){if(busy)return;const s=cur();if(same(s,H[i]))return;H=H.slice(0,i+1);H.push(s);i=H.length-1}
go=function(id){_go(id);rec()};setView=function(v){_sv(v);rec()};
function to(k){if(k<0||k>=H.length)return;i=k;const s=H[k];busy=true;try{if(s.v==='files'){const n=N.get(s.c);if(s.c!=='root'&&(!n||!live(n)))s.c='root';_go(s.c)}else _sv(s.v)}finally{busy=false}}
const back=()=>to(i-1),home=()=>{if(!(view==='r-dash'))setView('r-dash')},up=()=>{if(view==='files'&&cwd!=='root'){const n=N.get(cwd);go(n&&n.parent?n.parent:'root')}};
const nb=document.createElement('div');nb.id='navbar';nb.innerHTML='
⬆ Up 🏠 Home ';
document.querySelector('main').insertBefore(nb,$('#bar'));
const sb=document.createElement('button');sb.id='sideBack';sb.type='button';sb.title='Back to previous page (Alt+←)';sb.innerHTML='
◀ Back';$('#side').insertBefore(sb,$('#brand'));sb.onclick=back;$('#nUp').onclick=up;$('#nHome').onclick=home;
const upd=()=>{$('#sideBack').disabled=i<=0;$('#nUp').disabled=!(view==='files'&&cwd!=='root');$('#nHome').disabled=view==='r-dash'};
const _r=render;render=function(){_r();upd()};
document.addEventListener('keydown',e=>{if(e.altKey&&e.key==='ArrowLeft'){e.preventDefault();back()}});
document.addEventListener('mouseup',e=>{if(e.button===3){e.preventDefault();back()}});
/* ---- sidebar labels ---- */
const nav=document.querySelector('.nav'),first=nav.querySelector('[data-v=r-dash]');
if(first){first.textContent='🏠 Dashboard';const h=document.createElement('div');h.className='nav-h';h.textContent='RESOURCES';nav.insertBefore(h,first)}
const st=document.createElement('style');st.textContent=`#navbar{display:flex;gap:6px;padding:6px 10px;background:#fff;border-bottom:1px solid var(--line)}#navbar .b:disabled{opacity:.4;cursor:default}.nav .nav-h{font-weight:800;font-size:12px;letter-spacing:.08em;color:var(--blue);padding:8px 10px 2px}.nav button[data-v^=r-]{font-weight:700}.fsvg{width:1em;height:1em;display:inline-block;vertical-align:-.12em}.fsvg-s{display:inline-block;width:1.1em;height:1.1em;vertical-align:-.2em}.fsvg-s .fsvg{width:100%;height:100%}.icg{display:flex;gap:10px;flex-wrap:wrap}.icc{display:flex;flex-direction:column;align-items:center;gap:4px;width:150px;padding:12px 8px;border:2px solid var(--line);border-radius:12px;background:#fff;cursor:pointer}.icc:hover{background:#eef2fb}.icc.on{border-color:var(--blue);background:#eef2fb}.icc span{font-size:52px;line-height:1}.icc small{color:var(--mut);text-align:center}`;st.textContent+=`#sideBack{display:flex;align-items:center;gap:8px;width:calc(100% - 20px);margin:10px 10px 0;padding:8px 12px;font:inherit;font-weight:600;font-size:13px;color:var(--blue);background:#fff;border:1px solid var(--line);border-radius:10px;cursor:pointer;transition:background .15s,box-shadow .15s}#sideBack:hover:not(:disabled){background:var(--tint,#eef2fb);box-shadow:0 2px 8px #1b223614}#sideBack:disabled{opacity:.4;cursor:default}#sideBack span{font-size:11px}`;document.head.appendChild(st);
upd();
})();