Files
site/WESP_REL/static/js/wesp-zootech-notification-center.js
T
2026-07-17 12:57:18 +03:00

801 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Hub «К»: уведомления и план на день. FAB «К», модалка, badge.
*/
(function (global) {
const MODAL_ID = "zootechNotificationCenterModal";
const FAB_CLASS = "wesp-zt-notify-fab";
const POLL_MS = 60000;
const SCROLL_LOAD_THRESHOLD = 80;
let fabEl = null;
let badgeEl = null;
let modalEl = null;
let listEl = null;
let detailEl = null;
let hubEl = null;
let planMountEl = null;
let titleEl = null;
let readAllBtn = null;
let backBtn = null;
let normsBtn = null;
let pollTimer = null;
let itemsCache = [];
let currentView = "hub";
let dailyPlanPanel = null;
let toolbarEl = null;
let scrollRootEl = null;
let categoryFilter = "all";
let sortMode = "newest";
let unreadOnly = false;
let prevDate = null;
let loadingMore = false;
let listLoading = false;
let scrollWired = false;
const STORAGE_KEY = "wesp-notify-prefs";
const CATEGORY_LABELS = {
daily_plan: "План на день",
recipe: "Рейсы",
sync: "Синхронизация",
feed_quality: "Отклонения",
component: "Компоненты",
sklad: "Склад",
equipment: "Оборудование",
report: "Отчёты",
general: "Общее",
};
const CATEGORY_FILTERS = [
{ id: "all", label: "Все" },
{ id: "daily_plan", label: "План" },
{ id: "recipe", label: "Рейсы" },
{ id: "sync", label: "Синхр." },
{ id: "feed_quality", label: "Отклон." },
{ id: "component", label: "Компон." },
{ id: "sklad", label: "Склад" },
{ id: "equipment", label: "Оборуд." },
];
const KIND_ORDER = { error: 0, warning: 1, info: 2, success: 3 };
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function resolveNotificationHref(item) {
if (!item || !item.linkKind) return null;
if (item.linkKind === "component") return "/components";
if (item.linkKind === "sklad") return "/feed_consumption";
if (item.linkKind === "equipment") return "/feed_dispensers";
if (!item.linkId) return null;
const id = encodeURIComponent(item.linkId);
if (item.linkKind === "recipe") return `/recipes?recipe=${id}`;
if (item.linkKind === "report_loading") return `/reports?report=${id}`;
if (item.linkKind === "daily_plan") return `/recipes?dailyPlan=${id}`;
return null;
}
async function markReadQuiet(id) {
const item = itemsCache.find((x) => x.id === id);
if (!item || item.read) return;
try {
await fetch(`/api/notifications/${encodeURIComponent(id)}/read`, { method: "PATCH" });
item.read = true;
await refreshBadge();
} catch {
/* ignore */
}
}
async function openNotificationTarget(item) {
if (isDailyPlanItem(item)) {
await markReadQuiet(item.id);
openDailyPlan();
return;
}
const href = resolveNotificationHref(item);
if (!href) {
await showDetail(item.id);
return;
}
await markReadQuiet(item.id);
if (global.bootstrap?.Modal) {
const inst = global.bootstrap.Modal.getInstance(modalEl);
inst?.hide();
}
global.location.href = href;
}
function relativeTime(iso) {
if (!iso) return "";
const then = new Date(iso).getTime();
const diff = Date.now() - then;
const min = Math.floor(diff / 60000);
if (min < 1) return "только что";
if (min < 60) return `${min} мин назад`;
const hrs = Math.floor(min / 60);
if (hrs < 24) return `${hrs} ч назад`;
const days = Math.floor(hrs / 24);
return `${days} дн назад`;
}
function loadPrefs() {
try {
const raw = global.sessionStorage?.getItem(STORAGE_KEY);
if (!raw) return;
const prefs = JSON.parse(raw);
if (prefs.categoryFilter) categoryFilter = prefs.categoryFilter;
if (prefs.sortMode) sortMode = prefs.sortMode;
if (typeof prefs.unreadOnly === "boolean") unreadOnly = prefs.unreadOnly;
} catch {
/* ignore */
}
}
function savePrefs() {
try {
global.sessionStorage?.setItem(
STORAGE_KEY,
JSON.stringify({ categoryFilter, sortMode, unreadOnly })
);
} catch {
/* ignore */
}
}
function itemTimestamp(item) {
return item?.createdAt ? new Date(item.createdAt).getTime() : 0;
}
function sortItems(items) {
const list = [...items];
if (sortMode === "oldest") {
return list.sort((a, b) => itemTimestamp(a) - itemTimestamp(b));
}
if (sortMode === "unread_first") {
return list.sort((a, b) => {
const ra = a.read ? 1 : 0;
const rb = b.read ? 1 : 0;
if (ra !== rb) return ra - rb;
return itemTimestamp(b) - itemTimestamp(a);
});
}
if (sortMode === "severity") {
return list.sort((a, b) => {
const ka = KIND_ORDER[a.kind] ?? 9;
const kb = KIND_ORDER[b.kind] ?? 9;
if (ka !== kb) return ka - kb;
return itemTimestamp(b) - itemTimestamp(a);
});
}
return list.sort((a, b) => itemTimestamp(b) - itemTimestamp(a));
}
function formatDayLabel(isoDate) {
if (!isoDate) return "Ранее";
const parts = String(isoDate).slice(0, 10).split("-");
if (parts.length !== 3) return isoDate;
const d = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
const now = new Date();
const startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const startThat = new Date(d.getFullYear(), d.getMonth(), d.getDate());
const diffDays = Math.round((startToday - startThat) / 86400000);
if (diffDays === 0) return "Сегодня";
if (diffDays === 1) return "Вчера";
return d.toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" });
}
function dayKeyFromItem(item) {
if (!item?.createdAt) return "";
const d = new Date(item.createdAt);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function inferCategory(item) {
const raw = item?.category || "general";
if (raw === "daily_plan") return "daily_plan";
if (item?.linkKind === "daily_plan" || item?.page === "daily_plan") return "daily_plan";
const text = `${item?.title || ""} ${item?.detail || ""}`.toLowerCase();
if (
text.includes("из плана") ||
text.includes("в плане") ||
text.includes("замен") ||
text.includes("исключ") ||
text.includes("снова в план") ||
text.includes("план на день")
) {
return "daily_plan";
}
return raw;
}
function isDailyPlanItem(item) {
return inferCategory(item) === "daily_plan";
}
function categoryLabel(item) {
return CATEGORY_LABELS[inferCategory(item)] || CATEGORY_LABELS.general;
}
function buildListUrl(date) {
const params = new URLSearchParams({ sort: sortMode });
if (date) params.set("date", date);
if (categoryFilter && categoryFilter !== "all") params.set("category", categoryFilter);
if (unreadOnly) params.set("unread", "1");
return `/api/notifications?${params.toString()}`;
}
function ensureFab() {
if (fabEl && fabEl.isConnected) return fabEl;
fabEl = document.querySelector(`.${FAB_CLASS}`);
if (!fabEl) {
fabEl = document.createElement("button");
fabEl.type = "button";
fabEl.className = FAB_CLASS;
fabEl.setAttribute("data-action", "open-notification-center");
fabEl.setAttribute("aria-label", "Центр «К»");
fabEl.innerHTML =
'<span class="wesp-zt-notify-fab__glyph" aria-hidden="true">К</span>' +
'<span class="wesp-zt-notify-fab__badge" hidden aria-hidden="true"></span>';
document.body.appendChild(fabEl);
}
badgeEl = fabEl.querySelector(".wesp-zt-notify-fab__badge");
fabEl.addEventListener("click", openCenter);
return fabEl;
}
function categoryFilterButtonsHtml() {
return CATEGORY_FILTERS.map(
(f) =>
`<button type="button" class="zt-tab${f.id === categoryFilter ? " active" : ""}" ` +
`data-notify-category="${escapeHtml(f.id)}">${escapeHtml(f.label)}</button>`
).join("");
}
function modalTemplate() {
return (
`<div class="modal wesp-shell-modal" id="${MODAL_ID}" tabindex="-1" aria-hidden="true">` +
'<div class="modal-dialog modal-dialog--shell modal-dialog--shell-wide">' +
'<div class="modal-content wesp-shell-bsmodal border-0 shadow-none zt-k-hub-modal">' +
'<div class="zt-k-hub-modal__header">' +
'<div class="zt-k-hub-modal__header-left">' +
'<button type="button" class="zt-k-hub-modal__back" data-action="k-hub-back" hidden>&larr; Назад</button>' +
'<h2 class="wesp-shell-bsmodal-title zt-k-hub-modal__title" data-k-hub-title>Комптон - хаб</h2>' +
"</div>" +
'<div class="zt-k-hub-modal__header-right">' +
'<button type="button" class="btn btn-sm btn-outline-primary zt-k-hub-plan-norms-btn" data-action="k-hub-open-component-norms" hidden title="Изменить СВ% компонентов только в плане на день — рецепты в справочнике не меняются">Коррекция СВ%</button>' +
'<button type="button" class="zt-notify-center__read-all" data-action="notify-read-all" hidden>Прочитать все</button>' +
'<button type="button" class="wesp-shell-bsmodal-close" data-bs-dismiss="modal" aria-label="Закрыть">×</button>' +
"</div></div>" +
'<div class="zt-k-hub-modal__body">' +
'<div class="zt-k-hub" data-k-hub-panel></div>' +
'<div class="zt-notify-center__notify" data-k-notify-panel hidden>' +
'<div class="zt-notify-toolbar" data-notify-toolbar>' +
'<div class="zt-notify-toolbar__filters zt-tabs" role="tablist" data-notify-categories>' +
categoryFilterButtonsHtml() +
"</div>" +
'<div class="zt-notify-toolbar__row">' +
'<label class="zt-notify-toolbar__unread">' +
'<input type="checkbox" data-notify-unread-only> Только непрочит.' +
"</label>" +
'<label class="zt-notify-toolbar__sort">' +
'<span class="visually-hidden">Сортировка</span>' +
'<select data-notify-sort aria-label="Сортировка">' +
'<option value="newest">Сначала новые</option>' +
'<option value="oldest">Сначала старые</option>' +
'<option value="unread_first">Непрочит. сверху</option>' +
'<option value="severity">По важности</option>' +
"</select></label></div></div>" +
'<div class="zt-notify-list" data-notify-list></div>' +
'<div class="zt-notify-load-more" data-notify-load-more hidden></div>' +
'<div class="zt-notify-detail" data-notify-detail hidden></div>' +
"</div>" +
'<div class="zt-k-hub-plan-mount" data-k-plan-panel hidden></div>' +
"</div></div></div></div>"
);
}
function ensureModal() {
if (modalEl && modalEl.isConnected) return modalEl;
modalEl = document.getElementById(MODAL_ID);
if (!modalEl) {
document.body.insertAdjacentHTML("beforeend", modalTemplate());
modalEl = document.getElementById(MODAL_ID);
}
hubEl = modalEl.querySelector("[data-k-hub-panel]");
listEl = modalEl.querySelector("[data-notify-list]");
detailEl = modalEl.querySelector("[data-notify-detail]");
toolbarEl = modalEl.querySelector("[data-notify-toolbar]");
scrollRootEl = modalEl.querySelector(".zt-k-hub-modal__body");
planMountEl = modalEl.querySelector("[data-k-plan-panel]");
titleEl = modalEl.querySelector("[data-k-hub-title]");
readAllBtn = modalEl.querySelector("[data-action='notify-read-all']");
backBtn = modalEl.querySelector("[data-action='k-hub-back']");
normsBtn = modalEl.querySelector("[data-action='k-hub-open-component-norms']");
global.WespZootechModals?.wireAllShellModals?.();
readAllBtn?.addEventListener("click", readAll);
backBtn?.addEventListener("click", () => showHub());
normsBtn?.addEventListener("click", () => {
global.openDailyPlanComponentNormsModal?.();
});
wireNotifyToolbar();
wireScrollLoad();
hubEl?.querySelectorAll("[data-action]").forEach((btn) => {
btn.addEventListener("click", () => {
const action = btn.getAttribute("data-action");
if (action === "hub-open-notifications") openNotifications();
if (action === "hub-open-daily-plan") openDailyPlan();
});
});
wireModalLifecycle();
renderHubCards();
return modalEl;
}
function syncToolbarUi() {
if (!toolbarEl) return;
toolbarEl.querySelectorAll("[data-notify-category]").forEach((btn) => {
const cat = btn.getAttribute("data-notify-category") || "all";
btn.classList.toggle("active", cat === categoryFilter);
});
const sortSelect = toolbarEl.querySelector("[data-notify-sort]");
if (sortSelect) sortSelect.value = sortMode;
const unreadCb = toolbarEl.querySelector("[data-notify-unread-only]");
if (unreadCb) unreadCb.checked = unreadOnly;
}
function wireNotifyToolbar() {
if (!toolbarEl || toolbarEl.dataset.wired === "1") return;
toolbarEl.dataset.wired = "1";
loadPrefs();
syncToolbarUi();
toolbarEl.querySelectorAll("[data-notify-category]").forEach((btn) => {
btn.addEventListener("click", () => {
categoryFilter = btn.getAttribute("data-notify-category") || "all";
savePrefs();
syncToolbarUi();
reloadNotifications();
});
});
toolbarEl.querySelector("[data-notify-sort]")?.addEventListener("change", (ev) => {
sortMode = ev.target.value || "newest";
savePrefs();
reloadNotifications();
});
toolbarEl.querySelector("[data-notify-unread-only]")?.addEventListener("change", (ev) => {
unreadOnly = Boolean(ev.target.checked);
savePrefs();
reloadNotifications();
});
}
function wireScrollLoad() {
if (scrollWired || !scrollRootEl) return;
scrollWired = true;
scrollRootEl.addEventListener("scroll", () => {
if (currentView !== "notifications" || !prevDate || loadingMore || listLoading) return;
const nearBottom =
scrollRootEl.scrollTop + scrollRootEl.clientHeight >=
scrollRootEl.scrollHeight - SCROLL_LOAD_THRESHOLD;
if (nearBottom) loadMoreNotifications();
});
}
function itemPreview(item) {
const detail = String(item?.detail || "").trim();
if (detail) {
const cut = detail.replace(/\.\s+\d{1,2}\s+\S+\s+\d{4}.*$/u, "").trim();
const text = cut || detail;
return text.length > 140 ? `${text.slice(0, 137)}…` : text;
}
return item?.title || "";
}
function renderNotifyItem(item) {
const unread = !item.read ? " zt-notify-item--unread" : "";
const cat = categoryLabel(item);
const preview = itemPreview(item);
const showPreview = preview && preview !== item.title;
return (
`<button type="button" class="zt-notify-item${unread}" data-notify-id="${escapeHtml(item.id)}">` +
`<span class="zt-notify-item__dot zt-notify-item__dot--${escapeHtml(item.kind || "info")}"></span>` +
`<span class="zt-notify-item__main">` +
`<span class="zt-notify-item__title">${escapeHtml(item.title)}</span>` +
(showPreview
? `<span class="zt-notify-item__preview">${escapeHtml(preview)}</span>`
: "") +
`<span class="zt-notify-item__meta">` +
`<span class="zt-notify-item__category">${escapeHtml(cat)}</span>` +
`<span class="zt-notify-item__time">${escapeHtml(relativeTime(item.createdAt))}</span>` +
"</span></span></button>"
);
}
function renderLoadMoreState() {
const el = modalEl?.querySelector("[data-notify-load-more]");
if (!el) return;
if (loadingMore) {
el.hidden = false;
el.innerHTML =
'<div class="zt-notify-load-more__spinner"><div class="zt-spinner" role="status"></div>' +
'<span>Загрузка…</span></div>';
return;
}
if (prevDate) {
el.hidden = false;
el.innerHTML =
`<p class="zt-notify-load-more__hint">Прокрутите вниз — загрузятся уведомления за ${escapeHtml(formatDayLabel(prevDate))}</p>`;
return;
}
el.hidden = true;
el.innerHTML = "";
}
function renderHubCards() {
if (!hubEl) return;
hubEl.innerHTML =
'<div class="zt-k-hub__grid">' +
'<button type="button" class="zt-k-hub-card zt-card zt-card--interactive" data-action="hub-open-notifications">' +
'<span class="zt-k-hub-card__icon" aria-hidden="true"><i class="fas fa-bell"></i></span>' +
'<span class="zt-k-hub-card__text">' +
'<span class="zt-k-hub-card__title">Уведомления</span>' +
'<span class="zt-k-hub-card__desc">План, рейсы, синхронизация и отклонения</span>' +
"</span></button>" +
'<button type="button" class="zt-k-hub-card zt-card zt-card--interactive" data-action="hub-open-daily-plan">' +
'<span class="zt-k-hub-card__icon" aria-hidden="true"><i class="fas fa-calendar-day"></i></span>' +
'<span class="zt-k-hub-card__text">' +
'<span class="zt-k-hub-card__title">План на день</span>' +
'<span class="zt-k-hub-card__desc">Рейсы, ингредиенты и выгрузка по периодам</span>' +
"</span></button></div>";
hubEl.querySelectorAll("[data-action]").forEach((btn) => {
btn.addEventListener("click", () => {
const action = btn.getAttribute("data-action");
if (action === "hub-open-notifications") openNotifications();
if (action === "hub-open-daily-plan") openDailyPlan();
});
});
}
function applyModalLayout(view) {
if (!modalEl) return;
const dialog = modalEl.querySelector(".modal-dialog");
const body = modalEl.querySelector(".zt-k-hub-modal__body");
modalEl.classList.remove("zt-k-hub--plan", "zt-k-hub--notify");
body?.classList.remove("zt-k-hub-modal__body--plan");
dialog?.classList.remove("modal-dialog--shell-plan", "modal-dialog--shell-notify");
if (view === "daily-plan") {
modalEl.classList.add("zt-k-hub--plan");
body?.classList.add("zt-k-hub-modal__body--plan");
dialog?.classList.add("modal-dialog--shell-plan");
} else if (view === "notifications") {
modalEl.classList.add("zt-k-hub--notify");
dialog?.classList.add("modal-dialog--shell-notify");
}
}
function updateHeader(view) {
currentView = view;
applyModalLayout(view);
if (!titleEl || !backBtn || !readAllBtn) return;
if (normsBtn) normsBtn.hidden = view !== "daily-plan";
if (view === "hub") {
titleEl.textContent = "Комптон - хаб";
backBtn.hidden = true;
readAllBtn.hidden = true;
} else if (view === "notifications") {
titleEl.textContent = "Уведомления";
backBtn.hidden = false;
readAllBtn.hidden = false;
} else if (view === "daily-plan") {
titleEl.textContent = "План на день";
backBtn.hidden = false;
readAllBtn.hidden = true;
}
}
function dismissPlanOverlays() {
modalEl?.querySelectorAll(".zt-k-hub-plan-overlay").forEach((el) => el.remove());
}
function destroyDailyPlanPanel() {
dismissPlanOverlays();
global.closeDailyPlanComponentNormsModal?.();
dailyPlanPanel?.destroy?.();
dailyPlanPanel = null;
global.WespDailyPlanPanel?._setActivePanel?.(null);
}
function wireModalLifecycle() {
ensureModal();
if (modalEl.dataset.kHubLifecycleWired === "1") return;
modalEl.dataset.kHubLifecycleWired = "1";
modalEl.addEventListener("hide.bs.modal", dismissPlanOverlays);
modalEl.addEventListener("hidden.bs.modal", destroyDailyPlanPanel);
}
function showHub() {
ensureModal();
updateHeader("hub");
hubEl.hidden = false;
modalEl.querySelector("[data-k-notify-panel]").hidden = true;
planMountEl.hidden = true;
destroyDailyPlanPanel();
listEl.hidden = false;
detailEl.hidden = true;
}
function setBadge(unreadCount) {
ensureFab();
if (!badgeEl) return;
const count = Number(unreadCount) || 0;
if (count <= 0) {
badgeEl.hidden = true;
badgeEl.textContent = "";
fabEl.classList.remove("has-unread");
return;
}
badgeEl.hidden = false;
badgeEl.textContent = count > 9 ? "9+" : String(count);
fabEl.classList.add("has-unread");
}
async function refreshBadge() {
try {
const resp = await fetch("/api/notifications?summary=1");
if (!resp.ok) return;
const data = await resp.json();
setBadge(data.unreadCount);
} catch {
/* ignore */
}
}
function mergeItems(existing, incoming) {
const map = new Map(existing.map((x) => [x.id, x]));
incoming.forEach((item) => map.set(item.id, item));
return Array.from(map.values());
}
function matchesCategoryFilter(item) {
if (!categoryFilter || categoryFilter === "all") return true;
return inferCategory(item) === categoryFilter;
}
function filterVisibleItems(items) {
let list = items.filter(matchesCategoryFilter);
if (unreadOnly) list = list.filter((x) => !x.read);
return list;
}
async function fetchDay(date) {
const resp = await fetch(buildListUrl(date));
if (!resp.ok) throw new Error("Не удалось загрузить уведомления");
return resp.json();
}
async function loadInitialNotifications() {
let date = null;
let found = [];
for (let step = 0; step < 31; step += 1) {
const data = await fetchDay(date);
setBadge(data.unreadCount);
if (data.items?.length) {
found = mergeItems(found, data.items);
prevDate = data.hasMore ? data.prevDate : null;
return found;
}
if (!data.hasMore || !data.prevDate) {
prevDate = null;
return found;
}
date = data.prevDate;
}
prevDate = null;
return found;
}
async function reloadNotifications() {
listLoading = true;
itemsCache = [];
prevDate = null;
detailEl.hidden = true;
listEl.hidden = false;
listEl.innerHTML =
'<div class="zt-notify-empty zt-notify-empty--loading">' +
'<div class="zt-spinner" role="status"></div><p>Загрузка…</p></div>';
renderLoadMoreState();
try {
itemsCache = await loadInitialNotifications();
renderList();
} catch {
itemsCache = [];
prevDate = null;
renderList();
} finally {
listLoading = false;
renderLoadMoreState();
}
}
async function loadMoreNotifications() {
if (!prevDate || loadingMore || listLoading) return;
loadingMore = true;
renderLoadMoreState();
const loadDate = prevDate;
try {
const data = await fetchDay(loadDate);
itemsCache = mergeItems(itemsCache, data.items || []);
prevDate = data.hasMore ? data.prevDate : null;
setBadge(data.unreadCount);
renderList();
} catch {
/* keep current list */
} finally {
loadingMore = false;
renderLoadMoreState();
}
}
function wireListClicks() {
listEl.querySelectorAll("[data-notify-id]").forEach((btn) => {
btn.addEventListener("click", () => {
const id = btn.getAttribute("data-notify-id");
if (id) showDetail(id);
});
});
}
function renderList() {
ensureModal();
wireNotifyToolbar();
syncToolbarUi();
detailEl.hidden = true;
listEl.hidden = false;
const sorted = sortItems(filterVisibleItems(itemsCache));
if (!sorted.length) {
const hint =
unreadOnly || categoryFilter !== "all"
? prevDate
? "За сегодня ничего нет — прокрутите вниз для более ранних"
: "Нет уведомлений по выбранному фильтру"
: "Нет уведомлений";
listEl.innerHTML =
`<div class="zt-notify-empty"><i class="fas fa-bell-slash"></i><p>${escapeHtml(hint)}</p></div>`;
renderLoadMoreState();
return;
}
let html = "";
let lastGroup = null;
sorted.forEach((item) => {
const group = dayKeyFromItem(item);
if (group !== lastGroup) {
html += `<div class="zt-notify-group">${escapeHtml(formatDayLabel(group))}</div>`;
lastGroup = group;
}
html += renderNotifyItem(item);
});
listEl.innerHTML = html;
wireListClicks();
renderLoadMoreState();
}
async function showDetail(id) {
const item = itemsCache.find((x) => x.id === id);
if (!item) return;
ensureModal();
listEl.hidden = true;
modalEl.querySelector("[data-notify-load-more]")?.setAttribute("hidden", "");
detailEl.hidden = false;
detailEl.innerHTML =
`<button type="button" class="zt-notify-detail__back" data-action="notify-back">&larr; Назад</button>` +
`<p class="zt-notify-detail__kind zt-notify-detail__kind--${escapeHtml(item.kind)}">${escapeHtml(item.title)}</p>` +
`<p class="zt-notify-detail__meta">${escapeHtml(categoryLabel(item))}</p>` +
`<p class="zt-notify-detail__text">${escapeHtml(item.detail)}</p>` +
`<p class="zt-notify-detail__when">${escapeHtml(relativeTime(item.createdAt))}</p>` +
(resolveNotificationHref(item) || isDailyPlanItem(item)
? `<button type="button" class="btn btn-primary btn-sm mt-3" data-action="notify-open-target">Открыть план</button>`
: "");
detailEl.querySelector("[data-action='notify-open-target']")?.addEventListener("click", () => {
openNotificationTarget(item);
});
detailEl.querySelector("[data-action='notify-back']")?.addEventListener("click", () => {
renderList();
});
if (!item.read) {
try {
await fetch(`/api/notifications/${encodeURIComponent(id)}/read`, { method: "PATCH" });
item.read = true;
await refreshBadge();
} catch {
/* ignore */
}
}
}
async function readAll() {
try {
await fetch("/api/notifications/read-all", { method: "PATCH" });
await reloadNotifications();
} catch {
/* ignore */
}
}
function showModal() {
if (global.WespZootechModals?.show) {
global.WespZootechModals.show(MODAL_ID);
} else if (global.bootstrap?.Modal) {
global.bootstrap.Modal.getOrCreateInstance(modalEl).show();
}
}
async function openCenter() {
ensureModal();
showHub();
showModal();
}
async function openNotifications() {
ensureModal();
updateHeader("notifications");
hubEl.hidden = true;
modalEl.querySelector("[data-k-notify-panel]").hidden = false;
planMountEl.hidden = true;
destroyDailyPlanPanel();
await reloadNotifications();
showModal();
}
function openDailyPlan() {
ensureModal();
updateHeader("daily-plan");
hubEl.hidden = true;
modalEl.querySelector("[data-k-notify-panel]").hidden = true;
planMountEl.hidden = false;
destroyDailyPlanPanel();
dailyPlanPanel = global.WespDailyPlanPanel?.createDailyPlanPanel?.();
if (dailyPlanPanel && planMountEl) {
dailyPlanPanel.mount(planMountEl);
global.WespDailyPlanPanel?._setActivePanel?.(dailyPlanPanel);
} else if (planMountEl) {
planMountEl.innerHTML =
'<div class="zt-k-hub-plan__empty"><p>Модуль плана на день не загружен.</p></div>';
}
showModal();
}
function init() {
if (!document.body.classList.contains("zootech-app-body")) return;
loadPrefs();
ensureFab();
ensureModal();
refreshBadge();
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(refreshBadge, POLL_MS);
}
global.WespZootechNotificationCenter = {
init,
openCenter,
openNotifications,
openDailyPlan,
refreshBadge,
setBadge,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})(typeof window !== "undefined" ? window : globalThis);