178 lines
9.6 KiB
JavaScript
178 lines
9.6 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');
|
|
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();}};
|
|
}
|
|
};
|