Admin Dashboard · Staff Access Only
Forgot your password?
get(k){try{return JSON.parse(localStorage.getItem('trs_'+k))||null;}catch{return null;}}, set(k,v){localStorage.setItem('trs_'+k,JSON.stringify(v));}, getArr(k){return this.get(k)||[];}, push(k,item){const a=this.getArr(k);a.push(item);this.set(k,a);return item;}, update(k,id,ch){const a=this.getArr(k);const i=a.findIndex(x=>x.id===id);if(i>-1){a[i]={...a[i],...ch};this.set(k,a);return a[i];}return null;} }; function seedData(){ if(DB.get('seeded')) return; const users=[ {id:'u1',name:'Demo Customer',email:'demo@customer.com',password:'demo123',phone:'(817) 555-0100',company:'Demo Co.',role:'customer',createdAt:'2024-01-15'}, {id:'u2',name:'TRS Admin',email:'admin@trsembroidery.com',password:'TRS2018!',phone:'(682) 241-9553',company:'TRS Embroidery',role:'admin',createdAt:'2018-01-01'} ]; const orders=[ {id:'o1',customerId:'u1',orderNumber:'TRS-2024-0891',service:'Logo Embroidery',brand:'Richardson 112 Cap',quantity:48,status:'shipped',price:432.00,rush:false,artworkUrl:'uploaded',artworkApproved:true,createdAt:'2024-11-01',updatedAt:'2024-11-10',deadline:'2024-11-15',notes:'Navy caps, white logo, left panel',messages:[{from:'trs',text:'Your order is in production! Est. ship date Nov 10.',time:'Nov 5'}],invoiceItems:[{desc:'Richardson 112 Caps (48x)',qty:48,unit:6.50,total:312},{desc:'Logo Digitizing',qty:1,unit:65,total:65},{desc:'Embroidery Setup',qty:1,unit:55,total:55}]}, {id:'o2',customerId:'u1',orderNumber:'TRS-2024-0924',service:'Shirts & Hoodies',brand:'Port Authority Polo',quantity:24,status:'production',price:318.00,rush:false,artworkUrl:'submitted',artworkApproved:true,createdAt:'2024-11-08',updatedAt:'2024-11-12',deadline:'2024-11-20',notes:'Grey polos, left chest logo',messages:[{from:'trs',text:'Artwork approved! Moving to production.',time:'Nov 9'},{from:'customer',text:'Thanks! Please confirm size breakdown: 6S, 8M, 6L, 4XL.',time:'Nov 9'},{from:'trs',text:'Confirmed — sizes noted.',time:'Nov 10'}],invoiceItems:[{desc:'Port Authority Polo Shirts (24x)',qty:24,unit:9.00,total:216},{desc:'Embroidery - Left Chest (24x)',qty:24,unit:4.25,total:102}]}, {id:'o3',customerId:'u1',orderNumber:'TRS-2024-0956',service:'Custom Hats',brand:'Yupoong Snapback',quantity:12,status:'artwork',price:null,rush:true,artworkUrl:'submitted',artworkApproved:false,createdAt:'2024-11-13',updatedAt:'2024-11-13',deadline:'2024-11-18',notes:'Black snapbacks, gold embroidery. RUSH order.',messages:[{from:'trs',text:'Order received! Please upload your artwork to proceed.',time:'Nov 13'}],invoiceItems:[]}, {id:'o4',customerId:'u1',orderNumber:'TRS-2024-0877',service:'Patches',brand:'Merrow Patches',quantity:200,status:'delivered',price:580.00,rush:false,artworkUrl:'uploaded',artworkApproved:true,createdAt:'2024-10-15',updatedAt:'2024-10-28',deadline:'2024-10-30',notes:'3" merrow patches, full color',messages:[{from:'trs',text:'Delivered! Thank you for your order.',time:'Oct 28'}],invoiceItems:[{desc:'Merrow Embroidered Patches (200x)',qty:200,unit:2.40,total:480},{desc:'Digitizing & Setup',qty:1,unit:100,total:100}]} ]; DB.set('users',users);DB.set('orders',orders);DB.set('notifications',[]);DB.set('files',[]);DB.set('seeded',true); } // ═══════════════════════════════════════ // AUTH // ═══════════════════════════════════════ let adminUser=null; let _resetUserId=null; function doLogin(){ const email=document.getElementById('loginEmail').value.trim(); const pass=document.getElementById('loginPass').value; const users=DB.getArr('users'); const user=users.find(u=>u.email===email&&u.password===pass&&u.role==='admin'); if(!user){const e=document.getElementById('authError');e.textContent='Invalid credentials or insufficient access.';e.style.display='block';return;} adminUser=user;DB.set('adminSession',{userId:user.id}); document.getElementById('authScreen').style.display='none'; document.getElementById('appScreen').classList.remove('hidden'); updateBadges();renderDashboard();showView('dashboard'); } /* ── PASSWORD RESET ── */ function showLoginPanel(){ document.getElementById('loginPanel').style.display='block'; document.getElementById('resetPanel1').style.display='none'; document.getElementById('resetPanel2').style.display='none'; document.getElementById('resetSuccess').style.display='none'; document.getElementById('authError').style.display='none'; _resetUserId=null; } function showResetPanel(){ document.getElementById('loginPanel').style.display='none'; document.getElementById('resetPanel1').style.display='block'; document.getElementById('resetPanel2').style.display='none'; document.getElementById('resetSuccess').style.display='none'; document.getElementById('resetEmail').value=''; const e=document.getElementById('resetError1');e.style.display='none'; } function verifyResetEmail(){ const email=document.getElementById('resetEmail').value.trim(); const e=document.getElementById('resetError1'); if(!email){e.textContent='Please enter your email address.';e.style.display='block';return;} const user=DB.getArr('users').find(u=>u.email===email&&u.role==='admin'); if(!user){e.textContent='No admin account found with that email address.';e.style.display='block';return;} _resetUserId=user.id; document.getElementById('resetPanel1').style.display='none'; document.getElementById('resetPanel2').style.display='block'; document.getElementById('newPass1').value=''; document.getElementById('newPass2').value=''; document.getElementById('strengthBar').style.width='0%'; document.getElementById('strengthLabel').textContent=''; document.getElementById('resetError2').style.display='none'; } function checkPasswordStrength(){ const pass=document.getElementById('newPass1').value; const bar=document.getElementById('strengthBar'); const label=document.getElementById('strengthLabel'); if(!pass){bar.style.width='0%';label.textContent='';return;} let score=0; if(pass.length>=8) score++; if(pass.length>=12) score++; if(/[A-Z]/.test(pass)) score++; if(/[0-9]/.test(pass)) score++; if(/[^A-Za-z0-9]/.test(pass)) score++; const levels=[ {w:'20%',c:'#e57373',t:'Too weak'}, {w:'40%',c:'#FFB74D',t:'Weak'}, {w:'60%',c:'#FFD54F',t:'Fair'}, {w:'80%',c:'#81C784',t:'Strong'}, {w:'100%',c:'#4DB6AC',t:'Very strong'} ]; const lvl=levels[Math.min(score,4)]; bar.style.width=lvl.w;bar.style.background=lvl.c; label.textContent=lvl.t;label.style.color=lvl.c; } function saveNewPassword(){ const p1=document.getElementById('newPass1').value; const p2=document.getElementById('newPass2').value; const e=document.getElementById('resetError2'); if(!p1||p1.length<8){e.textContent='Password must be at least 8 characters.';e.style.display='block';return;} if(p1!==p2){e.textContent='Passwords do not match.';e.style.display='block';return;} if(!_resetUserId){e.textContent='Session expired. Please start over.';e.style.display='block';return;} DB.update('users',_resetUserId,{password:p1}); _resetUserId=null; document.getElementById('resetPanel2').style.display='none'; document.getElementById('resetSuccess').style.display='block'; } // ═══════════════════════════════════════ // NAVIGATION // ═══════════════════════════════════════ function showView(v){ document.querySelectorAll('.view').forEach(x=>x.classList.remove('active')); document.querySelectorAll('.nav-item').forEach(x=>x.classList.remove('active')); const el=document.getElementById('view-'+v);if(el) el.classList.add('active'); const ni=document.querySelector('[data-view="'+v+'"]');if(ni) ni.classList.add('active'); const titles={dashboard:'Dashboard',orders:'All Orders',artwork:'Artwork Approvals',quotes:'Quote Requests',customers:'Customers',analytics:'Analytics',settings:'Settings','order-detail':'Order Detail'}; document.getElementById('topbarTitle').textContent=titles[v]||v; if(v==='orders') renderOrders(); if(v==='artwork') renderArtwork(); if(v==='quotes') renderQuotes(); if(v==='customers') renderCustomers(); if(v==='analytics') renderAnalytics(); if(v==='dashboard') renderDashboard(); } // ═══════════════════════════════════════ // DASHBOARD // ═══════════════════════════════════════ function renderDashboard(){ const orders=DB.getArr('orders'); const customers=DB.getArr('users').filter(u=>u.role==='customer'); const revenue=orders.reduce((s,o)=>s+(o.price||0),0); const active=orders.filter(o=>!['delivered'].includes(o.status)).length; document.getElementById('dashStats').innerHTML=`
Total Revenue
$${revenue.toLocaleString('en',{minimumFractionDigits:0})}
↑ All orders
Total Orders
${orders.length}
All time
Active Orders
${active}
In progress
Customers
${customers.length}
Registered
Pending Review
${orders.filter(o=>o.artworkUrl&&!o.artworkApproved).length}
Artwork queue
`; // Revenue chart const months=['Jun','Jul','Aug','Sep','Oct','Nov']; const vals=[2100,3400,2800,4200,3900,revenue]; const max=Math.max(...vals); document.getElementById('revenueChart').innerHTML=vals.map((v,i)=>`
$${(v/1000).toFixed(1)}k
${months[i]}
`).join(''); // Status breakdown const statuses=['quote','confirmed','artwork','production','quality','shipped','delivered']; const labels={quote:'Quote',confirmed:'Confirmed',artwork:'Artwork',production:'Production',quality:'Quality',shipped:'Shipped',delivered:'Delivered'}; document.getElementById('statusBreakdown').innerHTML=statuses.map(s=>{ const c=orders.filter(o=>o.status===s).length; const pct=orders.length?Math.round((c/orders.length)*100):0; return`
${labels[s]}${c}
`; }).join(''); // Recent orders const recent=orders.slice(-5).reverse(); document.getElementById('dashRecentOrders').innerHTML=`${recent.map(o=>{const u=DB.getArr('users').find(x=>x.id===o.customerId);return``;}).join('')}
OrderCustomerStatusTotal
${o.orderNumber}${u?u.name:'—'}${statusBadge(o.status)}${o.price?'$'+o.price.toFixed(0):'—'}
`; // Actions needed const artworkNeeded=orders.filter(o=>o.artworkUrl&&!o.artworkApproved); const quotePending=orders.filter(o=>o.status==='quote'); let actions=''; artworkNeeded.forEach(o=>{actions+=`
🎨 Artwork submitted: ${o.orderNumber}
${o.brand}
`;}); quotePending.forEach(o=>{const u=DB.getArr('users').find(x=>x.id===o.customerId);actions+=`
📋 New quote: ${o.orderNumber}
${u?u.name:''} · ${o.service}
`;}); document.getElementById('dashActions').innerHTML=actions||'
✅ No actions needed.
'; } // ═══════════════════════════════════════ // ORDERS // ═══════════════════════════════════════ function renderOrders(filtered){ let orders=filtered||DB.getArr('orders'); orders=[...orders].reverse(); document.getElementById('ordersTableWrap').innerHTML=` ${orders.map(o=>{const u=DB.getArr('users').find(x=>x.id===o.customerId);return` `;}).join('')}
Order #CustomerServiceBrand / ItemQtyStatusTotalDeadlineActions
${o.orderNumber}${o.rush?'RUSH':''} ${u?u.name:'—'}
${u?u.company||u.email:''}
${o.service} ${o.brand} ${o.quantity} ${o.price?'$'+o.price.toFixed(0):'—'} ${o.deadline||'—'}
`; } function filterOrders(){ const q=document.getElementById('orderSearch').value.toLowerCase(); const s=document.getElementById('statusFilter').value; const users=DB.getArr('users'); let orders=DB.getArr('orders').filter(o=>{ const u=users.find(x=>x.id===o.customerId); const match=!q||(o.orderNumber.toLowerCase().includes(q)||(u&&u.name.toLowerCase().includes(q))||o.service.toLowerCase().includes(q)||o.brand.toLowerCase().includes(q)); const statusMatch=!s||o.status===s; return match&&statusMatch; }); renderOrders(orders); } function updateOrderStatus(id,status){ DB.update('orders',id,{status,updatedAt:new Date().toISOString().split('T')[0]}); updateBadges(); const el=document.querySelector('#view-dashboard'); if(el&&el.classList.contains('active')) renderDashboard(); } function confirmOrder(id){ DB.update('orders',id,{status:'confirmed'}); updateBadges();renderDashboard(); } function approveArtwork(id){ DB.update('orders',id,{artworkApproved:true,status:'production'}); const o=DB.getArr('orders').find(x=>x.id===id); addCustomerNotification(o.customerId,'Artwork Approved!','Your artwork for order '+o.orderNumber+' has been approved. Your order is now in production!','✅'); updateBadges();renderDashboard(); if(document.getElementById('view-artwork').classList.contains('active')) renderArtwork(); } // ═══════════════════════════════════════ // ORDER DETAIL (ADMIN) // ═══════════════════════════════════════ function openOrderDetail(id){ const o=DB.getArr('orders').find(x=>x.id===id); if(!o) return; const u=DB.getArr('users').find(x=>x.id===o.customerId); const invoiceTotal=o.invoiceItems.reduce((s,i)=>s+i.total,0); document.getElementById('orderDetailContent').innerHTML=`

