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+') Add'+(eq.length===0?'
No equipment documented yet
':eq.map(e=>'
'+(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+') Log'+(int.length===0?'
None logged
':int.map(i=>'
'+(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')+'
Edit Delete
';}
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?'
':'')+'
Archive Delete
';}
/* ═══════════ 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
$'+(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?' Up ':'')+''+(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 = '';
}
});
}
Hi! I'm the AES AI assistant. Ask me about shops, work orders, files, or anything in the CRM.
Recording... release to send