870 lines
29 KiB
JavaScript
870 lines
29 KiB
JavaScript
(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)) {
|
|
return new Intl.NumberFormat("en-US", {
|
|
maximumFractionDigits: 2
|
|
}).format(number);
|
|
}
|
|
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) ? new Intl.NumberFormat("en-US").format(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();
|
|
})();
|