${o.orderNumber} ${o.rush?'RUSH':''}

Placed ${o.createdAt} · ${o.service}
Order Details
Customer
${u?u.name:'—'}
Company
${u&&u.company?u.company:'—'}
Service
${o.service}
Brand / Item
${o.brand}
Quantity
${o.quantity} pcs
Total
${o.price?'$'+o.price.toFixed(2):'Not set'}
Deadline
${o.deadline||'TBD'}
Rush
${o.rush?'Yes':'No'}
${o.notes?`
Customer Notes
${o.notes}
`:''}
Artwork Status ${o.artworkUrl&&!o.artworkApproved?``:''}
${!o.artworkUrl?'
⏳ Waiting for customer to upload artwork.
': o.artworkApproved?'
✅ Artwork approved. Order in production.
': '
🎨 Artwork submitted — awaiting your approval.
'}
Customer Messages
${o.messages.map(m=>`
${m.from==='trs'?'TRS (You)':'Customer: '+(u?u.name:'')}
${m.text}
${m.time}
`).join('')}
Quick Actions
${!o.artworkApproved&&o.artworkUrl?``:''}
Invoice Builder
${o.invoiceItems.length?` ${o.invoiceItems.map(i=>``).join('')}
ITEMQTYTOTAL
${i.desc}${i.qty}$${i.total.toFixed(0)}
$${o.invoiceItems.reduce((s,i)=>s+i.total,0).toFixed(2)}
`:'
No invoice items yet.
'}
Customer Info
${u?`
${u.name}
${u.company||'—'}
${u.email}
${u.phone||'—'}
`:'No customer data.'}
`; showView('order-detail'); } function adminReply(orderId){ const input=document.getElementById('adminMsg_'+orderId); const text=input.value.trim(); if(!text) return; const now=new Date(); const time=now.toLocaleString('en',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); const o=DB.getArr('orders').find(x=>x.id===orderId); DB.update('orders',orderId,{messages:[...o.messages,{from:'trs',text,time}]}); const thread=document.getElementById('msgAdmin_'+orderId); thread.innerHTML+=`
TRS (You)
${text}
${time}
`; thread.scrollTop=thread.scrollHeight; input.value=''; addCustomerNotification(o.customerId,'New Message','TRS Embroidery replied to your order '+o.orderNumber,'💬'); } function editOrderPrice(id){ const o=DB.getArr('orders').find(x=>x.id===id); const price=prompt('Enter order total ($):',o.price||''); if(price!==null&&!isNaN(parseFloat(price))){ DB.update('orders',id,{price:parseFloat(price)}); const el=document.getElementById('orderPriceDisplay'); if(el) el.textContent='$'+parseFloat(price).toFixed(2); } } function deleteOrder(id){ if(!confirm('Delete this order? This cannot be undone.')) return; const orders=DB.getArr('orders').filter(o=>o.id!==id); DB.set('orders',orders); updateBadges();showView('orders'); } function addInvoiceItem(orderId){ const desc=prompt('Item description:');if(!desc) return; const qty=parseInt(prompt('Quantity:','1'));if(isNaN(qty)) return; const unit=parseFloat(prompt('Unit price ($):'));if(isNaN(unit)) return; const o=DB.getArr('orders').find(x=>x.id===orderId); const items=[...o.invoiceItems,{desc,qty,unit,total:qty*unit}]; const total=items.reduce((s,i)=>s+i.total,0); DB.update('orders',orderId,{invoiceItems:items,price:total}); openOrderDetail(orderId); } // ═══════════════════════════════════════ // ARTWORK APPROVALS // ═══════════════════════════════════════ function renderArtwork(){ const orders=DB.getArr('orders').filter(o=>o.artworkUrl&&!o.artworkApproved); const users=DB.getArr('users'); document.getElementById('artworkList').innerHTML=orders.length?orders.map(o=>{ const u=users.find(x=>x.id===o.customerId); return`
${o.orderNumber} ${o.rush?'RUSH':''}
${o.service} · ${o.brand}
Customer: ${u?u.name:'—'} · Submitted: ${o.updatedAt}
${o.notes?`
Notes: ${o.notes}
`:''}
`;}).join(''):'
No artwork pending approval.
'; } function rejectArtwork(id){ const reason=prompt('Reason for rejection (will be sent to customer):','Please resubmit with higher resolution (300 DPI minimum).'); if(!reason) return; const o=DB.getArr('orders').find(x=>x.id===id); DB.update('orders',id,{artworkUrl:null,messages:[...o.messages,{from:'trs',text:'Artwork rejected: '+reason+' Please re-upload.',time:'Just now'}]}); addCustomerNotification(o.customerId,'Artwork Needs Revision','Your artwork for '+o.orderNumber+' needs revision: '+reason,'⚠️'); renderArtwork();updateBadges(); } // ═══════════════════════════════════════ // QUOTES // ═══════════════════════════════════════ function renderQuotes(){ const quotes=DB.getArr('orders').filter(o=>o.status==='quote'); const users=DB.getArr('users'); document.getElementById('quotesWrap').innerHTML=quotes.length?` ${quotes.map(o=>{const u=users.find(x=>x.id===o.customerId);return``;}).join('')}
Quote #CustomerServiceQtyRushDeadlineActions
${o.orderNumber} ${u?u.name:'—'} ${o.service} ${o.quantity} ${o.rush?'RUSH':'No'} ${o.deadline||'—'}
`:'
📋
No pending quote requests.
'; } // ═══════════════════════════════════════ // CUSTOMERS // ═══════════════════════════════════════ function renderCustomers(filtered){ const users=(filtered||DB.getArr('users')).filter(u=>u.role==='customer'); const orders=DB.getArr('orders'); document.getElementById('customersWrap').innerHTML=users.length?` ${users.map(u=>{ const ords=orders.filter(o=>o.customerId===u.id); const spent=ords.reduce((s,o)=>s+(o.price||0),0); return``; }).join('')}
NameCompanyEmailPhoneOrdersTotal SpentMember Since
${u.name}${u.company||'—'}${u.email}${u.phone||'—'}${ords.length}$${spent.toFixed(0)}${u.createdAt}
`:'
👥
No customers registered yet.
'; } function filterCustomers(){ const q=document.getElementById('custSearch').value.toLowerCase(); const users=DB.getArr('users').filter(u=>u.role==='customer'&&(!q||u.name.toLowerCase().includes(q)||u.email.toLowerCase().includes(q)||(u.company&&u.company.toLowerCase().includes(q)))); renderCustomers(users); } // ═══════════════════════════════════════ // ANALYTICS // ═══════════════════════════════════════ function renderAnalytics(){ const orders=DB.getArr('orders'); const customers=DB.getArr('users').filter(u=>u.role==='customer'); const revenue=orders.reduce((s,o)=>s+(o.price||0),0); const avgOrder=orders.filter(o=>o.price).length?revenue/orders.filter(o=>o.price).length:0; document.getElementById('analyticsStats').innerHTML=`
Total Revenue
$${revenue.toLocaleString()}
Total Orders
${orders.length}
Avg Order Value
$${avgOrder.toFixed(0)}
Customers
${customers.length}
`; // Top services const svcCounts={}; orders.forEach(o=>{svcCounts[o.service]=(svcCounts[o.service]||0)+1;}); const sorted=Object.entries(svcCounts).sort((a,b)=>b[1]-a[1]); const maxC=Math.max(...sorted.map(s=>s[1])); document.getElementById('topServices').innerHTML=sorted.map(([s,c])=>`
${s}${c}
`).join(''); // Orders by month chart const months=['Jun','Jul','Aug','Sep','Oct','Nov']; const mvals=[3,5,4,7,6,orders.length]; const mmax=Math.max(...mvals); document.getElementById('ordersChart').innerHTML=mvals.map((v,i)=>`
${v}
${months[i]}
`).join(''); // Customer insights const custOrders=customers.map(u=>({name:u.name,orders:orders.filter(o=>o.customerId===u.id).length,spent:orders.filter(o=>o.customerId===u.id).reduce((s,o)=>s+(o.price||0),0)})).sort((a,b)=>b.spent-a.spent); document.getElementById('customerInsights').innerHTML=`${custOrders.map(c=>``).join('')}
CustomerOrdersTotal SpentAvg Order
${c.name}${c.orders}$${c.spent.toFixed(0)}$${c.orders?Math.round(c.spent/c.orders):0}
`; } // ═══════════════════════════════════════ // CREATE ORDER // ═══════════════════════════════════════ function showCreateOrderModal(){ const customers=DB.getArr('users').filter(u=>u.role==='customer'); document.getElementById('coCustomer').innerHTML=customers.map(u=>``).join(''); document.getElementById('createModal').classList.remove('hidden'); } function createOrder(){ const customerId=document.getElementById('coCustomer').value; const service=document.getElementById('coService').value; const brand=document.getElementById('coBrand').value.trim()||'TBD'; const qty=parseInt(document.getElementById('coQty').value)||1; const price=parseFloat(document.getElementById('coPrice').value)||null; const deadline=document.getElementById('coDeadline').value||null; const status=document.getElementById('coStatus').value; const rush=document.getElementById('coRush').value==='yes'; const notes=document.getElementById('coNotes').value; const o={id:'o'+Date.now(),customerId,orderNumber:'TRS-2024-'+Math.floor(1000+Math.random()*9000),service,brand,quantity:qty,status,price,rush,artworkUrl:null,artworkApproved:false,createdAt:new Date().toISOString().split('T')[0],updatedAt:new Date().toISOString().split('T')[0],deadline,notes,messages:[{from:'trs',text:'Your order has been created by TRS Embroidery.',time:'Just now'}],invoiceItems:[]}; DB.push('orders',o); addCustomerNotification(customerId,'New Order Created','Order '+o.orderNumber+' has been created for you. Service: '+service,'📦'); document.getElementById('createModal').classList.add('hidden'); updateBadges();renderOrders();showView('orders'); } // ═══════════════════════════════════════ // HELPERS // ═══════════════════════════════════════ function statusBadge(s){ const labels={quote:'Quote',confirmed:'Confirmed',artwork:'Artwork Review',production:'In Production',quality:'Quality Check',shipped:'Shipped',delivered:'Delivered'}; return`${labels[s]||s}`; } function addCustomerNotification(userId,title,text,icon){ DB.push('notifications',{id:'n'+Date.now(),userId,title,text,icon,read:false,time:'Just now'}); } function updateBadges(){ const orders=DB.getArr('orders'); document.getElementById('pendingBadge').textContent=orders.filter(o=>!['delivered'].includes(o.status)).length||''; document.getElementById('artworkBadge').textContent=orders.filter(o=>o.artworkUrl&&!o.artworkApproved).length||''; document.getElementById('quotesBadge').textContent=orders.filter(o=>o.status==='quote').length||''; } function saveSettings(){alert('Settings saved!');} function closeModal(){document.getElementById('orderModal').classList.add('hidden');} // ═══════════════════════════════════════ // INIT // ═══════════════════════════════════════ seedData(); const sess=DB.get('adminSession'); if(sess){ const user=DB.getArr('users').find(u=>u.id===sess.userI