/* Group configuration, explicit availability and versioned manual reassignment. */
(function(root){
function create(options){
const el = id => document.getElementById(id);
const escape = value => String(value == null ? '' : value).replace(/[&<>"']/g,
character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character]));
let snapshot = null, busy = false, beating = false, generation = 0, selectedVersion = null;
const controls = ['routing-presence-toggle','routing-refresh','routing-create-group','routing-save-member','routing-save-assignment'];
function message(text, error){
['routing-status','routing-management-status'].forEach(id=>{
el(id).textContent = text || '';
el(id).classList.toggle('is-error', Boolean(error));
});
}
function setBusy(value){
busy = value;
controls.forEach(id => { el(id).disabled = value || !snapshot; });
}
function fill(id, items, placeholder){
const input = el(id), previous = input.value;
input.innerHTML = (placeholder ? '' : '') +
items.map(item => '').join('');
input.value = items.some(item => String(item.value) === previous) ? previous :
(placeholder ? '' : items.length ? String(items[0].value) : '');
}
function renderPresence(online){
el('routing-presence-text').textContent = online ? '在线,参与自动分配' : '离线,不自动接待';
el('routing-presence-dot').dataset.online = String(online);
el('routing-presence-toggle').textContent = online ? '切换离线' : '上线接待';
el('routing-presence-toggle').setAttribute('aria-pressed', String(online));
}
function targets(preserveUnavailable){
if(!snapshot) return;
const group = Number(el('routing-target-group').value);
const previous = el('routing-target-owner').value;
const items = snapshot.agents.filter(agent => agent.online && agent.group_id === group)
.map(agent => ({value:agent.username,label:(agent.name || agent.username)+' · '+agent.active_count+' 个会话'}));
if(preserveUnavailable && previous && !items.some(item=>item.value === previous)){
items.push({value:previous,label:previous+' · 状态已改变,请重新选择'});
}
fill('routing-target-owner',items,'转入队列,等待手动接入');
}
function selectSession(){
const session = snapshot && snapshot.sessions.find(item => String(item.id) === el('routing-session').value);
selectedVersion = session ? session.version : null;
if(session){
el('routing-target-group').value = String(session.group_id);
targets();
el('routing-target-owner').value = session.owner_username || '';
}
}
function render(data){
snapshot = data;
renderPresence(Boolean(data.presence.online));
const mine = data.agents.find(agent => agent.username === options.username);
el('routing-group-label').textContent = mine ? mine.group_name : '综合客服';
el('routing-members').innerHTML = data.agents.length ? data.agents.map(agent =>
'
'+escape(agent.name || agent.username)+' '+escape(agent.username)+''+
''+escape(agent.group_name)+' · '+(agent.online ? '在线' : '离线')+' · '+agent.active_count+' 个会话').join('') : '暂无客服账号';
const groups = data.groups.map(group => ({value:group.id,label:group.name}));
fill('routing-member-group', groups);
fill('routing-target-group', groups);
fill('routing-member', data.agents.map(agent => ({value:agent.username,label:(agent.name || agent.username)+'('+agent.username+')'})), '选择客服');
fill('routing-session', data.sessions.map(session => ({value:session.id,
label:'#'+session.id+' · '+session.group_name+' · '+(session.owner_name || session.owner_username || '排队中')})), '选择会话');
if(!el('routing-session').value) selectedVersion = null;
targets(true);
const actions = {auto_assign:'自动分配',claim:'手动接入',reassign:'手动改派',queue:'转入分组队列',transfer:'转回队列',close:'结束会话'};
el('routing-events').innerHTML = data.events.length ? data.events.map(event =>
'#'+event.session_id+' · '+escape(actions[event.action] || event.action)+''+
escape(event.group_name)+' → '+escape(event.owner_username || '等待分配')+''+escape(event.actor === 'system' ? '系统' : event.actor === 'customer' ? '客户' : event.actor)+' · '+escape(event.ctime)+'').join('') : '暂无分配记录';
setBusy(busy);
}
async function refresh(afterWrite){
if(busy && afterWrite !== true) return;
const current = ++generation;
try{
const data = await options.request('/api/admin/routing');
if(current !== generation) return;
if(!data || !Array.isArray(data.groups) || !Array.isArray(data.agents) ||
!Array.isArray(data.sessions) || !Array.isArray(data.events) || !data.presence) throw new Error('Invalid routing data');
render(data);
}catch(error){
if(current === generation) message('分配状态加载失败,已保留现有信息;请点击刷新重试。', true);
}
}
async function heartbeat(){
if(busy || beating) return;
beating = true;
const current = generation;
try{
const data = await options.request('/api/admin/routing/presence', {method:'POST',
headers:{'Content-Type':'application/json'},body:'{}'});
if(current !== generation) return;
if(snapshot) snapshot.presence.online = Boolean(data.online);
renderPresence(Boolean(data.online));
await refresh();
}catch(error){
if(current === generation){
el('routing-presence-text').textContent = '接待状态待确认';
message('接待状态同步失败,断线超过 90 秒将停止自动分配;请刷新核对。', true);
}
}finally{ beating = false; }
}
async function setPresence(){
if(busy || !snapshot) return;
++generation;
setBusy(true);
const online = !snapshot.presence.online;
try{
const data = await options.request('/api/admin/routing/presence', {method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify({online})});
snapshot.presence.online = Boolean(data.online);
renderPresence(Boolean(data.online));
message(data.online ? '已上线,空闲时自动接入一个新客户;多接请手动领取。' : '已离线,已接待的会话继续保留。');
if(options.changed) options.changed();
}catch(error){ message('状态更新未确认成功,请点击刷新核对后重试。', true); }
finally{ setBusy(false); }
}
async function write(path, data, success){
if(busy || !snapshot) return false;
++generation;
setBusy(true);
try{
await options.request('/api/admin/routing/'+path, {method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
message(success);
if(options.changed) options.changed();
return true;
}catch(error){
message(error.userMessage || '操作未确认成功,请刷新核对后重试。', true);
return false;
}finally{ await refresh(true); setBusy(false); }
}
async function addGroup(){
const name = el('routing-group-name').value.trim();
if(!name || name.length > 40){ message('请输入 1–40 字的分组名称。', true); return; }
if(await write('groups',{name},'分组已创建,可将客服加入此组。')){
if(el('routing-group-name').value.trim() === name) el('routing-group-name').value = '';
}
}
async function moveMember(){
const username = el('routing-member').value, group_id = Number(el('routing-member-group').value);
if(!username){ message('请先选择客服。', true); return; }
await write('members',{username,group_id},'客服分组已更新,现有会话负责人不变。');
}
async function assign(){
const sid = Number(el('routing-session').value), group_id = Number(el('routing-target-group').value);
if(!sid || selectedVersion === null){ message('请先选择会话。', true); return; }
const version = selectedVersion, username = el('routing-target-owner').value || null;
await write('assign',{sid,group_id,version,username},'会话分配已更新。');
el('routing-session').value = ''; selectedVersion = null;
}
el('routing-presence-toggle').addEventListener('click',setPresence);
el('routing-refresh').addEventListener('click',refresh);
el('routing-session').addEventListener('change',selectSession);
el('routing-target-group').addEventListener('change',()=>targets(false));
el('routing-member').addEventListener('change',()=>{
const member = snapshot && snapshot.agents.find(agent => agent.username === el('routing-member').value);
if(member) el('routing-member-group').value = String(member.group_id);
});
[['routing-group-form',addGroup],['routing-member-form',moveMember],['routing-assign-form',assign]].forEach(([id,handler])=>{
el(id).addEventListener('submit',event=>{event.preventDefault();return handler();});
});
setBusy(false);
return {refresh,heartbeat,setPresence,assign};
}
root.AdminRouting = {create};
})(window);