ACP ERP  ·  Internal Mail
Inspire Africa Coffee Park  ·  Ntungamo, Uganda
🔔
NT
Nelson Tugume
CEO & Founder
← Dashboard
Portal ERP | HR Finance Production Quality Stores Procurement Infra Sales Office Agri Tourism Export RDP POW Analytics Inventory Mail
✉️
DepartmentInternal Communications
👤
Module ManagerKubariho Venture
🏢
LocationRwashameire, Ntungamo
SystemChecking...
📅
Date
Time
📆
Fiscal YearFY 2025/2026
Inbox
Channels & DMs
Announcements
All
Management
HR
Finance
Operations
Security
Urgent
Notifications
Filter by module in the main panel.
Settings
Select a message to read
Press C to compose • R to reply
💬
Select a channel or DM to start chatting
Thread
✎ Email Signature
🚫 Out of Office
🔔 Notification Preferences
👤 Display & Theme
❓ How To Use — Communications Hub
✎ New Message
'); win.document.close();win.focus();win.print(); } // ── Compose ── function openCompose(opts){ opts=opts||{}; composeToIds=opts.to||[]; document.getElementById('co-to-chips').innerHTML=''; composeToIds.forEach(id=>{const s=staffById(id);addToChip(s);}); document.getElementById('co-subject').value=opts.subject||''; const sigData=getData('erp_mail_settings',SETTINGS_DEFAULT); const rt=document.getElementById('co-body-rt'); rt.innerHTML=(opts.body?'

'+escHtml(opts.body).replace(/\n/g,'
')+'

