Files
baiyue-logistics-chat/static/js/customer_orders.js
T

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();}};
}
};