1406 lines
49 KiB
JavaScript
1406 lines
49 KiB
JavaScript
(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) => ({
|
||
"&": "&",
|
||
"<": "<",
|
||
">": ">",
|
||
'"': """,
|
||
"'": "'"
|
||
})[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();
|
||
})();
|