From 325d258bf4a629f4f4cc927dc3f264de6f88dd1f Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 May 2026 14:59:55 +0300 Subject: [PATCH] =?UTF-8?q?f7support:=20=D1=82=D0=BE=D1=87=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=BF=D1=80=D0=BE=D1=87=D0=B8=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8E=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=BE=D1=87=D0=BA=D0=B8=20(fingerprint)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Если API не отдаёт has_unread или не обновляет его в списке, показываем зелёную точку при смене activity/updated_at или preview относительно последнего зафиксированного состояния. Учёт альтернативных полей hasUnread/unread/unread_count. После открытия чата — fetchTickets для актуального отпечатка. --- js/main.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/js/main.js b/js/main.js index 1fe6e49..cb51b37 100644 --- a/js/main.js +++ b/js/main.js @@ -71,6 +71,8 @@ wsReconnectTimer: null, ticketsPollTimer: null, pendingFile: null, + /** @type {Record} последний известный «отпечаток» карточки по номеру тикета (для точки без has_unread с API) */ + ticketSeenFingerprint: {}, }; function clientIdentityHeaders() { @@ -711,9 +713,55 @@ window.setTimeout(run, 320); } + function ticketBoardFingerprint(t) { + const act = String(t.activity_at || t.updated_at || t.last_activity_at || t.created_at || ""); + const prev = t.preview_text != null ? String(t.preview_text) : ""; + return `${act}\u0001${prev}`; + } + + /** Для открытого чата учитываем последнее сообщение, чтобы отпечаток не отставал от превью в списке. */ + function effectiveBoardFingerprint(t) { + const base = ticketBoardFingerprint(t); + const tn = String(t.ticket_number); + if (state.chatModalOpen && state.currentTicket != null && String(state.currentTicket) === tn) { + const msgs = state.messages; + if (msgs && msgs.length) { + const last = msgs[msgs.length - 1]; + return `${base}\u0002${last.id ?? ""}\u0003${last.created_at ?? ""}`; + } + } + return base; + } + + function updateBoardUnreadFingerprints() { + for (const t of state.tickets) { + const tn = String(t.ticket_number); + const fp = effectiveBoardFingerprint(t); + const seenKey = tn; + if (!Object.prototype.hasOwnProperty.call(state.ticketSeenFingerprint, seenKey)) { + state.ticketSeenFingerprint[seenKey] = fp; + t._f7_board_unread = false; + continue; + } + if (state.chatModalOpen && state.currentTicket != null && String(state.currentTicket) === tn) { + state.ticketSeenFingerprint[seenKey] = fp; + t._f7_board_unread = false; + continue; + } + t._f7_board_unread = fp !== state.ticketSeenFingerprint[seenKey]; + } + } + function ticketHasUnread(ticket) { - const v = ticket?.has_unread; - return v === true || v === 1 || v === "1" || v === "true"; + if (ticket && ticket._f7_board_unread) return true; + const raw = + ticket?.has_unread ?? + ticket?.hasUnread ?? + ticket?.unread ?? + ticket?.is_unread ?? + ticket?.unread_count; + if (typeof raw === "number" && raw > 0) return true; + return raw === true || raw === 1 || raw === "1" || raw === "true" || raw === "yes"; } function scheduleTicketsBoardPolling() { @@ -792,6 +840,11 @@ } function closeChatModal() { + const closingTn = state.currentTicket; + if (closingTn != null) { + const row = state.tickets.find((x) => String(x.ticket_number) === String(closingTn)); + if (row) state.ticketSeenFingerprint[String(closingTn)] = ticketBoardFingerprint(row); + } disconnectTicketSocket(); revokeAttachmentUrls(); state.chatModalOpen = false; @@ -816,6 +869,7 @@ }); if (!response.ok) throw new Error("Не удалось загрузить список обращений"); state.tickets = await response.json(); + updateBoardUnreadFingerprints(); renderTickets(); } @@ -886,6 +940,7 @@ openChatModal(ticket); try { await fetchMessages(); + await fetchTickets(); const row = state.tickets.find((x) => x.ticket_number === ticketNumber); if (row) row.has_unread = false; renderTickets();