// A private, read-only order panel. It never replaces chat drafts or sends AI requests. var CustomerOrders = { create(options){ const node = name => document.getElementById('customer-orders'+(name ? '-'+name : '')); const root=node(''), toggle=node('toggle'), body=node('body'), list=node('list'); const detail=node('detail'), message=node('message'), more=node('more'); const refresh=node('refresh'), back=node('back'), retry=node('retry'); const overview=node('summary'), progress=node('progress'); const groups=['all','processing','completed','pending','cancelled','other']; const filters=groups.map(function(group){ const button=document.createElement('button'); button.type='button';button.className='customer-order-filter';button.dataset.filter=group; button.onclick=function(){ if(activeFilter === group && selected === null && !errorKey) return; activeFilter=group;loadList(false); }; overview.appendChild(button); return button; }); let summary=null, activeFilter='all'; let user=null, rows=[], cursor=null, card=null, selected=null; let expanded=false, loading=false, errorKey='', retryAction=null, version=0, controller=null, listScroll=0; function cancel(){ version++; if(controller) controller.abort(); controller=null; loading=false; } function render(){ root.hidden=!user; toggle.textContent=options.text('myOrders')+(summary ? ' · '+summary.total : ''); toggle.setAttribute('aria-expanded',String(expanded)); body.hidden=!expanded; refresh.hidden=!expanded; refresh.textContent=options.text('ordersRefresh'); back.textContent=options.text('ordersBack'); back.hidden=selected === null; list.hidden=selected !== null; detail.hidden=selected === null; overview.hidden=!summary || selected !== null; overview.setAttribute('aria-label',options.text('ordersOverview')); filters.forEach(function(button){ const group=button.dataset.filter; button.textContent=''; button.setAttribute('aria-pressed',String(activeFilter === group)); if(!summary) return; const label=document.createElement('span');label.textContent=options.text('ordersGroup_'+group); const count=document.createElement('strong');count.textContent=String(summary[group === 'all' ? 'total' : group]); button.append(label,count); }); progress.hidden=!summary || selected !== null || !rows.length || loading; progress.textContent=summary ? options.text('ordersProgress').replace('{loaded}',rows.length) .replace('{total}',summary[activeFilter === 'all' ? 'total' : activeFilter]) : ''; more.hidden=selected !== null || !cursor; more.disabled=loading; more.textContent=options.text('ordersMore'); retry.hidden=!errorKey || !retryAction; retry.textContent=options.text('ordersRetry'); message.textContent=errorKey ? options.text(errorKey) : loading ? options.text('ordersLoading') : !rows.length ? options.text(activeFilter === 'all' ? 'ordersEmpty' : 'ordersFilterEmpty') : ''; message.hidden=!message.textContent; body.setAttribute('aria-busy',String(loading)); } function renderRows(){ list.textContent=''; rows.forEach(function(row){ const button=document.createElement('button'); button.type='button';button.className='customer-order-row'; button.dataset.orderId=String(row.id); const identity=document.createElement('span');identity.className='customer-order-identity'; const label=document.createElement('span');label.className='customer-order-label'; label.textContent=options.text(row.inbound_code ? 'order_inbound' : 'orderNo'); const number=document.createElement('strong');number.textContent=row.inbound_code || row.order_no; identity.append(label,number); const route=document.createElement('span');route.className='customer-order-route'; route.textContent=[row.country_code,row.warehouse_code, row.transport_mode && (options.text('orderMode_'+row.transport_mode) || row.transport_mode)].filter(Boolean).join(' · '); const status=document.createElement('span');status.className='customer-order-status'; status.textContent=options.text('orderStatus_'+row.status) || row.status; const action=document.createElement('span');action.className='customer-order-action'; action.textContent=options.text('ordersTrack')+' →'; button.append(identity,route,status,action); button.onclick=()=>loadDetail(row.id); list.appendChild(button); }); } function renderCard(){ detail.textContent=''; if(card) options.renderCard(detail,card); } async function request(url,apply,retryRequest){ cancel(); const token=version; const active=new AbortController();controller=active; loading=true;errorKey='';retryAction=retryRequest;render(); let timedOut=false, receivedResponse=false; const timer=setTimeout(()=>{timedOut=true;active.abort();},10000); try{ const response=await options.fetch(url,{signal:active.signal,cache:'no-store'}); if(token !== version) return; if(response.status === 401){ setUser(null); if(options.onUnauthorized) options.onUnauthorized(); return; } receivedResponse=true; if(!response.ok){ const errors={400:'ordersInvalidRequest',403:'ordersForbidden',408:'ordersTimeout', 429:'ordersRateLimited',504:'ordersTimeout'}; errorKey=response.status === 404 && /\/orders\/\d+$/.test(url) ? 'ordersUnavailable' : errors[response.status] || (response.status >= 500 ? 'ordersServerError' : 'ordersFailed'); if([400,403,404].includes(response.status)) retryAction=null; return; } const result=await response.json(); if(token !== version) return; apply(result); }catch(error){ if(token === version) errorKey=timedOut ? 'ordersTimeout' : receivedResponse ? 'ordersInvalidResponse' : 'ordersNetworkError'; }finally{ clearTimeout(timer); if(token === version){ controller=null;loading=false;render(); } } } function loadList(append, preserveView){ if(!user || (append && (loading || !cursor))) return; if(!append && !preserveView){ selected=null;card=null;rows=[];cursor=null;body.scrollTop=0;renderCard();renderRows(); } const params=[]; if(activeFilter !== 'all') params.push('status='+activeFilter); if(append) params.push('before='+cursor); const suffix=params.length ? '?'+params.join('&') : ''; return request('/api/customer/orders'+suffix,function(result){ if(!Array.isArray(result.orders)) throw new Error('invalid order list'); // Older responses may lack an overview; never infer totals from one page. summary=result.summary || null; rows=append ? rows.concat(result.orders) : result.orders; cursor=result.next_before || null; renderRows(); },()=>loadList(append,preserveView)); } function loadDetail(id){ if(!user) return; if(selected === null) listScroll=body.scrollTop; selected=id;card=null;renderCard(); const pending=request('/api/customer/orders/'+id,function(result){ if(!result.card) throw new Error('invalid order detail'); card=result.card;renderCard(); },()=>loadDetail(id)); back.focus({preventScroll:true}); body.scrollTop=0; return pending; } function setUser(next){ if(user && next && user.id === next.id) return; cancel();user=next;rows=[];cursor=null;card=null;selected=null;errorKey='';retryAction=null; summary=null;activeFilter='all'; expanded=Boolean(user);renderRows();renderCard();render(); if(user) loadList(false); } toggle.onclick=function(){ expanded=!expanded;render(); }; refresh.onclick=()=>loadList(false); more.onclick=()=>loadList(true); retry.onclick=()=>retryAction && retryAction(); back.onclick=function(){ const id=selected; cancel();selected=null;card=null;errorKey='';renderCard();render(); const button=Array.from(list.children).find(item=>item.dataset.orderId === String(id)); body.scrollTop=listScroll; if(button) button.focus({preventScroll:true}); }; render(); return {setUser:setUser,refresh:function(){ if(user && !loading) return loadList(false,true); },render:function(){renderRows();renderCard();render();}}; } };