f7support: точка непрочитанного по изменению карточки (fingerprint)

Если API не отдаёт has_unread или не обновляет его в списке, показываем зелёную точку при смене activity/updated_at или preview относительно последнего зафиксированного состояния. Учёт альтернативных полей hasUnread/unread/unread_count. После открытия чата — fetchTickets для актуального отпечатка.
This commit is contained in:
root
2026-05-14 14:59:55 +03:00
parent 866a64d413
commit 325d258bf4
+57 -2
View File
@@ -71,6 +71,8 @@
wsReconnectTimer: null, wsReconnectTimer: null,
ticketsPollTimer: null, ticketsPollTimer: null,
pendingFile: null, pendingFile: null,
/** @type {Record<string, string>} последний известный «отпечаток» карточки по номеру тикета (для точки без has_unread с API) */
ticketSeenFingerprint: {},
}; };
function clientIdentityHeaders() { function clientIdentityHeaders() {
@@ -711,9 +713,55 @@
window.setTimeout(run, 320); 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) { function ticketHasUnread(ticket) {
const v = ticket?.has_unread; if (ticket && ticket._f7_board_unread) return true;
return v === true || v === 1 || v === "1" || v === "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() { function scheduleTicketsBoardPolling() {
@@ -792,6 +840,11 @@
} }
function closeChatModal() { 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(); disconnectTicketSocket();
revokeAttachmentUrls(); revokeAttachmentUrls();
state.chatModalOpen = false; state.chatModalOpen = false;
@@ -816,6 +869,7 @@
}); });
if (!response.ok) throw new Error("Не удалось загрузить список обращений"); if (!response.ok) throw new Error("Не удалось загрузить список обращений");
state.tickets = await response.json(); state.tickets = await response.json();
updateBoardUnreadFingerprints();
renderTickets(); renderTickets();
} }
@@ -886,6 +940,7 @@
openChatModal(ticket); openChatModal(ticket);
try { try {
await fetchMessages(); await fetchMessages();
await fetchTickets();
const row = state.tickets.find((x) => x.ticket_number === ticketNumber); const row = state.tickets.find((x) => x.ticket_number === ticketNumber);
if (row) row.has_unread = false; if (row) row.has_unread = false;
renderTickets(); renderTickets();