feat: add hyapp bd leader center

This commit is contained in:
170-carry 2026-04-28 23:22:04 +08:00
parent d72a0502ec
commit e1abcb0b4e
4 changed files with 2274 additions and 0 deletions

View File

@ -0,0 +1,204 @@
<!doctype html>
<html lang="en">
<head>
<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-2130" />
</head>
<body>
<div class="bd-leader-center" aria-label="BD Leader Center" data-i18n-aria="page_label" data-loading="true">
<div class="hero-bg" aria-hidden="true"></div>
<header class="status-bar" aria-hidden="true">
<span class="status-time">9:41</span>
<span class="status-icons">
<span class="cellular"><i></i><i></i><i></i><i></i></span>
<span class="wifi"><i></i><i></i><i></i></span>
<span class="battery"><i></i></span>
</span>
</header>
<nav class="title-bar">
<button class="back-button" type="button" aria-label="Back" data-i18n-aria="back">
<svg viewBox="0 0 32 32" aria-hidden="true">
<path d="M20 8 12 16l8 8" />
</svg>
</button>
<h1 data-i18n="title">BD Leader Center</h1>
<div class="language-switcher">
<button class="language-button" type="button" aria-label="Change language" data-i18n-aria="language_button_aria" aria-expanded="false">EN</button>
<div class="language-menu" hidden>
<button type="button" data-lang="en">EN</button>
<button type="button" data-lang="ar">AR</button>
<button type="button" data-lang="tr">TR</button>
<button type="button" data-lang="id">ID</button>
</div>
</div>
</nav>
<main class="content">
<section class="card profile-card">
<div class="avatar-shell">
<img class="avatar-image" id="profileAvatar" alt="" hidden />
<span class="avatar-fallback" id="profileAvatarFallback">B</span>
</div>
<div class="profile-copy">
<div class="role-pill" data-i18n="role">BD Leader</div>
<div class="name" id="profileName">-</div>
<div class="meta" id="profileUid">UID: -</div>
</div>
</section>
<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-period" id="billTitle">-</div>
</div>
<button class="text-link" type="button" data-route="/available-income" data-i18n="details">Details</button>
</div>
<div class="salary-value">
<span aria-hidden="true">$</span>
<strong id="availableBalance">0.00</strong>
</div>
</section>
<section class="card metrics-card">
<div class="metric">
<div class="label" data-i18n="bd_number">BD number</div>
<div class="value" id="bdNumber">0</div>
</div>
<div class="vertical-line" aria-hidden="true"></div>
<div class="metric">
<div class="label" data-i18n="team_salary">Team salary</div>
<div class="value metric-money">
<span aria-hidden="true">$</span>
<span id="teamSalary">0.00</span>
</div>
</div>
</section>
<section class="card menu-card">
<button class="menu-row" type="button" data-route="/policy?from=BDLead">
<span class="menu-left">
<span class="menu-icon policy-icon" aria-hidden="true">
<svg viewBox="0 0 28 28"><path d="M8 5h10l3 3v15H8Z" /><path d="M18 5v5h4M11 14h7M11 18h6" /></svg>
</span>
<span data-i18n="bd_policy_link">BD policy</span>
</span>
<span class="chevron">&rsaquo;</span>
</button>
<button class="menu-row" type="button" data-action="history">
<span class="menu-left">
<span class="menu-icon history-icon" aria-hidden="true">
<svg viewBox="0 0 28 28"><path d="M14 5a9 9 0 1 1-8.1 5.1" /><path d="M6 5v5h5M14 10v5l4 2" /></svg>
</span>
<span data-i18n="bd_leader_history">BD Leader history</span>
</span>
<span class="chevron">&rsaquo;</span>
</button>
<button class="menu-row" type="button" data-route="/agency-list">
<span class="menu-left">
<span class="menu-icon list-icon" aria-hidden="true">
<svg viewBox="0 0 28 28"><path d="M8 8h12M8 14h12M8 20h12" /><path d="M4 8h.1M4 14h.1M4 20h.1" /></svg>
</span>
<span data-i18n="agency_list_link">Agency list</span>
</span>
<span class="chevron">&rsaquo;</span>
</button>
<button class="menu-row" type="button" data-route="/bd-list">
<span class="menu-left">
<span class="menu-icon bd-icon" aria-hidden="true">
<svg viewBox="0 0 28 28"><path d="M10 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM20 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM3 24c1-6 4-9 7-9s6 3 7 9ZM16 22c.6-3.8 2.2-5.8 4-5.8 2.2 0 4 2.3 5 5.8" /></svg>
</span>
<span data-i18n="bd_list_link">BD List</span>
</span>
<span class="chevron">&rsaquo;</span>
</button>
<button class="menu-row" type="button" data-route="/invite-agency">
<span class="menu-left">
<span class="menu-icon invite-icon" aria-hidden="true">
<svg viewBox="0 0 28 28"><path d="M11 13a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM4 24c1-5.5 3.5-8 7-8 2 0 3.7.8 5 2.5M21 10v10M16 15h10" /></svg>
</span>
<span data-i18n="invite_become_agent">Invite to become agent</span>
</span>
<span class="chevron">&rsaquo;</span>
</button>
<button class="menu-row" type="button" data-route="/invite-bd">
<span class="menu-left">
<span class="menu-icon invite-icon" aria-hidden="true">
<svg viewBox="0 0 28 28"><path d="M11 13a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM4 24c1-5.5 3.5-8 7-8 2 0 3.7.8 5 2.5M21 10v10M16 15h10" /></svg>
</span>
<span data-i18n="invite_become_bd">Invite to become BD</span>
</span>
<span class="chevron">&rsaquo;</span>
</button>
</section>
<section class="card member-card">
<div class="section-head">
<div>
<div class="section-title" data-i18n="bd_list_preview">BD List</div>
<div class="section-subtitle" data-i18n="bd_list_preview_subtitle">Current settlement period</div>
</div>
<button class="text-link" type="button" data-route="/bd-list" data-i18n="more">More</button>
</div>
<div class="member-list" id="bdMemberList">
<div class="empty-state" data-i18n="loading_data">Loading...</div>
</div>
</section>
</main>
<div class="history-modal" id="historyModal" hidden>
<div class="history-backdrop" data-close-history></div>
<section class="history-dialog" role="dialog" aria-modal="true" aria-labelledby="historyTitle">
<div class="history-head">
<h2 id="historyTitle" data-i18n="bd_leader_history">BD Leader history</h2>
<button class="history-close" type="button" aria-label="Close" data-i18n-aria="close" data-close-history>&times;</button>
</div>
<div class="history-summary">
<div>
<span data-i18n="total_income">Total income</span>
<strong id="historyTotalIncome">$0.00</strong>
</div>
<div>
<span data-i18n="previous_period_income">Previous period income</span>
<strong id="historyPreviousIncome">$0.00</strong>
</div>
</div>
<div class="history-list" id="historyList"></div>
</section>
</div>
<div class="details-modal" id="detailsModal" hidden>
<div class="history-backdrop" data-close-details></div>
<section class="history-dialog" role="dialog" aria-modal="true" aria-labelledby="detailsTitle">
<div class="history-head">
<h2 id="detailsTitle">-</h2>
<button class="history-close" type="button" aria-label="Close" data-i18n-aria="close" data-close-details>&times;</button>
</div>
<div class="history-summary compact" id="detailsSummary"></div>
<div class="history-list" id="detailsList"></div>
</section>
</div>
<div class="home-indicator" aria-hidden="true"></div>
<div class="loading-mask" id="loadingMask" role="status" aria-live="polite" aria-busy="true">
<div class="runner-stick" aria-hidden="true">
<span class="runner-head"></span>
<span class="runner-body"></span>
<span class="runner-arm runner-arm-front"></span>
<span class="runner-arm runner-arm-back"></span>
<span class="runner-leg runner-leg-front"></span>
<span class="runner-leg runner-leg-back"></span>
<span class="runner-ground"></span>
</div>
<div class="loading-copy">loading....</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
</div>
<script src="./script.js?v=20260428-2130" defer></script>
</body>
</html>

