From e1abcb0b4e90f600eac6cccaaf213d3ea3672c79 Mon Sep 17 00:00:00 2001 From: 170-carry Date: Tue, 28 Apr 2026 23:22:04 +0800 Subject: [PATCH] feat: add hyapp bd leader center --- h5/hyapp/bd-leader-center/index.html | 204 ++++ .../bd-leader-center/original-vue-logic.md | 255 +++++ h5/hyapp/bd-leader-center/script.js | 806 +++++++++++++ h5/hyapp/bd-leader-center/style.css | 1009 +++++++++++++++++ 4 files changed, 2274 insertions(+) create mode 100644 h5/hyapp/bd-leader-center/index.html create mode 100644 h5/hyapp/bd-leader-center/original-vue-logic.md create mode 100644 h5/hyapp/bd-leader-center/script.js create mode 100644 h5/hyapp/bd-leader-center/style.css diff --git a/h5/hyapp/bd-leader-center/index.html b/h5/hyapp/bd-leader-center/index.html new file mode 100644 index 0000000..6c07f73 --- /dev/null +++ b/h5/hyapp/bd-leader-center/index.html @@ -0,0 +1,204 @@ + + + + + + BD Leader Center + + + +
+ + + + + + +
+
+
+ + B +
+
+
BD Leader
+
-
+
UID: -
+
+
+ +
+
+
+
My available salary
+
-
+
+ +
+
+ + 0.00 +
+
+ +
+
+
BD number
+
0
+
+ +
+
Team salary
+
+ + 0.00 +
+
+
+ + + +
+
+
+
BD List
+
Current settlement period
+
+ +
+
+
Loading...
+
+
+
+ + + + + + +
+ +
loading....
+
+ + +
+ + + diff --git a/h5/hyapp/bd-leader-center/original-vue-logic.md b/h5/hyapp/bd-leader-center/original-vue-logic.md new file mode 100644 index 0000000..0ee90d0 --- /dev/null +++ b/h5/hyapp/bd-leader-center/original-vue-logic.md @@ -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 ` + - 同时传 `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=` + +参数: +- `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=` +- 邀请:`POST /team/bd/invite-bd` + - body:`{ inviteUserId }` +- 取消邀请:`POST /team/bd/invite-bd-cancel/` + +## 邀请代理 + +入口:`/invite-agency` + +接口: +- 列表:`GET /team/bd/invite-message-own` +- 邀请:`POST /team/bd/invite-agent` + - body:`{ inviteUserId }` +- 取消邀请:`POST /team/bd/invite-agent-cancel/` + +## 其他菜单功能 + +- `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=...` +- 已保留旧页面跳转入口。 diff --git a/h5/hyapp/bd-leader-center/script.js b/h5/hyapp/bd-leader-center/script.js new file mode 100644 index 0000000..e043128 --- /dev/null +++ b/h5/hyapp/bd-leader-center/script.js @@ -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(); +})(); diff --git a/h5/hyapp/bd-leader-center/style.css b/h5/hyapp/bd-leader-center/style.css new file mode 100644 index 0000000..fd2aebb --- /dev/null +++ b/h5/hyapp/bd-leader-center/style.css @@ -0,0 +1,1009 @@ +:root { + --page-bg: #f6f7f7; + --card-bg: rgba(255, 255, 255, 0.96); + --text: #25282e; + --muted: #8f9196; + --line: #eef0f2; + --mint: #43e7d8; + --green: #25d878; + --shadow: 0 8px 22px rgba(36, 46, 60, 0.055); +} + +* { + box-sizing: border-box; +} + +html, +body { + width: 100%; + min-height: 100%; + margin: 0; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; +} + +body { + display: flex; + justify-content: center; + background: #111; + color: var(--text); + font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +button { + border: 0; + padding: 0; + background: transparent; + color: inherit; + font: inherit; +} + +[hidden] { + display: none !important; +} + +.bd-leader-center { + position: relative; + width: 100%; + max-width: 430px; + min-height: 100vh; + overflow: hidden; + background: var(--page-bg); +} + +.bd-leader-center[data-loading="true"] .title-bar, +.bd-leader-center[data-loading="true"] .content, +.bd-leader-center[data-loading="true"] .home-indicator { + visibility: hidden; +} + +.hero-bg { + position: absolute; + inset: 0 0 auto; + height: 215px; + background: + radial-gradient(circle at 72% 4%, rgba(209, 255, 248, 0.98) 0, rgba(209, 255, 248, 0.88) 23%, rgba(209, 255, 248, 0) 48%), + radial-gradient(circle at 14% 20%, rgba(185, 250, 244, 0.78) 0, rgba(185, 250, 244, 0) 35%), + linear-gradient(151deg, #ccf7ef 0%, #ebfffa 58%, rgba(242, 255, 252, 0.7) 100%); + clip-path: polygon(0 0, 100% 0, 100% 76%, 70% 47%, 40% 58%, 0 78%); +} + +.status-bar, +.title-bar, +.content, +.home-indicator { + position: relative; + z-index: 1; +} + +.status-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 47px; + padding: 18px 29px 0 38px; + color: #11161c; + direction: ltr; + font-size: 15px; + font-weight: 800; +} + +.status-icons { + display: flex; + align-items: center; + gap: 6px; +} + +.cellular { + display: flex; + align-items: flex-end; + gap: 2px; + width: 18px; + height: 12px; +} + +.cellular i { + width: 3px; + border-radius: 2px; + background: #11161c; +} + +.cellular i:nth-child(1) { height: 4px; } +.cellular i:nth-child(2) { height: 6px; } +.cellular i:nth-child(3) { height: 9px; } +.cellular i:nth-child(4) { height: 12px; } + +.wifi { + position: relative; + display: inline-flex; + width: 16px; + height: 12px; +} + +.wifi i { + position: absolute; + left: 50%; + bottom: 0; + border: 2px solid transparent; + border-top-color: #11161c; + border-radius: 50%; + transform: translateX(-50%); +} + +.wifi i:nth-child(1) { width: 16px; height: 16px; } +.wifi i:nth-child(2) { width: 11px; height: 11px; bottom: 2px; } +.wifi i:nth-child(3) { width: 5px; height: 5px; bottom: 4px; background: #11161c; border: 0; } + +.battery { + position: relative; + width: 24px; + height: 12px; + border: 2px solid #11161c; + border-radius: 2px; +} + +.battery::after { + content: ""; + position: absolute; + right: -4px; + top: 3px; + width: 2px; + height: 4px; + border-radius: 0 2px 2px 0; + background: #11161c; +} + +.battery i { + display: block; + width: 16px; + height: 100%; + background: #11161c; +} + +.title-bar { + display: grid; + grid-template-columns: 44px minmax(0, 1fr) 56px; + align-items: center; + min-height: 56px; + padding: 8px 14px 6px; +} + +.back-button, +.language-button { + display: flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + border-radius: 50%; + color: #2c3036; +} + +.back-button svg { + width: 30px; + height: 30px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.4; +} + +.title-bar h1 { + min-width: 0; + margin: 0; + overflow: hidden; + color: #24282e; + font-size: 19px; + font-weight: 900; + line-height: 1.2; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.language-switcher { + position: relative; + display: flex; + justify-content: flex-end; +} + +.language-button { + width: auto; + min-width: 42px; + height: 32px; + padding: 0 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.55); + font-size: 12px; + font-weight: 900; +} + +.language-menu { + position: absolute; + top: 38px; + right: 0; + z-index: 5; + display: grid; + gap: 4px; + min-width: 74px; + padding: 8px; + border: 1px solid rgba(34, 196, 180, 0.18); + border-radius: 8px; + background: #fff; + box-shadow: 0 12px 24px rgba(22, 31, 42, 0.12); +} + +.language-menu button { + min-height: 30px; + border-radius: 6px; + font-size: 12px; + font-weight: 850; +} + +.language-menu button.is-active { + background: #e7fbf8; + color: #0aa397; +} + +.content { + display: grid; + gap: 12px; + padding: 8px 14px 32px; +} + +.card { + border-radius: 8px; + background: var(--card-bg); + box-shadow: var(--shadow); +} + +.profile-card { + display: flex; + align-items: center; + min-height: 104px; + padding: 16px; +} + +.avatar-shell, +.member-avatar { + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + border-radius: 50%; + background: linear-gradient(145deg, #d9fff5, #43e7d8); +} + +.avatar-shell { + width: 70px; + height: 70px; + flex: 0 0 70px; +} + +.avatar-image, +.member-avatar img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-fallback, +.member-fallback { + color: #0b5f56; + font-size: 26px; + font-weight: 950; +} + +.profile-copy { + min-width: 0; + flex: 1; + margin-left: 16px; +} + +.role-pill { + display: inline-flex; + align-items: center; + min-height: 22px; + padding: 0 9px; + border-radius: 999px; + background: #e7fbf8; + color: #0b958a; + font-size: 11px; + font-weight: 900; +} + +.name { + margin-top: 7px; + overflow: hidden; + color: #25282e; + font-size: 20px; + font-weight: 900; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; +} + +.meta { + margin-top: 6px; + overflow: hidden; + color: var(--muted); + font-size: 13px; + font-weight: 750; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.salary-card { + min-height: 132px; + padding: 18px 20px; +} + +.salary-head, +.section-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.salary-label, +.section-title { + color: #24282e; + font-size: 16px; + font-weight: 900; + line-height: 1.2; +} + +.salary-period, +.section-subtitle { + margin-top: 6px; + color: #93959a; + font-size: 12px; + font-weight: 800; + line-height: 1.2; +} + +.text-link { + flex: 0 0 auto; + color: #10bcb0; + font-size: 13px; + font-weight: 900; +} + +.salary-value { + display: flex; + align-items: baseline; + gap: 6px; + margin-top: 16px; + color: #1f242b; +} + +.salary-value span, +.metric-money span { + color: #21b66a; + font-weight: 950; +} + +.salary-value strong { + min-width: 0; + overflow-wrap: anywhere; + font-size: 31px; + font-weight: 950; + line-height: 1.08; +} + +.metrics-card { + display: grid; + grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr); + min-height: 110px; + padding: 18px 16px; +} + +.metric { + display: flex; + min-width: 0; + flex-direction: column; + justify-content: center; +} + +.metric:first-child { + padding-right: 16px; +} + +.metric:last-child { + padding-left: 16px; +} + +.label { + color: var(--muted); + font-size: 13px; + font-weight: 800; + line-height: 1.2; +} + +.value { + margin-top: 8px; + overflow-wrap: anywhere; + color: #24282e; + font-size: 27px; + font-weight: 950; + line-height: 1.05; +} + +.metric-money { + display: flex; + align-items: baseline; + gap: 4px; + font-size: 24px; +} + +.vertical-line { + width: 1px; + background: var(--line); +} + +.menu-card { + overflow: hidden; +} + +.menu-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + width: 100%; + min-height: 58px; + padding: 0 16px; + border-bottom: 1px solid var(--line); +} + +.menu-row:last-child { + border-bottom: 0; +} + +.menu-left { + display: flex; + align-items: center; + min-width: 0; + gap: 14px; +} + +.menu-icon { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + flex: 0 0 30px; + border-radius: 8px; + background: #e9fbf4; + color: #1f9d66; +} + +.menu-icon svg { + width: 22px; + height: 22px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.1; +} + +.menu-left span:last-child { + min-width: 0; + overflow: hidden; + color: #464951; + font-size: 15px; + font-weight: 850; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chevron { + color: #8b8d92; + font-size: 24px; + line-height: 1; +} + +.member-card { + padding: 16px; +} + +.member-list { + display: grid; + gap: 10px; + margin-top: 14px; +} + +.member-row, +.history-row, +.details-row { + display: grid; + gap: 10px; + padding: 12px; + border: 1px solid #eef0f2; + border-radius: 8px; + background: #fbfcfc; +} + +.member-row { + grid-template-columns: 44px minmax(0, 1fr) auto; + align-items: center; +} + +.member-avatar { + width: 44px; + height: 44px; +} + +.member-fallback { + font-size: 18px; +} + +.member-main { + min-width: 0; +} + +.member-name { + overflow: hidden; + color: #25282e; + font-size: 15px; + font-weight: 900; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.member-meta { + margin-top: 4px; + overflow: hidden; + color: #8b8d92; + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.member-side { + display: grid; + gap: 3px; + color: #555b64; + font-size: 12px; + font-weight: 850; + line-height: 1.2; + text-align: right; +} + +.empty-state { + display: flex; + align-items: center; + justify-content: center; + min-height: 80px; + border: 1px dashed #e5e8ea; + border-radius: 8px; + color: #8b8d92; + font-size: 14px; + font-weight: 850; + text-align: center; +} + +.history-modal[hidden], +.details-modal[hidden] { + display: none; +} + +.history-modal, +.details-modal { + position: fixed; + z-index: 24; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 18px; +} + +.history-backdrop { + position: absolute; + inset: 0; + background: rgba(15, 21, 28, 0.38); +} + +.history-dialog { + position: relative; + display: flex; + flex-direction: column; + width: min(394px, 100%); + max-height: 78vh; + max-height: 78dvh; + overflow: hidden; + border-radius: 14px; + background: #fff; + box-shadow: 0 18px 44px rgba(23, 31, 42, 0.2); +} + +.history-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 58px; + padding: 16px 18px 12px; + border-bottom: 1px solid #eef0f2; +} + +.history-head h2 { + min-width: 0; + margin: 0; + overflow: hidden; + color: #24282e; + font-size: 18px; + font-weight: 950; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.history-close { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 50%; + background: #f4f6f7; + color: #50545b; + font-size: 25px; + font-weight: 500; + line-height: 1; +} + +.history-summary { + display: grid; + gap: 8px; + padding: 13px 18px; + border-bottom: 1px solid #f0f2f3; +} + +.history-summary.compact { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.history-summary span { + display: block; + color: #8b8d92; + font-size: 12px; + font-weight: 800; + line-height: 1.2; +} + +.history-summary strong { + display: block; + margin-top: 4px; + overflow-wrap: anywhere; + color: #24282e; + font-size: 18px; + font-weight: 950; + line-height: 1.2; +} + +.history-list { + display: grid; + flex: 1 1 auto; + gap: 10px; + min-height: 0; + overflow-y: auto; + padding: 14px 18px max(18px, env(safe-area-inset-bottom)); + -webkit-overflow-scrolling: touch; +} + +.history-row { + position: relative; + padding-top: 35px; +} + +.status-tag { + position: absolute; + top: 0; + left: 0; + min-width: 92px; + padding: 4px 9px; + border-radius: 8px 0 8px 0; + background: #3d73ff; + color: #fff; + font-size: 12px; + font-weight: 900; + text-align: center; +} + +.history-row.is-completed .status-tag { + background: #25c05c; +} + +.history-row.is-pending .status-tag { + background: #f0a92e; +} + +.history-row.is-out .status-tag { + background: #9ea1a7; +} + +.history-title { + color: #24282e; + font-size: 15px; + font-weight: 950; + line-height: 1.2; +} + +.history-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.history-cell span, +.details-cell span { + display: block; + color: #96989d; + font-size: 11px; + font-weight: 800; + line-height: 1.2; +} + +.history-cell strong, +.details-cell strong { + display: block; + margin-top: 4px; + overflow-wrap: anywhere; + color: #292d33; + font-size: 14px; + font-weight: 950; + line-height: 1.2; +} + +.more-button { + justify-self: end; + color: #10bcb0; + font-size: 13px; + font-weight: 900; +} + +.details-row { + grid-template-columns: 44px minmax(0, 1fr); + align-items: center; +} + +.loading-mask { + position: absolute; + z-index: 30; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 18px; + min-height: 100vh; + background: + radial-gradient(circle at 50% 35%, rgba(230, 255, 243, 0.98), rgba(246, 247, 247, 0.98) 44%, rgba(246, 247, 247, 0.96) 100%); +} + +.runner-stick { + position: relative; + width: 92px; + height: 82px; + color: #1ecf70; + filter: drop-shadow(0 8px 14px rgba(30, 207, 112, 0.22)); + animation: runner-bob 0.42s ease-in-out infinite; +} + +.runner-head, +.runner-body, +.runner-arm, +.runner-leg, +.runner-ground { + position: absolute; + display: block; +} + +.runner-head { + top: 5px; + left: 39px; + width: 18px; + height: 18px; + border: 4px solid currentColor; + border-radius: 50%; +} + +.runner-body { + top: 26px; + left: 47px; + width: 5px; + height: 28px; + border-radius: 999px; + background: currentColor; + transform-origin: 50% 3px; + animation: runner-body 0.42s ease-in-out infinite; +} + +.runner-arm, +.runner-leg { + left: 47px; + width: 5px; + border-radius: 999px; + background: currentColor; + transform-origin: 50% 2px; +} + +.runner-arm { + top: 31px; + height: 27px; +} + +.runner-arm-front { animation: runner-arm-front 0.42s ease-in-out infinite; } +.runner-arm-back { animation: runner-arm-back 0.42s ease-in-out infinite; } + +.runner-leg { + top: 51px; + height: 33px; +} + +.runner-leg-front { animation: runner-leg-front 0.42s ease-in-out infinite; } +.runner-leg-back { animation: runner-leg-back 0.42s ease-in-out infinite; } + +.runner-ground { + right: 6px; + bottom: 2px; + width: 70px; + height: 4px; + overflow: hidden; + border-radius: 999px; + background: rgba(30, 207, 112, 0.18); +} + +.runner-ground::before, +.runner-ground::after { + content: ""; + position: absolute; + top: 0; + width: 24px; + height: 4px; + border-radius: 999px; + background: currentColor; + animation: runner-ground 0.6s linear infinite; +} + +.runner-ground::after { + animation-delay: 0.3s; +} + +.loading-copy { + color: #139a55; + font-size: 15px; + font-weight: 900; + letter-spacing: 0; +} + +.toast { + position: fixed; + left: 50%; + bottom: calc(28px + env(safe-area-inset-bottom)); + z-index: 50; + max-width: 86vw; + transform: translateX(-50%); + padding: 10px 14px; + border-radius: 8px; + color: #fff; + background: rgba(0, 0, 0, 0.75); + font-size: 13px; + font-weight: 700; + text-align: center; +} + +.home-indicator { + position: absolute; + left: 50%; + bottom: 8px; + width: 154px; + height: 5px; + border-radius: 999px; + background: #050506; + transform: translateX(-50%); +} + +.modal-open { + overflow: hidden; +} + +[dir="rtl"] .content { + direction: rtl; +} + +[dir="rtl"] .profile-copy { + margin-right: 16px; + margin-left: 0; + text-align: right; +} + +[dir="rtl"] .member-side { + text-align: left; +} + +[dir="rtl"] .status-tag { + right: 0; + left: auto; + border-radius: 0 8px 0 8px; +} + +[dir="rtl"] .language-menu { + right: auto; + left: 0; +} + +@keyframes runner-bob { + 0%, + 100% { transform: translateY(0); } + 50% { transform: translateY(-5px); } +} + +@keyframes runner-body { + 0%, + 100% { transform: rotate(-7deg); } + 50% { transform: rotate(7deg); } +} + +@keyframes runner-arm-front { + 0%, + 100% { transform: rotate(62deg); } + 50% { transform: rotate(-58deg); } +} + +@keyframes runner-arm-back { + 0%, + 100% { transform: rotate(-58deg); } + 50% { transform: rotate(62deg); } +} + +@keyframes runner-leg-front { + 0%, + 100% { transform: rotate(-58deg); } + 50% { transform: rotate(54deg); } +} + +@keyframes runner-leg-back { + 0%, + 100% { transform: rotate(54deg); } + 50% { transform: rotate(-58deg); } +} + +@keyframes runner-ground { + 0% { transform: translateX(78px); } + 100% { transform: translateX(-30px); } +} + +@media (max-width: 360px) { + .status-bar { + padding-right: 22px; + padding-left: 30px; + } + + .content { + padding-right: 12px; + padding-left: 12px; + } + + .member-row { + grid-template-columns: 40px minmax(0, 1fr); + } + + .member-side { + grid-column: 2; + text-align: left; + } +}