(() => { "use strict"; const tg = window.Telegram?.WebApp; const tgUser = tg?.initDataUnsafe?.user; const query = new URLSearchParams(window.location.search); const previewUserId = Number(query.get("user_id")) || 0; const keyboardLaunch = query.get("mode") === "keyboard"; const user = tgUser || (previewUserId ? { id: previewUserId } : null); const userId = Number(user?.id) || 0; const state = { kind: "income", board: "all", summary: null, config: null }; const categories = { income: ["Зарплата", "Фриланс", "Подарок", "Продажа", "Инвестиции", "Другое"], expense: ["Продукты", "Жильё", "Транспорт", "Здоровье", "Развлечения", "Покупки", "Другое"], }; const $ = (selector) => document.querySelector(selector); const money = new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB", maximumFractionDigits: 2 }); function initTelegram() { if (!tg) return; const safely = (action) => { try { action(); } catch (error) { console.warn("Telegram WebApp method failed", error); } }; safely(() => tg.ready()); safely(() => tg.expand()); safely(() => tg.enableClosingConfirmation?.()); if (tg.isVersionAtLeast?.("6.1")) safely(() => tg.setHeaderColor("secondary_bg_color")); if (tg.isVersionAtLeast?.("7.10")) safely(() => tg.setBottomBarColor("secondary_bg_color")); } function showToast(message) { const toast = $("#toast"); toast.textContent = message; toast.classList.add("show"); window.setTimeout(() => toast.classList.remove("show"), 2600); } function setIdentity(profile = user) { const name = profile?.nickname || [profile?.first_name, profile?.last_name].filter(Boolean).join(" ") || (userId ? "Загрузка…" : "Откройте из бота"); $("#userName").textContent = name; $("#userInitial").textContent = name.slice(0, 1).toUpperCase(); } function bindNavigation() { document.querySelectorAll(".bottom-nav button").forEach((button) => { button.addEventListener("click", () => { document.querySelectorAll(".bottom-nav button, .view").forEach((item) => item.classList.remove("active")); button.classList.add("active"); $(`#${button.dataset.view}`).classList.add("active"); if (button.dataset.view === "leaderboardView") loadLeaderboard(); }); }); } function fillCategories() { const select = $("#category"); select.innerHTML = categories[state.kind].map((category) => ``).join(""); $("#customCategoryWrap").classList.add("hidden"); $("#customCategory").required = false; } function bindForm() { document.querySelectorAll(".segment").forEach((button) => { button.addEventListener("click", () => { document.querySelectorAll(".segment").forEach((item) => item.classList.remove("active")); button.classList.add("active"); state.kind = button.dataset.kind; fillCategories(); }); }); $("#category").addEventListener("change", (event) => { const custom = event.target.value === "Другое"; $("#customCategoryWrap").classList.toggle("hidden", !custom); $("#customCategory").required = custom; if (custom) $("#customCategory").focus(); }); $("#transactionForm").addEventListener("submit", submitTransaction); } function submitTransaction(event) { event.preventDefault(); const amount = Number($("#amount").value); const selected = $("#category").value; const category = selected === "Другое" ? $("#customCategory").value.trim() : selected; if (!Number.isFinite(amount) || amount <= 0 || !category) { showToast("Проверьте сумму и категорию"); return; } const payload = { v: 1, type: "transaction", kind: state.kind, amount: amount.toFixed(2), category: category.slice(0, 40), note: $("#note").value.trim().slice(0, 160), occurred_at: new Date().toISOString(), }; const encoded = JSON.stringify(payload); if (encoded.length > 4096) { showToast("Запись получилась слишком длинной"); return; } const isTelegramClient = Boolean(tg?.platform && tg.platform !== "unknown"); if (!tg?.sendData || !isTelegramClient || !keyboardLaunch) { showToast("Отправьте /start боту и откройте приложение кнопкой под сообщением"); return; } try { tg.HapticFeedback?.notificationOccurred("success"); tg.sendData(encoded); } catch (error) { console.error("Telegram sendData failed", error); showToast("Telegram не принял запись. Обновите приложение и попробуйте снова"); } } async function fetchJson(url) { const response = await fetch(url, { headers: { Accept: "application/json" } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } async function loadSummary() { if (!userId) return; try { state.summary = await fetchJson(`../api/users/${userId}/summary`); setIdentity(state.summary); renderSummary(state.summary); } catch (error) { console.warn("Summary is not available", error); } } function renderSummary(summary) { $("#balanceValue").textContent = money.format(summary.balance_cents / 100); $("#incomeValue").textContent = money.format(summary.income_cents / 100); $("#expenseValue").textContent = money.format(summary.expense_cents / 100); const percent = Number(summary.saved_percent); $("#savedPercent").textContent = `${percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`; $("#savingProgress").style.width = `${Math.max(0, Math.min(100, percent))}%`; renderCategories(summary.categories || []); } function renderCategories(items) { const list = $("#categoryList"); if (!items.length) return; const max = Math.max(...items.map((item) => item.amount_cents)); $("#categoryCaption").textContent = `${items.length} категорий`; list.classList.remove("empty-state"); list.innerHTML = items.slice(0, 8).map((item) => `
${escapeHtml(item.category)} ${item.kind === "income" ? "+" : "−"}${money.format(item.amount_cents / 100)}
`).join(""); } function escapeHtml(value) { const element = document.createElement("span"); element.textContent = value; return element.innerHTML; } function bindLeaderboard() { $("#allBoardButton").addEventListener("click", () => switchBoard("all")); $("#friendsBoardButton").addEventListener("click", () => switchBoard("friends")); $("#shareButton").addEventListener("click", shareInvite); } function switchBoard(board) { state.board = board; $("#allBoardButton").classList.toggle("active", board === "all"); $("#friendsBoardButton").classList.toggle("active", board === "friends"); loadLeaderboard(); } async function loadLeaderboard() { const list = $("#leaderboardList"); list.className = "leaderboard-list loading"; list.textContent = "Загружаем рейтинг…"; $("#friendHint").classList.add("hidden"); try { let url = "../api/leaderboard?limit=100"; if (state.board === "friends") { if (!userId) throw new Error("No Telegram user"); const friends = await fetchJson(`../api/users/${userId}/friends`); const ids = [userId, ...friends.user_ids]; url = `../api/leaderboard?user_ids=${encodeURIComponent(ids.join(","))}`; $("#friendHint").classList.toggle("hidden", friends.user_ids.length > 0); } const data = await fetchJson(url); renderLeaderboard(data.items || []); } catch (error) { list.textContent = "Не удалось загрузить рейтинг"; console.warn("Leaderboard is not available", error); } } function renderLeaderboard(items) { const list = $("#leaderboardList"); list.className = "leaderboard-list"; if (!items.length) { list.classList.add("loading"); list.textContent = "В рейтинге пока никого нет"; return; } list.innerHTML = items.map((item, index) => { const initial = escapeHtml((item.nickname || "?").slice(0, 1).toUpperCase()); const avatar = item.avatar_url ? `` : `${initial}`; const percent = Number(item.saved_percent); return `
${index + 1} ${avatar} ${escapeHtml(item.nickname)}${item.user_id === userId ? "Это вы" : money.format(item.balance_cents / 100)} ${percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%
`; }).join(""); } async function shareInvite() { if (!userId) { showToast("Откройте приложение из Telegram"); return; } try { state.config ||= await fetchJson("../api/config"); if (!state.config.bot_username) throw new Error("Bot username is missing"); const invite = `https://t.me/${state.config.bot_username}?start=friend_${userId}`; const shareUrl = `https://t.me/share/url?url=${encodeURIComponent(invite)}&text=${encodeURIComponent("Давай копить вместе в Пожитках")}`; if (tg?.openTelegramLink) tg.openTelegramLink(shareUrl); else window.open(shareUrl, "_blank", "noopener,noreferrer"); } catch (error) { showToast("Ссылка появится после запуска бота"); } } initTelegram(); setIdentity(); bindNavigation(); bindForm(); bindLeaderboard(); fillCategories(); loadSummary(); })();