View File

@ -0,0 +1,255 @@
# BD Leader Center 原 Vue/旧 H5 逻辑清单
## 页面入口
- 旧入口:`h5/bd-leader-center/index.html`
- 旧逻辑:`h5/bd-leader-center/bd-leader-center.js`
- 旧公共工具:`h5/bd-static/shared.js`
- 相关子页面:
- `h5/agency-list/`Agency list
- `h5/bd-list/`BD List
- `h5/invite-agency/`Invite to become agent
- `h5/invite-bd/`Invite to become BD
- `h5/bd-item-distribution/`Send welcome gift
- `h5/available-income/`Available salary details
- `h5/policy/?from=BDLead`BD Leader policy
## 鉴权和 App 通讯
- 旧逻辑优先通过 App bridge 获取 header
- Android`window.app.getAccessOrigin()`
- Flutter`window.FlutterApp.postMessage("requestAccessOrigin")`
- 回调:`window.renderData``window.getIosAccessOriginParam`
- Header 会被解析为:
- `authorization`
- `req-lang`
- `req-app-intel`
- `req-imei`
- `req-sys-origin`
- `req-client`
- `req-version`
- `req-zone`
- 新 `hyapp` 页面沿用 Host Center 的 URL token 模式:
- 从 URL `token` 读取 token
- 请求头传 `Authorization: Bearer <token>`
- 同时传 `req-lang``req-client=H5``req-version=V2``req-zone``req-app-intel``req-sys-origin`
## 初始化流程
1. 设置语言和方向。
2. 清空旧提现收款人缓存:
- `sessionStorage.removeItem("payee")`
- `localStorage.removeItem("payee")`
3. 写入身份缓存:
- `localStorage.identity = {"identity":"BD_LEADER"}`
4. 获取基础资料:
- `GET /team/member/profile`
- `GET /team/entry`
- `GET /app/h5/identity`
5. 获取薪资总余额:
- `GET /wallet/salary-account/balance/total`
6. 渲染可用余额。
## 用户信息
接口:`GET /team/member/profile`
用途:
- 头像
- 昵称
- UID/账号/靓号
可参考字段:
- `id`
- `account`
- `userNickname`
- `name`
- `userAvatar`
- `ownSpecialId.account`
## 团队信息
接口:`GET /team/entry`
用途:
- 获取当前用户关联团队资料。
- 旧代码取值:`body.teamProfile || body`
可参考字段:
- `id`
- `teamId`
- `teamName`
- `name`
## 身份信息
接口:`GET /app/h5/identity`
用途:
- 判断当前用户身份。
- 旧路由配置包含:
- `bd`
- `bdLeader`
- `agent`
- `anchor`
BD Leader Center 旧实现不强制拦截,只把页面身份设为 `BD_LEADER`
## 可用薪资
接口:`GET /wallet/salary-account/balance/total`
用途:
- 旧页面展示 `My available salary`
字段:
- `availableBalance`
- 其他余额字段以接口返回为准
旧页面展示:
- `$${availableBalance || 0}`
## BD Leader 成员账单 / BD List 数据
接口:`GET /team/bd/member-bill/list?type=BDLEADER`
用途:
- BD Leader 下属 BD 列表
- 当前账单周期标题
- BD 数量
- 团队薪资汇总
字段:
- `billTitle`
- `memberCount`
- `totalSalary`
- `memberBillList[]`
- `memberBillList[].agentAvatar`
- `memberBillList[].agentName`
- `memberBillList[].agentAccount`
- `memberBillList[].teamSalaryAmount`
- `memberBillList[].teamMemberCount`
- `bdHistoryCO`
`h5/bd-list` 展示:
- `number_of_bd: memberCount`
- `team_salary: totalSalary`
- 每个 BD
- 头像:`agentAvatar`
- 名称:`agentName`
- ID`agentAccount`
- 团队薪资:`teamSalaryAmount`
- 代理数量:`teamMemberCount`
## Agency List 数据
接口:`GET /team/bd/member-bill/list?type=BD`
用途:
- Agency list
- BD 下属代理列表
- 团队充值/团队薪资/主播数
字段:
- `billTitle`
- `totalRecharge`
- `totalSalary`
- `memberBillList[].agentAvatar`
- `memberBillList[].agentName`
- `memberBillList[].agentAccount`
- `memberBillList[].teamSalaryAmount`
- `memberBillList[].teamRechargeAmount`
- `memberBillList[].teamMemberCount`
- `bdHistoryCO`
## BD Leader 历史收入
接口:`GET /team/bd/leader/history`
用途:
- 展示 BD Leader 历史收入弹层。
字段:
- `totalIncome`
- `previousPeriodIncome`
- `incomeDetails[]`
- `incomeDetails[].billTitle`
- `incomeDetails[].statusText`
- `incomeDetails[].billBelong`
- `incomeDetails[].bdNumber`
- `incomeDetails[].teamSalaryAmount`
状态映射:
- `UNPAID` / `In Progress` -> `In Progress`
- `Pending` -> `Pending`
- `SETTLED` / `PAY_OUT` / `Completed` -> `Completed`
- `HANG_UP` / `Out of account` / `Out Of Account` -> `Out of account`
## BD Leader 历史详情
接口:`GET /team/bd/leader/history/more?billBelong=<normalizedBillBelong>`
参数:
- `billBelong`
- 旧工具 `historyMoreUrl` 会把各种日期格式转换成:
- `YYYY-MM-DDTHH:mm:ss`
字段:
- `period`
- `teamSalary`
- `bdNumber`
- `memberDetails[]`
- `memberDetails[].bdUserAvatar`
- `memberDetails[].bdUserName`
- `memberDetails[].account`
- `memberDetails[].salary`
- `memberDetails[].agencyCount`
## 邀请 BD
入口:`/invite-bd`
接口:
- 列表:`GET /team/bd/invite-bd-message-own`
- 搜索用户:`GET /user/user-profile/open-search?account=<account>`
- 邀请:`POST /team/bd/invite-bd`
- body`{ inviteUserId }`
- 取消邀请:`POST /team/bd/invite-bd-cancel/<id>`
## 邀请代理
入口:`/invite-agency`
接口:
- 列表:`GET /team/bd/invite-message-own`
- 邀请:`POST /team/bd/invite-agent`
- body`{ inviteUserId }`
- 取消邀请:`POST /team/bd/invite-agent-cancel/<id>`
## 其他菜单功能
- `Details` -> `/available-income`
- `BD policy` -> `/policy?from=BDLead`
- `Agency list` -> `/agency-list`
- `BD List` -> `/bd-list`
- `Bank cards` -> `/bank-cards`
- `Send welcome gift` -> `/bd-item-distribution`
## 新 `h5/hyapp/bd-leader-center` 第一版实现范围
- 复用 Host Center 风格:
- 顶部状态栏和标题栏
- 用户资料卡
- 可用薪资卡
- BD 数量/团队薪资统计卡
- 功能入口列表
- loading mask
- 已接入:
- `GET /team/member/profile`
- `GET /app/h5/identity`
- `GET /team/entry`
- `GET /wallet/salary-account/balance/total`
- `GET /team/bd/member-bill/list?type=BDLEADER`
- `GET /team/bd/leader/history`
- `GET /team/bd/leader/history/more?billBelong=...`
- 已保留旧页面跳转入口。

