941 lines
31 KiB
JavaScript
941 lines
31 KiB
JavaScript
(function () {
|
|
const API_BASE_URL = "https://jvapi.haiyihy.com";
|
|
const supportedLanguages = ["en", "ar", "tr", "id"];
|
|
const languageLabels = {
|
|
en: "EN",
|
|
ar: "AR",
|
|
tr: "TR",
|
|
id: "ID"
|
|
};
|
|
|
|
const translations = {
|
|
en: {
|
|
document_title: "Salary Bills",
|
|
page_label: "Salary Bills",
|
|
back: "Back",
|
|
title: "Salary Bills",
|
|
language_button_aria: "Change language",
|
|
history_team_income: "History team income",
|
|
bill_summary: "Bill summary",
|
|
total_bills: "Total bills",
|
|
completed_bills: "Completed",
|
|
work_report: "Work report",
|
|
host_salary: "Host salary",
|
|
agent_salary: "Agent salary",
|
|
total: "Total",
|
|
more: "More",
|
|
team_member: "Team member",
|
|
enter_host_id: "Enter host ID",
|
|
search: "Search",
|
|
cancel: "Cancel",
|
|
close: "Close",
|
|
help: "Help",
|
|
open_help: "Open help",
|
|
work_report_description: "Work report shows each team bill cycle and salary summary.",
|
|
status_description: "Status describes the current settlement state.",
|
|
status_completed: "Completed: salary has been settled.",
|
|
status_in_progress: "In progress: the bill is still running.",
|
|
status_out_of_account: "Out of account: the bill is suspended or unavailable.",
|
|
completed: "Completed",
|
|
in_progress: "In progress",
|
|
out_of_account: "Out of account",
|
|
pending: "Pending",
|
|
target: "Target",
|
|
days: "Days",
|
|
uid_prefix: "UID:",
|
|
gift_points: "Gift points",
|
|
loading: "Loading",
|
|
no_bills: "No bills",
|
|
no_members: "No hosts",
|
|
no_search_result: "No matching hosts",
|
|
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",
|
|
unknown_user: "Unknown user"
|
|
},
|
|
ar: {
|
|
document_title: "Salary Bills",
|
|
page_label: "Salary Bills",
|
|
back: "رجوع",
|
|
title: "Salary Bills",
|
|
language_button_aria: "تغيير اللغة",
|
|
history_team_income: "دخل الفريق السابق",
|
|
bill_summary: "ملخص الفواتير",
|
|
total_bills: "إجمالي الفواتير",
|
|
completed_bills: "مكتمل",
|
|
work_report: "تقرير العمل",
|
|
host_salary: "راتب المضيف",
|
|
agent_salary: "راتب الوكالة",
|
|
total: "الإجمالي",
|
|
more: "المزيد",
|
|
team_member: "عضو الفريق",
|
|
enter_host_id: "أدخل معرف المضيف",
|
|
search: "بحث",
|
|
cancel: "إلغاء",
|
|
close: "إغلاق",
|
|
help: "مساعدة",
|
|
open_help: "فتح المساعدة",
|
|
work_report_description: "يعرض تقرير العمل كل دورة فاتورة للفريق وملخص الراتب.",
|
|
status_description: "توضح الحالة وضع التسوية الحالي.",
|
|
status_completed: "مكتمل: تم تسوية الراتب.",
|
|
status_in_progress: "قيد التنفيذ: لا تزال الفاتورة قيد التشغيل.",
|
|
status_out_of_account: "خارج الحساب: الفاتورة موقوفة أو غير متاحة.",
|
|
completed: "مكتمل",
|
|
in_progress: "قيد التنفيذ",
|
|
out_of_account: "خارج الحساب",
|
|
pending: "قيد الانتظار",
|
|
target: "الهدف",
|
|
days: "الأيام",
|
|
uid_prefix: "UID:",
|
|
gift_points: "نقاط الهدايا",
|
|
loading: "جار التحميل",
|
|
no_bills: "لا توجد فواتير",
|
|
no_members: "لا يوجد مضيفون",
|
|
no_search_result: "لا توجد نتائج مطابقة",
|
|
missing_token: "رمز الدخول مفقود",
|
|
missing_token_text: "افتح هذه الصفحة من التطبيق أو أضف token في الرابط.",
|
|
missing_team: "معلومات الفريق مفقودة",
|
|
failed_to_load_data: "فشل تحميل البيانات",
|
|
unknown_user: "مستخدم غير معروف"
|
|
},
|
|
tr: {
|
|
document_title: "Salary Bills",
|
|
page_label: "Salary Bills",
|
|
back: "Geri",
|
|
title: "Salary Bills",
|
|
language_button_aria: "Dili degistir",
|
|
history_team_income: "Gecmis takim geliri",
|
|
bill_summary: "Fatura ozeti",
|
|
total_bills: "Toplam fatura",
|
|
completed_bills: "Tamamlandi",
|
|
work_report: "Calisma raporu",
|
|
host_salary: "Host maasi",
|
|
agent_salary: "Ajans maasi",
|
|
total: "Toplam",
|
|
more: "Daha fazla",
|
|
team_member: "Takim uyesi",
|
|
enter_host_id: "Host ID girin",
|
|
search: "Ara",
|
|
cancel: "Iptal",
|
|
close: "Kapat",
|
|
help: "Yardim",
|
|
open_help: "Yardimi ac",
|
|
work_report_description: "Calisma raporu her takim fatura dongusunu ve maas ozetini gosterir.",
|
|
status_description: "Durum mevcut odeme surecini aciklar.",
|
|
status_completed: "Tamamlandi: maas odendi.",
|
|
status_in_progress: "Devam ediyor: fatura hala calisiyor.",
|
|
status_out_of_account: "Hesap disi: fatura askida veya kullanilamiyor.",
|
|
completed: "Tamamlandi",
|
|
in_progress: "Devam ediyor",
|
|
out_of_account: "Hesap disi",
|
|
pending: "Beklemede",
|
|
target: "Hedef",
|
|
days: "Gun",
|
|
uid_prefix: "UID:",
|
|
gift_points: "Hediye puani",
|
|
loading: "Yukleniyor",
|
|
no_bills: "Fatura yok",
|
|
no_members: "Host yok",
|
|
no_search_result: "Eslesen host yok",
|
|
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",
|
|
unknown_user: "Bilinmeyen kullanici"
|
|
},
|
|
id: {
|
|
document_title: "Salary Bills",
|
|
page_label: "Salary Bills",
|
|
back: "Kembali",
|
|
title: "Salary Bills",
|
|
language_button_aria: "Ubah bahasa",
|
|
history_team_income: "Riwayat pendapatan tim",
|
|
bill_summary: "Ringkasan tagihan",
|
|
total_bills: "Total tagihan",
|
|
completed_bills: "Selesai",
|
|
work_report: "Laporan kerja",
|
|
host_salary: "Gaji host",
|
|
agent_salary: "Gaji agensi",
|
|
total: "Total",
|
|
more: "Lainnya",
|
|
team_member: "Anggota tim",
|
|
enter_host_id: "Masukkan ID host",
|
|
search: "Cari",
|
|
cancel: "Batal",
|
|
close: "Tutup",
|
|
help: "Bantuan",
|
|
open_help: "Buka bantuan",
|
|
work_report_description: "Laporan kerja menampilkan setiap siklus tagihan tim dan ringkasan gaji.",
|
|
status_description: "Status menjelaskan kondisi penyelesaian saat ini.",
|
|
status_completed: "Selesai: gaji telah diselesaikan.",
|
|
status_in_progress: "Berjalan: tagihan masih berjalan.",
|
|
status_out_of_account: "Di luar akun: tagihan ditangguhkan atau tidak tersedia.",
|
|
completed: "Selesai",
|
|
in_progress: "Berjalan",
|
|
out_of_account: "Di luar akun",
|
|
pending: "Tertunda",
|
|
target: "Target",
|
|
days: "Hari",
|
|
uid_prefix: "UID:",
|
|
gift_points: "Poin hadiah",
|
|
loading: "Memuat",
|
|
no_bills: "Tidak ada tagihan",
|
|
no_members: "Tidak ada host",
|
|
no_search_result: "Tidak ada host yang cocok",
|
|
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",
|
|
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 bills = [];
|
|
let currentPolicy = [];
|
|
let selectedBill = null;
|
|
let selectedMembers = [];
|
|
let displayMembers = [];
|
|
let memberSearchActive = false;
|
|
let detailLoading = false;
|
|
|
|
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 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(".hosts-bill-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();
|
|
renderSummary();
|
|
renderBills();
|
|
renderBillDetail();
|
|
}
|
|
|
|
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 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 pickPolicyRecords(body) {
|
|
if (Array.isArray(body)) return body;
|
|
if (Array.isArray(body?.policy)) return body.policy;
|
|
if (Array.isArray(body?.policies)) return body.policies;
|
|
return pickRecords(body);
|
|
}
|
|
|
|
function formatMoney(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? `$${number.toFixed(2)}` : "$0.00";
|
|
}
|
|
|
|
function formatPlainMoney(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number.toFixed(2) : "0.00";
|
|
}
|
|
|
|
function formatCount(value) {
|
|
if (value === null || value === undefined || value === "") return "0";
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return String(value);
|
|
return Math.trunc(number).toLocaleString("en-US");
|
|
}
|
|
|
|
function formatGift(value) {
|
|
if (value === null || value === undefined || value === "") return "0";
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return String(value);
|
|
return Math.trunc(number).toLocaleString("en-US");
|
|
}
|
|
|
|
function normalizeStatus(status) {
|
|
switch (String(status || "").toUpperCase()) {
|
|
case "SETTLED":
|
|
case "PAY_OUT":
|
|
case "COMPLETED":
|
|
return { key: "completed", className: "status-completed" };
|
|
case "HANG_UP":
|
|
case "OUT_OF_ACCOUNT":
|
|
case "OUT OF ACCOUNT":
|
|
return { key: "out_of_account", className: "status-out-of-account" };
|
|
case "PENDING":
|
|
return { key: "pending", className: "status-pending" };
|
|
case "UNPAID":
|
|
case "IN_PROGRESS":
|
|
case "IN PROGRESS":
|
|
default:
|
|
return { key: "in_progress", className: "status-in-progress" };
|
|
}
|
|
}
|
|
|
|
function statusText(status) {
|
|
const info = normalizeStatus(status);
|
|
return t(info.key, status || "-");
|
|
}
|
|
|
|
function setStatusBadge(node, status) {
|
|
if (!node) return;
|
|
const info = normalizeStatus(status);
|
|
node.className = `bill-status-badge ${info.className}`;
|
|
node.textContent = t(info.key);
|
|
}
|
|
|
|
function billTitle(bill) {
|
|
return bill?.billTitle || bill?.billDayTitle || String(bill?.billBelong || "-");
|
|
}
|
|
|
|
function billTotal(bill) {
|
|
return Number(bill?.settleResult?.totalSalary || 0);
|
|
}
|
|
|
|
function billMemberSalary(bill) {
|
|
return Number(bill?.settleResult?.memberSalary || 0);
|
|
}
|
|
|
|
function billAgentSalary(bill) {
|
|
return Number(bill?.settleResult?.ownSalary || 0);
|
|
}
|
|
|
|
function isCompletedBill(bill) {
|
|
const status = String(bill?.status || "").toUpperCase();
|
|
return status === "SETTLED" || status === "PAY_OUT" || status === "COMPLETED";
|
|
}
|
|
|
|
function calculateHistoryIncome() {
|
|
return bills.reduce((sum, bill) => {
|
|
if (!isCompletedBill(bill)) return sum;
|
|
return sum + billTotal(bill);
|
|
}, 0);
|
|
}
|
|
|
|
function replaceNodeChildren(node, children) {
|
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
children.forEach((child) => node.appendChild(child));
|
|
}
|
|
|
|
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 renderSummary() {
|
|
const historyIncome = document.querySelector("#historyIncome");
|
|
const totalBills = document.querySelector("#totalBills");
|
|
const completedBills = document.querySelector("#completedBills");
|
|
|
|
if (historyIncome) historyIncome.textContent = formatMoney(calculateHistoryIncome());
|
|
if (totalBills) totalBills.textContent = formatCount(bills.length);
|
|
if (completedBills) completedBills.textContent = formatCount(bills.filter(isCompletedBill).length);
|
|
}
|
|
|
|
function createMoneyItem(label, value) {
|
|
const item = document.createElement("div");
|
|
item.className = "bill-money-item";
|
|
|
|
const labelNode = document.createElement("span");
|
|
labelNode.textContent = label;
|
|
|
|
const valueNode = document.createElement("strong");
|
|
valueNode.textContent = formatMoney(value);
|
|
|
|
item.appendChild(labelNode);
|
|
item.appendChild(valueNode);
|
|
return item;
|
|
}
|
|
|
|
function createBillCard(bill) {
|
|
const card = document.createElement("article");
|
|
card.className = "bill-card";
|
|
|
|
const top = document.createElement("div");
|
|
top.className = "bill-card-top";
|
|
|
|
const date = document.createElement("div");
|
|
date.className = "bill-date";
|
|
date.textContent = billTitle(bill);
|
|
|
|
const badge = document.createElement("span");
|
|
setStatusBadge(badge, bill?.status);
|
|
|
|
top.appendChild(date);
|
|
top.appendChild(badge);
|
|
|
|
const moneyGrid = document.createElement("div");
|
|
moneyGrid.className = "bill-money-grid";
|
|
moneyGrid.appendChild(createMoneyItem(t("host_salary"), billMemberSalary(bill)));
|
|
moneyGrid.appendChild(createMoneyItem(t("agent_salary"), billAgentSalary(bill)));
|
|
moneyGrid.appendChild(createMoneyItem(t("total"), billTotal(bill)));
|
|
|
|
const more = document.createElement("button");
|
|
more.type = "button";
|
|
more.className = "bill-more-button";
|
|
more.textContent = t("more");
|
|
more.addEventListener("click", () => {
|
|
openBillDetail(bill);
|
|
});
|
|
|
|
card.appendChild(top);
|
|
card.appendChild(moneyGrid);
|
|
card.appendChild(more);
|
|
return card;
|
|
}
|
|
|
|
function setListStatus(message) {
|
|
const status = document.querySelector("#listStatus");
|
|
if (status) status.textContent = message || "";
|
|
}
|
|
|
|
function renderBills() {
|
|
renderSummary();
|
|
const list = document.querySelector("#billList");
|
|
if (!list) return;
|
|
replaceNodeChildren(list, bills.map(createBillCard));
|
|
setListStatus(bills.length ? "" : t("no_bills"));
|
|
}
|
|
|
|
function pickMemberProfile(member) {
|
|
return member?.memberProfile || member?.userProfile || member?.profile || member?.user || {};
|
|
}
|
|
|
|
function pickMemberTarget(member) {
|
|
return member?.target || member?.memberTarget || {};
|
|
}
|
|
|
|
function pickMemberName(profile, member) {
|
|
return profile?.userNickname || profile?.name || member?.userNickname || member?.name || t("unknown_user");
|
|
}
|
|
|
|
function pickMemberAccount(profile, member) {
|
|
return profile?.ownSpecialId?.account || profile?.account || member?.account || member?.userAccount || profile?.id || member?.userId || "-";
|
|
}
|
|
|
|
function memberIdentifiers(member) {
|
|
const profile = pickMemberProfile(member);
|
|
return [
|
|
profile?.id,
|
|
profile?.account,
|
|
profile?.ownSpecialId?.account,
|
|
member?.userId,
|
|
member?.account,
|
|
member?.userAccount
|
|
]
|
|
.filter((value) => value !== null && value !== undefined && value !== "")
|
|
.map((value) => String(value));
|
|
}
|
|
|
|
function calculateTargetLevel(acceptGiftValue) {
|
|
if (!Array.isArray(currentPolicy) || !currentPolicy.length) return "N/A";
|
|
const giftValue = Number(acceptGiftValue);
|
|
if (!Number.isFinite(giftValue)) return "N/A";
|
|
|
|
const sortedPolicy = currentPolicy
|
|
.slice()
|
|
.filter((item) => item && item.target !== null && item.target !== undefined)
|
|
.sort((left, right) => Number(right.target) - Number(left.target));
|
|
|
|
for (const policy of sortedPolicy) {
|
|
if (giftValue >= Number(policy.target)) return policy.level ?? "N/A";
|
|
}
|
|
|
|
const levels = sortedPolicy
|
|
.map((policy) => Number(policy.level))
|
|
.filter((level) => Number.isFinite(level));
|
|
return levels.length ? Math.min(...levels) : "N/A";
|
|
}
|
|
|
|
function createMemberAvatar(name, avatarURL) {
|
|
const avatar = document.createElement("div");
|
|
avatar.className = "bill-member-avatar";
|
|
|
|
const fallback = document.createElement("span");
|
|
fallback.textContent = (name || t("unknown_user")).slice(0, 1).toUpperCase();
|
|
avatar.appendChild(fallback);
|
|
|
|
if (avatarURL) {
|
|
const image = document.createElement("img");
|
|
image.src = avatarURL;
|
|
image.alt = name;
|
|
image.addEventListener("error", () => {
|
|
image.remove();
|
|
});
|
|
avatar.appendChild(image);
|
|
}
|
|
|
|
return avatar;
|
|
}
|
|
|
|
function createMemberCard(member) {
|
|
const profile = pickMemberProfile(member);
|
|
const target = pickMemberTarget(member);
|
|
const name = pickMemberName(profile, member);
|
|
const account = pickMemberAccount(profile, member);
|
|
const giftValue = target?.acceptGiftValue || 0;
|
|
const effectiveDay = target?.effectiveDay || 0;
|
|
|
|
const card = document.createElement("article");
|
|
card.className = "bill-member-card";
|
|
|
|
const user = document.createElement("div");
|
|
user.className = "bill-member-user";
|
|
user.appendChild(createMemberAvatar(name, profile?.userAvatar || member?.userAvatar || member?.avatar || ""));
|
|
|
|
const copy = document.createElement("div");
|
|
copy.className = "bill-member-copy";
|
|
|
|
const nameNode = document.createElement("div");
|
|
nameNode.className = "bill-member-name";
|
|
nameNode.textContent = name;
|
|
|
|
const accountNode = document.createElement("div");
|
|
accountNode.className = "bill-member-account";
|
|
accountNode.textContent = `${t("uid_prefix")} ${account}`.trim();
|
|
|
|
copy.appendChild(nameNode);
|
|
copy.appendChild(accountNode);
|
|
user.appendChild(copy);
|
|
|
|
const side = document.createElement("div");
|
|
side.className = "bill-member-side";
|
|
|
|
const level = document.createElement("span");
|
|
level.className = "bill-member-target";
|
|
level.textContent = `${t("target")}: ${calculateTargetLevel(giftValue)}`;
|
|
|
|
const gift = document.createElement("div");
|
|
gift.className = "bill-member-gift";
|
|
gift.textContent = formatGift(giftValue);
|
|
gift.title = `${t("gift_points")}: ${formatGift(giftValue)}`;
|
|
|
|
const days = document.createElement("div");
|
|
days.className = "bill-member-days";
|
|
days.textContent = `${t("days")}: ${effectiveDay}`;
|
|
|
|
side.appendChild(level);
|
|
side.appendChild(gift);
|
|
side.appendChild(days);
|
|
|
|
card.appendChild(user);
|
|
card.appendChild(side);
|
|
return card;
|
|
}
|
|
|
|
function setMemberStatus(message) {
|
|
const status = document.querySelector("#memberStatus");
|
|
if (status) status.textContent = message || "";
|
|
}
|
|
|
|
function renderSearchState() {
|
|
const cancelButton = document.querySelector("#memberCancelButton");
|
|
if (cancelButton) cancelButton.hidden = !memberSearchActive;
|
|
}
|
|
|
|
function renderBillDetail() {
|
|
const date = document.querySelector("#detailBillDate");
|
|
const total = document.querySelector("#detailBillTotal");
|
|
const status = document.querySelector("#detailBillStatus");
|
|
const list = document.querySelector("#billMemberList");
|
|
|
|
if (date) date.textContent = selectedBill ? billTitle(selectedBill) : "-";
|
|
if (total) total.textContent = selectedBill ? formatMoney(billTotal(selectedBill)) : "$0.00";
|
|
if (status && selectedBill) setStatusBadge(status, selectedBill.status);
|
|
if (list) replaceNodeChildren(list, displayMembers.map(createMemberCard));
|
|
|
|
renderSearchState();
|
|
|
|
if (detailLoading) {
|
|
setMemberStatus(`${t("loading")}...`);
|
|
return;
|
|
}
|
|
|
|
if (!displayMembers.length) {
|
|
setMemberStatus(memberSearchActive ? t("no_search_result") : t("no_members"));
|
|
return;
|
|
}
|
|
|
|
setMemberStatus("");
|
|
}
|
|
|
|
async function ensureTeamId() {
|
|
if (currentTeamId) return currentTeamId;
|
|
const body = await requestJSON("/team/entry");
|
|
currentTeamId = pickTeamId(pickTeamInfo(body));
|
|
return currentTeamId;
|
|
}
|
|
|
|
async function fetchBills() {
|
|
const query = new URLSearchParams({ id: String(currentTeamId) });
|
|
const body = await requestJSON(`/team/bill?${query.toString()}`);
|
|
bills = pickRecords(body);
|
|
renderBills();
|
|
}
|
|
|
|
async function fetchPolicy() {
|
|
const query = new URLSearchParams({ id: String(currentTeamId) });
|
|
const body = await requestJSON(`/team/release/policy?${query.toString()}`);
|
|
currentPolicy = pickPolicyRecords(body);
|
|
}
|
|
|
|
async function fetchBillMembers(billId) {
|
|
const query = new URLSearchParams({ id: String(billId) });
|
|
const body = await requestJSON(`/team/bill/work-members?${query.toString()}`);
|
|
return pickRecords(body);
|
|
}
|
|
|
|
async function openBillDetail(bill) {
|
|
selectedBill = bill;
|
|
selectedMembers = [];
|
|
displayMembers = [];
|
|
memberSearchActive = false;
|
|
detailLoading = true;
|
|
const input = document.querySelector("#memberSearchInput");
|
|
if (input) input.value = "";
|
|
|
|
const modal = document.querySelector("#billDetailModal");
|
|
if (modal) modal.hidden = false;
|
|
document.body.classList.add("daily-modal-open");
|
|
renderBillDetail();
|
|
|
|
try {
|
|
selectedMembers = await fetchBillMembers(bill.id);
|
|
displayMembers = selectedMembers.slice();
|
|
} catch (error) {
|
|
console.error("Failed to load bill members:", error);
|
|
showToast(error.message || t("failed_to_load_data"));
|
|
selectedMembers = [];
|
|
displayMembers = [];
|
|
} finally {
|
|
detailLoading = false;
|
|
renderBillDetail();
|
|
}
|
|
}
|
|
|
|
function setBillDetailOpen(open) {
|
|
const modal = document.querySelector("#billDetailModal");
|
|
if (!modal) return;
|
|
modal.hidden = !open;
|
|
document.body.classList.toggle("daily-modal-open", open || isHelpOpen());
|
|
if (!open) {
|
|
selectedBill = null;
|
|
selectedMembers = [];
|
|
displayMembers = [];
|
|
memberSearchActive = false;
|
|
detailLoading = false;
|
|
}
|
|
}
|
|
|
|
function isHelpOpen() {
|
|
return !document.querySelector("#helpModal")?.hidden;
|
|
}
|
|
|
|
function setHelpOpen(open) {
|
|
const modal = document.querySelector("#helpModal");
|
|
if (!modal) return;
|
|
modal.hidden = !open;
|
|
document.body.classList.toggle("daily-modal-open", open || !document.querySelector("#billDetailModal")?.hidden);
|
|
}
|
|
|
|
function searchMembers() {
|
|
const input = document.querySelector("#memberSearchInput");
|
|
const query = String(input?.value || "").trim();
|
|
if (!query) {
|
|
cancelMemberSearch();
|
|
return;
|
|
}
|
|
|
|
memberSearchActive = true;
|
|
displayMembers = selectedMembers.filter((member) => memberIdentifiers(member).some((id) => id === query));
|
|
renderBillDetail();
|
|
}
|
|
|
|
function cancelMemberSearch() {
|
|
const input = document.querySelector("#memberSearchInput");
|
|
if (input) input.value = "";
|
|
memberSearchActive = false;
|
|
displayMembers = selectedMembers.slice();
|
|
renderBillDetail();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function goBack() {
|
|
if (window.history.length > 1) {
|
|
window.history.back();
|
|
return;
|
|
}
|
|
window.location.href = buildPageURL("../agency-center/index.html");
|
|
}
|
|
|
|
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(".bill-help-button")?.addEventListener("click", () => {
|
|
setHelpOpen(true);
|
|
});
|
|
|
|
document.querySelector("#memberSearchForm")?.addEventListener("submit", (event) => {
|
|
event.preventDefault();
|
|
searchMembers();
|
|
});
|
|
|
|
document.querySelector("#memberCancelButton")?.addEventListener("click", cancelMemberSearch);
|
|
|
|
document.querySelectorAll("[data-close-detail]").forEach((node) => {
|
|
node.addEventListener("click", () => setBillDetailOpen(false));
|
|
});
|
|
|
|
document.querySelectorAll("[data-close-help]").forEach((node) => {
|
|
node.addEventListener("click", () => setHelpOpen(false));
|
|
});
|
|
|
|
document.addEventListener("click", () => {
|
|
setLanguageMenuOpen(false);
|
|
});
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") {
|
|
setLanguageMenuOpen(false);
|
|
setBillDetailOpen(false);
|
|
setHelpOpen(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function initializePage() {
|
|
setLoading(true);
|
|
applyLanguage(currentLang);
|
|
bindEvents();
|
|
|
|
if (!readRawParam("token")) {
|
|
setLoading(false);
|
|
setListStatus(t("missing_token_text"));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await ensureTeamId();
|
|
if (!currentTeamId) throw new Error(t("missing_team"));
|
|
|
|
const results = await Promise.allSettled([
|
|
fetchBills(),
|
|
fetchPolicy()
|
|
]);
|
|
results.forEach((result) => {
|
|
if (result.status === "rejected") console.warn("Hosts bill request failed:", result.reason);
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to initialize hosts bill:", error);
|
|
showToast(error.message || t("failed_to_load_data"));
|
|
setListStatus(error.message || t("failed_to_load_data"));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
initializePage();
|
|
})();
|