feat: route bd leader salary to wallet actions

This commit is contained in:
170-carry 2026-04-28 23:41:46 +08:00
parent 850b340600
commit e9bb86cb94
5 changed files with 184 additions and 29 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>BD Leader Center</title>
<link rel="stylesheet" href="./style.css?v=20260428-2145" />
<link rel="stylesheet" href="./style.css?v=20260428-2215" />
</head>
<body>
<div class="bd-leader-center" aria-label="BD Leader Center" data-i18n-aria="page_label" data-loading="true">
@ -53,10 +53,10 @@
<section class="card salary-card">
<div class="salary-head">
<div>
<div class="salary-label" data-i18n="my_available_salary">My available salary</div>
<div class="salary-label" data-i18n="my_available_salary">salary</div>
<div class="salary-period" id="billTitle">-</div>
</div>
<button class="text-link" type="button" data-route="/available-income" data-i18n="details">Details</button>
<button class="text-link" type="button" data-route="/hyapp/withdraw-exchange/index.html?identity=BD_LEADER&salaryType=BD_LEADER_SALARY" data-i18n="details">Withdraw/Exchange</button>
</div>
<div class="salary-value">
<span aria-hidden="true">$</span>
@ -223,6 +223,6 @@
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
</div>
<script src="./script.js?v=20260428-2145" defer></script>
<script src="./script.js?v=20260428-2215" defer></script>
</body>
</html>

View File