View File

@ -0,0 +1,806 @@
(function () {
const supportedLanguages = ["en", "ar", "tr", "id"];
const languageLabels = {
en: "EN",
ar: "AR",
tr: "TR",
id: "ID"
};
const API_BASE_URL = "https://jvapi.haiyihy.com";
const fallbackMessages = {
en: {
document_title: "BD Leader Center",
page_label: "BD Leader Center",
back: "Back",
title: "BD Leader Center",
language_button_aria: "Change language",
role: "BD Leader",
uid_prefix: "UID:",
my_available_salary: "My available salary",
details: "Details",
bd_number: "BD number",
team_salary: "Team salary",
bd_policy_link: "BD policy",
bd_leader_history: "BD Leader history",
agency_list_link: "Agency list",
bd_list_link: "BD List",
invite_become_agent: "Invite to become agent",
invite_become_bd: "Invite to become BD",
bd_list_preview: "BD List",
bd_list_preview_subtitle: "Current settlement period",
more: "More",
total_income: "Total income",
previous_period_income: "Previous period income",
salary: "Salary",
agencies: "Agencies",
loading_data: "Loading...",
no_data: "No data",
in_progress: "In Progress",
pending: "Pending",
completed: "Completed",
out_of_account: "Out of account",
close: "Close",
failed_to_load: "Failed to load data",
failed_to_load_history: "Failed to load history"
},
ar: {
document_title: "مركز قائد BD",
page_label: "مركز قائد BD",
back: "رجوع",
title: "مركز قائد BD",
language_button_aria: "تغيير اللغة",
role: "قائد BD",
uid_prefix: "UID:",
my_available_salary: "راتبي المتاح",
details: "التفاصيل",
bd_number: "عدد BD",
team_salary: "راتب الفريق",
bd_policy_link: "سياسة BD",
bd_leader_history: "سجل قائد BD",
agency_list_link: "قائمة الوكالات",
bd_list_link: "قائمة BD",
invite_become_agent: "دعوة ليصبح وكيلا",
invite_become_bd: "دعوة ليصبح BD",
bd_list_preview: "قائمة BD",
bd_list_preview_subtitle: "فترة التسوية الحالية",
more: "المزيد",
total_income: "إجمالي الدخل",
previous_period_income: "دخل الفترة السابقة",
salary: "الراتب",
agencies: "الوكالات",
loading_data: "جار التحميل...",
no_data: "لا توجد بيانات",
in_progress: "قيد التنفيذ",
pending: "قيد الانتظار",
completed: "مكتمل",
out_of_account: "خارج الحساب",
close: "إغلاق",
failed_to_load: "فشل تحميل البيانات",
failed_to_load_history: "فشل تحميل السجل"
},
tr: {
document_title: "BD Leader Merkezi",
page_label: "BD Leader Merkezi",
back: "Geri",
title: "BD Leader Merkezi",
language_button_aria: "Dili değiştir",
role: "BD Leader",
uid_prefix: "UID:",
my_available_salary: "Kullanılabilir maaşım",
details: "Detaylar",
bd_number: "BD sayısı",
team_salary: "Takım maaşı",
bd_policy_link: "BD politikası",
bd_leader_history: "BD Leader geçmişi",
agency_list_link: "Ajans listesi",
bd_list_link: "BD Listesi",
invite_become_agent: "Ajans olmaya davet et",
invite_become_bd: "BD olmaya davet et",
bd_list_preview: "BD Listesi",
bd_list_preview_subtitle: "Mevcut ödeme dönemi",
more: "Daha fazla",
total_income: "Toplam gelir",
previous_period_income: "Önceki dönem geliri",
salary: "Maaş",
agencies: "Ajanslar",
loading_data: "Yükleniyor...",
no_data: "Veri yok",
in_progress: "Devam ediyor",
pending: "Beklemede",
completed: "Tamamlandı",
out_of_account: "Hesap dışı",
close: "Kapat",
failed_to_load: "Veri yüklenemedi",
failed_to_load_history: "Geçmiş yüklenemedi"
},
id: {
document_title: "Pusat BD Leader",
page_label: "Pusat BD Leader",
back: "Kembali",
title: "Pusat BD Leader",
language_button_aria: "Ubah bahasa",
role: "BD Leader",
uid_prefix: "UID:",
my_available_salary: "Gaji tersedia",
details: "Detail",
bd_number: "Jumlah BD",
team_salary: "Gaji tim",
bd_policy_link: "Kebijakan BD",
bd_leader_history: "Riwayat BD Leader",
agency_list_link: "Daftar agensi",
bd_list_link: "Daftar BD",
invite_become_agent: "Undang menjadi agen",
invite_become_bd: "Undang menjadi BD",
bd_list_preview: "Daftar BD",
bd_list_preview_subtitle: "Periode settlement saat ini",
more: "Lainnya",
total_income: "Total pendapatan",
previous_period_income: "Pendapatan periode sebelumnya",
salary: "Gaji",
agencies: "Agensi",
loading_data: "Memuat...",
no_data: "Tidak ada data",
in_progress: "Sedang diproses",
pending: "Menunggu",
completed: "Selesai",
out_of_account: "Di luar akun",
close: "Tutup",
failed_to_load: "Gagal memuat data",
failed_to_load_history: "Gagal memuat riwayat"
}
};
const params = readURLParams();
let currentLang = normalizeLanguage(params.get("lang"));
let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en;
const state = {
profile: null,
identity: null,
teamEntry: null,
balanceTotal: {},
bill: {},
history: null,
historyMore: null
};
function readURLParams() {
const result = new URLSearchParams(window.location.search);
const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "";
const hashParams = new URLSearchParams(hashQuery);
hashParams.forEach((value, key) => {
if (!result.has(key)) result.set(key, value);
});
return result;
}
function readRawParam(name) {
const queries = [window.location.search.replace(/^\?/, "")];
if (window.location.hash.includes("?")) queries.push(window.location.hash.split("?")[1]);
for (const query of queries) {
for (const part of query.split("&")) {
if (!part) continue;
const equalIndex = part.indexOf("=");
const rawKey = equalIndex >= 0 ? part.slice(0, equalIndex) : part;
const rawValue = equalIndex >= 0 ? part.slice(equalIndex + 1) : "";
let decodedKey = "";
try {
decodedKey = decodeURIComponent(rawKey);
} catch (error) {
continue;
}
if (decodedKey !== name) continue;
try {
return decodeURIComponent(rawValue.replace(/\+/g, "%2B"));
} catch (error) {
return rawValue;
}
}
}
return "";
}
function normalizeLanguage(lang) {
const value = String(lang || "en").toLowerCase();
if (value.startsWith("ar")) return "ar";
if (value.startsWith("tr")) return "tr";
if (value.startsWith("id")) return "id";
return supportedLanguages.includes(value) ? value : "en";
}
function message(key, fallback = "") {
return currentMessages[key] || fallbackMessages.en[key] || fallback || key;
}
function renderMessages(messages) {
document.querySelectorAll("[data-i18n]").forEach((node) => {
const value = messages[node.dataset.i18n] || fallbackMessages.en[node.dataset.i18n];
if (value) node.textContent = value;
});
document.querySelectorAll("[data-i18n-aria]").forEach((node) => {
const value = messages[node.dataset.i18nAria] || fallbackMessages.en[node.dataset.i18nAria];
if (value) node.setAttribute("aria-label", value);
});
}
function apiBaseURL() {
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
if (override) return String(override).replace(/\/+$/, "");
return API_BASE_URL.replace(/\/+$/, "");
}
function normalizeAuthorization(token) {
const value = String(token || "").trim();
if (!value) return "";
return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`;
}
function authHeadersFromToken(token) {
return {
Authorization: normalizeAuthorization(token),
"req-lang": currentLang,
"req-client": "H5",
"req-version": "V2",
"req-zone": Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai",
"req-app-intel": "version=1.0.0;build=1;model=H5;sysVersion=Web;channel=Web",
"req-sys-origin": "origin=ATYOU;originChild=ATYOU"
};
}
async function requestJSON(path) {
const token = readRawParam("token");
if (!token) throw new Error("Missing token");
const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), {
cache: "no-store",
headers: authHeadersFromToken(token)
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
error.response = data;
throw error;
}
return data.body ?? data.data ?? null;
}
function setLoading(isLoading) {
const root = document.querySelector(".bd-leader-center");
const mask = document.querySelector("#loadingMask");
if (root) root.dataset.loading = isLoading ? "true" : "false";
if (mask) mask.hidden = !isLoading;
}
function updateModalLock() {
const hasOpen = Array.from(document.querySelectorAll(".history-modal, .details-modal")).some((modal) => !modal.hidden);
document.body.classList.toggle("modal-open", hasOpen);
}
function setModalOpen(selector, open) {
const modal = document.querySelector(selector);
if (!modal) return;
modal.hidden = !open;
updateModalLock();
}
function showToast(text) {
const toast = document.querySelector("#toast");
if (!toast) return;
toast.textContent = text || "";
toast.hidden = !text;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.hidden = true;
}, 2500);
}
function formatMoney(value) {
const number = Number(value);
if (!Number.isFinite(number)) return "0.00";
return number.toFixed(2);
}
function formatCount(value) {
if (value === null || value === undefined || value === "") return "0";
const number = Number(value);
if (Number.isFinite(number) && Number.isInteger(number)) return String(number);
if (Number.isFinite(number)) return String(Number(number.toFixed(2)));
return String(value);
}
function pickSpecialAccount(profile) {
return profile?.ownSpecialId?.account || profile?.specialId?.account || profile?.specialAccount || profile?.shortAccount || profile?.shortId || "";
}
function pickProfileName(profile) {
return profile?.userNickname || profile?.name || "-";
}
function pickProfileAccount(profile) {
return pickSpecialAccount(profile) || profile?.account || profile?.id || "-";
}
function renderAvatar(profile) {
const image = document.querySelector("#profileAvatar");
const fallback = document.querySelector("#profileAvatarFallback");
if (!image || !fallback) return;
const name = pickProfileName(profile);
const avatar = profile?.userAvatar || profile?.avatar || "";
fallback.textContent = String(name || "B").slice(0, 1).toUpperCase() || "B";
if (!avatar) {
image.hidden = true;
image.removeAttribute("src");
fallback.hidden = false;
return;
}
image.alt = name;
fallback.hidden = true;
image.hidden = false;
image.onerror = () => {
image.hidden = true;
image.removeAttribute("src");
fallback.hidden = false;
};
image.src = avatar;
}
function renderProfile(profile = state.profile) {
const name = pickProfileName(profile);
const account = pickProfileAccount(profile);
const nameNode = document.querySelector("#profileName");
const uidNode = document.querySelector("#profileUid");
if (nameNode) nameNode.textContent = name;
if (uidNode) uidNode.textContent = `${message("uid_prefix")} ${account}`.trim();
renderAvatar(profile);
}
function renderBalance() {
const balance = state.balanceTotal?.availableBalance ?? state.balanceTotal?.balance ?? 0;
const node = document.querySelector("#availableBalance");
if (node) node.textContent = formatMoney(balance);
}
function renderBill() {
const bill = state.bill || {};
const members = Array.isArray(bill.memberBillList) ? bill.memberBillList : [];
const billTitle = document.querySelector("#billTitle");
const bdNumber = document.querySelector("#bdNumber");
const teamSalary = document.querySelector("#teamSalary");
if (billTitle) billTitle.textContent = bill.billTitle || "-";
if (bdNumber) bdNumber.textContent = formatCount(bill.memberCount ?? bill.bdNumber ?? members.length);
if (teamSalary) teamSalary.textContent = formatMoney(bill.totalSalary ?? bill.teamSalary ?? 0);
renderMembers(members);
}
function createFallbackAvatar(name) {
const span = document.createElement("span");
span.className = "member-fallback";
span.textContent = String(name || "B").slice(0, 1).toUpperCase() || "B";
return span;
}
function createMemberRow(item) {
const name = item?.agentName || item?.bdUserName || "-";
const account = item?.agentAccount || item?.account || "-";
const row = document.createElement("article");
row.className = "member-row";
const avatar = document.createElement("div");
avatar.className = "member-avatar";
const avatarUrl = item?.agentAvatar || item?.bdUserAvatar || "";
if (avatarUrl) {
const image = document.createElement("img");
image.src = avatarUrl;
image.alt = "";
image.addEventListener("error", () => {
avatar.replaceChildren(createFallbackAvatar(name));
});
avatar.appendChild(image);
} else {
avatar.appendChild(createFallbackAvatar(name));
}
const main = document.createElement("div");
main.className = "member-main";
const nameNode = document.createElement("div");
nameNode.className = "member-name";
nameNode.textContent = name;
const metaNode = document.createElement("div");
metaNode.className = "member-meta";
metaNode.textContent = `${message("uid_prefix")} ${account}`.trim();
main.append(nameNode, metaNode);
const side = document.createElement("div");
side.className = "member-side";
const salary = document.createElement("div");
salary.textContent = `${message("team_salary")}: $${formatMoney(item?.teamSalaryAmount || 0)}`;
const agencies = document.createElement("div");
agencies.textContent = `${message("agencies")}: ${formatCount(item?.teamMemberCount || item?.agencyCount || 0)}`;
side.append(salary, agencies);
row.append(avatar, main, side);
return row;
}
function replaceChildren(node, children) {
while (node.firstChild) node.removeChild(node.firstChild);
children.forEach((child) => node.appendChild(child));
}
function renderMembers(members) {
const list = document.querySelector("#bdMemberList");
if (!list) return;
const preview = members.slice(0, 5);
if (!preview.length) {
const empty = document.createElement("div");
empty.className = "empty-state";
empty.textContent = message("no_data");
replaceChildren(list, [empty]);
return;
}
replaceChildren(list, preview.map(createMemberRow));
}
function normalizeStatus(status) {
if (status === "UNPAID" || status === "In Progress" || status === message("in_progress")) return message("in_progress");
if (status === "Pending" || status === message("pending")) return message("pending");
if (status === "SETTLED" || status === "PAY_OUT" || status === "Completed" || status === message("completed")) return message("completed");
if (status === "HANG_UP" || status === "Out of account" || status === "Out Of Account" || status === message("out_of_account")) return message("out_of_account");
return status || "";
}
function statusClass(status) {
const value = normalizeStatus(status);
if (value === message("completed")) return "is-completed";
if (value === message("pending")) return "is-pending";
if (value === message("out_of_account")) return "is-out";
return "is-progress";
}
function isCompletedStatus(status) {
return normalizeStatus(status) === message("completed");
}
function createHistoryCell(label, value) {
const cell = document.createElement("div");
cell.className = "history-cell";
const labelNode = document.createElement("span");
labelNode.textContent = label;
const valueNode = document.createElement("strong");
valueNode.textContent = value;
cell.append(labelNode, valueNode);
return cell;
}
function createHistoryRow(item) {
const row = document.createElement("article");
row.className = `history-row ${statusClass(item?.statusText)}`;
const status = document.createElement("div");
status.className = "status-tag";
status.textContent = normalizeStatus(item?.statusText);
const title = document.createElement("div");
title.className = "history-title";
title.textContent = item?.billTitle || "-";
const grid = document.createElement("div");
grid.className = "history-grid";
grid.append(
createHistoryCell(message("bd_number"), formatCount(item?.bdNumber || 0)),
createHistoryCell(message("team_salary"), `$${formatMoney(item?.teamSalaryAmount || 0)}`)
);
row.append(status, title, grid);
if (isCompletedStatus(item?.statusText)) {
const more = document.createElement("button");
more.className = "more-button";
more.type = "button";
more.textContent = message("more");
more.addEventListener("click", () => showHistoryMore(item?.billBelong));
row.appendChild(more);
}
return row;
}
function renderHistory() {
const history = state.history || {};
const total = document.querySelector("#historyTotalIncome");
const previous = document.querySelector("#historyPreviousIncome");
const list = document.querySelector("#historyList");
if (total) total.textContent = `$${formatMoney(history.totalIncome || 0)}`;
if (previous) previous.textContent = `$${formatMoney(history.previousPeriodIncome || 0)}`;
if (!list) return;
const items = Array.isArray(history.incomeDetails) ? history.incomeDetails : [];
if (!items.length) {
const empty = document.createElement("div");
empty.className = "empty-state";
empty.textContent = message("no_data");
replaceChildren(list, [empty]);
return;
}
replaceChildren(list, items.map(createHistoryRow));
}
function normalizeBillBelong(value) {
const text = String(value || "").trim();
if (!text) return "";
const iso = text.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?/);
if (iso) return `${iso[1]}-${iso[2]}-${iso[3]}T${iso[4] || "00"}:${iso[5] || "00"}:${iso[6] || "00"}`;
const compact = text.match(/^(\d{4})(\d{2})(\d{2})(?:(\d{2})(\d{2})(\d{2})?)?$/);
if (compact) return `${compact[1]}-${compact[2]}-${compact[3]}T${compact[4] || "00"}:${compact[5] || "00"}:${compact[6] || "00"}`;
return text;
}
function historyMorePath(billBelong) {
const normalized = normalizeBillBelong(billBelong);
return normalized ? `/team/bd/leader/history/more?billBelong=${encodeURIComponent(normalized)}` : "";
}
function createDetailsCell(label, value) {
const cell = document.createElement("div");
cell.className = "details-cell";
const labelNode = document.createElement("span");
labelNode.textContent = label;
const valueNode = document.createElement("strong");
valueNode.textContent = value;
cell.append(labelNode, valueNode);
return cell;
}
function renderDetails() {
const data = state.historyMore || {};
const title = document.querySelector("#detailsTitle");
const summary = document.querySelector("#detailsSummary");
const list = document.querySelector("#detailsList");
if (title) title.textContent = data.period || "-";
if (summary) {
replaceChildren(summary, [
createDetailsCell(message("team_salary"), `$${formatMoney(data.teamSalary || 0)}`),
createDetailsCell(message("bd_number"), formatCount(data.bdNumber || 0))
]);
}
if (!list) return;
const members = Array.isArray(data.memberDetails) ? data.memberDetails : [];
if (!members.length) {
const empty = document.createElement("div");
empty.className = "empty-state";
empty.textContent = message("no_data");
replaceChildren(list, [empty]);
return;
}
replaceChildren(list, members.map((item) => {
const row = createMemberRow({
agentName: item.bdUserName,
agentAvatar: item.bdUserAvatar,
agentAccount: item.account,
teamSalaryAmount: item.salary,
teamMemberCount: item.agencyCount
});
row.classList.add("details-row");
return row;
}));
}
async function fetchProfile() {
state.profile = await requestJSON("/team/member/profile");
renderProfile();
}
async function fetchIdentity() {
state.identity = await requestJSON("/app/h5/identity");
}
async function fetchTeamEntry() {
state.teamEntry = await requestJSON("/team/entry");
}
async function fetchBalanceTotal() {
state.balanceTotal = await requestJSON("/wallet/salary-account/balance/total") || {};
renderBalance();
}
async function fetchBDLeaderBill() {
state.bill = await requestJSON("/team/bd/member-bill/list?type=BDLEADER") || {};
renderBill();
}
async function fetchHistory() {
const history = await requestJSON("/team/bd/leader/history") || {};
const currentHistory = state.bill?.bdHistoryCO;
const details = Array.isArray(history.incomeDetails) ? [...history.incomeDetails] : [];
if (currentHistory) details.unshift(currentHistory);
state.history = { ...history, incomeDetails: details };
renderHistory();
}
async function showHistoryModal() {
try {
setModalOpen("#historyModal", true);
const list = document.querySelector("#historyList");
if (list) {
const loading = document.createElement("div");
loading.className = "empty-state";
loading.textContent = message("loading_data");
replaceChildren(list, [loading]);
}
await fetchHistory();
} catch (error) {
console.error("Failed to load BD leader history:", error);
showToast(error.response?.errorMsg || error.message || message("failed_to_load_history"));
renderHistory();
}
}
async function showHistoryMore(billBelong) {
const path = historyMorePath(billBelong);
if (!path) return;
try {
state.historyMore = await requestJSON(path) || {};
renderDetails();
setModalOpen("#detailsModal", true);
} catch (error) {
console.error("Failed to load BD leader history more:", error);
showToast(error.response?.errorMsg || error.message || message("failed_to_load_history"));
}
}
function buildRouteURL(route) {
const url = new URL(route, window.location.origin);
const token = readRawParam("token");
if (token) url.searchParams.set("token", token);
if (currentLang) url.searchParams.set("lang", currentLang);
return `${url.pathname}${url.search}${url.hash}`;
}
function navigateTo(route) {
window.location.href = buildRouteURL(route);
}
function closePage() {
try {
if (window.app && typeof window.app.closePage === "function") {
window.app.closePage();
return;
}
if (window.FlutterPageControl && typeof window.FlutterPageControl.postMessage === "function") {
window.FlutterPageControl.postMessage("close_page");
return;
}
if (window.history.length > 1) window.history.back();
} catch (error) {
console.error("close BD leader center page failed:", error);
if (window.history.length > 1) window.history.back();
}
}
function setLanguageMenuOpen(open) {
const button = document.querySelector(".language-button");
const menu = document.querySelector(".language-menu");
if (!button || !menu) return;
menu.hidden = !open;
button.setAttribute("aria-expanded", String(open));
}
function renderLanguageControls() {
const button = document.querySelector(".language-button");
if (button) {
button.textContent = languageLabels[currentLang];
button.setAttribute("aria-label", message("language_button_aria", "Change language"));
}
document.querySelectorAll(".language-menu [data-lang]").forEach((node) => {
const active = node.dataset.lang === currentLang;
node.classList.toggle("is-active", active);
node.setAttribute("aria-pressed", String(active));
});
}
async function applyLanguage(lang) {
currentLang = normalizeLanguage(lang);
currentMessages = fallbackMessages[currentLang] || fallbackMessages.en;
document.documentElement.lang = currentLang;
document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr";
document.title = message("document_title", "BD Leader Center");
renderMessages(currentMessages);
renderLanguageControls();
renderProfile();
renderBalance();
renderBill();
renderHistory();
renderDetails();
}
function clearPayee() {
try {
sessionStorage.removeItem("payee");
localStorage.removeItem("payee");
} catch (error) {
console.debug("payee cache clear skipped:", error);
}
}
function setIdentity() {
try {
localStorage.setItem("identity", JSON.stringify({ identity: "BD_LEADER" }));
} catch (error) {
console.debug("identity cache write skipped:", error);
}
}
function bindEvents() {
document.querySelector(".back-button")?.addEventListener("click", closePage);
document.querySelector(".language-button")?.addEventListener("click", (event) => {
event.stopPropagation();
const menu = document.querySelector(".language-menu");
setLanguageMenuOpen(Boolean(menu?.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("[data-route]").forEach((node) => {
node.addEventListener("click", () => navigateTo(node.dataset.route));
});
document.querySelector("[data-action='history']")?.addEventListener("click", showHistoryModal);
document.querySelectorAll("[data-close-history]").forEach((node) => {
node.addEventListener("click", () => setModalOpen("#historyModal", false));
});
document.querySelectorAll("[data-close-details]").forEach((node) => {
node.addEventListener("click", () => setModalOpen("#detailsModal", false));
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
setModalOpen("#historyModal", false);
setModalOpen("#detailsModal", false);
}
});
}
async function initializePage() {
setLoading(true);
clearPayee();
setIdentity();
bindEvents();
renderProfile();
renderBalance();
try {
await applyLanguage(currentLang);
const tasks = [
fetchProfile(),
fetchIdentity(),
fetchTeamEntry(),
fetchBalanceTotal(),
fetchBDLeaderBill()
];
const results = await Promise.allSettled(tasks);
const rejected = results.find((item) => item.status === "rejected");
if (rejected) {
console.warn("BD leader center partial load failed:", rejected.reason);
showToast(rejected.reason?.response?.errorMsg || rejected.reason?.message || message("failed_to_load"));
}
} catch (error) {
console.error("Failed to initialize BD leader center:", error);
showToast(error.response?.errorMsg || error.message || message("failed_to_load"));
} finally {
setLoading(false);
}
}
initializePage();
})();

File diff suppressed because it is too large Load Diff