Map
WOs
Shops
Stats
Files

AES Unified

Work Orders

Shops

Dashboard

Files

x.id===id);if(!w)return;document.getElementById('deskPanelTitle').textContent=w.customer||w.shop_name||'WO #'+w.id;document.getElementById('deskPanelBody').innerHTML=woHTML(w);document.getElementById('desktopPanel').classList.remove('hidden');setTimeout(()=>map.invalidateSize(),300)}else showWODetail(id)} async function showShopDesktop(id){if(isDesktop){currentShopId=id;const s=shops.find(x=>x.id===id);if(!s)return;document.getElementById('deskPanelTitle').textContent=s.name;const body=document.getElementById('deskPanelBody');body.innerHTML='

Loading...
';document.getElementById('desktopPanel').classList.remove('hidden');try{const r=await fetch(API+'/shops/'+id);const d=await r.json();const eq=d.equipment||[],int=d.interactions||[],wo=d.work_orders||[],dist=distFromHome(d.lat,d.lng);body.innerHTML=shopHTML(d,eq,int,wo,dist)}catch(e){body.innerHTML='
Error
'}setTimeout(()=>map.invalidateSize(),300)}else showShopDetail(id)} function shopHTML(d,eq,int,wo,dist){return '
Address'+(d.address||'-')+(d.city?', '+d.city:'')+'
Phone'+(d.phone||'-')+'
Contact'+(d.contact_name||'-')+(d.contact_title?' ('+d.contact_title+')':'')+'
Type'+(d.shop_type||'prospect')+'
Bays'+(d.bay_count||'-')+'
Employees'+(d.employee_count||'-')+'
Est. Revenue'+(d.estimated_annual_revenue?'$'+Number(d.estimated_annual_revenue).toLocaleString()+'/yr':'-')+'
'+(dist?'
Distance'+dist+' mi from home
':'')+'

Equipment ('+eq.length+')

'+(eq.length===0?'

No equipment documented yet
':eq.map(e=>'
'+esc(e.make)+' '+esc(e.model)+''+(e.documented_on_site?' Doc':' On site')+'
'+(e.equipment_type?''+e.equipment_type+'':'')+(e.year?''+e.year+'':'')+(e.serial_number?'S/N: '+esc(e.serial_number)+'':'')+(e.condition?''+e.condition+'':'')+(e.age?''+e.age+' yrs':'')+'
').join(''))+'

Interactions ('+int.length+')

'+(int.length===0?'

None logged
':int.map(i=>'
'+i.type+''+(i.date||i.created_at?new Date(i.date||i.created_at).toLocaleDateString():'')+'
'+(i.notes||'')+'
'+(i.outcome?'
'+i.outcome+'
':'')+(i.next_action?'
Next: '+i.next_action+(i.follow_up_date?' by '+new Date(i.follow_up_date).toLocaleDateString():'')+'
':'')+'
').join(''))+'

Work Orders ('+wo.length+')

'+(wo.length===0?'

None
':wo.map(w=>'
'+(w.issue||'Service')+''+(w.priority||'normal')+'
'+(w.technician||'')+' • '+new Date(w.created_at).toLocaleDateString()+'
').join(''))+'

Notes

'+(d.notes||'None')+'

';} function woHTML(w){return '

Work Order

Customer'+esc(w.customer||w.shop_name||'-')+'
Tech'+(w.technician||'-')+'
Priority'+(w.priority||'normal')+'
Status'+(w.status||'-')+'
Date'+new Date(w.created_at).toLocaleDateString()+'
'+(w.address?'
Address'+esc(w.address)+'
':'')+'

Issue

'+esc((w.issue||w.issue_description||'No description').substring(0,500))+'
'+(w.equipment?'

Equipment

'+esc(w.equipment)+'
':'')+(w.notes?'

Notes

'+esc(w.notes)+'
':'')+'
';} /* ═══════════ SHOP CRUD ═══════════ */ function showAddShop(){document.getElementById('addShopOverlay').classList.add('open')} function closeAddShop(){document.getElementById('addShopOverlay').classList.remove('open');document.getElementById('addShopForm').reset()} async function submitAddShop(e){e.preventDefault();const d=Object.fromEntries(new FormData(document.getElementById('addShopForm')));try{const r=await fetch(API+'/shops',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)});if(!r.ok)throw Error();showToast('Shop added!');closeAddShop();await loadShops();updateMapView()}catch(e){showToast('Error')}} function editShop(){const s=shops.find(x=>x.id===currentShopId);if(!s)return;editingShopId=currentShopId;document.getElementById('shopEditTitle').textContent='Edit '+s.name;document.getElementById('shopEditBody').innerHTML='
';document.getElementById('shopEditOverlay').classList.add('open')} async function saveShopEdit(){const d=Object.fromEntries(new FormData(document.getElementById('shopEditForm')));try{const r=await fetch(API+'/shops/'+editingShopId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)});if(!r.ok)throw Error();showToast('Saved!');closeShopEdit();await loadShops();if(isDesktop)showShopDesktop(editingShopId);else showShopDetail(editingShopId)}catch(e){showToast('Error')}} function closeShopEdit(){document.getElementById('shopEditOverlay').classList.remove('open');editingShopId=null} async function deleteShop(id){try{await fetch(API+'/shops/'+id,{method:'DELETE'});showToast('Deleted');closeShop();if(isDesktop)closeDesktopPanel();await loadShops();updateMapView()}catch(e){}} async function showShopDetail(id){currentShopId=id;const s=shops.find(x=>x.id===id);if(!s)return;if(window.innerWidth<768)document.querySelectorAll('.tab-content').forEach(p=>p.classList.remove('active'));document.getElementById('shopOverlayTitle').textContent=s.name;const b=document.getElementById('shopOverlayBody');b.innerHTML='

Loading...
';document.getElementById('shopOverlay').classList.add('open');try{const r=await fetch(API+'/shops/'+id);const d=await r.json();const dist=distFromHome(d.lat,d.lng);b.innerHTML=shopHTML(d,d.equipment||[],d.interactions||[],d.work_orders||[],dist)}catch(e){b.innerHTML='
Error
'}} function closeShop(){document.getElementById('shopOverlay').classList.remove('open');currentShopId=null} /* ═══════════ EQUIPMENT / INTERACTIONS ═══════════ */ let eqShopId=null,intShopId=null; function showEqForm(id){eqShopId=id;document.getElementById('eqOverlay').classList.add('open');document.getElementById('eqForm').reset()} function closeEqForm(){document.getElementById('eqOverlay').classList.remove('open');eqShopId=null} async function submitEquipment(){if(!eqShopId)return;const d=Object.fromEntries(new FormData(document.getElementById('eqForm')));try{const r=await fetch(API+'/shops/'+eqShopId+'/equipment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)});if(!r.ok)throw Error();showToast('Added!');closeEqForm();if(isDesktop)showShopDesktop(eqShopId);else showShopDetail(eqShopId)}catch(e){}} async function eqDoc(sid,eid){try{await fetch(API+'/shops/'+sid+'/equipment/'+eid,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({documented_on_site:1})});showToast('Marked!');if(isDesktop)showShopDesktop(sid);else showShopDetail(sid)}catch(e){}} async function eqDel(sid,eid){try{await fetch(API+'/shops/'+sid+'/equipment/'+eid,{method:'DELETE'});showToast('Deleted');if(isDesktop)showShopDesktop(sid);else showShopDetail(sid)}catch(e){}} function showIntForm(id){intShopId=id;document.getElementById('intOverlay').classList.add('open');document.getElementById('intForm').reset()} function closeIntForm(){document.getElementById('intOverlay').classList.remove('open');intShopId=null} async function submitInteraction(){if(!intShopId)return;const d=Object.fromEntries(new FormData(document.getElementById('intForm')));d.date=new Date().toISOString().split('T')[0];try{const r=await fetch(API+'/shops/'+intShopId+'/interactions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)});if(!r.ok)throw Error();showToast('Logged!');closeIntForm();if(isDesktop)showShopDesktop(intShopId);else showShopDetail(intShopId)}catch(e){}} /* ═══════════ WO ACTIONS ═══════════ */ function showWODetail(id){const w=wos.find(x=>x.id===id);if(!w)return;if(window.innerWidth<768)document.querySelectorAll('.tab-content').forEach(p=>p.classList.remove('active'));document.getElementById('woOverlayTitle').textContent=w.customer||w.shop_name||'WO #'+w.id;document.getElementById('woOverlayBody').innerHTML=woHTML(w);document.getElementById('woActions').innerHTML='';document.getElementById('woOverlay').classList.add('open')} function closeWO(){document.getElementById('woOverlay').classList.remove('open');currentWOId=null} function findShopFromWO(id){const w=wos.find(x=>x.id===id);if(!w)return;const n=(w.customer||w.shop_name||'').toLowerCase();const m=shops.find(s=>n.includes(s.name.toLowerCase())||s.name.toLowerCase().includes(n));if(m){closeWO();if(isDesktop)showShopDesktop(m.id);else showShopDetail(m.id)}else showToast('No match')} async function archiveWO(id){try{await fetch(API+'/workorders/'+id+'/archive',{method:'PUT'});showToast('Archived');closeWO();if(isDesktop)closeDesktopPanel();await loadWOs()}catch(e){}} async function deleteWO(id){try{await fetch(API+'/workorders/'+id,{method:'DELETE'});showToast('Deleted');closeWO();if(isDesktop)closeDesktopPanel();await loadWOs()}catch(e){}} /* ═══════════ DASHBOARD ═══════════ */ async function loadDashboard(){loadDashboardInto(document.getElementById('dashboardContent'))} async function loadDashboardInto(el){if(!el)return;el.innerHTML='

Loading...
';try{const t=shops.length,c=shops.filter(s=>s.shop_type==='customer').length,p=shops.filter(s=>s.shop_type==='prospect').length,l=shops.filter(s=>s.shop_type==='cold').length,tr=shops.reduce((s,x)=>s+(+x.estimated_annual_revenue||0),0),pr=shops.filter(s=>s.shop_type==='prospect').reduce((s,x)=>s+(+x.estimated_annual_revenue||0),0);el.innerHTML='
'+t+'
Shops
'+c+' customers • '+p+' prospects • '+l+' cold
'+wos.length+'
WOs
$'+(tr/1000).toFixed(0)+'K
Est. Rev/yr
$'+((tr+pr*0.3)/1000).toFixed(0)+'K
Projected

Top Shops

'+(shops.filter(s=>s.estimated_annual_revenue).sort((a,b)=>(b.estimated_annual_revenue||0)-(a.estimated_annual_revenue||0)).slice(0,5).map(s=>'
'+esc(s.name)+'$'+(+s.estimated_annual_revenue/1000).toFixed(0)+'K
'+(s.city||'')+' • '+s.shop_type+'
').join('')||'
No data
')+'

Cold Prospects ('+l+')

'+(shops.filter(s=>s.shop_type==='cold').slice(0,10).map(s=>'
'+esc(s.name)+'
'+(s.city||'')+'
').join('')||'
None
')+'
'}catch(e){el.innerHTML='
Error
'}} /* ═══════════ FILES ═══════════ */ let fileCurrentPath=''; async function loadFileList(){loadFileListInto(document.getElementById('filesContent'),fileCurrentPath)} async function loadFileListInto(el,p){if(!el)return;if(p===undefined)p=fileCurrentPath;try{const q=p?'?path='+encodeURIComponent(p):'';const r=await fetch(API+'/files'+q);if(!r.ok)throw Error();const d=await r.json();const f=d.entries||[];let h='

'+(p||'Files')+' ('+f.length+')

'+(p?'':'')+'
'+(f.length===0?'

Empty folder
':f.map(function(x){var ic=x.is_dir?'folder':'file';var click=x.is_dir?"fileOpenDir('"+esc(x.path)+"')":"window.open('"+API+"/files/download?path="+encodeURIComponent(x.path)+"','_blank')";return '
'+esc(x.name)+''+(x.size_hr||'')+'
'}).join(''))+'
';el.innerHTML=h}catch(e){el.innerHTML='

Error loading files
'}} function fileOpenDir(p){fileCurrentPath=p;loadFileList();var bp=document.getElementById('deskPanelBody');if(bp&&document.getElementById('desktopPanel')&&!document.getElementById('desktopPanel').classList.contains('hidden')&&activeTab==='files-tab')loadFileListInto(bp,p)} function fileGoUp(){var p=fileCurrentPath;var i=p.lastIndexOf('/');fileCurrentPath=i>0?p.substring(0,i):'';loadFileList();var bp=document.getElementById('deskPanelBody');if(bp&&document.getElementById('desktopPanel')&&!document.getElementById('desktopPanel').classList.contains('hidden')&&activeTab==='files-tab')loadFileListInto(bp,fileCurrentPath)} /* ═══════════ SETTINGS / TOAST / HELPERS ═══════════ */ function toggleSettings(){document.getElementById('settingsOverlay').classList.toggle('open')} function closeSettings(){document.getElementById('settingsOverlay').classList.remove('open')} function loadSettings(){const s=localStorage.getItem('aes_map_style');if(s&&map)setMapStyle(s)} function showToast(m){const e=document.querySelector('.toast');if(e)e.remove();const t=document.createElement('div');t.className='toast';t.textContent=m;document.body.appendChild(t);setTimeout(()=>{t.style.opacity='0';t.style.transition='opacity .3s';setTimeout(()=>t.remove(),300)},2500)} function esc(s){if(!s)return '';return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''')} /* === CHAT === */ const CHAT_API = '/api/hermes'; let chatOpen = false; let chatRecognition = null; let chatRecording = false; let chatFileAttachments = []; let chatSessionId = localStorage.getItem('aes_chat_session') || ''; let chatView = 'chat'; function toggleChat(){ chatOpen = !chatOpen; document.getElementById('chatPanel').classList.toggle('open', chatOpen); document.getElementById('chatBubble').innerHTML = chatOpen ? '' : ''; if(chatOpen) setTimeout(function(){document.getElementById('chatInput').focus()}, 300); if(chatOpen) chatShowView('chat'); } function chatAddMsg(text, role){ var el = document.getElementById('chatMsgs'); var d = document.createElement('div'); d.className = 'msg ' + role; d.textContent = text; el.appendChild(d); el.scrollTop = el.scrollHeight; return d; } function chatShowView(view){ chatView = view; var msgs = document.getElementById('chatMsgs'); var input = document.querySelector('.chat-input'); var recBar = document.getElementById('chatRecBar'); var tabBtns = document.querySelectorAll('.chat-tab-btn'); var backBtn = document.getElementById('chatBackBtn'); for(var i=0; i'; msgs.style.display = 'flex'; input.style.display = 'flex'; recBar.style.display = 'none'; backBtn.style.display = 'none'; } else if(view === 'history'){ msgs.style.display = 'flex'; input.style.display = 'none'; recBar.style.display = 'none'; backBtn.style.display = 'none'; chatLoadHistory(); } } function chatLoadHistory(){ var el = document.getElementById('chatMsgs'); el.innerHTML = '

Loading history...
'; fetch('/api/chat/sessions') .then(function(r){ return r.json(); }) .then(function(sessions){ if(!sessions || !sessions.length){ el.innerHTML = '
No past conversations yet. Start a new chat!
'; return; } el.innerHTML = '
Conversation History
'; for(var i=0; i
' + s.message_count + ' msgs • ' + date + '
'; d.onclick = (function(id){ return function(){ chatViewSession(id); }; })(s.id); el.appendChild(d); } el.scrollTop = 0; }) .catch(function(){ el.innerHTML = '
Error loading history
'; }); } var chatViewSession = function(sessionId){ var msgs = document.getElementById('chatMsgs'); var backBtn = document.getElementById('chatBackBtn'); var input = document.querySelector('.chat-input'); msgs.style.display = 'flex'; input.style.display = 'none'; backBtn.style.display = 'block'; msgs.innerHTML = '

Loading...
'; fetch('/api/chat/sessions/' + sessionId) .then(function(r){ return r.json(); }) .then(function(data){ msgs.innerHTML = ''; var msgsList = data.messages || []; if(msgsList.length === 0){ msgs.innerHTML = '
No messages
'; } else { for(var i=0; i 100){ panel.style.height = (window.visualViewport.height - 20) + 'px'; panel.style.bottom = '0px'; panel.style.top = 'auto'; } else { panel.style.height = ''; panel.style.bottom = ''; panel.style.top = ''; } }); }

AES AI

Hi! I'm the AES AI assistant. Ask me about shops, work orders, files, or anything in the CRM.
Recording... release to send