133 lines
6.8 KiB
JavaScript
133 lines
6.8 KiB
JavaScript
// 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');
|
|
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')+(rows.length ? ' · '+rows.length : '');
|
|
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;
|
|
more.hidden=selected !== null || !cursor;
|
|
more.disabled=loading;
|
|
more.textContent=options.text('ordersMore');
|
|
retry.hidden=!errorKey;
|
|
retry.textContent=options.text('ordersRetry');
|
|
message.textContent=errorKey ? options.text(errorKey) : loading ? options.text('ordersLoading')
|
|
: !rows.length ? options.text('ordersEmpty') : '';
|
|
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();
|
|
const timer=setTimeout(()=>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;
|
|
}
|
|
if(!response.ok) throw new Error('orders unavailable');
|
|
const result=await response.json();
|
|
if(token !== version) return;
|
|
apply(result);
|
|
}catch(error){
|
|
if(token === version) errorKey='ordersFailed';
|
|
}finally{
|
|
clearTimeout(timer);
|
|
if(token === version){ controller=null;loading=false;render(); }
|
|
}
|
|
}
|
|
function loadList(append){
|
|
if(!user || (append && (loading || !cursor))) return;
|
|
if(!append){ selected=null;card=null;rows=[];cursor=null;body.scrollTop=0;renderCard();renderRows(); }
|
|
const suffix=append ? '?before='+cursor : '';
|
|
return request('/api/customer/orders'+suffix,function(result){
|
|
if(!Array.isArray(result.orders)) throw new Error('invalid order list');
|
|
rows=append ? rows.concat(result.orders) : result.orders;
|
|
cursor=result.next_before || null;
|
|
renderRows();
|
|
},()=>loadList(append));
|
|
}
|
|
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;
|
|
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,render:function(){renderRows();renderCard();render();}};
|
|
}
|
|
};
|