2026-06-01 21:00:03 +08:00

1406 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
const API_BASE_URL = "https://jvapi.haiyihy.com";
const EXCHANGE_RATE = 80000;
const WITHDRAW_MIN_AMOUNT = 50;
const supportedLanguages = ["en", "ar", "tr", "id"];
const languageLabels = {
en: "EN",
ar: "AR",
tr: "TR",
id: "ID"
};
const translations = {
en: {
page_label: "Withdraw and exchange",
back: "Back",
title: "Withdraw/Exchange",
language_button_aria: "Change language",
available_income: "salary",
bd_salary: "BD salary",
bd_leader_salary: "BD Leader salary",
withdraw: "Withdraw",
exchange: "Exchange",
transfer: "Transfer",
withdraw_amount: "Withdraw amount",
exchange_amount: "Exchange amount",
transfer_amount: "Transfer amount",
transfer_to: "Transfer to",
search: "Search",
searching: "Searching...",
minimum: "Minimum 50 USD",
all: "All",
exchange_rate: "Exchange rate",
payment_method: "USDT TRC20 address",
payment_method_subtitle: "Withdrawals are sent to your TRC20 USDT address.",
no_payment_info: "No USDT address",
no_payment_info_copy: "Fill in a TRC20 USDT address before withdrawing.",
trc20_address: "TRC20 address",
save_address: "Save address",
saving: "Saving...",
usdt_only: "USDT only",
withdraw_now: "Withdraw now",
exchange_now: "Exchange now",
transfer_now: "Transfer now",
submitting: "Submitting...",
payee: "Payee",
uid_prefix: "UID:",
missing_token: "Missing token",
load_balance_failed: "Failed to load balance",
load_payment_failed: "Failed to load payment info",
missing_payment_info: "Please add a TRC20 USDT address",
invalid_address: "Please enter a valid TRC20 USDT address",
invalid_amount: "Please enter a valid amount",
amount_too_low: "Minimum withdrawal amount is 50 USD",
amount_exceeds_balance: "Amount exceeds salary",
payee_not_found: "User not found",
missing_payee: "Please search and select a user",
address_saved: "Address saved",
withdraw_success: "Withdrawal submitted",
withdraw_failed: "Withdrawal failed",
exchange_success: "Exchange success",
exchange_failed: "Exchange failed",
transfer_success: "Transfer success",
transfer_failed: "Transfer failed",
salary_history: "Salary history",
close: "Close",
loading: "Loading...",
no_salary_history: "No salary history",
income: "Income",
expense: "Expense",
balance: "Balance",
period: "Period",
days: "Days",
target: "Target",
work_report: "Work report",
load_salary_history_failed: "Failed to load salary history"
},
ar: {
page_label: "السحب والتبديل",
back: "رجوع",
title: "سحب/تبديل",
language_button_aria: "تغيير اللغة",
available_income: "الراتب",
bd_salary: "راتب BD",
bd_leader_salary: "راتب قائد BD",
withdraw: "سحب",
exchange: "تبديل",
transfer: "تحويل",
withdraw_amount: "مبلغ السحب",
exchange_amount: "مبلغ التبديل",
transfer_amount: "مبلغ التحويل",
transfer_to: "تحويل إلى",
search: "بحث",
searching: "جار البحث...",
minimum: "الحد الأدنى 50 USD",
all: "الكل",
exchange_rate: "سعر التبديل",
payment_method: "عنوان USDT TRC20",
payment_method_subtitle: "يتم إرسال السحب إلى عنوان USDT TRC20.",
no_payment_info: "لا يوجد عنوان USDT",
no_payment_info_copy: "أدخل عنوان USDT TRC20 قبل السحب.",
trc20_address: "عنوان TRC20",
save_address: "حفظ العنوان",
saving: "جار الحفظ...",
usdt_only: "USDT فقط",
withdraw_now: "اسحب الآن",
exchange_now: "بدل الآن",
transfer_now: "حوّل الآن",
submitting: "جار الإرسال...",
payee: "المستفيد",
uid_prefix: "UID:",
missing_token: "رمز الدخول مفقود",
load_balance_failed: "فشل تحميل الرصيد",
load_payment_failed: "فشل تحميل معلومات الدفع",
missing_payment_info: "يرجى إضافة عنوان USDT TRC20",
invalid_address: "يرجى إدخال عنوان USDT TRC20 صالح",
invalid_amount: "يرجى إدخال مبلغ صالح",
amount_too_low: "الحد الأدنى للسحب هو 50 USD",
amount_exceeds_balance: "المبلغ يتجاوز الراتب",
payee_not_found: "لم يتم العثور على المستخدم",
missing_payee: "يرجى البحث واختيار مستخدم",
address_saved: "تم حفظ العنوان",
withdraw_success: "تم إرسال طلب السحب",
withdraw_failed: "فشل السحب",
exchange_success: "تم التبديل",
exchange_failed: "فشل التبديل",
transfer_success: "تم التحويل",
transfer_failed: "فشل التحويل",
salary_history: "سجل الراتب",
close: "إغلاق",
loading: "جار التحميل...",
no_salary_history: "لا يوجد سجل راتب",
income: "دخل",
expense: "مصروف",
balance: "الرصيد",
period: "الفترة",
days: "الأيام",
target: "الهدف",
work_report: "تقرير العمل",
load_salary_history_failed: "فشل تحميل سجل الراتب"
},
tr: {
page_label: "Çekme ve değişim",
back: "Geri",
title: "Çek/Değiştir",
language_button_aria: "Dili değiştir",
available_income: "Maaş",
bd_salary: "BD maaşı",
bd_leader_salary: "BD Leader maaşı",
withdraw: "Withdraw",
exchange: "Exchange",
transfer: "Transfer",
withdraw_amount: "Withdraw amount",
exchange_amount: "Exchange amount",
transfer_amount: "Transfer amount",
transfer_to: "Transfer to",
search: "Search",
searching: "Searching...",
minimum: "Minimum 50 USD",
all: "All",
exchange_rate: "Exchange rate",
payment_method: "USDT TRC20 address",
payment_method_subtitle: "Withdrawals are sent to your TRC20 USDT address.",
no_payment_info: "No USDT address",
no_payment_info_copy: "Fill in a TRC20 USDT address before withdrawing.",
trc20_address: "TRC20 address",
save_address: "Save address",
saving: "Saving...",
usdt_only: "USDT only",
withdraw_now: "Withdraw now",
exchange_now: "Exchange now",
transfer_now: "Transfer now",
submitting: "Submitting...",
payee: "Payee",
uid_prefix: "UID:",
missing_token: "Missing token",
load_balance_failed: "Failed to load balance",
load_payment_failed: "Failed to load payment info",
missing_payment_info: "Please add a TRC20 USDT address",
invalid_address: "Please enter a valid TRC20 USDT address",
invalid_amount: "Please enter a valid amount",
amount_too_low: "Minimum withdrawal amount is 50 USD",
amount_exceeds_balance: "Amount exceeds salary",
payee_not_found: "User not found",
missing_payee: "Please search and select a user",
address_saved: "Address saved",
withdraw_success: "Withdrawal submitted",
withdraw_failed: "Withdrawal failed",
exchange_success: "Exchange success",
exchange_failed: "Exchange failed",
transfer_success: "Transfer success",
transfer_failed: "Transfer failed",
salary_history: "Salary history",
close: "Close",
loading: "Loading...",
no_salary_history: "No salary history",
income: "Income",
expense: "Expense",
balance: "Balance",
period: "Period",
days: "Days",
target: "Target",
work_report: "Work report",
load_salary_history_failed: "Failed to load salary history"
},
id: {
page_label: "Tarik dan tukar",
back: "Kembali",
title: "Tarik/Tukar",
language_button_aria: "Ubah bahasa",
available_income: "Gaji",
bd_salary: "Gaji BD",
bd_leader_salary: "Gaji BD Leader",
withdraw: "Tarik",
exchange: "Tukar",
transfer: "Transfer",
withdraw_amount: "Jumlah penarikan",
exchange_amount: "Jumlah penukaran",
transfer_amount: "Jumlah transfer",
transfer_to: "Transfer ke",
search: "Cari",
searching: "Mencari...",
minimum: "Minimum 50 USD",
all: "Semua",
exchange_rate: "Kurs penukaran",
payment_method: "Alamat USDT TRC20",
payment_method_subtitle: "Penarikan dikirim ke alamat USDT TRC20 Anda.",
no_payment_info: "Tidak ada alamat USDT",
no_payment_info_copy: "Isi alamat USDT TRC20 sebelum menarik.",
trc20_address: "Alamat TRC20",
save_address: "Simpan alamat",
saving: "Menyimpan...",
usdt_only: "Hanya USDT",
withdraw_now: "Tarik sekarang",
exchange_now: "Tukar sekarang",
transfer_now: "Transfer sekarang",
submitting: "Mengirim...",
payee: "Penerima",
uid_prefix: "UID:",
missing_token: "Token tidak ditemukan",
load_balance_failed: "Gagal memuat saldo",
load_payment_failed: "Gagal memuat informasi pembayaran",
missing_payment_info: "Tambahkan alamat USDT TRC20",
invalid_address: "Masukkan alamat USDT TRC20 yang valid",
invalid_amount: "Masukkan jumlah yang valid",
amount_too_low: "Minimum penarikan adalah 50 USD",
amount_exceeds_balance: "Jumlah melebihi gaji",
payee_not_found: "Pengguna tidak ditemukan",
missing_payee: "Cari dan pilih pengguna",
address_saved: "Alamat disimpan",
withdraw_success: "Penarikan terkirim",
withdraw_failed: "Penarikan gagal",
exchange_success: "Penukaran berhasil",
exchange_failed: "Penukaran gagal",
transfer_success: "Transfer berhasil",
transfer_failed: "Transfer gagal",
salary_history: "Riwayat gaji",
close: "Tutup",
loading: "Memuat...",
no_salary_history: "Tidak ada riwayat gaji",
income: "Masuk",
expense: "Keluar",
balance: "Saldo",
period: "Periode",
days: "Hari",
target: "Target",
work_report: "Laporan kerja",
load_salary_history_failed: "Gagal memuat riwayat gaji"
}
};
const params = readURLParams();
let currentLang = normalizeLanguage(params.get("lang"));
let currentMode = normalizeMode(params.get("mode")) || "withdraw";
const pageIdentity = normalizeIdentity(params.get("identity"));
let currentSalaryType = normalizeSalaryType(params.get("salaryType")) || defaultSalaryTypeForIdentity(pageIdentity);
let currentMessages = translations[currentLang] || translations.en;
let availableBalance = 0;
let salaryBalances = {};
let selectedPaymentInfo = null;
let selectedPayee = null;
let currentProfile = null;
let isSubmitting = false;
let isSavingAddress = false;
let isSearchingPayee = false;
let isSalaryHistoryLoading = false;
let salaryHistoryItems = [];
let salaryHistoryTotal = 0;
let salaryHistoryLoadedType = "";
const inputs = {
withdraw: document.querySelector("#withdrawAmount"),
exchange: document.querySelector("#exchangeAmount"),
transfer: document.querySelector("#transferAmount")
};
const balanceAmount = document.querySelector("#balanceAmount");
const emptyState = document.querySelector(".empty-address-state");
const addressForm = document.querySelector("#addressForm");
const trc20AddressInput = document.querySelector("#trc20Address");
const saveAddressButton = document.querySelector("#saveAddressButton");
const boundCard = document.querySelector(".bound-address-card");
const paymentSection = document.querySelector("[data-payment-section]");
const submitButton = document.querySelector(".submit-button");
const toast = document.querySelector(".toast");
const payeeResult = document.querySelector("#payeeResult");
const transferAccountInput = document.querySelector("#transferAccount");
const searchPayeeButton = document.querySelector("#searchPayeeButton");
const exchangeRateText = document.querySelector("#exchangeRateText");
const salarySelector = document.querySelector("#salarySelector");
const salaryHistoryButton = document.querySelector("#salaryHistoryButton");
const salaryHistoryModal = document.querySelector("#salaryHistoryModal");
const salaryHistoryList = document.querySelector("#salaryHistoryList");
const salaryHistoryTotalNode = document.querySelector("#salaryHistoryTotal");
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 splitHeaderPairs(value) {
const result = {};
String(value || "").split(";").forEach((item) => {
const index = item.indexOf("=");
if (index > -1) result[item.slice(0, index).trim()] = item.slice(index + 1).trim();
});
return result;
}
function firstRawParam(names) {
for (const name of names) {
const value = readRawParam(name);
if (value) return value;
}
return "";
}
function readRequestSysOrigin() {
const packed = splitHeaderPairs(firstRawParam(["req-sys-origin", "Req-Sys-Origin", "reqSysOrigin", "ReqSysOrigin"]));
const origin = String(firstRawParam(["sysOrigin", "origin", "Sys-Origin"]) || packed.origin || "LIKEI")
.trim()
.toUpperCase();
const originChild = String(
firstRawParam(["sysOriginChild", "originChild", "child", "Sys-Origin-Child"])
|| packed.originChild
|| packed.child
|| origin
).trim().toUpperCase();
return { origin, originChild };
}
function normalizeLanguage(lang) {
const value = String(lang || "en").toLowerCase();
return supportedLanguages.includes(value) ? value : "en";
}
function normalizeMode(mode) {
return ["withdraw", "exchange", "transfer"].includes(mode) ? mode : "";
}
function normalizeIdentity(identity) {
const value = String(identity || "").trim().toUpperCase();
return ["HOST", "BD", "BD_LEADER", "ADMIN"].includes(value) ? value : "";
}
function normalizeSalaryType(type) {
const value = String(type || "").trim().toUpperCase();
return ["HOST_SALARY", "BD_SALARY", "BD_LEADER_SALARY", "ADMIN_SALARY"].includes(value) ? value : "";
}
function defaultSalaryTypeForIdentity(identity) {
if (identity === "BD_LEADER") return "BD_LEADER_SALARY";
if (identity === "BD") return "BD_SALARY";
if (identity === "ADMIN") return "ADMIN_SALARY";
return "";
}
function isSalaryAccountMode() {
return Boolean(currentSalaryType && currentSalaryType !== "HOST_SALARY");
}
function activeSalaryType() {
return currentSalaryType || "HOST_SALARY";
}
function responseBody(data) {
return data?.body ?? data?.data ?? null;
}
function apiBaseURL() {
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
if (override) return String(override).replace(/\/+$/, "");
return API_BASE_URL;
}
function normalizeAuthorization(token) {
const value = String(token || "").trim();
if (!value) return "";
return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`;
}
function authHeadersFromToken(token, hasBody) {
const sysOrigin = readRequestSysOrigin();
const headers = {
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=${sysOrigin.origin};originChild=${sysOrigin.originChild}`
};
if (hasBody) headers["content-type"] = "application/json";
return headers;
}
async function requestJSON(path, options = {}) {
const token = readRawParam("token");
if (!token) throw new Error(message("missing_token", "Missing token"));
const response = await fetch(`${apiBaseURL()}${path}`, {
method: options.method || "GET",
cache: "no-store",
headers: authHeadersFromToken(token, options.body !== undefined),
body: options.body === undefined ? undefined : JSON.stringify(options.body)
});
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;
}
function message(key, fallback) {
return currentMessages[key] || translations.en[key] || fallback || key;
}
function formatMoney(value) {
const number = Number(value);
return Number.isFinite(number)
? new Intl.NumberFormat("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(number)
: "0.00";
}
function formatMoneyInput(value) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : "0.00";
}
function moneyNumber(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}
function currentUserId() {
return currentProfile?.id ||
currentProfile?.userId ||
firstRawParam(["userId", "UserId", "uid", "UID", "id"]);
}
function activeInput() {
return inputs[currentMode] || null;
}
function activeAmount() {
return moneyNumber(activeInput()?.value);
}
function formatExchangeRate() {
return `1$ = ${EXCHANGE_RATE} golds`;
}
function salaryBalanceValue(type) {
const balance = salaryBalances[type] || {};
return moneyNumber(balance.availableBalance ?? balance.balance ?? balance.currentSalary ?? 0);
}
function renderSalarySelector() {
if (!salarySelector) return;
const showSelector = pageIdentity === "BD_LEADER";
salarySelector.hidden = !showSelector;
if (!showSelector) return;
salarySelector.querySelectorAll("[data-salary-type]").forEach((button) => {
const type = normalizeSalaryType(button.dataset.salaryType);
const active = type === currentSalaryType;
const label = message(type === "BD_SALARY" ? "bd_salary" : "bd_leader_salary");
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
button.textContent = `${label} $${formatMoney(salaryBalanceValue(type))}`;
});
}
function setActiveSalaryType(type) {
const nextType = normalizeSalaryType(type);
if (!nextType || nextType === currentSalaryType) return;
currentSalaryType = nextType;
renderBalance(salaryBalanceValue(nextType));
if (salaryHistoryModal && !salaryHistoryModal.hidden && salaryHistoryLoadedType !== activeSalaryType()) {
openSalaryHistory();
}
}
function isValidTrc20Address(value) {
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(String(value || "").trim());
}
function maskAddress(value) {
const text = String(value || "").trim();
if (text.length <= 12) return text || "-";
return `${text.slice(0, 6)}...${text.slice(-6)}`;
}
function applyLanguage(lang) {
currentLang = normalizeLanguage(lang);
currentMessages = translations[currentLang] || translations.en;
document.documentElement.lang = currentLang;
document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr";
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);
});
const languageButton = document.querySelector(".language-button");
if (languageButton) {
languageButton.textContent = languageLabels[currentLang];
languageButton.setAttribute("aria-label", message("language_button_aria", "Change language"));
}
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));
});
if (exchangeRateText) exchangeRateText.textContent = formatExchangeRate();
renderSalarySelector();
renderPayee();
renderSalaryHistory();
updateSubmitState();
}
function setMode(mode) {
const nextMode = normalizeMode(mode) || "withdraw";
currentMode = nextMode;
document.querySelectorAll(".mode-tab").forEach((tab) => {
const active = tab.dataset.mode === nextMode;
tab.classList.toggle("active", active);
tab.setAttribute("aria-selected", String(active));
});
document.querySelectorAll(".mode-panel").forEach((panel) => {
panel.classList.toggle("active", panel.dataset.panel === nextMode);
});
if (paymentSection) paymentSection.hidden = nextMode !== "withdraw";
updateSubmitState();
}
function updateSubmitState() {
if (!submitButton) return;
if (isSubmitting) {
submitButton.textContent = message("submitting", "Submitting...");
submitButton.disabled = true;
return;
}
const submitLabels = {
withdraw: message("withdraw_now", "Withdraw now"),
exchange: message("exchange_now", "Exchange now"),
transfer: message("transfer_now", "Transfer now")
};
submitButton.textContent = submitLabels[currentMode];
const amount = activeAmount();
const exceedsBalance = amount > availableBalance;
const missingWithdrawInfo = currentMode === "withdraw" && !selectedPaymentInfo?.id;
const missingPayee = currentMode === "transfer" && !selectedPayee?.id;
submitButton.disabled = !amount ||
amount <= 0 ||
exceedsBalance ||
missingWithdrawInfo ||
missingPayee ||
(currentMode === "withdraw" && amount < WITHDRAW_MIN_AMOUNT);
}
function showToast(keyOrText, fallback) {
if (!toast) return;
toast.textContent = message(keyOrText, fallback || keyOrText);
toast.hidden = false;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.hidden = true;
}, 2200);
}
function renderBalance(value) {
availableBalance = moneyNumber(value);
if (balanceAmount) balanceAmount.textContent = formatMoney(availableBalance);
renderSalarySelector();
Object.values(inputs).forEach((input) => {
if (input) input.max = formatMoneyInput(availableBalance);
});
updateSubmitState();
}
function renderPaymentInfo(paymentInfo) {
selectedPaymentInfo = paymentInfo || null;
if (!selectedPaymentInfo) {
if (emptyState) emptyState.hidden = false;
if (addressForm) addressForm.hidden = false;
if (boundCard) boundCard.hidden = true;
updateSubmitState();
return;
}
const badge = boundCard?.querySelector(".network-badge");
const network = boundCard?.querySelector(".bound-network");
const address = boundCard?.querySelector(".bound-address");
const meta = boundCard?.querySelector(".bound-meta");
if (badge) badge.textContent = "TRC20";
if (network) network.textContent = "USDT TRC20";
if (address) address.textContent = maskAddress(selectedPaymentInfo.cardNo);
if (meta) meta.textContent = message("usdt_only", "USDT only");
if (emptyState) emptyState.hidden = true;
if (addressForm) addressForm.hidden = true;
if (boundCard) boundCard.hidden = false;
updateSubmitState();
}
function setSalaryHistoryModalOpen(open) {
if (!salaryHistoryModal) return;
salaryHistoryModal.hidden = !open;
document.body.classList.toggle("salary-history-open", open);
}
function formatHistoryDate(value) {
if (!value) return "";
const text = String(value).replace("T", " ");
return text.length > 16 ? text.slice(0, 16) : text;
}
function formatBillPeriod(value) {
const text = String(value || "");
if (text.length < 6) return text;
return `${text.slice(0, 4)}.${text.slice(4, 6)}`;
}
function salaryTypeLabel(type) {
const value = normalizeSalaryType(type);
if (value === "BD_SALARY") return message("bd_salary", "BD salary");
if (value === "BD_LEADER_SALARY") return message("bd_leader_salary", "BD Leader salary");
return message("available_income", "salary");
}
function salaryHistoryCode(item) {
const candidates = [
item?.salaryEvent,
item?.event,
item?.status,
item?.title,
item?.eventDesc,
item?.eventDescribe,
item?.salaryTypeName
];
for (const candidate of candidates) {
const value = String(candidate || "").trim().toUpperCase();
if (value) return value;
}
return "";
}
function isAgencySalaryHistoryItem(item) {
const code = salaryHistoryCode(item);
const salaryType = normalizeSalaryType(item?.salaryType);
if (
code.includes("SETTLEMENT_WAGES_AGENT") ||
code.includes("SETTLEMENT_WAGES_MEMBER") ||
code.includes("BD_SALARY_SETTLEMENT") ||
code.includes("BD_LEADER_SALARY_SETTLEMENT") ||
code.includes("ADMIN_SALARY_SETTLEMENT")
) {
return true;
}
return code === "ISSUE" && salaryType && salaryType !== "HOST_SALARY";
}
function isHostSalaryHistoryItem(item) {
if (isAgencySalaryHistoryItem(item)) return false;
const code = salaryHistoryCode(item);
const salaryType = normalizeSalaryType(item?.salaryType);
if (code === "SYSTEM_SETTLEMENT_WAGES" || code === "SYSTEM_AUTOMATIC_SETTLEMENT_WAGES" || code === "SEND_SALARY") {
return true;
}
if (code === "ISSUE" && salaryType === "HOST_SALARY") return true;
return code.includes("SETTLEMENT_WAGES") || code.includes("SETTLEMENT WAGES");
}
function parseHistoryObject(value) {
if (!value) return null;
if (typeof value === "object") return value;
if (typeof value !== "string") return null;
const text = value.trim();
if (!text || !text.startsWith("{")) return null;
try {
return JSON.parse(text);
} catch (error) {
return null;
}
}
function cleanHistoryName(value) {
const text = String(value || "").trim();
if (!text || text === "null" || text === "undefined" || /^\d+$/.test(text)) return "";
return text;
}
function cleanHistoryAvatar(value) {
const text = String(value || "").trim();
if (!text || text === "null" || text === "undefined" || /^\d+$/.test(text)) return "";
return text;
}
function historyNameFromObject(source) {
const object = parseHistoryObject(source);
if (!object) return "";
const keys = [
"providerName",
"agencyName",
"agentName",
"fromUserName",
"sourceName",
"senderName",
"userNickname",
"userNickName",
"nickName",
"nickname",
"name",
"account"
];
for (const key of keys) {
const name = cleanHistoryName(object[key]);
if (name) return name;
}
const nestedKeys = ["provider", "agency", "agent", "fromUser", "sourceUser", "sender", "user", "acceptUser", "acceptUserAccount", "profile"];
for (const key of nestedKeys) {
const name = historyNameFromObject(object[key]);
if (name) return name;
}
return "";
}
function historyAvatarFromObject(source) {
const object = parseHistoryObject(source);
if (!object) return "";
const keys = [
"userAvatar",
"avatar",
"avatarUrl",
"avatarURL",
"headPic",
"headImg",
"portrait",
"photo",
"profileImage",
"image"
];
for (const key of keys) {
const avatar = cleanHistoryAvatar(object[key]);
if (avatar) return avatar;
}
const nestedKeys = ["provider", "agency", "agent", "fromUser", "sourceUser", "sender", "user", "acceptUser", "acceptUserAccount", "profile"];
for (const key of nestedKeys) {
const avatar = historyAvatarFromObject(object[key]);
if (avatar) return avatar;
}
return "";
}
function agencySalaryProviderName(item) {
return historyNameFromObject(item) ||
historyNameFromObject(item?.details) ||
historyNameFromObject(item?.detail) ||
historyNameFromObject(item?.raw) ||
"Agency";
}
function agencySalaryProviderAvatar(item) {
return historyAvatarFromObject(item) ||
historyAvatarFromObject(item?.details) ||
historyAvatarFromObject(item?.detail) ||
historyAvatarFromObject(item?.raw) ||
"";
}
function agencySalaryDescription(item) {
if (!isAgencySalaryHistoryItem(item)) return "";
return `${agencySalaryProviderName(item)} provided you $${formatMoney(item.amount)} salary`;
}
function createAgencySalaryDescription(item) {
const text = agencySalaryDescription(item);
if (!text) return null;
const name = agencySalaryProviderName(item);
const avatarURL = agencySalaryProviderAvatar(item);
const description = document.createElement("div");
description.className = "salary-history-item-desc";
const avatar = document.createElement("span");
avatar.className = "salary-history-desc-avatar";
if (avatarURL) {
const image = document.createElement("img");
image.src = avatarURL;
image.alt = "";
avatar.appendChild(image);
} else {
avatar.textContent = (name.slice(0, 1) || "A").toUpperCase();
}
const copy = document.createElement("span");
copy.className = "salary-history-item-desc-text";
copy.textContent = text;
description.append(avatar, copy);
return description;
}
function shouldShowHistoryStatus(item) {
return Boolean(item.status && !isAgencySalaryHistoryItem(item) && !isHostSalaryHistoryItem(item));
}
function historyItemTitle(item) {
if (isAgencySalaryHistoryItem(item)) return "Agency salary";
if (isHostSalaryHistoryItem(item)) return "Host salary";
return item.title ||
item.eventDesc ||
item.eventDescribe ||
item.salaryTypeName ||
item.salaryEvent ||
item.event ||
message("work_report", "Work report");
}
function createSalaryHistoryItem(item) {
const row = document.createElement("article");
row.className = "salary-history-item";
const main = document.createElement("div");
main.className = "salary-history-item-main";
const title = document.createElement("div");
title.className = "salary-history-item-title";
title.textContent = historyItemTitle(item);
const description = createAgencySalaryDescription(item);
const meta = document.createElement("div");
meta.className = "salary-history-item-meta";
const metaItems = [];
if (item.date) metaItems.push(item.date);
if (item.period) metaItems.push(`${message("period", "Period")}: ${item.period}`);
if (item.target) metaItems.push(`${message("target", "Target")}: ${item.target}`);
if (item.days) metaItems.push(`${message("days", "Days")}: ${item.days}`);
if (item.balance !== undefined && item.balance !== null && item.balance !== "") {
metaItems.push(`${message("balance", "Balance")}: $${formatMoney(item.balance)}`);
}
meta.textContent = metaItems.join(" ");
main.append(title);
if (description) main.appendChild(description);
main.appendChild(meta);
const isOut = Number(item.type) === 1;
const amount = document.createElement("div");
amount.className = `salary-history-amount${isOut ? " is-out" : ""}`;
amount.textContent = `${isOut ? "-" : "+"}$${formatMoney(item.amount)}`;
row.append(main, amount);
if (shouldShowHistoryStatus(item)) {
const status = document.createElement("div");
status.className = "salary-history-status";
status.textContent = item.status;
row.appendChild(status);
}
return row;
}
function renderSalaryHistory() {
if (salaryHistoryTotalNode) salaryHistoryTotalNode.textContent = `$${formatMoney(salaryHistoryTotal)}`;
if (!salaryHistoryList) return;
if (isSalaryHistoryLoading) {
const loading = document.createElement("div");
loading.className = "salary-history-empty";
loading.textContent = message("loading", "Loading...");
salaryHistoryList.replaceChildren(loading);
return;
}
if (!salaryHistoryItems.length) {
const empty = document.createElement("div");
empty.className = "salary-history-empty";
empty.textContent = message("no_salary_history", "No salary history");
salaryHistoryList.replaceChildren(empty);
return;
}
salaryHistoryList.replaceChildren(...salaryHistoryItems.map(createSalaryHistoryItem));
}
function renderPayee() {
if (!payeeResult) return;
if (!selectedPayee) {
payeeResult.hidden = true;
payeeResult.innerHTML = "";
updateSubmitState();
return;
}
const name = selectedPayee.userNickname || selectedPayee.name || "-";
const account = selectedPayee.ownSpecialId?.account || selectedPayee.account || selectedPayee.id || "-";
const avatar = selectedPayee.userAvatar || selectedPayee.avatar || "";
payeeResult.hidden = false;
payeeResult.innerHTML = `
<div class="payee-avatar">${avatar ? `<img src="${escapeHTML(avatar)}" alt="">` : escapeHTML(name.slice(0, 1) || "U")}</div>
<div class="payee-copy">
<div class="payee-name">${escapeHTML(name)}</div>
<div class="payee-id">${escapeHTML(message("uid_prefix", "UID:"))} ${escapeHTML(account)}</div>
</div>
`;
updateSubmitState();
}
function escapeHTML(value) {
return String(value || "").replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
})[char]);
}
async function fetchBalance() {
try {
if (pageIdentity === "BD_LEADER") {
const salaryTypes = ["BD_SALARY", "BD_LEADER_SALARY"];
const results = await Promise.allSettled(
salaryTypes.map((type) => requestJSON(`/wallet/salary-account/balance?salaryType=${encodeURIComponent(type)}`))
);
salaryBalances = {};
results.forEach((result, index) => {
const type = salaryTypes[index];
salaryBalances[type] = result.status === "fulfilled" ? responseBody(result.value) || {} : {};
if (result.status === "rejected") console.warn(`Failed to load ${type} balance:`, result.reason);
});
if (results.every((result) => result.status === "rejected")) throw results[0].reason;
if (!currentSalaryType) currentSalaryType = "BD_LEADER_SALARY";
renderBalance(salaryBalanceValue(currentSalaryType));
return;
}
if (isSalaryAccountMode()) {
const data = await requestJSON(`/wallet/salary-account/balance?salaryType=${encodeURIComponent(currentSalaryType)}`);
const body = responseBody(data) || {};
salaryBalances = { [currentSalaryType]: body };
renderBalance(body.availableBalance ?? body.balance ?? body.currentSalary ?? 0);
return;
}
const data = await requestJSON("/wallet/bank/balance");
const body = responseBody(data) ?? 0;
renderBalance(typeof body === "number" ? body : body.currentSalary ?? body.availableBalance ?? body.balance ?? 0);
} catch (error) {
console.error("Failed to load balance:", error);
renderBalance(0);
showToast("load_balance_failed", error.message);
}
}
async function fetchUserProfile() {
try {
const data = await requestJSON("/team/member/profile");
currentProfile = data.body ?? data.data ?? null;
} catch (error) {
console.error("Failed to load user profile:", error);
currentProfile = null;
}
}
async function fetchSalaryAccountHistory() {
if (!currentProfile) await fetchUserProfile();
const query = new URLSearchParams({
salaryType: activeSalaryType(),
current: "1",
size: "20"
});
const userId = currentUserId();
if (userId) query.set("userId", String(userId));
const data = await requestJSON(`/wallet/salary-account/running-water?${query.toString()}`);
const body = responseBody(data) || data || {};
const records = Array.isArray(body.records) ? body.records : Array.isArray(body.list) ? body.list : [];
return {
total: availableBalance,
items: records.map((item) => ({
id: item.id,
title: item.eventDesc || item.salaryTypeName || salaryTypeLabel(item.salaryType),
eventDesc: item.eventDesc,
eventDescribe: item.eventDescribe,
salaryEvent: item.salaryEvent,
event: item.event || item.salaryEvent,
salaryType: item.salaryType || activeSalaryType(),
salaryTypeName: item.salaryTypeName,
details: item.details || item.detail,
detail: item.detail,
raw: item,
amount: item.amount,
balance: item.availableBalance ?? item.balance,
type: item.type,
status: item.salaryEvent || "",
period: item.billBelong ? formatBillPeriod(item.billBelong) : "",
date: formatHistoryDate(item.createdAt)
}))
};
}
async function fetchBankWaterHistory() {
const data = await requestJSON("/wallet/bank/balance/water/details");
const records = Array.isArray(responseBody(data)) ? responseBody(data) : Array.isArray(data) ? data : [];
return {
total: availableBalance,
items: records.map((item) => ({
id: item.id,
title: item.eventDescribe || item.event || message("salary_history", "Salary history"),
event: item.event,
eventDescribe: item.eventDescribe,
details: item.details || item.detail,
detail: item.detail,
raw: item,
amount: item.amount,
balance: item.balance,
type: item.type,
status: item.event || "",
date: formatHistoryDate(item.createTime)
}))
};
}
async function fetchSalaryHistory() {
if (isSalaryAccountMode()) return fetchSalaryAccountHistory();
return fetchBankWaterHistory();
}
async function openSalaryHistory() {
salaryHistoryLoadedType = activeSalaryType();
salaryHistoryTotal = availableBalance;
salaryHistoryItems = [];
isSalaryHistoryLoading = true;
renderSalaryHistory();
setSalaryHistoryModalOpen(true);
try {
const result = await fetchSalaryHistory();
salaryHistoryTotal = moneyNumber(result.total ?? availableBalance);
salaryHistoryItems = Array.isArray(result.items) ? result.items : [];
} catch (error) {
console.error("Failed to load salary history:", error);
salaryHistoryItems = [];
showToast(error.message || message("load_salary_history_failed", "Failed to load salary history"));
} finally {
isSalaryHistoryLoading = false;
renderSalaryHistory();
}
}
async function fetchPaymentInfo() {
try {
const data = await requestJSON("/wallet/bank-card/list");
const cards = Array.isArray(data.body) ? data.body : Array.isArray(data.data) ? data.data : [];
const usdtCards = cards.filter((item) => item.cardType === "USDT");
renderPaymentInfo(usdtCards.find((item) => item.use) || usdtCards[0] || null);
} catch (error) {
console.error("Failed to load payment info:", error);
renderPaymentInfo(null);
showToast("load_payment_failed", error.message);
}
}
function normalizeAmountInput(event) {
const input = event?.target || activeInput();
if (!input) return;
const raw = String(input.value || "").replace(/[^\d.]/g, "");
const parts = raw.split(".");
let nextValue = parts.length > 2 ? `${parts[0]}.${parts.slice(1).join("")}` : raw;
const amount = Number(nextValue);
if (Number.isFinite(amount) && amount > availableBalance) nextValue = formatMoneyInput(availableBalance);
input.value = nextValue;
updateSubmitState();
}
function validateAmount(requireMinimum) {
const amount = activeAmount();
if (!amount || amount <= 0) return "invalid_amount";
if (requireMinimum && amount < WITHDRAW_MIN_AMOUNT) return "amount_too_low";
if (amount > availableBalance) return "amount_exceeds_balance";
return "";
}
async function saveAddress() {
const address = String(trc20AddressInput?.value || "").trim();
if (!isValidTrc20Address(address)) {
showToast("invalid_address");
return;
}
if (!currentProfile) await fetchUserProfile();
isSavingAddress = true;
if (saveAddressButton) {
saveAddressButton.disabled = true;
saveAddressButton.textContent = message("saving", "Saving...");
}
try {
const data = await requestJSON("/wallet/bank-card/add", {
method: "POST",
body: {
userId: currentProfile?.id || currentProfile?.userId || "",
cardType: "USDT",
cardName: "USDT",
cardNo: address,
payee: "USDT-TRC20"
}
});
const created = data.body || data.data || null;
if (created?.id) {
renderPaymentInfo({ ...created, cardType: "USDT", cardNo: address });
await requestJSON("/wallet/bank-card/use", {
method: "POST",
body: { id: created.id }
}).catch((error) => console.warn("Failed to set USDT address in use:", error));
}
await fetchPaymentInfo();
showToast("address_saved");
} catch (error) {
console.error("Failed to save TRC20 address:", error);
showToast(error.message || message("load_payment_failed", "Failed to load payment info"));
} finally {
isSavingAddress = false;
if (saveAddressButton) {
saveAddressButton.disabled = false;
saveAddressButton.textContent = message("save_address", "Save address");
}
}
}
async function searchPayee() {
const account = String(transferAccountInput?.value || "").trim();
if (!account) {
showToast("missing_payee");
return;
}
selectedPayee = null;
renderPayee();
isSearchingPayee = true;
if (searchPayeeButton) {
searchPayeeButton.disabled = true;
searchPayeeButton.textContent = message("searching", "Searching...");
}
try {
const path = `/wallet/bank/search/user-profile?account=${encodeURIComponent(account)}&type=AGENT&searchType=TRANSFER`;
const data = await requestJSON(path);
const body = data.body ?? data.data ?? null;
if (!body?.id) {
showToast("payee_not_found");
return;
}
selectedPayee = body;
renderPayee();
} catch (error) {
console.error("Failed to search payee:", error);
showToast(error.message || message("payee_not_found", "User not found"));
} finally {
isSearchingPayee = false;
if (searchPayeeButton) {
searchPayeeButton.disabled = false;
searchPayeeButton.textContent = message("search", "Search");
}
}
}
function validateWithdraw() {
if (!selectedPaymentInfo?.id) return "missing_payment_info";
return validateAmount(true);
}
function validateTransfer() {
if (!selectedPayee?.id) return "missing_payee";
return validateAmount(false);
}
async function submitWithdraw() {
const errorKey = validateWithdraw();
if (errorKey) {
showToast(errorKey);
return;
}
await requestJSON("/wallet/bank/withdraw", {
method: "POST",
body: {
amount: activeAmount(),
acceptBankCardId: selectedPaymentInfo.id,
salaryType: activeSalaryType()
}
});
showToast("withdraw_success");
}
async function submitExchange() {
const errorKey = validateAmount(false);
if (errorKey) {
showToast(errorKey);
return;
}
await requestJSON(isSalaryAccountMode() ? "/wallet/salary-account/exchange/gold" : "/wallet/bank/exchange/gold", {
method: "POST",
body: isSalaryAccountMode()
? { amount: activeAmount(), salaryType: activeSalaryType() }
: { amount: activeAmount() }
});
showToast("exchange_success");
}
async function submitTransfer() {
const errorKey = validateTransfer();
if (errorKey) {
showToast(errorKey);
return;
}
await requestJSON(isSalaryAccountMode() ? "/wallet/salary-account/transferGold" : "/wallet/bank/transferGold", {
method: "POST",
body: isSalaryAccountMode()
? { amount: activeAmount(), acceptUserId: selectedPayee.id, salaryType: activeSalaryType() }
: { amount: activeAmount(), acceptUserId: selectedPayee.id, acceptMethod: "BANK_TRANSFER" }
});
showToast("transfer_success");
}
async function submitCurrentMode() {
if (isSubmitting) return;
isSubmitting = true;
updateSubmitState();
try {
if (currentMode === "withdraw") await submitWithdraw();
if (currentMode === "exchange") await submitExchange();
if (currentMode === "transfer") await submitTransfer();
const input = activeInput();
if (input) input.value = "";
await fetchBalance();
} catch (error) {
console.error(`Failed to submit ${currentMode}:`, error);
const fallback = {
withdraw: "withdraw_failed",
exchange: "exchange_failed",
transfer: "transfer_failed"
}[currentMode];
showToast(error.message || message(fallback, "Submit failed"));
} finally {
isSubmitting = false;
updateSubmitState();
}
}
function syncBackFallback() {
document.querySelector(".back-button")?.addEventListener("click", () => {
if (window.history.length > 1) {
window.history.back();
return;
}
const nextParams = new URLSearchParams(params);
const token = readRawParam("token");
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
const query = nextParams.toString();
const fallbackPage = pageIdentity === "BD_LEADER" ? "../bd-leader-center/index.html" : "../host-center/index.html";
window.location.href = `${fallbackPage}${query ? `?${query}` : ""}`;
});
}
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));
}
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.addEventListener("click", () => {
setLanguageMenuOpen(false);
});
document.querySelectorAll(".mode-tab").forEach((tab) => {
tab.addEventListener("click", () => setMode(tab.dataset.mode));
});
document.querySelectorAll("[data-salary-type]").forEach((button) => {
button.addEventListener("click", () => setActiveSalaryType(button.dataset.salaryType));
});
document.querySelector(".text-action")?.addEventListener("click", () => {
if (inputs.withdraw) inputs.withdraw.value = formatMoneyInput(availableBalance);
updateSubmitState();
});
Object.values(inputs).forEach((input) => input?.addEventListener("input", normalizeAmountInput));
transferAccountInput?.addEventListener("input", () => {
selectedPayee = null;
renderPayee();
});
transferAccountInput?.addEventListener("keydown", (event) => {
if (event.key === "Enter") searchPayee();
});
searchPayeeButton?.addEventListener("click", searchPayee);
saveAddressButton?.addEventListener("click", saveAddress);
submitButton?.addEventListener("click", submitCurrentMode);
salaryHistoryButton?.addEventListener("click", openSalaryHistory);
document.querySelectorAll("[data-close-salary-history]").forEach((node) => {
node.addEventListener("click", () => setSalaryHistoryModalOpen(false));
});
syncBackFallback();
applyLanguage(currentLang);
setMode(currentMode);
fetchUserProfile();
fetchBalance();
fetchPaymentInfo();
})();