/* Offline tickets: server ownership, versioned notes and polling-safe drafts. */ (function(global){ function create(options){ const el = id => document.getElementById(id); const states = {pending:"待处理", processing:"处理中", completed:"已完成"}; let rows = [], selected = null, busy = false, loading = false; let generation = 0, expanded = false, hasMore = false; let activeQuery = "", latestSeen = null, unreadNew = 0, summaryLoading = false, overdueOnly = false; let historyRows = [], historyHasMore = false, historyTicketId = null, historyLoading = false, historyRequest = 0, historyError = ""; const drafts = new Map(); const filter = el("ticket-filter"), note = el("ticket-note"); filter.value = "all"; async function refreshSummary(){ if(summaryLoading) return; summaryLoading = true; try{ const data = await options.request("/api/tickets/summary"+(latestSeen === null ? "" : "?after="+latestSeen)); el("ticket-pending-count").textContent = String(data.pending_count); const overdueCount = Number(data.overdue_count || 0); el("ticket-overdue-count").textContent = String(overdueCount); el("ticket-overdue-toggle").setAttribute("aria-label", "查看超时留言 "+overdueCount+" 条,点击筛选"); el("ticket-section-count").textContent = "待处理 "+data.pending_count+" · 超时 "+overdueCount; el("ticket-inbox-toggle").setAttribute("aria-label", "待处理留言 "+data.pending_count+" 条,点击查看"); el("ticket-summary-status").textContent = ""; if(latestSeen !== null && data.latest_id > latestSeen && data.new_count > 0){ unreadNew += data.new_count; el("ticket-alert-text").textContent = "收到 "+unreadNew+" 条新留言"; el("ticket-alert").hidden = false; if(options.notify) options.notify("新离线留言", "有 "+data.new_count+" 条新留言,请进入后台查看。"); } latestSeen = Math.max(latestSeen || 0, data.latest_id); }catch(error){ el("ticket-summary-status").textContent = "待处理数量暂时无法更新,稍后自动重试。"; }finally{ summaryLoading = false; } } function message(text, failed){ el("ticket-status-message").textContent = text || ""; el("ticket-status-message").classList.toggle("is-error", Boolean(failed)); } const eventLabels = {submitted:"提交留言", claimed:"领取留言", note_updated:"修改备注", completed:"标记完成", legacy_snapshot:"升级时保留状态"}; function eventDetail(event){ if(event.action === "note_updated") return "旧备注:"+(event.old_note || "(空)")+"\n新备注:"+(event.new_note || "(空)"); if(event.action === "completed") return event.new_note ? "处理结果:"+event.new_note : ""; if(event.action === "legacy_snapshot") return "升级前的处理过程未记录,仅保留当时已知状态。"; return ""; } function renderHistory(){ const list = el("ticket-history-list"), status = el("ticket-history-status"); el("ticket-history-more").hidden = !historyHasMore; el("ticket-history-more").disabled = historyLoading; if(historyLoading && !historyRows.length){ status.textContent = "正在加载处理记录…"; list.innerHTML = ""; return; } if(!historyRows.length){ status.textContent = historyError || "暂无处理记录。"; list.innerHTML = ""; return; } status.textContent = historyError; list.innerHTML = historyRows.map(function(event){ const detail = eventDetail(event); return '
  • '+options.escape(eventLabels[event.action] || event.action)+' · '+options.escape(event.actor_name || "系统")+' · '+options.escape(event.ctime || "")+''+ (detail ? ''+options.escape(detail)+'' : '')+'
  • '; }).join(""); } async function loadHistory(reset){ if(!selected) return; const id = selected.id, request = ++historyRequest; if(reset || historyTicketId !== id){ historyRows = []; historyHasMore = false; historyTicketId = id; } historyLoading = true; historyError = ""; renderHistory(); const before = !reset && historyRows.length ? "&before="+historyRows[historyRows.length-1].id : ""; try{ const data = await options.request("/api/tickets/"+id+"/events?limit=20"+before); if(request !== historyRequest || !selected || selected.id !== id) return; const list = Array.isArray(data) ? data : []; historyRows = reset ? list : historyRows.concat(list.filter(function(item){ return !historyRows.some(function(row){ return row.id === item.id; }); })); historyHasMore = list.length === 20; el("ticket-history-status").textContent = ""; }catch(error){ if(request === historyRequest) historyError = "处理记录加载失败,请点击刷新记录重试。"; }finally{ if(request === historyRequest){ historyLoading = false; renderHistory(); } } } function renderDetail(){ el("ticket-selected").hidden = !selected; if(!selected) return; const mine = selected.status === "processing" && selected.owner_username === options.username; const draft = drafts.get(selected.id); const conflict = Boolean(draft && draft.version !== selected.version); el("ticket-content").textContent = selected.content; el("ticket-meta").textContent = "#"+selected.id+" · "+selected.ctime+" · 电话:"+(selected.phone || "未提供"); el("ticket-owner").textContent = "负责人:"+(selected.owner_name || selected.owner_username || "未领取")+ (selected.owner_username === options.username ? "(我)" : ""); const overdue = el("ticket-overdue-hint"); if(selected.overdue_kind){ overdue.hidden = false; overdue.textContent = (selected.overdue_kind === "pending" ? "尚未领取,已超时 " : "领取后处理已超时 ")+selected.overdue_minutes+" 分钟。"; }else if(selected.status === "processing" && !selected.claimed_at){ overdue.hidden = false; overdue.textContent = "缺少领取时间,无法计算处理超时。"; }else{ overdue.hidden = true; overdue.textContent = ""; } if(selected.status === "processing" && selected.claimed_at_estimated){ overdue.hidden = false; overdue.textContent += " 旧留言按升级前最后更新时间估算领取时间。"; } el("ticket-state").textContent = states[selected.status] || selected.status; el("ticket-state").setAttribute("data-state", selected.status); note.value = draft ? draft.text : (selected.note || ""); note.readOnly = !mine || busy; el("ticket-claim").hidden = selected.status !== "pending"; el("ticket-save").hidden = !mine; el("ticket-complete").hidden = !mine; ["ticket-claim", "ticket-save", "ticket-complete"].forEach(id => { el(id).disabled = busy || (id !== "ticket-claim" && conflict); }); el("ticket-server-note").hidden = !conflict; el("ticket-server-note").textContent = conflict ? "这条留言已更新,当前保存的备注:"+(selected.note || "(空)") : ""; el("ticket-rebase").hidden = !conflict || !mine; el("ticket-rebase").disabled = busy; el("ticket-activity").textContent = selected.completed_at ? "完成时间:"+selected.completed_at : (selected.updated_at ? "更新时间:"+selected.updated_at : ""); renderHistory(); } function render(){ el("ticketList").innerHTML = rows.length ? rows.map(ticket => '' ).join("") : '

    '+(activeQuery ? "没有匹配的留言,请更换关键词或状态" : "该状态下暂无留言")+'

    '; el("ticket-more").hidden = !hasMore; el("ticket-more").disabled = loading || busy; el("ticket-refresh").disabled = loading || busy; filter.disabled = busy; el("ticket-search").disabled = busy; el("ticket-search-submit").disabled = busy; el("ticket-search-clear").hidden = !activeQuery; el("ticket-search-clear").disabled = busy; el("ticket-overdue-filter").disabled = busy; renderDetail(); } async function load(more, manual){ if(busy || (loading && !manual) || (expanded && !more && !manual)) return; const current = ++generation; loading = true; el("ticket-refresh").disabled = true; el("ticket-more").disabled = true; const before = more && rows.length ? "&before="+rows[rows.length-1].id : ""; const previousId = selected && selected.id, previousVersion = selected && selected.version; try{ const list = await options.request("/api/tickets?status="+filter.value+"&limit=20"+before+ (activeQuery ? "&q="+encodeURIComponent(activeQuery) : "")+(overdueOnly ? "&overdue=1" : "")); if(current !== generation) return; rows = more ? rows.concat(list.filter(item => !rows.some(row => row.id === item.id))) : list; hasMore = list.length === 20; expanded = Boolean(more); if(selected){ const found = rows.find(item => item.id === selected.id); if(found) selected = found; else{ const id = selected.id; const data = await options.request("/api/tickets/"+id); if(current !== generation) return; if(selected && selected.id === id) selected = data.ticket; } }else if(rows.length){ selected = rows[0]; } if(current === generation){ render(); if(selected && (selected.id !== previousId || selected.version !== previousVersion || historyTicketId !== selected.id)) loadHistory(true); } }catch(error){ if(current === generation) message("留言加载失败,请点击刷新重试;未保存的备注仍保留。", true); }finally{ if(current === generation){ loading = false; render(); } } } function select(id){ if(busy) return; const ticket = rows.find(row => row.id === Number(id)); if(!ticket) return; selected = ticket; message(""); render(); loadHistory(true); } async function update(action){ if(busy || !selected) return; const ticket = selected; const draft = drafts.get(ticket.id); if(action !== "claim" && (ticket.status !== "processing" || ticket.owner_username !== options.username)) return; if(action !== "claim" && draft && draft.version !== ticket.version){ message("留言已更新,请核对当前保存的备注后再处理。", true); return; } const text = note.value; if(action === "completed" && !text.trim()){ message("请先填写处理结果,再标记完成。", true); note.focus(); return; } if(action !== "claim") drafts.set(ticket.id, {text:text, version:draft ? draft.version : ticket.version}); ++generation; loading = false; busy = true; render(); message(action === "claim" ? "正在领取…" : "正在保存…"); try{ const result = await options.request("/api/tickets/"+ticket.id+(action === "claim" ? "/claim" : ""), { method: action === "claim" ? "POST" : "PATCH", headers:{"Content-Type":"application/json"}, body:JSON.stringify(action === "claim" ? {} : {version:draft ? draft.version : ticket.version, note:text, status:action === "completed" ? "completed" : "processing"}) }); selected = result.ticket; rows = rows.map(row => row.id === ticket.id ? result.ticket : row); drafts.delete(ticket.id); message(action === "claim" ? "已领取,由你负责跟进。" : action === "completed" ? "已完成,记录已保留。" : "备注已保存。"); }catch(error){ message(error.userMessage || "操作未确认成功,请刷新核对后重试;备注已保留。", true); if(error.status === 409 || error.status === 403){ try{ const data = await options.request("/api/tickets/"+ticket.id); selected = data.ticket; rows = rows.map(row => row.id === ticket.id ? selected : row); }catch(refreshError){ /* Keep the local draft and error until explicit retry. */ } } }finally{ busy = false; render(); loadHistory(true); await refreshSummary(); } } function search(){ if(busy) return; const query = el("ticket-search").value.trim(); if(query.length > 100){ message("搜索关键词最多 100 字。", true); return; } activeQuery = query; selected = null; rows = []; expanded = false; hasMore = false; message(""); render(); return load(false, true); } function viewPending(){ if(busy){ message("正在保存,请稍后查看新留言。"); return; } unreadNew = 0; el("ticket-alert").hidden = true; filter.value = "pending"; overdueOnly = false; el("ticket-overdue-filter").checked = false; el("ticket-search").value = ""; if(options.open) options.open(); return search(); } note.addEventListener("input", function(){ if(!selected || note.readOnly) return; const previous = drafts.get(selected.id); drafts.set(selected.id, {text:note.value, version:previous ? previous.version : selected.version}); }); el("ticketList").addEventListener("click", function(event){ const button = event.target.closest("[data-ticket-id]"); if(button) select(button.getAttribute("data-ticket-id")); }); filter.addEventListener("change", function(){ selected = null; rows = []; expanded = false; message(""); render(); load(false, true); }); el("ticket-search-form").addEventListener("submit", function(event){ event.preventDefault(); return search(); }); el("ticket-search").addEventListener("keydown", function(event){ if(event.key === "Enter" && event.isComposing) event.preventDefault(); }); el("ticket-search-clear").onclick = function(){ el("ticket-search").value = ""; return search(); }; el("ticket-inbox-toggle").onclick = viewPending; el("ticket-alert-view").onclick = viewPending; el("ticket-overdue-toggle").onclick = function(){ if(busy) return; filter.value = "all"; overdueOnly = true; el("ticket-overdue-filter").checked = true; el("ticket-search").value = ""; if(options.open) options.open("ticket-overdue-toggle"); return search(); }; el("ticket-overdue-filter").addEventListener("change", function(){ overdueOnly = Boolean(this.checked); selected = null; rows = []; expanded = false; return load(false, true); }); el("ticket-history-reload").onclick = function(){ return loadHistory(true); }; el("ticket-history-more").onclick = function(){ if(!historyLoading && historyHasMore) return loadHistory(false); }; el("ticket-refresh").onclick = function(){ return load(false, true); }; el("ticket-more").onclick = function(){ return load(true); }; el("ticket-claim").onclick = function(){ return update("claim"); }; el("ticket-save").onclick = function(){ return update("save"); }; el("ticket-complete").onclick = function(){ return update("completed"); }; el("ticket-rebase").onclick = function(){ if(!selected || busy) return; const draft = drafts.get(selected.id); if(draft){ draft.version = selected.version; render(); message("已保留你的草稿,请确认内容后保存。"); } }; return {load:load, select:select, refreshSummary:refreshSummary}; } global.AdminTickets = {create:create}; })(window);