':'')+'
'+(sigData.signature||SETTINGS_DEFAULT.signature); document.getElementById('co-priority').value='normal'; document.getElementById('co-attach').value=''; document.getElementById('co-schedule').value=''; document.getElementById('co-draft-note').textContent=''; document.getElementById('compose-overlay').classList.add('open'); if(composeDraftInterval)clearInterval(composeDraftInterval); composeDraftInterval=setInterval(autoSaveDraft,30000); buildTemplateMenu(); } function closeCompose(){ document.getElementById('compose-overlay').classList.remove('open'); if(composeDraftInterval){clearInterval(composeDraftInterval);composeDraftInterval=null;} } function discardCompose(){ if(confirm('Discard this draft?'))closeCompose(); } function addToChip(s){ if(composeToIds.includes(s.id))return; composeToIds.push(s.id); const chip=document.createElement('span'); chip.className='co-chip'; chip.innerHTML=escHtml(s.name)+''; const input=document.getElementById('co-to-input'); document.getElementById('co-to-chips').insertBefore(chip,input); input.value=''; document.getElementById('co-to-ac').classList.remove('open'); } function removeToChip(id,btn){ composeToIds=composeToIds.filter(x=>x!==id); btn.parentElement.remove(); } function showToAutocomplete(val){ const ac=document.getElementById('co-to-ac'); if(!val){ac.classList.remove('open');return;} const q=val.toLowerCase(); const matches=STAFF.filter(s=>s.name.toLowerCase().includes(q)||s.email.toLowerCase().includes(q)).slice(0,8); if(!matches.length){ac.classList.remove('open');return;} ac.innerHTML=matches.map(s=>`
${initials(s.name)}
${escHtml(s.name)}
${escHtml(s.role)}
`).join(''); ac.classList.add('open'); } function toInputKeydown(e){ if(e.key==='Enter'||e.key===','){e.preventDefault();const v=e.target.value.trim();if(v){const m=STAFF.find(s=>s.name.toLowerCase().includes(v.toLowerCase()));if(m)addToChip(m);}}} function rtCmd(cmd,val){document.getElementById('co-body-rt').focus();document.execCommand(cmd,false,val||null);} async function sendComposedMsg(){ if(!composeToIds.length){alert('Please add at least one recipient.');return;} const subj=document.getElementById('co-subject').value.trim(); if(!subj){alert('Please enter a subject.');return;} const bodyHtml=document.getElementById('co-body-rt').innerHTML; const bodyText=document.getElementById('co-body-rt').innerText; const attach=document.getElementById('co-attach').value.trim(); const sched=document.getElementById('co-schedule').value; const msgs=getMsgs(); const newMsg={id:'MSG-'+Date.now(),from:CURRENT_USER.id,to:[...composeToIds],cc:[],bcc:[],subject:subj,body:bodyText,bodyHtml,folder:sched?'drafts':'sent',labels:[],priority:document.getElementById('co-priority').value,read:true,starred:false,attachments:attach?[{name:attach,type:'File',size:'—'}]:[],threadId:null,scheduledAt:sched||null,ts:Date.now(),date:new Date().toISOString().slice(0,10),time:new Date().toTimeString().slice(0,5)}; // persist to API then fall back to localStorage try{if(window.api&&api.isOnline()!==false)await api.mail.send({to:newMsg.to,subject:newMsg.subject,body:newMsg.body,bodyHtml:newMsg.bodyHtml,priority:newMsg.priority,folder:newMsg.folder,scheduledAt:newMsg.scheduledAt||null,attachments:newMsg.attachments});}catch(e){} msgs.push(newMsg);saveMsgs(msgs); closeCompose();renderMailList(); alert(sched?'Message scheduled.':'Message sent.'); } function autoSaveDraft(){ const subj=document.getElementById('co-subject').value; if(!subj&&!composeToIds.length)return; const msgs=getMsgs(); const bodyHtml=document.getElementById('co-body-rt').innerHTML; const bodyText=document.getElementById('co-body-rt').innerText; const draft=msgs.find(m=>m.folder==='drafts'&&m._autoId); const ts=Date.now(); if(draft){draft.subject=subj;draft.body=bodyText;draft.bodyHtml=bodyHtml;draft.to=[...composeToIds];draft.ts=ts;} else{msgs.push({id:'MSG-AUTO-'+ts,_autoId:true,from:CURRENT_USER.id,to:[...composeToIds],cc:[],bcc:[],subject:subj||'(no subject)',body:bodyText,bodyHtml,folder:'drafts',labels:[],priority:'normal',read:true,starred:false,attachments:[],threadId:null,scheduledAt:null,ts,date:new Date().toISOString().slice(0,10),time:new Date().toTimeString().slice(0,5)});} saveMsgs(msgs); document.getElementById('co-draft-note').textContent='Auto-saved '+new Date().toLocaleTimeString(); } function saveAsDraft(){autoSaveDraft();closeCompose();renderMailList();} function buildTemplateMenu(){ const settings=getData('erp_mail_settings',SETTINGS_DEFAULT); const custom=settings.customTemplates||[]; const defaults=['Noted, will action by [date]','Approved — please proceed','On hold — awaiting further information','Please resend with supporting documents','Meeting scheduled — see calendar invite','This has been escalated to [name]']; const all=[...defaults,...custom]; document.getElementById('template-menu').innerHTML=all.map((t,i)=>`
${escHtml(t)}
`).join('')+'
+ Save current as template
'; window._tmplList=all; } function toggleTemplateMenu(){document.getElementById('template-menu').classList.toggle('open');} function applyTemplate(i){ const t=window._tmplList[i]; document.getElementById('co-body-rt').focus(); document.execCommand('insertText',false,t); document.getElementById('template-menu').classList.remove('open'); } function saveCustomTemplate(){ const text=document.getElementById('co-body-rt').innerText.slice(0,120); if(!text)return; const settings=getData('erp_mail_settings',SETTINGS_DEFAULT); if(!settings.customTemplates)settings.customTemplates=[]; settings.customTemplates.push(text); saveData('erp_mail_settings',settings); buildTemplateMenu(); alert('Template saved.'); } // ── CHAT ── function getChats(){return getData('erp_chats',CHATS_SEED);} function saveChats(c){saveData('erp_chats',c);} function getChannels(){return getData('erp_channels',CHANNELS_SEED);} function renderChannelList(){ const q=(document.getElementById('chat-list-search').value||'').toLowerCase(); const channels=getChannels(); const chats=getChats(); const body=document.getElementById('chat-list-body'); let html='
Channels
'; channels.filter(ch=>!q||ch.name.includes(q)).forEach(ch=>{ const msgs=chats[ch.id]||[]; const last=msgs[msgs.length-1]; const active=currentChannel&¤tChannel.id===ch.id; html+=`
${ch.type==='readonly'?'📌':'#'}
${escHtml(ch.name)}
${last?escHtml(staffById(last.from).name.split(' ')[0]+': '+last.text).slice(0,40):escHtml(ch.description).slice(0,40)}
`; }); html+='
Direct Messages
'; STAFF.filter(s=>s.id!==CURRENT_USER.id&&(!q||s.name.toLowerCase().includes(q))).slice(0,15).forEach(s=>{ const dmId='dm-'+[CURRENT_USER.id,s.id].sort().join('-'); const active=currentChannel&¤tChannel.id===dmId; html+=`
${initials(s.name)}
${escHtml(s.name)}
`; }); body.innerHTML=html; } function openChannel(id){ const channels=getChannels(); const ch=channels.find(c=>c.id===id)||{id,name:id,description:'',type:'channel',members:'all'}; currentChannel=ch; renderChannelList(); showChatPanel(ch); renderChatMessages(); } function openDM(userId){ const s=staffById(userId); const dmId='dm-'+[CURRENT_USER.id,userId].sort().join('-'); currentChannel={id:dmId,name:s.name,description:'Direct message with '+s.name,type:'dm',members:[CURRENT_USER.id,userId]}; renderChannelList(); showChatPanel(currentChannel); renderChatMessages(); } function showChatPanel(ch){ document.getElementById('chat-welcome').style.display='none'; const act=document.getElementById('chat-active'); act.classList.remove('hidden'); act.style.display='flex'; document.getElementById('ch-hdr-name').textContent=(ch.type==='channel'||ch.type==='readonly'?'# ':'')+ch.name; document.getElementById('ch-hdr-desc').textContent=ch.description||''; const members=ch.members==='all'?STAFF.length:Array.isArray(ch.members)?ch.members.length:'?'; document.getElementById('ch-hdr-meta').textContent=ch.type==='dm'?'Direct Message':members+' members'; const readonly=ch.type==='readonly'&&CURRENT_USER.id!=='CEO-001'; document.getElementById('chat-input').placeholder=readonly?'This channel is read-only':'Message #'+ch.name+'...'; document.getElementById('chat-input').disabled=readonly; // pinned bar const chats=getChats(); const msgs=chats[ch.id]||[]; const pinned=msgs.find(m=>m.pinned); const pb=document.getElementById('pinned-bar'); if(pinned){pb.classList.remove('hidden');document.getElementById('pinned-msg-text').textContent=pinned.text.slice(0,80);} else pb.classList.add('hidden'); } function renderChatMessages(){ if(!currentChannel)return; const chats=getChats(); const msgs=(chats[currentChannel.id]||[]).slice(-200); const q=(document.getElementById('chat-msg-search').value||'').toLowerCase(); const filtered=q?msgs.filter(m=>m.text.toLowerCase().includes(q)||staffById(m.from).name.toLowerCase().includes(q)):msgs; const container=document.getElementById('chat-messages'); let html=''; let lastFrom='';let lastTs=0; filtered.forEach((m,i)=>{ const s=staffById(m.from); const grouped=(m.from===lastFrom&&(m.ts-lastTs)<300000); const reactions=Object.entries(m.reactions||{}).map(([emoji,users])=>{ const mine=users.includes(CURRENT_USER.id); return `${emoji} ${users.length}`; }).join(''); const mentionText=highlightMentions(escHtml(m.text)); if(!grouped){ html+=`
${initials(s.name)}
${escHtml(s.name)}${fmtTs(m.ts)}
${mentionText}
${reactions?`
${reactions}
`:''}
`; } else { html+=`
${fmtTs(m.ts)}${mentionText}
${reactions?`
${reactions}
`:''}
`; } lastFrom=m.from;lastTs=m.ts; }); if(!filtered.length)html='
💬
No messages yet
'; container.innerHTML=html+''; scrollChatToBottom(); } function highlightMentions(text){ return text.replace(/@([A-Za-z]+\s[A-Za-z]+)/g,'@$1'); } function scrollChatToBottom(){const c=document.getElementById('chat-messages');if(c)c.scrollTop=c.scrollHeight;} function filterChatMsgs(){renderChatMessages();} function sendChatMsg(){ const input=document.getElementById('chat-input'); const text=input.value.trim(); if(!text||!currentChannel)return; const chats=getChats(); if(!chats[currentChannel.id])chats[currentChannel.id]=[]; const msgs=chats[currentChannel.id]; if(msgs.length>=200)msgs.shift(); msgs.push({id:'CHT-'+Date.now(),channelId:currentChannel.id,from:CURRENT_USER.id,text,mentions:extractMentions(text),reactions:{},pinned:false,threadCount:0,ts:Date.now(),time:new Date().toTimeString().slice(0,5)}); saveChats(chats); input.value=''; renderChatMessages(); renderChannelList(); } function extractMentions(text){ const ids=[]; STAFF.forEach(s=>{if(text.includes('@'+s.name))ids.push(s.id);}); return ids; } function chatInputKeydown(e){ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChatMsg();return;} if(e.key==='@'){showMentionDropdown();} else{document.getElementById('mention-dropdown').classList.remove('open');} simulateTyping(); } function chatInputChange(el){ el.style.height='auto';el.style.height=Math.min(el.scrollHeight,120)+'px'; simulateTyping(); } function simulateTyping(){ clearTimeout(typingTimer); document.getElementById('typing-indicator').textContent=''; typingTimer=setTimeout(()=>{document.getElementById('typing-indicator').textContent='';},2000); } function showMentionDropdown(){ const dd=document.getElementById('mention-dropdown'); dd.innerHTML=STAFF.slice(0,10).map(s=>`
${initials(s.name)}
${escHtml(s.name)}
${escHtml(s.dept)}
`).join(''); dd.classList.add('open'); } function insertMention(name){ const input=document.getElementById('chat-input'); input.value+=name+' '; document.getElementById('mention-dropdown').classList.remove('open'); input.focus(); } function toggleReaction(msgId,emoji){ const chats=getChats(); for(const ch in chats){ const m=chats[ch].find(x=>x.id===msgId); if(m){ if(!m.reactions)m.reactions={}; if(!m.reactions[emoji])m.reactions[emoji]=[]; const idx=m.reactions[emoji].indexOf(CURRENT_USER.id); if(idx>=0)m.reactions[emoji].splice(idx,1);else m.reactions[emoji].push(CURRENT_USER.id); if(!m.reactions[emoji].length)delete m.reactions[emoji]; break; } } saveChats(chats);renderChatMessages(); } function addReactionPrompt(msgId){ const emojis=['👍','❤','😂','🔥','✅','😮','😢','🙏','👏','💯']; const e=prompt('Add reaction: '+emojis.join(' ')); if(e&&emojis.includes(e.trim()))toggleReaction(msgId,e.trim()); } function pinChatMsg(msgId){ const chats=getChats(); for(const ch in chats){const m=chats[ch].find(x=>x.id===msgId);if(m){m.pinned=!m.pinned;break;}} saveChats(chats);showChatPanel(currentChannel);renderChatMessages(); } function openThread(msgId){ threadParentId=msgId; const chats=getChats(); let parent=null; for(const ch in chats){const m=chats[ch].find(x=>x.id===msgId);if(m){parent=m;break;}} if(!parent)return; document.getElementById('thread-panel').classList.add('open'); const s=staffById(parent.from); document.getElementById('thread-msgs').innerHTML=`
${escHtml(s.name)}${fmtTs(parent.ts)}
${escHtml(parent.text)}
`; } function toggleThreadPanel(){document.getElementById('thread-panel').classList.toggle('open');} function sendThreadReply(){ const input=document.getElementById('thread-input'); const text=input.value.trim(); if(!text||!threadParentId)return; input.value=''; const el=document.getElementById('thread-msgs'); const s=staffById(CURRENT_USER.id); el.innerHTML+=`
${initials(s.name)}
${escHtml(s.name)}just now
${escHtml(text)}
`; el.scrollTop=el.scrollHeight; } function toggleEmojiPicker(){ emojiPickerOpen=!emojiPickerOpen; const ep=document.getElementById('emoji-picker'); ep.classList.toggle('open',emojiPickerOpen); if(emojiPickerOpen&&!ep.innerHTML){ const emojis=['😀','😂','😍','🤔','😎','😢','🔥','❤','👍','👏','✅','🙏','🎉','💯','😮','🤝','📌','💪','🌟','☕','🇺🇬','📊','📋','🏆','⚠️','📦','🚚','💰','📞','🗓']; ep.innerHTML='
'+emojis.map(e=>``).join('')+'
'; } } function insertEmoji(e){ const input=document.getElementById('chat-input'); input.value+=e;input.focus(); document.getElementById('emoji-picker').classList.remove('open'); emojiPickerOpen=false; } // ── ANNOUNCEMENTS ── function getAnns(){return getData('erp_announcements',ANN_SEED);} function saveAnns(a){saveData('erp_announcements',a.slice(0,100));} function setAnnCat(cat,el){ annCategory=cat; document.querySelectorAll('.ann-cat').forEach(c=>c.classList.remove('active')); if(el)el.classList.add('active'); renderAnnouncements(); } function priorityIcon(p){return p==='urgent'?'🔴':p==='important'?'🟡':'🟢';} function renderAnnouncements(){ let anns=getAnns(); if(annCategory!=='all') anns=anns.filter(a=>a.category===annCategory||(annCategory==='Urgent'&&a.priority==='urgent')); anns.sort((a,b)=>(b.pinned?1:0)-(a.pinned?1:0)||b.ts-a.ts); const feed=document.getElementById('ann-feed'); if(!anns.length){feed.innerHTML='
📢
No announcements
';return;} const isCEO=CURRENT_USER.id==='CEO-001'||CURRENT_USER.id==='PM-001'; document.getElementById('ann-compose-fab').style.display=isCEO?'flex':'none'; feed.innerHTML=anns.map(a=>{ const author=staffById(a.author); const readPct=Math.round(((a.readBy||[]).length/Math.max(STAFF.length,1))*100); const isRead=(a.readBy||[]).includes(CURRENT_USER.id); return `
${priorityIcon(a.priority)}
${escHtml(a.title)}
${escHtml(a.category)} ${a.pinned?'📌 Pinned':''}
${escHtml(a.body).replace(/\n/g,'
')}
${initials(author.name)}
${escHtml(author.name)} • ${escHtml(author.dept)}
${fmtTsFull(a.ts)}
Read by ${(a.readBy||[]).length} of ${STAFF.length} staff (${readPct}%)
`; }).join(''); } function expandAnn(id,btn){ document.getElementById('ann-body-'+id).classList.toggle('expanded'); btn.textContent=btn.textContent==='Read more'?'Show less':'Read more'; } function markAnnRead(id,btn){ const anns=getAnns(); const a=anns.find(x=>x.id===id); if(a){if(!a.readBy)a.readBy=[];if(!a.readBy.includes(CURRENT_USER.id))a.readBy.push(CURRENT_USER.id);saveAnns(anns);} btn.classList.add('done');btn.textContent='✓ Read'; renderAnnouncements(); } function openAnnCompose(){document.getElementById('ann-modal').classList.add('open');} function closeAnnModal(){document.getElementById('ann-modal').classList.remove('open');} function postAnnouncement(){ const title=document.getElementById('ann-title-input').value.trim(); if(!title){alert('Please enter a title.');return;} const anns=getAnns(); anns.unshift({id:'ANN-'+Date.now(),title,category:document.getElementById('ann-cat-input').value,priority:document.getElementById('ann-priority-input').value,body:document.getElementById('ann-body-input').value,author:CURRENT_USER.id,pinned:document.getElementById('ann-pin-input').checked,targetDepts:[document.getElementById('ann-target-input').value],readBy:[CURRENT_USER.id],comments:[],scheduledAt:null,ts:Date.now(),date:new Date().toISOString().slice(0,10)}); saveAnns(anns);closeAnnModal();renderAnnouncements(); } // ── NOTIFICATIONS ── function getNotifs(){return getData('erp_notifications',NOTIF_SEED);} function saveNotifs(n){saveData('erp_notifications',n.slice(0,200));} function typeIcon(type){const map={low_stock:'⚠️',leave:'🗓',invoice_due:'💰',ncr:'🔴',production:'✅',procurement:'🛒',system:'⚙️',new_staff:'👤',infrastructure:'🔧',budget_overspend:'💸'};return map[type]||'🔔';} function setNotifFilter(mod,el){ notifFilter=mod; document.querySelectorAll('.nf-btn').forEach(b=>b.classList.remove('active')); if(el)el.classList.add('active'); renderNotifications(); } function renderNotifications(){ let notifs=getNotifs(); if(notifFilter==='unread') notifs=notifs.filter(n=>!n.read); else if(notifFilter!=='all') notifs=notifs.filter(n=>n.module===notifFilter); const panel=document.getElementById('notif-panel'); const unread=notifs.filter(n=>!n.read).length; const tb=document.getElementById('notif-tab-badge'); const nb=document.getElementById('tb-notif-badge'); if(tb){tb.textContent=unread;tb.classList.toggle('hidden',!unread);} if(nb){nb.textContent=unread;nb.classList.toggle('hidden',!unread);} if(!notifs.length){panel.innerHTML='
🔔
No notifications
';return;} panel.innerHTML=notifs.map(n=>`
${typeIcon(n.type)}
${escHtml(n.title)}
${escHtml(n.body)}
`).join(''); } function openNotif(id){ const notifs=getNotifs(); const n=notifs.find(x=>x.id===id); if(n){n.read=true;saveNotifs(notifs);renderNotifications();} if(n&&n.link)window.location.href=n.link; } function dismissNotif(e,id){ e.stopPropagation(); const notifs=getNotifs(); const n=notifs.find(x=>x.id===id); if(n){n.read=true;saveNotifs(notifs);renderNotifications();} } function markAllNotifsRead(){ const notifs=getNotifs(); notifs.forEach(n=>n.read=true); saveNotifs(notifs); localStorage.setItem('erp_mail_unread',0); renderNotifications(); } // ── SETTINGS ── function renderSettings(){ const s=getData('erp_mail_settings',SETTINGS_DEFAULT); document.getElementById('sig-textarea').value=s.signature||''; document.getElementById('ooo-toggle').checked=s.outOfOffice&&s.outOfOffice.enabled; document.getElementById('ooo-message').value=s.outOfOffice&&s.outOfOffice.message||''; document.getElementById('ooo-from').value=s.outOfOffice&&s.outOfOffice.from||''; document.getElementById('ooo-to').value=s.outOfOffice&&s.outOfOffice.to||''; document.getElementById('display-name-input').value=s.displayName||CURRENT_USER.name; document.getElementById('chat-sounds-toggle').checked=s.chatSounds!==false; document.getElementById('dark-mode-toggle').checked=s.darkMode||false; // notification prefs const prefs=s.notifPrefs||SETTINGS_DEFAULT.notifPrefs; const types=['low_stock','leave','invoice_due','ncr','production','procurement','system','new_staff','infrastructure']; const labels={low_stock:'Low Stock Alerts',leave:'Leave Requests',invoice_due:'Invoice Due Alerts',ncr:'Quality NCR',production:'Production Completions',procurement:'Procurement Approvals',system:'System Messages',new_staff:'New Staff Onboarded',infrastructure:'Infrastructure Snags'}; document.getElementById('notif-prefs-body').innerHTML=types.map(t=>`
`).join(''); // how to const howtos=[ {t:'Compose and send an internal mail',d:'Click the "Compose" button in the sidebar or press C. Fill in the To field (start typing a name to search staff), enter a Subject, write your message body, and click Send.'}, {t:'Reply and forward messages',d:'Open a message and use the Reply, Reply All, or Forward buttons in the toolbar. A quick reply pane is also available at the bottom of the reading pane.'}, {t:'Use labels and folders',d:'Assign a label using the Label dropdown in the reading pane. Use the sidebar folders to filter mail. Labels include: Important, Action Required, Finance, HR, Urgent.'}, {t:'Join and message in chat channels',d:'Click the Chat tab, then select a channel from the list. Type your message in the input bar and press Enter (or click the send button) to post.'}, {t:'Start a Direct Message',d:'In the Chat tab, scroll down to "Direct Messages" and click any staff member\'s name to open a 1:1 DM conversation.'}, {t:'Use @mentions in chat',d:'Type @ in the chat input bar. A dropdown of staff names will appear. Click a name to insert the mention, which will be highlighted in blue in your message.'}, {t:'Post an announcement',d:'Click the Announcements tab, then click the + button (bottom right). Fill in the title, category, priority, and body. Toggle "Pin to top" for urgent announcements. Only CEO/PM can post.'}, {t:'Manage notifications',d:'Click the Notifications tab to see all system alerts. Use the filter buttons to filter by module or unread. Click x to mark individual notifications as read, or use Mark All Read.'}, {t:'Set your out-of-office reply',d:'Go to Settings > Out of Office. Toggle it on, enter your message and date range. All incoming mail will show your out-of-office status to senders.'}, {t:'Use Quick Reply Templates',d:'When composing a message, click the Templates button. Select a pre-written template to insert it into the body, or click Save current as template to save your own.'}, ]; document.getElementById('howto-body').innerHTML=howtos.map((h,i)=>`
${i+1}
${escHtml(h.t)}${escHtml(h.d)}
`).join(''); } function saveSetting(key){ const s=getData('erp_mail_settings',SETTINGS_DEFAULT); if(key==='signature') s.signature=document.getElementById('sig-textarea').value; if(key==='ooo'){s.outOfOffice={enabled:document.getElementById('ooo-toggle').checked,message:document.getElementById('ooo-message').value,from:document.getElementById('ooo-from').value,to:document.getElementById('ooo-to').value};} if(key==='displayName') s.displayName=document.getElementById('display-name-input').value; if(key==='chatSounds') s.chatSounds=document.getElementById('chat-sounds-toggle').checked; saveData('erp_mail_settings',s); alert('Settings saved.'); } function saveNotifPref(type,val){ const s=getData('erp_mail_settings',SETTINGS_DEFAULT); if(!s.notifPrefs)s.notifPrefs={}; s.notifPrefs[type]=val; saveData('erp_mail_settings',s); } function toggleDarkMode(){ const on=document.getElementById('dark-mode-toggle').checked; document.body.classList.toggle('dark-mode',on); const s=getData('erp_mail_settings',SETTINGS_DEFAULT); s.darkMode=on;saveData('erp_mail_settings',s); } function scrollSettings(id){ const el=document.getElementById('sect-'+id); if(el)el.scrollIntoView({behavior:'smooth'}); } // ── Badges refresh ── function refreshBadges(){ updateMailBadges(); const notifs=getNotifs(); const unread=notifs.filter(n=>!n.read).length; const tb=document.getElementById('notif-tab-badge'); const nb=document.getElementById('tb-notif-badge'); if(tb){tb.textContent=unread;tb.classList.toggle('hidden',!unread);} if(nb){nb.textContent=unread;nb.classList.toggle('hidden',!unread);} } // ── Keyboard shortcuts ── document.addEventListener('keydown',e=>{ if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.contentEditable==='true')return; if(e.key==='c'||e.key==='C'){openCompose();} if(e.key==='r'||e.key==='R'){replyMsg();} if(e.key==='Escape'){closeCompose();document.getElementById('ann-modal').classList.remove('open');document.getElementById('emoji-picker').classList.remove('open');document.getElementById('mention-dropdown').classList.remove('open');} }); // ── Init ── async function init(){ // seed localStorage fallbacks on first load if(!localStorage.getItem('erp_messages'))saveData('erp_messages',MAIL_SEED); if(!localStorage.getItem('erp_channels'))saveData('erp_channels',CHANNELS_SEED); if(!localStorage.getItem('erp_chats'))saveData('erp_chats',CHATS_SEED); if(!localStorage.getItem('erp_announcements'))saveData('erp_announcements',ANN_SEED); if(!localStorage.getItem('erp_notifications'))saveData('erp_notifications',NOTIF_SEED); // seed unread count from seed data on first load if(!localStorage.getItem('erp_mail_unread')){ const unreadMail=MAIL_SEED.filter(m=>!m.read&&(m.folder==='inbox')).length; const unreadNotifs=NOTIF_SEED.filter(n=>!n.read).length; localStorage.setItem('erp_mail_unread', unreadMail+unreadNotifs); } // dark mode const s=getData('erp_mail_settings',SETTINGS_DEFAULT); if(s.darkMode)document.body.classList.add('dark-mode'); // render with localStorage data immediately so the UI is not blank renderTopbarUser(); renderMailList(); renderChannelList(); renderNotifications(); refreshBadges(); switchTab('mail'); setInterval(refreshBadges,10000); // then hydrate messages from API in the background try{ if(window.api&&api.isOnline()!==false){ // fetch unread count badge from server const unreadRes=await api.mail.unread(); if(unreadRes&&typeof unreadRes.count==='number'){ localStorage.setItem('erp_mail_unread',unreadRes.count); refreshBadges(); } // fetch full inbox + sent and merge into localStorage cache await loadMsgsFromApi(); renderMailList(); updateMailBadges(); } }catch(e){} } init();