(function () { const API_BASE_URL = "https://jvapi.haiyihy.com"; const PAGE_SIZE = 20; const supportedLanguages = ["en", "ar", "tr", "id"]; const languageLabels = { en: "EN", ar: "AR", tr: "TR", id: "ID" }; const translations = { en: { document_title: "Host List", page_label: "Host List", back: "Back", title: "Host List", host_list: "Host List", team_member_subtitle: "Manage hosts in your organization", members: "Members", language_button_aria: "Change language", search: "Search", cancel: "Cancel", confirm: "Confirm", tips: "Tips", enter_host_id: "Enter host ID", uid_prefix: "UID:", target: "Target", work_time: "Work Time", loading: "Loading", loading_more: "Loading more", no_more_data: "No more data", no_members: "No hosts", no_search_result: "No matching hosts", remove_member: "Remove host", confirm_remove_member_start: "Confirm remove", confirm_remove_member_end: "from your organization?", member_removed: "Host removed", missing_token: "Missing token", missing_token_text: "Open this page from the app or add a token in the URL.", missing_team: "Missing team information", failed_to_load_data: "Failed to load data", failed_to_delete_member: "Failed to remove host", operation_failed: "Operation failed", unknown_user: "Unknown user" }, ar: { document_title: "قائمة المضيفين", page_label: "قائمة المضيفين", back: "رجوع", title: "قائمة المضيفين", host_list: "قائمة المضيفين", team_member_subtitle: "إدارة المضيفين في منظمتك", members: "الأعضاء", language_button_aria: "تغيير اللغة", search: "بحث", cancel: "إلغاء", confirm: "تأكيد", tips: "تنبيه", enter_host_id: "أدخل معرف المضيف", uid_prefix: "UID:", target: "الهدف", work_time: "وقت العمل", loading: "جار التحميل", loading_more: "جار تحميل المزيد", no_more_data: "لا توجد بيانات أخرى", no_members: "لا يوجد مضيفون", no_search_result: "لا توجد نتائج مطابقة", remove_member: "إزالة المضيف", confirm_remove_member_start: "تأكيد إزالة", confirm_remove_member_end: "من منظمتك؟", member_removed: "تمت إزالة المضيف", missing_token: "رمز الدخول مفقود", missing_token_text: "افتح هذه الصفحة من التطبيق أو أضف token في الرابط.", missing_team: "معلومات الفريق مفقودة", failed_to_load_data: "فشل تحميل البيانات", failed_to_delete_member: "فشلت إزالة المضيف", operation_failed: "فشلت العملية", unknown_user: "مستخدم غير معروف" }, tr: { document_title: "Host Listesi", page_label: "Host Listesi", back: "Geri", title: "Host Listesi", host_list: "Host Listesi", team_member_subtitle: "Organizasyonunuzdaki hostlari yonetin", members: "Uyeler", language_button_aria: "Dili degistir", search: "Ara", cancel: "Iptal", confirm: "Onayla", tips: "Ipuclari", enter_host_id: "Host ID girin", uid_prefix: "UID:", target: "Hedef", work_time: "Calisma Suresi", loading: "Yukleniyor", loading_more: "Daha fazla yukleniyor", no_more_data: "Baska veri yok", no_members: "Host yok", no_search_result: "Eslesen host yok", remove_member: "Hostu kaldir", confirm_remove_member_start: "Kaldirmayi onayla:", confirm_remove_member_end: "organizasyonunuzdan?", member_removed: "Host kaldirildi", missing_token: "Token eksik", missing_token_text: "Bu sayfayi uygulamadan acin veya URL'ye token ekleyin.", missing_team: "Takim bilgisi eksik", failed_to_load_data: "Veri yuklenemedi", failed_to_delete_member: "Host kaldirilamadi", operation_failed: "Islem basarisiz", unknown_user: "Bilinmeyen kullanici" }, id: { document_title: "Daftar Host", page_label: "Daftar Host", back: "Kembali", title: "Daftar Host", host_list: "Daftar Host", team_member_subtitle: "Kelola host di organisasi Anda", members: "Anggota", language_button_aria: "Ubah bahasa", search: "Cari", cancel: "Batal", confirm: "Konfirmasi", tips: "Tips", enter_host_id: "Masukkan ID host", uid_prefix: "UID:", target: "Target", work_time: "Waktu Kerja", loading: "Memuat", loading_more: "Memuat lagi", no_more_data: "Tidak ada data lagi", no_members: "Tidak ada host", no_search_result: "Tidak ada host yang cocok", remove_member: "Hapus host", confirm_remove_member_start: "Konfirmasi hapus", confirm_remove_member_end: "dari organisasi Anda?", member_removed: "Host dihapus", missing_token: "Token tidak ditemukan", missing_token_text: "Buka halaman ini dari aplikasi atau tambahkan token di URL.", missing_team: "Informasi tim tidak ditemukan", failed_to_load_data: "Gagal memuat data", failed_to_delete_member: "Gagal menghapus host", operation_failed: "Operasi gagal", unknown_user: "Pengguna tidak dikenal" } }; const params = readURLParams(); let currentLang = normalizeLanguage(params.get("lang")); let currentMessages = translations[currentLang] || translations.en; let currentTeamId = params.get("teamId") || params.get("id") || ""; let currentTotalMembers = parseOptionalNumber(params.get("members")); let currentProfile = null; let members = []; let displayMembers = []; let lastId = ""; let noMoreData = false; let loadingList = false; let loadingMore = false; let searchActive = false; let pendingRemoveMember = null; let loadObserver = null; function readURLParams() { const result = new URLSearchParams(window.location.search); const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : ""; const hashParams = new URLSearchParams(hashQuery); hashParams.forEach((value, key) => { if (!result.has(key)) result.set(key, value); }); return result; } function readRawParam(name) { const queries = [window.location.search.replace(/^\?/, "")]; if (window.location.hash.includes("?")) queries.push(window.location.hash.split("?")[1]); for (const query of queries) { for (const part of query.split("&")) { if (!part) continue; const equalIndex = part.indexOf("="); const rawKey = equalIndex >= 0 ? part.slice(0, equalIndex) : part; const rawValue = equalIndex >= 0 ? part.slice(equalIndex + 1) : ""; let decodedKey = ""; try { decodedKey = decodeURIComponent(rawKey); } catch (error) { continue; } if (decodedKey !== name) continue; try { return decodeURIComponent(rawValue.replace(/\+/g, "%2B")); } catch (error) { return rawValue; } } } return ""; } function normalizeLanguage(lang) { const value = String(lang || "en").toLowerCase(); return supportedLanguages.includes(value) ? value : "en"; } function t(key, fallback) { return currentMessages[key] || translations.en[key] || fallback || key; } function parseOptionalNumber(value) { if (value === null || value === undefined || value === "") return null; const number = Number(value); return Number.isFinite(number) ? number : null; } function apiBaseURL() { const override = window.HYAPP_API_BASE_URL || params.get("apiBase"); if (override) return String(override).replace(/\/+$/, ""); return API_BASE_URL.replace(/\/+$/, ""); } function normalizeAuthorization(token) { const value = String(token || "").trim(); if (!value) return ""; return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`; } function authHeadersFromToken(token) { return { Authorization: normalizeAuthorization(token), "req-lang": currentLang, "req-client": "H5", "req-version": "V2", "req-zone": Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai", "req-app-intel": "version=1.0.0;build=1;model=H5;sysVersion=Web;channel=Web", "req-sys-origin": "origin=ATYOU;originChild=ATYOU" }; } async function requestJSON(path, options = {}) { const token = readRawParam("token"); if (!token) throw new Error(t("missing_token", "Missing token")); const headers = authHeadersFromToken(token); const method = options.method || "GET"; const hasBody = options.body !== undefined; if (hasBody) headers["Content-Type"] = "application/json"; const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), { method, cache: "no-store", headers, body: hasBody ? JSON.stringify(options.body) : undefined }); const data = await response.json().catch(() => ({})); if (!response.ok || data.status === false) { const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); error.response = data; throw error; } return data.body ?? data.data ?? null; } function buildPageURL(target) { const url = new URL(target, window.location.href); const nextParams = new URLSearchParams(url.search); params.forEach((value, key) => { if (!nextParams.has(key)) nextParams.set(key, value); }); const token = readRawParam("token"); if (token) nextParams.set("token", token); if (currentLang) nextParams.set("lang", currentLang); if (currentTeamId) nextParams.set("teamId", currentTeamId); url.search = nextParams.toString(); return url.toString(); } function setLoading(isLoading) { const root = document.querySelector(".host-list-page"); const loadingMask = document.querySelector("#loadingMask"); if (root) root.dataset.loading = isLoading ? "true" : "false"; if (loadingMask) { loadingMask.hidden = !isLoading; loadingMask.setAttribute("aria-busy", String(isLoading)); } } function renderLanguageControls() { const languageButton = document.querySelector(".language-button"); if (languageButton) { languageButton.textContent = languageLabels[currentLang]; languageButton.setAttribute("aria-label", t("language_button_aria")); } document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { const isActive = button.dataset.lang === currentLang; button.classList.toggle("is-active", isActive); button.setAttribute("aria-pressed", String(isActive)); }); } function renderMessages() { document.querySelectorAll("[data-i18n]").forEach((node) => { const value = currentMessages[node.dataset.i18n]; if (value) node.textContent = value; }); document.querySelectorAll("[data-i18n-aria]").forEach((node) => { const value = currentMessages[node.dataset.i18nAria]; if (value) node.setAttribute("aria-label", value); }); document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => { const value = currentMessages[node.dataset.i18nPlaceholder]; if (value) node.setAttribute("placeholder", value); }); } function applyLanguage(lang) { currentLang = normalizeLanguage(lang); currentMessages = translations[currentLang] || translations.en; document.documentElement.lang = currentLang; document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; document.title = t("document_title"); renderMessages(); renderLanguageControls(); renderTotalMembers(); renderList(); renderRemoveConfirm(); } function setLanguageMenuOpen(open) { const languageButton = document.querySelector(".language-button"); const languageMenu = document.querySelector(".language-menu"); if (!languageButton || !languageMenu) return; languageMenu.hidden = !open; languageButton.setAttribute("aria-expanded", String(open)); } function pickTeamInfo(body) { return body?.teamProfile || body || null; } function pickTeamId(teamInfo) { return teamInfo?.id || teamInfo?.teamId || teamInfo?.teamProfile?.id || ""; } function pickMemberCount(body) { if (typeof body === "number") return body; if (body && typeof body === "object") { return body.memberQuantity ?? body.count ?? body.total ?? body.memberCount ?? null; } return null; } function pickRecords(body) { if (Array.isArray(body)) return body; if (Array.isArray(body?.records)) return body.records; if (Array.isArray(body?.list)) return body.list; if (Array.isArray(body?.items)) return body.items; return []; } function pickMemberProfile(member) { return member?.userProfile || member?.memberProfile || member?.profile || member?.user || {}; } function pickAccount(profile, member) { return profile?.ownSpecialId?.account || profile?.account || member?.account || member?.userAccount || member?.userId || member?.id || "-"; } function pickMemberName(profile, member) { return profile?.userNickname || profile?.name || member?.userNickname || member?.name || t("unknown_user"); } function collectUserIdentifiers(profile = {}) { return [ profile?.id, profile?.account, profile?.ownSpecialId?.account ] .filter((value) => value !== null && value !== undefined && value !== "") .map((value) => String(value)); } function collectMemberUserIdentifiers(member = {}) { const profile = pickMemberProfile(member); return [ profile?.id, profile?.account, profile?.ownSpecialId?.account, member?.userId, member?.userAccount, member?.account ] .filter((value) => value !== null && value !== undefined && value !== "") .map((value) => String(value)); } function isCurrentUserMember(member) { const currentIds = new Set(collectUserIdentifiers(currentProfile)); if (!currentIds.size) return false; return collectMemberUserIdentifiers(member).some((value) => currentIds.has(value)); } function formatCountValue(value) { if (value === null || value === undefined || value === "") return "-"; const number = Number(value); if (Number.isFinite(number) && Number.isInteger(number)) return String(number); if (Number.isFinite(number)) return String(Number(number.toFixed(2))); return String(value); } function normalizeMember(member) { const profile = pickMemberProfile(member); const name = pickMemberName(profile, member); return { raw: member, id: member?.id || member?.memberId || member?.userId || profile?.id || "", name, avatar: profile?.userAvatar || member?.userAvatar || member?.avatar || "", account: pickAccount(profile, member), target: member?.targetFormat ?? member?.target ?? member?.giftTarget ?? member?.giftValue ?? "-", time: member?.timeHours ?? member?.onlineTime ?? member?.workTime ?? "-", isSelf: isCurrentUserMember(member) }; } function createSvg(paths) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); paths.forEach((pathValue) => { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", pathValue); svg.appendChild(path); }); return svg; } function createMemberAvatar(member) { const avatar = document.createElement("div"); avatar.className = "member-avatar"; const fallback = document.createElement("span"); fallback.textContent = (member.name || t("unknown_user")).slice(0, 1).toUpperCase(); avatar.appendChild(fallback); if (member.avatar) { const image = document.createElement("img"); image.src = member.avatar; image.alt = member.name; image.addEventListener("error", () => { image.remove(); }); avatar.appendChild(image); } return avatar; } function createStat(iconPaths, label, value) { const stat = document.createElement("div"); stat.className = "member-stat"; stat.setAttribute("aria-label", `${label}: ${value}`); const icon = document.createElement("span"); icon.className = "member-stat-icon"; icon.appendChild(createSvg(iconPaths)); const text = document.createElement("strong"); text.textContent = formatCountValue(value); text.title = `${label}: ${formatCountValue(value)}`; stat.appendChild(icon); stat.appendChild(text); return stat; } function createMemberCard(member, index) { const normalized = normalizeMember(member); const card = document.createElement("article"); card.className = "host-member-card"; if (normalized.isSelf) card.classList.add("is-self"); const indexNode = document.createElement("div"); indexNode.className = "member-index"; indexNode.textContent = String(index + 1); const user = document.createElement("div"); user.className = "member-user"; user.appendChild(createMemberAvatar(normalized)); const copy = document.createElement("div"); copy.className = "member-copy"; const name = document.createElement("div"); name.className = "member-name"; name.textContent = normalized.name; const account = document.createElement("div"); account.className = "member-account"; account.textContent = `${t("uid_prefix")} ${normalized.account}`.trim(); copy.appendChild(name); copy.appendChild(account); user.appendChild(copy); const stats = document.createElement("div"); stats.className = "member-stats"; stats.appendChild(createStat(["M12 3 14.4 8.1 20 8.8 15.8 12.7 16.9 18.2 12 15.4 7.1 18.2 8.2 12.7 4 8.8 9.6 8.1Z"], t("target"), normalized.target)); stats.appendChild(createStat(["M12 6v6l4 2M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"], t("work_time"), normalized.time)); card.appendChild(indexNode); card.appendChild(user); card.appendChild(stats); if (!normalized.isSelf) { const remove = document.createElement("button"); remove.type = "button"; remove.className = "member-remove-button"; remove.setAttribute("aria-label", t("remove_member")); remove.appendChild(createSvg(["M4 7h16M10 11v6M14 11v6M6 7l1 14h10l1-14M9 7l1-3h4l1 3"])); remove.addEventListener("click", () => { setRemoveConfirmOpen(true, normalized); }); card.appendChild(remove); } return card; } function replaceNodeChildren(node, children) { while (node.firstChild) node.removeChild(node.firstChild); children.forEach((child) => node.appendChild(child)); } function setListStatus(message) { const status = document.querySelector("#listStatus"); if (status) status.textContent = message || ""; } function renderTotalMembers() { const totalNode = document.querySelector("#memberTotal"); if (!totalNode) return; const number = Number(currentTotalMembers); totalNode.textContent = Number.isFinite(number) ? String(Math.max(0, Math.trunc(number))) : "-"; } function renderSearchState() { const searchButton = document.querySelector("#memberSearchButton"); const cancelButton = document.querySelector("#memberCancelButton"); if (searchButton) searchButton.hidden = false; if (cancelButton) cancelButton.hidden = !searchActive; } function renderList() { const list = document.querySelector("#memberList"); if (!list) return; const normalizedItems = Array.isArray(displayMembers) ? displayMembers : []; replaceNodeChildren(list, normalizedItems.map((member, index) => createMemberCard(member, index))); renderSearchState(); if (loadingList) { setListStatus(`${t("loading")}...`); return; } if (loadingMore) { setListStatus(`${t("loading_more")}...`); return; } if (!normalizedItems.length) { setListStatus(searchActive ? t("no_search_result") : t("no_members")); return; } if (!searchActive && noMoreData) { setListStatus(t("no_more_data")); return; } setListStatus(""); } function renderRemoveConfirm() { const text = document.querySelector("#removeConfirmText"); if (!text) return; if (!pendingRemoveMember) { text.textContent = ""; return; } text.textContent = `${t("confirm_remove_member_start")} ${pendingRemoveMember.account || pendingRemoveMember.name} ${t("confirm_remove_member_end")}`; } function setRemoveConfirmOpen(open, member = null) { const modal = document.querySelector("#removeConfirmModal"); if (!modal) return; pendingRemoveMember = open ? member : null; modal.hidden = !open; document.body.classList.toggle("daily-modal-open", open); renderRemoveConfirm(); } function showToast(message) { const toast = document.querySelector("#toast"); if (!toast) return; toast.textContent = message; toast.hidden = false; window.clearTimeout(showToast.timer); showToast.timer = window.setTimeout(() => { toast.hidden = true; }, 2200); } async function ensureTeamId() { if (currentTeamId) return currentTeamId; const body = await requestJSON("/team/entry"); currentTeamId = pickTeamId(pickTeamInfo(body)); return currentTeamId; } async function fetchCurrentProfile() { currentProfile = await requestJSON("/team/member/profile"); return currentProfile; } async function fetchMemberCount(teamId) { if (!teamId) return; try { const query = new URLSearchParams({ id: String(teamId) }); const body = await requestJSON(`/team/members/count?${query.toString()}`); currentTotalMembers = pickMemberCount(body); renderTotalMembers(); } catch (error) { console.error("Failed to load host count:", error); renderTotalMembers(); } } async function fetchMembers(loadMore = false) { if (!currentTeamId || loadingList || loadingMore || searchActive) return; if (loadMore && noMoreData) return; try { if (loadMore) { loadingMore = true; } else { loadingList = true; lastId = ""; noMoreData = false; members = []; displayMembers = []; } renderList(); const query = new URLSearchParams({ id: String(currentTeamId) }); if (loadMore && lastId) query.set("lastId", String(lastId)); const body = await requestJSON(`/team/members?${query.toString()}`); const nextMembers = pickRecords(body); if (loadMore) { members = members.concat(nextMembers); } else { members = nextMembers; } displayMembers = members.slice(); if (nextMembers.length > 0) { lastId = nextMembers[nextMembers.length - 1]?.id || lastId; } if (nextMembers.length < PAGE_SIZE) noMoreData = true; } catch (error) { console.error("Failed to load hosts:", error); showToast(error.message || t("failed_to_load_data")); } finally { loadingList = false; loadingMore = false; renderList(); } } async function searchMembers() { const input = document.querySelector("#memberSearchInput"); const account = String(input?.value || "").trim(); if (!account) { cancelSearch(); return; } searchActive = true; loadingList = true; displayMembers = []; renderList(); try { const query = new URLSearchParams({ account }); const body = await requestJSON(`/team/search-members?${query.toString()}`); displayMembers = pickRecords(body); } catch (error) { console.error("Failed to search hosts:", error); showToast(error.message || t("failed_to_load_data")); displayMembers = []; } finally { loadingList = false; renderList(); } } function cancelSearch() { const input = document.querySelector("#memberSearchInput"); if (input) input.value = ""; searchActive = false; displayMembers = members.slice(); renderList(); } async function removeCurrentMember() { if (!pendingRemoveMember?.id) return; if (pendingRemoveMember.isSelf) return; const button = document.querySelector("#confirmRemoveButton"); if (button) button.disabled = true; try { await requestJSON("/team-member/client/removeMemberTeam", { method: "POST", body: { ids: [pendingRemoveMember.id] } }); const removedId = String(pendingRemoveMember.id); members = members.filter((member) => String(normalizeMember(member).id) !== removedId); displayMembers = displayMembers.filter((member) => String(normalizeMember(member).id) !== removedId); if (Number.isFinite(Number(currentTotalMembers))) { currentTotalMembers = Math.max(0, Number(currentTotalMembers) - 1); } renderTotalMembers(); renderList(); setRemoveConfirmOpen(false); showToast(t("member_removed")); } catch (error) { console.error("Failed to remove host:", error); showToast(error.message || t("failed_to_delete_member")); } finally { if (button) button.disabled = false; } } function goBack() { if (window.history.length > 1) { window.history.back(); return; } window.location.href = buildPageURL("./index.html"); } function setupLoadObserver() { const sentinel = document.querySelector("#loadMoreSentinel"); const root = document.querySelector(".host-list-page"); if (!sentinel || !root || !("IntersectionObserver" in window)) return; if (loadObserver) loadObserver.disconnect(); loadObserver = new IntersectionObserver((entries) => { if (entries[0]?.isIntersecting && !searchActive && !loadingList && !loadingMore && !noMoreData) { fetchMembers(true); } }, { root, rootMargin: "140px 0px" }); loadObserver.observe(sentinel); } function bindEvents() { document.querySelector(".back-button")?.addEventListener("click", goBack); document.querySelector(".language-button")?.addEventListener("click", (event) => { event.stopPropagation(); const languageMenu = document.querySelector(".language-menu"); setLanguageMenuOpen(Boolean(languageMenu?.hidden)); }); document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { button.addEventListener("click", (event) => { event.stopPropagation(); setLanguageMenuOpen(false); applyLanguage(button.dataset.lang); }); }); document.querySelector("#teamSearchForm")?.addEventListener("submit", (event) => { event.preventDefault(); searchMembers(); }); document.querySelector("#memberCancelButton")?.addEventListener("click", cancelSearch); document.querySelector("#confirmRemoveButton")?.addEventListener("click", removeCurrentMember); document.querySelectorAll("[data-close-remove]").forEach((node) => { node.addEventListener("click", () => { setRemoveConfirmOpen(false); }); }); const root = document.querySelector(".host-list-page"); root?.addEventListener("scroll", () => { if (searchActive || loadingList || loadingMore || noMoreData) return; const distanceToBottom = root.scrollHeight - root.scrollTop - root.clientHeight; if (distanceToBottom < 140) fetchMembers(true); }, { passive: true }); document.addEventListener("click", () => { setLanguageMenuOpen(false); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { setRemoveConfirmOpen(false); setLanguageMenuOpen(false); } }); } async function initializePage() { setLoading(true); applyLanguage(currentLang); bindEvents(); setupLoadObserver(); renderTotalMembers(); if (!readRawParam("token")) { setLoading(false); setListStatus(t("missing_token_text")); return; } try { await ensureTeamId(); if (!currentTeamId) throw new Error(t("missing_team")); await fetchCurrentProfile(); await Promise.allSettled([ fetchMemberCount(currentTeamId), fetchMembers(false) ]).then((results) => { results.forEach((result) => { if (result.status === "rejected") console.warn("Host list request failed:", result.reason); }); }); } catch (error) { console.error("Failed to initialize host list:", error); showToast(error.message || t("failed_to_load_data")); setListStatus(error.message || t("failed_to_load_data")); } finally { setLoading(false); } } initializePage(); })();