@ -17,8 +17,8 @@
language_button_aria: "Change language",
role: "BD Leader",
uid_prefix: "UID:",
my_available_salary: "My available salary",
details: "Details",
my_available_salary: "salary",
details: "Withdraw/Exchange",
bd_number: "BD number",
team_salary: "Team salary",
bd_policy_link: "BD policy",
@ -67,8 +67,8 @@
language_button_aria: "تغيير اللغة",
role: "قائد BD",
uid_prefix: "UID:",
my_available_salary: "راتبي المتاح",
details: "التفاصيل",
my_available_salary: "الراتب",
details: "سحب/تبديل",
bd_number: "عدد BD",
team_salary: "راتب الفريق",
bd_policy_link: "سياسة BD",
@ -117,8 +117,8 @@
language_button_aria: "Dili değiştir",
role: "BD Leader",
uid_prefix: "UID:",
my_available_salary: "Kullanılabilir maaşım",
details: "Detaylar",
my_available_salary: "Maaş",
details: "Withdraw/Exchange",
bd_number: "BD sayısı",
team_salary: "Takım maaşı",
bd_policy_link: "BD politikası",
@ -167,8 +167,8 @@
language_button_aria: "Ubah bahasa",
role: "BD Leader",
uid_prefix: "UID:",
my_available_salary: "Gaji tersedia",
details: "Detail",
my_available_salary: "Gaji",
details: "Withdraw/Exchange",
bd_number: "Jumlah BD",
team_salary: "Gaji tim",
bd_policy_link: "Kebijakan BD",
@ -451,6 +451,11 @@
if (node) node.textContent = formatMoney(balance);
}
function balanceAmount(balance) {
const value = Number(balance?.availableBalance ?? balance?.balance ?? balance?.currentSalary ?? 0);
return Number.isFinite(value) ? value : 0;
}
function renderBill() {
const bill = state.bill || {};
const members = Array.isArray(bill.memberBillList) ? bill.memberBillList : [];
@ -1019,7 +1024,24 @@
}
async function fetchBalanceTotal() {
state.balanceTotal = await requestJSON("/wallet/salary-account/balance/total") || {};
const salaryTypes = ["BD_SALARY", "BD_LEADER_SALARY"];
const results = await Promise.allSettled(
salaryTypes.map((salaryType) => requestJSON(`/wallet/salary-account/balance?salaryType=${encodeURIComponent(salaryType)}`))
);
const balances = {};
results.forEach((result, index) => {
const salaryType = salaryTypes[index];
if (result.status === "fulfilled") {
balances[salaryType] = result.value || {};
} else {
balances[salaryType] = {};
console.warn(`Failed to load ${salaryType} balance:`, result.reason);
}
});
state.balanceTotal = {
...balances,
availableBalance: salaryTypes.reduce((total, salaryType) => total + balanceAmount(balances[salaryType]), 0)
};
renderBalance();
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Withdraw/Exchange</title>
<link rel="stylesheet" href="./style.css?v=20260428-2030" />
<link rel="stylesheet" href="./style.css?v=20260428-2215" />
</head>
<body>
<div class="wallet-page" aria-label="Withdraw and exchange" data-i18n-aria="page_label">
@ -46,6 +46,13 @@
</div>
</section>
<section class="salary-selector-card" id="salarySelector" hidden>
<div class="salary-selector-tabs">
<button class="salary-selector-tab" type="button" data-salary-type="BD_SALARY" data-i18n="bd_salary">BD salary</button>
<button class="salary-selector-tab" type="button" data-salary-type="BD_LEADER_SALARY" data-i18n="bd_leader_salary">BD Leader salary</button>
</div>
</section>
<section class="mode-card">
<div class="mode-tabs" role="tablist" aria-label="Withdraw, exchange, or transfer">
<button class="mode-tab active" type="button" role="tab" aria-selected="true" data-mode="withdraw" data-i18n="withdraw">Withdraw</button>
@ -147,6 +154,6 @@
<div class="toast" hidden></div>
<div class="home-indicator" aria-hidden="true"></div>
</div>
<script src="./script.js?v=20260428-2030" defer></script>
<script src="./script.js?v=20260428-2215" defer></script>
</body>
</html>

View File

@ -16,6 +16,8 @@
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",
@ -66,6 +68,8 @@
title: "سحب/تبديل",
language_button_aria: "تغيير اللغة",
available_income: "الراتب",
bd_salary: "راتب BD",
bd_leader_salary: "راتب قائد BD",
withdraw: "سحب",
exchange: "تبديل",
transfer: "تحويل",
@ -116,6 +120,8 @@
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",
@ -166,6 +172,8 @@
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",
@ -215,8 +223,11 @@
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;
@ -242,6 +253,7 @@
const transferAccountInput = document.querySelector("#transferAccount");
const searchPayeeButton = document.querySelector("#searchPayeeButton");
const exchangeRateText = document.querySelector("#exchangeRateText");
const salarySelector = document.querySelector("#salarySelector");
function readURLParams() {
const result = new URLSearchParams(window.location.search);
@ -289,6 +301,35 @@
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(/\/+$/, "");
@ -360,6 +401,34 @@
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));
}
function isValidTrc20Address(value) {
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(String(value || "").trim());
}
@ -396,6 +465,7 @@
button.setAttribute("aria-pressed", String(isActive));
});
if (exchangeRateText) exchangeRateText.textContent = formatExchangeRate();
renderSalarySelector();
renderPayee();
updateSubmitState();
}
@ -456,6 +526,7 @@
function renderBalance(value) {
availableBalance = moneyNumber(value);
if (balanceAmount) balanceAmount.textContent = formatMoney(availableBalance);
renderSalarySelector();
Object.values(inputs).forEach((input) => {
if (input) input.max = formatMoney(availableBalance);
});
@ -524,11 +595,36 @@
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 = data.body ?? data.data ?? 0;
const body = responseBody(data) ?? 0;
renderBalance(typeof body === "number" ? body : body.currentSalary ?? body.availableBalance ?? body.balance ?? 0);
} catch (error) {
console.error("Failed to load bank balance:", error);
console.error("Failed to load balance:", error);
renderBalance(0);
showToast("load_balance_failed", error.message);
}
@ -683,7 +779,7 @@
body: {
amount: activeAmount(),
acceptBankCardId: selectedPaymentInfo.id,
salaryType: "HOST_SALARY"
salaryType: activeSalaryType()
}
});
showToast("withdraw_success");
@ -696,11 +792,11 @@
return;
}
await requestJSON("/wallet/bank/exchange/gold", {
await requestJSON(isSalaryAccountMode() ? "/wallet/salary-account/exchange/gold" : "/wallet/bank/exchange/gold", {
method: "POST",
body: {
amount: activeAmount()
}
body: isSalaryAccountMode()
? { amount: activeAmount(), salaryType: activeSalaryType() }
: { amount: activeAmount() }
});
showToast("exchange_success");
}
@ -712,13 +808,11 @@
return;
}
await requestJSON("/wallet/bank/transferGold", {
await requestJSON(isSalaryAccountMode() ? "/wallet/salary-account/transferGold" : "/wallet/bank/transferGold", {
method: "POST",
body: {
amount: activeAmount(),
acceptUserId: selectedPayee.id,
acceptMethod: "BANK_TRANSFER"
}
body: isSalaryAccountMode()
? { amount: activeAmount(), acceptUserId: selectedPayee.id, salaryType: activeSalaryType() }
: { amount: activeAmount(), acceptUserId: selectedPayee.id, acceptMethod: "BANK_TRANSFER" }
});
showToast("transfer_success");
}
@ -760,7 +854,8 @@
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
const query = nextParams.toString();
window.location.href = `../host-center/index.html${query ? `?${query}` : ""}`;
const fallbackPage = pageIdentity === "BD_LEADER" ? "../bd-leader-center/index.html" : "../host-center/index.html";
window.location.href = `${fallbackPage}${query ? `?${query}` : ""}`;
});
}
@ -795,6 +890,10 @@
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 = formatMoney(availableBalance);
updateSubmitState();

View File

@ -294,6 +294,7 @@ select {
}
.balance-card,
.salary-selector-card,
.mode-card,
.card-section,
.bind-panel {
@ -332,6 +333,32 @@ select {
line-height: 1;
}
.salary-selector-card {
padding: 10px;
}
.salary-selector-tabs {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.salary-selector-tab {
min-height: 40px;
padding: 0 8px;
border-radius: 8px;
background: #eef1f1;
color: #777a80;
font-size: 13px;
font-weight: 900;
}
.salary-selector-tab.active {
background: #e7fbf8;
color: #0e8f82;
box-shadow: inset 0 0 0 1px rgba(22, 185, 172, 0.22);
}
.mode-card {
padding: 16px;
}